@woosh/meep-engine 2.121.13 → 2.122.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "description": "Fully featured ECS game engine written in JavaScript",
6
6
  "type": "module",
7
7
  "author": "Alexander Goldring",
8
- "version": "2.121.13",
8
+ "version": "2.122.1",
9
9
  "main": "build/meep.module.js",
10
10
  "module": "build/meep.module.js",
11
11
  "exports": {
@@ -0,0 +1,113 @@
1
+ export const ALLOCATOR_NO_SPACE: 4294967295;
2
+ /**
3
+ * Fast hard realtime O(1) offset allocator with minimal fragmentation.
4
+ *
5
+ * Uses 256 bins with 8 bit floating point distribution (3 bit mantissa + 5 bit exponent) and a two level bitfield to find the next available bin using 2x LZCNT instructions to make all operations O(1). Bin sizes following the floating point distribution ensures hard bounds for memory overhead percentage regardless of size class. Pow2 bins would waste up to +100% memory (+50% on average). Our float bins waste up to +12.5% (+6.25% on average).
6
+ *
7
+ * The allocation metadata is stored in a separate data structure, making this allocator suitable for sub-allocating any resources, such as GPU heaps, buffers and arrays. Returns an offset to the first element of the allocated contiguous range.
8
+ *
9
+ * @see "A comparison of memory allocators for real-time applications" by Miguel Masmano et al
10
+ * @example
11
+ * const allocator = new Allocator(12345); // Allocator with 12345 contiguous elements in total
12
+ *
13
+ * const a = allocator.allocate(1337); // Allocate a 1337 element contiguous range
14
+ * const offset_a = a.offset; // Provides offset to the first element of the range
15
+ * do_something(offset_a);
16
+ *
17
+ * const b = allocator.allocate(123); // Allocate a 123 element contiguous range
18
+ * const offset_b = b.offset; // Provides offset to the first element of the range
19
+ * do_something(offset_b);
20
+ *
21
+ * allocator.free(a); // Free allocation a
22
+ * allocator.free(b); // Free allocation b
23
+ */
24
+ export class OffsetAllocator {
25
+ /**
26
+ *
27
+ * @param {number} size
28
+ * @param {number} maxAllocs
29
+ */
30
+ constructor(size: number, maxAllocs?: number);
31
+ /**
32
+ * Total managed space
33
+ * uint32
34
+ * @type {number}
35
+ */
36
+ size: number;
37
+ /**
38
+ * Limit on number of allocations
39
+ * uint32
40
+ * @type {number}
41
+ */
42
+ maxAllocs: number;
43
+ /**
44
+ * uint32
45
+ * @type {number}
46
+ */
47
+ freeStorage: number;
48
+ /**
49
+ * uint32
50
+ * @type {number}
51
+ */
52
+ usedBinsTop: number;
53
+ /**
54
+ *
55
+ * @type {Uint8Array}
56
+ */
57
+ usedBins: Uint8Array;
58
+ /**
59
+ *
60
+ * @type {Uint32Array}
61
+ */
62
+ binIndices: Uint32Array;
63
+ /**
64
+ *
65
+ * @type {RowFirstTable}
66
+ */
67
+ nodes: RowFirstTable;
68
+ /**
69
+ * @type {Uint32Array}
70
+ */
71
+ freeNodes: Uint32Array;
72
+ /**
73
+ * uint32
74
+ * @type {number}
75
+ */
76
+ freeOffset: number;
77
+ /**
78
+ *
79
+ * @param {number} size
80
+ * @returns {Allocation}
81
+ */
82
+ allocate(size: number): Allocation;
83
+ /**
84
+ *
85
+ * @param {Allocation} allocation
86
+ */
87
+ free(allocation: Allocation): void;
88
+ reset(): void;
89
+ /**
90
+ * Useful for debug and UI
91
+ * @return {StorageReport}
92
+ */
93
+ storageReport(): StorageReport;
94
+ #private;
95
+ }
96
+ import { RowFirstTable } from "../../collection/table/RowFirstTable.js";
97
+ declare class Allocation {
98
+ /**
99
+ *
100
+ * @param {number} offset
101
+ * @param {number} metadata
102
+ * @return {Allocation}
103
+ */
104
+ static from(offset: number, metadata: number): Allocation;
105
+ offset: number;
106
+ metadata: number;
107
+ }
108
+ declare class StorageReport {
109
+ totalFreeSpace: number;
110
+ largestFreeRegion: number;
111
+ }
112
+ export {};
113
+ //# sourceMappingURL=OffsetAllocator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OffsetAllocator.d.ts","sourceRoot":"","sources":["../../../../../src/core/binary/allocator/OffsetAllocator.js"],"names":[],"mappings":"AAaA,iCAAkC,UAAU,CAAC;AAmF7C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH;IAwDI;;;;OAIG;IACH,kBAHW,MAAM,cACN,MAAM,EAYhB;IAtED;;;;OAIG;IACH,MAFU,MAAM,CAEP;IAET;;;;OAIG;IACH,WAFU,MAAM,CAEF;IAEd;;;OAGG;IACH,aAFU,MAAM,CAEA;IAEhB;;;OAGG;IACH,aAFU,MAAM,CAEA;IAEhB;;;OAGG;IACH,UAFU,UAAU,CAEoB;IAExC;;;OAGG;IACH,YAFU,WAAW,CAEuB;IAE5C;;;OAGG;IACH,OAFU,aAAa,CAEmB;IAE1C;;OAEG;IACH,WAFU,WAAW,CAEX;IAEV;;;OAGG;IACH,YAFU,MAAM,CAED;IAmBf;;;;OAIG;IACH,eAHW,MAAM,GACJ,UAAU,CAkGtB;IAED;;;OAGG;IACH,iBAFW,UAAU,QAsFpB;IAqID,cA4BC;IAED;;;OAGG;IACH,iBAFY,aAAa,CA2BxB;;CACJ;8BA9jB6B,yCAAyC;AAgDvE;IAII;;;;;OAKG;IACH,oBAJW,MAAM,YACN,MAAM,GACL,UAAU,CASrB;IAhBD,eAA4B;IAC5B,iBAA8B;CAgBjC;AAoBD;IACI,uBAAkB;IAClB,0BAAqB;CACxB"}
@@ -0,0 +1,576 @@
1
+ import { assert } from "../../assert.js";
2
+ import { RowFirstTable } from "../../collection/table/RowFirstTable.js";
3
+ import { RowFirstTableSpec } from "../../collection/table/RowFirstTableSpec.js";
4
+ import { ctz32 } from "../ctz32.js";
5
+ import { floatToUint, uintToFloatRoundDown, uintToFloatRoundUp } from "../float/SmallFloat.js";
6
+ import { msb_32 } from "../msb_32.js";
7
+ import { BinaryDataType } from "../type/BinaryDataType.js";
8
+
9
+ const NUM_TOP_BINS = 32;
10
+ const BINS_PER_LEAF = 8;
11
+ const TOP_BINS_INDEX_SHIFT = 3;
12
+ const LEAF_BINS_INDEX_MASK = 0x7;
13
+ const NUM_LEAF_BINS = NUM_TOP_BINS * BINS_PER_LEAF;
14
+ export const ALLOCATOR_NO_SPACE = 0xffffffff;
15
+ const NODE_UNUSED = 0xffffffff
16
+
17
+ /**
18
+ * Can potentially use u16, but number of allocation slots will be limited to 65k, so a severe limitation for little gain
19
+ * @type {BinaryDataType}
20
+ */
21
+ const NODE_INDEX_DATA_TYPE = BinaryDataType.Uint32;
22
+
23
+ const DEBUG_VERBOSE = false;
24
+
25
+ /**
26
+ * @see https://github.com/sebbbi/OffsetAllocator/blob/3610a7377088b1e8c8f1525f458c96038a4e6fc0/offsetAllocator.hpp#L70
27
+ * @type {RowFirstTableSpec}
28
+ */
29
+ const NODE_TABLE_SPEC = RowFirstTableSpec.get([
30
+ BinaryDataType.Uint32, // Data Offset
31
+ BinaryDataType.Uint32, // Data Size
32
+ NODE_INDEX_DATA_TYPE, // Bin List Prev
33
+ NODE_INDEX_DATA_TYPE, // Bin List Next
34
+ NODE_INDEX_DATA_TYPE, // Neighbour Prev
35
+ NODE_INDEX_DATA_TYPE, // Neighbour Next
36
+ BinaryDataType.Uint8, // FLAGS
37
+ ]);
38
+
39
+ const NODE_FLAGS_USED = 1;
40
+
41
+
42
+ const NODE_FIELD_DATA_OFFSET = 0;
43
+ const NODE_FIELD_DATA_SIZE = 1;
44
+ const NODE_FIELD_BIN_LIST_PREV = 2;
45
+ const NODE_FIELD_BIN_LIST_NEXT = 3;
46
+ const NODE_FIELD_NEIGHBOR_PREV = 4;
47
+ const NODE_FIELD_NEIGHBOR_NEXT = 5;
48
+ const NODE_FIELD_FLAGS = 6;
49
+
50
+ class Allocation {
51
+ offset = ALLOCATOR_NO_SPACE;
52
+ metadata = ALLOCATOR_NO_SPACE;
53
+
54
+ /**
55
+ *
56
+ * @param {number} offset
57
+ * @param {number} metadata
58
+ * @return {Allocation}
59
+ */
60
+ static from(offset, metadata) {
61
+ const r = new Allocation();
62
+
63
+ r.offset = offset;
64
+ r.metadata = metadata;
65
+
66
+ return r;
67
+ }
68
+ }
69
+
70
+ /**
71
+ *
72
+ * @param {number} bitMask
73
+ * @param {number} startBitIndex
74
+ * @return {number}
75
+ */
76
+ function findLowestSetBitAfter(bitMask, startBitIndex) {
77
+ const maskBeforeStartIndex = (1 << startBitIndex) - 1;
78
+ const maskAfterStartIndex = ~maskBeforeStartIndex;
79
+ const bitsAfter = bitMask & maskAfterStartIndex;
80
+
81
+ if (bitsAfter === 0) {
82
+ return ALLOCATOR_NO_SPACE;
83
+ }
84
+
85
+ return ctz32(bitsAfter);
86
+ }
87
+
88
+ class StorageReport {
89
+ totalFreeSpace = 0
90
+ largestFreeRegion = 0
91
+ }
92
+
93
+ /*
94
+ Largely a port of https://github.com/sebbbi/OffsetAllocator by "Sebastian Aaltonen"
95
+ */
96
+
97
+ /**
98
+ * Fast hard realtime O(1) offset allocator with minimal fragmentation.
99
+ *
100
+ * Uses 256 bins with 8 bit floating point distribution (3 bit mantissa + 5 bit exponent) and a two level bitfield to find the next available bin using 2x LZCNT instructions to make all operations O(1). Bin sizes following the floating point distribution ensures hard bounds for memory overhead percentage regardless of size class. Pow2 bins would waste up to +100% memory (+50% on average). Our float bins waste up to +12.5% (+6.25% on average).
101
+ *
102
+ * The allocation metadata is stored in a separate data structure, making this allocator suitable for sub-allocating any resources, such as GPU heaps, buffers and arrays. Returns an offset to the first element of the allocated contiguous range.
103
+ *
104
+ * @see "A comparison of memory allocators for real-time applications" by Miguel Masmano et al
105
+ * @example
106
+ * const allocator = new Allocator(12345); // Allocator with 12345 contiguous elements in total
107
+ *
108
+ * const a = allocator.allocate(1337); // Allocate a 1337 element contiguous range
109
+ * const offset_a = a.offset; // Provides offset to the first element of the range
110
+ * do_something(offset_a);
111
+ *
112
+ * const b = allocator.allocate(123); // Allocate a 123 element contiguous range
113
+ * const offset_b = b.offset; // Provides offset to the first element of the range
114
+ * do_something(offset_b);
115
+ *
116
+ * allocator.free(a); // Free allocation a
117
+ * allocator.free(b); // Free allocation b
118
+ */
119
+ export class OffsetAllocator {
120
+ /**
121
+ * Total managed space
122
+ * uint32
123
+ * @type {number}
124
+ */
125
+ size = 0;
126
+
127
+ /**
128
+ * Limit on number of allocations
129
+ * uint32
130
+ * @type {number}
131
+ */
132
+ maxAllocs = 0;
133
+
134
+ /**
135
+ * uint32
136
+ * @type {number}
137
+ */
138
+ freeStorage = 0;
139
+
140
+ /**
141
+ * uint32
142
+ * @type {number}
143
+ */
144
+ usedBinsTop = 0;
145
+
146
+ /**
147
+ *
148
+ * @type {Uint8Array}
149
+ */
150
+ usedBins = new Uint8Array(NUM_TOP_BINS);
151
+
152
+ /**
153
+ *
154
+ * @type {Uint32Array}
155
+ */
156
+ binIndices = new Uint32Array(NUM_LEAF_BINS);
157
+
158
+ /**
159
+ *
160
+ * @type {RowFirstTable}
161
+ */
162
+ nodes = new RowFirstTable(NODE_TABLE_SPEC)
163
+
164
+ /**
165
+ * @type {Uint32Array}
166
+ */
167
+ freeNodes;
168
+
169
+ /**
170
+ * uint32
171
+ * @type {number}
172
+ */
173
+ freeOffset = 0;
174
+
175
+ /**
176
+ *
177
+ * @param {number} size
178
+ * @param {number} maxAllocs
179
+ */
180
+ constructor(size, maxAllocs = 128 * 1024) {
181
+ assert.isNonNegativeInteger(size, 'size');
182
+ assert.isNonNegativeInteger(maxAllocs, 'maxAllocs');
183
+
184
+ this.size = size;
185
+ this.maxAllocs = maxAllocs;
186
+
187
+ this.freeNodes = new Uint32Array(this.maxAllocs);
188
+
189
+ this.reset();
190
+ }
191
+
192
+ /**
193
+ *
194
+ * @param {number} size
195
+ * @returns {Allocation}
196
+ */
197
+ allocate(size) {
198
+ assert.isNonNegativeInteger(size, 'size');
199
+
200
+ // Out of allocations?
201
+ if (this.freeOffset === 0) {
202
+ return Allocation.from(ALLOCATOR_NO_SPACE, ALLOCATOR_NO_SPACE);
203
+ }
204
+
205
+ // Round up to bin index to ensure that alloc >= bin
206
+ // Gives us min bin index that fits the size
207
+ let minBinIndex = uintToFloatRoundUp(size);
208
+
209
+ const minTopBinIndex = minBinIndex >>> TOP_BINS_INDEX_SHIFT;
210
+ const minLeafBinIndex = minBinIndex & LEAF_BINS_INDEX_MASK;
211
+
212
+ let topBinIndex = minTopBinIndex;
213
+ let leafBinIndex = ALLOCATOR_NO_SPACE;
214
+
215
+ // If top bin exists, scan its leaf bin. This can fail (NO_SPACE).
216
+ if ((this.usedBinsTop & (1 << topBinIndex)) !== 0) {
217
+ leafBinIndex = findLowestSetBitAfter(this.usedBins[topBinIndex], minLeafBinIndex);
218
+ }
219
+
220
+ // If we didn't find space in top bin, we search top bin from +1
221
+ if (leafBinIndex === ALLOCATOR_NO_SPACE) {
222
+ topBinIndex = findLowestSetBitAfter(this.usedBinsTop, minTopBinIndex + 1);
223
+
224
+ // Out of space?
225
+ if (topBinIndex === ALLOCATOR_NO_SPACE) {
226
+ return Allocation.from(ALLOCATOR_NO_SPACE, ALLOCATOR_NO_SPACE);
227
+ }
228
+
229
+ // All leaf bins here fit the alloc, since the top bin was rounded up. Start leaf search from bit 0.
230
+ // NOTE: This search can't fail since at least one leaf bit was set because the top bit was set.
231
+ leafBinIndex = ctz32(this.usedBins[topBinIndex]);
232
+ }
233
+
234
+ let bin_index = ((topBinIndex << TOP_BINS_INDEX_SHIFT) | leafBinIndex) >>> 0; // ">>> 0" shift is to force UINT32
235
+
236
+ // Pop the top node of the bin. Bin top = node.next.
237
+ let node_index = this.binIndices[bin_index];
238
+
239
+ const nodes = this.nodes;
240
+
241
+ const nodeTotalSize = nodes.readCellValue(node_index, NODE_FIELD_DATA_SIZE);
242
+ nodes.writeCellValue(node_index, NODE_FIELD_DATA_SIZE, size);
243
+ nodes.writeCellValue(node_index, NODE_FIELD_FLAGS, NODE_FLAGS_USED); //mark as USED
244
+
245
+ const node_bin_list_next = nodes.readCellValue(node_index, NODE_FIELD_BIN_LIST_NEXT);
246
+ this.binIndices[bin_index] = node_bin_list_next;
247
+
248
+ if (node_bin_list_next !== NODE_UNUSED) {
249
+ nodes.writeCellValue(node_bin_list_next, NODE_FIELD_BIN_LIST_PREV, NODE_UNUSED);
250
+ }
251
+
252
+ this.freeStorage -= nodeTotalSize;
253
+
254
+ if (DEBUG_VERBOSE) {
255
+ console.log(`Free storage: ${this.freeStorage} (-${nodeTotalSize}) (allocate)`);
256
+ }
257
+
258
+ // Bin empty?
259
+ if (this.binIndices[bin_index] === NODE_UNUSED) {
260
+ // Remove a leaf bin mask bit
261
+ this.usedBins[topBinIndex] &= ~(1 << leafBinIndex);
262
+
263
+ // All leaf bins empty?
264
+ if (this.usedBins[topBinIndex] === 0) {
265
+ // Remove a top bin mask bit
266
+ this.usedBinsTop &= ~(1 << topBinIndex);
267
+ }
268
+ }
269
+
270
+ // Push back reminder N elements to a lower bin
271
+ const reminderSize = nodeTotalSize - size;
272
+
273
+ const node_data_offset = nodes.readCellValue(node_index, NODE_FIELD_DATA_OFFSET);
274
+
275
+ if (reminderSize > 0) {
276
+ const new_node_index = this.#insertNodeIntoBin(reminderSize, node_data_offset + size);
277
+
278
+ const node_neighbor_next = nodes.readCellValue(node_index, NODE_FIELD_NEIGHBOR_NEXT);
279
+
280
+ // Link nodes next to each other so that we can merge them later if both are free
281
+ // And update the old next neighbor to point to the new node (in middle)
282
+ if (node_neighbor_next !== NODE_UNUSED) {
283
+ nodes.writeCellValue(node_neighbor_next, NODE_FIELD_NEIGHBOR_PREV, new_node_index);
284
+ }
285
+ nodes.writeCellValue(new_node_index, NODE_FIELD_NEIGHBOR_PREV, node_index);
286
+ nodes.writeCellValue(new_node_index, NODE_FIELD_NEIGHBOR_NEXT, node_neighbor_next);
287
+
288
+ nodes.writeCellValue(node_index, NODE_FIELD_NEIGHBOR_NEXT, new_node_index);
289
+ }
290
+
291
+
292
+ return Allocation.from(node_data_offset, node_index);
293
+ }
294
+
295
+ /**
296
+ *
297
+ * @param {Allocation} allocation
298
+ */
299
+ free(allocation) {
300
+ assert.notEqual(allocation.metadata, ALLOCATOR_NO_SPACE, 'Invalid allocation');
301
+ // if (!m_nodes) return;
302
+
303
+ const node_index = allocation.metadata;
304
+
305
+ const nodes = this.nodes;
306
+ const node_flags = nodes.readCellValue(node_index, NODE_FIELD_FLAGS);
307
+
308
+ // Double delete check
309
+ assert.notEqual(node_flags & NODE_FLAGS_USED, 0, 'Node is not in use');
310
+
311
+ // Merge with neighbors...
312
+ let offset = nodes.readCellValue(node_index, NODE_FIELD_DATA_OFFSET)
313
+ let size = nodes.readCellValue(node_index, NODE_FIELD_DATA_SIZE);
314
+
315
+ const node_neighbour_prev = nodes.readCellValue(node_index, NODE_FIELD_NEIGHBOR_PREV);
316
+
317
+ if (
318
+ (node_neighbour_prev !== NODE_UNUSED)
319
+ && ((nodes.readCellValue(node_neighbour_prev, NODE_FIELD_FLAGS) & NODE_FLAGS_USED) === 0) // not used
320
+ ) {
321
+ // Previous (contiguous) free node: Change offset to previous node offset. Sum sizes
322
+
323
+ const previous_offset = nodes.readCellValue(node_neighbour_prev, NODE_FIELD_DATA_OFFSET);
324
+ const previous_size = nodes.readCellValue(node_neighbour_prev, NODE_FIELD_DATA_SIZE);
325
+
326
+ offset = previous_offset;
327
+ size += previous_size;
328
+
329
+ // Remove node from the bin linked list and put it in the freelist
330
+ this.#removeNodeFromBin(node_neighbour_prev);
331
+
332
+ assert.equal(nodes.readCellValue(node_neighbour_prev, NODE_FIELD_NEIGHBOR_NEXT), node_index);
333
+
334
+ nodes.writeCellValue(
335
+ node_index, NODE_FIELD_NEIGHBOR_PREV,
336
+ nodes.readCellValue(node_neighbour_prev, NODE_FIELD_NEIGHBOR_PREV)
337
+ );
338
+ }
339
+
340
+ const node_neighbour_next = nodes.readCellValue(node_index, NODE_FIELD_NEIGHBOR_NEXT);
341
+
342
+ if (
343
+ (node_neighbour_next !== NODE_UNUSED)
344
+ && ((nodes.readCellValue(node_neighbour_next, NODE_FIELD_FLAGS) & NODE_FLAGS_USED) === 0) // not used
345
+ ) {
346
+ // Next (contiguous) free node: Offset remains the same. Sum sizes.
347
+
348
+ size += nodes.readCellValue(node_neighbour_next, NODE_FIELD_DATA_SIZE);
349
+
350
+ // Remove node from the bin linked list and put it in the freelist
351
+ this.#removeNodeFromBin(node_neighbour_next);
352
+
353
+ assert.equal(nodes.readCellValue(node_neighbour_next, NODE_FIELD_NEIGHBOR_PREV), node_index);
354
+
355
+ nodes.writeCellValue(
356
+ node_index, NODE_FIELD_NEIGHBOR_NEXT,
357
+ nodes.readCellValue(node_neighbour_next, NODE_FIELD_NEIGHBOR_NEXT)
358
+ );
359
+ }
360
+
361
+ const neighborNext = nodes.readCellValue(node_index, NODE_FIELD_NEIGHBOR_NEXT);
362
+ const neighborPrev = nodes.readCellValue(node_index, NODE_FIELD_NEIGHBOR_PREV);
363
+
364
+ // Insert the removed node to freelist
365
+ if (DEBUG_VERBOSE) {
366
+ console.log(`Putting node ${node_index} into freelist[${this.freeOffset + 1}] (free)`);
367
+ }
368
+
369
+ this.freeNodes[++this.freeOffset] = node_index;
370
+
371
+ // Insert the (combined) free node to bin
372
+ const combinedNodeIndex = this.#insertNodeIntoBin(size, offset);
373
+
374
+ // Connect neighbors with the new combined node
375
+ if (neighborNext !== NODE_UNUSED) {
376
+ nodes.writeCellValue(combinedNodeIndex, NODE_FIELD_NEIGHBOR_NEXT, neighborNext);
377
+ nodes.writeCellValue(neighborNext, NODE_FIELD_NEIGHBOR_PREV, combinedNodeIndex);
378
+ }
379
+ if (neighborPrev !== NODE_UNUSED) {
380
+ nodes.writeCellValue(combinedNodeIndex, NODE_FIELD_NEIGHBOR_PREV, neighborPrev);
381
+ nodes.writeCellValue(neighborPrev, NODE_FIELD_NEIGHBOR_NEXT, combinedNodeIndex);
382
+ }
383
+ }
384
+
385
+ /**
386
+ *
387
+ * @param {number} size
388
+ * @param {number} data_offset
389
+ * @returns {number}
390
+ */
391
+ #insertNodeIntoBin(size, data_offset) {
392
+ assert.isNonNegativeInteger(size, 'size');
393
+ assert.isNonNegativeInteger(data_offset, 'size');
394
+
395
+ // Round down to bin index to ensure that bin >= alloc
396
+ const bin_index = uintToFloatRoundDown(size);
397
+
398
+ const top_bin_index = bin_index >>> TOP_BINS_INDEX_SHIFT;
399
+ const leafBinIndex = bin_index & LEAF_BINS_INDEX_MASK;
400
+
401
+ // Bin was empty before?
402
+ if (this.binIndices[bin_index] === NODE_UNUSED) {
403
+ // Set bin mask bits
404
+ this.usedBins[top_bin_index] |= 1 << leafBinIndex;
405
+ this.usedBinsTop |= 1 << top_bin_index;
406
+ }
407
+
408
+ // Take a freelist node and insert on top of the bin linked list (next = old top)
409
+ const top_node_index = this.binIndices[bin_index];
410
+ const node_index = this.freeNodes[this.freeOffset--];
411
+
412
+ if (DEBUG_VERBOSE) {
413
+ console.log(`Getting node ${node_index} from freelist[${this.freeOffset + 1}]`);
414
+ }
415
+
416
+ const nodes = this.nodes;
417
+
418
+ // initialize node
419
+
420
+ nodes.writeCellValue(node_index, NODE_FIELD_DATA_OFFSET, data_offset);
421
+ nodes.writeCellValue(node_index, NODE_FIELD_DATA_SIZE, size);
422
+
423
+ nodes.writeCellValue(node_index, NODE_FIELD_BIN_LIST_PREV, NODE_UNUSED);
424
+ nodes.writeCellValue(node_index, NODE_FIELD_BIN_LIST_NEXT, top_node_index);
425
+
426
+ nodes.writeCellValue(node_index, NODE_FIELD_NEIGHBOR_PREV, NODE_UNUSED);
427
+ nodes.writeCellValue(node_index, NODE_FIELD_NEIGHBOR_NEXT, NODE_UNUSED);
428
+
429
+ nodes.writeCellValue(node_index, NODE_FIELD_FLAGS, 0);
430
+
431
+ if (top_node_index !== NODE_UNUSED) {
432
+ nodes.writeCellValue(top_node_index, NODE_FIELD_BIN_LIST_PREV, node_index);
433
+ }
434
+
435
+ this.binIndices[bin_index] = node_index;
436
+
437
+ this.freeStorage += size;
438
+
439
+ if (DEBUG_VERBOSE) {
440
+ console.log(`Free storage: ${this.freeStorage} (+${size}) (insertNodeIntoBin)`);
441
+ }
442
+
443
+ return node_index;
444
+ }
445
+
446
+ /**
447
+ *
448
+ * @param {number} node_index
449
+ */
450
+ #removeNodeFromBin(node_index) {
451
+ assert.isNonNegativeInteger(node_index, 'node_index');
452
+
453
+ const nodes = this.nodes;
454
+ const node_bin_list_prev = nodes.readCellValue(node_index, NODE_FIELD_BIN_LIST_PREV);
455
+ if (node_bin_list_prev !== NODE_UNUSED) {
456
+ // Easy case: We have previous node. Just remove this node from the middle of the list.
457
+ const node_bin_list_next = nodes.readCellValue(node_index, NODE_FIELD_BIN_LIST_NEXT);
458
+ nodes.writeCellValue(
459
+ node_bin_list_prev, NODE_FIELD_BIN_LIST_NEXT,
460
+ node_bin_list_next
461
+ );
462
+
463
+ if (node_bin_list_next !== NODE_UNUSED) {
464
+ nodes.writeCellValue(
465
+ node_bin_list_next, NODE_FIELD_BIN_LIST_PREV,
466
+ node_bin_list_prev
467
+ )
468
+ }
469
+ } else {
470
+ // Hard case: We are the first node in a bin. Find the bin.
471
+
472
+ // Round down to bin index to ensure that bin >= alloc
473
+ const node_data_size = nodes.readCellValue(node_index, NODE_FIELD_DATA_SIZE);
474
+ const binIndex = uintToFloatRoundDown(node_data_size);
475
+
476
+ const topBinIndex = binIndex >> TOP_BINS_INDEX_SHIFT;
477
+ const leafBinIndex = binIndex & LEAF_BINS_INDEX_MASK;
478
+
479
+ const node_bin_list_next = nodes.readCellValue(node_index, NODE_FIELD_BIN_LIST_NEXT);
480
+ this.binIndices[binIndex] = node_bin_list_next;
481
+ if (node_bin_list_next !== NODE_UNUSED) {
482
+ nodes.writeCellValue(
483
+ node_bin_list_next, NODE_FIELD_BIN_LIST_PREV,
484
+ NODE_UNUSED
485
+ );
486
+ }
487
+
488
+ // Bin empty?
489
+ if (this.binIndices[binIndex] === NODE_UNUSED) {
490
+ // Remove a leaf bin mask bit
491
+ this.usedBins[topBinIndex] &= ~(1 << leafBinIndex);
492
+
493
+ // All leaf bins empty?
494
+ if (this.usedBins[topBinIndex] === 0) {
495
+ // Remove a top bin mask bit
496
+ this.usedBinsTop &= ~(1 << topBinIndex);
497
+ }
498
+ }
499
+ }
500
+
501
+ // Insert the node to freelist
502
+ if (DEBUG_VERBOSE) {
503
+ console.log(`Putting node ${node_index} into freelist[${this.freeOffset + 1}] (removeNodeFromBin)`);
504
+ }
505
+
506
+ this.freeNodes[++this.freeOffset] = node_index;
507
+
508
+ const node_data_size = nodes.readCellValue(node_index, NODE_FIELD_DATA_SIZE);
509
+ this.freeStorage -= node_data_size;
510
+
511
+ if (DEBUG_VERBOSE) {
512
+ console.log(`Free storage: ${this.freeStorage} (-${node_data_size}) (removeNodeFromBin)`);
513
+ }
514
+ }
515
+
516
+ reset() {
517
+ this.freeStorage = 0;
518
+ this.usedBinsTop = 0;
519
+
520
+ const maxAllocs = this.maxAllocs;
521
+
522
+ this.freeOffset = maxAllocs - 1;
523
+
524
+ for (let i = 0; i < NUM_TOP_BINS; i++) {
525
+ this.usedBins[i] = 0;
526
+ }
527
+ for (let i = 0; i < NUM_LEAF_BINS; i++) {
528
+ this.binIndices[i] = NODE_UNUSED;
529
+ }
530
+
531
+ // drop data from the node table
532
+ this.nodes.length = 0;
533
+ this.nodes.setCapacity(maxAllocs);
534
+
535
+ // Freelist is a stack. Nodes in inverse order so that [0] pops first.
536
+ for (let i = 0; i < maxAllocs; i++) {
537
+ this.freeNodes[i] = maxAllocs - i - 1;
538
+ }
539
+
540
+ // Start state: Whole storage as one big node
541
+ // Algorithm will split remainders and push them back as smaller nodes
542
+ this.#insertNodeIntoBin(this.size, 0);
543
+
544
+ }
545
+
546
+ /**
547
+ * Useful for debug and UI
548
+ * @return {StorageReport}
549
+ */
550
+ storageReport() {
551
+ let largestFreeRegion = 0;
552
+ let freeStorage = 0;
553
+
554
+ // Out of allocations? -> Zero free space
555
+ if (this.freeOffset > 0) {
556
+ freeStorage = this.freeStorage;
557
+
558
+ if (this.usedBinsTop !== 0) {
559
+ const topBinIndex = msb_32(this.usedBinsTop);
560
+ const leafBinIndex = msb_32(this.usedBins[topBinIndex]);
561
+
562
+ largestFreeRegion = floatToUint((topBinIndex << TOP_BINS_INDEX_SHIFT) | leafBinIndex);
563
+
564
+ assert.greaterThanOrEqual(freeStorage, largestFreeRegion);
565
+ }
566
+
567
+ }
568
+
569
+ const report = new StorageReport();
570
+
571
+ report.largestFreeRegion = largestFreeRegion;
572
+ report.totalFreeSpace = freeStorage;
573
+
574
+ return report;
575
+ }
576
+ }