mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-17 22:11:36 +00:00
ifcviewer-web: stop large-model network streaming thrash (grow before fetch)
Battle-testing real sidecars over HTTP Range exposed severe thrash: a
531 MB model re-fetched 2.25 GB (4×) and never converged — viewAll puts
the whole model in frustum, so every chunk wants to be resident, and the
web async path made it worse two ways:
- A web load only consumes pool space when it COMPLETES (async), so the
per-frame issuance over-committed the pool; completions then failed
applyStreamedChunk on a full pool, the chunk re-candidated with no
cooldown, and re-fetched every frame.
- Pool growth is itself async on web (provisional sub-buffers validated
off the JS event loop), so even fetched chunks failed to alloc until
the pool caught up, and re-fetched.
Fix: gate web chunk issuance on VALIDATED free space + in-flight
reservation, and grow the pool BEFORE fetching:
- streaming_web_inflight_bytes_ reserves each in-flight load's footprint
so we never have more bytes in flight than the pool can place.
- When a visible chunk doesn't fit validated free, don't fetch — call
pool_.requestGrowth() (BufferPool: drives the async provisional grow
without allocating) and short-back-off; the chunk is fetched once,
after space exists. When the pool is saturated (model > GPU memory),
long-cooldown so a never-fitting chunk isn't re-fetched. Gating before
the evictor also kills phase-2 visible↔visible swap thrash.
- On async load failure, cool down (short if the pool can still grow,
long if saturated) instead of re-candidating next frame.
Result (manual battle tool, host.mjs + real files): 531 MB now loads
23/23 chunks, 322 MB loads 14/14 — resident climbs monotonically with
ZERO thrash warnings and a stable resident set, vs the old re-fetch loop.
The whole model resides on the GPU and stays. (Remaining ~3× ramp
over-fetch — per-chunk re-loads during the async-growth ramp + read
amplification from chunk byte-locality — is a separate efficiency
follow-up, not thrash.) 6/6 web smoke + 107/107 unit pass; desktop
unaffected (the gate is web-only; requestGrowth is a no-op wrapper there).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -890,6 +890,23 @@ private:
|
||||
int streaming_loads_this_frame_ = 0;
|
||||
bool streaming_more_pending_ = false;
|
||||
|
||||
// Frames a chunk that couldn't fit (or whose async load failed) is held off
|
||||
// the candidate list before retrying. Shared by the sync evictor and the
|
||||
// web async-failure path so a saturated pool backs off instead of thrashing.
|
||||
static constexpr std::uint64_t kBlockedCooldownFrames = 180;
|
||||
// Short backoff when a web load couldn't fit but the pool can still grow
|
||||
// (provisional sub-buffer validating) — retry soon, don't long-cooldown.
|
||||
static constexpr std::uint64_t kGrowBackoffFrames = 8;
|
||||
|
||||
// Web only: bytes reserved by in-flight async chunk loads. A web load only
|
||||
// consumes pool space when it COMPLETES (async), so without reserving here
|
||||
// the per-frame issuance over-commits the pool — chunks get fetched, then
|
||||
// applyStreamedChunk fails on a full pool and re-fetches (observed: a 531 MB
|
||||
// model re-fetched 4× over the network). Incremented at issue, decremented
|
||||
// when the load resolves (success or failure); driveStreamingLoads blocks
|
||||
// candidates that won't fit total_free - this.
|
||||
std::uint64_t streaming_web_inflight_bytes_ = 0;
|
||||
|
||||
// Settle burst: keep the render loop alive for a few frames after any
|
||||
// streaming activity so the cull→load→display latency (the draw + cull
|
||||
// precede driveStreamingLoads, so a freshly-resident chunk paints a frame
|
||||
|
||||
Reference in New Issue
Block a user