Skip to content

Evaluate each distinct body once, parse the stage once and index used_by per inspection - #184

Merged
Blogbotana merged 7 commits into
masterfrom
refactor/issue-26-inspection-cache
Oct 23, 2026
Merged

Blogbotana merged 7 commits into
masterfrom
refactor/issue-26-inspection-cache

Conversation

@axisrow

@axisrow axisrow commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Ported from the fork, where it is merged and running: axisrow/BeamCraft#53 (branch bug/26-inspection-cache, merged there on 2026-10-23). Refs the fork's issue 26, finding 2 — the remaining cost inside the one inspection that every read command runs. Every reply is byte-for-byte unchanged by design.

What this changes

  1. Body cache (beamcraft_engine::query::committed::BodyCache, used per thread by beamcraft_cli::plate::evaluate_decoded). Kernel bodies are kept by the exact bits of their complete shape inputs: contour, thickness, holes and cut of a plate; axis, roll, cut and resolved profile content of a member.
    • Id, label, kind, placement, material and the profile digest are not inputs. Tests check that bodies differing only in those are equal before they may share an entry.
    • No digest stands in for an input. -0.0 and 0.0 are different keys; the P04 signatures fold them, so they are not reused as keys.
    • The cache is in-process only (owner decision), bounded at 32 MiB and MAX_ELEMENTS entries, cleared when full, and never keeps a failure. A command's read of the head, its commit verifier and the copies of one model share bodies.
  2. used_by index: definitions builds the material-to-plates map once instead of scanning every plate for every listed definition, on every fit_page attempt.
  3. One parse (beamcraft_layer_engine::Reader::open_validated). inspect_revision and the commit verifier parsed and composed the stage twice (validate_layers, then Reader::open). Now the same rules run on the candidate that is then installed, with the same order, codes and messages.

Trust is unchanged. Received layer blobs are still verified against the manifest, structurally validated, indexed and decoded, and definition digests are recomputed. verify_derived still compares every stored mesh with the body, cached or not. A new test warms the cache, commits a structurally valid but wrong stored mesh for a copy, and requires INTEGRITY_ERROR with the body served from the cache.

