Skip to main content

brep_kernel/props/mass_properties/
profile.rs

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    /// Hierarchical child clipping inside the quadtree (a subset of `cells_ms`).
33    pub child_clip_ms: f64,
34    /// Tensor Gauss stations over fully covered cells (a subset of `cells_ms`).
35    pub full_ms: f64,
36    /// Triangle cubature over depth-limit leaf cells (a subset of `cells_ms`).
37    pub leaf_ms: f64,
38    pub corr_ms: f64,
39    pub untrimmed_ms: f64,
40    pub gate_ms: f64,
41    /// Choosing the panels and orders of a span from its weights
42    /// (`rule`), and how many spans had to read a weight at all: the cost of
43    /// the rational rule's decision, apart from the stations it then asks for.
44    pub rule_ms: f64,
45    pub rule_spans: u64,
46    pub rule_sampled: u64,
47    /// Emit one `mass.face` line per face (`BREP_PROFILE_MASS_FACES`).
48    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
64/// Read-modify-write the thread-local profile when it is enabled; a no-op
65/// (one flag read) otherwise.
66pub(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
80/// The live counters, for a before/after delta around one face.
81pub(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
89/// Which route the face just took, read off the counter that moved.
90fn 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/// One `mass.face` line: the face's surface kind and trim size beside the
113/// route it took, the stations it cost and where its milliseconds went.
114/// `before` is [`snapshot`] taken before the face's integration, `wall_ms`
115/// the face's whole elapsed time. Only called when `face_lines` is armed.
116#[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
178/// Milliseconds since `started` when profiling is on; `None` otherwise.
179pub(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
187/// Start a profiled call: resets the counters and arms them when
188/// `BREP_PROFILE` is set. Returns whether profiling is on.
189pub(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
202/// End a profiled call: print the line and disarm the counters.
203pub(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
225// ---------------------------------------------------------------------------
226// Sign-check attribution: the identity of the face set a call integrated.
227// ---------------------------------------------------------------------------
228
229thread_local! {
230    /// The call site that is asking, for the duration of one integrator call.
231    static CALLER: Cell<Option<&'static str>> = const { Cell::new(None) };
232}
233
234/// Names the call site for every integrator call made while it is alive, so
235/// the `mass.identity` line can say WHO paid for the integral rather than
236/// leaving a log reader to infer it from the neighbouring profile lines.
237///
238/// Diagnostic only: nothing but the identity line reads it, and the identity
239/// line only exists under `BREP_PROFILE_MASS_IDENTITY` /
240/// `BREP_PROFILE_MASS_DUMP`. The guard restores the previous name on drop so
241/// a labelled site nested inside another labelled site reports itself and
242/// then hands the outer name back.
243pub 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
259/// One line per integrator call naming the CONTENT of the face set it read
260/// (`BREP_PROFILE_MASS_IDENTITY`), and optionally the solid itself written to
261/// disk (`BREP_PROFILE_MASS_DUMP=<dir>`, which implies the line).
262///
263/// This is the instrument behind "how many sign checks integrate a face set a
264/// later measurement integrates again". The 2026-09-13 integrator record
265/// paired them by STATION SIGNATURE — face count and station counts — and said
266/// plainly that two congruent solids would match too. A content hash settles
267/// that: equal hashes mean the two calls saw the same geometry in the same
268/// order, so the second one's answer was already computed.
269///
270/// It is a measurement lever, not a product path: hashing every solid costs
271/// its control points, which is why it is off unless asked for, and why the
272/// timing tables in the record are taken with it OFF.
273pub(crate) struct IdentityDump {
274    /// `None` unless `BREP_PROFILE_MASS_DUMP` named a directory.
275    directory: Option<String>,
276}
277
278thread_local! {
279    /// Per-process call counter so a dump's file name orders the calls the way
280    /// stderr does. Thread-local like the rest of this module; the populations
281    /// this instrument serves are single-threaded replays.
282    static DUMP_SEQUENCE: Cell<u64> = const { Cell::new(0) };
283}
284
285/// Read the two switches. Returns `None` when neither is set, which is the
286/// only cost on the shipped path.
287pub(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
295/// Emit the identity line for a finished call, and write the solid when a dump
296/// directory was named. `values` are the raw f64 bits the call returned, in
297/// the order the probe reads them back.
298pub(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        // A sidecar beside the solid so the probe needs only the directory:
330        // the stderr line carries the same fields, but correlating a probe run
331        // against a captured log is one more thing to get wrong.
332        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}