@ifc-lite/geometry 7.2.0 → 7.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/coordinate-handler.js +4 -4
- package/dist/coordinate-handler.js.map +1 -1
- package/dist/coordinate-types.d.ts +8 -1
- package/dist/coordinate-types.d.ts.map +1 -1
- package/dist/geometry-parallel-options.d.ts +29 -0
- package/dist/geometry-parallel-options.d.ts.map +1 -1
- package/dist/geometry-parallel.d.ts.map +1 -1
- package/dist/geometry-parallel.js +228 -87
- package/dist/geometry-parallel.js.map +1 -1
- package/dist/geometry-worker-init.d.ts +9 -0
- package/dist/geometry-worker-init.d.ts.map +1 -0
- package/dist/geometry-worker-init.js +64 -0
- package/dist/geometry-worker-init.js.map +1 -0
- package/dist/geometry.worker.d.ts +16 -0
- package/dist/geometry.worker.d.ts.map +1 -1
- package/dist/geometry.worker.js +15 -8
- package/dist/geometry.worker.js.map +1 -1
- package/dist/hung-job-recovery.d.ts +145 -0
- package/dist/hung-job-recovery.d.ts.map +1 -0
- package/dist/hung-job-recovery.js +289 -0
- package/dist/hung-job-recovery.js.map +1 -0
- package/dist/index.d.ts +18 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- package/dist/index.js.map +1 -1
- package/dist/rtc-rebase.d.ts +101 -0
- package/dist/rtc-rebase.d.ts.map +1 -0
- package/dist/rtc-rebase.js +135 -0
- package/dist/rtc-rebase.js.map +1 -0
- package/dist/stall-phase.d.ts +111 -0
- package/dist/stall-phase.d.ts.map +1 -0
- package/dist/stall-phase.js +164 -0
- package/dist/stall-phase.js.map +1 -0
- package/dist/types.d.ts +6 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/world-frame.d.ts +44 -5
- package/dist/world-frame.d.ts.map +1 -1
- package/dist/world-frame.js +70 -6
- package/dist/world-frame.js.map +1 -1
- package/package.json +9 -4
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
import { postGeometryWorkerInit } from './geometry-worker-init.js';
|
|
4
5
|
import { mergeGeometryDiagnostics } from './diagnostics.js';
|
|
5
6
|
import { computeWorkerCount } from './worker-count.js';
|
|
6
7
|
import { notifyIfWasmAssetUnavailable, notifyIfWorkerScriptUnavailable } from './wasm-asset-error.js';
|
|
@@ -11,6 +12,8 @@ import { restashWasmPanicLocation } from './wasm-panic-forward.js';
|
|
|
11
12
|
import { compileSharedWasmModule } from './wasm-shared-module.js';
|
|
12
13
|
import { stitchShards } from './shard-stitch.js';
|
|
13
14
|
import { resolveRtcFrame } from './rtc-frame.js';
|
|
15
|
+
import { emptyStylesPrepassEvent, frozenStallPhase, GateTracker, PhaseBoundTimers, preWorkerPhaseFailureDiagnostics, } from './stall-phase.js';
|
|
16
|
+
import { SkippedHungElementsCollector, startHungJobMonitor, WorkerJobLedger, } from './hung-job-recovery.js';
|
|
14
17
|
/**
|
|
15
18
|
* Prepass class-byte layout, mirroring the `PREPASS_CLASS_*` definitions in
|
|
16
19
|
* `rust/processing/src/shard_classes.rs` (the source of truth — these are
|
|
@@ -160,6 +163,23 @@ function terminateWorkerQuietly(worker, label) {
|
|
|
160
163
|
console.warn(`[stream] ${label} terminate failed:`, err);
|
|
161
164
|
}
|
|
162
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* A recorded `set-entity-index` setup (#4884). Built OUTSIDE `deliverEntityIndex`
|
|
168
|
+
* on purpose: a replay closure created in that scope would keep the pre-pass's
|
|
169
|
+
* transferred id/start/length columns (12 B per entity, ~180 MB on a 1 GB file)
|
|
170
|
+
* alive for the whole load, where before they were collectable right after the
|
|
171
|
+
* copy into the shared buffers. This one captures only its `columns` factory.
|
|
172
|
+
*/
|
|
173
|
+
function entityIndexSetup(columns) {
|
|
174
|
+
return (w) => {
|
|
175
|
+
try {
|
|
176
|
+
w.postMessage({ type: 'set-entity-index', ...columns() });
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
console.warn('[stream] set-entity-index dispatch failed:', err);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
163
183
|
let nextSourceSessionId = 0;
|
|
164
184
|
export async function* processParallel(buffer, coordinator, sharedRtcOffset,
|
|
165
185
|
/** Optional pre-allocated SAB the caller already shares with another worker. */
|
|
@@ -204,6 +224,13 @@ existingSab, options) {
|
|
|
204
224
|
resolveWaiting = null;
|
|
205
225
|
}
|
|
206
226
|
};
|
|
227
|
+
// Which pre-worker gate is still open (#4902) — see stall-phase.ts.
|
|
228
|
+
const gateTracker = new GateTracker();
|
|
229
|
+
if (options?.stallPhaseHandle)
|
|
230
|
+
options.stallPhaseHandle.getStallPhase = () => gateTracker.getStallPhase();
|
|
231
|
+
// Bounded waits for those gates (#4902); cleared in the `finally` below so
|
|
232
|
+
// none outlives this load's teardown — see stall-phase.ts.
|
|
233
|
+
const phaseBoundTimers = new PhaseBoundTimers();
|
|
207
234
|
// Pre-pass worker drives the entire pipeline via streaming events.
|
|
208
235
|
let prepassMeta = null;
|
|
209
236
|
let prepassJobsTotal = 0;
|
|
@@ -247,6 +274,10 @@ existingSab, options) {
|
|
|
247
274
|
const shardResults = [];
|
|
248
275
|
let shardResultsRemaining = 0;
|
|
249
276
|
let shardScanDispatchedAt = -1;
|
|
277
|
+
// #4902: settles once, normally or via the bounded-wait timeout below.
|
|
278
|
+
let shardScanSettled = false;
|
|
279
|
+
let stylesSlicesSettled = false;
|
|
280
|
+
let finalizeSettled = false;
|
|
250
281
|
const stylesSliceResults = [];
|
|
251
282
|
let stylesSlicesRemaining = 0;
|
|
252
283
|
// Support spans extracted from the shard classes (sharded mode only).
|
|
@@ -277,7 +308,11 @@ existingSab, options) {
|
|
|
277
308
|
// crash from the worker SCRIPT failing to load (stale-deploy 404, #1251).
|
|
278
309
|
let workerSpoke = false;
|
|
279
310
|
worker.onmessage = (e) => {
|
|
311
|
+
// A worker replaced after a hang (#4884) may still flush queued messages.
|
|
312
|
+
if (workers[workerIndex] !== worker)
|
|
313
|
+
return;
|
|
280
314
|
workerSpoke = true;
|
|
315
|
+
ledger.onHeard(workerIndex, performance.now());
|
|
281
316
|
const msg = e.data;
|
|
282
317
|
if (msg.type === 'ready') {
|
|
283
318
|
console.log(`[stream] worker[${workerIndex}] WASM ready @ ${elapsed()}ms`);
|
|
@@ -307,6 +342,10 @@ existingSab, options) {
|
|
|
307
342
|
return;
|
|
308
343
|
}
|
|
309
344
|
if (msg.type === 'styles-final') {
|
|
345
|
+
// A late reply after the #4902 finalize bound already drained with
|
|
346
|
+
// default colours must not reopen that gate.
|
|
347
|
+
if (finalizeSettled)
|
|
348
|
+
return;
|
|
310
349
|
// Finalized styles payload from worker 0 — feed it through the SAME
|
|
311
350
|
// prepass styles-event path (gates, logging, distribution) by
|
|
312
351
|
// synthesizing a prepass-stream message. The handler is a plain
|
|
@@ -347,10 +386,19 @@ existingSab, options) {
|
|
|
347
386
|
// Forward it so the consumer's stall watchdog measures "one WASM
|
|
348
387
|
// call went silent", not "no meshes lately" — a CSG-heavy stretch
|
|
349
388
|
// can keep every worker busy past the watchdog with nothing to show.
|
|
389
|
+
if (typeof msg.seq === 'number') {
|
|
390
|
+
ledger.onCallStart(workerIndex, msg.seq, msg.processedJobs, msg.callJobs ?? 1, performance.now());
|
|
391
|
+
}
|
|
392
|
+
if (msg.diagnostics)
|
|
393
|
+
diagnostics = mergeGeometryDiagnostics(diagnostics, msg.diagnostics);
|
|
350
394
|
eventQueue.push({ type: 'progress', phase: 'workers' });
|
|
351
395
|
wake();
|
|
352
396
|
return;
|
|
353
397
|
}
|
|
398
|
+
if (msg.type === 'slice-done') {
|
|
399
|
+
ledger.onSliceDone(workerIndex, msg.seq, performance.now());
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
354
402
|
if (msg.type === 'batch') {
|
|
355
403
|
if (firstBatchByWorker[workerIndex] === undefined) {
|
|
356
404
|
firstBatchByWorker[workerIndex] = elapsed();
|
|
@@ -444,6 +492,8 @@ existingSab, options) {
|
|
|
444
492
|
}
|
|
445
493
|
};
|
|
446
494
|
worker.onerror = (err) => {
|
|
495
|
+
if (workers[workerIndex] !== worker)
|
|
496
|
+
return;
|
|
447
497
|
// A hard worker crash (e.g. the wasm thread aborting under memory
|
|
448
498
|
// pressure) fires an ErrorEvent with an empty `message`. Emitting the
|
|
449
499
|
// literal "undefined" produced a cryptic, unclassifiable error in tracking
|
|
@@ -489,8 +539,25 @@ existingSab, options) {
|
|
|
489
539
|
console.log('[stream] shared wasm module compiled once — workers initSync (no per-worker recompile)');
|
|
490
540
|
}
|
|
491
541
|
const workers = [];
|
|
542
|
+
// Hung-call recovery (#4884): unfinished slices per worker, and every per-load
|
|
543
|
+
// state message in order (never per-worker work) to rebuild a replacement.
|
|
544
|
+
// Opt-in: without a budget nothing is recorded, so the pool costs what it did.
|
|
545
|
+
const hungJobTimeoutMs = Math.max(0, options?.hungJobTimeoutMs ?? 0);
|
|
546
|
+
const ledger = new WorkerJobLedger(workerCount, performance.now(), hungJobTimeoutMs > 0);
|
|
547
|
+
// Always recorded (#4902 review): a pre-worker phase bound below may need
|
|
548
|
+
// to replace a worker regardless of whether #4884's in-call recovery is
|
|
549
|
+
// enabled, and replaying this log is how a replacement reaches the same
|
|
550
|
+
// state every other worker is in.
|
|
551
|
+
const workerSetup = [];
|
|
552
|
+
const broadcastSetup = (setup) => {
|
|
553
|
+
workerSetup.push(setup);
|
|
554
|
+
for (const w of workers)
|
|
555
|
+
setup(w);
|
|
556
|
+
};
|
|
557
|
+
const postInitMessages = (worker) => postGeometryWorkerInit(worker, options, sharedWasmModule);
|
|
558
|
+
workerSetup.push(postInitMessages);
|
|
492
559
|
// This loop runs BEFORE the try/finally below (which owns teardown for the
|
|
493
|
-
// rest of the pipeline), so it needs its own: `
|
|
560
|
+
// rest of the pipeline), so it needs its own: `postInitMessages` can throw
|
|
494
561
|
// (e.g. a `wasmModule` structured-clone failure — the same class of error
|
|
495
562
|
// `dispatchJobsChunkInternal` already guards against further down), and
|
|
496
563
|
// without this try/catch any worker already pushed to `workers` before the
|
|
@@ -500,58 +567,7 @@ existingSab, options) {
|
|
|
500
567
|
const worker = makeGeometryWorker();
|
|
501
568
|
workers.push(worker);
|
|
502
569
|
installWorkerHandlers(worker, i);
|
|
503
|
-
|
|
504
|
-
// worker `initSync`s it (cheap); otherwise it falls back to compiling from
|
|
505
|
-
// bytes. The worker's tail-promise serialiser guarantees this `init`
|
|
506
|
-
// completes before any subsequent `stream-start`/`stream-chunk` runs.
|
|
507
|
-
//
|
|
508
|
-
// `wasmUrl` is forwarded only when the consumer explicitly provided one AND
|
|
509
|
-
// no shared module is available — undefined leaves the worker on
|
|
510
|
-
// wasm-bindgen's default `import.meta.url`-based resolution (Vite + webpack).
|
|
511
|
-
const wasmUrlForWorker = options?.wasmUrls?.wasm;
|
|
512
|
-
worker.postMessage({
|
|
513
|
-
type: 'init',
|
|
514
|
-
...(sharedWasmModule
|
|
515
|
-
? { wasmModule: sharedWasmModule }
|
|
516
|
-
: wasmUrlForWorker
|
|
517
|
-
? { wasmUrl: wasmUrlForWorker }
|
|
518
|
-
: {}),
|
|
519
|
-
});
|
|
520
|
-
// Issue #540: forward the user's "Merge Multilayer Walls" toggle
|
|
521
|
-
// BEFORE any stream-start so the worker's IfcAPI has the flag set
|
|
522
|
-
// before its first parse call. The tail-promise serialiser inside
|
|
523
|
-
// each worker preserves this order even though the messages are
|
|
524
|
-
// posted back-to-back. We always send the message so the controller
|
|
525
|
-
// path doesn't have to remember whether the host called it — the
|
|
526
|
-
// default `false` is a cheap no-op.
|
|
527
|
-
worker.postMessage({
|
|
528
|
-
type: 'set-merge-layers',
|
|
529
|
-
enabled: options?.mergeLayers === true,
|
|
530
|
-
});
|
|
531
|
-
// GPU-instancing partition toggle — default ON; the host sets false for federated
|
|
532
|
-
// loads so a federated model's geometry stays flat (instancing is primary-only).
|
|
533
|
-
worker.postMessage({
|
|
534
|
-
type: 'set-instancing-enabled',
|
|
535
|
-
enabled: options?.enableInstancing !== false,
|
|
536
|
-
});
|
|
537
|
-
// Issue #924: forward the geometry-hash tolerance the same way — always
|
|
538
|
-
// sent so the controller path stays uniform; null is a cheap no-op.
|
|
539
|
-
worker.postMessage({
|
|
540
|
-
type: 'set-compute-geometry-hashes',
|
|
541
|
-
tolerance: options?.geometryHashTolerance ?? null,
|
|
542
|
-
});
|
|
543
|
-
// Issue #976: forward the tessellation-quality level the same way —
|
|
544
|
-
// null keeps the Rust default (Medium / historical densities).
|
|
545
|
-
worker.postMessage({
|
|
546
|
-
type: 'set-tessellation-quality',
|
|
547
|
-
level: options?.tessellationQuality ?? null,
|
|
548
|
-
});
|
|
549
|
-
// Issue #1286: forward the small-cut skip the same way — always sent so a
|
|
550
|
-
// worker reused by a later export (which omits it) resets to false.
|
|
551
|
-
worker.postMessage({
|
|
552
|
-
type: 'set-skip-small-cuts',
|
|
553
|
-
enabled: options?.skipSmallCuts === true,
|
|
554
|
-
});
|
|
570
|
+
postInitMessages(worker);
|
|
555
571
|
}
|
|
556
572
|
}
|
|
557
573
|
catch (err) {
|
|
@@ -559,6 +575,31 @@ existingSab, options) {
|
|
|
559
575
|
terminateWorkerQuietly(w, 'process worker (init)');
|
|
560
576
|
throw err;
|
|
561
577
|
}
|
|
578
|
+
/**
|
|
579
|
+
* Terminate and replace pool worker `index` (#4902 review). A silent
|
|
580
|
+
* `scan-shard` / `resolve-styles-shard` / `finalize-styles` reply means
|
|
581
|
+
* that worker is wedged inside ONE synchronous WASM call —
|
|
582
|
+
* `geometry.worker.js` serializes every message behind its tail promise, so
|
|
583
|
+
* continuing the fallback without replacing it would queue the fallback's
|
|
584
|
+
* own `stream-start`/chunks/`stream-end` forever behind the stuck call, and
|
|
585
|
+
* `workersCompleted` would never reach `workers.length`. None of the three
|
|
586
|
+
* bounds fires after any `stream-chunk` has reached a worker (they all sit
|
|
587
|
+
* strictly before `dispatchJobsChunk`'s gate can open), so there is no
|
|
588
|
+
* in-flight slice to replay — replaying `workerSetup` alone brings the
|
|
589
|
+
* replacement to the same state every other worker is in.
|
|
590
|
+
*/
|
|
591
|
+
function replacePreWorkerPhaseWorker(index, reason) {
|
|
592
|
+
const hung = workers[index];
|
|
593
|
+
const replacement = makeGeometryWorker();
|
|
594
|
+
workers[index] = replacement; // swap first — the stale-worker guard then drops anything the hung one flushes
|
|
595
|
+
terminateWorkerQuietly(hung, `process worker (${reason})`);
|
|
596
|
+
installWorkerHandlers(replacement, index);
|
|
597
|
+
for (const setup of workerSetup)
|
|
598
|
+
setup(replacement);
|
|
599
|
+
if (endSentToWorkers)
|
|
600
|
+
replacement.postMessage({ type: 'stream-end' });
|
|
601
|
+
console.warn(`[stream] worker[${index}] replaced: silent past its #4902 ${reason} bound`);
|
|
602
|
+
}
|
|
562
603
|
const sendStreamEnd = () => {
|
|
563
604
|
if (endSentToWorkers)
|
|
564
605
|
return;
|
|
@@ -607,12 +648,13 @@ existingSab, options) {
|
|
|
607
648
|
const emptyU32 = new Uint32Array(0);
|
|
608
649
|
const emptyU8 = new Uint8Array(0);
|
|
609
650
|
const batchSizing = options?.batchSizing ?? readBatchSizingOverride();
|
|
610
|
-
|
|
651
|
+
const frameMeta = prepassMeta;
|
|
652
|
+
broadcastSetup((worker) => {
|
|
611
653
|
worker.postMessage({
|
|
612
654
|
type: 'stream-start', sourceSessionId,
|
|
613
655
|
sharedBuffer,
|
|
614
|
-
unitScale:
|
|
615
|
-
planeAngleToRadians:
|
|
656
|
+
unitScale: frameMeta.unitScale,
|
|
657
|
+
planeAngleToRadians: frameMeta.planeAngleToRadians,
|
|
616
658
|
rtcX, rtcY, rtcZ,
|
|
617
659
|
needsShift: effectiveNeedsShift,
|
|
618
660
|
voidKeys: emptyU32,
|
|
@@ -622,7 +664,7 @@ existingSab, options) {
|
|
|
622
664
|
styleColors: emptyU8,
|
|
623
665
|
...(batchSizing ? { batchSizing } : {}),
|
|
624
666
|
});
|
|
625
|
-
}
|
|
667
|
+
});
|
|
626
668
|
// Don't drain queued chunks here — wait for the `styles` event so
|
|
627
669
|
// every chunk gets processed with resolved colours. The styles
|
|
628
670
|
// handler does the drain after posting set-styles.
|
|
@@ -649,9 +691,14 @@ existingSab, options) {
|
|
|
649
691
|
sub[o + 1] = jobs[src + 1];
|
|
650
692
|
sub[o + 2] = jobs[src + 2];
|
|
651
693
|
}
|
|
652
|
-
|
|
694
|
+
postChunk(i, sub);
|
|
653
695
|
}
|
|
654
696
|
}
|
|
697
|
+
/** Record a slice in the ledger, then transfer it to worker `i` (#4884). */
|
|
698
|
+
function postChunk(i, jobs, maxBatchJobs) {
|
|
699
|
+
const seq = ledger.recordDispatch(i, jobs, maxBatchJobs);
|
|
700
|
+
workers[i].postMessage({ type: 'stream-chunk', jobsFlat: jobs, seq, ...(maxBatchJobs !== undefined ? { maxBatchJobs } : {}) }, [jobs.buffer]);
|
|
701
|
+
}
|
|
655
702
|
function dispatchJobsChunkInternal(jobs, affinity) {
|
|
656
703
|
if (workers.length === 0 || jobs.length === 0)
|
|
657
704
|
return;
|
|
@@ -681,7 +728,7 @@ existingSab, options) {
|
|
|
681
728
|
sub[j + 1] = jobs[src + 1];
|
|
682
729
|
sub[j + 2] = jobs[src + 2];
|
|
683
730
|
}
|
|
684
|
-
|
|
731
|
+
postChunk(i, sub);
|
|
685
732
|
}
|
|
686
733
|
}
|
|
687
734
|
catch (err) {
|
|
@@ -725,6 +772,15 @@ existingSab, options) {
|
|
|
725
772
|
streamEndPendingQueueDrain = false;
|
|
726
773
|
sendStreamEnd();
|
|
727
774
|
}
|
|
775
|
+
// Every pre-worker gate is open, so no #4902 phase-bound timeout can still
|
|
776
|
+
// need `workerSetup` to replace a worker (each of the three settles at or
|
|
777
|
+
// before this point — see stall-phase.ts). With #4884 in-call recovery
|
|
778
|
+
// OFF, nothing else ever replays it either: release the closures (styles/
|
|
779
|
+
// prepass-columns retain the large prepass arrays) instead of holding them
|
|
780
|
+
// for the rest of the load. Recovery ON still needs the full log to bring
|
|
781
|
+
// a LATER in-call replacement up to date, so it is left alone.
|
|
782
|
+
if (!(hungJobTimeoutMs > 0))
|
|
783
|
+
workerSetup.length = 0;
|
|
728
784
|
};
|
|
729
785
|
// Step-by-step timing so we can tell exactly where time goes.
|
|
730
786
|
const t0 = performance.now();
|
|
@@ -771,19 +827,9 @@ existingSab, options) {
|
|
|
771
827
|
new Uint32Array(sabStarts).set(starts);
|
|
772
828
|
new Uint32Array(sabLengths).set(lengths);
|
|
773
829
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
type: 'set-entity-index',
|
|
778
|
-
ids: new Uint32Array(sabIds),
|
|
779
|
-
starts: new Uint32Array(sabStarts),
|
|
780
|
-
lengths: new Uint32Array(sabLengths),
|
|
781
|
-
});
|
|
782
|
-
}
|
|
783
|
-
catch (err) {
|
|
784
|
-
console.warn('[stream] set-entity-index dispatch failed:', err);
|
|
785
|
-
}
|
|
786
|
-
}
|
|
830
|
+
broadcastSetup(entityIndexSetup(() => ({
|
|
831
|
+
ids: new Uint32Array(sabIds), starts: new Uint32Array(sabStarts), lengths: new Uint32Array(sabLengths),
|
|
832
|
+
})));
|
|
787
833
|
if (options?.onEntityIndex) {
|
|
788
834
|
try {
|
|
789
835
|
options.onEntityIndex(new Uint32Array(sabIds), new Uint32Array(sabStarts), new Uint32Array(sabLengths), oversizedIdCount, malformedRecordCount);
|
|
@@ -794,17 +840,10 @@ existingSab, options) {
|
|
|
794
840
|
}
|
|
795
841
|
}
|
|
796
842
|
else {
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
ids: ids.slice(), starts: starts.slice(), lengths: lengths.slice(),
|
|
802
|
-
});
|
|
803
|
-
}
|
|
804
|
-
catch (err) {
|
|
805
|
-
console.warn('[stream] set-entity-index dispatch failed:', err);
|
|
806
|
-
}
|
|
807
|
-
}
|
|
843
|
+
const copyIds = ids, copyStarts = starts, copyLengths = lengths;
|
|
844
|
+
broadcastSetup(entityIndexSetup(() => ({
|
|
845
|
+
ids: copyIds.slice(), starts: copyStarts.slice(), lengths: copyLengths.slice(),
|
|
846
|
+
})));
|
|
808
847
|
if (options?.onEntityIndex) {
|
|
809
848
|
try {
|
|
810
849
|
options.onEntityIndex(ids.slice(), starts.slice(), lengths.slice(), oversizedIdCount, malformedRecordCount);
|
|
@@ -815,6 +854,7 @@ existingSab, options) {
|
|
|
815
854
|
}
|
|
816
855
|
}
|
|
817
856
|
entityIndexReceived = true;
|
|
857
|
+
gateTracker.markEntityIndexReceived();
|
|
818
858
|
drainQueuedChunksIfReady();
|
|
819
859
|
};
|
|
820
860
|
/**
|
|
@@ -826,6 +866,11 @@ existingSab, options) {
|
|
|
826
866
|
* the serial pre-pass path — identical to flag-off behaviour.
|
|
827
867
|
*/
|
|
828
868
|
const onAllShardsReceived = () => {
|
|
869
|
+
// A late straggler shard result may land after the #4902 bound already fell back.
|
|
870
|
+
if (shardScanSettled)
|
|
871
|
+
return;
|
|
872
|
+
shardScanSettled = true;
|
|
873
|
+
gateTracker.markShardScanDone();
|
|
829
874
|
const shards = shardResults;
|
|
830
875
|
const stitched = stitchShards(shards);
|
|
831
876
|
if (!stitched) {
|
|
@@ -880,6 +925,24 @@ existingSab, options) {
|
|
|
880
925
|
const slice = styledSpans.slice(from, to);
|
|
881
926
|
workers[i % workers.length].postMessage({ type: 'resolve-styles-shard', sourceSessionId, sharedBuffer, sliceIndex: i, spans: slice }, [slice.buffer]);
|
|
882
927
|
}
|
|
928
|
+
// #4902 bound: a missing slice at the deadline is treated as empty
|
|
929
|
+
// (`onAllStyleSlicesReceived` already skips a `null` entry) — see stall-phase.ts.
|
|
930
|
+
phaseBoundTimers.arm(fileSizeMB, () => stylesSlicesSettled, () => {
|
|
931
|
+
console.warn(`[stream][shard] ${stylesSlicesRemaining}/${sliceCount} style slice(s) silent — proceeding with the slices that answered (#4902)`);
|
|
932
|
+
diagnostics = mergeGeometryDiagnostics(diagnostics, preWorkerPhaseFailureDiagnostics('style-slice-timeout'));
|
|
933
|
+
// Replace every worker owning a still-silent slice BEFORE continuing —
|
|
934
|
+
// same reasoning as the shard-scan bound above: `resolve-styles-shard`
|
|
935
|
+
// is synchronous WASM work on that worker's own FIFO queue.
|
|
936
|
+
const hungWorkers = new Set();
|
|
937
|
+
for (let i = 0; i < sliceCount; i++) {
|
|
938
|
+
if (!stylesSliceResults[i])
|
|
939
|
+
hungWorkers.add(i % workers.length);
|
|
940
|
+
}
|
|
941
|
+
for (const idx of hungWorkers)
|
|
942
|
+
replacePreWorkerPhaseWorker(idx, 'style-slice-timeout');
|
|
943
|
+
stylesSlicesRemaining = 0;
|
|
944
|
+
onAllStyleSlicesReceived();
|
|
945
|
+
});
|
|
883
946
|
// Start the sharded pre-pass with the stitched index columns + classes
|
|
884
947
|
// (stage 2: the pre-pass discovers jobs/spans from the class column and
|
|
885
948
|
// never byte-scans the file).
|
|
@@ -895,6 +958,10 @@ existingSab, options) {
|
|
|
895
958
|
* canonical flatten emits the styles event through the same channel.
|
|
896
959
|
*/
|
|
897
960
|
const onAllStyleSlicesReceived = () => {
|
|
961
|
+
// A late straggler slice may land after the #4902 bound already merged.
|
|
962
|
+
if (stylesSlicesSettled)
|
|
963
|
+
return;
|
|
964
|
+
stylesSlicesSettled = true;
|
|
898
965
|
const orphan = new Map(); // id -> base float index (slice,i)
|
|
899
966
|
const geom = new Map();
|
|
900
967
|
// First pass: count winners to size the merged columns.
|
|
@@ -959,6 +1026,18 @@ existingSab, options) {
|
|
|
959
1026
|
planeAngleToRadians: prepassMeta.planeAngleToRadians ?? 1,
|
|
960
1027
|
}, [m.orphanIds.buffer, m.orphanColors.buffer, m.geomIds.buffer, m.geomColors.buffer]);
|
|
961
1028
|
console.log(`[stream][shard] styles finalize dispatched to worker[0] @ ${elapsed()}ms`);
|
|
1029
|
+
// #4902 bound: replay the empty-styles event (stall-phase.ts) so every
|
|
1030
|
+
// held chunk drains with default colours instead of never draining.
|
|
1031
|
+
phaseBoundTimers.arm(fileSizeMB, () => finalizeSettled, () => {
|
|
1032
|
+
finalizeSettled = true;
|
|
1033
|
+
console.warn('[stream][shard] styles finalize silent — draining with default colours (#4902)');
|
|
1034
|
+
diagnostics = mergeGeometryDiagnostics(diagnostics, preWorkerPhaseFailureDiagnostics('styles-finalize-timeout'));
|
|
1035
|
+
// worker[0] is the sole target of `finalize-styles`; replace it before
|
|
1036
|
+
// the fallback drains chunks, or its still-wedged FIFO queue would
|
|
1037
|
+
// never consume the `stream-chunk`/`stream-end` the drain sends it.
|
|
1038
|
+
replacePreWorkerPhaseWorker(0, 'styles-finalize-timeout');
|
|
1039
|
+
prepassWorker.onmessage({ data: emptyStylesPrepassEvent() });
|
|
1040
|
+
});
|
|
962
1041
|
};
|
|
963
1042
|
// SPIKE: kick off the shard scans on the idle workers NOW (before the pre-pass
|
|
964
1043
|
// worker's single scan). Gated: flag on, SAB available, file big enough, ≥2
|
|
@@ -974,6 +1053,7 @@ existingSab, options) {
|
|
|
974
1053
|
shardResults.length = n;
|
|
975
1054
|
shardResultsRemaining = n;
|
|
976
1055
|
shardScanDispatchedAt = elapsed();
|
|
1056
|
+
gateTracker.markShardScanStarted();
|
|
977
1057
|
console.log(`[stream][shard] dispatching ${n} shard scans over ${(len / (1024 * 1024)).toFixed(1)}MB @ ${shardScanDispatchedAt}ms`);
|
|
978
1058
|
for (let i = 0; i < n; i++) {
|
|
979
1059
|
const rangeStart = Math.floor((i * len) / n);
|
|
@@ -986,6 +1066,23 @@ existingSab, options) {
|
|
|
986
1066
|
rangeEnd,
|
|
987
1067
|
});
|
|
988
1068
|
}
|
|
1069
|
+
// #4902 bound: falls back to the serial pre-pass, same as an unresolved
|
|
1070
|
+
// stitch (`onAllShardsReceived`'s `!stitched` branch) — see stall-phase.ts.
|
|
1071
|
+
phaseBoundTimers.arm(fileSizeMB, () => shardScanSettled, () => {
|
|
1072
|
+
shardScanSettled = true;
|
|
1073
|
+
gateTracker.markShardScanDone();
|
|
1074
|
+
console.warn(`[stream][shard] ${shardResultsRemaining}/${n} shard scan(s) silent — falling back to the serial pre-pass (#4902)`);
|
|
1075
|
+
diagnostics = mergeGeometryDiagnostics(diagnostics, preWorkerPhaseFailureDiagnostics('shard-scan-timeout'));
|
|
1076
|
+
// Replace every worker still silent on scan-shard BEFORE continuing:
|
|
1077
|
+
// `scan-shard` is a synchronous WASM call, so a missing reply means
|
|
1078
|
+
// that worker is wedged, not just slow — and the fallback's own
|
|
1079
|
+
// stream-start/chunks/stream-end would queue forever behind it.
|
|
1080
|
+
for (let i = 0; i < n; i++) {
|
|
1081
|
+
if (!shardResults[i])
|
|
1082
|
+
replacePreWorkerPhaseWorker(i, 'shard-scan-timeout');
|
|
1083
|
+
}
|
|
1084
|
+
startPrepass(false);
|
|
1085
|
+
});
|
|
989
1086
|
}
|
|
990
1087
|
const prepassWorker = makePrepassWorker();
|
|
991
1088
|
// Wrap the rest of the pipeline so worker teardown runs not only on
|
|
@@ -994,7 +1091,32 @@ existingSab, options) {
|
|
|
994
1091
|
// is suspended at a `yield` or the `resolveWaiting` await. The viewer's
|
|
995
1092
|
// `watchedGeometryStream` relies on this `finally` to tear down workers
|
|
996
1093
|
// on break / abort / watchdog (see boundedIteratorReturn). The existing
|
|
1094
|
+
// Abandonment and hung-call recovery (#4884). `return()` cannot reach this
|
|
1095
|
+
// `finally` while the loop is parked on a silent worker, so a stalled load
|
|
1096
|
+
// leaked its pool (hung worker included) into the retry: abort terminates it.
|
|
1097
|
+
let aborted = false;
|
|
1098
|
+
const onAbort = () => {
|
|
1099
|
+
aborted = true;
|
|
1100
|
+
for (const w of workers)
|
|
1101
|
+
terminateWorkerQuietly(w, 'process worker (abort)');
|
|
1102
|
+
terminateWorkerQuietly(prepassWorker, 'pre-pass worker (abort)');
|
|
1103
|
+
wake();
|
|
1104
|
+
};
|
|
1105
|
+
const skippedHungElements = new SkippedHungElementsCollector();
|
|
1106
|
+
let stopHungJobMonitor = () => { };
|
|
997
1107
|
try {
|
|
1108
|
+
if (options?.signal?.aborted)
|
|
1109
|
+
onAbort();
|
|
1110
|
+
else
|
|
1111
|
+
options?.signal?.addEventListener('abort', onAbort, { once: true });
|
|
1112
|
+
stopHungJobMonitor = startHungJobMonitor({
|
|
1113
|
+
workers, ledger, makeWorker: makeGeometryWorker, installHandlers: installWorkerHandlers,
|
|
1114
|
+
setup: workerSetup, postChunk, streamEndSent: () => endSentToWorkers,
|
|
1115
|
+
terminate: terminateWorkerQuietly, source: new Uint8Array(sharedBuffer), skipped: skippedHungElements,
|
|
1116
|
+
isLive: () => !aborted && !workerError && !prepassError,
|
|
1117
|
+
onLiveness: () => { eventQueue.push({ type: 'progress', phase: 'workers' }); wake(); },
|
|
1118
|
+
onFailed: (error) => { workerError ??= error; wake(); },
|
|
1119
|
+
}, hungJobTimeoutMs);
|
|
998
1120
|
// Forward the consumer-supplied wasm URL to the pre-pass worker so it
|
|
999
1121
|
// doesn't fall back to wasm-bindgen's `import.meta.url` default. The
|
|
1000
1122
|
// pre-pass worker uses the same `geometry.worker.js` bundle and the
|
|
@@ -1073,7 +1195,7 @@ existingSab, options) {
|
|
|
1073
1195
|
const materialColorCounts = evt.materialColorCounts ?? new Uint32Array(0);
|
|
1074
1196
|
const materialColors = evt.materialColors ?? new Uint8Array(0);
|
|
1075
1197
|
console.log(`[stream] styles @ ${elapsed()}ms (${styleIds.length} styled, ${voidKeys.length} void hosts), draining ${queuedChunks.length} queued chunks`);
|
|
1076
|
-
|
|
1198
|
+
broadcastSetup((w) => {
|
|
1077
1199
|
// Slice each typed array per-worker so each can be in its own
|
|
1078
1200
|
// transfer list without conflict. The slice cost is bounded by
|
|
1079
1201
|
// `styleIds.length * 4` bytes — under 1 MB for ~250K styles.
|
|
@@ -1104,8 +1226,10 @@ existingSab, options) {
|
|
|
1104
1226
|
catch (err) {
|
|
1105
1227
|
console.warn('[stream] set-styles dispatch failed:', err);
|
|
1106
1228
|
}
|
|
1107
|
-
}
|
|
1229
|
+
});
|
|
1108
1230
|
stylesReceived = true;
|
|
1231
|
+
finalizeSettled = true; // reached via the real path — the #4902 bound is moot now
|
|
1232
|
+
gateTracker.markStylesReceived();
|
|
1109
1233
|
// Drain only when ALL gates are open (entity-index too). The
|
|
1110
1234
|
// worker's tail-promise serialiser ensures any set-* runs
|
|
1111
1235
|
// before any subsequent stream-chunk.
|
|
@@ -1154,7 +1278,7 @@ existingSab, options) {
|
|
|
1154
1278
|
const mliLayerMaterialIds = evt.mliLayerMaterialIds;
|
|
1155
1279
|
const mliLayerThicknesses = evt.mliLayerThicknesses;
|
|
1156
1280
|
console.log(`[stream] prepass-columns @ ${elapsed()}ms (${referencedRepmaps.length} repmaps, ${instantiatedTypeIds.length} inst-types, ${mliElementIds.length} layer-elems)`);
|
|
1157
|
-
|
|
1281
|
+
broadcastSetup((w) => {
|
|
1158
1282
|
try {
|
|
1159
1283
|
const rRepmaps = referencedRepmaps.slice();
|
|
1160
1284
|
const rTypeIds = instantiatedTypeIds.slice();
|
|
@@ -1186,7 +1310,7 @@ existingSab, options) {
|
|
|
1186
1310
|
catch (err) {
|
|
1187
1311
|
console.warn('[stream] set-prepass-columns dispatch failed:', err);
|
|
1188
1312
|
}
|
|
1189
|
-
}
|
|
1313
|
+
});
|
|
1190
1314
|
// Not a dispatch gate (see dispatchJobsChunk); the pre-pass emits this
|
|
1191
1315
|
// before the first jobs chunk, so drain here only for symmetry with the
|
|
1192
1316
|
// other post-scan events in case chunks were queued behind styles/index.
|
|
@@ -1252,6 +1376,7 @@ existingSab, options) {
|
|
|
1252
1376
|
// After we see the Rust `complete` event we can sendStreamEnd.
|
|
1253
1377
|
const onPrepassComplete = () => {
|
|
1254
1378
|
prepassDone = true;
|
|
1379
|
+
gateTracker.markPrepassDone();
|
|
1255
1380
|
// Only signal stream-end to workers if they actually got
|
|
1256
1381
|
// stream-start (which gates on `meta`). Zero-geometry files
|
|
1257
1382
|
// never trigger meta → workers never start → no stream-end
|
|
@@ -1342,9 +1467,13 @@ existingSab, options) {
|
|
|
1342
1467
|
// b) all workers reported `complete`
|
|
1343
1468
|
let prepassCompleteSeen = false;
|
|
1344
1469
|
while (true) {
|
|
1470
|
+
if (aborted)
|
|
1471
|
+
return;
|
|
1345
1472
|
while (eventQueue.length > 0) {
|
|
1346
1473
|
yield eventQueue.shift();
|
|
1347
1474
|
}
|
|
1475
|
+
if (aborted)
|
|
1476
|
+
return;
|
|
1348
1477
|
if (workerError) {
|
|
1349
1478
|
for (const w of workers) {
|
|
1350
1479
|
terminateWorkerQuietly(w, 'process worker');
|
|
@@ -1391,14 +1520,26 @@ existingSab, options) {
|
|
|
1391
1520
|
`${loadDiagnostics.productsWithFailures} product(s) this load - see ` +
|
|
1392
1521
|
`diagnostics.failuresByReason; not every reason leaves an opening/void uncut`);
|
|
1393
1522
|
}
|
|
1523
|
+
const skippedReport = skippedHungElements.report();
|
|
1394
1524
|
yield {
|
|
1395
1525
|
type: 'complete',
|
|
1396
1526
|
totalMeshes,
|
|
1397
1527
|
coordinateInfo,
|
|
1398
1528
|
...(loadDiagnostics ? { diagnostics: loadDiagnostics } : {}),
|
|
1529
|
+
...(skippedReport ? { skippedHungElements: skippedReport } : {}),
|
|
1399
1530
|
};
|
|
1400
1531
|
}
|
|
1401
1532
|
finally {
|
|
1533
|
+
phaseBoundTimers.clearAll();
|
|
1534
|
+
// Re-seat the caller's handle to a frozen snapshot (#4979 review): the
|
|
1535
|
+
// live reader closes over `gateTracker`, and transitively this whole
|
|
1536
|
+
// generator's scope (including `sharedBuffer`) — the caller may still
|
|
1537
|
+
// hold the handle long after this teardown runs.
|
|
1538
|
+
if (options?.stallPhaseHandle) {
|
|
1539
|
+
options.stallPhaseHandle.getStallPhase = frozenStallPhase(gateTracker.getStallPhase());
|
|
1540
|
+
}
|
|
1541
|
+
stopHungJobMonitor();
|
|
1542
|
+
options?.signal?.removeEventListener('abort', onAbort);
|
|
1402
1543
|
for (const w of workers) {
|
|
1403
1544
|
terminateWorkerQuietly(w, 'process worker');
|
|
1404
1545
|
}
|