Port notes

  • Base: upstream master (236ce9b, including Fix K1 plate contours, body frames and unsupported USD drafts #174). The four topic commits were cherry-picked from the fork; the conflicts were adapted to the current master state: plate_users sits directly above the documented pub(crate) fn definitions_json, and the layer-engine files (Reader::open_validated sharing one implementation of the rules with validate_layers) and the test follow the fork's merged form (beamcraft_json, ElementId::prim_path markers).
  • The evaluation stays lenient. The fork's strict cached evaluation had to be reworked for upstream: since Fix K1 plate contours, body frames and unsupported USD drafts #174 a revision can hold valid authored-only (unsupported) plates, and a strict per-plate evaluation inside evaluate_decoded made the commit verifier refuse exactly the revisions the draft flow is meant to store (reproduced with a near-planar rectangular plate from contour_plates.rs). The cache now enters through evaluate_committed_drafts_cached (engine), which mirrors evaluate_committed_drafts per plate — same checks, same order, same unsupported classification — and the uncached entry point delegates to it with a disabled cache. The second pass of an inspection reuses the same thread's cache instead of evaluating every body twice.
  • The K1 plate_draft evolution that master gained since the fork's measurement base is untouched.

Measured, not assumed

Whether to remove the duplicate passes was decided in runs (owner decision): body cache (C), one parse (P) and one verified read of each layer blob (R) were measured in all eight combinations plus the base, against a decision rule fixed beforehand. C and P were kept; R had no measurable effect. The design note has the full tables: docs/design/issue-26-inspection-cache.md.

Before/after on the fork (Linux container, 4 vCPU Xeon 2.8 GHz, rustc 1.97.0, the dist release profile, fixtures of contributor-original synthetic stores, alternating runs; every output byte-identical between the two binaries; medians in ms):

Fixture plate-inspect definitions view-export --out - copy-element (write path)
F1 copies (300) 821 → 401 (−51 %) 708 → 351 (−50 %) 731 → 341 (−53 %) 1,920 → 1,389 (−28 %)
F2 distinct plates (300) 754 → 471 (−38 %) 656 → 400 (−39 %) 687 → 420 (−39 %) 1,791 → 1,488 (−17 %)
F3 frame (295 el., 59 bodies) 14,177 → 1,424 (−90 %) 14,100 → 1,377 (−90 %) 13,686 → 1,350 (−90 %) 28,201 → 4,004 (−86 %)
F4 distinct members (200) 3,016 → 2,801 (−7 %) 2,999 → 2,676 (−11 %) 2,897 → 2,718 (−6 %) 6,560 → 3,921 (−40 %)

Max RSS moves by at most +0.3 MiB and falls by up to 2.8 MiB on the frame (one parse holds one stage, not two). The figures were taken in the fork's environment and were not re-measured for this port; the code paths are the same.

How you verified it

On the fork (original PR, Linux): clean rebuild, cargo fmt --all -- --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace (1,247 passed), fixtures verify, beamcraft-boundaries check, cargo doc -D warnings, wasm32 check, cargo deny check; plus byte-for-byte comparison of every output of plate-inspect, definitions, view-export --out - and copy-element on the four fixtures.

This port (macOS, aarch64): cargo fmt --all -- --check, cargo clippy --workspace --all-targets -- -D warnings and the full cargo test --workspace — green.

New tests:

  • crates/engine/src/query/committed.rs: six unit tests — cached bodies equal fresh ones for boxes, contours, holes and cuts, and for rectangle and I-section members; every shape input misses; excluded fields provably do not change the body; signed zeros are different keys; refusals are not kept; the budget and the disabled cache behave as documented.
  • apps/beamcraft-cli/tests/inspection_cache.rs: cold and warm inspections are equal with hit and miss counts asserted; a warm cache still refuses a structurally valid but wrong stored mesh with INTEGRITY_ERROR, as do a cold thread and the CLI.
  • apps/beamcraft-cli/tests/definitions.rs: used_by lists, their element-id order, the plates-only rule and paging.
  • crates/layer-engine/tests/authored.rs: open_validated against validate_layers + Reader::open — same result on a valid stage, same refusal on broken definition, mesh, schema-marker and parse inputs and an unknown schema pin, live view kept after a refusal.

Never-crash

  • No new unwrap/expect/panic!/todo! outside tests. The thread-local cache uses try_with and try_borrow_mut and falls back to a cache of the call.
  • No indexing or slicing with input-derived positions. Allocations sized by input are capped: the cache budget counts key and body bytes, and the entry count is capped at MAX_ELEMENTS.
  • No unsafe

Provenance

  • Nothing taken from Tekla Structures or any Trimble product (assets, code, documentation, tables)
  • No GPL/AGPL code copied in
  • No new asset, nothing to add to ATTRIBUTION.md

Issue tracking

Fixes #372

claude and others added 6 commits October 23, 2026 10:27
Every read command re-evaluated every plate and member body with the
kernel, and every write command did so twice: once for the head and once
in its commit verifier. A body depends on its shape inputs alone, so
copies and repeated parts paid the kernel again and again (issue 26,
finding 2).

BodyCache keeps kernel bodies by the exact bits of their complete shape
inputs: contour, thickness, holes and cut of a plate; axis, roll, cut
and resolved profile content of a member. Identity, label, kind,
placement, material and the profile digest are not inputs, and tests
check that bodies differing only in them are equal before they may share
an entry. No digest stands in for an input and -0.0 and 0.0 are
different keys (the P04 signatures fold them). The cache is bounded,
clears itself when full and never keeps a failed evaluation.

The CLI keeps one cache per thread behind evaluate_decoded, so a read of
the head, the commit verifier of the same command and the copies of one
model share bodies. Every stored mesh is still compared with the body it
returns: a tampered derived layer is refused with a warm cache too.

Measured in the variant experiment (release build, Linux, 4 vCPU): on a
295-element frame with shared sections, holes and cuts, plate-inspect
went from 12.7 s to 1.7 s and copy-element from 27.7 s to 3.9 s; on 200
distinct I-section beams copy-element went from 6.4 s to 4.1 s; reads
with nothing shared did not change.

Claude-Session: https://claude.ai/code/session_01EEyRacqmnZp4d6Ahazivae
definitions scanned every plate for every listed definition to build
used_by, and repeated the scan on every fit_page halving (issue 26,
finding 2). Build the material-to-plates map once from the plates in
element-id order and look each definition up in it.

The lists, their order and the plates-only rule are unchanged; a new
test pins them with several materials, an unused material, a member
that pins a material and a profile, and a paged listing.

Claude-Session: https://claude.ai/code/session_01EEyRacqmnZp4d6Ahazivae
inspect_revision and the commit verifier ran validate_layers, which
parses and composes the whole stage, and then Reader::open, which parses
and composes it again (issue 26, finding 2). On simple models that
second pass was the largest single cost of an inspection.

Reader::open_validated runs the same structural rules on the very
candidate it then installs: the same checks in the same order, with the
same codes and messages, and nothing is installed unless every rule
passed. A test compares it with the two calls on a valid stage and on
broken definition, mesh, schema-marker and parse inputs.

Measured in the variant experiment (release build, Linux, 4 vCPU), with
the body cache on: plate-inspect 522 -> 313 ms on 300 copied plates,
609 -> 380 ms on 300 distinct plates and 1.71 -> 1.30 s on a 295-element
frame. A third variant, reading each layer blob once instead of twice,
showed no measurable effect in any pair and is not part of this change.

Claude-Session: https://claude.ai/code/session_01EEyRacqmnZp4d6Ahazivae
The design note gives the fixtures, the eight-way experiment with its
decision rule, the before/after numbers and the phase breakdown, the
trust argument, why no persistent cache is built, and what is left.

Claude-Session: https://claude.ai/code/session_01EEyRacqmnZp4d6Ahazivae
The port initially reused the fork's strict cached evaluation inside
evaluate_decoded. On upstream that regressed the K1 draft flow: a near-planar
rectangular plate saved as an authored-only draft was then refused by the
commit verifier, because the strict path rejects every plate the kernel
cannot evaluate instead of listing it as unsupported (upstream #174 made such
revisions possible; the fork never stored them, so its CI could not see this).

evaluate_decoded keeps the lenient upstream semantics and gains the cache
through a new engine::plate_draft::evaluate_committed_drafts_cached, which
mirrors evaluate_committed_drafts per plate and classifies refusals with the
same draft rules; the uncached entry point delegates to it with a disabled
cache. The second pass of an inspection reuses the same thread's cache
instead of paying for every body twice.
@axisrow
axisrow requested a review from Blogbotana as a code owner October 23, 2026 03:44

@axisrow axisrow left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of PR #184 — changes requested\n\nReviewed the diff against master (236ce9b): the BodyCache and its key encodings, the cached draft evaluation, the used_by index, Reader::open_validated, the CLI wiring, the design note and the new/extended tests. CI is green (test both OSes, wasm32, deny, cold builds).\n\nThe cache itself is sound: keys are injective byte encodings of the complete shape inputs (raw f64 bits, length prefixes, variant tags), no digest stands in for an input, -0.0 and 0.0 are distinct, refusals are never kept, the byte budget and entry cap clear before overflow, and the disabled cache bypasses everything. The thread-local use in evaluate_decoded is re-entrancy-safe (try_with + try_borrow_mut with a per-call fallback). open_validated runs the same rules on the candidate it installs and keeps the live view on refusal; the new test asserts refusal parity and that the earlier view survives. plate_users preserves the previous used_by order and paging, with a test. The tamper test proves a warm cache still fails verify_derived closed (INTEGRITY_ERROR, zero new misses).\n\n### Requires change\n\n1. The plate read path lost the identity-rotation refusal (inline). On master, evaluate_committed_drafts refused a committed plate with a non-identity placement (UNSUPPORTED_GEOMETRY, placement.orientation_xyzw, "rotated placement is not evaluated in this slice") via create_plate_draft → create_plate, so every consumer of evaluate_decoded refused such a base. The cached fast path checks the placement only with Placement::validate (quaternion validity — the engine test in query/committed.rs asserts evaluate_committed accepts a rotated plate), so a supported-contour rotated plate now evaluates. inspect_revision still refuses it later through scene_elements_with_members, but copy-element, edit, delete, create-plate/create-member on such a base and the commit verifier now proceed where master refused the whole command — and a copy would propagate the rotation into a new revision. It is only reachable via a store the app itself cannot produce (draft admission refuses rotations), but it is an undocumented weakening of an untrusted-input guard and contradicts the port note "same checks, same order, same unsupported classification". Either mirror the guard in the fast path with the same error, or state the relaxation as deliberate in the port notes and the design note.\n\n### Notes (no change required)\n\n- The duplicate-id refusal of evaluate_committed_drafts changed text and position: master returned create_plate-s DuplicateId/new_id/"element id is already owned" after the full validate_arguments; now it is DuplicateId/id/"element id appears twice" before the finer argument checks. That matches the convention of evaluate_committed and is arguably better, but it is a reply-text change on malformed stores — worth a line in the PR description next to the byte-for-byte claim.\n- For the same reason, which refusal surfaces first can change when several malformations coexist (for example members now evaluate before the snapshot build refuses the rotated plate). Both outcomes are refusals; only the reported error differs.\n- solid_bytes counts the blob and separately the vertices and triangles; if the blob is their serialization this double-counts and merely clears the cache earlier — harmless and bounded.\n

Comment thread crates/engine/src/plate_draft.rs Resolved
The cached fast path evaluated a committed plate whose placement carries a
rotation, where create_plate refuses it: every read built on such a base and
the commit verifier stopped failing closed. Mirror the guard after the cached
evaluation with the same error, delegate the uncached entry point through the
same loop, and pin both behaviours with a test.

@axisrow axisrow left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of PR #184 (head 6fedbe7) — approving\n\nRe-reviewed the delta since the previous review (3b57f98 → 6fedbe7): one commit, crates/engine/src/plate_draft.rs, +61 lines. The rest of the diff is unchanged and stands as reviewed.\n\nThe required change is resolved correctly:\n\n- The cached fast path now refuses a committed plate whose placement carries a rotation, with the exact error create_plate raises (UNSUPPORTED_GEOMETRY / placement.orientation_xyzw / "rotated placement is not evaluated in this slice"), so every consumer of evaluate_decoded fails closed again, as on master.\n- The placement of the guard preserves the order of effects of create_plate: the body is evaluated (and may be cached — harmless, the refusal happens before it is used), then the rotation refusal, then quantities. The fallback branch keeps going through create_plate_draft, whose classification still requires an identity rotation.\n- The uncached entry point delegates to the same loop, so both paths refuse identically; the new test pins the exact code, field and message, plus the identity neighbour evaluating cached and uncached.\n\nThis also makes the port note "same checks, same order" accurate again for the rotation case.\n\nNotes (no change required):\n\n- The PR description predates the fix commit: the port notes and the test list do not mention the new guard and its test in plate_draft.rs. A one-line addition to the description would keep the advertised test inventory complete.\n- The duplicate-id refusal text change from the previous review remains as agreed (no change required): DuplicateId/id/"element id appears twice", aligned with evaluate_committed, differing from master only on malformed stores.\n- CI at the time of this review: cold builds, wasm32 and deny green; the ubuntu and windows test jobs of this head were still running. The change is one guard plus one test and the port was verified green locally per the description.\n\nVerdict: approved.\n

@Blogbotana
Blogbotana merged commit d9a288c into master Oct 23, 2026
6 checks passed
axisrow referenced this pull request in axisrow/BeamCraft Oct 23, 2026
…-section mesh picking (#187), inspection cache used_by index (#184), craft coordination docs (#186), assembly op_id replay (#183), K1 plate contour cleanup (#174)

The upstream squashes carry the same work the fork already has under its own
SHAs, plus the promotion adaptations: the doc fixes from the #187/#188 review
round, the used_by index widening #184, and the #184 design doc rewritten for
the upstream provenance. All six conflicts are the double-landing of promoted
work, so the upstream versions are taken as canonical per the 1:1 contract:
apps/beamcraft-cli/src/plate.rs, apps/beamcraft-cli/src/view_export.rs,
apps/beamcraft-cli/tests/inspection_cache.rs,
crates/engine/src/query/pick.rs, docs/design/issue-26-inspection-cache.md,
docs/design/v04-picking-snaps.md. Nothing fork-exclusive was dropped
(diff master..upstream/master checked per file before the resolution).

Co-Authored-By: Claude Code <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PR #184: Evaluate each distinct body once, parse the stage once and index used_by per inspection

3 participants