@bldrs-ai/conway 1.404.1241 → 1.408.1248
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/examples/browser-bundled.cjs +1 -1
- package/compiled/examples/cli-bundled.cjs +1 -1
- package/compiled/examples/cli-step-bundled.cjs +1 -1
- package/compiled/examples/validator-bundled.cjs +1 -1
- package/compiled/src/core/composed_model_skeleton.d.ts +63 -0
- package/compiled/src/core/composed_model_skeleton.d.ts.map +1 -0
- package/compiled/src/core/composed_model_skeleton.js +66 -0
- package/compiled/src/core/composed_model_skeleton.test.d.ts +2 -0
- package/compiled/src/core/composed_model_skeleton.test.d.ts.map +1 -0
- package/compiled/src/core/composed_model_skeleton.test.js +69 -0
- package/compiled/src/core/geometry_tile_pool.d.ts +99 -0
- package/compiled/src/core/geometry_tile_pool.d.ts.map +1 -0
- package/compiled/src/core/geometry_tile_pool.js +104 -0
- package/compiled/src/core/geometry_tile_pool.test.d.ts +2 -0
- package/compiled/src/core/geometry_tile_pool.test.d.ts.map +1 -0
- package/compiled/src/core/geometry_tile_pool.test.js +180 -0
- package/compiled/src/core/mem/chunked_pool.d.ts +116 -0
- package/compiled/src/core/mem/chunked_pool.d.ts.map +1 -0
- package/compiled/src/core/mem/chunked_pool.js +149 -0
- package/compiled/src/core/mem/chunked_pool.test.d.ts +2 -0
- package/compiled/src/core/mem/chunked_pool.test.d.ts.map +1 -0
- package/compiled/src/core/mem/chunked_pool.test.js +63 -0
- package/compiled/src/core/mem/shared_asset_pool.d.ts +82 -0
- package/compiled/src/core/mem/shared_asset_pool.d.ts.map +1 -0
- package/compiled/src/core/mem/shared_asset_pool.js +113 -0
- package/compiled/src/core/mem/shared_asset_pool.test.d.ts +2 -0
- package/compiled/src/core/mem/shared_asset_pool.test.d.ts.map +1 -0
- package/compiled/src/core/mem/shared_asset_pool.test.js +49 -0
- package/compiled/src/core/model_registry.d.ts +87 -0
- package/compiled/src/core/model_registry.d.ts.map +1 -0
- package/compiled/src/core/model_registry.js +101 -0
- package/compiled/src/core/model_registry.test.d.ts +2 -0
- package/compiled/src/core/model_registry.test.d.ts.map +1 -0
- package/compiled/src/core/model_registry.test.js +60 -0
- package/compiled/src/core/model_uri.d.ts +57 -0
- package/compiled/src/core/model_uri.d.ts.map +1 -0
- package/compiled/src/core/model_uri.js +101 -0
- package/compiled/src/core/model_uri.test.d.ts +2 -0
- package/compiled/src/core/model_uri.test.d.ts.map +1 -0
- package/compiled/src/core/model_uri.test.js +49 -0
- package/compiled/src/core/shared_byte_budget.d.ts +60 -0
- package/compiled/src/core/shared_byte_budget.d.ts.map +1 -0
- package/compiled/src/core/shared_byte_budget.js +85 -0
- package/compiled/src/core/shared_byte_budget.test.d.ts +2 -0
- package/compiled/src/core/shared_byte_budget.test.d.ts.map +1 -0
- package/compiled/src/core/shared_byte_budget.test.js +55 -0
- package/compiled/src/step/parsing/cross_reference_registry.d.ts +95 -0
- package/compiled/src/step/parsing/cross_reference_registry.d.ts.map +1 -0
- package/compiled/src/step/parsing/cross_reference_registry.js +91 -0
- package/compiled/src/step/parsing/cross_reference_registry.test.d.ts +2 -0
- package/compiled/src/step/parsing/cross_reference_registry.test.d.ts.map +1 -0
- package/compiled/src/step/parsing/cross_reference_registry.test.js +66 -0
- package/compiled/src/version/version.js +1 -1
- package/compiled/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A fixed-chunk budget pool — the resident-memory primitive (M3 production
|
|
3
|
+
* design; see "Resident memory: two regimes" in the design doc).
|
|
4
|
+
*
|
|
5
|
+
* The wasm heap grows and never shrinks: `free()` returns bytes to the
|
|
6
|
+
* allocator's freelists, not to the browser, so the tab pays the heap's
|
|
7
|
+
* **high-water mark forever** and external fragmentation is a permanent leak,
|
|
8
|
+
* not a throughput nuisance. General-purpose allocation is therefore the
|
|
9
|
+
* wrong tool for large, evictable resident data. This pool is the explicit
|
|
10
|
+
* alternative: one region carved into fixed-size chunks with a freelist —
|
|
11
|
+
* acquire rounds up to whole chunks, release is O(chunks), and the region's
|
|
12
|
+
* high-water mark is the budget, by construction. Fragmentation is confined
|
|
13
|
+
* to bounded internal waste (the tail of the last chunk).
|
|
14
|
+
*
|
|
15
|
+
* This TS class is the pool's **accounting/policy half** and the executable
|
|
16
|
+
* spec for its C++ twin in conway-geom (which owns real bytes inside the wasm
|
|
17
|
+
* heap). It is also usable directly for JS-side resident budgets (property
|
|
18
|
+
* caches, sidecar caches) — the abstraction is deliberately not
|
|
19
|
+
* geometry-specific. The intended layering:
|
|
20
|
+
*
|
|
21
|
+
* ChunkedPool — chunks and bytes (this file)
|
|
22
|
+
* SharedAssetPool — refcounted assets living in those chunks
|
|
23
|
+
* GeometryTilePool — the geometry narrowing (products ⇄ assets)
|
|
24
|
+
*
|
|
25
|
+
* Lifetime regimes, for orientation: *phase-bounded* scratch (tessellation
|
|
26
|
+
* temporaries) stays on the AFTP bump arenas — reset, never freed piecemeal.
|
|
27
|
+
* *Demand-bounded* residents (data that lives until evicted) live here. The
|
|
28
|
+
* general allocator keeps only the small, messy-lifetime residual.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* An acquired run of chunks. `chunks` are chunk indices into the pool's
|
|
32
|
+
* region (index × chunkBytes = byte offset in the C++ twin); `byteSize` is
|
|
33
|
+
* the caller's requested size, ≤ `chunks.length × chunkBytes`.
|
|
34
|
+
*/
|
|
35
|
+
export interface ChunkSpan {
|
|
36
|
+
chunks: number[];
|
|
37
|
+
byteSize: number;
|
|
38
|
+
}
|
|
39
|
+
/** Runtime counters for a pool, for tests and telemetry. */
|
|
40
|
+
export interface ChunkedPoolStats {
|
|
41
|
+
totalChunks: number;
|
|
42
|
+
freeChunks: number;
|
|
43
|
+
bytesInUse: number;
|
|
44
|
+
acquires: number;
|
|
45
|
+
releases: number;
|
|
46
|
+
failedAcquires: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A budgeted pool of fixed-size chunks with a freelist. Purely accounting —
|
|
50
|
+
* see the module docs for how it maps onto real memory.
|
|
51
|
+
*/
|
|
52
|
+
export declare class ChunkedPool {
|
|
53
|
+
private readonly chunkBytes_;
|
|
54
|
+
private readonly totalChunks_;
|
|
55
|
+
/** Free chunk indices (LIFO for locality in the C++ twin). */
|
|
56
|
+
private readonly freeList_;
|
|
57
|
+
private acquires_;
|
|
58
|
+
private releases_;
|
|
59
|
+
private failedAcquires_;
|
|
60
|
+
/**
|
|
61
|
+
* @param budgetBytes The pool's total byte budget (rounded down to whole
|
|
62
|
+
* chunks; must fit at least one chunk).
|
|
63
|
+
* @param chunkBytes The fixed chunk size in bytes.
|
|
64
|
+
*/
|
|
65
|
+
constructor(budgetBytes: number, chunkBytes: number);
|
|
66
|
+
/**
|
|
67
|
+
* @return {number} The fixed chunk size in bytes.
|
|
68
|
+
*/
|
|
69
|
+
get chunkBytes(): number;
|
|
70
|
+
/**
|
|
71
|
+
* @return {number} Total chunks in the pool.
|
|
72
|
+
*/
|
|
73
|
+
get totalChunks(): number;
|
|
74
|
+
/**
|
|
75
|
+
* @return {number} Chunks currently free.
|
|
76
|
+
*/
|
|
77
|
+
get freeChunks(): number;
|
|
78
|
+
/**
|
|
79
|
+
* @return {number} The pool's total byte capacity (whole chunks).
|
|
80
|
+
*/
|
|
81
|
+
get totalBytes(): number;
|
|
82
|
+
/**
|
|
83
|
+
* @return {number} Physical bytes currently acquired (chunk-rounded).
|
|
84
|
+
*/
|
|
85
|
+
get bytesInUse(): number;
|
|
86
|
+
/**
|
|
87
|
+
* The physical (chunk-rounded) cost of a byte size — what an acquire of
|
|
88
|
+
* `bytes` actually consumes. Use this for any budget accounting layered
|
|
89
|
+
* above the pool so logical charges cover physical use.
|
|
90
|
+
*
|
|
91
|
+
* @param bytes The byte size to round.
|
|
92
|
+
* @return {number} The chunk-rounded byte cost.
|
|
93
|
+
*/
|
|
94
|
+
chunkRound(bytes: number): number;
|
|
95
|
+
/**
|
|
96
|
+
* Acquire chunks to hold `bytes`. All-or-nothing: returns undefined (and
|
|
97
|
+
* changes nothing) if the free chunks can't cover the request — the
|
|
98
|
+
* caller's cue to evict and retry.
|
|
99
|
+
*
|
|
100
|
+
* @param bytes The byte size to hold (0 is allowed and acquires no chunks).
|
|
101
|
+
* @return {ChunkSpan | undefined} The span, or undefined if it can't fit.
|
|
102
|
+
*/
|
|
103
|
+
acquire(bytes: number): ChunkSpan | undefined;
|
|
104
|
+
/**
|
|
105
|
+
* Return a span's chunks to the freelist. The span must not be used (or
|
|
106
|
+
* released) again afterwards.
|
|
107
|
+
*
|
|
108
|
+
* @param span The span to release.
|
|
109
|
+
*/
|
|
110
|
+
release(span: ChunkSpan): void;
|
|
111
|
+
/**
|
|
112
|
+
* @return {ChunkedPoolStats} A snapshot of runtime counters.
|
|
113
|
+
*/
|
|
114
|
+
get stats(): ChunkedPoolStats;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=chunked_pool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunked_pool.d.ts","sourceRoot":"","sources":["../../../../src/core/mem/chunked_pool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAGD,4DAA4D;AAC5D,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,cAAc,EAAE,MAAM,CAAA;CACvB;AAGD;;;GAGG;AACH,qBAAa,WAAW;IAEtB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAQ;IAEpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IAErC,8DAA8D;IAC9D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAU;IAEpC,OAAO,CAAC,SAAS,CAAI;IAErB,OAAO,CAAC,SAAS,CAAI;IAErB,OAAO,CAAC,eAAe,CAAI;IAE3B;;;;OAIG;gBACU,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM;IAsBpD;;OAEG;IACH,IAAW,UAAU,IAAI,MAAM,CAE9B;IAED;;OAEG;IACH,IAAW,WAAW,IAAI,MAAM,CAE/B;IAED;;OAEG;IACH,IAAW,UAAU,IAAI,MAAM,CAE9B;IAED;;OAEG;IACH,IAAW,UAAU,IAAI,MAAM,CAE9B;IAED;;OAEG;IACH,IAAW,UAAU,IAAI,MAAM,CAE9B;IAED;;;;;;;OAOG;IACI,UAAU,CAAE,KAAK,EAAE,MAAM,GAAI,MAAM;IAS1C;;;;;;;OAOG;IACI,OAAO,CAAE,KAAK,EAAE,MAAM,GAAI,SAAS,GAAG,SAAS;IAoBtD;;;;;OAKG;IACI,OAAO,CAAE,IAAI,EAAE,SAAS,GAAI,IAAI;IAUvC;;OAEG;IACH,IAAW,KAAK,IAAI,gBAAgB,CASnC;CACF"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A fixed-chunk budget pool — the resident-memory primitive (M3 production
|
|
3
|
+
* design; see "Resident memory: two regimes" in the design doc).
|
|
4
|
+
*
|
|
5
|
+
* The wasm heap grows and never shrinks: `free()` returns bytes to the
|
|
6
|
+
* allocator's freelists, not to the browser, so the tab pays the heap's
|
|
7
|
+
* **high-water mark forever** and external fragmentation is a permanent leak,
|
|
8
|
+
* not a throughput nuisance. General-purpose allocation is therefore the
|
|
9
|
+
* wrong tool for large, evictable resident data. This pool is the explicit
|
|
10
|
+
* alternative: one region carved into fixed-size chunks with a freelist —
|
|
11
|
+
* acquire rounds up to whole chunks, release is O(chunks), and the region's
|
|
12
|
+
* high-water mark is the budget, by construction. Fragmentation is confined
|
|
13
|
+
* to bounded internal waste (the tail of the last chunk).
|
|
14
|
+
*
|
|
15
|
+
* This TS class is the pool's **accounting/policy half** and the executable
|
|
16
|
+
* spec for its C++ twin in conway-geom (which owns real bytes inside the wasm
|
|
17
|
+
* heap). It is also usable directly for JS-side resident budgets (property
|
|
18
|
+
* caches, sidecar caches) — the abstraction is deliberately not
|
|
19
|
+
* geometry-specific. The intended layering:
|
|
20
|
+
*
|
|
21
|
+
* ChunkedPool — chunks and bytes (this file)
|
|
22
|
+
* SharedAssetPool — refcounted assets living in those chunks
|
|
23
|
+
* GeometryTilePool — the geometry narrowing (products ⇄ assets)
|
|
24
|
+
*
|
|
25
|
+
* Lifetime regimes, for orientation: *phase-bounded* scratch (tessellation
|
|
26
|
+
* temporaries) stays on the AFTP bump arenas — reset, never freed piecemeal.
|
|
27
|
+
* *Demand-bounded* residents (data that lives until evicted) live here. The
|
|
28
|
+
* general allocator keeps only the small, messy-lifetime residual.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* A budgeted pool of fixed-size chunks with a freelist. Purely accounting —
|
|
32
|
+
* see the module docs for how it maps onto real memory.
|
|
33
|
+
*/
|
|
34
|
+
export class ChunkedPool {
|
|
35
|
+
/**
|
|
36
|
+
* @param budgetBytes The pool's total byte budget (rounded down to whole
|
|
37
|
+
* chunks; must fit at least one chunk).
|
|
38
|
+
* @param chunkBytes The fixed chunk size in bytes.
|
|
39
|
+
*/
|
|
40
|
+
constructor(budgetBytes, chunkBytes) {
|
|
41
|
+
this.acquires_ = 0;
|
|
42
|
+
this.releases_ = 0;
|
|
43
|
+
this.failedAcquires_ = 0;
|
|
44
|
+
if (chunkBytes <= 0 || !Number.isInteger(chunkBytes)) {
|
|
45
|
+
throw new Error(`Invalid chunkBytes ${chunkBytes}`);
|
|
46
|
+
}
|
|
47
|
+
const totalChunks = Math.floor(budgetBytes / chunkBytes);
|
|
48
|
+
if (totalChunks < 1) {
|
|
49
|
+
throw new Error(`Budget ${budgetBytes} does not fit one chunk of ${chunkBytes}`);
|
|
50
|
+
}
|
|
51
|
+
this.chunkBytes_ = chunkBytes;
|
|
52
|
+
this.totalChunks_ = totalChunks;
|
|
53
|
+
this.freeList_ = [];
|
|
54
|
+
for (let chunk = totalChunks - 1; chunk >= 0; --chunk) {
|
|
55
|
+
this.freeList_.push(chunk);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* @return {number} The fixed chunk size in bytes.
|
|
60
|
+
*/
|
|
61
|
+
get chunkBytes() {
|
|
62
|
+
return this.chunkBytes_;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* @return {number} Total chunks in the pool.
|
|
66
|
+
*/
|
|
67
|
+
get totalChunks() {
|
|
68
|
+
return this.totalChunks_;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* @return {number} Chunks currently free.
|
|
72
|
+
*/
|
|
73
|
+
get freeChunks() {
|
|
74
|
+
return this.freeList_.length;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* @return {number} The pool's total byte capacity (whole chunks).
|
|
78
|
+
*/
|
|
79
|
+
get totalBytes() {
|
|
80
|
+
return this.totalChunks_ * this.chunkBytes_;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* @return {number} Physical bytes currently acquired (chunk-rounded).
|
|
84
|
+
*/
|
|
85
|
+
get bytesInUse() {
|
|
86
|
+
return (this.totalChunks_ - this.freeList_.length) * this.chunkBytes_;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The physical (chunk-rounded) cost of a byte size — what an acquire of
|
|
90
|
+
* `bytes` actually consumes. Use this for any budget accounting layered
|
|
91
|
+
* above the pool so logical charges cover physical use.
|
|
92
|
+
*
|
|
93
|
+
* @param bytes The byte size to round.
|
|
94
|
+
* @return {number} The chunk-rounded byte cost.
|
|
95
|
+
*/
|
|
96
|
+
chunkRound(bytes) {
|
|
97
|
+
if (bytes < 0) {
|
|
98
|
+
throw new Error(`Invalid byte size ${bytes}`);
|
|
99
|
+
}
|
|
100
|
+
return Math.ceil(bytes / this.chunkBytes_) * this.chunkBytes_;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Acquire chunks to hold `bytes`. All-or-nothing: returns undefined (and
|
|
104
|
+
* changes nothing) if the free chunks can't cover the request — the
|
|
105
|
+
* caller's cue to evict and retry.
|
|
106
|
+
*
|
|
107
|
+
* @param bytes The byte size to hold (0 is allowed and acquires no chunks).
|
|
108
|
+
* @return {ChunkSpan | undefined} The span, or undefined if it can't fit.
|
|
109
|
+
*/
|
|
110
|
+
acquire(bytes) {
|
|
111
|
+
const needed = Math.ceil(Math.max(0, bytes) / this.chunkBytes_);
|
|
112
|
+
if (needed > this.freeList_.length) {
|
|
113
|
+
++this.failedAcquires_;
|
|
114
|
+
return void 0;
|
|
115
|
+
}
|
|
116
|
+
const chunks = new Array(needed);
|
|
117
|
+
for (let where = 0; where < needed; ++where) {
|
|
118
|
+
chunks[where] = this.freeList_.pop();
|
|
119
|
+
}
|
|
120
|
+
++this.acquires_;
|
|
121
|
+
return { chunks, byteSize: bytes };
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Return a span's chunks to the freelist. The span must not be used (or
|
|
125
|
+
* released) again afterwards.
|
|
126
|
+
*
|
|
127
|
+
* @param span The span to release.
|
|
128
|
+
*/
|
|
129
|
+
release(span) {
|
|
130
|
+
for (const chunk of span.chunks) {
|
|
131
|
+
this.freeList_.push(chunk);
|
|
132
|
+
}
|
|
133
|
+
span.chunks.length = 0;
|
|
134
|
+
++this.releases_;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* @return {ChunkedPoolStats} A snapshot of runtime counters.
|
|
138
|
+
*/
|
|
139
|
+
get stats() {
|
|
140
|
+
return {
|
|
141
|
+
totalChunks: this.totalChunks_,
|
|
142
|
+
freeChunks: this.freeList_.length,
|
|
143
|
+
bytesInUse: this.bytesInUse,
|
|
144
|
+
acquires: this.acquires_,
|
|
145
|
+
releases: this.releases_,
|
|
146
|
+
failedAcquires: this.failedAcquires_,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunked_pool.test.d.ts","sourceRoot":"","sources":["../../../../src/core/mem/chunked_pool.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/* eslint-disable no-magic-numbers */
|
|
2
|
+
// mem: the fixed-chunk pool bounds resident memory by construction — acquire
|
|
3
|
+
// rounds to whole chunks, all-or-nothing, and the high-water mark can never
|
|
4
|
+
// exceed the budget.
|
|
5
|
+
import { describe, expect, test } from "@jest/globals";
|
|
6
|
+
import { ChunkedPool } from "./chunked_pool.js";
|
|
7
|
+
describe("ChunkedPool", () => {
|
|
8
|
+
test("sizes itself in whole chunks from the byte budget", () => {
|
|
9
|
+
const pool = new ChunkedPool(1050, 100);
|
|
10
|
+
expect(pool.totalChunks).toBe(10);
|
|
11
|
+
expect(pool.totalBytes).toBe(1000);
|
|
12
|
+
expect(pool.freeChunks).toBe(10);
|
|
13
|
+
expect(pool.bytesInUse).toBe(0);
|
|
14
|
+
});
|
|
15
|
+
test("acquire rounds up to whole chunks", () => {
|
|
16
|
+
const pool = new ChunkedPool(1000, 100);
|
|
17
|
+
const span = pool.acquire(250);
|
|
18
|
+
expect(span?.chunks.length).toBe(3);
|
|
19
|
+
expect(span?.byteSize).toBe(250);
|
|
20
|
+
expect(pool.bytesInUse).toBe(300);
|
|
21
|
+
expect(pool.chunkRound(250)).toBe(300);
|
|
22
|
+
});
|
|
23
|
+
test("acquire is all-or-nothing at exhaustion", () => {
|
|
24
|
+
const pool = new ChunkedPool(300, 100);
|
|
25
|
+
expect(pool.acquire(200)).toBeDefined();
|
|
26
|
+
// 2 chunks needed, 1 free → refused with no state change.
|
|
27
|
+
const refused = pool.acquire(101);
|
|
28
|
+
expect(refused).toBeUndefined();
|
|
29
|
+
expect(pool.freeChunks).toBe(1);
|
|
30
|
+
expect(pool.stats.failedAcquires).toBe(1);
|
|
31
|
+
// Exactly one chunk still fits.
|
|
32
|
+
expect(pool.acquire(100)).toBeDefined();
|
|
33
|
+
expect(pool.freeChunks).toBe(0);
|
|
34
|
+
});
|
|
35
|
+
test("released chunks are reusable (steady-state churn stays bounded)", () => {
|
|
36
|
+
const pool = new ChunkedPool(500, 100);
|
|
37
|
+
for (let round = 0; round < 50; ++round) {
|
|
38
|
+
const a = pool.acquire(300);
|
|
39
|
+
const b = pool.acquire(200);
|
|
40
|
+
expect(a).toBeDefined();
|
|
41
|
+
expect(b).toBeDefined();
|
|
42
|
+
expect(pool.bytesInUse).toBe(500);
|
|
43
|
+
pool.release(a);
|
|
44
|
+
pool.release(b);
|
|
45
|
+
expect(pool.bytesInUse).toBe(0);
|
|
46
|
+
}
|
|
47
|
+
// The pool never grew: churn recycles the same chunks.
|
|
48
|
+
expect(pool.totalChunks).toBe(5);
|
|
49
|
+
expect(pool.stats.acquires).toBe(100);
|
|
50
|
+
expect(pool.stats.releases).toBe(100);
|
|
51
|
+
});
|
|
52
|
+
test("a zero-byte acquire holds no chunks but round-trips", () => {
|
|
53
|
+
const pool = new ChunkedPool(100, 100);
|
|
54
|
+
const span = pool.acquire(0);
|
|
55
|
+
expect(span?.chunks.length).toBe(0);
|
|
56
|
+
expect(pool.freeChunks).toBe(1);
|
|
57
|
+
pool.release(span);
|
|
58
|
+
});
|
|
59
|
+
test("rejects invalid construction", () => {
|
|
60
|
+
expect(() => new ChunkedPool(100, 0)).toThrow();
|
|
61
|
+
expect(() => new ChunkedPool(50, 100)).toThrow();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { ChunkedPool } from "./chunked_pool.js";
|
|
2
|
+
/**
|
|
3
|
+
* Refcounted **assets** resident in a {@link ChunkedPool} — the sharing layer
|
|
4
|
+
* of the memory system (see `src/core/mem/`).
|
|
5
|
+
*
|
|
6
|
+
* The general relationship this models: many **instances** reference shared
|
|
7
|
+
* **assets** (the definition/occurrence split that recurs across CAD — STEP
|
|
8
|
+
* AP214 literally names it *occurrence*). Concretely for geometry: products
|
|
9
|
+
* are instances, representation geometry is the asset, and mapped items make
|
|
10
|
+
* many products share one representation. But nothing here is
|
|
11
|
+
* geometry-specific — an asset is any refcounted, chunk-resident payload
|
|
12
|
+
* (a parsed property block, a decoded sidecar, a texture).
|
|
13
|
+
*
|
|
14
|
+
* Storage is keyed and refcounted on the **asset**, so the correctness rule
|
|
15
|
+
* for shared data falls out structurally: releasing one instance's reference
|
|
16
|
+
* never frees an asset another instance still holds; chunks return to the
|
|
17
|
+
* pool only on the last release. (Evicting product A must not free the
|
|
18
|
+
* representation product B still renders — the mapped-item bug a
|
|
19
|
+
* per-instance store invites.)
|
|
20
|
+
*
|
|
21
|
+
* Instance-level bookkeeping (which assets an instance holds, demand
|
|
22
|
+
* priorities, eviction order) belongs to the layer above — see
|
|
23
|
+
* `GeometryTilePool` for the geometry narrowing and `DemandGeometryQueue`
|
|
24
|
+
* for the scheduling policy.
|
|
25
|
+
*/
|
|
26
|
+
export declare class SharedAssetPool<AssetID> {
|
|
27
|
+
private readonly pool_;
|
|
28
|
+
private readonly assets_;
|
|
29
|
+
/**
|
|
30
|
+
* @param pool The chunk pool assets reside in.
|
|
31
|
+
*/
|
|
32
|
+
constructor(pool: ChunkedPool);
|
|
33
|
+
/**
|
|
34
|
+
* @return {ChunkedPool} The underlying chunk pool.
|
|
35
|
+
*/
|
|
36
|
+
get pool(): ChunkedPool;
|
|
37
|
+
/**
|
|
38
|
+
* @return {number} The number of resident assets.
|
|
39
|
+
*/
|
|
40
|
+
get assetCount(): number;
|
|
41
|
+
/**
|
|
42
|
+
* Take a reference on an asset, acquiring chunks for it if it is not yet
|
|
43
|
+
* resident. All-or-nothing: returns false (and changes nothing) only when
|
|
44
|
+
* the asset is absent and the pool can't fit it — the caller's cue to evict
|
|
45
|
+
* and retry. A `wasAbsent` result of true tells the caller to materialise
|
|
46
|
+
* the payload into the asset's chunks (the 0→1 transition).
|
|
47
|
+
*
|
|
48
|
+
* @param assetID The asset to reference.
|
|
49
|
+
* @param byteSize Its payload size (used only on first residency).
|
|
50
|
+
* @return {{ retained: boolean, wasAbsent: boolean }} Whether the reference
|
|
51
|
+
* was taken, and whether this was the residency-creating reference.
|
|
52
|
+
*/
|
|
53
|
+
retain(assetID: AssetID, byteSize: number): {
|
|
54
|
+
retained: boolean;
|
|
55
|
+
wasAbsent: boolean;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Drop a reference on an asset. On the last reference (1→0) the asset's
|
|
59
|
+
* chunks return to the pool and the caller should discard the payload.
|
|
60
|
+
*
|
|
61
|
+
* @param assetID The asset to release.
|
|
62
|
+
* @return {boolean} True if this was the last reference (asset freed).
|
|
63
|
+
*/
|
|
64
|
+
release(assetID: AssetID): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* @param assetID The asset to check.
|
|
67
|
+
* @return {boolean} True if the asset is resident.
|
|
68
|
+
*/
|
|
69
|
+
isResident(assetID: AssetID): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* @param assetID The asset to query.
|
|
72
|
+
* @return {number} The asset's current reference count (0 if absent).
|
|
73
|
+
*/
|
|
74
|
+
refCountOf(assetID: AssetID): number;
|
|
75
|
+
/**
|
|
76
|
+
* @param assetID The asset to query.
|
|
77
|
+
* @return {number} The asset's physical (chunk-rounded) resident bytes
|
|
78
|
+
* (0 if absent).
|
|
79
|
+
*/
|
|
80
|
+
physicalBytesOf(assetID: AssetID): number;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=shared_asset_pool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared_asset_pool.d.ts","sourceRoot":"","sources":["../../../../src/core/mem/shared_asset_pool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAa,MAAM,gBAAgB,CAAA;AAGvD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,eAAe,CAAC,OAAO;IAElC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAa;IAEnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4D;IAEpF;;OAEG;gBACU,IAAI,EAAE,WAAW;IAI9B;;OAEG;IACH,IAAW,IAAI,IAAI,WAAW,CAE7B;IAED;;OAEG;IACH,IAAW,UAAU,IAAI,MAAM,CAE9B;IAED;;;;;;;;;;;OAWG;IACI,MAAM,CAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAC7C;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE;IAoB7C;;;;;;OAMG;IACI,OAAO,CAAE,OAAO,EAAE,OAAO,GAAI,OAAO;IAkB3C;;;OAGG;IACI,UAAU,CAAE,OAAO,EAAE,OAAO,GAAI,OAAO;IAI9C;;;OAGG;IACI,UAAU,CAAE,OAAO,EAAE,OAAO,GAAI,MAAM;IAI7C;;;;OAIG;IACI,eAAe,CAAE,OAAO,EAAE,OAAO,GAAI,MAAM;CAOnD"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refcounted **assets** resident in a {@link ChunkedPool} — the sharing layer
|
|
3
|
+
* of the memory system (see `src/core/mem/`).
|
|
4
|
+
*
|
|
5
|
+
* The general relationship this models: many **instances** reference shared
|
|
6
|
+
* **assets** (the definition/occurrence split that recurs across CAD — STEP
|
|
7
|
+
* AP214 literally names it *occurrence*). Concretely for geometry: products
|
|
8
|
+
* are instances, representation geometry is the asset, and mapped items make
|
|
9
|
+
* many products share one representation. But nothing here is
|
|
10
|
+
* geometry-specific — an asset is any refcounted, chunk-resident payload
|
|
11
|
+
* (a parsed property block, a decoded sidecar, a texture).
|
|
12
|
+
*
|
|
13
|
+
* Storage is keyed and refcounted on the **asset**, so the correctness rule
|
|
14
|
+
* for shared data falls out structurally: releasing one instance's reference
|
|
15
|
+
* never frees an asset another instance still holds; chunks return to the
|
|
16
|
+
* pool only on the last release. (Evicting product A must not free the
|
|
17
|
+
* representation product B still renders — the mapped-item bug a
|
|
18
|
+
* per-instance store invites.)
|
|
19
|
+
*
|
|
20
|
+
* Instance-level bookkeeping (which assets an instance holds, demand
|
|
21
|
+
* priorities, eviction order) belongs to the layer above — see
|
|
22
|
+
* `GeometryTilePool` for the geometry narrowing and `DemandGeometryQueue`
|
|
23
|
+
* for the scheduling policy.
|
|
24
|
+
*/
|
|
25
|
+
export class SharedAssetPool {
|
|
26
|
+
/**
|
|
27
|
+
* @param pool The chunk pool assets reside in.
|
|
28
|
+
*/
|
|
29
|
+
constructor(pool) {
|
|
30
|
+
this.assets_ = new Map();
|
|
31
|
+
this.pool_ = pool;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* @return {ChunkedPool} The underlying chunk pool.
|
|
35
|
+
*/
|
|
36
|
+
get pool() {
|
|
37
|
+
return this.pool_;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* @return {number} The number of resident assets.
|
|
41
|
+
*/
|
|
42
|
+
get assetCount() {
|
|
43
|
+
return this.assets_.size;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Take a reference on an asset, acquiring chunks for it if it is not yet
|
|
47
|
+
* resident. All-or-nothing: returns false (and changes nothing) only when
|
|
48
|
+
* the asset is absent and the pool can't fit it — the caller's cue to evict
|
|
49
|
+
* and retry. A `wasAbsent` result of true tells the caller to materialise
|
|
50
|
+
* the payload into the asset's chunks (the 0→1 transition).
|
|
51
|
+
*
|
|
52
|
+
* @param assetID The asset to reference.
|
|
53
|
+
* @param byteSize Its payload size (used only on first residency).
|
|
54
|
+
* @return {{ retained: boolean, wasAbsent: boolean }} Whether the reference
|
|
55
|
+
* was taken, and whether this was the residency-creating reference.
|
|
56
|
+
*/
|
|
57
|
+
retain(assetID, byteSize) {
|
|
58
|
+
const existing = this.assets_.get(assetID);
|
|
59
|
+
if (existing !== void 0) {
|
|
60
|
+
++existing.refCount;
|
|
61
|
+
return { retained: true, wasAbsent: false };
|
|
62
|
+
}
|
|
63
|
+
const span = this.pool_.acquire(byteSize);
|
|
64
|
+
if (span === void 0) {
|
|
65
|
+
return { retained: false, wasAbsent: true };
|
|
66
|
+
}
|
|
67
|
+
this.assets_.set(assetID, { span, refCount: 1 });
|
|
68
|
+
return { retained: true, wasAbsent: true };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Drop a reference on an asset. On the last reference (1→0) the asset's
|
|
72
|
+
* chunks return to the pool and the caller should discard the payload.
|
|
73
|
+
*
|
|
74
|
+
* @param assetID The asset to release.
|
|
75
|
+
* @return {boolean} True if this was the last reference (asset freed).
|
|
76
|
+
*/
|
|
77
|
+
release(assetID) {
|
|
78
|
+
const asset = this.assets_.get(assetID);
|
|
79
|
+
if (asset === void 0) {
|
|
80
|
+
throw new Error(`Release of non-resident asset ${String(assetID)}`);
|
|
81
|
+
}
|
|
82
|
+
if (--asset.refCount > 0) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
this.pool_.release(asset.span);
|
|
86
|
+
this.assets_.delete(assetID);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* @param assetID The asset to check.
|
|
91
|
+
* @return {boolean} True if the asset is resident.
|
|
92
|
+
*/
|
|
93
|
+
isResident(assetID) {
|
|
94
|
+
return this.assets_.has(assetID);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* @param assetID The asset to query.
|
|
98
|
+
* @return {number} The asset's current reference count (0 if absent).
|
|
99
|
+
*/
|
|
100
|
+
refCountOf(assetID) {
|
|
101
|
+
return this.assets_.get(assetID)?.refCount ?? 0;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* @param assetID The asset to query.
|
|
105
|
+
* @return {number} The asset's physical (chunk-rounded) resident bytes
|
|
106
|
+
* (0 if absent).
|
|
107
|
+
*/
|
|
108
|
+
physicalBytesOf(assetID) {
|
|
109
|
+
const asset = this.assets_.get(assetID);
|
|
110
|
+
return asset === void 0 ?
|
|
111
|
+
0 : asset.span.chunks.length * this.pool_.chunkBytes;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared_asset_pool.test.d.ts","sourceRoot":"","sources":["../../../../src/core/mem/shared_asset_pool.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/* eslint-disable no-magic-numbers */
|
|
2
|
+
// mem: assets are stored once and refcounted — the sharing layer's rule is
|
|
3
|
+
// that a release never frees an asset another holder still references.
|
|
4
|
+
import { describe, expect, test } from "@jest/globals";
|
|
5
|
+
import { ChunkedPool } from "./chunked_pool.js";
|
|
6
|
+
import { SharedAssetPool } from "./shared_asset_pool.js";
|
|
7
|
+
describe("SharedAssetPool", () => {
|
|
8
|
+
test("first retain acquires chunks and reports wasAbsent", () => {
|
|
9
|
+
const assets = new SharedAssetPool(new ChunkedPool(1000, 100));
|
|
10
|
+
const first = assets.retain("rep-A", 250);
|
|
11
|
+
expect(first).toEqual({ retained: true, wasAbsent: true });
|
|
12
|
+
expect(assets.isResident("rep-A")).toBe(true);
|
|
13
|
+
expect(assets.physicalBytesOf("rep-A")).toBe(300);
|
|
14
|
+
expect(assets.pool.bytesInUse).toBe(300);
|
|
15
|
+
});
|
|
16
|
+
test("a second retain shares storage instead of duplicating it", () => {
|
|
17
|
+
const assets = new SharedAssetPool(new ChunkedPool(1000, 100));
|
|
18
|
+
assets.retain("rep-A", 250);
|
|
19
|
+
const second = assets.retain("rep-A", 250);
|
|
20
|
+
expect(second).toEqual({ retained: true, wasAbsent: false });
|
|
21
|
+
expect(assets.refCountOf("rep-A")).toBe(2);
|
|
22
|
+
// Stored once: physical bytes unchanged.
|
|
23
|
+
expect(assets.pool.bytesInUse).toBe(300);
|
|
24
|
+
});
|
|
25
|
+
test("release frees only on the last reference", () => {
|
|
26
|
+
const assets = new SharedAssetPool(new ChunkedPool(1000, 100));
|
|
27
|
+
assets.retain("rep-A", 100);
|
|
28
|
+
assets.retain("rep-A", 100);
|
|
29
|
+
expect(assets.release("rep-A")).toBe(false);
|
|
30
|
+
expect(assets.isResident("rep-A")).toBe(true);
|
|
31
|
+
expect(assets.pool.bytesInUse).toBe(100);
|
|
32
|
+
expect(assets.release("rep-A")).toBe(true);
|
|
33
|
+
expect(assets.isResident("rep-A")).toBe(false);
|
|
34
|
+
expect(assets.pool.bytesInUse).toBe(0);
|
|
35
|
+
});
|
|
36
|
+
test("retain fails cleanly when the pool cannot fit a new asset", () => {
|
|
37
|
+
const assets = new SharedAssetPool(new ChunkedPool(200, 100));
|
|
38
|
+
assets.retain("rep-A", 200);
|
|
39
|
+
const refused = assets.retain("rep-B", 100);
|
|
40
|
+
expect(refused).toEqual({ retained: false, wasAbsent: true });
|
|
41
|
+
expect(assets.isResident("rep-B")).toBe(false);
|
|
42
|
+
// A retain of an already-resident asset still succeeds at full pool.
|
|
43
|
+
expect(assets.retain("rep-A", 200).retained).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
test("releasing a non-resident asset is an error, not a silent no-op", () => {
|
|
46
|
+
const assets = new SharedAssetPool(new ChunkedPool(100, 100));
|
|
47
|
+
expect(() => assets.release("ghost")).toThrow(/non-resident/);
|
|
48
|
+
});
|
|
49
|
+
});
|