Skip to content

Commit f68e752

Browse files
committed
reanalyze: add granular timing breakdown for analysis phases
Break down timing into sub-phases: - CMT processing: - File loading: time spent reading and traversing CMT files - Result collection: time spent merging results from domains - Analysis: - Merging: combining per-file data into global structures - Solving: running the liveness solver Signed-Off-By: Cursor AI <noreply@cursor.com>
1 parent 43a682d commit f68e752

File tree

3 files changed

+137
-106
lines changed

3 files changed

+137
-106
lines changed

analysis/reanalyze/src/Reanalyze.ml

Lines changed: 91 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -129,20 +129,21 @@ let collectCmtFilePaths ~cmtRoot : string list =
129129
(** Process files sequentially *)
130130
let processFilesSequential ~config (cmtFilePaths : string list) :
131131
all_files_result =
132-
let dce_data_list = ref [] in
133-
let exception_results = ref [] in
134-
cmtFilePaths
135-
|> List.iter (fun cmtFilePath ->
136-
match loadCmtFile ~config cmtFilePath with
137-
| Some {dce_data; exception_data} -> (
138-
(match dce_data with
139-
| Some data -> dce_data_list := data :: !dce_data_list
140-
| None -> ());
141-
match exception_data with
142-
| Some data -> exception_results := data :: !exception_results
143-
| None -> ())
144-
| None -> ());
145-
{dce_data_list = !dce_data_list; exception_results = !exception_results}
132+
Timing.time_phase `FileLoading (fun () ->
133+
let dce_data_list = ref [] in
134+
let exception_results = ref [] in
135+
cmtFilePaths
136+
|> List.iter (fun cmtFilePath ->
137+
match loadCmtFile ~config cmtFilePath with
138+
| Some {dce_data; exception_data} -> (
139+
(match dce_data with
140+
| Some data -> dce_data_list := data :: !dce_data_list
141+
| None -> ());
142+
match exception_data with
143+
| Some data -> exception_results := data :: !exception_results
144+
| None -> ())
145+
| None -> ());
146+
{dce_data_list = !dce_data_list; exception_results = !exception_results})
146147

147148
(** Process files in parallel using OCaml 5 Domains *)
148149
let processFilesParallel ~config ~numDomains (cmtFilePaths : string list) :
@@ -178,42 +179,43 @@ let processFilesParallel ~config ~numDomains (cmtFilePaths : string list) :
178179
allExceptionData := !localExn @ !allExceptionData;
179180
Mutex.unlock resultsMutex
180181
in
181-
(* Spawn domains for parallel processing *)
182-
let domains =
183-
Array.init numDomains (fun i ->
184-
let startIdx = i * chunkSize in
185-
let endIdx = min ((i + 1) * chunkSize) numFiles in
186-
if startIdx < numFiles then
187-
Some (Domain.spawn (fun () -> processChunk startIdx endIdx))
188-
else None)
189-
in
190-
(* Wait for all domains to complete *)
191-
Array.iter
192-
(function
193-
| Some d -> Domain.join d
194-
| None -> ())
195-
domains;
182+
(* Time the overall parallel processing *)
183+
Timing.time_phase `FileLoading (fun () ->
184+
(* Spawn domains for parallel processing *)
185+
let domains =
186+
Array.init numDomains (fun i ->
187+
let startIdx = i * chunkSize in
188+
let endIdx = min ((i + 1) * chunkSize) numFiles in
189+
if startIdx < numFiles then
190+
Some (Domain.spawn (fun () -> processChunk startIdx endIdx))
191+
else None)
192+
in
193+
(* Wait for all domains to complete *)
194+
Array.iter
195+
(function
196+
| Some d -> Domain.join d
197+
| None -> ())
198+
domains);
196199
{dce_data_list = !allDceData; exception_results = !allExceptionData}
197200

