Skip to content

Commit 20efe16

Browse files
authored
Add reanalyze-server with transparent delegation (#8127)
* Add reanalyze-server with transparent delegation - Implement reanalyze-server subcommand that maintains reactive state - Add reanalyze-server-request client for IPC via Unix socket - **Transparent delegation**: Regular 'reanalyze' calls automatically use the server if running on default socket (/tmp/rescript-reanalyze.sock) - Works with unmodified VS Code extension - just start the server manually - Create comprehensive test harness with clean warmup/benchmark phases - Color-coded logging: [BUILD], [SERVER], [REACTIVE], [STANDALONE], [EDIT] - Test scenarios: add dead code (+1 issue), make dead code live (-1 issue) - Results: 2.5x speedup (small project), 10x+ speedup (benchmark) Usage: # Start server (once, in background) rescript-editor-analysis reanalyze-server --cwd /path/to/project -- -config -ci -json # Now regular reanalyze calls automatically use the server: rescript-editor-analysis reanalyze -config -ci -json Signed-Off-By: Cristiano Calcagno <[email protected]> * reformat * Add detailed request stats to reanalyze-server - Log request number, timing, issues, dead/live counts, files processed/cached - Pass file_stats through runAnalysis to capture processing statistics - Use locally created mutable stats record instead of global state - Stats show files processed (new or modified) vs cached (unchanged) * reanalyze-server: project-root socket + cleanup - Default socket lives in project root and is discovered by walking up to rescript.json/bsconfig.json - Use relative socket name to avoid macOS unix socket path length limits - Unlink socket on exit and on common termination signals - Update reanalyze README with VS Code (-json) usage * Extract reanalyze server into dedicated module Move server/IPC/socket-path logic into ReanalyzeServer.ml and keep Reanalyze.ml focused on analysis. * reanalyze: add reactive server under rescript-tools - reanalyze-server runs editor-mode (-json) with no args - reanalyze delegates to server only for -json - update reanalyze tests/harness to use rescript-tools Signed-off-by: Cristiano Calcagno <[email protected]> * changelog: mention reanalyze-server Signed-off-by: Cristiano Calcagno <[email protected]> * reanalyze-server: compact heap after each request, simplify mem output - Run Gc.compact() after every request to keep memory stable (~257MB) - Simplify server output to just show live heap: 'mem: 257.0MB' - Add opt-out via RESCRIPT_REANALYZE_SERVER_SKIP_COMPACT=1 env var * Reanalyze: support rewatch monorepos via root .sourcedirs.json cmt_scan - Extend rewatch to emit version:2 .sourcedirs.json with explicit cmt_scan (build roots + scan dirs) for root-level monorepo builds. - Update reanalyze (editor invocation: rescript-tools reanalyze -json) to prefer cmt_scan when present, with legacy single-project fallback. - Ensure deterministic output ordering by sorting .cmt/.cmti entries in the cmt_scan path. Tests: make test-analysis Signed-off-by: Cristiano Calcagno <[email protected]> --------- Signed-off-by: Cristiano Calcagno <[email protected]> Signed-off-by: Cristiano Calcagno <[email protected]>
1 parent 10edf2c commit 20efe16

File tree

20 files changed

+1785
-102
lines changed

20 files changed

+1785
-102
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
- Add support for Set, Map, WeakSet and WeakMap to `@unboxed`. https://github.com/rescript-lang/rescript/pull/8009
2222
- Reanalyze: add reactive incremental analysis (`-reactive`, `-runs`, `-churn`) and Mermaid pipeline dumping (`-mermaid`). https://github.com/rescript-lang/rescript/pull/8092
2323

24+
- Reanalyze: add `reanalyze-server` (long-lived server) with transparent delegation for `rescript-tools reanalyze -json`. https://github.com/rescript-lang/rescript/pull/8127
25+
2426
#### :bug: Bug fix
2527

2628
- Fix rewatch swallowing parse warnings (%todo). https://github.com/rescript-lang/rescript/pull/8135

analysis/bin/main.ml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -191,13 +191,6 @@ let main () =
191191
in
192192
Printf.printf "\"%s\"" res
193193
| [_; "diagnosticSyntax"; path] -> Commands.diagnosticSyntax ~path
194-
| _ :: "reanalyze" :: _ ->
195-
let len = Array.length Sys.argv in
196-
for i = 1 to len - 2 do
197-
Sys.argv.(i) <- Sys.argv.(i + 1)
198-
done;
199-
Sys.argv.(len - 1) <- "";
200-
Reanalyze.cli ()
201194
| [_; "references"; path; line; col] ->
202195
Commands.references ~path
203196
~pos:(int_of_string line, int_of_string col)

analysis/reanalyze/README.md

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ Dead code analysis and other experimental analyses for ReScript.
1212

1313
```bash
1414
# Run DCE analysis on current project (reads rescript.json)
15-
rescript-editor-analysis reanalyze -config
15+
rescript-tools reanalyze -config
1616

1717
# Run DCE analysis on specific CMT directory
18-
rescript-editor-analysis reanalyze -dce-cmt path/to/lib/bs
18+
rescript-tools reanalyze -dce-cmt path/to/lib/bs
1919

2020
# Run all analyses
21-
rescript-editor-analysis reanalyze -all
21+
rescript-tools reanalyze -all
2222
```
2323

2424
## Performance Options
@@ -28,7 +28,7 @@ rescript-editor-analysis reanalyze -all
2828
Cache processed file data and skip unchanged files on subsequent runs:
2929

3030
```bash
31-
rescript-editor-analysis reanalyze -config -reactive
31+
rescript-tools reanalyze -config -reactive
3232
```
3333

3434
This provides significant speedup for repeated analysis (e.g., in a watch mode or service):
@@ -43,7 +43,7 @@ This provides significant speedup for repeated analysis (e.g., in a watch mode o
4343
Run analysis multiple times to measure cache effectiveness:
4444

4545
```bash
46-
rescript-editor-analysis reanalyze -config -reactive -timing -runs 3
46+
rescript-tools reanalyze -config -reactive -timing -runs 3
4747
```
4848

4949
## CLI Flags
@@ -85,7 +85,68 @@ The reactive mode (`-reactive`) caches processed per-file results and efficientl
8585
2. **Subsequent runs**: Only changed files are re-processed
8686
3. **Unchanged files**: Return cached `file_data` immediately (no I/O or unmarshalling)
8787

88-
This is the foundation for a persistent analysis service that can respond to file changes in milliseconds.
88+
This is the foundation for the **reanalyze-server** — a persistent analysis service that keeps reactive state warm across requests.
89+
90+
## Reanalyze Server
91+
92+
A long-lived server process that keeps reactive analysis state warm across multiple requests. This enables fast incremental analysis for editor integration.
93+
94+
### Transparent Server Delegation
95+
96+
When a server is running on the default socket (`<projectRoot>/.rescript-reanalyze.sock`), the regular `reanalyze` command **automatically delegates** to it. This means:
97+
98+
1. **Start the server once** (in the background)
99+
2. **Use the editor normally** — all `reanalyze` calls go through the server
100+
3. **Enjoy fast incremental analysis** — typically 10x faster after the first run
101+
102+
This works transparently with the VS Code extension's "Start Code Analyzer" command.
103+
104+
### Quick Start
105+
106+
```bash
107+
# From anywhere inside your project, start the server:
108+
rescript-tools reanalyze-server
109+
110+
# Now any reanalyze call will automatically use the server:
111+
rescript-tools reanalyze -json # → delegates to server
112+
```
113+
114+
### Starting the Server
115+
116+
```bash
117+
rescript-tools reanalyze-server [--socket <path>]
118+
```
119+
120+
Options:
121+
- `--socket <path>` — Unix domain socket path (default: `<projectRoot>/.rescript-reanalyze.sock`)
122+
123+
Examples:
124+
125+
```bash
126+
# Start server with default socket (recommended)
127+
rescript-tools reanalyze-server \
128+
129+
# With custom socket path
130+
rescript-tools reanalyze-server \
131+
--socket /tmp/my-custom.sock \
132+
```
133+
134+
### Behavior
135+
136+
- **Transparent delegation**: Regular `reanalyze` calls automatically use the server if running
137+
- **Default socket**: `<projectRoot>/.rescript-reanalyze.sock` (used by both server and client)
138+
- **Socket location invariant**: socket is always in the project root; `reanalyze` may be called from anywhere inside the project
139+
- **Reactive mode forced**: The server always runs with `-reactive` enabled internally
140+
- **Same output**: stdout/stderr/exit-code match what a direct CLI invocation would produce
141+
- **Incremental updates**: When source files change and the project is rebuilt, subsequent requests reflect the updated analysis
142+
143+
### Typical Workflow
144+
145+
1. **Start server** (once, in background)
146+
2. **Edit source files**
147+
3. **Rebuild project** (`yarn build` / `rescript build`)
148+
4. **Use editor** — analysis requests automatically go through the server
149+
5. **Stop server** when done (or leave running)
89150

90151
## Development
91152

analysis/reanalyze/src/EmitJson.ml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
let items = ref 0
2-
let start () = Printf.printf "["
2+
let start () =
3+
items := 0;
4+
Printf.printf "["
35
let finish () = Printf.printf "\n]\n"
46
let emitClose () = "\n}"
57

analysis/reanalyze/src/Paths.ml

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,14 @@ let rec findProjectRoot ~dir =
2626

2727
let runConfig = RunConfig.runConfig
2828

29-
let setReScriptProjectRoot =
30-
lazy
31-
(runConfig.projectRoot <- findProjectRoot ~dir:(Sys.getcwd ());
32-
runConfig.bsbProjectRoot <-
33-
(match Sys.getenv_opt "BSB_PROJECT_ROOT" with
34-
| None -> runConfig.projectRoot
35-
| Some s -> s))
29+
let setProjectRootFromCwd () =
30+
runConfig.projectRoot <- findProjectRoot ~dir:(Sys.getcwd ());
31+
runConfig.bsbProjectRoot <-
32+
(match Sys.getenv_opt "BSB_PROJECT_ROOT" with
33+
| None -> runConfig.projectRoot
34+
| Some s -> s)
35+
36+
let setReScriptProjectRoot = lazy (setProjectRootFromCwd ())
3637

3738
module Config = struct
3839
let readSuppress conf =
@@ -84,7 +85,7 @@ module Config = struct
8485

8586
(* Read the config from rescript.json and apply it to runConfig and suppress and unsuppress *)
8687
let processBsconfig () =
87-
Lazy.force setReScriptProjectRoot;
88+
setProjectRootFromCwd ();
8889
let rescriptFile = Filename.concat runConfig.projectRoot rescriptJson in
8990
let bsconfigFile = Filename.concat runConfig.projectRoot bsconfig in
9091

@@ -204,3 +205,72 @@ let readSourceDirs ~configSources =
204205
Log_.item "Types for cross-references will not be found.\n");
205206
dirs := readDirsFromConfig ~configSources);
206207
!dirs
208+
209+
type cmt_scan_entry = {
210+
build_root: string;
211+
scan_dirs: string list;
212+
also_scan_build_root: bool;
213+
}
214+
(** Read explicit `.cmt/.cmti` scan plan from `.sourcedirs.json`.
215+
216+
This is a v2 extension produced by `rewatch` to support monorepos without requiring
217+
reanalyze-side package resolution.
218+
219+
The scan plan is a list of build roots (usually `<pkg>/lib/bs`) relative to the project root,
220+
plus a list of subdirectories (relative to that build root) to scan for `.cmt/.cmti`.
221+
222+
If missing, returns the empty list and callers should fall back to legacy behavior. *)
223+
224+
let readCmtScan () =
225+
let sourceDirsFile =
226+
["lib"; "bs"; ".sourcedirs.json"]
227+
|> List.fold_left Filename.concat runConfig.bsbProjectRoot
228+
in
229+
let entries = ref [] in
230+
let read_entry (json : Ext_json_types.t) =
231+
match json with
232+
| Ext_json_types.Obj {map} -> (
233+
let build_root =
234+
match StringMap.find_opt map "build_root" with
235+
| Some (Ext_json_types.Str {str}) -> Some str
236+
| _ -> None
237+
in
238+
let scan_dirs =
239+
match StringMap.find_opt map "scan_dirs" with
240+
| Some (Ext_json_types.Arr {content = arr}) ->
241+
arr |> Array.to_list
242+
|> List.filter_map (fun x ->
243+
match x with
244+
| Ext_json_types.Str {str} -> Some str
245+
| _ -> None)
246+
| _ -> []
247+
in
248+
let also_scan_build_root =
249+
match StringMap.find_opt map "also_scan_build_root" with
250+
| Some (Ext_json_types.True _) -> true
251+
| Some (Ext_json_types.False _) -> false
252+
| _ -> false
253+
in
254+
match build_root with
255+
| Some build_root ->
256+
entries := {build_root; scan_dirs; also_scan_build_root} :: !entries
257+
| None -> ())
258+
| _ -> ()
259+
in
260+
let read_cmt_scan (json : Ext_json_types.t) =
261+
match json with
262+
| Ext_json_types.Obj {map} -> (
263+
match StringMap.find_opt map "cmt_scan" with
264+
| Some (Ext_json_types.Arr {content = arr}) ->
265+
arr |> Array.iter read_entry
266+
| _ -> ())
267+
| _ -> ()
268+
in
269+
if sourceDirsFile |> Sys.file_exists then (
270+
let jsonOpt = sourceDirsFile |> Ext_json_parse.parse_json_from_file in
271+
match jsonOpt with
272+
| exception _ -> []
273+
| json ->
274+
read_cmt_scan json;
275+
!entries |> List.rev)
276+
else []

