@woosh/meep-engine 2.121.13 → 2.122.0
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 +1 -1
- package/src/core/binary/allocator/OffsetAllocator.d.ts +113 -0
- package/src/core/binary/allocator/OffsetAllocator.d.ts.map +1 -0
- package/src/core/binary/allocator/OffsetAllocator.js +576 -0
- package/src/core/binary/float/SmallFloat.d.ts +20 -0
- package/src/core/binary/float/SmallFloat.d.ts.map +1 -0
- package/src/core/binary/float/SmallFloat.js +84 -0
- package/src/core/collection/table/RowFirstTable.d.ts +29 -50
- package/src/core/collection/table/RowFirstTable.d.ts.map +1 -1
- package/src/core/collection/table/RowFirstTable.js +107 -121
package/package.json
CHANGED
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bin sizes follow floating point (exponent + mantissa) distribution (piecewise linear log approx)
|
|
3
|
+
* This ensures that for each size class, the average overhead percentage stays the same
|
|
4
|
+
* @param {number} size
|
|
5
|
+
* @returns {number} uint32
|
|
6
|
+
*/
|
|
7
|
+
export function uintToFloatRoundUp(size: number): number;
|
|
8
|
+
/**
|
|
9
|
+
*
|
|
10
|
+
* @param {number} size
|
|
11
|
+
* @return {number}
|
|
12
|
+
*/
|
|
13
|
+
export function uintToFloatRoundDown(size: number): number;
|
|
14
|
+
/**
|
|
15
|
+
*
|
|
16
|
+
* @param {number} floatValue
|
|
17
|
+
* @return {number}
|
|
18
|
+
*/
|
|
19
|
+
export function floatToUint(floatValue: number): number;
|
|
20
|
+
//# sourceMappingURL=SmallFloat.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SmallFloat.d.ts","sourceRoot":"","sources":["../../../../../src/core/binary/float/SmallFloat.js"],"names":[],"mappings":"AASA;;;;;GAKG;AACH,yCAHW,MAAM,GACJ,MAAM,CA0BlB;AAED;;;;GAIG;AACH,2CAHW,MAAM,GACL,MAAM,CAoBjB;AAED;;;;GAIG;AACH,wCAHW,MAAM,GACL,MAAM,CAcjB"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { assert } from "../../assert.js";
|
|
2
|
+
import { msb_32 } from "../msb_32.js";
|
|
3
|
+
|
|
4
|
+
// TODO this has some overlap with `frexp` function behavior, perhaps we can generalize
|
|
5
|
+
|
|
6
|
+
const MANTISSA_BITS = 3;
|
|
7
|
+
const MANTISSA_VALUE = 1 << MANTISSA_BITS;
|
|
8
|
+
const MANTISSA_MASK = MANTISSA_VALUE - 1;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Bin sizes follow floating point (exponent + mantissa) distribution (piecewise linear log approx)
|
|
12
|
+
* This ensures that for each size class, the average overhead percentage stays the same
|
|
13
|
+
* @param {number} size
|
|
14
|
+
* @returns {number} uint32
|
|
15
|
+
*/
|
|
16
|
+
export function uintToFloatRoundUp(size) {
|
|
17
|
+
let exp = 0;
|
|
18
|
+
let mantissa = 0;
|
|
19
|
+
|
|
20
|
+
if (size < MANTISSA_VALUE) {
|
|
21
|
+
// Denorm: 0..(MANTISSA_VALUE-1)
|
|
22
|
+
mantissa = size;
|
|
23
|
+
} else {
|
|
24
|
+
// Normalized: Hidden high bit always 1. Not stored. Just like float.
|
|
25
|
+
const highestSetBit = msb_32(size);
|
|
26
|
+
|
|
27
|
+
const mantissaStartBit = highestSetBit - MANTISSA_BITS;
|
|
28
|
+
exp = mantissaStartBit + 1;
|
|
29
|
+
mantissa = (size >>> mantissaStartBit) & MANTISSA_MASK;
|
|
30
|
+
|
|
31
|
+
const lowBitsMask = (1 << mantissaStartBit) - 1;
|
|
32
|
+
|
|
33
|
+
// Round up!
|
|
34
|
+
if ((size & lowBitsMask) !== 0) {
|
|
35
|
+
mantissa++;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (exp << MANTISSA_BITS) + mantissa; // + allows mantissa->exp overflow for round up
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
*
|
|
44
|
+
* @param {number} size
|
|
45
|
+
* @return {number}
|
|
46
|
+
*/
|
|
47
|
+
export function uintToFloatRoundDown(size) {
|
|
48
|
+
let exp = 0;
|
|
49
|
+
let mantissa = 0;
|
|
50
|
+
|
|
51
|
+
if (size < MANTISSA_VALUE) {
|
|
52
|
+
// Denorm: 0..(MANTISSA_VALUE-1)
|
|
53
|
+
mantissa = size;
|
|
54
|
+
} else {
|
|
55
|
+
// Normalized: Hidden high bit always 1. Not stored. Just like float.
|
|
56
|
+
const highestSetBit = msb_32(size);
|
|
57
|
+
|
|
58
|
+
const mantissaStartBit = highestSetBit - MANTISSA_BITS;
|
|
59
|
+
|
|
60
|
+
exp = mantissaStartBit + 1;
|
|
61
|
+
mantissa = (size >>> mantissaStartBit) & MANTISSA_MASK;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (exp << MANTISSA_BITS) | mantissa;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
*
|
|
69
|
+
* @param {number} floatValue
|
|
70
|
+
* @return {number}
|
|
71
|
+
*/
|
|
72
|
+
export function floatToUint(floatValue) {
|
|
73
|
+
assert.isNonNegativeInteger(floatValue, 'floatValue');
|
|
74
|
+
|
|
75
|
+
let exponent = floatValue >>> MANTISSA_BITS;
|
|
76
|
+
let mantissa = floatValue & MANTISSA_MASK;
|
|
77
|
+
if (exponent === 0) {
|
|
78
|
+
// Denorms
|
|
79
|
+
return mantissa;
|
|
80
|
+
} else {
|
|
81
|
+
const x = (mantissa | MANTISSA_VALUE) << (exponent - 1);
|
|
82
|
+
return x >>> 0; // convert to unsigned in case of sign overflow
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Compact binary table storage
|
|
3
|
-
* Very efficient in terms of memory usage and allocation
|
|
4
|
-
*
|
|
2
|
+
* Compact binary table storage.
|
|
3
|
+
* Very efficient in terms of memory usage and allocation.
|
|
4
|
+
* Read and write speeds are optimized through code generation.
|
|
5
|
+
* You can think of it as an SQL table but only for numeric types.
|
|
6
|
+
*
|
|
7
|
+
* @copyright Company Named Limited (c) 2025
|
|
8
|
+
* @author Alex Goldring
|
|
5
9
|
*/
|
|
6
10
|
export class RowFirstTable {
|
|
7
11
|
/**
|
|
@@ -44,26 +48,8 @@ export class RowFirstTable {
|
|
|
44
48
|
added: Signal;
|
|
45
49
|
};
|
|
46
50
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*/
|
|
50
|
-
get types(): BinaryDataType[];
|
|
51
|
-
/**
|
|
52
|
-
* @deprecated access {@link RowFirstTableSpec#bytesPerRecord} directly instead
|
|
53
|
-
* @return {number}
|
|
54
|
-
*/
|
|
55
|
-
get bytesPerRecord(): number;
|
|
56
|
-
/**
|
|
57
|
-
* @deprecated use {@link RowFirstTable.spec.readRowMethod} instead
|
|
58
|
-
* @return {function(DataView, number, number[]): void}
|
|
59
|
-
*/
|
|
60
|
-
get readRowMethod(): (arg0: DataView, arg1: number, arg2: number[]) => void;
|
|
61
|
-
/**
|
|
62
|
-
@deprecated use {@link RowFirstTable.spec.writeRowMethod} instead
|
|
63
|
-
* @return {function(DataView, number, number[]): void}
|
|
64
|
-
*/
|
|
65
|
-
get writeRowMethod(): (arg0: DataView, arg1: number, arg2: number[]) => void;
|
|
66
|
-
/**
|
|
51
|
+
* Useful for deserialization.
|
|
52
|
+
* This is an unsafe method, avoid using it if you are not sure.
|
|
67
53
|
* NOTE: capacity is set automatically
|
|
68
54
|
* NOTE: length is not set automatically, you have to do that yourself
|
|
69
55
|
* @param {ArrayBuffer|SharedArrayBuffer} buffer
|
|
@@ -130,7 +116,7 @@ export class RowFirstTable {
|
|
|
130
116
|
*/
|
|
131
117
|
addRow(values: Array<number>): number;
|
|
132
118
|
/**
|
|
133
|
-
*
|
|
119
|
+
* @deprecated Use {@link addRow} and {@link setCapacity} instead
|
|
134
120
|
* @param {number} count number of rows to be added
|
|
135
121
|
* @param {function(row_index:number, row:Array.<number>):*} valueSupplier supplier of row values, called with row index and an empty row to be filled
|
|
136
122
|
*/
|
|
@@ -141,17 +127,6 @@ export class RowFirstTable {
|
|
|
141
127
|
* @param {number} target
|
|
142
128
|
*/
|
|
143
129
|
copyRow(source: number, target: number): void;
|
|
144
|
-
/**
|
|
145
|
-
* Copy data from another table. Specs must match.
|
|
146
|
-
* NOTE: does not send onAdded signal
|
|
147
|
-
* @param {RowFirstTable} other
|
|
148
|
-
*/
|
|
149
|
-
copy(other: RowFirstTable): void;
|
|
150
|
-
/**
|
|
151
|
-
* @param {RowFirstTable} other
|
|
152
|
-
* @returns {boolean}
|
|
153
|
-
*/
|
|
154
|
-
equals(other: RowFirstTable): boolean;
|
|
155
130
|
/**
|
|
156
131
|
* Read a single row of values from the table
|
|
157
132
|
* @param {number} index
|
|
@@ -165,6 +140,14 @@ export class RowFirstTable {
|
|
|
165
140
|
* @param {number[]} record
|
|
166
141
|
*/
|
|
167
142
|
writeRow(index: number, record: number[]): void;
|
|
143
|
+
/**
|
|
144
|
+
* Sets memory region of the row to 0s.
|
|
145
|
+
* Useful for initializing dirty rows for re-use.
|
|
146
|
+
* If you have a specific value in mind - use {@link writeRow} method instead
|
|
147
|
+
* NOTE: All numeric types will produce a 0 after this, so every cell of this row will be 0
|
|
148
|
+
* @param {number} index
|
|
149
|
+
*/
|
|
150
|
+
clearRow(index: number): void;
|
|
168
151
|
/**
|
|
169
152
|
* Reverse order of rows, row-0 will end up at and previously last row will become the first row etc.
|
|
170
153
|
*/
|
|
@@ -174,31 +157,27 @@ export class RowFirstTable {
|
|
|
174
157
|
*/
|
|
175
158
|
clear(): void;
|
|
176
159
|
/**
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*/
|
|
180
|
-
getNumColumns(): number;
|
|
181
|
-
/**
|
|
182
|
-
*
|
|
160
|
+
* Utility method. Returns table contents as a 2d array of [row][cell] form.
|
|
161
|
+
* Primarily useful for data conversion and debugging.
|
|
183
162
|
* @returns {number[][]}
|
|
184
163
|
*/
|
|
185
164
|
toRowArray(): number[][];
|
|
186
165
|
/**
|
|
187
|
-
* Print the table to console
|
|
166
|
+
* Print the table to console.
|
|
167
|
+
* Useful for debugging.
|
|
188
168
|
*/
|
|
189
169
|
printToConsole(): void;
|
|
190
170
|
/**
|
|
191
|
-
*
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* @deprecated use {@link RowFirstTable#readRow} instead
|
|
171
|
+
* Copy data from another table. Specs must match.
|
|
172
|
+
* NOTE: does not dispatch {@link onAdded} signal
|
|
173
|
+
* @param {RowFirstTable} other
|
|
196
174
|
*/
|
|
197
|
-
|
|
175
|
+
copy(other: RowFirstTable): void;
|
|
198
176
|
/**
|
|
199
|
-
@
|
|
177
|
+
* @param {RowFirstTable} other
|
|
178
|
+
* @returns {boolean}
|
|
200
179
|
*/
|
|
201
|
-
|
|
180
|
+
equals(other: RowFirstTable): boolean;
|
|
202
181
|
}
|
|
203
182
|
import Signal from "../../events/signal/Signal.js";
|
|
204
183
|
//# sourceMappingURL=RowFirstTable.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RowFirstTable.d.ts","sourceRoot":"","sources":["../../../../../src/core/collection/table/RowFirstTable.js"],"names":[],"mappings":"AA+BA
|
|
1
|
+
{"version":3,"file":"RowFirstTable.d.ts","sourceRoot":"","sources":["../../../../../src/core/collection/table/RowFirstTable.js"],"names":[],"mappings":"AA+BA;;;;;;;;GAQG;AACH;IACI;;;;;OAKG;IACH,kBAJW,iBAAiB,iBACjB,OAAO,EAsDjB;IAzCG;;;OAGG;IACH,MAFU,iBAAiB,CAEX;IAEhB;;;OAGG;IACH,MAFU,WAAW,CAE4D;IAEjF;;;OAGG;IACH,QAFU,MAAM,CAED;IAEf;;;OAGG;IACH,UAFU,MAAM,CAEgB;IAEhC;;;OAGG;IACH,UAFU,QAAQ,CAEqB;IAEvC;;;OAGG;IACH,IAFU;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,CAOxB;IAKL;;;;;;OAMG;IACH,yBAFW,WAAW,GAAC,iBAAiB,EAQvC;IAED;;;OAGG;IACH,QAFa,MAAM,CA+BlB;IAED;;;OAGG;IACH,sBAFW,MAAM,QAgDhB;IAED;;OAEG;IACH,aAEC;IAED;;;OAGG;IACH,kBAFW,MAAM,QAsBhB;IAED;;;;;OAKG;IACH,0BAJW,MAAM,gBACN,MAAM,SACN,MAAM,QAmBhB;IAED;;;;;OAKG;IACH,yBAJW,MAAM,gBACN,MAAM,GACJ,MAAM,CAmBlB;IAED;;;;OAIG;IACH,kBAHW,MAAM,aACN,MAAM,QAyBhB;IAED;;;;;;OAMG;IACH,kBAHW,MAAM,aACN,MAAM,QAwBhB;IAED;;;;;OAKG;IACH,kBAFY,MAAM,CAYjB;IAED;;;;OAIG;IACH,eAHW,KAAK,CAAE,MAAM,CAAC,GACZ,MAAM,CAYlB;IAED;;;;OAIG;IACH,eAHW,MAAM,4BA8ChB;IAED;;;;OAIG;IACH,gBAHW,MAAM,UACN,MAAM,QAwBhB;IAGD;;;;;OAKG;IACH,eAJW,MAAM,WACN,MAAM,EAAE,GACN,MAAM,EAAE,CAUpB;IAED;;;;OAIG;IACH,gBAHW,MAAM,UACN,MAAM,EAAE,QAUlB;IAED;;;;;;OAMG;IACH,gBAFW,MAAM,QAgBhB;IAED;;OAEG;IACH,qBA4BC;IAED;;OAEG;IACH,cAIC;IAED;;;;OAIG;IACH,cAFa,MAAM,EAAE,EAAE,CAetB;IAED;;;OAGG;IACH,uBAIC;IAED;;;;OAIG;IACH,YAFW,aAAa,QAmBvB;IAED;;;OAGG;IACH,cAHW,aAAa,GACX,OAAO,CAwCnB;CACJ;mBA7oBkB,+BAA+B"}
|
|
@@ -30,9 +30,13 @@ const ALLOCATION_GROW_MINIMUM_STEP = 32;
|
|
|
30
30
|
const ALLOCATION_SHRINK_FACTOR = 0.5;
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
* Compact binary table storage
|
|
34
|
-
* Very efficient in terms of memory usage and allocation
|
|
35
|
-
*
|
|
33
|
+
* Compact binary table storage.
|
|
34
|
+
* Very efficient in terms of memory usage and allocation.
|
|
35
|
+
* Read and write speeds are optimized through code generation.
|
|
36
|
+
* You can think of it as an SQL table but only for numeric types.
|
|
37
|
+
*
|
|
38
|
+
* @copyright Company Named Limited (c) 2025
|
|
39
|
+
* @author Alex Goldring
|
|
36
40
|
*/
|
|
37
41
|
export class RowFirstTable {
|
|
38
42
|
/**
|
|
@@ -41,11 +45,16 @@ export class RowFirstTable {
|
|
|
41
45
|
* @param {boolean} [shared_array] should we use SharedArrayBuffer instead of ArrayBuffer?
|
|
42
46
|
* @constructor
|
|
43
47
|
*/
|
|
44
|
-
constructor(
|
|
48
|
+
constructor(
|
|
49
|
+
spec,
|
|
50
|
+
shared_array = false
|
|
51
|
+
) {
|
|
45
52
|
assert.defined(spec, 'spec');
|
|
46
53
|
assert.notNull(spec, 'spec');
|
|
47
54
|
assert.equal(spec.isRowFirstTableSpec, true, 'spec.isRowFirstTableSpec !== true');
|
|
48
55
|
|
|
56
|
+
assert.isBoolean(shared_array, 'shared_array');
|
|
57
|
+
|
|
49
58
|
/**
|
|
50
59
|
*
|
|
51
60
|
* @type {RowFirstTableSpec}
|
|
@@ -89,40 +98,10 @@ export class RowFirstTable {
|
|
|
89
98
|
|
|
90
99
|
}
|
|
91
100
|
|
|
92
|
-
/**
|
|
93
|
-
* @deprecated access directly via spec instead
|
|
94
|
-
* @return {BinaryDataType[]}
|
|
95
|
-
*/
|
|
96
|
-
get types() {
|
|
97
|
-
return this.spec.types;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* @deprecated access {@link RowFirstTableSpec#bytesPerRecord} directly instead
|
|
102
|
-
* @return {number}
|
|
103
|
-
*/
|
|
104
|
-
get bytesPerRecord() {
|
|
105
|
-
return this.spec.bytesPerRecord;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* @deprecated use {@link RowFirstTable.spec.readRowMethod} instead
|
|
110
|
-
* @return {function(DataView, number, number[]): void}
|
|
111
|
-
*/
|
|
112
|
-
get readRowMethod() {
|
|
113
|
-
return this.spec.readRowMethod;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
@deprecated use {@link RowFirstTable.spec.writeRowMethod} instead
|
|
118
|
-
* @return {function(DataView, number, number[]): void}
|
|
119
|
-
*/
|
|
120
|
-
get writeRowMethod() {
|
|
121
|
-
return this.spec.writeRowMethod;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
101
|
|
|
125
102
|
/**
|
|
103
|
+
* Useful for deserialization.
|
|
104
|
+
* This is an unsafe method, avoid using it if you are not sure.
|
|
126
105
|
* NOTE: capacity is set automatically
|
|
127
106
|
* NOTE: length is not set automatically, you have to do that yourself
|
|
128
107
|
* @param {ArrayBuffer|SharedArrayBuffer} buffer
|
|
@@ -402,7 +381,7 @@ export class RowFirstTable {
|
|
|
402
381
|
}
|
|
403
382
|
|
|
404
383
|
/**
|
|
405
|
-
*
|
|
384
|
+
* @deprecated Use {@link addRow} and {@link setCapacity} instead
|
|
406
385
|
* @param {number} count number of rows to be added
|
|
407
386
|
* @param {function(row_index:number, row:Array.<number>):*} valueSupplier supplier of row values, called with row index and an empty row to be filled
|
|
408
387
|
*/
|
|
@@ -480,68 +459,6 @@ export class RowFirstTable {
|
|
|
480
459
|
}
|
|
481
460
|
}
|
|
482
461
|
|
|
483
|
-
/**
|
|
484
|
-
* Copy data from another table. Specs must match.
|
|
485
|
-
* NOTE: does not send onAdded signal
|
|
486
|
-
* @param {RowFirstTable} other
|
|
487
|
-
*/
|
|
488
|
-
copy(other) {
|
|
489
|
-
|
|
490
|
-
// check that the spec is equivalent
|
|
491
|
-
if (!this.spec.equals(other.spec)) {
|
|
492
|
-
throw new Error('Different table specs');
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
const record_count = other.length;
|
|
496
|
-
|
|
497
|
-
this.resize(record_count);
|
|
498
|
-
|
|
499
|
-
this.length = other.length;
|
|
500
|
-
|
|
501
|
-
// copy data
|
|
502
|
-
const data_byte_count = other.spec.bytesPerRecord * record_count;
|
|
503
|
-
|
|
504
|
-
array_buffer_copy(other.data, 0, this.data, 0, data_byte_count);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
/**
|
|
508
|
-
* @param {RowFirstTable} other
|
|
509
|
-
* @returns {boolean}
|
|
510
|
-
*/
|
|
511
|
-
equals(other) {
|
|
512
|
-
if (other === this) {
|
|
513
|
-
// shortcut
|
|
514
|
-
return true;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
if (other === undefined || other === null) {
|
|
518
|
-
// this should not generally not happen
|
|
519
|
-
return false;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
if (!this.spec.equals(other.spec)) {
|
|
523
|
-
return false;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
const this_length = this.length;
|
|
527
|
-
|
|
528
|
-
if (this_length !== other.length) {
|
|
529
|
-
return false;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
const total_bytes = this_length * this.spec.bytesPerRecord;
|
|
533
|
-
|
|
534
|
-
for (let i = 0; i < total_bytes; i++) {
|
|
535
|
-
const a = this.dataView.getUint8(i);
|
|
536
|
-
const b = other.dataView.getUint8(i);
|
|
537
|
-
|
|
538
|
-
if (a !== b) {
|
|
539
|
-
return false;
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
return true;
|
|
544
|
-
}
|
|
545
462
|
|
|
546
463
|
/**
|
|
547
464
|
* Read a single row of values from the table
|
|
@@ -574,6 +491,29 @@ export class RowFirstTable {
|
|
|
574
491
|
spec.writeRowMethod(this.dataView, spec.bytesPerRecord * index, record);
|
|
575
492
|
}
|
|
576
493
|
|
|
494
|
+
/**
|
|
495
|
+
* Sets memory region of the row to 0s.
|
|
496
|
+
* Useful for initializing dirty rows for re-use.
|
|
497
|
+
* If you have a specific value in mind - use {@link writeRow} method instead
|
|
498
|
+
* NOTE: All numeric types will produce a 0 after this, so every cell of this row will be 0
|
|
499
|
+
* @param {number} index
|
|
500
|
+
*/
|
|
501
|
+
clearRow(index) {
|
|
502
|
+
assert.isNonNegativeInteger(index, 'index');
|
|
503
|
+
|
|
504
|
+
const spec = this.spec;
|
|
505
|
+
|
|
506
|
+
const bytesPerRecord = spec.bytesPerRecord;
|
|
507
|
+
|
|
508
|
+
const address = index * bytesPerRecord;
|
|
509
|
+
|
|
510
|
+
const dataView = this.dataView;
|
|
511
|
+
|
|
512
|
+
for (let i = 0; i < bytesPerRecord; i++) {
|
|
513
|
+
dataView.setUint8(address + i, 0);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
577
517
|
/**
|
|
578
518
|
* Reverse order of rows, row-0 will end up at and previously last row will become the first row etc.
|
|
579
519
|
*/
|
|
@@ -617,15 +557,8 @@ export class RowFirstTable {
|
|
|
617
557
|
}
|
|
618
558
|
|
|
619
559
|
/**
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
*/
|
|
623
|
-
getNumColumns() {
|
|
624
|
-
return this.spec.getColumnCount();
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
/**
|
|
628
|
-
*
|
|
560
|
+
* Utility method. Returns table contents as a 2d array of [row][cell] form.
|
|
561
|
+
* Primarily useful for data conversion and debugging.
|
|
629
562
|
* @returns {number[][]}
|
|
630
563
|
*/
|
|
631
564
|
toRowArray() {
|
|
@@ -644,7 +577,8 @@ export class RowFirstTable {
|
|
|
644
577
|
}
|
|
645
578
|
|
|
646
579
|
/**
|
|
647
|
-
* Print the table to console
|
|
580
|
+
* Print the table to console.
|
|
581
|
+
* Useful for debugging.
|
|
648
582
|
*/
|
|
649
583
|
printToConsole() {
|
|
650
584
|
const rows = this.toRowArray();
|
|
@@ -653,18 +587,70 @@ export class RowFirstTable {
|
|
|
653
587
|
}
|
|
654
588
|
|
|
655
589
|
/**
|
|
656
|
-
*
|
|
590
|
+
* Copy data from another table. Specs must match.
|
|
591
|
+
* NOTE: does not dispatch {@link onAdded} signal
|
|
592
|
+
* @param {RowFirstTable} other
|
|
657
593
|
*/
|
|
658
|
-
|
|
659
|
-
|
|
594
|
+
copy(other) {
|
|
595
|
+
|
|
596
|
+
// check that the spec is equivalent
|
|
597
|
+
if (!this.spec.equals(other.spec)) {
|
|
598
|
+
throw new Error('Different table specs');
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const record_count = other.length;
|
|
602
|
+
|
|
603
|
+
this.resize(record_count);
|
|
604
|
+
|
|
605
|
+
this.length = other.length;
|
|
606
|
+
|
|
607
|
+
// copy data
|
|
608
|
+
const data_byte_count = other.spec.bytesPerRecord * record_count;
|
|
609
|
+
|
|
610
|
+
array_buffer_copy(other.data, 0, this.data, 0, data_byte_count);
|
|
660
611
|
}
|
|
661
|
-
}
|
|
662
612
|
|
|
663
|
-
/**
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
613
|
+
/**
|
|
614
|
+
* @param {RowFirstTable} other
|
|
615
|
+
* @returns {boolean}
|
|
616
|
+
*/
|
|
617
|
+
equals(other) {
|
|
618
|
+
if (other === this) {
|
|
619
|
+
// shortcut
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (other === undefined || other === null) {
|
|
624
|
+
// this should not generally not happen
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (!this.spec.equals(other.spec)) {
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const this_length = this.length;
|
|
633
|
+
|
|
634
|
+
if (this_length !== other.length) {
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const total_bytes = this_length * this.spec.bytesPerRecord;
|
|
639
|
+
|
|
640
|
+
// TODO can use `is_array_buffer_equals` to speed this up
|
|
641
|
+
|
|
642
|
+
const this_data_view = this.dataView;
|
|
643
|
+
const other_data_view = other.dataView;
|
|
644
|
+
|
|
645
|
+
for (let i = 0; i < total_bytes; i++) {
|
|
646
|
+
const a = this_data_view.getUint8(i);
|
|
647
|
+
const b = other_data_view.getUint8(i);
|
|
648
|
+
|
|
649
|
+
if (a !== b) {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return true;
|
|
655
|
+
}
|
|
656
|
+
}
|