198201
(** Process all cmt files and return results for DCE and Exception analysis.
199202
Conceptually: map process_cmt_file over all files. *)
200203
let processCmtFiles ~config ~cmtRoot : all_files_result =
201-
Timing.time_phase `CmtProcessing (fun () ->
202-
let cmtFilePaths = collectCmtFilePaths ~cmtRoot in
203-
let numDomains =
204-
match !Cli.parallel with
205-
| n when n > 0 -> n
206-
| n when n < 0 ->
207-
(* Auto-detect: use recommended domain count (number of cores) *)
208-
Domain.recommended_domain_count ()
209-
| _ -> 0
210-
in
211-
if numDomains > 0 then (
212-
if !Cli.timing then
213-
Printf.eprintf "Using %d parallel domains for %d files\n%!" numDomains
214-
(List.length cmtFilePaths);
215-
processFilesParallel ~config ~numDomains cmtFilePaths)
216-
else processFilesSequential ~config cmtFilePaths)
204+
let cmtFilePaths = collectCmtFilePaths ~cmtRoot in
205+
let numDomains =
206+
match !Cli.parallel with
207+
| n when n > 0 -> n
208+
| n when n < 0 ->
209+
(* Auto-detect: use recommended domain count (number of cores) *)
210+
Domain.recommended_domain_count ()
211+
| _ -> 0
212+
in
213+
if numDomains > 0 then (
214+
if !Cli.timing then
215+
Printf.eprintf "Using %d parallel domains for %d files\n%!" numDomains
216+
(List.length cmtFilePaths);
217+
processFilesParallel ~config ~numDomains cmtFilePaths)
218+
else processFilesSequential ~config cmtFilePaths
217219

218220
(* Shuffle a list using Fisher-Yates algorithm *)
219221
let shuffle_list lst =
@@ -243,44 +245,47 @@ let runAnalysis ~dce_config ~cmtRoot =
243245
in
244246
(* Analysis phase: merge data and solve *)
245247
let analysis_result =
246-
Timing.time_phase `Analysis (fun () ->
247-
if dce_config.DceConfig.run.dce then (
248-
(* Merge: combine all builders -> immutable data *)
249-
let annotations =
250-
FileAnnotations.merge_all
251-
(dce_data_list
252-
|> List.map (fun fd -> fd.DceFileProcessing.annotations))
253-
in
254-
let decls =
255-
Declarations.merge_all
256-
(dce_data_list |> List.map (fun fd -> fd.DceFileProcessing.decls))
257-
in
258-
let cross_file =
259-
CrossFileItems.merge_all
260-
(dce_data_list
261-
|> List.map (fun fd -> fd.DceFileProcessing.cross_file))
262-
in
263-
(* Merge refs and file_deps into builders for cross-file items processing *)
264-
let refs_builder = References.create_builder () in
265-
let file_deps_builder = FileDeps.create_builder () in
266-
dce_data_list
267-
|> List.iter (fun fd ->
268-
References.merge_into_builder ~from:fd.DceFileProcessing.refs
269-
~into:refs_builder;
270-
FileDeps.merge_into_builder
271-
~from:fd.DceFileProcessing.file_deps ~into:file_deps_builder);
272-
(* Compute type-label dependencies after merge (no global TypeLabels table during traversal) *)
273-
DeadType.process_type_label_dependencies ~config:dce_config ~decls
274-
~refs:refs_builder;
275-
let find_exception = DeadException.find_exception_from_decls decls in
276-
(* Process cross-file exception refs - they write to refs_builder and file_deps_builder *)
277-
CrossFileItems.process_exception_refs cross_file ~refs:refs_builder
278-
~file_deps:file_deps_builder ~find_exception ~config:dce_config;
279-
(* Now freeze refs and file_deps for solver *)
280-
let refs = References.freeze_builder refs_builder in
281-
let file_deps = FileDeps.freeze_builder file_deps_builder in
282-
(* Run the solver - returns immutable AnalysisResult.t.
283-
Optional-args checks are done in a separate pass after liveness is known. *)
248+
if dce_config.DceConfig.run.dce then (
249+
(* Merging phase: combine all builders -> immutable data *)
250+
let annotations, decls, cross_file, refs, file_deps =
251+
Timing.time_phase `Merging (fun () ->
252+
let annotations =
253+
FileAnnotations.merge_all
254+
(dce_data_list
255+
|> List.map (fun fd -> fd.DceFileProcessing.annotations))
256+
in
257+
let decls =
258+
Declarations.merge_all
259+
(dce_data_list |> List.map (fun fd -> fd.DceFileProcessing.decls))
260+
in
261+
let cross_file =
262+
CrossFileItems.merge_all
263+
(dce_data_list
264+
|> List.map (fun fd -> fd.DceFileProcessing.cross_file))
265+
in
266+
(* Merge refs and file_deps into builders for cross-file items processing *)
267+
let refs_builder = References.create_builder () in
268+
let file_deps_builder = FileDeps.create_builder () in
269+
dce_data_list
270+
|> List.iter (fun fd ->
271+
References.merge_into_builder ~from:fd.DceFileProcessing.refs
272+
~into:refs_builder;
273+
FileDeps.merge_into_builder
274+
~from:fd.DceFileProcessing.file_deps ~into:file_deps_builder);
275+
(* Compute type-label dependencies after merge *)
276+
DeadType.process_type_label_dependencies ~config:dce_config ~decls
277+
~refs:refs_builder;
278+
let find_exception = DeadException.find_exception_from_decls decls in
279+
(* Process cross-file exception refs *)
280+
CrossFileItems.process_exception_refs cross_file ~refs:refs_builder
281+
~file_deps:file_deps_builder ~find_exception ~config:dce_config;
282+
(* Freeze refs and file_deps for solver *)
283+
let refs = References.freeze_builder refs_builder in
284+
let file_deps = FileDeps.freeze_builder file_deps_builder in
285+
(annotations, decls, cross_file, refs, file_deps))
286+
in
287+
(* Solving phase: run the solver and collect issues *)
288+
Timing.time_phase `Solving (fun () ->
284289
let empty_optional_args_state = OptionalArgsState.create () in
285290
let analysis_result_core =
286291
DeadCommon.solveDead ~annotations ~decls ~refs ~file_deps
@@ -295,8 +300,7 @@ let runAnalysis ~dce_config ~cmtRoot =
295300
| None -> true
296301
in
297302
let optional_args_state =
298-
CrossFileItems.compute_optional_args_state cross_file ~decls
299-
~is_live
303+
CrossFileItems.compute_optional_args_state cross_file ~decls ~is_live
300304
in
301305
(* Collect optional args issues only for live declarations *)
302306
let optional_args_issues =
@@ -312,9 +316,8 @@ let runAnalysis ~dce_config ~cmtRoot =
312316
decls []
313317
|> List.rev
314318
in
315-
Some
316-
(AnalysisResult.add_issues analysis_result_core optional_args_issues))
317-
else None)
319+
Some (AnalysisResult.add_issues analysis_result_core optional_args_issues)))
320+
else None
318321
in
319322
(* Reporting phase *)
320323
Timing.time_phase `Reporting (fun () ->

analysis/reanalyze/src/Timing.ml

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,32 @@
33
let enabled = ref false
44

55
type phase_times = {
6-
mutable cmt_processing: float;
7-
mutable analysis: float;
6+
(* CMT processing sub-phases *)
7+
mutable file_loading: float;
8+
mutable result_collection: float;
9+
(* Analysis sub-phases *)
10+
mutable merging: float;
11+
mutable solving: float;
12+
(* Reporting *)
813
mutable reporting: float;
914
}
1015

11-
let times = {cmt_processing = 0.0; analysis = 0.0; reporting = 0.0}
16+
let times = {
17+
file_loading = 0.0;
18+
result_collection = 0.0;
19+
merging = 0.0;
20+
solving = 0.0;
21+
reporting = 0.0;
22+
}
23+
24+
(* Mutex to protect timing updates from concurrent domains *)
25+
let timing_mutex = Mutex.create ()
1226

1327
let reset () =
14-
times.cmt_processing <- 0.0;
15-
times.analysis <- 0.0;
28+
times.file_loading <- 0.0;
29+
times.result_collection <- 0.0;
30+
times.merging <- 0.0;
31+
times.solving <- 0.0;
1632
times.reporting <- 0.0
1733

1834
let now () = Unix.gettimeofday ()
@@ -22,21 +38,33 @@ let time_phase phase_name f =
2238
let start = now () in
2339
let result = f () in
2440
let elapsed = now () -. start in
41+
(* Use mutex to safely update shared timing state *)
42+
Mutex.lock timing_mutex;
2543
(match phase_name with
26-
| `CmtProcessing -> times.cmt_processing <- times.cmt_processing +. elapsed
27-
| `Analysis -> times.analysis <- times.analysis +. elapsed
44+
| `FileLoading -> times.file_loading <- times.file_loading +. elapsed
45+
| `ResultCollection ->
46+
times.result_collection <- times.result_collection +. elapsed
47+
| `Merging -> times.merging <- times.merging +. elapsed
48+
| `Solving -> times.solving <- times.solving +. elapsed
2849
| `Reporting -> times.reporting <- times.reporting +. elapsed);
50+
Mutex.unlock timing_mutex;
2951
result)
3052
else f ()
3153

