Skip to content

Commit 62e41d5

Browse files
authored
perf(parser): share exact prepass source fingerprints (#4014)
* perf(parser): share exact prepass source fingerprints (#3985) * docs(wasm): use canonical source fingerprint MPL header * docs(perf): preserve combined local landing validation
1 parent 9dd8ba1 commit 62e41d5

30 files changed

Lines changed: 673 additions & 52 deletions
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@ifc-lite/wasm": minor
3+
"@ifc-lite/geometry": minor
4+
"@ifc-lite/parser": minor
5+
---
6+
7+
Share an exact full-source fingerprint from the existing prepass through a fresh optional per-load cell. Preserve previous Rust and JavaScript methods, worker scheduling, parser fallback and partial/final source identity.

apps/viewer/src/hooks/entityIndexHandoff.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,12 @@ export function forwardEntityIndexTo(
7878
if (deferUntilWorkerFinishes) timer = setTimeout(release, maximumWaitMs);
7979
return Object.assign(forward, { release });
8080
}
81+
82+
/** One immutable-source fingerprint slot per load; never cache or reuse it. */
83+
export function createSourceFingerprintCell(source: SharedArrayBuffer | null | undefined, enabled: boolean): SharedArrayBuffer | undefined {
84+
if (!enabled || !source) return undefined;
85+
const cell = new SharedArrayBuffer(16), words = new Uint32Array(cell);
86+
words[0] = source.byteLength >>> 0;
87+
words[1] = Math.floor(source.byteLength / 0x100000000);
88+
return cell;
89+
}

apps/viewer/src/hooks/useIfcLoader.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import { resolveResourceRetryTier } from '../lib/resource-retry.js';
3939
import { acquireFileBuffer, type AcquiredBuffer } from '../utils/acquireFileBuffer.js';
4040
import { buildSpatialIndexGuarded, buildSpatialIndexForModel } from '../utils/loadingUtils.js';
4141
import { buildGeometryCacheKey } from './geometryCacheKey.js';
42-
import { forwardEntityIndexTo, type EntityIndexSink } from './entityIndexHandoff.js';
42+
import { forwardEntityIndexTo, createSourceFingerprintCell, type EntityIndexSink } from './entityIndexHandoff.js';
4343
import { type GeometryData } from '@ifc-lite/cache';
4444

4545
import { SERVER_URL, USE_SERVER, CACHE_SIZE_THRESHOLD, CACHE_MAX_SOURCE_SIZE, CACHE_MESH_ONLY_MAX_SIZE, getDynamicBatchConfig } from '../utils/ifcConfig.js';
@@ -1450,6 +1450,7 @@ export function useIfcLoader() {
14501450
// sync threshold (2 MB) and the desktop-stable path don't fire it
14511451
// — gate `waitForEntityIndex` so the parser doesn't hang.
14521452
const ADAPTIVE_SYNC_THRESHOLD_MB = 2;
1453+
const sourceFingerprint = createSourceFingerprintCell(sharedSource, useParserWorker);
14531454
const geometryWillEmitEntityIndex =
14541455
useParserWorker
14551456
&& fileSizeMB >= ADAPTIVE_SYNC_THRESHOLD_MB;
@@ -1468,6 +1469,7 @@ export function useIfcLoader() {
14681469
const worker = new WorkerParser();
14691470
workerParserInstance = worker;
14701471
return worker.parseColumnar(sharedSource, {
1472+
sourceFingerprint,
14711473
onSpatialReady: onPartialDataStore,
14721474
// Hold the parser's WASM scan until the pre-pass hands over
14731475
// the entity index — but only when we know the geometry
@@ -1607,6 +1609,7 @@ export function useIfcLoader() {
16071609
// reference arrays. Small loads still receive immediately.
16081610
// Refusal counts and malformed-stop diagnostics remain attached.
16091611
onEntityIndex: parserEntityIndexHandoff,
1612+
sourceFingerprint,
16101613
// `?geomWorkers=N` A/B knob — overrides the cores/memory worker-
16111614
// count heuristic so the host's thermal sweet spot can be measured.
16121615
// Still clamped to the memory budget by the engine. Geometry output

docs/api/wasm.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,3 +524,16 @@ bash scripts/build-wasm.sh
524524
| `bundler` | CommonJS | Webpack/Rollup |
525525
| `nodejs` | Node.js | Server-side |
526526
| `no-modules` | Global | Script tag |
527+
528+
529+
### Streaming prepass source fingerprints
530+
531+
`IfcAPI.buildPrePassStreamingWithSourceFingerprint` and
532+
`IfcAPI.buildPrePassStreamingShardedWithSourceFingerprint` accept the same arguments
533+
as `buildPrePassStreaming` and `buildPrePassStreamingSharded`, respectively. They
534+
run the same prepass and add `sourceContentKey` to the final `complete` event. The
535+
key is the existing full-byte FNV-1a source identity, including comments and any
536+
unparsed tail. Existing methods and their Rust signatures remain unchanged and do
537+
not compute this extra key. Feature-detect the new methods when supporting older
538+
WASM builds. The viewer uses these methods only when a matching parser has a fresh
539+
fingerprint cell; no additional file-sized buffer is created.

docs/guide/geometry.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,3 +615,13 @@ console.log(` Total vertices: ${result.totalVertices}`);
615615
- [Rendering Guide](rendering.md) - Display geometry with WebGPU
616616
- [Parsing Guide](parsing.md) - Parse options and streaming
617617
- [API Reference](../api/typescript.md) - Complete API docs
618+
619+
### Sharing the source fingerprint with a parser worker
620+
621+
`GeometryProcessor.processAdaptive` accepts an optional `sourceFingerprint` shared
622+
cell, also accepted as the last optional argument of `processParallel`. Pass the
623+
same fresh per-load cell to `WorkerParser.parseColumnar`; its layout and lifetime
624+
are documented in [Browser Worker Mode](./parsing.md#browser-worker-mode). Only the
625+
parallel prepass produces this key. Other geometry paths and older WASM builds
626+
leave the cell unavailable, and the parser computes its ordinary source key without
627+
waiting. Geometry streaming and parser index-handoff deadlines remain unchanged.

docs/guide/parsing.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,17 @@ if (WorkerParser.isSupported()) {
115115
}
116116
```
117117

118+
For an integrated geometry/parser load, both `WorkerParser.parseColumnar` and
119+
`GeometryProcessor.processAdaptive` accept an optional `sourceFingerprint` cell.
120+
Use a fresh 16-byte `SharedArrayBuffer` for each immutable source and pass that
121+
same cell to both calls. Its four unsigned 32-bit words hold source length low
122+
and high halves, hash, and readiness; initialize only the two length words before
123+
starting either call. Never reuse a cell for another source or load. The existing
124+
prepass worker computes the full-source key when its WASM supports that operation;
125+
the parser uses it only if ready, otherwise it computes the same key itself without
126+
waiting. Omitting the cell retains ordinary parsing behavior. This optimization
127+
does not change partial/final source accessor identity or cache keys.
128+
118129
### Streaming Geometry
119130

120131
For large files, stream geometry progressively using `GeometryProcessor.processStreaming()`:
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4+
5+
export interface ByteStreamingPrePassResult {
6+
jobs: Uint32Array;
7+
totalJobs: number;
8+
unitScale: number;
9+
rtcOffset?: Float64Array;
10+
needsShift: boolean;
11+
buildingRotation?: number | null;
12+
voidKeys: Uint32Array;
13+
voidCounts: Uint32Array;
14+
voidValues: Uint32Array;
15+
styleIds: Uint32Array;
16+
styleColors: Uint8Array;
17+
/** Prepass-resolved plane-angle→radians scale (additive wire field). */
18+
planeAngleToRadians?: number;
19+
/** #407/#913 §2.3 per-element material colour lists (flat encoding). */
20+
materialElementIds?: Uint32Array;
21+
materialColorCounts?: Uint32Array;
22+
materialColors?: Uint8Array;
23+
}

packages/geometry/src/geometry-parallel-options.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import type { TessellationQuality } from './types.js';
66
import type { BatchSizingConfig } from './batch-sizing.js';
77

88
export interface ProcessParallelOptions {
9+
/** Fresh per-load fingerprint cell shared only with the matching parser. */
10+
sourceFingerprint?: SharedArrayBuffer;
911
/**
1012
* Fires when the streaming pre-pass finishes building the entity index
1113
* (after styles), with SAB-backed Uint32Array views over the shared

packages/geometry/src/geometry-parallel.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1450,6 +1450,7 @@ export async function* processParallel(
14501450
}
14511451
prepassWorker.postMessage({
14521452
type: 'prepass-streaming-sharded',
1453+
sourceFingerprint: options?.sourceFingerprint,
14531454
sharedBuffer,
14541455
chunkSize: 50_000,
14551456
...(visibilityFilter?.disabledTypes ? { disabledTypes: visibilityFilter.disabledTypes } : {}),
@@ -1462,6 +1463,7 @@ export async function* processParallel(
14621463
} else {
14631464
prepassWorker.postMessage({
14641465
type: 'prepass-streaming',
1466+
sourceFingerprint: options?.sourceFingerprint,
14651467
sharedBuffer,
14661468
chunkSize: 50_000,
14671469
...(visibilityFilter?.disabledTypes ? { disabledTypes: visibilityFilter.disabledTypes } : {}),

packages/geometry/src/geometry.worker.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
44

55
import { ownedWasmBuffer } from './wasm-owned-buffer.js';
6+
import { publishPrepassFingerprint, runPrepassWithFingerprint } from './prepass-source-fingerprint.js';
67
import { canReuseWorkerSource, type SourcePrepassApi, type FinalizeStyleArgs } from './worker-prepass-source.js';
78
import init, { initSync, IfcAPI } from '@ifc-lite/wasm';
89
import { initWasmWithRetry } from './wasm-init-retry.js';
@@ -174,6 +175,7 @@ export interface GeometryWorkerScanShardMessage {
174175

175176
export interface GeometryWorkerPrePassMessage {
176177
type: 'prepass-streaming';
178+
sourceFingerprint?: SharedArrayBuffer;
177179
sharedBuffer: SharedArrayBuffer;
178180
/** Jobs per chunk (defaults to 50_000). */
179181
chunkSize?: number;
@@ -319,6 +321,7 @@ export interface GeometryWorkerStylesShardResultMessage {
319321
*/
320322
export interface GeometryWorkerPrePassShardedMessage {
321323
type: 'prepass-streaming-sharded';
324+
sourceFingerprint?: SharedArrayBuffer;
322325
sharedBuffer: SharedArrayBuffer;
323326
chunkSize?: number;
324327
disabledTypes?: string[];
@@ -1384,26 +1387,20 @@ async function handleMessage(e: MessageEvent<GeometryWorkerRequest>): Promise<vo
13841387
const ifcApi = await ensureInit();
13851388
(self as unknown as Worker).postMessage({ type: 'prepass-progress', phase: 'parsing' });
13861389
const { sharedBuffer, indexIds, indexStarts, indexLengths, indexClasses } = e.data;
1390+
const sourceFingerprint = e.data.sourceFingerprint;
13871391
const chunkSize = e.data.chunkSize ?? 50_000;
13881392
const disabledTypes = e.data.disabledTypes ?? undefined;
13891393
const skipTypeGeometry = e.data.skipTypeGeometry === true;
13901394
const onEvent = (event: unknown) => {
1395+
publishPrepassFingerprint(sourceFingerprint, sharedBuffer.byteLength, event);
13911396
(self as unknown as Worker).postMessage({ type: 'prepass-stream', event });
13921397
};
13931398
const run = (
13941399
bytes: Uint8Array,
13951400
ids: Uint32Array, starts: Uint32Array, lengths: Uint32Array, classes: Uint8Array,
13961401
) =>
1397-
(ifcApi as unknown as {
1398-
buildPrePassStreamingSharded: (
1399-
data: Uint8Array, onEvent: (e: unknown) => void, chunkSize: number,
1400-
disabledTypes: string[] | undefined, skipTypeGeometry: boolean,
1401-
ids: Uint32Array, starts: Uint32Array, lengths: Uint32Array, classes: Uint8Array,
1402-
) => unknown;
1403-
}).buildPrePassStreamingSharded(
1404-
bytes, onEvent, chunkSize, disabledTypes, skipTypeGeometry,
1405-
ids, starts, lengths, classes,
1406-
);
1402+
runPrepassWithFingerprint(ifcApi, [bytes, onEvent, chunkSize, disabledTypes, skipTypeGeometry],
1403+
sourceFingerprint, [ids, starts, lengths, classes]);
14071404
try {
14081405
run(viewSharedBytes(sharedBuffer), indexIds, indexStarts, indexLengths, indexClasses);
14091406
} catch (err) {
@@ -1469,6 +1466,7 @@ async function handleMessage(e: MessageEvent<GeometryWorkerRequest>): Promise<vo
14691466
// before the first chunk lands.
14701467
(self as unknown as Worker).postMessage({ type: 'prepass-progress', phase: 'parsing' });
14711468
const sharedBuffer = e.data.sharedBuffer;
1469+
const sourceFingerprint = e.data.sourceFingerprint;
14721470
const chunkSize = e.data.chunkSize ?? 50_000;
14731471
// #1097 load-time visibility filter (skip disabled types at job gen).
14741472
const disabledTypes = e.data.disabledTypes ?? undefined;
@@ -1479,10 +1477,11 @@ async function handleMessage(e: MessageEvent<GeometryWorkerRequest>): Promise<vo
14791477
// zero-copy view first, fall back to a materialised copy only if
14801478
// wasm-bindgen rejects the view.
14811479
const onEvent = (event: unknown) => {
1480+
publishPrepassFingerprint(sourceFingerprint, sharedBuffer.byteLength, event);
14821481
(self as unknown as Worker).postMessage({ type: 'prepass-stream', event });
14831482
};
14841483
const runPrepass = (bytes: Uint8Array) =>
1485-
ifcApi.buildPrePassStreaming(bytes, onEvent, chunkSize, disabledTypes, skipTypeGeometry);
1484+
runPrepassWithFingerprint(ifcApi, [bytes, onEvent, chunkSize, disabledTypes, skipTypeGeometry], sourceFingerprint);
14861485
try {
14871486
// Zero-copy SAB view first; wasm-bindgen copies it into linear memory.
14881487
runPrepass(viewSharedBytes(sharedBuffer));
@@ -1593,7 +1592,8 @@ async function handleMessage(e: MessageEvent<GeometryWorkerRequest>): Promise<vo
15931592
// size from a previous dense model).
15941593
batchSizing = resolveBatchSizing(e.data.batchSizing);
15951594
adaptiveBatchJobs = batchSizing.maxJobs;
1596-
// Reuse this load's shard source; reset non-sharded/reused loads before geometry (#3989).
1595+
// Shard scanning has already installed this load's source (#3989).
1596+
// Non-sharded/reused loads still reset before their first geometry batch.
15971597
if (!sourcePreparedForStream || !canReuseWorkerSource(installedSourceSessionId, e.data.sourceSessionId)) {
15981598
sourceBytesApplied = false;
15991599
cachedSourceBytes = null;

0 commit comments

Comments
 (0)