analysis/reanalyze/src/ReactiveAnalysis.ml

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ type all_files_result = {
1919
type t = (Cmt_format.cmt_infos, cmt_file_result option) ReactiveFileCollection.t
2020
(** The reactive collection type *)
2121

22+
type processing_stats = {
23+
mutable total_files: int;
24+
mutable processed: int;
25+
mutable from_cache: int;
26+
}
27+
(** Stats from a process_files call *)
28+
2229
(** Process cmt_infos into a file result *)
2330
let process_cmt_infos ~config ~cmtFilePath cmt_infos : cmt_file_result option =
2431
let excludePath sourceFile =
@@ -75,8 +82,10 @@ let create ~config : t =
7582

7683
(** Process all files incrementally using ReactiveFileCollection.
7784
First run processes all files. Subsequent runs only process changed files.
78-
Uses batch processing to emit all changes as a single Batch delta. *)
79-
let process_files ~(collection : t) ~config:_ cmtFilePaths : all_files_result =
85+
Uses batch processing to emit all changes as a single Batch delta.
86+
Returns (result, stats) where stats contains processing information. *)
87+
let process_files ~(collection : t) ~config:_ cmtFilePaths :
88+
all_files_result * processing_stats =
8089
Timing.time_phase `FileLoading (fun () ->
8190
let total_files = List.length cmtFilePaths in
8291
let cached_before =
@@ -90,6 +99,7 @@ let process_files ~(collection : t) ~config:_ cmtFilePaths : all_files_result =
9099
ReactiveFileCollection.process_files_batch collection cmtFilePaths
91100
in
92101
let from_cache = total_files - processed in
102+
let stats = {total_files; processed; from_cache} in
93103

94104
if !Cli.timing then
95105
Printf.eprintf
@@ -113,10 +123,11 @@ let process_files ~(collection : t) ~config:_ cmtFilePaths : all_files_result =
113123
| None -> ())
114124
collection;
115125

116-
{
117-
dce_data_list = List.rev !dce_data_list;
118-
exception_results = List.rev !exception_results;
119-
})
126+
( {
127+
dce_data_list = List.rev !dce_data_list;
128+
exception_results = List.rev !exception_results;
129+
},
130+
stats ))
120131

121132
(** Get collection length *)
122133
let length (collection : t) = ReactiveFileCollection.length collection

0 commit comments

Comments
 (0)