1use std::time::Instant;
47
48use ifc_lite_core::build_entity_index;
49use ifc_lite_geometry::csg::{reset_csg_census, take_csg_census};
50use ifc_lite_processing::{process_geometry, ProcessingStats};
51
52struct Probe {
54 path: String,
55 file_mb: f64,
56 entities: usize,
57 index_build_ms: f64,
58 stats: ProcessingStats,
60 all_totals_ms: Vec<u64>,
61 census: Option<CensusSummary>,
62}
63
64#[derive(Default)]
66struct CensusSummary {
67 subtract: u64,
68 union: u64,
69 intersection: u64,
70 clip: u64,
71 operand_tris: u64,
74}
75
76const OP_SUBTRACT: u8 = 0;
81const OP_UNION: u8 = 1;
82const OP_INTERSECTION: u8 = 2;
83const OP_CLIP: u8 = 3;
84
85fn summarize_census() -> CensusSummary {
86 let mut s = CensusSummary::default();
87 for r in take_csg_census() {
88 match r.op {
89 OP_SUBTRACT => s.subtract += 1,
90 OP_UNION => s.union += 1,
91 OP_INTERSECTION => s.intersection += 1,
92 OP_CLIP => s.clip += 1,
93 _ => {}
94 }
95 s.operand_tris += r.a_tris as u64 + r.b_tris as u64;
96 }
97 s
98}
99
100fn run(path: &str, iters: usize, want_census: bool) -> Option<Probe> {
101 let content = match std::fs::read(path) {
102 Ok(c) => c,
103 Err(e) => {
104 eprintln!("skip {path}: {e}");
105 return None;
106 }
107 };
108 let file_mb = content.len() as f64 / 1.048_576e6;
109
110 let mut index_build_ms = f64::INFINITY;
113 let mut entities = 0usize;
114 for _ in 0..3 {
115 let t = Instant::now();
116 let idx = build_entity_index(&content);
117 let ms = t.elapsed().as_secs_f64() * 1e3;
118 entities = idx.len();
119 index_build_ms = index_build_ms.min(ms);
120 }
121
122 let mut best: Option<ProcessingStats> = None;
125 let mut best_total = u64::MAX;
126 let mut best_census: Option<CensusSummary> = None;
127 let mut all_totals_ms = Vec::with_capacity(iters);
128 for _ in 0..iters.max(1) {
129 if want_census {
130 reset_csg_census();
131 }
132 let result = process_geometry(&content);
133 let census = if want_census {
134 Some(summarize_census())
135 } else {
136 None
137 };
138 all_totals_ms.push(result.stats.total_time_ms);
139 if result.stats.total_time_ms <= best_total {
140 best_total = result.stats.total_time_ms;
141 best = Some(result.stats);
142 best_census = census;
143 }
144 }
145
146 Some(Probe {
147 path: path.to_string(),
148 file_mb,
149 entities,
150 index_build_ms,
151 stats: best?,
152 all_totals_ms,
153 census: best_census,
154 })
155}
156
157fn pct(part: u64, whole: u64) -> f64 {
158 if whole == 0 {
159 0.0
160 } else {
161 part as f64 / whole as f64 * 100.0
162 }
163}
164
165fn print_human(p: &Probe) {
166 let s = &p.stats;
167 let total = s.total_time_ms.max(1);
168 let parse = s.parse_time_ms;
169 let geom = s.geometry_time_ms;
170 let tris = s.total_triangles;
171 let mtris_s = if geom > 0 {
172 tris as f64 / (geom as f64 / 1e3) / 1e6
173 } else {
174 0.0
175 };
176 let cache_refs = s.point_cache_hits + s.point_cache_misses;
177 let hit_rate = pct(s.point_cache_hits, cache_refs);
178
179 eprintln!("\n=== {} ===", p.path);
180 eprintln!(
181 " {:.1} MB | {} entities | {} meshes | {} verts | {} tris | {:.2} Mtris/s (geom)",
182 p.file_mb, p.entities, s.total_meshes, s.total_vertices, tris, mtris_s,
183 );
184 eprintln!(
185 " best total {} ms (runs: {:?} ms)",
186 s.total_time_ms, p.all_totals_ms
187 );
188 eprintln!(" phase ms % total");
189 eprintln!(
190 " parse (pre-geometry) {:>8} {:>5.1}%",
191 parse,
192 pct(parse, total)
193 );
194 eprintln!(
195 " - index-scan alone {:>8.1} {:>5.1}% (isolated build_entity_index)",
196 p.index_build_ms,
197 pct(p.index_build_ms as u64, total)
198 );
199 eprintln!(
200 " - entity_scan {:>8} {:>5.1}%",
201 s.entity_scan_time_ms,
202 pct(s.entity_scan_time_ms, total)
203 );
204 eprintln!(
205 " - lookup/styles {:>8} {:>5.1}%",
206 s.lookup_time_ms,
207 pct(s.lookup_time_ms, total)
208 );
209 eprintln!(
210 " - preprocess {:>8} {:>5.1}%",
211 s.preprocess_time_ms,
212 pct(s.preprocess_time_ms, total)
213 );
214 eprintln!(
215 " geometry {:>8} {:>5.1}%",
216 geom,
217 pct(geom, total)
218 );
219 if s.faceted_brep_time_ms > 0 {
220 eprintln!(
221 " - faceted-brep {:>8} {:>5.1}% (observability build)",
222 s.faceted_brep_time_ms,
223 pct(s.faceted_brep_time_ms, total)
224 );
225 }
226 if cache_refs > 0 {
227 eprintln!(
228 " brep point-cache {} hits / {} misses ({:.1}% memoized)",
229 s.point_cache_hits, s.point_cache_misses, hit_rate
230 );
231 }
232 if s.total_csg_failures > 0 {
233 eprintln!(
234 " csg failures {} across {} products",
235 s.total_csg_failures, s.products_with_failures
236 );
237 }
238 if s.degenerate_triangles_dropped > 0 {
239 eprintln!(
240 " degenerate dropped {}",
241 s.degenerate_triangles_dropped
242 );
243 }
244 if let Some(c) = &p.census {
245 eprintln!(
246 " csg census {} subtract / {} union / {} intersect / {} clip | {} operand-tris",
247 c.subtract, c.union, c.intersection, c.clip, c.operand_tris
248 );
249 }
250}
251
252fn print_json(probes: &[Probe]) {
253 let mut out = String::from("[\n");
256 for (i, p) in probes.iter().enumerate() {
257 let s = &p.stats;
258 let census = p
259 .census
260 .as_ref()
261 .map(|c| {
262 format!(
263 r#","csg":{{"subtract":{},"union":{},"intersection":{},"clip":{},"operandTris":{}}}"#,
264 c.subtract, c.union, c.intersection, c.clip, c.operand_tris
265 )
266 })
267 .unwrap_or_default();
268 out.push_str(&format!(
269 concat!(
270 " {{",
271 r#""path":{:?},"fileMb":{:.3},"entities":{},"meshes":{},"vertices":{},"triangles":{},"#,
272 r#""indexBuildMs":{:.2},"parseMs":{},"entityScanMs":{},"lookupMs":{},"preprocessMs":{},"#,
273 r#""geometryMs":{},"facetedBrepMs":{},"totalMs":{},"allTotalsMs":{:?},"#,
274 r#""pointCacheHits":{},"pointCacheMisses":{},"csgFailures":{},"degenerateDropped":{}{}}}"#,
275 ),
276 p.path,
277 p.file_mb,
278 p.entities,
279 s.total_meshes,
280 s.total_vertices,
281 s.total_triangles,
282 p.index_build_ms,
283 s.parse_time_ms,
284 s.entity_scan_time_ms,
285 s.lookup_time_ms,
286 s.preprocess_time_ms,
287 s.geometry_time_ms,
288 s.faceted_brep_time_ms,
289 s.total_time_ms,
290 p.all_totals_ms,
291 s.point_cache_hits,
292 s.point_cache_misses,
293 s.total_csg_failures,
294 s.degenerate_triangles_dropped,
295 census,
296 ));
297 out.push_str(if i + 1 < probes.len() { ",\n" } else { "\n" });
298 }
299 out.push(']');
300 println!("{out}");
301}
302
303const SUITE: &[&str] = &[
309 "tests/models/ara3d/AC20-FZK-Haus.ifc", "tests/models/various/01_Snowdon_Towers_Sample_Structural(1).ifc", "tests/models/various/01_BIMcollab_Example_ARC.ifc", "tests/models/ara3d/schependomlaan.ifc", "tests/models/ara3d/ISSUE_053_20181220Holter_Tower_10.ifc", "tests/models/various/O-S1-BWK-BIM architectural - BIM bouwkundig.ifc", ];
316
317fn main() {
318 let mut iters = 3usize;
319 let mut json = false;
320 let mut census = false;
321 let mut suite = false;
322 let mut fixtures: Vec<String> = Vec::new();
323
324 let mut args = std::env::args().skip(1);
325 while let Some(a) = args.next() {
326 match a.as_str() {
327 "--iters" => {
328 iters = args
329 .next()
330 .and_then(|v| v.parse().ok())
331 .filter(|n| *n >= 1)
332 .unwrap_or_else(|| {
333 eprintln!("--iters expects a positive integer");
334 std::process::exit(2);
335 });
336 }
337 "--json" => json = true,
338 "--census" => census = true,
339 "--suite" => suite = true,
340 other if other.starts_with("--") => {
341 eprintln!("unknown flag: {other}");
342 eprintln!("usage: perf_probe [<file.ifc>...] [--suite] [--iters N] [--census] [--json]");
343 std::process::exit(2);
344 }
345 other => fixtures.push(other.to_string()),
346 }
347 }
348 if suite {
349 for f in SUITE {
350 fixtures.push((*f).to_string());
351 }
352 }
353 if fixtures.is_empty() {
354 eprintln!("usage: perf_probe [<file.ifc>...] [--suite] [--iters N] [--census] [--json]");
355 eprintln!(" no fixtures given; try --suite (uses catalogued models on disk)");
356 std::process::exit(2);
357 }
358
359 eprintln!(
360 "perf_probe: {} fixture(s), best-of-{}{}",
361 fixtures.len(),
362 iters,
363 if census { ", +csg-census" } else { "" }
364 );
365
366 let mut probes = Vec::new();
367 for f in &fixtures {
368 if let Some(p) = run(f, iters, census) {
369 print_human(&p);
370 probes.push(p);
371 }
372 }
373
374 if json {
375 print_json(&probes);
376 }
377
378 if probes.is_empty() {
379 eprintln!("\nno fixtures measured (all missing?). Fetch with: pnpm fixtures <path>");
380 std::process::exit(1);
381 }
382}