# 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 3.17 ms parsing, 6.06 ms reconstructing B-reps and 45.58 ms preparing/orienting bodies. The five complete imports had a median of 59.21 ms. The expensive orientation mesh, rather than the text parser, was the first parallelization target for #287. Desktop import now prepares consecutive independent solids in a representation with up to eight workers. Each batch contains at most 64 solids. Parsing, B-rep reconstruction, assembly traversal, first-use unit resolution, naming and result collection remain ordered. Mapped items end a batch. Fewer than four solids use the serial path; wasm always uses it. Thread creation failure falls back to serial work, and kernel/worker failures remain per-body errors reported by the importer. Windows x64, AMD Ryzen AI Max+ 395 (16 cores / 32 logical processors), optimized `ci` profile, no concurrent build: three fresh test processes per fixture, five alternating serial/parallel pairs in each process. These are complete uncached kernel imports, including parsing and body preparation; fixture generation/export and later desktop display meshing are outside the timer. Both paths use the same input text and code. | Original generated fixture | Bodies | Serial median | 8 workers median | Reduction | |---|---:|---:|---:|---:| | Cylinders, radius 5 mm, height 12 mm | 48 | 57.54 ms | 17.38 ms | 69.8% | | Cylinders, radius 5 mm, height 12 mm | 512 | 896.31 ms | 313.06 ms | 65.1% | | Tori, major radius 20 mm, minor radius 4 mm | 48 | 269.05 ms | 51.90 ms | 80.7% | | Boxes, 10 × 15 × 12 mm | 512 | 247.97 ms | 90.15 ms | 63.6% | | Boxes, 10 × 15 × 12 mm | 4 | 1.49 ms | 1.02 ms | 31.5% | | One cylinder (serial fallback) | 1 | 1.22 ms | 1.29 ms | serial fallback | There are 15 samples per cell. The single-cylinder modes run identical serial code; their 0.07 ms median difference is within the overlapping 1.11–1.57 / 1.13–1.52 ms ranges. The 512-cylinder runs ranged from 567.40–983.61 ms serial and 197.72–345.51 ms parallel; scheduling/thermal variation is material. These synthetic results do not establish a speedup for every assembly or complete desktop Open. In particular, parts containing only one solid are not parallelized across assembly nodes. Cross-part scheduling, parallel B-rep reconstruction and later display meshes remain open. Reproduce after compiling, without other builds running: ```sh cargo test -p solidcraft-kernel --profile ci benchmark_independent_step_solids --no-run SOLIDCRAFT_STEP_BENCH_COUNT=512 SOLIDCRAFT_STEP_BENCH_KIND=cylinder \ cargo test -p solidcraft-kernel --profile ci benchmark_independent_step_solids \ -- --ignored --nocapture --test-threads=1 ``` Use `torus` or `box` for the other fixtures and repeat in three fresh processes. `SOLIDCRAFT_STEP_BENCH_FIXTURE` optionally writes the original STEP for native inspection. No timing threshold runs in CI. Regressions compare serial and threaded imports with different worker counts: exact display meshes, measures, body/face source identities, names, colours/opacity, units, rotated assembly placements, duplicates and partial failures across the batch boundary. The public analytic/FreeCAD reference suite also checks STEP export/import; its current results are reported in docs/oracle.md. ## Design storage: container and binary payloads (#303, phase one) Design containers avoid formatting/parsing imported mesh coordinates as JSON numbers. The metadata serializer replaces embedded fields before creating JSON values, without cloning the full document or duplicating its schema. Binary coordinates retain `f64` precision, indices retain `u32`, and SHA-256 deduplicates identical payloads. The reader checks archive budgets, CRCs, hashes, exact lengths, finite coordinates and index bounds. Reproduce the storage measurements with an optimized `ci` IO test executable: ```sh cargo test -p solidcraft-io --profile ci benchmark_ -- --ignored --nocapture --test-threads=1 ``` The benchmarks generate an original 100,000-vertex/100,000-triangle mesh and a STEP containing 512 disjoint 10 mm boxes. Fixtures are written under `CARGO_TARGET_DIR` (or the crate's `target` directory). Each compares nine encoding/decoding samples; atomic-save samples include encoding, temporary-file flush, replacement and `.bak` creation and alternate JSON/container ordering. Documents are compared after decoding. These are storage measurements, separate from timeline evaluation or first-frame latency. On Windows x64, based on upstream `e6e0a7e` before STEP parallelism (#312), three consecutive optimized runs without concurrent compilation gave the following medians of the three nine-sample medians (milliseconds, fast mode): | Fixture / operation | JSON | Container | |---|---:|---:| | Mesh size, bytes | 15,139,115 | 3,600,828 | | Mesh encode | 23.336 | 6.094 | | Mesh decode | 57.175 | 2.962 | | Mesh atomic save | 52.924 | 14.856 | | STEP size, bytes | 1,998,793 | 1,950,056 | | STEP encode | 1.507 | 1.473 | | STEP decode | 1.479 | 1.318 | | STEP atomic save | 7.213 | 7.087 | Three alternating fresh-process `solidcraft-cli bench-open` runs of each STEP encoding returned the same 512 bodies and 6,144 triangles. Median read-plus-parse was 2.274 ms for JSON and 2.151 ms for the container; median evaluation was 218.559/236.915 ms and display meshing 203.160/197.790 ms respectively. Geometry dominates and these variable results do not establish an end-to-end STEP opening improvement. Fresh processes do not imply a cold operating-system file cache, and these are not desktop first-frame times. Fast mode stores STEP and mesh payloads directly. Explicit `"compression":"compact"` compresses them at Deflate level one; it is a space/CPU tradeoff. In the initial 512-box STEP experiment, compression reduced 1,998,793 JSON bytes to 578,659 package bytes but increased median encoding from 1.68 to 5.02 ms and decoding from 1.76 to 4.18 ms. That regression is why compression is not the default. PNG/JPEG are stored directly in either policy; small parametric metadata uses Deflate level one. Plain JSON remains a supported Save As/export format; `.json` saves keep JSON and scripts may explicitly select either format. The CLI detects packages before attempting JSON parsing. Desktop/browser recovery and browser downloads also use the container. See [the versioned container format](design-container.md) for layout and bounds. This phase does not restore evaluated B-reps, skip STEP import, or make cold geometry rebuilds instant. A matching cache key is not enough to trust unvalidated geometry. The native B-rep increment described below supplies provisional display, not editable session restoration. It is separate from #287's import parallelism. ## Opt-in mesh preview measurements (#303, phase one) The phase-one desktop implementation can show an experimental saved picture while the file worker rebuilds geometry. It requires an explicit `preview: true` engine Save/Save As call. Ordinary saves omit display meshes to avoid the measured overhead; these measurements predate native B-rep caching. The cache contains only already warm evaluated display meshes, capped at 8 MiB, 1,024 bodies and 100,000 triangles. Larger or cold models save without it. Cached geometry is never used for editing, measurements or picking. Source/document fingerprints and payload validation reject stale or malformed caches; cancellation and document/revision checks prevent a background result from replacing newer work. The cached picture is prepared on the file worker from the same file snapshot as the normal Open command. It adds work before reconstruction, rather than bypassing it, so measure preview readiness and full editing readiness separately. Cached display also adds file size and save work; the storage-only results above exclude that optional data. For an engine-level save benchmark, first generate the original 512-box STEP fixture with the ignored IO benchmark, then run the warmed-session benchmark: ```sh cargo test --workspace --profile ci benchmark_step_storage -- --ignored --nocapture --test-threads=1 cargo test --workspace --profile ci benchmark_warm_design_save_with_preview -- --ignored --nocapture --test-threads=1 ``` Set `CARGO_TARGET_DIR` to the workspace's absolute target directory in both commands. The engine benchmark alternates nine JSON/container atomic saves, includes the optional display cache, and verifies both preview decoding and authoritative document identity. Three runs on Windows, with no concurrent build or test suite, measured median atomic JSON saves of 7.679/7.610/7.630 ms and container-with-preview saves of 10.319/10.633/10.541 ms. Preview decoding took 0.964/0.961/1.130 ms; the container was 2,510,490 bytes. This optional preview costs save time on this STEP fixture. An actual desktop sensor-enclosure opening on e8a8e0f prepared its saved picture in 24.320 ms; geometry reconstruction, display warmup and thumbnail preparation took 8,913.212 ms. The same preview experiment is now explicitly opt-in; full geometry readiness still requires reconstruction. For actual desktop opening phases, launch with `SOLIDCRAFT_PROFILE_OPEN=1`: `ready_ms` reports saved-picture preparation, while the normal phase log reports reconstruction, display warmup and thumbnail completion. Neither log measures the exact GPU first frame. ## Native cache opening (#303) The portable native B-rep cache is opt-in with `cache: true` on warm container saves, without duplicating display meshes. Ordinary saves and browser downloads default to `cache: false`: the measurements below do not justify imposing the additional save time and file size on every design. Desktop Open reads metadata and optional geometry through a seekable file, produces a bounded provisional picture, then resolves original sources and reconstructs the complete session before editing. This improves the opportunity for early display, not the cost of authoritative feature evaluation. An absent, invalid or incompatible cache uses the original reconstruction. Browser OPFS/downloads share the format; browser opening still rebuilds synchronously. The reproducible engine benchmarks separate atomic saving, provisional mesh readiness, reconstruction and final display-mesh readiness: ```sh cargo test -p solidcraft-io --profile ci benchmark_step_storage -- --ignored --nocapture --test-threads=1 cargo test -p solidcraft-engine --profile ci benchmark_native_cache_save -- --ignored --nocapture --test-threads=1 cargo test -p solidcraft-engine --profile ci benchmark_native_cache_open -- --ignored --nocapture --test-threads=1 ``` Use an absolute `CARGO_TARGET_DIR`. The save benchmark defaults to the generated 512-box STEP document and accepts `SOLIDCRAFT_BENCH_FILE` for another public design. It alternates nine saves of each encoding, including flush/replacement/backup, and verifies document identity and native-cache presence. It never times initial fixture evaluation as save work. Open runs once per process; `SOLIDCRAFT_BENCH_ENCODING` selects 0=JSON, 1=container without cache, 2=native cache, 3=compact native cache. Changing only `cache/key.json`'s application version provides a cache-miss control. Fresh processes do not imply a cold operating-system file cache. The engine callback time is provisional mesh readiness, not a rendered desktop frame; use the desktop profile log and actual screenshots to check the saved picture and completed model. Measured 2026-10-13 on Windows 11, Ryzen AI MAX+ 395 (16 cores/32 threads), `ci` profile, with the neutral codec revision `75381fa`, envelope `SCBREP03`, producer `solidcraft-truck-adapter/2`, and opening integration revision `3243422` on upstream `2880525` (0.6.0). These replace the earlier prototype measurements. They compare encodings on the same code and do not attribute upstream modelling improvements to the cache. Runs started after local builds and the complete CI suite finished; ordinary desktop activity remained. Save values are medians of nine alternating atomic saves. Open values are medians of five fresh processes per encoding, alternating forward/reverse order. Memory is each process's peak working set from Windows `GetProcessMemoryInfo`, sampled every 10 ms, then the median across those five processes; it excludes a desktop window/GPU. The original STEP fixture is a 32 by 16 array of 10 mm cubes, 12 mm apart, exported by SolidCraft: 512 bodies and 6,144 final triangles. Its JSON embeds about 2 MB of STEP. Every encoding reopens without feature errors and with those same counts. | STEP encoding | Bytes | Atomic save, ms | |---|---:|---:| | JSON | 1,998,826 | 12.616 | | Container, no cache | 1,950,084 | 10.516 | | Native cache, fast | 4,694,035 | 47.755 | | Native cache, compact | 837,067 | 44.015 | | STEP encoding | Provisional mesh, ms | Reconstructed, ms | Final meshes, ms | Peak working set, MiB | |---|---:|---:|---:|---:| | JSON | none | 175.842 | 748.911 | 40.102 | | Container, no cache | none | 175.885 | 722.306 | 40.504 | | Native cache, fast | 62.099 | 228.794 | 747.976 | 40.883 | | Native cache, compact | 65.981 | 235.403 | 762.120 | 40.918 | | Native cache miss | none | 169.039 | 647.617 | 40.453 | The public `examples/acceptance/sensor_enclosure.json` script, saved as a JSON design, has 21 features, two bodies and 4,364 final triangles on this revision. | Sensor enclosure encoding | Bytes | Atomic save, ms | |---|---:|---:| | JSON | 52,281 | 5.243 | | Container, no cache | 5,545 | 4.969 | | Native cache, fast | 379,095 | 12.646 | | Native cache, compact | 72,603 | 14.111 | | Sensor enclosure encoding | Provisional mesh, ms | Reconstructed, ms | Final meshes, ms | Peak working set, MiB | |---|---:|---:|---:|---:| | JSON | none | 3,979.063 | 3,984.593 | 70.438 | | Container, no cache | none | 4,038.127 | 4,042.006 | 71.789 | | Native cache, fast | 241.541 | 4,269.124 | 4,274.272 | 70.465 | | Native cache, compact | 236.739 | 4,271.254 | 4,275.159 | 70.148 | | Native cache miss | none | 4,136.517 | 4,140.903 | 69.941 | Native display is available earlier, at the cost of encoding, file size and work before reconstruction. It does not make the full design editable earlier, and these results do not establish a memory reduction. Final-mesh readiness varied from 611 to 856 ms for STEP and 3.84 to 4.60 s for the enclosure; small differences in full-opening medians should not be read as modelling speedups. Compact storage reduces this STEP example's size and atomic-save time, but costs slightly more save time on the enclosure. The default `cache: false` retains the phase-one save path; `compression: "compact"` is an explicit space/CPU choice. Larger or unsupported geometry can exceed the documented budgets and reopen without a provisional image. A separate desktop check used a fresh 0.6.0 application process for each enclosure encoding, with no local compilation or test suite running. The native cache's provisional picture was prepared at 429 ms and captured visibly during opening; the completed model was adopted in 6.48 s. JSON had no provisional picture and completed in 6.08 s. These are single visual checks, not repeated timing medians; the worker reported 6.10/5.66 s respectively, and external observations include UI scheduling and screenshot requests. During native opening, 94 control-channel inspections had a median round trip of 20.8 ms and a maximum of 106.5 ms (JSON: 95 inspections, 20.3/36.9 ms). The provisional picture is explicitly labelled as being checked before editing; the final frame shows the rebuilt Case and Lid and their editable feature timeline. This demonstrates early feedback, not faster completion of feature evaluation or a worst-case UI-latency guarantee. ## Analytic extrusion source preparation New-body extrusion certificates now prepare the sketch's bounded analytic segments once per certification pass instead of allocating segments and resolving their points for each generated cap boundary and side face. Saved designs run this same code during cold rebuilds. Exact containment tolerances, ambiguous-source rejection and the certification work budget are unchanged. Prepared segments borrow an immutable evaluated sketch and are not retained across edits. The reproducible microbenchmark is: ```sh cargo test -p solidcraft-doc --profile ci benchmark_prepared_extrude_sources -- --ignored --nocapture ``` Three Windows x64 optimized `ci` runs, during other compilation, compared the previous segment reconstruction loop and the prepared matcher in the same process. For 1,000 analytic lines and 1,000 generated face segments, median time fell from **45.90 ms to 12.30 ms** (73% less, about 3.7x faster). Preparation is included and every returned source identity is compared. This measures certification only, not complete extrusion, file-open latency or Boolean operations; it does not establish an end-to-end loading speedup. Remaining cold geometry rebuild and import costs are tracked in [#255](https://github.com/bherbruck/solidcraft/issues/255); certification work is [#254](https://github.com/bherbruck/solidcraft/issues/254). ## Cold geometry rebuilds after 0.3.1 Three alternating cold-process comparisons on the same Windows machine and optimized `ci` profile, with no concurrent build, compare the unchanged 0.3.1 source with the straight-line projection and exact surface-refinement reuse changes: | Saved design | Before, median evaluation | After | Reduction | |---|---:|---:|---:| | Public sensor enclosure, 21 features, 52 KB | 11.96 s | 8.85 s | 26.0% | | Local Pi case assembly, 25 features, 2,990 bodies | 14.75 s | 11.57 s | 21.6% | These are geometry rebuild times, not complete desktop opening times. The public fixture can be generated with `solidcraft-cli run examples/acceptance/sensor_enclosure.json --save sensor.solidcraft --quiet`, then measured with `solidcraft-cli bench-open sensor.solidcraft`. Each benchmark starts a new process; this does not measure a warm timeline-cache hit. The local assembly is private and is not part of the repository. Straight segments now use guarded algebraic closest-point projection instead of sampling 65 points and running Newton iterations. Other curve types retain their existing behavior. Curved-face refinement reuses derivative and midpoint evaluations with exact finite input keys and bounded, local caches. It keeps the original tolerance, seam and singularity checks, refinement passes, and triangle limits. Identity assembly placements retain the definition body's shared display-mesh cache. Changed placements continue to build separate meshes in their own coordinates. Face-name preparation remains eager during placement: deferring it merely shifted expensive work to thumbnails and the first face query, so that approach was excluded. Both saved fixtures retain exactly the same body names, face/edge/vertex/shell counts, bounds, volumes, and surface areas. Focused tests compare cached and uncached refinement coordinates and normals bit for bit on periodic, singular and transformed surfaces. For desktop phase profiling, set `SOLIDCRAFT_PROFILE_OPEN=1` before launching the app. Interactive Open reports geometry, placement, tessellation, thumbnail and total worker times to stderr without logging the source path. Thumbnails were about 0.1 seconds of the slow assembly's open and were not the main bottleneck. Further cold Boolean and import costs remain; these changes do not make all documents open instantly. ### Prepared assembly geometry and selection Prepared world geometry is now keyed by the document, evaluated model and exact placement context instead of the session revision. Selecting a face or edge, changing visibility, or moving the view no longer discards already prepared assembly bodies and meshes. Geometry edits, pending occurrence moves, authoring context changes, undo/redo and timeline rollback still invalidate the cache. Scratch sessions share prepared geometry while their inputs match, and adopting a computed session retains its warm cache. On the private 2,990-body Pi assembly, a matched Linux desktop control probe reduced an edge-selection round trip from 9.24 s to 0.57 s. Initial opening stayed essentially unchanged (33.54 s to 33.32 s); this fixes a separate selection stall, not cold geometry evaluation. Three fresh Windows desktop runs with the corrected cache measured a median 15.87 s to ready: 13.04 s geometry, 2.20 s placement, 0.53 s meshing and 0.07 s thumbnails. These used the optimized `ci` profile and actual Vulkan rendering, with isolated settings and caches. Linux and Windows timings are separate measurements and must not be compared as a speedup. `solidcraft-cli bench-open design.solidcraft` reports JSON lines for file reading, JSON decoding and migrations (including input-name repair), timeline evaluation, individual features, and world placement/display meshing. `--parse-only` stops before geometry evaluation. The parse timer includes document validation and repair, not just the JSON parser. Windows x64, optimized `ci` profile, measured during other builds: | Design | Parse/repair | Evaluate | Display meshes | |---|---:|---:|---:| | 19-feature enclosure, 34 KB | 1.2 ms | 14.9 s | 6 ms | | 25-feature assembly, 15.2 MB, 2,990 bodies | 21 ms | 12.5 s | 2.1 s | | Generated 1,000 independent boxes, 185 KB | 5 ms | 0.82 s | 0.15 s | | Generated 5,000 boxes in five grids, 10 features | 0.45 ms | 17.6 s | 0.81 s | The enclosures spend most of evaluation in joins and cuts, not parsing. Designs save feature definitions; opening them rebuilds their geometry. Those times remain after the repair fix. A separate generated timeline with missing input names reproduced minutes in the reader: | Timeline (geometry rolled back) | Parse/repair before | After | |---|---:|---:| | 2,000 boxes, 6,000 unnamed inputs | 78.1 s | 10–20 ms | | 5,000 boxes, 15,000 unnamed inputs | interrupted after 131 s, still repairing | 26–46 ms | Previously, each missing input restarted the `d1`, `d2`, … search, scanning the document for every candidate. This approaches cubic work as unnamed inputs accumulate. Loading now reserves names once and assigns missing names with an advancing cursor. Existing names, names on later features, parameter collisions and reuse of surplus names retain the previous behavior. Regression tests compare the old and bulk repairs and load a 5,000-feature unnamed timeline. # Viewport performance Measured with the control channel (`ui.inspect` `frame_ms`: CPU time of one UI frame, which includes picking under the cursor) on the sample plate patterned 20 × 20 (400 bodies, about 600k triangles), 1600 × 1000 window under Xvfb, release build. Hover: 60 cursor moves across the model; orbit: a right-drag. | | hover median | hover p90 | orbit median | |---|---|---|---| | before | 18.7 ms | 37.5 ms | 21.0 ms | | after | 1.9 ms | 2.4 ms | 2.6 ms | | after, Browser hidden | 0.4 ms | — | 0.4 ms | What changed: - **Picking culls by bounding box.** Per-body boxes are cached until the model changes. The face raycast skips bodies whose box the ray misses or enters behind the nearest hit so far. Edge and vertex tests skip bodies whose box, projected on screen, is not under the cursor. - **Picks are reused** while the cursor, camera and model are unchanged, and **no picking runs while the view moves** (orbit, pan, zoom). - **The model extent is cached** (it sets the view's depth range and was recomputed from every mesh several times per frame). With 49 bodies the frame was already 2.8 ms (hover) before these changes. The remaining frame time with the Browser shown is mostly the Browser listing every body row. GPU ID-buffer picking was not needed at this size. ## Browser Same model and method (400 bodies, Bodies folder open, 60 hover moves over the viewport): | | median | p90 | |---|---|---| | Browser shown, before | 1.95 ms | 2.71 ms | | Browser shown, after | 0.72 ms | 1.19 ms | | Browser hidden | 0.30 ms | 0.72 ms | What changed: rows scrolled out of view only reserve their height. They register no widgets and paint nothing (no text layout, icons or eye/fold buttons), so the cost follows the rows on screen rather than the rows in the tree. Scrolling, Find in Browser and drag and drop still see every row's place. # Recompute performance `solidcraft-cli bench-edit