Skip to content

feat(viewer): the loading card shows the decoder's own progress, and a model too large for the tab says so (#1195) - #1297

Merged
DrunkOnJava merged 10 commits into
mainfrom
feat/1195-viewer-progress-too-large
Nov 3, 2026
Merged

DrunkOnJava merged 10 commits into
mainfrom
feat/1195-viewer-progress-too-large

Conversation

@DrunkOnJava

@DrunkOnJava DrunkOnJava commented Nov 3, 2026 •

Copy link
Copy Markdown
Owner

Summary

The viewer's loading card shows the decoder's own progress (the WalkerControl stages rvt-gltf --progress prints) instead of a sweep and an "about N s" guess from the file size, and says plainly when a model is too large for a browser tab instead of surfacing RuntimeError: unreachable. Revised after review: the progress tests read recorded reports rather than sampling, the out-of-memory classifier is not V8-only, the advice names what really reads a larger file, and a failure after the large-model retry says nothing about memory unless it is memory.

Motivation

#1195. Deploy viewer's runner takes 17 to 24 s to open Core Interior, which the "about N s" hint (11 MB/s, a laptop's figure) puts at 3 s, and a model past wasm32's 4 GiB fails with a bare trap.

Changes

  • src/ifc/mod.rs, src/reader.rs: RvtDocExporter::export_with_diagnostics_mode_limits_and_control (the diagnostics export with a WalkerControl; the old method calls it with the default). RevitFile reports each first inflate of a Partitions/* stream to the control of the export in progress as PartitionScan (set and cleared around export_rvt_doc; held as AssertUnwindSafe so the handle stays UnwindSafe), because the first pass over the partitions inflates all of them and has no control of its own. rvt-gltf --progress gets the same lines.
  • rvt-wasm/src/lib.rs, Cargo.toml, Cargo.lock: openRvtBytesWithDiagnosticsModeAndOpenLimits takes an optional fourth argument, a JS function (stage, done, total), throttled to a stage's start, end and each whole percent. Not a breaking change (tools/ci/wasm-api.py compat and docs pass).
  • viewer/src/worker.ts: posts { type: 'stage', ... } from inside the wasm call. A failure is classified as memory when it is a RuntimeError matching /unreach/i with no rvt-rs wasm panic: line before it (V8 unreachable, Firefox unreachable executed, Safari Unreachable code should not be executed), or a panic or engine message naming an allocation. Any other panic is sent with its own message. lastPanic is reset just before the open call (and after a swallowed fileMetadata failure). The stop is blamed on the decoder's last report inside the open call, and on the worker's step (scene graph, glTF, schedule) after it. The engine's message goes to the page.
  • viewer/src/main.ts, index.html, styles.css:
    • The bar is determinate while a report has a count (candidate scan, element decode, each partition stream inflated, saved meshes) and sweeps for the rest. The page records, with each report, what the card showed after applying it (data-load-progress).
    • The stretch the decoder reports nothing in is named "Building the model" (and "Finishing the model" after the saved meshes), and a stop in it says "while building the model". #status, a live region, is told each text once.
    • The estimate (at least about 10 times the file) warns before the decode and the open goes ahead.
    • On a memory stop: "Too large for the browser" when the instance reached half the ceiling or the file's estimate passes it; otherwise "Ran out of memory", saying the file may be damaged or hit another limit, because a stop a long way short of the ceiling on a small file is no evidence the file needs more memory than a tab has. A literal "below 10 times" test would not have caught the review's example (a corrupt 2 MB file at 60 MiB is above its 20 MiB estimate); distance from the ceiling does.
    • The engine's own message is the status panel's "Engine message" row. The worker is ended after a failure. The failure note is red, as its toast is, and above the empty state.
    • Advice, checked against src/cli.rs, src/reader.rs and rvt-py/src/lib.rs: rvt-ifc and rvt-gltf with --large-model read streams up to 2 GiB and, on a 64-bit machine, inflate up to 8 GiB; they have no flag for max_file_bytes, the 2 GiB cap on the whole file, so a larger file is rejected whatever the flags. Python's RevitFile has no preset: a larger max_stream_bytes and max_inflate_bytes, and max_file_bytes over 2 GiB. Not "models of any size". File and memory sizes are in MiB.
    • After "Retry with large-model limits" an error that is not a memory stop is shown as it is: the suffix that said a tab can use at most 4 GiB is gone (memory stops go to the too-large path whatever limits the open ran under). The previous file's worker is ended before the next file is read, so a large read does not have to fit beside its wasm memory.
    • Why the firm/hedge threshold is half the ceiling (one wasm32 allocation cannot pass 2 GiB), and that a browser giving a tab less than 4 GiB gets the hedged wording, are in docs/viewer-api.md.
  • Tests (below), docs/viewer-api.md, viewer/README.md, docs/status.md, docs/viewer-build-pipeline.md, changelog.d/added/1195-viewer-progress-too-large.md.

Testing

Unit tests are banned in this repository: none added. No local build or test runs (the repository's rule).

  • CI passed is green on 46720a1 (29 checks, none failing).
  • Deploy viewer 41718446894 on 46720a1: 43 browser tests and 7 measurement tests passed. Its viewer-saved-meshes-measurement artifact has the screenshots and the records.
    • load-progress.spec.ts: the stage list on the tier-1 fixture; on Core Interior, every report is checked against the card's recorded state (the bar's value is the percent of the report's count where it has one and a sweep where not, the partition bar rises over 8 streams, and "Building the model" is told to #status exactly once, read from a record the page keeps of what it announced rather than from a MutationObserver, which batches a replay of held messages). Nothing hard-fails on timing. The screenshot holds the worker's messages in the frame the card first reads "Reading partition streams" at 25% or more (the partition stage runs about a second, 1.1 s to 2.1 s into the open, and a picture takes longer): load-progress-core-interior.png shows 25% · 2 of 8 streams, and its JSON records what the card said.
    • memory-ceiling-capped.spec.ts, Chromium with --js-flags=--wasm-max-mem-pages=N: Einhoven under a 16 MiB cap is reported as too large with the engine message in the status panel only and the worker ended; the same stop in a page that believes 4 GiB is "Ran out of memory" and never says "needs more"; Core Interior under a 310 MiB cap (it takes 291.6 MiB for its partitions and 324 MiB in all) stops in the silent stretch and says "while building the model".
    • worker-failures.spec.ts: the worker's own script is served by a route with a few lines in front that fail one wasm export (the page's CSP allows workers from 'self' only, so not a blob). Firefox's and Safari's wording of the trap are memory failures; a panic with a non-allocation message ("index out of bounds") shows that message and nothing about memory; a failure in buildSceneGraph after the open call is blamed on "building the scene graph"; a panic in the worker of a "Retry with large-model limits" (the first open runs as shipped under ?maxStreamBytes=200) shows the panic's message and nothing on the page mentions memory, 4 GiB or size.
    • memory-ceiling.spec.ts: the warning before the decode, and no warning under the ceiling.

Measurement

Deploy viewer 41714155908 (same peaks in 41700152800, 41712293625 and 41718446894), Chromium 156 on ubuntu-24.04 (4 CPUs), one open each, saved meshes on. Estimate is 10 times the file; wasm peak is the linear memory after the open (it only grows).

Model File MiB Wasm peak MiB Peak / file Estimate MiB Estimate / peak
2024_Core_Interior 32 324 10.1 322 0.99
RE1-Electrical 13 170 13.2 128 0.76
RE1-Architecture 6 100 15.9 63 0.63
RE1-Plumbing 3 60 18.0 33 0.56
RE1-Mechanical 3 71 22.1 32 0.45
Revit_IFC5_Einhoven 0.9 23 26.9 9 0.37

Peak per file runs from 27 times down to 10 as files grow, and the only larger evidence goes the other way (#1075's native peaks: 8.4 and 4.5 times). So "at least about 10 times" holds for the six models here, the estimate under-counts every small one, and for a file of hundreds of MiB it may over-count: the warning can fire on a file that fits (a 438 MB file is warned about at 4.1 GiB; 8.4 times is 3.4 GiB). docs/viewer-api.md says so. What the bar can show: on Core Interior the 8 partition inflates are real counts for the first 2 s, and the partition-schema recovery after them (most of a 17 s open) reports nothing, so the card says "Building the model" and sweeps; determinate progress there needs a control threaded into those passes, which this PR does not do.

Byte identity of exports against main: Measure run 41708042458, -f base=main -f models="core-interior einhoven re1-*" -f scorers=none: base 8664b94 (main), head 6147785 (this branch with main merged). The six ifc.sha256 files (Core Interior, Einhoven, the four RE1) are equal, and scorer-diff.txt is empty. The commits after 6147785 (up to 46720a1) touch no src/, rvt-wasm/ or Cargo.lock (git diff 6147785d 46720a1d -- src rvt-wasm Cargo.lock is empty), so the head's exporter is what was measured and Measure was not rerun.

Audit-honesty checklist

  • README / docs reflect the current state after this PR
  • A user-visible change has its entry as a fragment under changelog.d/added/
  • No emojis in code, docs, or commit messages
  • No private paths, personal email addresses, or internal jargon in committed files

Legal and contribution hygiene

  • I agree this work is licensable under Apache-2.0
  • This PR contains no Autodesk proprietary source, decompiled internals, or NDA'd SDK content
  • No new test fixtures

Breaking changes

None. The wasm API change is a new optional parameter.

Related issues

Refs #1195. Done: the progress bullet, the plain too-large message naming what reads a larger file, and the pre-check. Not done: the issue's last sentence says the options for opening larger models (wasm64, streaming partitions, reading fewer streams) are "measured and the choice documented". They are documented in docs/viewer-api.md with what each would do and its status, but wasm64, partition streaming and a one-copy binding are not built or measured, so the issue stays open for it. Not done either: determinate progress through the partition-schema recovery.

…a model too large for the tab says so (#1195)

The wasm open binding takes an optional progress function that the decoder's
WalkerControl reports to, throttled to one call per whole percent of a stage.
The worker posts each report while the wasm call runs, the card names the
stage and fills its bar with the stage's count, and the size-derived estimate
stays as the first thing it says. A file whose size says it may need more than
a tab's 4 GiB is warned about before the decode; a wasm instance that runs out
of memory (a bare unreachable with no panic message, or a capacity overflow)
is reported as too large for the browser, with how far it got and the CLIs
that open it. Playwright covers the stages, the part-way bar on Core Interior,
the warning, and a real stop under a V8-capped wasm memory.
… card falls back to the size estimate while a stage has no count (#1195)

The first run measured where an open goes: 20.3 of Core Interior's 22.0 s sat
under partition-scan, which reported 0 and then 1 of 1, so the bar swept for
nine tenths of the open. The ArcWall scan, the walk's first pass over the
partitions, inflates every Partitions stream; it now reports PartitionScan with
the stream's index and the stream count, through a control passed by the walk
(the memoised scan's other callers keep the default control). The card shows
the size-derived estimate while a stage has no count. The Playwright tests that
set launch options are split so the option is top level, which Playwright
requires, and the docs carry the measured estimate and the options for larger
models.
…ery project's first partition pass does (#1195)

The ArcWall scan the last commit instrumented is version-gated to 2023, so on
Core Interior (2024) the 15.8 s under partition-scan still reported nothing.
The first pass over the partitions, whichever it is, inflates every Partitions
stream and the handle keeps them, so the report moves there: RevitFile reports
each first inflate of a partition stream, out of the partition streams, to the
control of the export in progress (set and cleared around export_rvt_doc, so
rvt-gltf --progress gets it too). The ArcWall edits are undone.
…File stays unwind-safe (#1195)

The first Playwright run found the failure note under the dropzone, which is
opaque and one z-index above it, so a person would not have seen it while the
test saw it as visible; it is lifted above the dropzone for a failure to open,
and the test now asks what is on top at its centre. semver checks found that
holding the walk's control on RevitFile removed UnwindSafe and RefUnwindSafe
from it, because a control wraps a caller's closure; it is held as
AssertUnwindSafe. The size estimate is shown only before the decoder first
reports, since after that a stage with no count has none to give, and the docs
carry the measured timeline: 84% of a Core Interior open sits in the partition
schema recovery, which reports nothing.
…he out-of-memory classifier is not V8-only, and the advice names what reads a larger file (#1195)

The load-progress test hard-failed unless a 200 ms sample landed in a window of
a few hundred milliseconds. The page now records, with each decoder report,
what the card showed once it had applied it, and the test checks the bar's
value, step and hint against the report from that record; the screenshot is
taken by polling each animation frame while the partition stage runs, and is
best effort.

The worker matched only V8's 'unreachable': it now matches /unreach/i, so
Firefox's 'unreachable executed' and Safari's 'Unreachable code should not be
executed' are memory failures too. The engine's own message is shown in the
status panel. A stop a long way short of the ceiling on a small file, which a
damaged file also produces, says 'Ran out of memory' and that the file may be
damaged, and does not say the file needs more memory than a tab has. A panic
that is not an allocation failure shows its own message and nothing about
memory. A stop after the open call is blamed on the worker's step, a stop in
the stretch the decoder reports nothing in on building the model, lastPanic is
reset just before the open call, and the worker is ended after a failure.

rvt-ifc and rvt-gltf do not read models of any size. The advice now says
--large-model (streams up to 2 GiB, inflating up to 8 GiB), and Python with a
larger max_stream_bytes and max_inflate_bytes, which has no preset. The
estimate is 'at least about 10 times', and the docs say it can fire on a file
that fits, since the native peaks of #1075 are 8.4 and 4.5 times.

Tests: worker-failures.spec.ts fails one wasm export in a replacement worker to
cover the other engines' wording, a non-memory panic and a stop after the open
call; memory-ceiling-capped.spec.ts gets the hedged case and a stop in the
silent stretch on Core Interior under a 310 MiB cap.
… the end of a stage with nothing after it included (#1195)

The card gives a determinate bar to any report with a total above one that is
not the end of a stage it hands over from (partition-scan, saved-meshes), so the
candidate scan's last report reads 100%. The test required done below total and
expected a sweep there.
…ce the CSP allows no blob workers, and the hedged note drops a sentence its own test read as a claim (#1195)

index.html allows workers from 'self' only, so the replacement worker built
from a blob never started. The test now serves the worker's own script with the
patch in front of it, and checks it was served. The hedged note had a doubled
comma and said it was 'not reported as too large'; it now says what happened
and that the file may be damaged.
… messages held, so it shows the partition stage (#1195)

The last run's picture showed 'Building the model', because the partition stage
runs about a second and a screenshot takes longer. The test now holds back the
worker's messages, in order, in the frame the bar first reads 25% or more, takes
the picture and lets them through, and writes down what the card said.
… memory unless it is memory, and the advice states the 2 GiB file cap (#1195)

After 'Retry with large-model limits' every error not routed to the memory path
was given 'A browser tab can use at most 4 GiB of memory, so this file may be
too large', a panic included. Memory stops already go to showTooLarge, so the
suffix is dropped, and worker-failures.spec.ts retries under a stream limit
into a worker that panics and checks nothing on the page mentions memory.

The previous file's worker is ended before the next file is read, so a large
read does not have to fit beside its wasm memory. The advice says that
rvt-ifc and rvt-gltf with --large-model inflate up to 8 GiB on a 64-bit machine
only, that they have no flag for the 2 GiB cap on the whole file (max_file_bytes,
which Python can raise), and viewer-api.md says why the firm threshold is half
the ceiling (one wasm32 allocation cannot pass 2 GiB) and that a browser with
less than 4 GiB gets the hedged wording. user-guide.md no longer says rvt-ifc
--large-model has no ceiling.

load-progress.spec.ts holds the screenshot only when the card reads 'Reading
partition streams' at 25% or more, says how long that stage runs, and asserts
the once-only announcement from a record the page keeps of what it told #status
instead of a MutationObserver, which batches a replay of held messages.
@DrunkOnJava
DrunkOnJava marked this pull request as ready for review November 3, 2026 21:22
@DrunkOnJava
DrunkOnJava merged commit 53e06c0 into main Nov 3, 2026
29 checks passed
@DrunkOnJava
DrunkOnJava deleted the feat/1195-viewer-progress-too-large branch November 3, 2026 21:22
@DrunkOnJava

Copy link
Copy Markdown
Owner Author

Automated Claude Code review (the maintainer's review agent; it read the code, the tests and the Deploy viewer runs, over two rounds): three blocking items in the first round, fixed in 030bc27. The progress test depended on timing and now reads the recorded decoder reports. The out-of-memory message matched only Chrome's wording; it now matches Firefox's and Safari's, keeps a non-memory panic's own message, and hedges when the tab used little memory. The advice named limits the command line does not have; it now names --large-model's 2 GiB stream cap and 8 GiB inflate cap on a 64-bit machine. One blocking item in the second round, fixed in 46720a1: a failure after "Retry with large-model limits" no longer mentions memory unless it is a memory stop.

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.

1 participant