Skip to content

Commit b180c92

Browse files
committed
Add benchmarking for output generation time and optimize cycle detection in dependency graph
- Introduced timing for output generation in main.ml to benchmark performance. - Enhanced cycle detection in dependency_graph.ml by using memoization and a set to track unique cycles, improving efficiency. - Optimized JSON output generation in formatter.ml by pre-computing dependencies and paths, reducing redundant calculations.
1 parent cc52cff commit b180c92

File tree

3 files changed

+176
-56
lines changed

3 files changed

+176
-56
lines changed

bin/main.ml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ let main () =
135135
| Rescriptdep.Formatter.Dot -> "dot"
136136
| Rescriptdep.Formatter.Json -> "json");
137137

138+
let output_start_time = Unix.gettimeofday () in
139+
138140
(match !output_file with
139141
| Some file ->
140142
(* Output to file only *)
@@ -147,6 +149,13 @@ let main () =
147149
if !verbose then Printf.eprintf "Writing output to stdout\n";
148150
Rescriptdep.Formatter.output_graph !format focused_graph stdout);
149151

152+
let output_end_time = Unix.gettimeofday () in
153+
let output_time = output_end_time -. output_start_time in
154+
155+
if !benchmark then
156+
Printf.eprintf "[BENCH] Pure output generation: %.4f seconds\n"
157+
output_time;
158+
150159
time_checkpoint "Output generation completed";
151160

