@bldrs-ai/conway 1.405.1243 → 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/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/version/version.js +1 -1
- package/compiled/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -30,7 +30,7 @@ var import_node_process = require("node:process");
|
|
|
30
30
|
var readline = __toESM(require("node:readline"), 1);
|
|
31
31
|
|
|
32
32
|
// compiled/src/version/version.js
|
|
33
|
-
var versionString = "Conway v1.
|
|
33
|
+
var versionString = "Conway v1.408.1248";
|
|
34
34
|
|
|
35
35
|
// compiled/dependencies/conway-geom/interface/conway_geometry.js
|
|
36
36
|
var wasmType = "";
|
|
@@ -14965,7 +14965,7 @@ ${t5.join("\n")}` : "";
|
|
|
14965
14965
|
var import_process = require("process");
|
|
14966
14966
|
|
|
14967
14967
|
// compiled/src/version/version.js
|
|
14968
|
-
var versionString = "Conway v1.
|
|
14968
|
+
var versionString = "Conway v1.408.1248";
|
|
14969
14969
|
|
|
14970
14970
|
// compiled/dependencies/conway-geom/interface/conway_geometry.js
|
|
14971
14971
|
function pThreadsAllowed() {
|
|
@@ -15943,7 +15943,7 @@ var ParsingBuffer = class {
|
|
|
15943
15943
|
};
|
|
15944
15944
|
|
|
15945
15945
|
// compiled/src/version/version.js
|
|
15946
|
-
var versionString = "Conway v1.
|
|
15946
|
+
var versionString = "Conway v1.408.1248";
|
|
15947
15947
|
|
|
15948
15948
|
// compiled/dependencies/conway-geom/interface/conway_geometry.js
|
|
15949
15949
|
function pThreadsAllowed() {
|
|
@@ -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.408.1248";
|
|
948
948
|
|
|
949
949
|
// compiled/dependencies/conway-geom/interface/conway_geometry.js
|
|
950
950
|
var wasmType = "";
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { GeometryTiles } from "./demand_geometry_queue.js";
|
|
2
|
+
import { ChunkedPool } from "./mem/chunked_pool.js";
|
|
3
|
+
import { SharedAssetPool } from "./mem/shared_asset_pool.js";
|
|
4
|
+
/**
|
|
5
|
+
* One shareable geometry payload an instance needs resident: the asset's
|
|
6
|
+
* identity and its byte size. For IFC, the asset is a representation's
|
|
7
|
+
* tessellated geometry and `byteSize` comes from the wasm-side extract.
|
|
8
|
+
*/
|
|
9
|
+
export interface GeometryAsset<AssetID> {
|
|
10
|
+
assetID: AssetID;
|
|
11
|
+
byteSize: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The domain half the tile pool delegates to: what assets an instance is made
|
|
15
|
+
* of, and how to materialise / discard one asset's payload. This is the seam
|
|
16
|
+
* the production conway-geom surface implements (extract → tessellate into
|
|
17
|
+
* the asset's chunks; discard on last release); tests implement it with
|
|
18
|
+
* recording mocks.
|
|
19
|
+
*/
|
|
20
|
+
export interface InstanceAssetSource<AssetID> {
|
|
21
|
+
/**
|
|
22
|
+
* The assets an instance requires resident, with their byte sizes. For a
|
|
23
|
+
* simple product this is one asset; mapped/instanced representations return
|
|
24
|
+
* the same assetID from many instances — that sharing is the point.
|
|
25
|
+
*
|
|
26
|
+
* @param instanceID The instance (product local ID in the geometry case).
|
|
27
|
+
* @return {GeometryAsset<AssetID>[]} The instance's assets.
|
|
28
|
+
*/
|
|
29
|
+
assetsOf(instanceID: number): GeometryAsset<AssetID>[];
|
|
30
|
+
/**
|
|
31
|
+
* Materialise an asset's payload (called exactly once per residency, on
|
|
32
|
+
* the 0→1 reference). In production: run the wasm extract/tessellate and
|
|
33
|
+
* write the result into the asset's pool chunks.
|
|
34
|
+
*
|
|
35
|
+
* @param assetID The asset to materialise.
|
|
36
|
+
*/
|
|
37
|
+
materialize(assetID: AssetID): void;
|
|
38
|
+
/**
|
|
39
|
+
* Discard an asset's payload (called exactly once per residency, on the
|
|
40
|
+
* 1→0 reference, after its chunks are already back in the pool).
|
|
41
|
+
*
|
|
42
|
+
* @param assetID The asset to discard.
|
|
43
|
+
*/
|
|
44
|
+
discard(assetID: AssetID): void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The geometry narrowing of the general memory system (`src/core/mem/`): a
|
|
48
|
+
* {@link GeometryTiles} backend where a product's "tile" is a set of
|
|
49
|
+
* refcounted, chunk-resident **assets** in a {@link SharedAssetPool}.
|
|
50
|
+
*
|
|
51
|
+
* Composed with {@link DemandGeometryQueue} this completes the M3 resident
|
|
52
|
+
* design: the queue owns demand ordering and the logical budget; this pool
|
|
53
|
+
* owns physical residency. Two accounting views, one invariant:
|
|
54
|
+
*
|
|
55
|
+
* - **Logical (queue-side):** `extract` charges an instance the full
|
|
56
|
+
* chunk-rounded cost of *every* asset it references, shared or not. Sharing
|
|
57
|
+
* is thus double-charged logically — deliberately conservative: summed
|
|
58
|
+
* logical charges always cover physical use, so with the queue's budget set
|
|
59
|
+
* to the pool's budget, an acquire can never fail mid-extract. (Heavy
|
|
60
|
+
* sharing under-utilises the logical budget; the production wiring can
|
|
61
|
+
* widen the queue budget once measured. The safe direction first.)
|
|
62
|
+
* - **Physical (pool-side):** `bytesInUse` counts real chunks. Shared assets
|
|
63
|
+
* are stored once, refcounted, and freed only on the last release — so
|
|
64
|
+
* evicting product A never discards the representation product B still
|
|
65
|
+
* renders (the mapped-item correctness rule, held structurally).
|
|
66
|
+
*/
|
|
67
|
+
export declare class GeometryTilePool<AssetID> implements GeometryTiles {
|
|
68
|
+
private readonly assets_;
|
|
69
|
+
private readonly source_;
|
|
70
|
+
/** Assets each extracted instance holds references on. */
|
|
71
|
+
private readonly held_;
|
|
72
|
+
/**
|
|
73
|
+
* @param pool The chunk pool residents live in.
|
|
74
|
+
* @param source The domain half (asset composition + materialise/discard).
|
|
75
|
+
*/
|
|
76
|
+
constructor(pool: ChunkedPool, source: InstanceAssetSource<AssetID>);
|
|
77
|
+
/**
|
|
78
|
+
* @return {SharedAssetPool<AssetID>} The refcounted asset layer (physical
|
|
79
|
+
* accounting lives here / on its pool).
|
|
80
|
+
*/
|
|
81
|
+
get assets(): SharedAssetPool<AssetID>;
|
|
82
|
+
/**
|
|
83
|
+
* Materialise an instance's tile: take a reference on each of its assets,
|
|
84
|
+
* materialising the ones not yet resident.
|
|
85
|
+
*
|
|
86
|
+
* @param instanceID The instance (product local ID).
|
|
87
|
+
* @return {number} The instance's logical byte cost (chunk-rounded, every
|
|
88
|
+
* asset fully charged — see class docs for why sharing is double-charged).
|
|
89
|
+
*/
|
|
90
|
+
extract(instanceID: number): number;
|
|
91
|
+
/**
|
|
92
|
+
* Release an instance's tile: drop the reference on each of its assets,
|
|
93
|
+
* discarding those this instance was the last holder of.
|
|
94
|
+
*
|
|
95
|
+
* @param instanceID The instance (product local ID).
|
|
96
|
+
*/
|
|
97
|
+
release(instanceID: number): void;
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=geometry_tile_pool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"geometry_tile_pool.d.ts","sourceRoot":"","sources":["../../../src/core/geometry_tile_pool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAGzD;;;;GAIG;AACH,MAAM,WAAW,aAAa,CAAC,OAAO;IACpC,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAGD;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB,CAAC,OAAO;IAE1C;;;;;;;OAOG;IACH,QAAQ,CAAE,UAAU,EAAE,MAAM,GAAI,aAAa,CAAC,OAAO,CAAC,EAAE,CAAA;IAExD;;;;;;OAMG;IACH,WAAW,CAAE,OAAO,EAAE,OAAO,GAAI,IAAI,CAAA;IAErC;;;;;OAKG;IACH,OAAO,CAAE,OAAO,EAAE,OAAO,GAAI,IAAI,CAAA;CAClC;AAGD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,gBAAgB,CAAC,OAAO,CAAE,YAAW,aAAa;IAE7D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAElD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA8B;IAEtD,0DAA0D;IAC1D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA8C;IAEpE;;;OAGG;gBACU,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,mBAAmB,CAAC,OAAO,CAAC;IAKpE;;;OAGG;IACH,IAAW,MAAM,IAAI,eAAe,CAAC,OAAO,CAAC,CAE5C;IAED;;;;;;;OAOG;IACI,OAAO,CAAE,UAAU,EAAE,MAAM,GAAI,MAAM;IAoD5C;;;;;OAKG;IACI,OAAO,CAAE,UAAU,EAAE,MAAM,GAAI,IAAI;CAgB3C"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { SharedAssetPool } from "./mem/shared_asset_pool.js";
|
|
2
|
+
/**
|
|
3
|
+
* The geometry narrowing of the general memory system (`src/core/mem/`): a
|
|
4
|
+
* {@link GeometryTiles} backend where a product's "tile" is a set of
|
|
5
|
+
* refcounted, chunk-resident **assets** in a {@link SharedAssetPool}.
|
|
6
|
+
*
|
|
7
|
+
* Composed with {@link DemandGeometryQueue} this completes the M3 resident
|
|
8
|
+
* design: the queue owns demand ordering and the logical budget; this pool
|
|
9
|
+
* owns physical residency. Two accounting views, one invariant:
|
|
10
|
+
*
|
|
11
|
+
* - **Logical (queue-side):** `extract` charges an instance the full
|
|
12
|
+
* chunk-rounded cost of *every* asset it references, shared or not. Sharing
|
|
13
|
+
* is thus double-charged logically — deliberately conservative: summed
|
|
14
|
+
* logical charges always cover physical use, so with the queue's budget set
|
|
15
|
+
* to the pool's budget, an acquire can never fail mid-extract. (Heavy
|
|
16
|
+
* sharing under-utilises the logical budget; the production wiring can
|
|
17
|
+
* widen the queue budget once measured. The safe direction first.)
|
|
18
|
+
* - **Physical (pool-side):** `bytesInUse` counts real chunks. Shared assets
|
|
19
|
+
* are stored once, refcounted, and freed only on the last release — so
|
|
20
|
+
* evicting product A never discards the representation product B still
|
|
21
|
+
* renders (the mapped-item correctness rule, held structurally).
|
|
22
|
+
*/
|
|
23
|
+
export class GeometryTilePool {
|
|
24
|
+
/**
|
|
25
|
+
* @param pool The chunk pool residents live in.
|
|
26
|
+
* @param source The domain half (asset composition + materialise/discard).
|
|
27
|
+
*/
|
|
28
|
+
constructor(pool, source) {
|
|
29
|
+
/** Assets each extracted instance holds references on. */
|
|
30
|
+
this.held_ = new Map();
|
|
31
|
+
this.assets_ = new SharedAssetPool(pool);
|
|
32
|
+
this.source_ = source;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* @return {SharedAssetPool<AssetID>} The refcounted asset layer (physical
|
|
36
|
+
* accounting lives here / on its pool).
|
|
37
|
+
*/
|
|
38
|
+
get assets() {
|
|
39
|
+
return this.assets_;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Materialise an instance's tile: take a reference on each of its assets,
|
|
43
|
+
* materialising the ones not yet resident.
|
|
44
|
+
*
|
|
45
|
+
* @param instanceID The instance (product local ID).
|
|
46
|
+
* @return {number} The instance's logical byte cost (chunk-rounded, every
|
|
47
|
+
* asset fully charged — see class docs for why sharing is double-charged).
|
|
48
|
+
*/
|
|
49
|
+
extract(instanceID) {
|
|
50
|
+
if (this.held_.has(instanceID)) {
|
|
51
|
+
throw new Error(`Instance ${instanceID} already extracted`);
|
|
52
|
+
}
|
|
53
|
+
const assets = this.source_.assetsOf(instanceID);
|
|
54
|
+
const pool = this.assets_.pool;
|
|
55
|
+
// Validate and price every asset BEFORE taking any references, so a bad
|
|
56
|
+
// byte size (a source bug) throws at zero state instead of mid-extract
|
|
57
|
+
// with references already taken. chunkRound throws on negatives.
|
|
58
|
+
let logicalBytes = 0;
|
|
59
|
+
for (const asset of assets) {
|
|
60
|
+
logicalBytes += pool.chunkRound(asset.byteSize);
|
|
61
|
+
}
|
|
62
|
+
for (const asset of assets) {
|
|
63
|
+
const { retained, wasAbsent } = this.assets_.retain(asset.assetID, asset.byteSize);
|
|
64
|
+
if (!retained) {
|
|
65
|
+
// Unwind references already taken so a failed extract has no effect —
|
|
66
|
+
// keeping the materialize/discard pairing: an asset this unwind takes
|
|
67
|
+
// to zero references must be discarded like any other last release.
|
|
68
|
+
for (const taken of assets) {
|
|
69
|
+
if (taken === asset) {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
if (this.assets_.release(taken.assetID)) {
|
|
73
|
+
this.source_.discard(taken.assetID);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`Geometry pool exhausted materialising instance ${instanceID} — ` +
|
|
77
|
+
"the scheduler budget must not exceed the pool budget");
|
|
78
|
+
}
|
|
79
|
+
if (wasAbsent) {
|
|
80
|
+
this.source_.materialize(asset.assetID);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
this.held_.set(instanceID, assets);
|
|
84
|
+
return logicalBytes;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Release an instance's tile: drop the reference on each of its assets,
|
|
88
|
+
* discarding those this instance was the last holder of.
|
|
89
|
+
*
|
|
90
|
+
* @param instanceID The instance (product local ID).
|
|
91
|
+
*/
|
|
92
|
+
release(instanceID) {
|
|
93
|
+
const assets = this.held_.get(instanceID);
|
|
94
|
+
if (assets === void 0) {
|
|
95
|
+
throw new Error(`Release of un-extracted instance ${instanceID}`);
|
|
96
|
+
}
|
|
97
|
+
this.held_.delete(instanceID);
|
|
98
|
+
for (const asset of assets) {
|
|
99
|
+
if (this.assets_.release(asset.assetID)) {
|
|
100
|
+
this.source_.discard(asset.assetID);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"geometry_tile_pool.test.d.ts","sourceRoot":"","sources":["../../../src/core/geometry_tile_pool.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/* eslint-disable no-magic-numbers */
|
|
2
|
+
// M3 production design: the geometry narrowing of the mem system, alone and
|
|
3
|
+
// composed with DemandGeometryQueue. The load-bearing assertions: shared
|
|
4
|
+
// representations are stored once and never freed while any product still
|
|
5
|
+
// holds them, and the queue's logical budget always covers the pool's
|
|
6
|
+
// physical use (so an extract can never blow the pool mid-flight).
|
|
7
|
+
import { describe, expect, test } from "@jest/globals";
|
|
8
|
+
import { DemandGeometryQueue } from "./demand_geometry_queue.js";
|
|
9
|
+
import { GeometryTilePool } from "./geometry_tile_pool.js";
|
|
10
|
+
import { ChunkedPool } from "./mem/chunked_pool.js";
|
|
11
|
+
/**
|
|
12
|
+
* A recording mock of the domain half: instance → assets from a fixture map,
|
|
13
|
+
* with materialize/discard calls captured.
|
|
14
|
+
*
|
|
15
|
+
* @param composition Instance → assets fixture.
|
|
16
|
+
* @return {object} `{ source, materialized, discarded }`.
|
|
17
|
+
*/
|
|
18
|
+
function mockSource(composition) {
|
|
19
|
+
const materialized = [];
|
|
20
|
+
const discarded = [];
|
|
21
|
+
const source = {
|
|
22
|
+
assetsOf: (instanceID) => composition[instanceID] ?? [],
|
|
23
|
+
materialize: (assetID) => {
|
|
24
|
+
materialized.push(assetID);
|
|
25
|
+
},
|
|
26
|
+
discard: (assetID) => {
|
|
27
|
+
discarded.push(assetID);
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
return { source, materialized, discarded };
|
|
31
|
+
}
|
|
32
|
+
describe("GeometryTilePool", () => {
|
|
33
|
+
test("a shared representation is materialised once and stored once", () => {
|
|
34
|
+
// Products 1 and 2 both instance rep-X (mapped item); 2 also has its own.
|
|
35
|
+
const { source, materialized } = mockSource({
|
|
36
|
+
1: [{ assetID: "rep-X", byteSize: 100 }],
|
|
37
|
+
2: [{ assetID: "rep-X", byteSize: 100 }, { assetID: "rep-Y", byteSize: 100 }],
|
|
38
|
+
});
|
|
39
|
+
const tiles = new GeometryTilePool(new ChunkedPool(1000, 100), source);
|
|
40
|
+
tiles.extract(1);
|
|
41
|
+
tiles.extract(2);
|
|
42
|
+
expect(materialized).toEqual(["rep-X", "rep-Y"]);
|
|
43
|
+
// Physically: two assets, 200 bytes — not 300.
|
|
44
|
+
expect(tiles.assets.pool.bytesInUse).toBe(200);
|
|
45
|
+
expect(tiles.assets.refCountOf("rep-X")).toBe(2);
|
|
46
|
+
});
|
|
47
|
+
test("evicting one product never discards a representation another still renders", () => {
|
|
48
|
+
const { source, discarded } = mockSource({
|
|
49
|
+
1: [{ assetID: "rep-X", byteSize: 100 }],
|
|
50
|
+
2: [{ assetID: "rep-X", byteSize: 100 }],
|
|
51
|
+
});
|
|
52
|
+
const tiles = new GeometryTilePool(new ChunkedPool(1000, 100), source);
|
|
53
|
+
tiles.extract(1);
|
|
54
|
+
tiles.extract(2);
|
|
55
|
+
tiles.release(1);
|
|
56
|
+
expect(discarded).toEqual([]);
|
|
57
|
+
expect(tiles.assets.isResident("rep-X")).toBe(true);
|
|
58
|
+
tiles.release(2);
|
|
59
|
+
expect(discarded).toEqual(["rep-X"]);
|
|
60
|
+
expect(tiles.assets.pool.bytesInUse).toBe(0);
|
|
61
|
+
});
|
|
62
|
+
test("extract charges the logical (chunk-rounded, fully-charged) cost", () => {
|
|
63
|
+
const { source } = mockSource({
|
|
64
|
+
1: [{ assetID: "a", byteSize: 150 }, { assetID: "b", byteSize: 10 }],
|
|
65
|
+
2: [{ assetID: "a", byteSize: 150 }],
|
|
66
|
+
});
|
|
67
|
+
const tiles = new GeometryTilePool(new ChunkedPool(1000, 100), source);
|
|
68
|
+
// 150 → 200 rounded, 10 → 100 rounded.
|
|
69
|
+
expect(tiles.extract(1)).toBe(300);
|
|
70
|
+
// Sharing is deliberately double-charged logically (conservative)...
|
|
71
|
+
expect(tiles.extract(2)).toBe(200);
|
|
72
|
+
// ...while physical stays single-stored.
|
|
73
|
+
expect(tiles.assets.pool.bytesInUse).toBe(300);
|
|
74
|
+
});
|
|
75
|
+
test("a failed extract unwinds cleanly, discarding what it materialised", () => {
|
|
76
|
+
// Asset a fits; asset b cannot (pool holds 2 chunks, b needs 3). The
|
|
77
|
+
// extract must throw, and the unwind must keep the materialize/discard
|
|
78
|
+
// pairing for a (which it took to zero references) and leave the pool
|
|
79
|
+
// exactly as before — a failed extract has no effect.
|
|
80
|
+
const { source, materialized, discarded } = mockSource({
|
|
81
|
+
1: [{ assetID: "a", byteSize: 100 }, { assetID: "b", byteSize: 300 }],
|
|
82
|
+
});
|
|
83
|
+
const pool = new ChunkedPool(200, 100);
|
|
84
|
+
const tiles = new GeometryTilePool(pool, source);
|
|
85
|
+
expect(() => tiles.extract(1)).toThrow(/exhausted/);
|
|
86
|
+
expect(materialized).toEqual(["a"]);
|
|
87
|
+
expect(discarded).toEqual(["a"]);
|
|
88
|
+
expect(pool.bytesInUse).toBe(0);
|
|
89
|
+
expect(tiles.assets.isResident("a")).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
test("a failed extract never discards an asset another instance still holds", () => {
|
|
92
|
+
// Instance 1 holds shared. Instance 2 wants shared + big; big fails.
|
|
93
|
+
// The unwind drops 2's reference on shared but must NOT discard it —
|
|
94
|
+
// instance 1 still renders it.
|
|
95
|
+
const { source, discarded } = mockSource({
|
|
96
|
+
1: [{ assetID: "shared", byteSize: 100 }],
|
|
97
|
+
2: [{ assetID: "shared", byteSize: 100 }, { assetID: "big", byteSize: 300 }],
|
|
98
|
+
});
|
|
99
|
+
const pool = new ChunkedPool(200, 100);
|
|
100
|
+
const tiles = new GeometryTilePool(pool, source);
|
|
101
|
+
tiles.extract(1);
|
|
102
|
+
expect(() => tiles.extract(2)).toThrow(/exhausted/);
|
|
103
|
+
expect(discarded).toEqual([]);
|
|
104
|
+
expect(tiles.assets.isResident("shared")).toBe(true);
|
|
105
|
+
expect(tiles.assets.refCountOf("shared")).toBe(1);
|
|
106
|
+
expect(pool.bytesInUse).toBe(100);
|
|
107
|
+
});
|
|
108
|
+
test("double extract and unmatched release are errors", () => {
|
|
109
|
+
const { source } = mockSource({ 1: [{ assetID: "a", byteSize: 10 }] });
|
|
110
|
+
const tiles = new GeometryTilePool(new ChunkedPool(1000, 100), source);
|
|
111
|
+
tiles.extract(1);
|
|
112
|
+
expect(() => tiles.extract(1)).toThrow(/already extracted/);
|
|
113
|
+
expect(() => tiles.release(2)).toThrow(/un-extracted/);
|
|
114
|
+
});
|
|
115
|
+
test("a malformed byte size throws before any state changes", () => {
|
|
116
|
+
// A buggy source returning a negative size must fail at zero state —
|
|
117
|
+
// no references taken, nothing materialised, pool untouched.
|
|
118
|
+
const { source, materialized } = mockSource({
|
|
119
|
+
1: [{ assetID: "ok", byteSize: 100 }, { assetID: "bad", byteSize: -5 }],
|
|
120
|
+
});
|
|
121
|
+
const pool = new ChunkedPool(1000, 100);
|
|
122
|
+
const tiles = new GeometryTilePool(pool, source);
|
|
123
|
+
expect(() => tiles.extract(1)).toThrow(/Invalid byte size/);
|
|
124
|
+
expect(materialized).toEqual([]);
|
|
125
|
+
expect(pool.bytesInUse).toBe(0);
|
|
126
|
+
expect(tiles.assets.isResident("ok")).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
test("composed with the demand queue: budget holds, shared reps survive eviction", () => {
|
|
129
|
+
// 20 products all sharing one heavy rep, each with a light unique rep —
|
|
130
|
+
// the mapped-item shape (fixtures, repeated windows).
|
|
131
|
+
const composition = {};
|
|
132
|
+
for (let id = 0; id < 20; ++id) {
|
|
133
|
+
composition[id] = [
|
|
134
|
+
{ assetID: "shared-heavy", byteSize: 400 },
|
|
135
|
+
{ assetID: `unique-${id}`, byteSize: 100 },
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
const { source, materialized, discarded } = mockSource(composition);
|
|
139
|
+
const pool = new ChunkedPool(4000, 100);
|
|
140
|
+
const tiles = new GeometryTilePool(pool, source);
|
|
141
|
+
// Queue budget == pool budget: the safety invariant under test.
|
|
142
|
+
const queue = new DemandGeometryQueue(tiles, 4000);
|
|
143
|
+
// Stream demand across all 20 products with rising priority.
|
|
144
|
+
for (let id = 0; id < 20; ++id) {
|
|
145
|
+
queue.request(id, id);
|
|
146
|
+
queue.pump();
|
|
147
|
+
// Physical use never exceeds the logical budget the queue enforces.
|
|
148
|
+
expect(pool.bytesInUse).toBeLessThanOrEqual(queue.stats.residentBytes);
|
|
149
|
+
expect(queue.stats.residentBytes).toBeLessThanOrEqual(4000);
|
|
150
|
+
}
|
|
151
|
+
// The shared rep was materialised exactly once across all that churn...
|
|
152
|
+
expect(materialized.filter((a) => a === "shared-heavy").length).toBe(1);
|
|
153
|
+
// ...and never discarded, because some product always held it.
|
|
154
|
+
expect(discarded).not.toContain("shared-heavy");
|
|
155
|
+
// Evicting everything finally discards it and empties the pool.
|
|
156
|
+
queue.evictAll();
|
|
157
|
+
expect(discarded).toContain("shared-heavy");
|
|
158
|
+
expect(pool.bytesInUse).toBe(0);
|
|
159
|
+
});
|
|
160
|
+
test("composed queue at exact pool budget never exhausts the pool", () => {
|
|
161
|
+
// Every product unique (worst case for the invariant: no sharing slack).
|
|
162
|
+
const composition = {};
|
|
163
|
+
for (let id = 0; id < 50; ++id) {
|
|
164
|
+
composition[id] = [{ assetID: `u-${id}`, byteSize: 190 }];
|
|
165
|
+
}
|
|
166
|
+
const { source } = mockSource(composition);
|
|
167
|
+
const pool = new ChunkedPool(1000, 100);
|
|
168
|
+
const tiles = new GeometryTilePool(pool, source);
|
|
169
|
+
const queue = new DemandGeometryQueue(tiles, 1000);
|
|
170
|
+
// 190 rounds to 200 → 5 tiles fit. Churn all 50 through; extract must
|
|
171
|
+
// never throw pool-exhausted because logical charges cover physical.
|
|
172
|
+
for (let id = 0; id < 50; ++id) {
|
|
173
|
+
queue.request(id, id);
|
|
174
|
+
queue.pump();
|
|
175
|
+
}
|
|
176
|
+
expect(queue.stats.residentCount).toBe(5);
|
|
177
|
+
expect(pool.bytesInUse).toBe(1000);
|
|
178
|
+
expect(pool.stats.failedAcquires).toBe(0);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -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"}
|