3254
let report () =
3355
if !enabled then (
34-
let total = times.cmt_processing +. times.analysis +. times.reporting in
56+
let cmt_total = times.file_loading +. times.result_collection in
57+
let analysis_total = times.merging +. times.solving in
58+
let total = cmt_total +. analysis_total +. times.reporting in
3559
Printf.eprintf "\n=== Timing ===\n";
36-
Printf.eprintf " CMT processing: %.3fs (%.1f%%)\n" times.cmt_processing
37-
(100.0 *. times.cmt_processing /. total);
38-
Printf.eprintf " Analysis: %.3fs (%.1f%%)\n" times.analysis
39-
(100.0 *. times.analysis /. total);
40-
Printf.eprintf " Reporting: %.3fs (%.1f%%)\n" times.reporting
60+
Printf.eprintf " CMT processing: %.3fs (%.1f%%)\n" cmt_total
61+
(100.0 *. cmt_total /. total);
62+
Printf.eprintf " - File loading: %.3fs\n" times.file_loading;
63+
Printf.eprintf " - Result collection: %.3fs\n" times.result_collection;
64+
Printf.eprintf " Analysis: %.3fs (%.1f%%)\n" analysis_total
65+
(100.0 *. analysis_total /. total);
66+
Printf.eprintf " - Merging: %.3fs\n" times.merging;
67+
Printf.eprintf " - Solving: %.3fs\n" times.solving;
68+
Printf.eprintf " Reporting: %.3fs (%.1f%%)\n" times.reporting
4169
(100.0 *. times.reporting /. total);
42-
Printf.eprintf " Total: %.3fs\n" total)
70+
Printf.eprintf " Total: %.3fs\n" total)

