perf: prism pairs before the mesh, bodies measured once, and a STEP import that copies less (#714)

A cut or an intersection between prisms (a plate and the tools through it) is now decided in 2D before either operand is meshed. A body's size and volume are measured once, a placement reads its orientation from its matrix, the evaluation cache is bounded to 64 MiB, and STEP import copies fewer shells. Hole plates evaluate 20-30% faster with less memory, and models are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
SahenandClaude Opus 5.5 authored and GitHub committed 2026-10-13 10:55:56 -04:00
1 parent a1cb8d4d36
commit 2bb24d1d2a
13 files changed
+630 -71

No files matched your search

+122 -1
View File
@@ -1,5 +1,45 @@
# Design loading
## STEP import: shells and item solids without copies (2026-10-13)
A file that lists one product definition many times (a bolt used 70 times, a panel in several
assemblies) is imported item by item, and every item's B-rep was deep copied twice before it was
handed on: once by the caller of `Solid::try_new`, which clones the shells so that the failure
path still has them, and once by the reader's own cache, which kept the solid for a later item
that might list the same definition.
`read_item` now runs those four checks (non-empty, connected, closed, no singular vertices) on
borrowed shells, so a shell that passes is moved into the solid instead of cloned. The reader
keeps each prepared solid in an `Arc`, and an item counts as *once* when one representation
lists it and exactly one product-definition walk reaches it: that item's solid is moved out of
the cache with `Arc::try_unwrap` instead of copied, because no later item can ask for it. The
walk follows `MAPPED_ITEM` targets too, so an item reached through a mapping in another product's
node is still converted once - a second conversion would repeat that item's warnings. A file
whose walk exceeds the work budget (`MAX_CLOSURE_WORK`, 65 536 nodes) falls back wholesale to
the copying path, as does any file with component occurrences, which walks a definition again per
instance.
Nothing observable changed. Fifteen fixtures (primitives, a void, a fillet, a revolve, colours,
centimetre units, 70 duplicates of one definition, a broken file with mappings, a two-instance
assembly and an expanded one; 99 bodies) give the same import dump - name, path, closed flag,
file faces, face origin, topology and merged counts, bounding box, volume, area, centroid, paint,
warnings and tree - apart from the per-feature milliseconds, and all fifteen STEP in/out round
trips are byte-identical.
Two generated flat STEP files, timed like the table above (same binary pair, no other work
running); four alternating runs each, two for the 31 MB file:
| File | Baseline peak RSS | Patched peak RSS | Import |
|---|---:|---:|---:|
| 2500 boxes, 9.5 MB | 150.1 MB | 145.9 MB | 1.81 s → 1.74 s |
| 2500 filleted boxes, 31 MB | 428 MB | 433 MB | 70.3 s → 71.0 s |
The first row's ranges do not overlap (150.0-150.6 MB against 145.0-146.0 MB), and the saving is
the shell copy that is no longer made for every item. The second file's runs land between 422 MB
and 444 MB either way, so two runs each do not resolve it: there the conversion of the geometry
dominates the per-item copy. An item that keeps every conversion - an assembly, or a file over the
work budget - is unchanged.
## Parallel STEP body preparation (2026-10-12)
Profiling an original 48-cylinder STEP fixture before changing the importer measured
@@ -438,7 +478,7 @@ each body is identified by the evaluation that produced it (bodies a feature pas
unchanged keep their identity). The same feature on the same inputs is never evaluated twice:
setting a value back, undo, redo, a preview followed by the same command, and the repeated
evaluations while a script builds the design all reuse earlier results. The cache is shared by a
model and its copies (previews, scratch sessions) and keeps the last 4096 results.
model and its copies (previews, scratch sessions), and keeps the last 4096 results within 64 MiB.
An edit still recomputes the edited feature and every later feature whose starting model
changed; in these parts every later feature works on the edited body, so early edits recompute
@@ -450,6 +490,87 @@ its inputs are not exactly the old ones. Oracle 59's first parameter edit fails
expression the bench's `* 1.02` can't scale) and is left out.
## Prism pairs, body measurements and the evaluation cache budget (2026-10-13)
A design whose features repeat one shape (a hole pattern, a plate cut by many tools, a part built
by one growing body) pays several per-feature costs that a rebuild was paying again and again.
**A body's size is measured once, not once per call.** Every kernel tolerance is a fraction of
the body's bounding-box diagonal, and `Body::size()` walked every vertex of the solid to work it
out. `Body::new` already reads those vertices to orient the body, so it keeps the diagonal it
measured there; a body built by hand, or a mesh body, still measures once.
**The same volume is not measured twice.** A boolean measures both operands' volumes to bound its
result, and the body it returns becomes the next feature's operand, so a timeline of *n* cuts
meshed every intermediate body twice. `Body::volume_at(tol)` keeps the last measurement for the
solid, shared by its clones (they share the solid), and the bounds ask at the same tolerance every
time.
**A prism pair is decided in 2D before either operand is meshed.** The general boolean builds a
refined mesh of both operands before it can decide anything, and that mesh is most of its cost: a
pattern, or a sketch with several profiles, rebuilds the whole target's mesh once per tool. A
plate and a tool through it are prisms along one direction, which `prism::prism_boolean` decides
exactly from their 2D sections - and that route ran *after* the meshes. A cut or an intersection
now asks it first and returns without building them. A pair the route cannot answer takes the old
path unchanged, meshes and all; a union still asks after the sampled shortcut. One answer is sent
back to that shortcut on purpose: when the 2D route returns one of its operands (a tool that
misses its target, or covers it whole), the sampled path returns that very body, so an untouched
body keeps its identity in the timeline (`ops::prism_fast_path_tests`).
**A placement reads its orientation from its matrix.** Placing a body (a pattern instance, an
aligned copy) used to mesh the whole copy to decide which way its faces point and to count the
faces its coarse mesh misses. A rigid placement leaves both as they were; a mirrored one (a
negative determinant) turns the solid inside out. Both are now read from the matrix, so only the
geometry is transformed.
**The evaluation cache is bounded by the bytes it keeps.** It held the last 4096 results, and a
result is as big as the model it holds, so a long editing session kept every superseded state:
nothing dropped the entries nothing referred to any more. It now also holds at most 64 MiB, oldest
first, and always keeps the newest result, so a single evaluation is never thrown away in the same
breath.
Release `solidcraft-cli`, `eval <design> --quiet`, wall time of the whole process and peak RSS
from `/usr/bin/time -v`, alternating the baseline and patched binaries, median of 12 runs each
(8 for the 400-hole plate and the vent plate), no other build or test running (Fedora Linux, AMD
Ryzen 7 5700X, 8 cores / 16 threads, 32 GB):
| Design | Baseline | Patched | Change | Baseline peak RSS | Patched peak RSS |
|---|---:|---:|---:|---:|---:|
| 400 × 400 × 6 mm box, one sketch with 100 circles cut through it | 1.24 s | 0.83 s | -33% | 151 MB | 119 MB |
| The same plate with 400 circles in the sketch | 5.67 s | 4.04 s | -29% | 556 MB | 481 MB |
| Vent plate, a 10 × 8 pattern of one cut | 0.10 s | 0.08 s | -20% | 22 MB | 22 MB |
| 14 × 14 pattern of a filleted box (196 copies) | 6.70 s | 6.52 s | -2.6% | 67 MB | 67 MB |
| 60 sketches, 60 extrudes into one growing body | 8.80 s | 8.88 s | +0.9% | 225 MB | 225 MB |
| `examples/acceptance/sensor_enclosure.json` | 3.81 s | 3.30 s | -13% | 198 MB | 196 MB |
The hole plates are what the prism route is for: one boolean whose tool is a prism with 100 or 400
sections, in a sketch with that many circles, so the tool's mesh alone is what the route saves.
A variant of the patched tree with the route asked in the old order - after the operands' meshes -
timed the 100-hole plate at 1.27 s and 157 MB (range 1.22-1.30 s), the baseline's own numbers
(1.25 s and 156 MB over the same runs, range 1.21-1.33 s). The two plate rows, and the peak they
lose, are the prism route.
The last three designs are the other side: their booleans are not that case. The 196-copy design
and the 60-sketch design are within 2.6% and 0.9% with overlapping ranges. The sensor enclosure's
medians differ by 13%, but its runs land anywhere between 3.0 s and 5.0 s and the two ranges
overlap (3.01-4.97 s against 3.00-4.89 s), so its row is not a result either way and is left as
measured. The body measurements, the placement and the cache budget are not what these rows
measure: they stop work the corpus repeats too rarely to see (a hidden vertex walk, an operand
meshed twice for one number, an orientation mesh per placement, a cache with no byte bound).
No model changed. The evaluation dump of 21 designs (10 to 400 circles in a plate, 16 × 16 to
31 × 31 box grids with and without fillets, 2500-body grids, assemblies of 40 to 500 bodies, 30 and
60 sketches, a 196-instance pattern, the bundled sample and the sensor enclosure) is the same model
before and after - bodies, volumes, areas, centres, bounding boxes, topology counts and warnings -
apart from the per-feature milliseconds. The
kernel's test suite passes, including four new tests for the prism route and its identity rule.
Reproduce after compiling a release build, without other builds running: time
`target/release/solidcraft-cli eval <design> --quiet` with `/usr/bin/time -v`, alternating the
baseline and patched binaries, and compare the middle of the runs. The corpus (20 generated designs:
hole plates, box grids, assemblies, many-sketch parts) and the measuring scripts are in
https://github.com/sahenjp/solidcraft-perf under `perf/`.
## Desktop loading and recent thumbnails (2026-10-10)
The Arduino Uno enclosure (19 features, 34 KB) and Raspberry Pi 5 case (24 features,