152161
if !benchmark then (

lib/dependency_graph.ml

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,77 @@ let has_cycle graph start_module =
8181

8282
(* Find all cycles in the dependency graph *)
8383
let find_all_cycles graph =
84+
(* Use a hash table to memoize visited modules and their cycles *)
85+
let visited = Hashtbl.create (StringMap.cardinal graph.dependencies) in
86+
let cycle_cache = Hashtbl.create (StringMap.cardinal graph.dependencies) in
87+
let result = ref [] in
88+
89+
let rec check_cycle path module_name =
90+
(* Check if this module is part of a cycle we've already processed *)
91+
if Hashtbl.mem cycle_cache module_name then
92+
Hashtbl.find cycle_cache module_name
93+
else if List.mem module_name path then (
94+
(* Cycle detected - extract just the cycle part *)
95+
let cycle_start =
96+
let rec find_index i = function
97+
| [] -> 0
98+
| x :: _ when x = module_name -> i
99+
| _ :: xs -> find_index (i + 1) xs
100+
in
101+
find_index 0 path
102+
in
103+
let cycle =
104+
let rec take_until n = function
105+
| [] -> []
106+
| x :: _ when n = 0 -> []
107+
| x :: xs -> x :: take_until (n - 1) xs
108+
in
109+
module_name :: take_until cycle_start path |> List.rev
110+
in
111+
Hashtbl.add cycle_cache module_name (Some cycle);
112+
Some cycle)
113+
else if Hashtbl.mem visited module_name then
114+
(* Already visited, no cycle through this path *)
115+
None
116+
else (
117+
Hashtbl.add visited module_name true;
118+
let new_path = module_name :: path in
119+
let deps = get_dependencies graph module_name in
120+
121+
(* Sort dependencies to ensure consistent cycle detection *)
122+
let sorted_deps = List.sort String.compare deps in
123+
124+
let cycle_result =
125+
try List.find_map (fun dep -> check_cycle new_path dep) sorted_deps
126+
with Not_found -> None
127+
in
128+
129+
Hashtbl.add cycle_cache module_name cycle_result;
130+
cycle_result)
131+
in
132+
133+
(* Create a set to track unique cycles *)
134+
let module CycleSet = Set.Make (struct
135+
type t = string list
136+
137+
let compare = compare
138+
end) in
139+
let unique_cycles = ref CycleSet.empty in
140+
84141
let modules = get_modules graph in
85-
List.fold_left
86-
(fun acc module_name ->
87-
match has_cycle graph module_name with
88-
| Some cycle when not (List.exists (fun c -> c = cycle) acc) ->
89-
cycle :: acc
90-
| _ -> acc)
91-
[] modules
142+
List.iter
143+
(fun module_name ->
144+
if not (Hashtbl.mem visited module_name) then
145+
match check_cycle [] module_name with
146+
| Some cycle ->
147+
(* Only add if this exact cycle hasn't been seen before *)
148+
if not (CycleSet.mem cycle !unique_cycles) then (
149+
unique_cycles := CycleSet.add cycle !unique_cycles;
150+
result := cycle :: !result)
151+
| None -> ())
152+
modules;
153+
154+
!result
92155

93156
(* Topological sort of modules (dependencies first) *)
94157
let topological_sort graph =

lib/formatter.ml

Lines changed: 97 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -85,15 +85,86 @@ and output_dot graph out_channel =
8585
(* Output as JSON format *)
8686
and output_json graph out_channel =
8787
let modules = Dependency_graph.get_modules graph in
88+
89+
(* Pre-compute all expensive operations once *)
8890
let metrics = Dependency_graph.calculate_metrics graph in
8991
let cycles = Dependency_graph.find_all_cycles graph in
92+
let sccs = Dependency_graph.find_strongly_connected_components graph in
93+
94+
(* Create a lookup table for modules in cycles *)
95+
let in_cycle_table = Hashtbl.create (List.length modules) in
96+
List.iter
97+
(fun scc ->
98+
if List.length scc > 1 then
99+
List.iter (fun m -> Hashtbl.replace in_cycle_table m true) scc)
100+
sccs;
101+
102+
(* Pre-compute all module paths and create a lookup map *)
103+
let path_map = Hashtbl.create (List.length modules) in
104+
List.iter
105+
(fun m ->
106+
match Dependency_graph.get_module_path graph m with
107+
| Some path -> Hashtbl.add path_map m path
108+
| None -> ())
109+
modules;
90110

91111
(* Get all source directories from module paths for dependency resolution *)
92-
let source_dirs =
93-
modules
94-
|> List.filter_map (fun m -> Dependency_graph.get_module_path graph m)
95-
|> List.map Filename.dirname
96-
|> List.sort_uniq String.compare
112+
let source_dirs = ref [] in
113+
List.iter
114+
(fun m ->
115+
match Dependency_graph.get_module_path graph m with
116+
| Some path ->
117+
let dir = Filename.dirname path in
118+
if not (List.mem dir !source_dirs) then
119+
source_dirs := dir :: !source_dirs
120+
| None -> ())
121+
modules;
122+
123+
(* Pre-compute all dependencies and dependents *)
124+
let deps_map = Hashtbl.create (List.length modules) in
125+
let dependents_map = Hashtbl.create (List.length modules) in
126+
127+
(* First compute all dependencies *)
128+
List.iter
129+
(fun m ->
130+
let deps = Dependency_graph.get_dependencies graph m in
131+
Hashtbl.add deps_map m deps)
132+
modules;
133+
134+
(* Then compute all dependents using the deps map *)
135+
List.iter
136+
(fun m ->
137+
let deps = try Hashtbl.find deps_map m with Not_found -> [] in
138+
List.iter
139+
(fun dep ->
140+
let current_dependents =
141+
try Hashtbl.find dependents_map dep with Not_found -> []
142+
in
143+
Hashtbl.replace dependents_map dep (m :: current_dependents))
144+
deps)
145+
modules;
146+
147+
(* Helper function to resolve path for a module *)
148+
let resolve_path m =
149+
try Some (Hashtbl.find path_map m)
150+
with Not_found -> (
151+
match Parse_utils.find_implementation_file_by_name m !source_dirs with
152+
| Some p ->
153+
Hashtbl.add path_map m p;
154+
Some p
155+
| None -> (
156+
(* If not found, try to resolve from node_modules *)
157+
let node_path_opt =
158+
if List.length !source_dirs > 0 then
159+
Parse_utils.find_external_module_path m
160+
(Filename.dirname (List.hd !source_dirs))
161+
else None
162+
in
163+
match node_path_opt with
164+
| Some p ->
165+
Hashtbl.add path_map m p;
166+
Some p
167+
| None -> None))
97168
in
98169

99170
(* JSON opening *)
@@ -104,15 +175,19 @@ and output_json graph out_channel =
104175

105176
List.iteri
106177
(fun i module_name ->
107-
let deps = Dependency_graph.get_dependencies graph module_name in
108-
let dependents = Dependency_graph.find_dependents graph module_name in
109-
let path = Dependency_graph.get_module_path graph module_name in
178+
let deps = try Hashtbl.find deps_map module_name with Not_found -> [] in
179+
let dependents =
180+
try Hashtbl.find dependents_map module_name with Not_found -> []
181+
in
182+
let path_opt =
183+
try Some (Hashtbl.find path_map module_name) with Not_found -> None
184+
in
110185

111186
output_string out_channel " {\n";
112187
output_string out_channel (" \"name\": \"" ^ module_name ^ "\",\n");
113188

114189
(* Add file path info if available *)
115-
(match path with
190+
(match path_opt with
116191
| Some path_str ->
117192
output_string out_channel (" \"path\": \"" ^ path_str ^ "\",\n")
118193
| None -> output_string out_channel " \"path\": null,\n");
@@ -124,27 +199,13 @@ and output_json graph out_channel =
124199
output_string out_channel "\n";
125200
List.iteri
126201
(fun j dep ->
127-
let dep_path = Dependency_graph.get_module_path graph dep in
202+
let dep_path_opt = resolve_path dep in
203+
128204
output_string out_channel " {\n";
129205
output_string out_channel (" \"name\": \"" ^ dep ^ "\"");
130206

131-
(* Try to resolve path if it's null *)
132-
let resolved_path =
133-
match dep_path with
134-
| Some path_str -> Some path_str
135-
| None -> (
136-
match
137-
Parse_utils.find_implementation_file_by_name dep source_dirs
138-
with
139-
| Some path -> Some path
140-
| None ->
141-
(* If not found, try to resolve from node_modules *)
142-
Parse_utils.find_external_module_path dep
143-
(Filename.dirname (List.hd source_dirs)))
144-
in
145-
146207
(* Add dependency file path if available *)
147-
(match resolved_path with
208+
(match dep_path_opt with
148209
| Some path_str ->
149210
output_string out_channel
150211
(",\n \"path\": \"" ^ path_str ^ "\"")
@@ -165,30 +226,17 @@ and output_json graph out_channel =
165226
output_string out_channel "\n";
166227
List.iteri
167228
(fun j dependent ->
168-
let dependent_path =
169-
match Dependency_graph.get_module_path graph dependent with
170-
| Some path -> Some path
171-
| None -> (
172-
match
173-
Parse_utils.find_implementation_file_by_name dependent
174-
source_dirs
175-
with
176-
| Some path -> Some path
177-
| None ->
178-
(* If not found, try to resolve from node_modules *)
179-
Parse_utils.find_external_module_path dependent
180-
(Filename.dirname (List.hd source_dirs)))
181-
in
229+
let dependent_path_opt = resolve_path dependent in
182230

183231
output_string out_channel " {\n";
184232
output_string out_channel
185233
(" \"name\": \"" ^ dependent ^ "\"");
186234

187235
(* Add dependent file path if available *)
188-
(match dependent_path with
189-
| Some path ->
236+
(match dependent_path_opt with
237+
| Some path_str ->
190238
output_string out_channel
191-
(",\n \"path\": \"" ^ path ^ "\"")
239+
(",\n \"path\": \"" ^ path_str ^ "\"")
192240
| None -> output_string out_channel ",\n \"path\": null");
193241

194242
output_string out_channel "\n }";
@@ -207,11 +255,9 @@ and output_json graph out_channel =
207255
output_string out_channel
208256
(",\n \"fan_out\": " ^ string_of_int fan_out);
209257

210-
(* Check for circular dependencies *)
258+
(* Check for circular dependencies using pre-computed table *)
211259
let is_in_cycle =
212-
List.exists
213-
(fun scc -> List.length scc > 1 && List.mem module_name scc)
214-
(Dependency_graph.find_strongly_connected_components graph)
260+
try Hashtbl.find in_cycle_table module_name with Not_found -> false
215261
in
216262
output_string out_channel
217263
(",\n \"in_cycle\": " ^ string_of_bool is_in_cycle);
@@ -266,8 +312,10 @@ and output_json graph out_channel =
266312

267313
let format_float f =
268314
let s = string_of_float f in
269-
if String.ends_with ~suffix:"." s then s ^ "0"
270-
(* Add 0 if ending with decimal point *) else s
315+
let len = String.length s in
316+
if len > 0 && s.[len - 1] = '.' then s ^ "0"
317+
(* Add 0 if ending with decimal point *)
318+
else s
271319
in
272320

273321
output_string out_channel

0 commit comments

Comments
 (0)