tests/analysis_tests/tests-reanalyze/deadcode-benchmark/Makefile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,18 @@ benchmark: generate build
2222
@echo "=== Benchmark: $(COPIES) copies (~$$(find src -name '*.res' | wc -l) files) ==="
2323
@echo ""
2424
@echo "Sequential:"
25-
@dune exec rescript-editor-analysis -- reanalyze -config -ci -timing 2>&1 | grep -E "Analysis reported|=== Timing ===|CMT processing|Analysis:|Reporting:|Total:"
25+
@dune exec rescript-editor-analysis -- reanalyze -config -ci -timing 2>&1 | grep -E "Analysis reported|=== Timing|CMT processing|File loading|Result collection|Analysis:|Merging|Solving|Reporting:|Total:"
2626
@echo ""
2727
@echo "Parallel (auto-detect cores):"
28-
@dune exec rescript-editor-analysis -- reanalyze -config -ci -timing -parallel -1 2>&1 | grep -E "Analysis reported|=== Timing ===|CMT processing|Analysis:|Reporting:|Total:"
28+
@dune exec rescript-editor-analysis -- reanalyze -config -ci -timing -parallel -1 2>&1 | grep -E "Analysis reported|=== Timing|CMT processing|File loading|Result collection|Analysis:|Merging|Solving|Reporting:|Total:"
2929

3030
# Just time analysis (assumes already built)
3131
time:
3232
@echo "Sequential:"
33-
@dune exec rescript-editor-analysis -- reanalyze -config -ci -timing 2>&1 | grep -E "Analysis reported|=== Timing ===|CMT processing|Analysis:|Reporting:|Total:"
33+
@dune exec rescript-editor-analysis -- reanalyze -config -ci -timing 2>&1 | grep -E "Analysis reported|=== Timing|CMT processing|File loading|Result collection|Analysis:|Merging|Solving|Reporting:|Total:"
3434
@echo ""
3535
@echo "Parallel (auto-detect cores):"
36-
@dune exec rescript-editor-analysis -- reanalyze -config -ci -timing -parallel -1 2>&1 | grep -E "Analysis reported|=== Timing ===|CMT processing|Analysis:|Reporting:|Total:"
36+
@dune exec rescript-editor-analysis -- reanalyze -config -ci -timing -parallel -1 2>&1 | grep -E "Analysis reported|=== Timing|CMT processing|File loading|Result collection|Analysis:|Merging|Solving|Reporting:|Total:"
3737

3838
.DEFAULT_GOAL := benchmark
3939

0 commit comments

Comments
 (0)