@ifc-lite/geometry 7.6.0 → 7.8.1

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.
Files changed (101) hide show
  1. package/README.md +64 -0
  2. package/dist/domain-export-call.d.ts +4 -0
  3. package/dist/domain-export-call.d.ts.map +1 -0
  4. package/dist/domain-export-call.js +19 -0
  5. package/dist/domain-export-call.js.map +1 -0
  6. package/dist/geometry-parallel-options.d.ts +15 -0
  7. package/dist/geometry-parallel-options.d.ts.map +1 -1
  8. package/dist/geometry-parallel.d.ts +2 -47
  9. package/dist/geometry-parallel.d.ts.map +1 -1
  10. package/dist/geometry-parallel.js +50 -146
  11. package/dist/geometry-parallel.js.map +1 -1
  12. package/dist/geometry-worker-pool.d.ts +72 -0
  13. package/dist/geometry-worker-pool.d.ts.map +1 -0
  14. package/dist/geometry-worker-pool.js +176 -0
  15. package/dist/geometry-worker-pool.js.map +1 -0
  16. package/dist/geometry.worker.d.ts.map +1 -1
  17. package/dist/geometry.worker.js +34 -36
  18. package/dist/geometry.worker.js.map +1 -1
  19. package/dist/huge-file-error.d.ts +1 -2
  20. package/dist/huge-file-error.d.ts.map +1 -1
  21. package/dist/huge-file-error.js +4 -5
  22. package/dist/huge-file-error.js.map +1 -1
  23. package/dist/hung-job-recovery.d.ts +26 -2
  24. package/dist/hung-job-recovery.d.ts.map +1 -1
  25. package/dist/hung-job-recovery.js +42 -7
  26. package/dist/hung-job-recovery.js.map +1 -1
  27. package/dist/ifc-lite-bridge.d.ts +2 -0
  28. package/dist/ifc-lite-bridge.d.ts.map +1 -1
  29. package/dist/ifc-lite-bridge.js +19 -35
  30. package/dist/ifc-lite-bridge.js.map +1 -1
  31. package/dist/in-call-heartbeat.d.ts +20 -0
  32. package/dist/in-call-heartbeat.d.ts.map +1 -0
  33. package/dist/in-call-heartbeat.js +39 -0
  34. package/dist/in-call-heartbeat.js.map +1 -0
  35. package/dist/index.d.ts +9 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +15 -30
  38. package/dist/index.js.map +1 -1
  39. package/dist/main-engine-init.d.ts +5 -0
  40. package/dist/main-engine-init.d.ts.map +1 -0
  41. package/dist/main-engine-init.js +42 -0
  42. package/dist/main-engine-init.js.map +1 -0
  43. package/dist/map-normalization-capability.d.ts +7 -0
  44. package/dist/map-normalization-capability.d.ts.map +1 -0
  45. package/dist/map-normalization-capability.js +32 -0
  46. package/dist/map-normalization-capability.js.map +1 -0
  47. package/dist/native-bridge.d.ts +1 -0
  48. package/dist/native-bridge.d.ts.map +1 -1
  49. package/dist/native-bridge.js +1 -0
  50. package/dist/native-bridge.js.map +1 -1
  51. package/dist/perf-flags.d.ts +55 -0
  52. package/dist/perf-flags.d.ts.map +1 -0
  53. package/dist/perf-flags.js +66 -0
  54. package/dist/perf-flags.js.map +1 -0
  55. package/dist/platform-bridge.d.ts +2 -0
  56. package/dist/platform-bridge.d.ts.map +1 -1
  57. package/dist/platform-bridge.js.map +1 -1
  58. package/dist/prepass-routing.d.ts +53 -0
  59. package/dist/prepass-routing.d.ts.map +1 -0
  60. package/dist/prepass-routing.js +100 -0
  61. package/dist/prepass-routing.js.map +1 -0
  62. package/dist/remesh/remesh-client.d.ts.map +1 -1
  63. package/dist/remesh/remesh-client.js +5 -1
  64. package/dist/remesh/remesh-client.js.map +1 -1
  65. package/dist/remesh/remesh-core.d.ts.map +1 -1
  66. package/dist/remesh/remesh-core.js +29 -11
  67. package/dist/remesh/remesh-core.js.map +1 -1
  68. package/dist/remesh/remesh-protocol.d.ts +10 -4
  69. package/dist/remesh/remesh-protocol.d.ts.map +1 -1
  70. package/dist/remesh/remesh-protocol.js.map +1 -1
  71. package/dist/remesh/remesh.worker.js +25 -10
  72. package/dist/remesh/remesh.worker.js.map +1 -1
  73. package/dist/scan-proposals.d.ts +132 -0
  74. package/dist/scan-proposals.d.ts.map +1 -0
  75. package/dist/scan-proposals.js +12 -0
  76. package/dist/scan-proposals.js.map +1 -0
  77. package/dist/scan-segmentation.d.ts +205 -0
  78. package/dist/scan-segmentation.d.ts.map +1 -0
  79. package/dist/scan-segmentation.js +17 -0
  80. package/dist/scan-segmentation.js.map +1 -0
  81. package/dist/warm-pool.d.ts +27 -0
  82. package/dist/warm-pool.d.ts.map +1 -0
  83. package/dist/warm-pool.js +48 -0
  84. package/dist/warm-pool.js.map +1 -0
  85. package/dist/wasm-shared-module.d.ts +17 -19
  86. package/dist/wasm-shared-module.d.ts.map +1 -1
  87. package/dist/wasm-shared-module.js +90 -77
  88. package/dist/wasm-shared-module.js.map +1 -1
  89. package/dist/wasm-streaming-guard.d.ts +2 -0
  90. package/dist/wasm-streaming-guard.d.ts.map +1 -0
  91. package/dist/wasm-streaming-guard.js +22 -0
  92. package/dist/wasm-streaming-guard.js.map +1 -0
  93. package/dist/worker-count.d.ts +10 -0
  94. package/dist/worker-count.d.ts.map +1 -1
  95. package/dist/worker-count.js +19 -0
  96. package/dist/worker-count.js.map +1 -1
  97. package/dist/worker-trace.d.ts +16 -0
  98. package/dist/worker-trace.d.ts.map +1 -0
  99. package/dist/worker-trace.js +39 -0
  100. package/dist/worker-trace.js.map +1 -0
  101. package/package.json +17 -6
package/README.md CHANGED
@@ -154,6 +154,70 @@ works.
154
154
 
155
155
  See the [Geometry Guide](https://ifclite.dev/docs/guide/geometry/) and [API Reference](https://ifclite.dev/docs/api/typescript/#ifc-litegeometry).
156
156
 
157
+ `GeometryProcessor.planMapConversionNormalization(content)` returns a JSON
158
+ entity-patch plan for opt-in STEP coordinate compatibility export. Initialize
159
+ the processor first and dispose it in `finally`. Canonical Rust validates units,
160
+ placements and representation ownership; unsupported coordinate consumers return
161
+ warnings with no patches. `StepExporter.exportAsync({ normalizeMapGeometry:
162
+ true })` applies this plan after edits using its existing modification ledger.
163
+ See the [exporting guide](https://ifclite.dev/docs/guide/exporting/) for the
164
+ supported subset and warning handling. Third-party platform bridges may expose
165
+ the optional planner capability; the bundled native bridge explicitly refuses it.
166
+
167
+ ## Scan plane segmentation
168
+
169
+ `@ifc-lite/geometry/scan-segmentation` types the wasm plane and cylinder
170
+ detector for point clouds. Pass xyz metres, either a whole buffer or a reservoir with a `count`.
171
+ Every option is optional; see the
172
+ [segmentation contract](https://ifclite.dev/docs/api/wasm/#scan-plane-segmentation)
173
+ for the defaults.
174
+
175
+ ```ts
176
+ import { segmentScan } from '@ifc-lite/geometry/scan-segmentation';
177
+
178
+ const api = new IfcAPI(); // after the wasm module is initialised
179
+ const positions = new Float32Array([/* x, y, z, ... in metres */]);
180
+ try {
181
+ const report = segmentScan(api, { positions }, { scannerPosition: [3, 2, 1.5] });
182
+ for (const plane of report.planes) {
183
+ console.log(plane.orientation, plane.normal, plane.d, plane.areaSquareMetres);
184
+ }
185
+ for (const cylinder of report.cylinders) {
186
+ // `faceted` is set for a polygonal column (radius is then the circumradius).
187
+ console.log(cylinder.orientation, cylinder.axisStart, cylinder.radius, cylinder.length, cylinder.faceted?.faces);
188
+ }
189
+ } finally {
190
+ api.free();
191
+ }
192
+ ```
193
+
194
+ ## Scan element proposals
195
+
196
+ `@ifc-lite/geometry/scan-proposals` turns a segmentation report into proposed
197
+ `IfcWall`, `IfcSlab`, `IfcColumn` and pipe elements in the IFC model frame (Z
198
+ up, metres): opposite wall faces pair into walls of measured thickness, floors
199
+ and ceilings into slabs, vertical cylinders into columns. See the
200
+ [proposal contract](https://ifclite.dev/docs/api/wasm/#scan-element-proposals).
201
+
202
+ ```ts
203
+ import { segmentScan } from '@ifc-lite/geometry/scan-segmentation';
204
+ import { proposeScanElements } from '@ifc-lite/geometry/scan-proposals';
205
+
206
+ const api = new IfcAPI(); // after the wasm module is initialised
207
+ const positions = new Float32Array([/* x, y, z, ... Y-up metres */]);
208
+ try {
209
+ const report = segmentScan(api, { positions }, { upAxis: [0, 1, 0] });
210
+ // Y-up (x, y, z) -> IFC Z-up (x, -z, y), row-major.
211
+ const scanToModel = [1, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, 0, 0, 0, 0, 1];
212
+ const { proposals } = proposeScanElements(api, report, { scanToModel, schema: 'IFC4' });
213
+ for (const p of proposals) {
214
+ console.log(p.id, p.ifcClass, p.basis, p.confidence.toFixed(2), p.geometry);
215
+ }
216
+ } finally {
217
+ api.free();
218
+ }
219
+ ```
220
+
157
221
  ## License
158
222
 
159
223
  [MPL-2.0](../../LICENSE)
@@ -0,0 +1,4 @@
1
+ import type { IfcAPI } from '@ifc-lite/wasm';
2
+ /** Shared initialization/error boundary for canonical domain exports. */
3
+ export declare function runDomainExport<T>(api: IfcAPI | null, operation: string, content: Uint8Array, run: (api: IfcAPI) => T, onError: (error: unknown) => void): T;
4
+ //# sourceMappingURL=domain-export-call.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"domain-export-call.d.ts","sourceRoot":"","sources":["../src/domain-export-call.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAI7C,yEAAyE;AACzE,wBAAgB,eAAe,CAAC,CAAC,EAC/B,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GACrH,CAAC,CASH"}
@@ -0,0 +1,19 @@
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
+ import { createLogger } from '@ifc-lite/data';
5
+ const log = createLogger('Geometry');
6
+ /** Shared initialization/error boundary for canonical domain exports. */
7
+ export function runDomainExport(api, operation, content, run, onError) {
8
+ if (!api)
9
+ throw new Error('IFC-Lite not initialized. Call init() first.');
10
+ try {
11
+ return run(api);
12
+ }
13
+ catch (error) {
14
+ log.error(`Failed to ${operation}`, error, { operation, data: { contentLength: content.length } });
15
+ onError(error);
16
+ throw error;
17
+ }
18
+ }
19
+ //# sourceMappingURL=domain-export-call.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"domain-export-call.js","sourceRoot":"","sources":["../src/domain-export-call.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAG9C,MAAM,GAAG,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC;AAErC,yEAAyE;AACzE,MAAM,UAAU,eAAe,CAC7B,GAAkB,EAAE,SAAiB,EAAE,OAAmB,EAAE,GAAuB,EAAE,OAAiC;IAEtH,IAAI,CAAC,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;IAC1E,IAAI,CAAC;QACH,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC;IAClB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,GAAG,CAAC,KAAK,CAAC,aAAa,SAAS,EAAE,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,aAAa,EAAE,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACnG,OAAO,CAAC,KAAK,CAAC,CAAC;QACf,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -1,6 +1,8 @@
1
1
  import type { TessellationQuality } from './types.js';
2
2
  import type { BatchSizingConfig } from './batch-sizing.js';
3
3
  import type { StallPhaseHandle } from './stall-phase.js';
4
+ import type { LoadTrace } from '@ifc-lite/load-trace';
5
+ import type { GeometryWorkerPool } from './geometry-worker-pool.js';
4
6
  export interface ProcessParallelOptions {
5
7
  /** Fresh per-load fingerprint cell shared only with the matching parser. */
6
8
  sourceFingerprint?: SharedArrayBuffer;
@@ -121,5 +123,18 @@ export interface ProcessParallelOptions {
121
123
  * covers, not this issue's pre-worker class.
122
124
  */
123
125
  stallPhaseHandle?: StallPhaseHandle;
126
+ /**
127
+ * Issue #6956 — the caller's load trace. The pool records `geometry.pool`,
128
+ * `geometry.firstBatch`, the shard scan/stitch and pre-pass phases on it, and
129
+ * asks its workers to post their own spans back. Omitted ⇒ nothing recorded.
130
+ */
131
+ trace?: LoadTrace;
132
+ /**
133
+ * Lease the workers a host prewarmed for this load from this pool (#7036)
134
+ * instead of spawning them; any it lacks are spawned. Every worker still
135
+ * serves only this load and is terminated by it. Omitted, every worker is
136
+ * spawned here, as before.
137
+ */
138
+ workerPool?: GeometryWorkerPool;
124
139
  }
125
140
  //# sourceMappingURL=geometry-parallel-options.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"geometry-parallel-options.d.ts","sourceRoot":"","sources":["../src/geometry-parallel-options.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEzD,MAAM,WAAW,sBAAsB;IACrC,4EAA4E;IAC5E,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IACtC;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,CACd,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,WAAW,EAAE,gBAAgB,CAAC,EAAE,MAAM,EAAE,wBAAwB;IACzE,oBAAoB,CAAC,EAAE,MAAM,KAC1B,IAAI,CAAC;IACV;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;OAMG;IACH,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACjD;;;;;OAKG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,EAAE;QACT,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;IACF;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACzC;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,gBAAgB,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAC5E;;;;;;;OAOG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;;;;OASG;IACH,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;CACrC"}
1
+ {"version":3,"file":"geometry-parallel-options.d.ts","sourceRoot":"","sources":["../src/geometry-parallel-options.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAEpE,MAAM,WAAW,sBAAsB;IACrC,4EAA4E;IAC5E,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IACtC;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,CACd,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,WAAW,EAAE,gBAAgB,CAAC,EAAE,MAAM,EAAE,wBAAwB;IACzE,oBAAoB,CAAC,EAAE,MAAM,KAC1B,IAAI,CAAC;IACV;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;OAMG;IACH,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACjD;;;;;OAKG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,EAAE;QACT,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;IACF;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACzC;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,gBAAgB,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAC5E;;;;;;;OAOG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;;;;OASG;IACH,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IACpC;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,kBAAkB,CAAC;CACjC"}
@@ -22,53 +22,8 @@
22
22
  import type { ProcessParallelOptions } from './geometry-parallel-options.js';
23
23
  import type { CoordinateHandler } from './coordinate-handler.js';
24
24
  import type { StreamingGeometryEvent } from './index.js';
25
- /**
26
- * Prepass class-byte layout, mirroring the `PREPASS_CLASS_*` definitions in
27
- * `rust/processing/src/shard_classes.rs` (the source of truth — these are
28
- * pinned to it by `prepass-class-spans.test.ts`).
29
- *
30
- * The producer packs a named code in the LOW bits and composes FLAG bits on
31
- * top (`PREPASS_CLASS_FLAG_GEOMETRY_JOB` 0x80, `..._FLAG_TYPE_CANDIDATE`
32
- * 0x40), so a consumer must mask before comparing — the Rust consumer does
33
- * (`gpu_meshes/prepass_discovery.rs`), and so does {@link extractPrepassSpanLists}.
34
- */
35
- export declare const PREPASS_CLASS_CODE_MASK = 63;
36
- /** `IFCSTYLEDITEM`. */
37
- export declare const PREPASS_CLASS_STYLED_ITEM = 4;
38
- /** `IFCINDEXEDCOLOURMAP`. */
39
- export declare const PREPASS_CLASS_INDEXED_COLOUR_MAP = 5;
40
- /** `IFCMATERIALDEFINITIONREPRESENTATION`. */
41
- export declare const PREPASS_CLASS_MATERIAL_DEF_REPR = 6;
42
- /** `IFCRELASSOCIATESMATERIAL`. */
43
- export declare const PREPASS_CLASS_REL_ASSOCIATES_MATERIAL = 7;
44
- /** `IFCRELVOIDSELEMENT`. */
45
- export declare const PREPASS_CLASS_REL_VOIDS = 8;
46
- /** `IFCRELFILLSELEMENT`. */
47
- export declare const PREPASS_CLASS_REL_FILLS = 9;
48
- /** `IFCRELAGGREGATES`. */
49
- export declare const PREPASS_CLASS_REL_AGGREGATES = 10;
50
- /**
51
- * Build one `(id, start, length)` span list per host-consumed prepass class
52
- * from the stitched shard columns, in FILE ORDER. Every comparison goes
53
- * through {@link PREPASS_CLASS_CODE_MASK}, so a record that carries a flag bit
54
- * alongside its named code still lands in its list instead of being dropped.
55
- * Returns exact-size arrays (one entry per class in `HOST_SPAN_CLASSES`,
56
- * empty when the file has none).
57
- */
58
- export declare function extractPrepassSpanLists(classes: Uint8Array, ids: Uint32Array, starts: Uint32Array, lengths: Uint32Array): Map<number, Uint32Array>;
59
- /**
60
- * Plan content-affinity routing for one chunk: assign each job (by index) to a
61
- * worker bucket so that every job sharing an affinity key lands on the SAME
62
- * worker — across the whole stream, since `keyToWorker` is the caller's sticky
63
- * map. New keys are handed out round-robin from `startWorker`, so each worker
64
- * owns roughly `1/workerCount` of the distinct keys (≈ distinct geometries, the
65
- * dominant meshing cost). Pure: mutates only the passed `keyToWorker` and returns
66
- * the advanced round-robin cursor. (#1130 follow-up — see `affinity_key` in Rust.)
67
- */
68
- export declare function planAffinityRouting(affinity: Uint32Array, totalJobs: number, workerCount: number, keyToWorker: Map<number, number>, startWorker: number): {
69
- buckets: number[][];
70
- nextWorker: number;
71
- };
25
+ export { GeometryWorkerPool, DEFAULT_POOL_LIMITS, FRESH_WORKER_HEAP_BYTES } from './geometry-worker-pool.js';
26
+ export { PREPASS_CLASS_CODE_MASK, PREPASS_CLASS_STYLED_ITEM, PREPASS_CLASS_INDEXED_COLOUR_MAP, PREPASS_CLASS_MATERIAL_DEF_REPR, PREPASS_CLASS_REL_ASSOCIATES_MATERIAL, PREPASS_CLASS_REL_VOIDS, PREPASS_CLASS_REL_FILLS, PREPASS_CLASS_REL_AGGREGATES, extractPrepassSpanLists, planAffinityRouting, } from './prepass-routing.js';
72
27
  export type { ProcessParallelOptions } from './geometry-parallel-options.js';
73
28
  export declare function processParallel(buffer: Uint8Array, coordinator: CoordinateHandler, sharedRtcOffset?: {
74
29
  x: number;
@@ -1 +1 @@
1
- {"version":3,"file":"geometry-parallel.d.ts","sourceRoot":"","sources":["../src/geometry-parallel.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAG7E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAyBzD;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,KAAO,CAAC;AAC5C,uBAAuB;AACvB,eAAO,MAAM,yBAAyB,IAAI,CAAC;AAC3C,6BAA6B;AAC7B,eAAO,MAAM,gCAAgC,IAAI,CAAC;AAClD,6CAA6C;AAC7C,eAAO,MAAM,+BAA+B,IAAI,CAAC;AACjD,kCAAkC;AAClC,eAAO,MAAM,qCAAqC,IAAI,CAAC;AACvD,4BAA4B;AAC5B,eAAO,MAAM,uBAAuB,IAAI,CAAC;AACzC,4BAA4B;AAC5B,eAAO,MAAM,uBAAuB,IAAI,CAAC;AACzC,0BAA0B;AAC1B,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAa/C;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,UAAU,EACnB,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,WAAW,GACnB,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAmB1B;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,WAAW,EACrB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAChC,WAAW,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAc7C;AA4FD,YAAY,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAI7E,wBAAuB,eAAe,CACpC,MAAM,EAAE,UAAU,EAClB,WAAW,EAAE,iBAAiB,EAC9B,eAAe,CAAC,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE;AACrD,gFAAgF;AAChF,WAAW,CAAC,EAAE,iBAAiB,EAC/B,OAAO,CAAC,EAAE,sBAAsB,GAC/B,cAAc,CAAC,sBAAsB,CAAC,CA83CxC"}
1
+ {"version":3,"file":"geometry-parallel.d.ts","sourceRoot":"","sources":["../src/geometry-parallel.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAE7E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAazD,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAgB7G,OAAO,EACL,uBAAuB,EAAE,yBAAyB,EAAE,gCAAgC,EAAE,+BAA+B,EACrH,qCAAqC,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,4BAA4B,EACrH,uBAAuB,EAAE,mBAAmB,GAC7C,MAAM,sBAAsB,CAAC;AAuD9B,YAAY,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAM7E,wBAAuB,eAAe,CACpC,MAAM,EAAE,UAAU,EAClB,WAAW,EAAE,iBAAiB,EAC9B,eAAe,CAAC,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE;AACrD,gFAAgF;AAChF,WAAW,CAAC,EAAE,iBAAiB,EAC/B,OAAO,CAAC,EAAE,sBAAsB,GAC/B,cAAc,CAAC,sBAAsB,CAAC,CAu4CxC"}
@@ -3,7 +3,8 @@
3
3
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
4
  import { postGeometryWorkerInit } from './geometry-worker-init.js';
5
5
  import { mergeGeometryDiagnostics } from './diagnostics.js';
6
- import { computeWorkerCount } from './worker-count.js';
6
+ import { planLoadWorkerCount } from './worker-count.js';
7
+ import { readBatchSizingOverride, readShardScanFlag, readVisibilityFilterOverride } from './perf-flags.js';
7
8
  import { notifyIfWasmAssetUnavailable, notifyIfWorkerScriptUnavailable } from './wasm-asset-error.js';
8
9
  import { restashWasmPanicLocation } from './wasm-panic-forward.js';
9
10
  import { mergeShardStyleSlices } from './shard-style-merge.js';
@@ -12,138 +13,14 @@ import { mergeShardStyleSlices } from './shard-style-merge.js';
12
13
  // (and vice versa) instead of fetching the same binary a second time.
13
14
  import { compileSharedWasmModule } from './wasm-shared-module.js';
14
15
  import { stitchShards } from './shard-stitch.js';
16
+ import { spawnGeometryWorker } from './geometry-worker-pool.js';
17
+ export { GeometryWorkerPool, DEFAULT_POOL_LIMITS, FRESH_WORKER_HEAP_BYTES } from './geometry-worker-pool.js';
18
+ import { NOOP_LOAD_TRACE, accountWorkerMessages, countCopy, enableWorkerTrace, isTraceSpansMessage } from '@ifc-lite/load-trace';
15
19
  import { resolveRtcFrame } from './rtc-frame.js';
16
20
  import { emptyStylesPrepassEvent, frozenStallPhase, GateTracker, PhaseBoundTimers, preWorkerPhaseFailureDiagnostics, } from './stall-phase.js';
17
21
  import { SkippedHungElementsCollector, startHungJobMonitor, WorkerJobLedger, } from './hung-job-recovery.js';
18
- /**
19
- * Prepass class-byte layout, mirroring the `PREPASS_CLASS_*` definitions in
20
- * `rust/processing/src/shard_classes.rs` (the source of truth — these are
21
- * pinned to it by `prepass-class-spans.test.ts`).
22
- *
23
- * The producer packs a named code in the LOW bits and composes FLAG bits on
24
- * top (`PREPASS_CLASS_FLAG_GEOMETRY_JOB` 0x80, `..._FLAG_TYPE_CANDIDATE`
25
- * 0x40), so a consumer must mask before comparing — the Rust consumer does
26
- * (`gpu_meshes/prepass_discovery.rs`), and so does {@link extractPrepassSpanLists}.
27
- */
28
- export const PREPASS_CLASS_CODE_MASK = 0x3f;
29
- /** `IFCSTYLEDITEM`. */
30
- export const PREPASS_CLASS_STYLED_ITEM = 4;
31
- /** `IFCINDEXEDCOLOURMAP`. */
32
- export const PREPASS_CLASS_INDEXED_COLOUR_MAP = 5;
33
- /** `IFCMATERIALDEFINITIONREPRESENTATION`. */
34
- export const PREPASS_CLASS_MATERIAL_DEF_REPR = 6;
35
- /** `IFCRELASSOCIATESMATERIAL`. */
36
- export const PREPASS_CLASS_REL_ASSOCIATES_MATERIAL = 7;
37
- /** `IFCRELVOIDSELEMENT`. */
38
- export const PREPASS_CLASS_REL_VOIDS = 8;
39
- /** `IFCRELFILLSELEMENT`. */
40
- export const PREPASS_CLASS_REL_FILLS = 9;
41
- /** `IFCRELAGGREGATES`. */
42
- export const PREPASS_CLASS_REL_AGGREGATES = 10;
43
- /** The classes the host builds span lists for (every other code is ignored). */
44
- const HOST_SPAN_CLASSES = [
45
- PREPASS_CLASS_STYLED_ITEM,
46
- PREPASS_CLASS_INDEXED_COLOUR_MAP,
47
- PREPASS_CLASS_MATERIAL_DEF_REPR,
48
- PREPASS_CLASS_REL_ASSOCIATES_MATERIAL,
49
- PREPASS_CLASS_REL_VOIDS,
50
- PREPASS_CLASS_REL_FILLS,
51
- PREPASS_CLASS_REL_AGGREGATES,
52
- ];
53
- /**
54
- * Build one `(id, start, length)` span list per host-consumed prepass class
55
- * from the stitched shard columns, in FILE ORDER. Every comparison goes
56
- * through {@link PREPASS_CLASS_CODE_MASK}, so a record that carries a flag bit
57
- * alongside its named code still lands in its list instead of being dropped.
58
- * Returns exact-size arrays (one entry per class in `HOST_SPAN_CLASSES`,
59
- * empty when the file has none).
60
- */
61
- export function extractPrepassSpanLists(classes, ids, starts, lengths) {
62
- // Sized by the code mask rather than by the highest class the host consumes:
63
- // a masked code is always < 64, so a class added on the Rust side cannot
64
- // write out of bounds here (typed arrays discard such writes silently).
65
- const counts = new Uint32Array(PREPASS_CLASS_CODE_MASK + 1);
66
- for (let i = 0; i < classes.length; i++)
67
- counts[classes[i] & PREPASS_CLASS_CODE_MASK]++;
68
- const slots = new Map();
69
- for (const k of HOST_SPAN_CLASSES)
70
- slots.set(k, { arr: new Uint32Array(counts[k] * 3), w: 0 });
71
- for (let i = 0; i < classes.length; i++) {
72
- const slot = slots.get(classes[i] & PREPASS_CLASS_CODE_MASK);
73
- if (!slot)
74
- continue;
75
- slot.arr[slot.w] = ids[i];
76
- slot.arr[slot.w + 1] = starts[i];
77
- slot.arr[slot.w + 2] = lengths[i];
78
- slot.w += 3;
79
- }
80
- const spans = new Map();
81
- for (const [k, slot] of slots)
82
- spans.set(k, slot.arr);
83
- return spans;
84
- }
85
- /**
86
- * Plan content-affinity routing for one chunk: assign each job (by index) to a
87
- * worker bucket so that every job sharing an affinity key lands on the SAME
88
- * worker — across the whole stream, since `keyToWorker` is the caller's sticky
89
- * map. New keys are handed out round-robin from `startWorker`, so each worker
90
- * owns roughly `1/workerCount` of the distinct keys (≈ distinct geometries, the
91
- * dominant meshing cost). Pure: mutates only the passed `keyToWorker` and returns
92
- * the advanced round-robin cursor. (#1130 follow-up — see `affinity_key` in Rust.)
93
- */
94
- export function planAffinityRouting(affinity, totalJobs, workerCount, keyToWorker, startWorker) {
95
- const buckets = Array.from({ length: workerCount }, () => []);
96
- let nextWorker = startWorker % workerCount;
97
- for (let j = 0; j < totalJobs; j++) {
98
- const key = affinity[j];
99
- let w = keyToWorker.get(key);
100
- if (w === undefined) {
101
- w = nextWorker;
102
- nextWorker = (nextWorker + 1) % workerCount;
103
- keyToWorker.set(key, w);
104
- }
105
- buckets[w].push(j);
106
- }
107
- return { buckets, nextWorker };
108
- }
109
- /**
110
- * Optional runtime override for the geometry worker's adaptive batch sizing
111
- * (#1097), read off `globalThis` on the host thread. A zero-cost escape hatch
112
- * for hardware-specific tuning / field-debugging the watchdog↔throughput
113
- * trade-off without a rebuild; unset ⇒ the worker uses DEFAULT_BATCH_SIZING.
114
- * Validation/merge happens in the worker via `resolveBatchSizing`.
115
- */
116
- function readBatchSizingOverride() {
117
- const g = globalThis;
118
- const v = g.__IFC_LITE_BATCH_SIZING;
119
- return v && typeof v === 'object' ? v : undefined;
120
- }
121
- /**
122
- * Optional load-time visibility filter (#1097), read off `globalThis` on the
123
- * host thread (tuning / benchmarking escape hatch). `{ disabledTypes,
124
- * skipTypeGeometry }` skip the matching geometry jobs at the prepass so they're
125
- * never decoded/meshed/uploaded. Unset ⇒ load everything.
126
- */
127
- function readVisibilityFilterOverride() {
128
- const g = globalThis;
129
- const v = g.__IFC_LITE_VISIBILITY_FILTER;
130
- return v && typeof v === 'object' ? v : undefined;
131
- }
132
- /**
133
- * SPIKE flag: shard the entity-index scan across the idle geometry workers and
134
- * deliver the stitched index early, instead of waiting for the pre-pass
135
- * worker's single-threaded post-scan `entity-index` emission. Read off
136
- * `globalThis.__IFC_LITE_SHARD_SCAN` (benchmark A/B knob). Truthy ⇒ on.
137
- */
138
- function readShardScanFlag() {
139
- const g = globalThis;
140
- const v = g.__IFC_LITE_SHARD_SCAN;
141
- // ON by default; 0/'0'/false is the kill switch (same convention as the
142
- // other #1682 load/render knobs).
143
- if (v === 0 || v === '0' || v === false)
144
- return false;
145
- return true;
146
- }
22
+ export { PREPASS_CLASS_CODE_MASK, PREPASS_CLASS_STYLED_ITEM, PREPASS_CLASS_INDEXED_COLOUR_MAP, PREPASS_CLASS_MATERIAL_DEF_REPR, PREPASS_CLASS_REL_ASSOCIATES_MATERIAL, PREPASS_CLASS_REL_VOIDS, PREPASS_CLASS_REL_FILLS, PREPASS_CLASS_REL_AGGREGATES, extractPrepassSpanLists, planAffinityRouting, } from './prepass-routing.js';
23
+ import { PREPASS_CLASS_STYLED_ITEM, PREPASS_CLASS_INDEXED_COLOUR_MAP, PREPASS_CLASS_MATERIAL_DEF_REPR, PREPASS_CLASS_REL_ASSOCIATES_MATERIAL, PREPASS_CLASS_REL_VOIDS, PREPASS_CLASS_REL_FILLS, PREPASS_CLASS_REL_AGGREGATES, extractPrepassSpanLists, planAffinityRouting, } from './prepass-routing.js';
147
24
  /**
148
25
  * Terminate a pool worker on a teardown path that is already unwinding.
149
26
  *
@@ -182,10 +59,15 @@ function entityIndexSetup(columns) {
182
59
  };
183
60
  }
184
61
  let nextSourceSessionId = 0;
62
+ /** A load above this size terminates the prewarmed workers it did not lease (#7036). */
63
+ const LARGE_LOAD_POOL_DRAIN_MB = 64;
185
64
  export async function* processParallel(buffer, coordinator, sharedRtcOffset,
186
65
  /** Optional pre-allocated SAB the caller already shares with another worker. */
187
66
  existingSab, options) {
188
67
  const sourceSessionId = `geometry-source-${++nextSourceSessionId}`;
68
+ // #6956: spans for this pool; worker spans hang under `geometry.pool`.
69
+ const trace = options?.trace ?? NOOP_LOAD_TRACE;
70
+ let poolSpan = -1, tracedWorkers = 0;
189
71
  coordinator.reset();
190
72
  yield { type: 'start', totalEstimate: buffer.length / 1000 };
191
73
  yield { type: 'model-open', modelID: 0 };
@@ -210,12 +92,18 @@ existingSab, options) {
210
92
  }
211
93
  else {
212
94
  sharedBuffer = new SharedArrayBuffer(buffer.byteLength);
213
- new Uint8Array(sharedBuffer).set(buffer);
95
+ new Uint8Array(sharedBuffer).set(countCopy('source.geometrySab', buffer));
214
96
  }
215
97
  // N independent WASM-instance workers, each running
216
98
  // `geometry.worker.js` (one `@ifc-lite/wasm` instance per worker).
217
- const makeGeometryWorker = () => new Worker(new URL('./geometry.worker.js', import.meta.url), { type: 'module' });
218
- const makePrepassWorker = () => new Worker(new URL('./geometry.worker.js', import.meta.url), { type: 'module' });
99
+ // #6957: `accountWorkerMessages` counts each pool's messages per direction (identity when counters are off).
100
+ // #7036: lease the workers a host prewarmed when this load was requested
101
+ // (spawned beside the file read); spawn the rest. Either way a worker serves
102
+ // this load only and is terminated by it, exactly as before.
103
+ const pool = options?.workerPool ?? null, poolKey = options?.wasmUrls?.wasm ?? '';
104
+ const acquireWorker = (role, thread) => enableWorkerTrace(pool ? pool.acquire(role, poolKey).worker : accountWorkerMessages(spawnGeometryWorker(), role), trace, thread);
105
+ const makeGeometryWorker = () => acquireWorker('geometry', `geom-${tracedWorkers++}`);
106
+ const makePrepassWorker = () => acquireWorker('prepass', 'prepass');
219
107
  // Shared aggregator state used by every worker callback below.
220
108
  const eventQueue = [];
221
109
  let resolveWaiting = null;
@@ -274,7 +162,7 @@ existingSab, options) {
274
162
  let entityIndexDeliveredEarly = false;
275
163
  const shardResults = [];
276
164
  let shardResultsRemaining = 0;
277
- let shardScanDispatchedAt = -1;
165
+ let shardScanDispatchedAt = -1, shardScanSpan = -1;
278
166
  // #4902: settles once, normally or via the bounded-wait timeout below.
279
167
  let shardScanSettled = false;
280
168
  let stylesSlicesSettled = false;
@@ -316,6 +204,10 @@ existingSab, options) {
316
204
  workerSpoke = true;
317
205
  ledger.onHeard(workerIndex, performance.now());
318
206
  const msg = e.data;
207
+ if (isTraceSpansMessage(msg)) {
208
+ trace.merge(msg.payload, poolSpan);
209
+ return;
210
+ }
319
211
  if (msg.type === 'ready') {
320
212
  console.log(`[stream] worker[${workerIndex}] WASM ready @ ${elapsed()}ms`);
321
213
  return;
@@ -394,6 +286,11 @@ existingSab, options) {
394
286
  }
395
287
  if (msg.diagnostics)
396
288
  diagnostics = mergeGeometryDiagnostics(diagnostics, msg.diagnostics);
289
+ // An in-call heartbeat (no seq) past the absolute call bound no longer
290
+ // counts as liveness, so with recovery off the consumer watchdog still
291
+ // bounds a call that reports progress forever (MAX_GEOMETRY_CALL_MS).
292
+ if (typeof msg.seq !== 'number' && ledger.callOverCap(workerIndex, performance.now()))
293
+ return;
397
294
  eventQueue.push({ type: 'progress', phase: 'workers' });
398
295
  wake();
399
296
  return;
@@ -405,6 +302,7 @@ existingSab, options) {
405
302
  if (msg.type === 'batch') {
406
303
  if (firstBatchByWorker[workerIndex] === undefined) {
407
304
  firstBatchByWorker[workerIndex] = elapsed();
305
+ trace.milestone('geometry.firstBatch');
408
306
  console.log(`[stream] worker[${workerIndex}] first batch @ ${elapsed()}ms (${msg.meshes?.length ?? 0} meshes)`);
409
307
  }
410
308
  // The worker already emits `MeshData`-shaped objects (see the worker's
@@ -521,18 +419,8 @@ existingSab, options) {
521
419
  // totalJobs estimate; use file-size proxy. The memory-budget cap in
522
420
  // `computeWorkerCount` keeps an over-estimate harmless, and still bounds an
523
421
  // explicit `workerCountOverride` so the A/B knob can't OOM the tab.
524
- const cores = typeof navigator !== 'undefined' ? (navigator.hardwareConcurrency ?? 2) : 2;
525
- const deviceMemoryGB = typeof navigator !== 'undefined'
526
- ? (navigator.deviceMemory ?? 8) : 8;
527
422
  const fileSizeMB = buffer.byteLength / (1024 * 1024);
528
- const estimatedJobs = Math.max(1, Math.ceil(fileSizeMB * 100));
529
- const workerCountResult = computeWorkerCount({
530
- fileSizeMB,
531
- cores,
532
- deviceMemoryGB,
533
- totalJobs: estimatedJobs,
534
- workerCountOverride: options?.workerCountOverride,
535
- });
423
+ const { cores, ...workerCountResult } = planLoadWorkerCount(fileSizeMB, options?.workerCountOverride);
536
424
  const workerCount = workerCountResult.count;
537
425
  // Await the ONE shared compile (started up-front) so every worker instantiates
538
426
  // the SAME module via `initSync` instead of recompiling the binary N+1 times.
@@ -788,6 +676,8 @@ existingSab, options) {
788
676
  // Step-by-step timing so we can tell exactly where time goes.
789
677
  const t0 = performance.now();
790
678
  const elapsed = () => Math.round(performance.now() - t0);
679
+ trace.setAttrs({ workerCount });
680
+ poolSpan = trace.begin('geometry.pool', { workerCount });
791
681
  const overrideNote = options?.workerCountOverride != null
792
682
  ? ` (override=${options.workerCountOverride}, bound=${workerCountResult.reason})`
793
683
  : ` (cores=${cores}, bound=${workerCountResult.reason})`;
@@ -807,6 +697,7 @@ existingSab, options) {
807
697
  // record the parser will not find" but "every record after it is missing".
808
698
  malformedRecordCount) => {
809
699
  console.log(`[stream] entity-index (${source}) @ ${elapsed()}ms (${ids.length} entries)`);
700
+ trace.milestone('geometry.entityIndex');
810
701
  if (typeof SharedArrayBuffer !== 'undefined') {
811
702
  // Sharded-stitch columns arrive already SAB-backed (stitchShards writes
812
703
  // its exact-size output into SABs) — share them as-is. The serial
@@ -874,8 +765,9 @@ existingSab, options) {
874
765
  return;
875
766
  shardScanSettled = true;
876
767
  gateTracker.markShardScanDone();
768
+ trace.end(shardScanSpan);
877
769
  const shards = shardResults;
878
- const stitched = stitchShards(shards);
770
+ const stitched = trace.span('shard.stitch', () => stitchShards(shards), undefined, poolSpan);
879
771
  if (!stitched) {
880
772
  console.warn('[stream][shard] stitch fallback triggered (handoff not found) — serial pre-pass');
881
773
  startPrepass(false);
@@ -1020,6 +912,7 @@ existingSab, options) {
1020
912
  shardResults.length = n;
1021
913
  shardResultsRemaining = n;
1022
914
  shardScanDispatchedAt = elapsed();
915
+ shardScanSpan = trace.begin('geometry.shardScan', { shards: n }, poolSpan);
1023
916
  gateTracker.markShardScanStarted();
1024
917
  console.log(`[stream][shard] dispatching ${n} shard scans over ${(len / (1024 * 1024)).toFixed(1)}MB @ ${shardScanDispatchedAt}ms`);
1025
918
  for (let i = 0; i < n; i++) {
@@ -1039,6 +932,7 @@ existingSab, options) {
1039
932
  shardScanSettled = true;
1040
933
  gateTracker.markShardScanDone();
1041
934
  console.warn(`[stream][shard] ${shardResultsRemaining}/${n} shard scan(s) silent — falling back to the serial pre-pass (#4902)`);
935
+ trace.end(shardScanSpan, { fallback: true });
1042
936
  diagnostics = mergeGeometryDiagnostics(diagnostics, preWorkerPhaseFailureDiagnostics('shard-scan-timeout'));
1043
937
  // Replace every worker still silent on scan-shard BEFORE continuing:
1044
938
  // `scan-shard` is a synchronous WASM call, so a missing reply means
@@ -1052,6 +946,9 @@ existingSab, options) {
1052
946
  });
1053
947
  }
1054
948
  const prepassWorker = makePrepassWorker();
949
+ // #7036: idle workers this large load did not lease would only add to its peak.
950
+ if (pool && fileSizeMB > LARGE_LOAD_POOL_DRAIN_MB)
951
+ pool.drain('large-load');
1055
952
  // Wrap the rest of the pipeline so worker teardown runs not only on
1056
953
  // normal completion / error / zero-jobs branches, but also when the
1057
954
  // consumer abandons the generator via `.return()` / `.throw()` while it
@@ -1111,6 +1008,10 @@ existingSab, options) {
1111
1008
  prepassWorker.onmessage = (e) => {
1112
1009
  prepassSpoke = true;
1113
1010
  const data = e.data;
1011
+ if (isTraceSpansMessage(data)) {
1012
+ trace.merge(data.payload, poolSpan);
1013
+ return;
1014
+ }
1114
1015
  if (data.type === 'prepass-progress') {
1115
1016
  eventQueue.push({ type: 'progress', phase: 'prepass' });
1116
1017
  wake();
@@ -1164,6 +1065,7 @@ existingSab, options) {
1164
1065
  const materialColorCounts = evt.materialColorCounts ?? new Uint32Array(0);
1165
1066
  const materialColors = evt.materialColors ?? new Uint8Array(0);
1166
1067
  console.log(`[stream] styles @ ${elapsed()}ms (${styleIds.length} styled, ${voidKeys.length} void hosts), draining ${queuedChunks.length} queued chunks`);
1068
+ trace.milestone('prepass.styles');
1167
1069
  broadcastSetup((w) => {
1168
1070
  // Slice each typed array per-worker so each can be in its own
1169
1071
  // transfer list without conflict. The slice cost is bounded by
@@ -1290,6 +1192,7 @@ existingSab, options) {
1290
1192
  else if (evt.type === 'complete') {
1291
1193
  prepassJobsTotal = evt.totalJobs;
1292
1194
  console.log(`[stream] prepass complete @ ${elapsed()}ms totalJobs=${prepassJobsTotal} chunks=${chunkArrivals}`);
1195
+ trace.milestone('prepass.complete');
1293
1196
  // Unconditionally drive the prepass-complete handler here.
1294
1197
  // The outer loop's `prepassJobsTotal > 0` gate would skip
1295
1198
  // zero-geometry files (no IFC geometry entities), causing
@@ -1501,6 +1404,7 @@ existingSab, options) {
1501
1404
  };
1502
1405
  }
1503
1406
  finally {
1407
+ trace.end(poolSpan);
1504
1408
  phaseBoundTimers.clearAll();
1505
1409
  // Re-seat the caller's handle to a frozen snapshot (#4979 review): the
1506
1410
  // live reader closes over `gateTracker`, and transitively this whole