| name | profiling |
| description | Use when investigating or fixing performance in brepkit: a benchmark got slower, an operation (boolean, fuse, tessellation) is unexpectedly slow or hangs, a criterion bench times out or shows wild variance, or a PR needs before/after perf numbers. Covers flamegraphs, the criterion bench suite, cross-kernel comparison, and the perf bug classes this codebase has actually had. |
Profiling and Performance Debugging
The bar
brepkit must beat the reference kernel on performance, not merely pass tests. Perf regressions are release blockers. CI runs boolean_tracking on a shared runner and only comments on regressions over 200% (.github/workflows/benchmark.yml, fail-on-alert: false), so the automated gate is looser than the real bar. You are the gate: any PR touching a hot path pastes before/after criterion numbers in its body. Cross-kernel claims require ./scripts/bench-compare.sh ~/Git/brepjs output, not native-only numbers (see the parity-benchmarking skill).
Quick reference
cargo bench-fast
cargo bench-full
cargo bench -p brepkit-operations --bench boolean_perf -- "N=64"
cargo flamegraph --profile profiling --bench cad_operations -p brepkit-operations \
-o /tmp/flamegraph.svg -- --bench "<filter>"
cargo run --profile profiling --example profile_boolean -- honeycomb
cargo run --profile profiling --example tess_profile
./scripts/bench-compare.sh ~/Git/brepjs
The [profile.profiling] block in the workspace Cargo.toml is release optimization plus debug symbols with lto = false: fast rebuilds, full symbol names in flamegraphs. Always profile with it, never with plain release (LTO mangles frames) or dev (measures nothing real).
Bench files (crates/operations/benches/), one line each:
| Bench | Covers |
|---|
cad_operations.rs | Head-to-head suite (fuse(box,box) x10, mesh sphere (tol=0.01), tessellate 64-hole plate, ...); paired with brepjs/benchmarks/kernel-comparison.bench.test.ts via the NAME_MAP table in scripts/bench-report.ts, so a new head-to-head bench must be added to that table or it will not appear in the comparison report |
boolean_perf.rs | Boolean scaling: sequential_cylinder_cuts/N={4,16,64} and single_boolean_at_face_count |
boolean_tracking.rs | Small fast suite CI tracks for trend (boolean/cut_cylinder_through_box, boolean/perforated_cut_36) |
compound_cut_perf.rs | compound_cut vs sequential cuts: cylinder grids and honeycomb grids |
fuse_perf.rs | fuse_all tree-reduce vs sequential left-fold (fuse_balanced has balanced_N= and sequential_N=; fuse_touching has only balanced_N=) |
Method: the perf loop
- Baseline.
cargo bench -p brepkit-operations --bench <file> -- "<filter>". Expect a line like sequential_cylinder_cuts/N=64 time: [x.xx s x.xx s x.xx s]. Save it verbatim. Criterion stores history in target/criterion/, so later runs print change-% automatically.
- Isolate. Narrow the filter to one benchmark ID. If the bench seems to hang or shows wild run-to-run variance, do not fight criterion: write a plain timed loop that runs the op N times on fresh
Topology instances and prints per-iteration wall clock. Huge iteration spread is itself the diagnosis (see bug class B).
- Flamegraph that exact workload. Use the quick-reference command with the same filter, or for boolean work use
--example profile_boolean -- <scenario> (scenarios: honeycomb, cylinders, fuse, large-honeycomb, scale, xl, tess; the example header documents them). Open the SVG in a browser and look for wide frames. Checkpoint: if the widest frames are in mesh_boolean on analytic inputs, stop, that is the bug (class D).
- Fix. Vary one variable at a time. Match the symptom to a known class first (table below) before inventing a new theory.
- Re-run the same bench with the same filter. Criterion prints the delta against the stored baseline. Checkpoint: expect
change: [-XX% ...] Performance has improved. If the number moved less than the flamegraph suggested, the wide frame was not on the measured path; re-check the filter.
- PR body. Paste both criterion lines verbatim. For scaling fixes, also add a deterministic complexity-guard test so the win cannot regress on noisy runners; pattern:
scaling_perforated_cut_is_subquadratic in crates/operations/src/boolean/tests.rs. Historical absolute times are machine-specific; never write them into tests as thresholds, assert growth ratios instead.
Known perf bug classes
| Symptom | Likely cause | Fix shape | Detail |
|---|
| N-tool boolean scales worse than linear; each cut/fuse slower than the last | Pairwise accumulation: acc = fuse(acc, next) pays growing cost every step in GFA (the general-fuse boolean engine in crates/algo), O(N²) total | Batch it: compound_ops::fuse_all (bbox-partition + disjoint merge + tree-reduce) or boolean::compound_cut; boolean() already fast-paths provably disjoint Fuse | reference.md, class A |
| Same op varies 100x+ between identical runs; criterion "hangs"; results differ across runs | HashMap iteration order feeding downstream branching; the random seed sometimes samples a pathological path | Sort (and dedup) any collection derived from HashMap iteration before it drives decisions | reference.md, class B |
| Preview/mesh path slow; triangle counts explode | Deflection too fine for the use; re-tessellating at export tolerance | Coarse deflection for preview; drill down with tess_profile | reference.md, class C |
Flamegraph dominated by mesh_boolean on analytic inputs | B-Rep path bailed via error-variant fallthrough into the mesh fallback | Fix the error handling, not the algorithm; the fallthrough is the bug | reference.md, class D |
Anti-patterns: what NOT to conclude
- A hung or 100x-variance bench does NOT mean "the algorithm is slow". Rule out nondeterminism (class B) first with the fresh-Topology timed loop.
- A green CI benchmark run does NOT mean no regression. The alert threshold is 200% and it only comments. Compare criterion output yourself.
- Native criterion wins do NOT prove "beats the reference kernel". Only
bench-compare.sh output does; wasm behaves differently.
- Do NOT profile with
--release (LTO destroys symbols) or trust dev-profile timings at all.
- Do NOT "fix" a scaling problem by making each pairwise call faster. If cost grows with accumulator size, the fix is batching or disjointness detection, not micro-optimization.
- Do NOT paste historical numbers from old PRs as expected timings. Re-measure on the current machine and commit.
- Do NOT speed up tessellation by loosening deflection inside core geometry or booleans. Mesh approximation in core paths is a fidelity bug (see the tessellation and analytic-preservation skills).
Deep detail
- reference.md: full command catalog, bug classes A-D with verified symbols and rg patterns,
bench-compare.sh pipeline, profiling-example internals, glossary.
- Sibling skills: parity-benchmarking (cross-kernel harness), tessellation (deflection semantics), boolean-debugging (when the slow path is also wrong), debugging-doctrine (bisection discipline), pr-workflow (getting the numbers merged).