1use std::cell::Cell;
2use web_time::Instant;
3
4#[derive(Default, Clone, Copy)]
5pub(crate) struct MassProfile {
6 pub enabled: bool,
7 pub faces: u64,
8 pub trimmed_faces: u64,
9 pub route_biperiodic: u64,
10 pub route_winding: u64,
11 pub route_seam_band: u64,
12 pub route_cap: u64,
13 pub route_wall: u64,
14 pub route_hopped_raw: u64,
15 pub route_hopped_unwrapped: u64,
16 pub route_straddle: u64,
17 pub route_periodic_raw: u64,
18 pub route_raw: u64,
19 pub route_untrimmed: u64,
20 pub route_affine: u64,
21 pub polygon_chords: u64,
22 pub base_cells: u64,
23 pub full_cells: u64,
24 pub full_stations: u64,
25 pub leaf_cells: u64,
26 pub tri_stations: u64,
27 pub corr_chords: u64,
28 pub corr_stations: u64,
29 pub polygons_ms: f64,
30 pub base_clip_ms: f64,
31 pub cells_ms: f64,
32 pub child_clip_ms: f64,
34 pub full_ms: f64,
36 pub leaf_ms: f64,
38 pub corr_ms: f64,
39 pub untrimmed_ms: f64,
40 pub gate_ms: f64,
41 pub rule_ms: f64,
45 pub rule_spans: u64,
46 pub rule_sampled: u64,
47 pub face_lines: bool,
49}
50
51thread_local! {
52 static PROFILE: Cell<MassProfile> = const { Cell::new(MassProfile {
53 enabled: false, faces: 0, trimmed_faces: 0, route_biperiodic: 0, route_winding: 0,
54 route_seam_band: 0, route_cap: 0, route_wall: 0, route_hopped_raw: 0,
55 route_hopped_unwrapped: 0, route_straddle: 0, route_periodic_raw: 0, route_raw: 0,
56 route_untrimmed: 0, route_affine: 0, polygon_chords: 0, base_cells: 0, full_cells: 0,
57 full_stations: 0, leaf_cells: 0, tri_stations: 0, corr_chords: 0, corr_stations: 0,
58 polygons_ms: 0.0, base_clip_ms: 0.0, cells_ms: 0.0, child_clip_ms: 0.0, full_ms: 0.0,
59 leaf_ms: 0.0, corr_ms: 0.0, untrimmed_ms: 0.0, gate_ms: 0.0, rule_ms: 0.0,
60 rule_spans: 0, rule_sampled: 0, face_lines: false,
61 }) };
62}
63
64pub(crate) fn mass_profile(update: impl FnOnce(&mut MassProfile)) {
67 PROFILE.with(|cell| {
68 let mut profile = cell.get();
69 if profile.enabled {
70 update(&mut profile);
71 cell.set(profile);
72 }
73 });
74}
75
76pub(crate) fn mass_profile_enabled() -> bool {
77 PROFILE.with(|cell| cell.get().enabled)
78}
79
80pub(crate) fn snapshot() -> MassProfile {
82 PROFILE.with(|cell| cell.get())
83}
84
85pub(crate) fn face_lines_enabled() -> bool {
86 PROFILE.with(|cell| cell.get().face_lines)
87}
88
89fn route_name(delta: &MassProfile) -> &'static str {
91 for (count, name) in [
92 (delta.route_affine, "affine"),
93 (delta.route_untrimmed, "untrimmed"),
94 (delta.route_biperiodic, "biperiodic"),
95 (delta.route_winding, "winding"),
96 (delta.route_seam_band, "seam_band"),
97 (delta.route_cap, "cap"),
98 (delta.route_wall, "wall"),
99 (delta.route_hopped_raw, "hopped_raw"),
100 (delta.route_hopped_unwrapped, "hopped_unwrapped"),
101 (delta.route_straddle, "straddle"),
102 (delta.route_periodic_raw, "periodic_raw"),
103 (delta.route_raw, "raw"),
104 ] {
105 if count > 0 {
106 return name;
107 }
108 }
109 "none"
110}
111
112#[allow(clippy::too_many_arguments)]
117pub(crate) fn emit_face(
118 call: &str,
119 shell: usize,
120 face: usize,
121 kind: &str,
122 degree_u: usize,
123 degree_v: usize,
124 closed_u: bool,
125 closed_v: bool,
126 loops: usize,
127 coedges: usize,
128 before: MassProfile,
129 wall_ms: f64,
130) {
131 let now = PROFILE.with(|cell| cell.get());
132 let d = MassProfile {
133 route_biperiodic: now.route_biperiodic - before.route_biperiodic,
134 route_winding: now.route_winding - before.route_winding,
135 route_seam_band: now.route_seam_band - before.route_seam_band,
136 route_cap: now.route_cap - before.route_cap,
137 route_wall: now.route_wall - before.route_wall,
138 route_hopped_raw: now.route_hopped_raw - before.route_hopped_raw,
139 route_hopped_unwrapped: now.route_hopped_unwrapped - before.route_hopped_unwrapped,
140 route_straddle: now.route_straddle - before.route_straddle,
141 route_periodic_raw: now.route_periodic_raw - before.route_periodic_raw,
142 route_raw: now.route_raw - before.route_raw,
143 route_untrimmed: now.route_untrimmed - before.route_untrimmed,
144 route_affine: now.route_affine - before.route_affine,
145 polygon_chords: now.polygon_chords - before.polygon_chords,
146 base_cells: now.base_cells - before.base_cells,
147 full_cells: now.full_cells - before.full_cells,
148 full_stations: now.full_stations - before.full_stations,
149 leaf_cells: now.leaf_cells - before.leaf_cells,
150 tri_stations: now.tri_stations - before.tri_stations,
151 corr_chords: now.corr_chords - before.corr_chords,
152 corr_stations: now.corr_stations - before.corr_stations,
153 polygons_ms: now.polygons_ms - before.polygons_ms,
154 base_clip_ms: now.base_clip_ms - before.base_clip_ms,
155 cells_ms: now.cells_ms - before.cells_ms,
156 child_clip_ms: now.child_clip_ms - before.child_clip_ms,
157 full_ms: now.full_ms - before.full_ms,
158 leaf_ms: now.leaf_ms - before.leaf_ms,
159 corr_ms: now.corr_ms - before.corr_ms,
160 untrimmed_ms: now.untrimmed_ms - before.untrimmed_ms,
161 gate_ms: now.gate_ms - before.gate_ms,
162 rule_ms: now.rule_ms - before.rule_ms,
163 rule_spans: now.rule_spans - before.rule_spans,
164 rule_sampled: now.rule_sampled - before.rule_sampled,
165 ..MassProfile::default()
166 };
167 eprintln!(
168 "mass.face call={call} shell={shell} face={face} kind={kind} deg={degree_u}x{degree_v} closed={}{} loops={loops} coedges={coedges} route={} chords={} base_cells={} full_cells={} full_stations={} leaf_cells={} tri_stations={} corr_chords={} corr_stations={} ms={:.3} gate_ms={:.3} polygons_ms={:.3} base_clip_ms={:.3} child_clip_ms={:.3} full_ms={:.3} leaf_ms={:.3} corr_ms={:.3} untrimmed_ms={:.3} cells_ms={:.3}",
169 if closed_u { "u" } else { "-" },
170 if closed_v { "v" } else { "-" },
171 route_name(&d), d.polygon_chords, d.base_cells, d.full_cells, d.full_stations,
172 d.leaf_cells, d.tri_stations, d.corr_chords, d.corr_stations, wall_ms, d.gate_ms,
173 d.polygons_ms, d.base_clip_ms, d.child_clip_ms, d.full_ms, d.leaf_ms, d.corr_ms,
174 d.untrimmed_ms, d.cells_ms,
175 );
176}
177
178pub(crate) fn profile_started() -> Option<Instant> {
180 mass_profile_enabled().then(Instant::now)
181}
182
183pub(crate) fn elapsed_ms(started: Option<Instant>) -> f64 {
184 started.map_or(0.0, |s| s.elapsed().as_secs_f64() * 1_000.0)
185}
186
187pub(crate) fn begin(call: &str) -> Option<(String, Instant)> {
190 let enabled = std::env::var("BREP_PROFILE").is_ok();
191 let face_lines = enabled && std::env::var("BREP_PROFILE_MASS_FACES").is_ok();
192 PROFILE.with(|cell| {
193 cell.set(MassProfile {
194 enabled,
195 face_lines,
196 ..MassProfile::default()
197 })
198 });
199 enabled.then(|| (call.to_string(), Instant::now()))
200}
201
202pub(crate) fn end(token: Option<(String, Instant)>) {
204 let Some((call, started)) = token else {
205 return;
206 };
207 let p = PROFILE.with(|cell| cell.get());
208 eprintln!(
209 "mass.profile call={call} ms={:.2} faces={} trimmed={} routes: affine={} untrimmed={} biperiodic={} winding={} seam_band={} cap={} wall={} hopped_raw={} hopped_unwrapped={} straddle={} periodic_raw={} raw={} | chords={} base_cells={} full_cells={} full_stations={} leaf_cells={} tri_stations={} corr_chords={} corr_stations={} | gate_ms={:.2} polygons_ms={:.2} base_clip_ms={:.2} cells_ms={:.2} child_clip_ms={:.2} full_ms={:.2} leaf_ms={:.2} corr_ms={:.2} untrimmed_ms={:.2} rule_ms={:.2} rule_spans={} rule_sampled={}",
210 started.elapsed().as_secs_f64() * 1_000.0,
211 p.faces, p.trimmed_faces, p.route_affine, p.route_untrimmed, p.route_biperiodic,
212 p.route_winding, p.route_seam_band, p.route_cap, p.route_wall, p.route_hopped_raw,
213 p.route_hopped_unwrapped, p.route_straddle, p.route_periodic_raw, p.route_raw,
214 p.polygon_chords, p.base_cells, p.full_cells, p.full_stations, p.leaf_cells,
215 p.tri_stations, p.corr_chords, p.corr_stations, p.gate_ms, p.polygons_ms,
216 p.base_clip_ms, p.cells_ms, p.child_clip_ms, p.full_ms, p.leaf_ms, p.corr_ms,
217 p.untrimmed_ms,
218 p.rule_ms,
219 p.rule_spans,
220 p.rule_sampled,
221 );
222 PROFILE.with(|cell| cell.set(MassProfile::default()));
223}
224
225thread_local! {
230 static CALLER: Cell<Option<&'static str>> = const { Cell::new(None) };
232}
233
234pub struct MassCaller(Option<&'static str>);
244
245impl Drop for MassCaller {
246 fn drop(&mut self) {
247 CALLER.with(|cell| cell.set(self.0));
248 }
249}
250
251pub fn mass_caller(name: &'static str) -> MassCaller {
252 MassCaller(CALLER.with(|cell| cell.replace(Some(name))))
253}
254
255fn current_caller() -> &'static str {
256 CALLER.with(|cell| cell.get()).unwrap_or("unlabelled")
257}
258
259pub(crate) struct IdentityDump {
274 directory: Option<String>,
276}
277
278thread_local! {
279 static DUMP_SEQUENCE: Cell<u64> = const { Cell::new(0) };
283}
284
285pub(crate) fn identity_dump() -> Option<IdentityDump> {
288 let directory = std::env::var("BREP_PROFILE_MASS_DUMP").ok();
289 if directory.is_none() && std::env::var("BREP_PROFILE_MASS_IDENTITY").is_err() {
290 return None;
291 }
292 Some(IdentityDump { directory })
293}
294
295pub(crate) fn emit_identity(
299 dump: IdentityDump,
300 call: &str,
301 solid: &crate::BrepSolid,
302 values: &[(&str, f64)],
303) {
304 let started = Instant::now();
305 let identity = super::identity::face_set_identity(solid);
306 let hash_ms = started.elapsed().as_secs_f64() * 1_000.0;
307 let sequence = DUMP_SEQUENCE.with(|cell| {
308 let next = cell.get() + 1;
309 cell.set(next);
310 next
311 });
312 let printed: Vec<String> = values
313 .iter()
314 .map(|(name, value)| format!("{name}={:016x}", value.to_bits()))
315 .collect();
316 let mut path = String::new();
317 if let Some(directory) = dump.directory {
318 let file = format!("{directory}/{sequence:05}-{call}.json");
319 match serde_json::to_string(solid) {
320 Ok(text) => {
321 if let Err(error) = std::fs::write(&file, text) {
322 eprintln!("mass.dump write failed for {file}: {error}");
323 } else {
324 path = file;
325 }
326 }
327 Err(error) => eprintln!("mass.dump serialize failed for {file}: {error}"),
328 }
329 let meta = format!(
333 "{{\"call\":\"{call}\",\"seq\":{sequence},\"faces\":{},\"hash\":\"{}\",\"values\":\"{}\"}}",
334 identity.faces,
335 identity.hex(),
336 printed.join(" "),
337 );
338 let meta_file = format!("{directory}/{sequence:05}-{call}.meta.json");
339 if let Err(error) = std::fs::write(&meta_file, meta) {
340 eprintln!("mass.dump write failed for {meta_file}: {error}");
341 }
342 }
343 eprintln!(
344 "mass.identity call={call} caller={} seq={sequence} faces={} hash={} hash_ms={hash_ms:.3} {}{}",
345 current_caller(),
346 identity.faces,
347 identity.hex(),
348 printed.join(" "),
349 if path.is_empty() {
350 String::new()
351 } else {
352 format!(" path={path}")
353 },
354 );
355}