Skip to content

perf(test): derive the census RTC frame once per model, not once per host (#4127) - #4158

Open
louistrue wants to merge 2 commits into
mainfrom
fix/4127-census-hoist-rtc
Open

perf(test): derive the census RTC frame once per model, not once per host (#4127)#4158
louistrue wants to merge 2 commits into
mainfrom
fix/4127-census-hoist-rtc

Conversation

@louistrue

Copy link
Copy Markdown
Collaborator

Part of #4127.

The heavy census lane has never completed. Both runs it has ever had were cancelled at
the 60-minute cap with no census row emitted. The cause is a performance regression in
the test harness, and this is the fix for it. The workflow-side reporting change is a
separate PR.

Measured

Exact workflow invocation, cargo test -p ifc-lite-geometry --features triangulation-alt --test triangulation_invariance -- --ignored --nocapture --test-threads=1 heavy_fixture:

before   751.21 s
after      6.76 s

Both fixtures. That is faster than the pre-regression baseline too, because #3933 was
not the only per-host cost.

Cause

sweep() calls process() twice per void host and a third time for a torn one, roughly
1300 times across the two heavy fixtures. Since #3933 each call went through
census_rtc::router, which per call built an entity index over every record and then
walked every record again calling has_geometry_by_name to collect the geometry jobs.
On ISSUE_053 that is 2.8 M entities, twice, per call.

Fix

The frame is a pure function of the file, so derive it ONCE per model. ModelFrame holds
the Arc<EntityIndex> and the RTC offset; sweep() builds one before the host loop.

The router is deliberately NOT hoisted. It carries per-host state (mapped_item_cache,
item_dedup_cache, geometry_hash_cache, content_sig_memo, voids_consumed_hosts,
plus the CSG-failure and host-opening diagnostics), and sharing it would let information
carry from host N to host N+1. Every host still gets a fresh GeometryRouter::with_units
and a fresh EntityDecoder; only the read-only index and the offset are shared.

That is what answers the golden question by construction rather than by hoping.

The golden did not move

  • Census run rows are byte-identical before and after, md5-matched, on BOTH fixtures.
  • ISSUE_053: 289 void hosts, 0 torn. ISSUE_068: 363 hosts, 26 torn, matching
    ISSUE_068_KNOWN_TORN_HOSTS.
  • Both: regressed : 0, coverage loss : 0, added : 0, reclassified : 0,
    retessellated : 0, volume moved : 0.
  • The committed golden is untouched. Bless mode was never armed.

An independent reviewer reproduced this: their HEAD rows md5-match mine, and they
separately measured the pre-change code's ISSUE_053 rows as identical to the post-change
rows.

Why dropping the per-host offset pass is safe

resolve_scaled_placement, sample_element_translation,
detect_rtc_offset_with_fallback and get_placement_transform_from_element are all
&self with no borrow_mut; they mutate only the decoder's id-keyed memos. The subtle
case: the old pass warmed placement_transform_cache BEFORE set_rtc_offset was called.
Had it baked the offset into a cached transform, the old harness would have been carrying
rtc-less transforms into meshing. It does not: rtc_offset is read at mesh-transform
time, so the cache is a pure function of placement id and a warm cache equals a cold one.

The offset is also independent of the alt-triangulator toggle (neither sampler
tessellates), which matters because the frame is now built outside the base/alt toggling.

ModelFrame binds its index to its content

ModelFrame<'a> STORES the content; decoder() and router() take no content
parameter. That, not the lifetime, is what makes a wrong pairing unrepresentable: two
distinct Strings can both yield &'a str for one 'a, so a re-added parameter would
type-check against a different string, and the index holds absolute byte spans that
EntityDecoder slices unchecked. Demonstrated by compiling the bad shape, not argued.

What is still per host, stated because the first draft did not

with_units calls scan_unit_scale, which walks to the first IFCPROJECT. On the two
heavy fixtures that is byte 4,246 and 5,070, so it costs nothing on the lane this fixes.
But some exporters emit IFCPROJECT last, and three corpus fixtures under
MAX_FIXTURE_BYTES do: duplex.ifc at 2,380,634 of 2,380,763; dental_clinic.ifc at
9,496,389 of 13,003,205; 01_BIMcollab_Example_ARC.ifc at 6,252,152 of 18,230,149.

Hoisting the unit scale is declined here on purpose: with_scale_and_rtc does not call
arm_content_dedup() the way with_units does, so a naive swap silently disarms item
content-dedup for the census, which is exactly the class of change this PR must not make.

Verification

cargo test --workspace --no-fail-fast                    exit 0
cargo clippy --workspace --all-targets -- -D warnings    exit 0
per-PR census lane                                       64 passed, regressed : 0

Test harness only. No kernel change, no changeset (no packages/*).

🤖 Generated with Claude Code

https://claude.ai/code/session_01NfAavu3wBCfAPxEsv9BJ4K

…host (#4127)

The heavy watertightness census lane has never finished a scheduled run. Since
#3933 every `process()` call in `triangulation_invariance` went through
`census_rtc::router`, which built an entity index over the whole file, walked
every entity again calling `has_geometry_by_name` (2,807,815 records for
ISSUE_053) to collect geometry jobs, and only then sampled placements for the
RTC offset. The sweep makes two of those calls per void host plus a third for a
torn one, so the two heavy fixtures paid that O(file) work 1330 times
(2 * 652 hosts + 26 torn).

Both hoisted values are pure functions of the file, so they move out of the
per-host loop into one `ModelFrame` per model: the entity index (handed to each
host's decoder as the same `Arc`, which is the store `with_index` builds anyway)
and the RTC offset.

The router is deliberately NOT hoisted. It carries per-host caches and
bookkeeping (mapped items, item dedup, geometry hashes, consumed-void hosts,
CSG failure diagnostics), so sharing one across hosts would be a question about
census rows rather than about speed. Every host still gets a fresh
`GeometryRouter` and a fresh `EntityDecoder`; only the read-only index and the
offset are shared. Nothing outside `rust/geometry/tests/` changes.

ONE per-host O(file) walk survives, and the module doc now says so instead of
reading as "nothing per-host is O(file)". `ModelFrame::router` calls
`GeometryRouter::with_units`, whose `scan_unit_scale` reads entities from the
start of the file until it finds `IFCPROJECT`. Usually that stops in the first
few KB (ISSUE_053: byte 4246 of 177 MB). Measured, three gated fixtures where
it does not, because the exporter emitted `IFCPROJECT` last:

  ara3d/duplex.ifc                      byte 2,380,634 of  2,380,763
  ara3d/dental_clinic.ifc               byte 9,496,389 of 13,003,205
  various/01_BIMcollab_Example_ARC.ifc  byte 6,252,152 of 18,230,149 (line 100819)

The unit scale is not hoisted here either, and that is a decision rather than an
oversight: the only way to reuse it is `GeometryRouter::with_scale_and_rtc`,
which unlike `with_units` does not call `arm_content_dedup()`. In
rust/geometry/src/router/mod.rs, `with_units` and `with_units_and_rtc` are the
only two callers of `arm_content_dedup`. Swapping it in would silently disable
item content-dedup for the census and could move rows, which is the one thing
this change must not do. It needs its own argument about dedup.

`ModelFrame` now borrows the content it was built from and hands it back out
itself rather than taking a `&str` per call. The index holds absolute byte spans
and `EntityDecoder::get_raw_bytes` slices them unchecked, so
`ModelFrame::new(&a)` followed by `process(&frame, &b, ..)` compiled and would
have decoded the wrong entities or panicked out of bounds. PROBED: a frame that
outlives its content is now E0597 at compile time. The lifetime also drops seven
redundant `&content` / `&ifc` arguments across the call sites.

MEASURED, this machine, the workflow's exact invocation
(`cargo test -p ifc-lite-geometry --features triangulation-alt --test
triangulation_invariance -- --ignored --nocapture --test-threads=1
heavy_fixture`), debug/test profile, both heavy fixtures:

  before (0546d79): 751.21 s   after: 6.8 to 7.0 s over three runs

THE GOLDEN DID NOT MOVE. `watertightness_census_heavy.tsv` is untouched and both
lanes still gate against it with `regressed : 0` and `coverage loss : 0`.
Stronger than that: the per-fixture run rows this sweep writes to
`target/watertightness_census.heavy.*.tsv` are BYTE-IDENTICAL over all 652 hosts
(289 + 363, 26 torn on ISSUE_068), before the perf change, after it, and after
the lifetime refactor:

  b54873d5ab75a8176ca79e4620ed58ce  watertightness_census.heavy.ISSUE_053_20181220Holter_Tower_10.tsv
  3d121119264ae66a8a525056d95b6908  watertightness_census.heavy.ISSUE_068_ARK_NUS_skolebygg.tsv

The per-PR census lane (`--features triangulation-alt` without `--ignored`) is
also green: 64 passed, 0 failed.

Two corrections to #4127. The 5.5x figure understates the cost, because it
compared against 4417f9a, which already rebuilt the entity index per call;
against the current lane the sweep is over 100x faster. And the fix is not
"hoist the router and entity index" as the issue suggests: hoisting the router
would have changed what the census can see.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NfAavu3wBCfAPxEsv9BJ4K
@cursor

cursor Bot commented Sep 8, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_bd4dbf15-49d0-4afe-8e16-2c8fa1a21795)

@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown

Warning

Review limit reached

  • Run on-demand review

This review includes 2 billable files and costs up to $0.50.

Or wait 7 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 6e4b245d-d471-4aa5-a4d4-ce5811a21136

📥 Commits

Reviewing files that changed from the base of the PR and between ab17a7c and 915aab6.

📒 Files selected for processing (2)
  • rust/geometry/tests/census_rtc/mod.rs
  • rust/geometry/tests/triangulation_invariance.rs

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 8, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-08T09:51:34.680382Z 55797eb PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Claude review - no findings for 55797ebef

Reviewed this diff and found nothing to flag.

@github-actions github-actions Bot added the llm-reviewed A review was verified as posted for this PR's head. label Sep 8, 2026
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Viewer benchmark

✅ No threshold regressions detected.

01_Snowdon_Towers_Sample_Structural(1).ifc

Baseline recorded 2026-07-01T20:31:05.538Z on github-actions ubuntu-latest, viewer-benchmark-ci (headless Chrome, SwiftShader ANGLE), production build.

Metric Current Baseline Delta Threshold Status
firstBatchWaitMs 2858ms 2905ms -1.6% +50%
firstVisibleGeometryMs 3354ms 3652ms -8.2% +50%
streamCompleteMs 3539ms 3598ms -1.6% +50%
spatialReadyMs 1215ms 1032ms +17.7% +50%
metadataCompleteMs 1818ms 3063ms -40.6% +50%
totalWallClockMs 3700ms 3700ms 0.0% +50%

AC20-FZK-Haus.ifc

Baseline recorded 2026-07-01T20:30:59.972Z on github-actions ubuntu-latest, viewer-benchmark-ci (headless Chrome, SwiftShader ANGLE), production build.

Metric Current Baseline Delta Threshold Status
firstBatchWaitMs 372ms 1075ms -65.4% +50%
firstVisibleGeometryMs 1293ms 1572ms -17.7% +50%
streamCompleteMs 1138ms 1980ms -42.5% +50%
spatialReadyMs 867ms 915ms -5.2% +50%
metadataCompleteMs 1079ms 1392ms -22.5% +50%
totalWallClockMs 1400ms 3300ms -57.6% +50%

Refresh the baseline from a CI run: dispatch the Benchmark workflow with record_baseline, download the benchmark-baseline artifact, and commit baseline.json (see tests/benchmark/README.md).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 55797ebef4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +81 to +83
let index = Arc::new(build_entity_index(content));
let mut decoder = EntityDecoder::with_arc_index(content, Arc::clone(&index));
let offset = detect_rtc_offset(content, &mut decoder);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Record the census speedup in the performance ledger

This is a measured performance change—the commit reports the heavy census dropping from 751.21 s to 6.76 s—but the patch leaves scripts/perf/README.md unchanged. Record the verdict and lesson there in this PR so future optimization work does not repeat this investigation, as required for measured performance changes.

AGENTS.md reference: AGENTS.md:L74-L74

Useful? React with 👍 / 👎.

@vercel

vercel Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated
ifc-lite-dev Ignored Ignored Sep 8, 2026 10:21am UTC
ifc-lite-viewer-embed Ignored Ignored Sep 8, 2026 10:21am UTC

@cursor

cursor Bot commented Sep 8, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_ff6d56ca-1869-43cc-8fdc-f32aa17308db)

@github-actions github-actions Bot removed the llm-reviewed A review was verified as posted for this PR's head. label Sep 8, 2026
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Claude review - no findings for 915aab65b

Reviewed this diff and found nothing to flag.

@github-actions github-actions Bot added the llm-reviewed A review was verified as posted for this PR's head. label Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

llm-reviewed A review was verified as posted for this PR's head.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant