@bldrs-ai/conway 1.400.1226 → 1.403.1239
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/compiled/Dist/ConwayGeomWasmNode.js +0 -0
- package/compiled/Dist/ConwayGeomWasmNodeMT.js +0 -0
- package/compiled/Dist/ConwayGeomWasmWeb.js +0 -0
- package/compiled/Dist/ConwayGeomWasmWebMT.js +1 -1
- package/compiled/Dist/ConwayGeomWasmWebMT.wasm +0 -0
- package/compiled/dependencies/conway-geom/Dist/ConwayGeomWasmNode.js +0 -0
- package/compiled/dependencies/conway-geom/Dist/ConwayGeomWasmNodeMT.js +0 -0
- package/compiled/dependencies/conway-geom/Dist/ConwayGeomWasmWeb.js +0 -0
- package/compiled/dependencies/conway-geom/Dist/ConwayGeomWasmWebMT.js +1 -1
- package/compiled/dependencies/conway-geom/Dist/ConwayGeomWasmWebMT.wasm +0 -0
- package/compiled/examples/browser-bundled.cjs +13 -6
- package/compiled/examples/cli-bundled.cjs +42 -35
- package/compiled/examples/cli-step-bundled.cjs +40 -33
- package/compiled/examples/validator-bundled.cjs +13 -6
- package/compiled/src/core/demand_geometry_queue.d.ts +147 -0
- package/compiled/src/core/demand_geometry_queue.d.ts.map +1 -0
- package/compiled/src/core/demand_geometry_queue.js +223 -0
- package/compiled/src/core/demand_geometry_queue.test.d.ts +2 -0
- package/compiled/src/core/demand_geometry_queue.test.d.ts.map +1 -0
- package/compiled/src/core/demand_geometry_queue.test.js +133 -0
- package/compiled/src/step/parsing/incremental_type_index.d.ts +64 -0
- package/compiled/src/step/parsing/incremental_type_index.d.ts.map +1 -0
- package/compiled/src/step/parsing/incremental_type_index.js +92 -0
- package/compiled/src/step/parsing/incremental_type_index.test.d.ts +2 -0
- package/compiled/src/step/parsing/incremental_type_index.test.d.ts.map +1 -0
- package/compiled/src/step/parsing/incremental_type_index.test.js +55 -0
- package/compiled/src/step/parsing/step_parser.d.ts +6 -1
- package/compiled/src/step/parsing/step_parser.d.ts.map +1 -1
- package/compiled/src/step/parsing/step_parser.js +10 -3
- package/compiled/src/step/parsing/streaming_index_builder.d.ts +11 -1
- package/compiled/src/step/parsing/streaming_index_builder.d.ts.map +1 -1
- package/compiled/src/step/parsing/streaming_index_builder.js +12 -2
- package/compiled/src/step/parsing/streaming_record_dispatcher.d.ts +55 -0
- package/compiled/src/step/parsing/streaming_record_dispatcher.d.ts.map +1 -0
- package/compiled/src/step/parsing/streaming_record_dispatcher.js +67 -0
- package/compiled/src/step/parsing/streaming_record_dispatcher.test.d.ts +2 -0
- package/compiled/src/step/parsing/streaming_record_dispatcher.test.d.ts.map +1 -0
- package/compiled/src/step/parsing/streaming_record_dispatcher.test.js +66 -0
- package/compiled/src/version/version.js +1 -1
- package/compiled/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -944,7 +944,7 @@ var EntityTypesIfcCount = 909;
|
|
|
944
944
|
var entity_types_ifc_gen_default = EntityTypesIfc;
|
|
945
945
|
|
|
946
946
|
// compiled/src/version/version.js
|
|
947
|
-
var versionString = "Conway v1.
|
|
947
|
+
var versionString = "Conway v1.403.1239";
|
|
948
948
|
|
|
949
949
|
// compiled/dependencies/conway-geom/interface/conway_geometry.js
|
|
950
950
|
var wasmType = "";
|
|
@@ -3493,11 +3493,16 @@ var StepParser = class extends StepHeaderParser {
|
|
|
3493
3493
|
* @param input The input parsing buffer, positioned at the data section.
|
|
3494
3494
|
* @param onRecordBoundary Called at each top-level record boundary with the
|
|
3495
3495
|
* buffer; the callback may rebase the buffer's window in place.
|
|
3496
|
+
* @param onRecordIndexed Called as each top-level record is indexed, with
|
|
3497
|
+
* its localID, expressID and typeID (0 for external-mapping records) — the
|
|
3498
|
+
* seam for incremental semantic consumers (type index, roots registry,
|
|
3499
|
+
* names skeleton). Must be synchronous and cheap; expensive work belongs on
|
|
3500
|
+
* a demand queue, not the parse path.
|
|
3496
3501
|
* @param onProgress Optional byte-cursor progress callback.
|
|
3497
3502
|
* @return {BlockParseResult} The parsing result, including the index and result enum.
|
|
3498
3503
|
*/
|
|
3499
|
-
parseDataBlockStreamed(input, onRecordBoundary, onProgress) {
|
|
3500
|
-
const parser211 = this.parseDataBlockIncremental(input, onRecordBoundary);
|
|
3504
|
+
parseDataBlockStreamed(input, onRecordBoundary, onRecordIndexed, onProgress) {
|
|
3505
|
+
const parser211 = this.parseDataBlockIncremental(input, onRecordBoundary, onRecordIndexed);
|
|
3501
3506
|
while (true) {
|
|
3502
3507
|
const next = parser211.next();
|
|
3503
3508
|
if (next.done === true) {
|
|
@@ -3543,7 +3548,7 @@ var StepParser = class extends StepHeaderParser {
|
|
|
3543
3548
|
* @yields {number} The current byte cursor within the input buffer.
|
|
3544
3549
|
* @return {BlockParseResult} The parsing result, including the index and result enum.
|
|
3545
3550
|
*/
|
|
3546
|
-
*parseDataBlockIncremental(input, onRecordBoundary) {
|
|
3551
|
+
*parseDataBlockIncremental(input, onRecordBoundary, onRecordIndexed) {
|
|
3547
3552
|
const indexResult = { elements: [] };
|
|
3548
3553
|
const match = input.match;
|
|
3549
3554
|
const comment = () => match(commentParser2);
|
|
@@ -3720,6 +3725,7 @@ var StepParser = class extends StepHeaderParser {
|
|
|
3720
3725
|
if (!charws(SEMICOLON)) {
|
|
3721
3726
|
return syntaxError();
|
|
3722
3727
|
}
|
|
3728
|
+
onRecordIndexed?.(indexResult.elements.length, expressID, 0);
|
|
3723
3729
|
indexResult.elements.push({
|
|
3724
3730
|
address: startElement2,
|
|
3725
3731
|
length: input.address - startElement2,
|
|
@@ -3811,6 +3817,7 @@ var StepParser = class extends StepHeaderParser {
|
|
|
3811
3817
|
if (!charws(SEMICOLON)) {
|
|
3812
3818
|
return syntaxError();
|
|
3813
3819
|
}
|
|
3820
|
+
onRecordIndexed?.(indexResult.elements.length, expressID, foundItem);
|
|
3814
3821
|
indexResult.elements.push({
|
|
3815
3822
|
address: startElement,
|
|
3816
3823
|
length: input.address - startElement,
|
|
@@ -70774,7 +70781,7 @@ var IfcStepModel = class extends StepModelBase {
|
|
|
70774
70781
|
};
|
|
70775
70782
|
|
|
70776
70783
|
// compiled/src/step/parsing/streaming_index_builder.js
|
|
70777
|
-
function buildIndexStreaming(source, parser211, pool3) {
|
|
70784
|
+
function buildIndexStreaming(source, parser211, pool3, onRecordIndexed) {
|
|
70778
70785
|
const fileSize = source.byteLength;
|
|
70779
70786
|
let windowBytes = Math.max(pool3, MIN_WINDOW);
|
|
70780
70787
|
for (; ; ) {
|
|
@@ -70820,7 +70827,7 @@ function buildIndexStreaming(source, parser211, pool3) {
|
|
|
70820
70827
|
buffer.rebaseWindow(window2, 0, windowLen, windowStartFile);
|
|
70821
70828
|
++slides;
|
|
70822
70829
|
};
|
|
70823
|
-
const [index, result] = parser211.parseDataBlockStreamed(input, onRecordBoundary);
|
|
70830
|
+
const [index, result] = parser211.parseDataBlockStreamed(input, onRecordBoundary, onRecordIndexed);
|
|
70824
70831
|
const stoppedShort = result !== ParseResult.COMPLETE;
|
|
70825
70832
|
const notAtEof = windowStartFile + windowLen < fileSize;
|
|
70826
70833
|
if (stoppedShort && notAtEof) {
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Demand-driven, budgeted geometry materialisation (M3).
|
|
3
|
+
*
|
|
4
|
+
* The streaming loader inverts geometry: instead of extracting every product
|
|
5
|
+
* up front, extraction becomes a **cache fill keyed by product**, ordered by
|
|
6
|
+
* demand (viewport frustum + distance, explicit selection, prefetch hints)
|
|
7
|
+
* and bounded by an explicit byte budget. This module is the scheduler: a
|
|
8
|
+
* priority queue over product local IDs plus an evictable resident set, with
|
|
9
|
+
* the actual extract / release delegated to a pluggable {@link GeometryTiles}.
|
|
10
|
+
*
|
|
11
|
+
* The heavy half — the wasm-side extract → tessellate → upload and the
|
|
12
|
+
* per-product native reclaim — lives behind {@link GeometryTiles}. Its
|
|
13
|
+
* production implementation needs a **conway-geom API to free one product's
|
|
14
|
+
* native geometry** (today the wasm heap has no per-product reclaim); scoping
|
|
15
|
+
* that C++ surface is the M3 blocker tracked in the design doc. This scheduler
|
|
16
|
+
* is engine-side, fully deterministic, and tested against a mock tiles impl,
|
|
17
|
+
* so the queue/budget/eviction policy is settled independently of the wasm
|
|
18
|
+
* work.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* The per-product geometry work behind the queue. Implementations do the
|
|
22
|
+
* expensive materialisation; the queue owns only ordering and the budget.
|
|
23
|
+
*/
|
|
24
|
+
export interface GeometryTiles {
|
|
25
|
+
/**
|
|
26
|
+
* Materialise a product's geometry tile and return its resident cost in
|
|
27
|
+
* bytes (GPU/scene + any retained wasm working set the budget should count).
|
|
28
|
+
* Called at most once per product between evictions.
|
|
29
|
+
*
|
|
30
|
+
* @param productLocalID The product's local ID.
|
|
31
|
+
* @return {number} The tile's resident byte cost (≥ 0).
|
|
32
|
+
*/
|
|
33
|
+
extract(productLocalID: number): number;
|
|
34
|
+
/**
|
|
35
|
+
* Release a product's materialised tile, freeing its resident bytes — the
|
|
36
|
+
* scene mesh and, crucially, the product's native (wasm) geometry. This is
|
|
37
|
+
* the per-product reclaim the production conway-geom surface must provide.
|
|
38
|
+
*
|
|
39
|
+
* @param productLocalID The product's local ID.
|
|
40
|
+
*/
|
|
41
|
+
release(productLocalID: number): void;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Runtime counters for a queue, for tests and telemetry.
|
|
45
|
+
*/
|
|
46
|
+
export interface DemandQueueStats {
|
|
47
|
+
extractions: number;
|
|
48
|
+
evictions: number;
|
|
49
|
+
residentBytes: number;
|
|
50
|
+
residentCount: number;
|
|
51
|
+
pendingCount: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A budgeted, demand-ordered geometry materialisation queue.
|
|
55
|
+
*
|
|
56
|
+
* Usage: `request(productLocalID, priority)` as demand changes (higher
|
|
57
|
+
* priority = more wanted); `pump()` to materialise the most-wanted pending
|
|
58
|
+
* products until the queue drains or the byte budget forces eviction of
|
|
59
|
+
* lower-priority resident tiles. Re-requesting an evicted product re-fills it.
|
|
60
|
+
* A steady stream of `request` + `pump` keeps a bounded working set of the
|
|
61
|
+
* highest-priority products resident — the "full model navigable under a
|
|
62
|
+
* fixed budget" behaviour.
|
|
63
|
+
*/
|
|
64
|
+
export declare class DemandGeometryQueue {
|
|
65
|
+
private readonly tiles_;
|
|
66
|
+
private readonly budgetBytes_;
|
|
67
|
+
/** Highest priority a product currently wants materialising at. */
|
|
68
|
+
private readonly pending_;
|
|
69
|
+
/** Resident tiles by product local ID. */
|
|
70
|
+
private readonly resident_;
|
|
71
|
+
private residentBytes_;
|
|
72
|
+
private extractions_;
|
|
73
|
+
private evictions_;
|
|
74
|
+
/**
|
|
75
|
+
* @param tiles The extract/release backend.
|
|
76
|
+
* @param budgetBytes Maximum resident tile bytes before eviction kicks in.
|
|
77
|
+
*/
|
|
78
|
+
constructor(tiles: GeometryTiles, budgetBytes: number);
|
|
79
|
+
/**
|
|
80
|
+
* Request a product be materialised at the given demand priority. Updates
|
|
81
|
+
* the priority if already pending (keeping the higher of the two) or already
|
|
82
|
+
* resident (so eviction ranks it correctly). Requesting a resident product
|
|
83
|
+
* does not re-extract it.
|
|
84
|
+
*
|
|
85
|
+
* @param productLocalID The product to materialise.
|
|
86
|
+
* @param priority The demand priority (higher = more wanted).
|
|
87
|
+
*/
|
|
88
|
+
request(productLocalID: number, priority: number): void;
|
|
89
|
+
/**
|
|
90
|
+
* Materialise pending products in descending priority order, evicting the
|
|
91
|
+
* lowest-priority resident tiles whenever the budget is exceeded. A pending
|
|
92
|
+
* product whose priority is below every resident tile AND that can't fit is
|
|
93
|
+
* left pending (it will fill once demand raises it or resident tiles drop).
|
|
94
|
+
*
|
|
95
|
+
* @param maxExtractions Optional cap on how many tiles to extract this pump
|
|
96
|
+
* (e.g. a per-frame budget); unbounded when omitted.
|
|
97
|
+
* @return {number} The number of tiles extracted this pump.
|
|
98
|
+
*/
|
|
99
|
+
pump(maxExtractions?: number): number;
|
|
100
|
+
/**
|
|
101
|
+
* Evict every resident tile (e.g. on model close), releasing all bytes.
|
|
102
|
+
*/
|
|
103
|
+
evictAll(): void;
|
|
104
|
+
/**
|
|
105
|
+
* @return {boolean} True if the product's tile is resident.
|
|
106
|
+
* @param productLocalID The product to check.
|
|
107
|
+
*/
|
|
108
|
+
isResident(productLocalID: number): boolean;
|
|
109
|
+
/**
|
|
110
|
+
* @return {DemandQueueStats} A snapshot of runtime counters.
|
|
111
|
+
*/
|
|
112
|
+
get stats(): DemandQueueStats;
|
|
113
|
+
/**
|
|
114
|
+
* Pop the highest-priority pending request. Linear scan — swap for a heap
|
|
115
|
+
* if a real workload shows the pending set growing large per pump.
|
|
116
|
+
*
|
|
117
|
+
* @return {PendingRequest | undefined} The request, or undefined if none.
|
|
118
|
+
*/
|
|
119
|
+
private popHighestPending_;
|
|
120
|
+
/**
|
|
121
|
+
* Ensure there is conceptual room for a tile requested at `priority` by
|
|
122
|
+
* evicting resident tiles strictly lower in priority while over budget.
|
|
123
|
+
* Returns false if the budget is full of tiles at least as wanted as this
|
|
124
|
+
* request (so it shouldn't displace them).
|
|
125
|
+
*
|
|
126
|
+
* @param priority The incoming request's priority.
|
|
127
|
+
* @return {boolean} True if extraction should proceed.
|
|
128
|
+
*/
|
|
129
|
+
private ensureRoomFor_;
|
|
130
|
+
/**
|
|
131
|
+
* Evict lowest-priority resident tiles until within budget (used after an
|
|
132
|
+
* extraction that pushed us over — e.g. a large tile).
|
|
133
|
+
*/
|
|
134
|
+
private evictToBudget_;
|
|
135
|
+
/**
|
|
136
|
+
* @return {{ productLocalID: number, priority: number } | undefined} The
|
|
137
|
+
* lowest-priority resident tile, or undefined if none.
|
|
138
|
+
*/
|
|
139
|
+
private lowestPriorityResident_;
|
|
140
|
+
/**
|
|
141
|
+
* Release one resident tile.
|
|
142
|
+
*
|
|
143
|
+
* @param productLocalID The tile to evict.
|
|
144
|
+
*/
|
|
145
|
+
private evictOne_;
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=demand_geometry_queue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"demand_geometry_queue.d.ts","sourceRoot":"","sources":["../../../src/core/demand_geometry_queue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;GAGG;AACH,MAAM,WAAW,aAAa;IAE5B;;;;;;;OAOG;IACH,OAAO,CAAE,cAAc,EAAE,MAAM,GAAI,MAAM,CAAA;IAEzC;;;;;;OAMG;IACH,OAAO,CAAE,cAAc,EAAE,MAAM,GAAI,IAAI,CAAA;CACxC;AAiBD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,aAAa,EAAE,MAAM,CAAA;IACrB,YAAY,EAAE,MAAM,CAAA;CACrB;AAGD;;;;;;;;;;GAUG;AACH,qBAAa,mBAAmB;IAE9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IAEtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IAErC,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4B;IAErD,0CAA0C;IAC1C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAkC;IAE5D,OAAO,CAAC,cAAc,CAAI;IAE1B,OAAO,CAAC,YAAY,CAAI;IAExB,OAAO,CAAC,UAAU,CAAI;IAEtB;;;OAGG;gBACU,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM;IAUtD;;;;;;;;OAQG;IACI,OAAO,CAAE,cAAc,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAI,IAAI;IAiBhE;;;;;;;;;OASG;IACI,IAAI,CAAE,cAAc,GAAE,MAAiC,GAAI,MAAM;IAoCxE;;OAEG;IACI,QAAQ,IAAI,IAAI;IASvB;;;OAGG;IACI,UAAU,CAAE,cAAc,EAAE,MAAM,GAAI,OAAO;IAIpD;;OAEG;IACH,IAAW,KAAK,IAAI,gBAAgB,CAQnC;IAED;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;IAqB1B;;;;;;;;OAQG;IACH,OAAO,CAAC,cAAc;IAuBtB;;;OAGG;IACH,OAAO,CAAC,cAAc;IAYtB;;;OAGG;IACH,OAAO,CAAC,uBAAuB;IAiB/B;;;;OAIG;IACH,OAAO,CAAC,SAAS;CAYlB"}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Demand-driven, budgeted geometry materialisation (M3).
|
|
3
|
+
*
|
|
4
|
+
* The streaming loader inverts geometry: instead of extracting every product
|
|
5
|
+
* up front, extraction becomes a **cache fill keyed by product**, ordered by
|
|
6
|
+
* demand (viewport frustum + distance, explicit selection, prefetch hints)
|
|
7
|
+
* and bounded by an explicit byte budget. This module is the scheduler: a
|
|
8
|
+
* priority queue over product local IDs plus an evictable resident set, with
|
|
9
|
+
* the actual extract / release delegated to a pluggable {@link GeometryTiles}.
|
|
10
|
+
*
|
|
11
|
+
* The heavy half — the wasm-side extract → tessellate → upload and the
|
|
12
|
+
* per-product native reclaim — lives behind {@link GeometryTiles}. Its
|
|
13
|
+
* production implementation needs a **conway-geom API to free one product's
|
|
14
|
+
* native geometry** (today the wasm heap has no per-product reclaim); scoping
|
|
15
|
+
* that C++ surface is the M3 blocker tracked in the design doc. This scheduler
|
|
16
|
+
* is engine-side, fully deterministic, and tested against a mock tiles impl,
|
|
17
|
+
* so the queue/budget/eviction policy is settled independently of the wasm
|
|
18
|
+
* work.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* A budgeted, demand-ordered geometry materialisation queue.
|
|
22
|
+
*
|
|
23
|
+
* Usage: `request(productLocalID, priority)` as demand changes (higher
|
|
24
|
+
* priority = more wanted); `pump()` to materialise the most-wanted pending
|
|
25
|
+
* products until the queue drains or the byte budget forces eviction of
|
|
26
|
+
* lower-priority resident tiles. Re-requesting an evicted product re-fills it.
|
|
27
|
+
* A steady stream of `request` + `pump` keeps a bounded working set of the
|
|
28
|
+
* highest-priority products resident — the "full model navigable under a
|
|
29
|
+
* fixed budget" behaviour.
|
|
30
|
+
*/
|
|
31
|
+
export class DemandGeometryQueue {
|
|
32
|
+
/**
|
|
33
|
+
* @param tiles The extract/release backend.
|
|
34
|
+
* @param budgetBytes Maximum resident tile bytes before eviction kicks in.
|
|
35
|
+
*/
|
|
36
|
+
constructor(tiles, budgetBytes) {
|
|
37
|
+
/** Highest priority a product currently wants materialising at. */
|
|
38
|
+
this.pending_ = new Map();
|
|
39
|
+
/** Resident tiles by product local ID. */
|
|
40
|
+
this.resident_ = new Map();
|
|
41
|
+
this.residentBytes_ = 0;
|
|
42
|
+
this.extractions_ = 0;
|
|
43
|
+
this.evictions_ = 0;
|
|
44
|
+
if (budgetBytes <= 0) {
|
|
45
|
+
throw new Error(`Invalid budgetBytes ${budgetBytes}`);
|
|
46
|
+
}
|
|
47
|
+
this.tiles_ = tiles;
|
|
48
|
+
this.budgetBytes_ = budgetBytes;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Request a product be materialised at the given demand priority. Updates
|
|
52
|
+
* the priority if already pending (keeping the higher of the two) or already
|
|
53
|
+
* resident (so eviction ranks it correctly). Requesting a resident product
|
|
54
|
+
* does not re-extract it.
|
|
55
|
+
*
|
|
56
|
+
* @param productLocalID The product to materialise.
|
|
57
|
+
* @param priority The demand priority (higher = more wanted).
|
|
58
|
+
*/
|
|
59
|
+
request(productLocalID, priority) {
|
|
60
|
+
const tile = this.resident_.get(productLocalID);
|
|
61
|
+
if (tile !== void 0) {
|
|
62
|
+
// Already resident — just refresh its eviction ranking.
|
|
63
|
+
tile.priority = Math.max(tile.priority, priority);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const existing = this.pending_.get(productLocalID);
|
|
67
|
+
this.pending_.set(productLocalID, existing === void 0 ? priority : Math.max(existing, priority));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Materialise pending products in descending priority order, evicting the
|
|
71
|
+
* lowest-priority resident tiles whenever the budget is exceeded. A pending
|
|
72
|
+
* product whose priority is below every resident tile AND that can't fit is
|
|
73
|
+
* left pending (it will fill once demand raises it or resident tiles drop).
|
|
74
|
+
*
|
|
75
|
+
* @param maxExtractions Optional cap on how many tiles to extract this pump
|
|
76
|
+
* (e.g. a per-frame budget); unbounded when omitted.
|
|
77
|
+
* @return {number} The number of tiles extracted this pump.
|
|
78
|
+
*/
|
|
79
|
+
pump(maxExtractions = Number.POSITIVE_INFINITY) {
|
|
80
|
+
let extractedThisPump = 0;
|
|
81
|
+
while (extractedThisPump < maxExtractions && this.pending_.size > 0) {
|
|
82
|
+
const next = this.popHighestPending_();
|
|
83
|
+
if (next === void 0) {
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
// If the working set is full and this request can't beat the cheapest
|
|
87
|
+
// evictable (lower-priority) resident tile, stop — nothing more fits.
|
|
88
|
+
if (!this.ensureRoomFor_(next.priority)) {
|
|
89
|
+
// Put it back; it stays pending for a future pump.
|
|
90
|
+
this.pending_.set(next.productLocalID, next.priority);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
const bytes = this.tiles_.extract(next.productLocalID);
|
|
94
|
+
this.resident_.set(next.productLocalID, { bytes, priority: next.priority });
|
|
95
|
+
this.residentBytes_ += bytes;
|
|
96
|
+
++this.extractions_;
|
|
97
|
+
++extractedThisPump;
|
|
98
|
+
// A single oversized tile can still exceed budget; evict what we can.
|
|
99
|
+
this.evictToBudget_();
|
|
100
|
+
}
|
|
101
|
+
return extractedThisPump;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Evict every resident tile (e.g. on model close), releasing all bytes.
|
|
105
|
+
*/
|
|
106
|
+
evictAll() {
|
|
107
|
+
for (const productLocalID of this.resident_.keys()) {
|
|
108
|
+
this.tiles_.release(productLocalID);
|
|
109
|
+
++this.evictions_;
|
|
110
|
+
}
|
|
111
|
+
this.resident_.clear();
|
|
112
|
+
this.residentBytes_ = 0;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* @return {boolean} True if the product's tile is resident.
|
|
116
|
+
* @param productLocalID The product to check.
|
|
117
|
+
*/
|
|
118
|
+
isResident(productLocalID) {
|
|
119
|
+
return this.resident_.has(productLocalID);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* @return {DemandQueueStats} A snapshot of runtime counters.
|
|
123
|
+
*/
|
|
124
|
+
get stats() {
|
|
125
|
+
return {
|
|
126
|
+
extractions: this.extractions_,
|
|
127
|
+
evictions: this.evictions_,
|
|
128
|
+
residentBytes: this.residentBytes_,
|
|
129
|
+
residentCount: this.resident_.size,
|
|
130
|
+
pendingCount: this.pending_.size,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Pop the highest-priority pending request. Linear scan — swap for a heap
|
|
135
|
+
* if a real workload shows the pending set growing large per pump.
|
|
136
|
+
*
|
|
137
|
+
* @return {PendingRequest | undefined} The request, or undefined if none.
|
|
138
|
+
*/
|
|
139
|
+
popHighestPending_() {
|
|
140
|
+
let bestID;
|
|
141
|
+
let bestPriority = Number.NEGATIVE_INFINITY;
|
|
142
|
+
for (const [productLocalID, priority] of this.pending_) {
|
|
143
|
+
if (priority > bestPriority) {
|
|
144
|
+
bestPriority = priority;
|
|
145
|
+
bestID = productLocalID;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (bestID === void 0) {
|
|
149
|
+
return void 0;
|
|
150
|
+
}
|
|
151
|
+
this.pending_.delete(bestID);
|
|
152
|
+
return { productLocalID: bestID, priority: bestPriority };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Ensure there is conceptual room for a tile requested at `priority` by
|
|
156
|
+
* evicting resident tiles strictly lower in priority while over budget.
|
|
157
|
+
* Returns false if the budget is full of tiles at least as wanted as this
|
|
158
|
+
* request (so it shouldn't displace them).
|
|
159
|
+
*
|
|
160
|
+
* @param priority The incoming request's priority.
|
|
161
|
+
* @return {boolean} True if extraction should proceed.
|
|
162
|
+
*/
|
|
163
|
+
ensureRoomFor_(priority) {
|
|
164
|
+
if (this.residentBytes_ < this.budgetBytes_) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
// Over/at budget: only proceed if there's a lower-priority tile to evict.
|
|
168
|
+
let evictedAny = false;
|
|
169
|
+
while (this.residentBytes_ >= this.budgetBytes_) {
|
|
170
|
+
const victim = this.lowestPriorityResident_();
|
|
171
|
+
if (victim === void 0 || victim.priority >= priority) {
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
this.evictOne_(victim.productLocalID);
|
|
175
|
+
evictedAny = true;
|
|
176
|
+
}
|
|
177
|
+
return this.residentBytes_ < this.budgetBytes_ || evictedAny;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Evict lowest-priority resident tiles until within budget (used after an
|
|
181
|
+
* extraction that pushed us over — e.g. a large tile).
|
|
182
|
+
*/
|
|
183
|
+
evictToBudget_() {
|
|
184
|
+
while (this.residentBytes_ > this.budgetBytes_) {
|
|
185
|
+
const victim = this.lowestPriorityResident_();
|
|
186
|
+
if (victim === void 0) {
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
this.evictOne_(victim.productLocalID);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* @return {{ productLocalID: number, priority: number } | undefined} The
|
|
194
|
+
* lowest-priority resident tile, or undefined if none.
|
|
195
|
+
*/
|
|
196
|
+
lowestPriorityResident_() {
|
|
197
|
+
let worstID;
|
|
198
|
+
let worstPriority = Number.POSITIVE_INFINITY;
|
|
199
|
+
for (const [productLocalID, tile] of this.resident_) {
|
|
200
|
+
if (tile.priority < worstPriority) {
|
|
201
|
+
worstPriority = tile.priority;
|
|
202
|
+
worstID = productLocalID;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return worstID === void 0 ?
|
|
206
|
+
void 0 : { productLocalID: worstID, priority: worstPriority };
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Release one resident tile.
|
|
210
|
+
*
|
|
211
|
+
* @param productLocalID The tile to evict.
|
|
212
|
+
*/
|
|
213
|
+
evictOne_(productLocalID) {
|
|
214
|
+
const tile = this.resident_.get(productLocalID);
|
|
215
|
+
if (tile === void 0) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
this.tiles_.release(productLocalID);
|
|
219
|
+
this.resident_.delete(productLocalID);
|
|
220
|
+
this.residentBytes_ -= tile.bytes;
|
|
221
|
+
++this.evictions_;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"demand_geometry_queue.test.d.ts","sourceRoot":"","sources":["../../../src/core/demand_geometry_queue.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/* eslint-disable no-magic-numbers */
|
|
2
|
+
// M3: the demand-geometry scheduler — priority ordering, byte budget, and
|
|
3
|
+
// eviction — tested against a mock tiles backend so the policy is settled
|
|
4
|
+
// independently of the wasm extract/reclaim work.
|
|
5
|
+
import { describe, expect, test } from "@jest/globals";
|
|
6
|
+
import { DemandGeometryQueue } from "./demand_geometry_queue.js";
|
|
7
|
+
/**
|
|
8
|
+
* A mock tiles backend: every product costs a fixed number of bytes and
|
|
9
|
+
* records the extract/release calls so tests can assert on them.
|
|
10
|
+
*
|
|
11
|
+
* @param costBytes Per-tile resident cost.
|
|
12
|
+
* @return {object} `{ tiles, extracted, released }`.
|
|
13
|
+
*/
|
|
14
|
+
function mockTiles(costBytes) {
|
|
15
|
+
const extracted = [];
|
|
16
|
+
const released = [];
|
|
17
|
+
const tiles = {
|
|
18
|
+
extract(id) {
|
|
19
|
+
extracted.push(id);
|
|
20
|
+
return costBytes;
|
|
21
|
+
},
|
|
22
|
+
release(id) {
|
|
23
|
+
released.push(id);
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
return { tiles, extracted, released };
|
|
27
|
+
}
|
|
28
|
+
describe("DemandGeometryQueue", () => {
|
|
29
|
+
test("materialises highest-priority requests first", () => {
|
|
30
|
+
const { tiles, extracted } = mockTiles(10);
|
|
31
|
+
const q = new DemandGeometryQueue(tiles, 1000);
|
|
32
|
+
q.request(1, 5);
|
|
33
|
+
q.request(2, 50);
|
|
34
|
+
q.request(3, 20);
|
|
35
|
+
q.pump();
|
|
36
|
+
// Extracted in descending priority: 2 (50), 3 (20), 1 (5).
|
|
37
|
+
expect(extracted).toEqual([2, 3, 1]);
|
|
38
|
+
expect(q.stats.residentCount).toBe(3);
|
|
39
|
+
expect(q.stats.residentBytes).toBe(30);
|
|
40
|
+
});
|
|
41
|
+
test("evicts the lowest-priority tile when the budget is exceeded", () => {
|
|
42
|
+
const { tiles, released } = mockTiles(100);
|
|
43
|
+
// Budget holds 3 tiles (300).
|
|
44
|
+
const q = new DemandGeometryQueue(tiles, 300);
|
|
45
|
+
for (const [id, pri] of [[1, 10], [2, 20], [3, 30]]) {
|
|
46
|
+
q.request(id, pri);
|
|
47
|
+
}
|
|
48
|
+
q.pump();
|
|
49
|
+
expect(q.stats.residentCount).toBe(3);
|
|
50
|
+
// A 4th, higher-priority request must evict the lowest (id 1, pri 10).
|
|
51
|
+
q.request(4, 40);
|
|
52
|
+
q.pump();
|
|
53
|
+
expect(q.isResident(4)).toBe(true);
|
|
54
|
+
expect(q.isResident(1)).toBe(false);
|
|
55
|
+
expect(released).toContain(1);
|
|
56
|
+
expect(q.stats.residentBytes).toBeLessThanOrEqual(300);
|
|
57
|
+
});
|
|
58
|
+
test("a request below every resident tile does not displace them", () => {
|
|
59
|
+
const { tiles, extracted } = mockTiles(100);
|
|
60
|
+
const q = new DemandGeometryQueue(tiles, 200); // holds 2
|
|
61
|
+
q.request(1, 50);
|
|
62
|
+
q.request(2, 40);
|
|
63
|
+
q.pump();
|
|
64
|
+
expect(q.stats.residentCount).toBe(2);
|
|
65
|
+
// Low-priority newcomer can't beat either resident tile → stays pending.
|
|
66
|
+
q.request(3, 5);
|
|
67
|
+
const n = q.pump();
|
|
68
|
+
expect(n).toBe(0);
|
|
69
|
+
expect(q.isResident(3)).toBe(false);
|
|
70
|
+
expect(extracted).toEqual([1, 2]);
|
|
71
|
+
expect(q.stats.pendingCount).toBe(1);
|
|
72
|
+
});
|
|
73
|
+
test("re-requesting an evicted product re-extracts it", () => {
|
|
74
|
+
const { tiles, extracted } = mockTiles(100);
|
|
75
|
+
const q = new DemandGeometryQueue(tiles, 100); // holds 1
|
|
76
|
+
q.request(1, 10);
|
|
77
|
+
q.pump();
|
|
78
|
+
q.request(2, 20); // evicts 1
|
|
79
|
+
q.pump();
|
|
80
|
+
expect(q.isResident(1)).toBe(false);
|
|
81
|
+
q.request(1, 30); // evicts 2, re-extracts 1
|
|
82
|
+
q.pump();
|
|
83
|
+
expect(q.isResident(1)).toBe(true);
|
|
84
|
+
// 1 extracted twice (first fill, then re-fill after eviction).
|
|
85
|
+
expect(extracted.filter((id) => id === 1).length).toBe(2);
|
|
86
|
+
});
|
|
87
|
+
test("requesting an already-resident product does not re-extract", () => {
|
|
88
|
+
const { tiles, extracted } = mockTiles(10);
|
|
89
|
+
const q = new DemandGeometryQueue(tiles, 1000);
|
|
90
|
+
q.request(1, 10);
|
|
91
|
+
q.pump();
|
|
92
|
+
q.request(1, 99); // already resident — just bumps ranking
|
|
93
|
+
q.pump();
|
|
94
|
+
expect(extracted).toEqual([1]);
|
|
95
|
+
});
|
|
96
|
+
test("a per-pump extraction cap bounds work per call", () => {
|
|
97
|
+
const { tiles } = mockTiles(1);
|
|
98
|
+
const q = new DemandGeometryQueue(tiles, 1000000);
|
|
99
|
+
for (let id = 0; id < 100; ++id) {
|
|
100
|
+
q.request(id, id);
|
|
101
|
+
}
|
|
102
|
+
expect(q.pump(10)).toBe(10);
|
|
103
|
+
expect(q.stats.residentCount).toBe(10);
|
|
104
|
+
expect(q.stats.pendingCount).toBe(90);
|
|
105
|
+
});
|
|
106
|
+
test("evictAll releases everything", () => {
|
|
107
|
+
const { tiles, released } = mockTiles(10);
|
|
108
|
+
const q = new DemandGeometryQueue(tiles, 1000);
|
|
109
|
+
for (let id = 0; id < 5; ++id) {
|
|
110
|
+
q.request(id, id);
|
|
111
|
+
}
|
|
112
|
+
q.pump();
|
|
113
|
+
q.evictAll();
|
|
114
|
+
expect(q.stats.residentCount).toBe(0);
|
|
115
|
+
expect(q.stats.residentBytes).toBe(0);
|
|
116
|
+
expect(released.sort()).toEqual([0, 1, 2, 3, 4]);
|
|
117
|
+
});
|
|
118
|
+
test("keeps the resident set bounded by the budget across a demand stream", () => {
|
|
119
|
+
const { tiles } = mockTiles(100);
|
|
120
|
+
const q = new DemandGeometryQueue(tiles, 500); // holds 5
|
|
121
|
+
// A moving "viewport": 200 products stream past with rising priority.
|
|
122
|
+
for (let id = 0; id < 200; ++id) {
|
|
123
|
+
q.request(id, id);
|
|
124
|
+
q.pump();
|
|
125
|
+
expect(q.stats.residentBytes).toBeLessThanOrEqual(500);
|
|
126
|
+
}
|
|
127
|
+
// The 5 most-wanted (highest ids) remain.
|
|
128
|
+
expect(q.stats.residentCount).toBe(5);
|
|
129
|
+
for (const id of [195, 196, 197, 198, 199]) {
|
|
130
|
+
expect(q.isResident(id)).toBe(true);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { StepEntityConstructorAbstract } from "../step_entity_constructor.js";
|
|
2
|
+
import { RecordHandler } from "./streaming_record_dispatcher.js";
|
|
3
|
+
/**
|
|
4
|
+
* A type index built **incrementally** from the streaming parse's per-record
|
|
5
|
+
* events (M2), rather than from the finished element array. Feed its
|
|
6
|
+
* {@link handler} to a {@link StreamingRecordDispatcher} (or directly to
|
|
7
|
+
* `buildIndexStreaming`'s `onRecordIndexed`) and it accumulates, per concrete
|
|
8
|
+
* type, the set of express IDs seen — so the moment parsing reaches a record
|
|
9
|
+
* it is queryable, without waiting for end-of-parse.
|
|
10
|
+
*
|
|
11
|
+
* Queries take entity constructors and expand to their subtype closure via
|
|
12
|
+
* the generated `query` (conway #383), so `expressIDsOfTypes(IfcProduct)`
|
|
13
|
+
* unions every product subtype exactly as the resident model's type index
|
|
14
|
+
* does.
|
|
15
|
+
*
|
|
16
|
+
* Express IDs are held in per-type `Set`s, so the consumer is idempotent
|
|
17
|
+
* under the streaming builder's rare grow-and-restart (records re-fire from
|
|
18
|
+
* localID 0): re-adding an express ID is a no-op. Memory is O(records) — the
|
|
19
|
+
* same order as the element index it mirrors.
|
|
20
|
+
*
|
|
21
|
+
* Scope: membership is keyed on the raw parse `typeID`. External-mapping /
|
|
22
|
+
* complex records (typeID 0) are therefore not attributed to their mapped
|
|
23
|
+
* classes here — resolving those needs the record's `multiMapping`, which the
|
|
24
|
+
* event stream doesn't carry; that's the model's construction-time index. For
|
|
25
|
+
* the overwhelming majority of records (simple, one type each) this index is
|
|
26
|
+
* exact, which the parity test pins.
|
|
27
|
+
*/
|
|
28
|
+
export declare class IncrementalTypeIndex<TypeIDType extends number> {
|
|
29
|
+
/** Concrete typeID → express IDs of records of exactly that type. */
|
|
30
|
+
private readonly byType_;
|
|
31
|
+
/**
|
|
32
|
+
* Record one indexed entity. Bound as {@link handler} for direct use as a
|
|
33
|
+
* dispatcher subscription / `onRecordIndexed` callback.
|
|
34
|
+
*
|
|
35
|
+
* @param localID Unused (kept for the RecordHandler shape).
|
|
36
|
+
* @param expressID The record's express ID.
|
|
37
|
+
* @param typeID The record's concrete type ID (undefined / 0 records are
|
|
38
|
+
* ignored — they carry no concrete queryable type here).
|
|
39
|
+
*/
|
|
40
|
+
readonly handler: RecordHandler<TypeIDType>;
|
|
41
|
+
/**
|
|
42
|
+
* The distinct concrete type IDs seen so far.
|
|
43
|
+
*
|
|
44
|
+
* @return {IterableIterator<TypeIDType>} The concrete types.
|
|
45
|
+
*/
|
|
46
|
+
concreteTypes(): IterableIterator<TypeIDType>;
|
|
47
|
+
/**
|
|
48
|
+
* Lazily iterate the express IDs of all records of the given types
|
|
49
|
+
* (including subtypes).
|
|
50
|
+
*
|
|
51
|
+
* @param types The entity constructors to query (subtype closures unioned).
|
|
52
|
+
* @return {IterableIterator<number>} Express IDs of matching records.
|
|
53
|
+
* @yields {number} Each matching express ID.
|
|
54
|
+
*/
|
|
55
|
+
expressIDsOfTypes(...types: StepEntityConstructorAbstract<TypeIDType>[]): IterableIterator<number>;
|
|
56
|
+
/**
|
|
57
|
+
* Count records of the given types (including subtypes) seen so far.
|
|
58
|
+
*
|
|
59
|
+
* @param types The entity constructors to count.
|
|
60
|
+
* @return {number} The number of matching records.
|
|
61
|
+
*/
|
|
62
|
+
count(...types: StepEntityConstructorAbstract<TypeIDType>[]): number;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=incremental_type_index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"incremental_type_index.d.ts","sourceRoot":"","sources":["../../../../src/step/parsing/incremental_type_index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAA;AAC1E,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAG7D;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,oBAAoB,CAAC,UAAU,SAAS,MAAM;IAEzD,qEAAqE;IACrE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqC;IAE7D;;;;;;;;OAQG;IACH,SAAgB,OAAO,EAAE,aAAa,CAAC,UAAU,CAAC,CAe/C;IAEH;;;;OAIG;IACI,aAAa,IAAI,gBAAgB,CAAC,UAAU,CAAC;IAIpD;;;;;;;OAOG;IACK,iBAAiB,CACrB,GAAG,KAAK,EAAE,6BAA6B,CAAC,UAAU,CAAC,EAAE,GACrD,gBAAgB,CAAC,MAAM,CAAC;IAc5B;;;;;OAKG;IACI,KAAK,CAAE,GAAG,KAAK,EAAE,6BAA6B,CAAC,UAAU,CAAC,EAAE,GAAI,MAAM;CAa9E"}
|