@daneren2005/shared-memory-objects 0.0.0 → 0.0.2

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.
@@ -0,0 +1,666 @@
1
+ import type { Pow2 } from './interfaces/pow2';
2
+ import type { TypedArray } from './interfaces/typed-array';
3
+ import { lock, unlock } from './lock/simple-lock';
4
+ import { typedArray } from './utils/typedarray';
5
+
6
+ const STATE_FREE = 0;
7
+ const STATE_USED = 1;
8
+ const STATE_TOP = 2;
9
+ const STATE_END = 3;
10
+ const STATE_ALIGN = 4;
11
+ const STATE_FLAGS = 5;
12
+ const STATE_MIN_SPLIT = 6;
13
+
14
+ const MASK_COMPACT = 1;
15
+ const MASK_SPLIT = 2;
16
+
17
+ const SIZEOF_STATE = 8 * 4;
18
+
19
+ const MEM_BLOCK_SIZE = 0;
20
+ const MEM_BLOCK_NEXT = 1;
21
+
22
+ const SIZEOF_MEM_BLOCK = 2 * 4;
23
+
24
+ // Copied from https://github.com/thi-ng/umbrella/blob/develop/packages/malloc/src/pool.ts
25
+ // Changes: Atomic.load/store on state, simple lock on malloc/free
26
+ // Added bytesFor/lengthOf to be able to grab expected length of a block
27
+ export default class MemoryBuffer {
28
+ buf: ArrayBufferLike;
29
+
30
+ protected readonly start: number;
31
+ protected u8: Uint8Array;
32
+ protected u32: Uint32Array;
33
+ protected state: Uint32Array;
34
+ protected lock: Int32Array;
35
+
36
+ constructor(opts: Partial<MemoryBufferConfig> = {}) {
37
+ this.buf = opts.buf ? opts.buf : new ArrayBuffer(opts.size || 0x1000);
38
+ this.start = opts.start != null ? align(Math.max(opts.start, 0), 4) : 0;
39
+ this.u8 = new Uint8Array(this.buf);
40
+ this.u32 = new Uint32Array(this.buf);
41
+ this.state = new Uint32Array(this.buf, this.start, SIZEOF_STATE / 4);
42
+ this.lock = new Int32Array(this.buf, this.start + this.state.byteLength - 4, 1);
43
+
44
+ if(!opts.skipInitialization) {
45
+ const _align = opts.align || 8;
46
+ if(_align < 8) {
47
+ throw new Error(`invalid alignment: ${_align}, must be a pow2 and >= 8`);
48
+ }
49
+ const top = this.initialTop(_align);
50
+ const resolvedEnd =
51
+ opts.end != null
52
+ ? Math.min(opts.end, this.buf.byteLength)
53
+ : this.buf.byteLength;
54
+
55
+ if(top >= resolvedEnd) {
56
+ throw new Error(
57
+ `insufficient address range (0x${this.start.toString(
58
+ 16
59
+ )} - 0x${resolvedEnd.toString(16)})`
60
+ );
61
+ }
62
+
63
+ this.align = _align;
64
+ this.doCompact = opts.compact !== false;
65
+ this.doSplit = opts.split !== false;
66
+ this.minSplit = opts.minSplit || 16;
67
+ this.end = resolvedEnd;
68
+ this.top = top;
69
+ this._free = 0;
70
+ this._used = 0;
71
+ }
72
+ }
73
+
74
+ stats(): Readonly<MemoryBufferStats> {
75
+ const listStats = (block: number) => {
76
+ let count = 0;
77
+ let size = 0;
78
+ while(block) {
79
+ count++;
80
+ size += this.blockSize(block);
81
+ block = this.blockNext(block);
82
+
83
+ if(block > this.end) {
84
+ console.error(`Trying to get stats for block past end of buffer: ${block} > ${this.end}`);
85
+ break;
86
+ }
87
+ }
88
+ return { count, size };
89
+ };
90
+ const free = listStats(this._free);
91
+ return {
92
+ free,
93
+ used: listStats(this._used),
94
+ top: this.top,
95
+ available: this.end - this.top + free.size,
96
+ total: this.buf.byteLength
97
+ };
98
+ }
99
+
100
+ callocAs<T extends Type>(type: T, num: number, fill = 0) {
101
+ const block = this.mallocAs(type, num);
102
+ block && block.fill(fill);
103
+ return block;
104
+ }
105
+
106
+ mallocAs<T extends Type>(type: T, num: number) {
107
+ const addr = this.malloc(num * SIZEOF[type]);
108
+ return addr ? typedArray(type, this.buf, addr, num) : undefined;
109
+ }
110
+
111
+ calloc(bytes: number, fill = 0) {
112
+ const addr = this.malloc(bytes);
113
+ addr && this.u8.fill(fill, addr, addr + bytes);
114
+ return addr;
115
+ }
116
+
117
+ malloc(bytes: number) {
118
+ if(bytes <= 0) {
119
+ return 0;
120
+ }
121
+ lock(this.lock);
122
+ const paddedSize = align(bytes + SIZEOF_MEM_BLOCK, this.align);
123
+ const end = this.end;
124
+ let top = this.top;
125
+ let block = this._free;
126
+ let prev = 0;
127
+ while(block) {
128
+ const blockSize = this.blockSize(block);
129
+ const isTop = block + blockSize >= top;
130
+ if(isTop || blockSize >= paddedSize) {
131
+ let result = this.mallocTop(
132
+ block,
133
+ prev,
134
+ blockSize,
135
+ paddedSize,
136
+ isTop
137
+ );
138
+
139
+ unlock(this.lock);
140
+ return result;
141
+ }
142
+ prev = block;
143
+ block = this.blockNext(block);
144
+ }
145
+ block = top;
146
+ top = block + paddedSize;
147
+ if(top <= end) {
148
+ this.initBlock(block, paddedSize, this._used);
149
+ this._used = block;
150
+ this.top = top;
151
+ let result = blockDataAddress(block);
152
+ unlock(this.lock);
153
+
154
+ return result;
155
+ }
156
+ unlock(this.lock);
157
+ return 0;
158
+ }
159
+
160
+ private mallocTop(
161
+ block: number,
162
+ prev: number,
163
+ blockSize: number,
164
+ paddedSize: number,
165
+ isTop: boolean
166
+ ) {
167
+ if(isTop && block + paddedSize > this.end) return 0;
168
+ if(prev) {
169
+ this.unlinkBlock(prev, block);
170
+ } else {
171
+ this._free = this.blockNext(block);
172
+ }
173
+ this.setBlockNext(block, this._used);
174
+ this._used = block;
175
+ if(isTop) {
176
+ this.top = block + this.setBlockSize(block, paddedSize);
177
+ } else if(this.doSplit) {
178
+ const excess = blockSize - paddedSize;
179
+ excess >= this.minSplit &&
180
+ this.splitBlock(block, paddedSize, excess);
181
+ }
182
+ return blockDataAddress(block);
183
+ }
184
+
185
+ realloc(ptr: number, bytes: number) {
186
+ if(bytes <= 0) {
187
+ return 0;
188
+ }
189
+ const oldAddr = blockSelfAddress(ptr);
190
+ let newAddr = 0;
191
+ let block = this._used;
192
+ let blockEnd = 0;
193
+ while(block) {
194
+ if(block === oldAddr) {
195
+ [newAddr, blockEnd] = this.reallocBlock(block, bytes);
196
+ break;
197
+ }
198
+ block = this.blockNext(block);
199
+ }
200
+ // copy old block contents to new addr
201
+ if(newAddr && newAddr !== oldAddr) {
202
+ this.u8.copyWithin(
203
+ blockDataAddress(newAddr),
204
+ blockDataAddress(oldAddr),
205
+ blockEnd
206
+ );
207
+ }
208
+ return blockDataAddress(newAddr);
209
+ }
210
+
211
+ private reallocBlock(block: number, bytes: number) {
212
+ const blockSize = this.blockSize(block);
213
+ const blockEnd = block + blockSize;
214
+ const isTop = blockEnd >= this.top;
215
+ const paddedSize = align(bytes + SIZEOF_MEM_BLOCK, this.align);
216
+ // shrink & possibly split existing block
217
+ if(paddedSize <= blockSize) {
218
+ if(this.doSplit) {
219
+ const excess = blockSize - paddedSize;
220
+ if(excess >= this.minSplit) {
221
+ this.splitBlock(block, paddedSize, excess);
222
+ } else if(isTop) {
223
+ this.top = block + paddedSize;
224
+ }
225
+ } else if(isTop) {
226
+ this.top = block + paddedSize;
227
+ }
228
+ return [block, blockEnd];
229
+ }
230
+ // try to enlarge block if current top
231
+ if(isTop && block + paddedSize < this.end) {
232
+ this.top = block + this.setBlockSize(block, paddedSize);
233
+ return [block, blockEnd];
234
+ }
235
+ // fallback to free & malloc
236
+ this.free(block);
237
+ return [blockSelfAddress(this.malloc(bytes)), blockEnd];
238
+ }
239
+
240
+ reallocArray<T extends TypedArray>(array: T, num: number): T | undefined {
241
+ if(array.buffer !== this.buf) {
242
+ return;
243
+ }
244
+ const addr = this.realloc(
245
+ array.byteOffset,
246
+ num * array.BYTES_PER_ELEMENT
247
+ );
248
+ return addr
249
+ ? new (<any>array.constructor)(this.buf, addr, num)
250
+ : undefined;
251
+ }
252
+
253
+ bytesFor(ptrOrArray: number | TypedArray): number | undefined {
254
+ let addr: number;
255
+ if(typeof ptrOrArray !== 'number') {
256
+ if(ptrOrArray.buffer !== this.buf) {
257
+ return undefined;
258
+ }
259
+ addr = ptrOrArray.byteOffset;
260
+ } else {
261
+ addr = ptrOrArray;
262
+ }
263
+
264
+ addr = blockSelfAddress(addr);
265
+ let block = this._used;
266
+ while(block) {
267
+ if(block === addr) {
268
+ return this.blockSize(addr);
269
+ }
270
+ block = this.blockNext(block);
271
+ }
272
+
273
+ return undefined;
274
+ }
275
+ lengthOf(ptrOrArray: number | TypedArray): number | undefined {
276
+ let bytes = this.bytesFor(ptrOrArray);
277
+ if(bytes) {
278
+ return bytes / this.u32.BYTES_PER_ELEMENT;
279
+ } else {
280
+ return undefined;
281
+ }
282
+ }
283
+
284
+ free(ptrOrArray: number | TypedArray) {
285
+ let addr: number;
286
+ if(typeof ptrOrArray !== 'number') {
287
+ if(ptrOrArray.buffer !== this.buf) {
288
+ return false;
289
+ }
290
+ addr = ptrOrArray.byteOffset;
291
+ } else {
292
+ addr = ptrOrArray;
293
+ }
294
+ lock(this.lock);
295
+ addr = blockSelfAddress(addr);
296
+ let block = this._used;
297
+ let prev = 0;
298
+ while(block) {
299
+ if(block === addr) {
300
+ if(prev) {
301
+ this.unlinkBlock(prev, block);
302
+ } else {
303
+ this._used = this.blockNext(block);
304
+ }
305
+ this.insert(block);
306
+ this.doCompact && this.compact();
307
+
308
+ unlock(this.lock);
309
+ return true;
310
+ }
311
+ prev = block;
312
+ block = this.blockNext(block);
313
+ }
314
+
315
+ unlock(this.lock);
316
+ return false;
317
+ }
318
+
319
+ freeAll() {
320
+ this._free = 0;
321
+ this._used = 0;
322
+ this.top = this.initialTop();
323
+ }
324
+
325
+ release() {
326
+ delete (<any> this).u8;
327
+ delete (<any> this).u32;
328
+ delete (<any> this).state;
329
+ delete (<any> this).buf;
330
+ return true;
331
+ }
332
+
333
+ protected get align() {
334
+ return <Pow2> this.state[STATE_ALIGN];
335
+ }
336
+
337
+ protected set align(x: Pow2) {
338
+ this.state[STATE_ALIGN] = x;
339
+ }
340
+
341
+ protected get end() {
342
+ return this.state[STATE_END];
343
+ }
344
+
345
+ protected set end(x: number) {
346
+ this.state[STATE_END] = x;
347
+ }
348
+
349
+ protected get top() {
350
+ return Atomics.load(this.state, STATE_TOP);
351
+ }
352
+
353
+ protected set top(x: number) {
354
+ Atomics.store(this.state, STATE_TOP, x);
355
+ }
356
+
357
+ protected get _free() {
358
+ return Atomics.load(this.state, STATE_FREE);
359
+ }
360
+
361
+ protected set _free(block: number) {
362
+ Atomics.store(this.state, STATE_FREE, block);
363
+ }
364
+
365
+ protected get _used() {
366
+ return Atomics.load(this.state, STATE_USED);
367
+ }
368
+
369
+ protected set _used(block: number) {
370
+ Atomics.store(this.state, STATE_USED, block);
371
+ }
372
+
373
+ protected get doCompact() {
374
+ return !!(this.state[STATE_FLAGS] & MASK_COMPACT);
375
+ }
376
+
377
+ protected set doCompact(flag: boolean) {
378
+ flag
379
+ ? (this.state[STATE_FLAGS] |= 1 << (MASK_COMPACT - 1))
380
+ : (this.state[STATE_FLAGS] &= ~MASK_COMPACT);
381
+ }
382
+
383
+ protected get doSplit() {
384
+ return !!(this.state[STATE_FLAGS] & MASK_SPLIT);
385
+ }
386
+
387
+ protected set doSplit(flag: boolean) {
388
+ flag
389
+ ? (this.state[STATE_FLAGS] |= 1 << (MASK_SPLIT - 1))
390
+ : (this.state[STATE_FLAGS] &= ~MASK_SPLIT);
391
+ }
392
+
393
+ protected get minSplit() {
394
+ return this.state[STATE_MIN_SPLIT];
395
+ }
396
+
397
+ protected set minSplit(x: number) {
398
+ if(x <= SIZEOF_MEM_BLOCK) {
399
+ throw new Error(`illegal min split threshold: ${x}, require at least ${
400
+ SIZEOF_MEM_BLOCK + 1
401
+ }`);
402
+ }
403
+ this.state[STATE_MIN_SPLIT] = x;
404
+ }
405
+
406
+ protected blockSize(block: number) {
407
+ return Atomics.load(this.u32, (block >> 2) + MEM_BLOCK_SIZE);
408
+ }
409
+
410
+ /**
411
+ * Sets & returns given block size.
412
+ *
413
+ * @param block -
414
+ * @param size -
415
+ */
416
+ protected setBlockSize(block: number, size: number) {
417
+ Atomics.store(this.u32, (block >> 2) + MEM_BLOCK_SIZE, size);
418
+ return size;
419
+ }
420
+
421
+ protected blockNext(block: number) {
422
+ return Atomics.load(this.u32, (block >> 2) + MEM_BLOCK_NEXT);
423
+ }
424
+
425
+ /**
426
+ * Sets block next pointer to `next`. Use zero to indicate list end.
427
+ *
428
+ * @param block -
429
+ */
430
+ protected setBlockNext(block: number, next: number) {
431
+ Atomics.store(this.u32, (block >> 2) + MEM_BLOCK_NEXT, next);
432
+ }
433
+
434
+ /**
435
+ * Initializes block header with given `size` and `next` pointer. Returns `block`.
436
+ *
437
+ * @param block -
438
+ * @param size -
439
+ * @param next -
440
+ */
441
+ protected initBlock(block: number, size: number, next: number) {
442
+ const idx = block >>> 2;
443
+ Atomics.store(this.u32, idx + MEM_BLOCK_SIZE, size);
444
+ Atomics.store(this.u32, idx + MEM_BLOCK_NEXT, next);
445
+ return block;
446
+ }
447
+
448
+ protected unlinkBlock(prev: number, block: number) {
449
+ this.setBlockNext(prev, this.blockNext(block));
450
+ }
451
+
452
+ protected splitBlock(block: number, blockSize: number, excess: number) {
453
+ this.insert(
454
+ this.initBlock(
455
+ block + this.setBlockSize(block, blockSize),
456
+ excess,
457
+ 0
458
+ )
459
+ );
460
+ this.doCompact && this.compact();
461
+ }
462
+
463
+ protected initialTop(_align = this.align) {
464
+ return (
465
+ align(this.start + SIZEOF_STATE + SIZEOF_MEM_BLOCK, _align) -
466
+ SIZEOF_MEM_BLOCK
467
+ );
468
+ }
469
+
470
+ /**
471
+ * Traverses free list and attempts to recursively merge blocks
472
+ * occupying consecutive memory regions. Returns true if any blocks
473
+ * have been merged. Only called if `compact` option is enabled.
474
+ */
475
+ protected compact() {
476
+ let block = this._free;
477
+ let prev = 0;
478
+ let scan = 0;
479
+ let scanPrev: number;
480
+ let res = false;
481
+ while(block) {
482
+ scanPrev = block;
483
+ scan = this.blockNext(block);
484
+ while(scan && scanPrev + this.blockSize(scanPrev) === scan) {
485
+ // console.log("merge:", scan.addr, scan.size);
486
+ scanPrev = scan;
487
+ scan = this.blockNext(scan);
488
+ }
489
+ if(scanPrev !== block) {
490
+ const newSize = scanPrev - block + this.blockSize(scanPrev);
491
+ // console.log("merged size:", newSize);
492
+ this.setBlockSize(block, newSize);
493
+ const next = this.blockNext(scanPrev);
494
+ let tmp = this.blockNext(block);
495
+ while(tmp && tmp !== next) {
496
+ // console.log("release:", tmp.addr);
497
+ const tn = this.blockNext(tmp);
498
+ this.setBlockNext(tmp, 0);
499
+ tmp = tn;
500
+ }
501
+ this.setBlockNext(block, next);
502
+ res = true;
503
+ }
504
+ // re-adjust top if poss
505
+ if(block + this.blockSize(block) >= this.top) {
506
+ this.top = block;
507
+ prev
508
+ ? this.unlinkBlock(prev, block)
509
+ : (this._free = this.blockNext(block));
510
+ }
511
+ prev = block;
512
+ block = this.blockNext(block);
513
+ }
514
+ return res;
515
+ }
516
+
517
+ /**
518
+ * Inserts given block into list of free blocks, sorted by address.
519
+ *
520
+ * @param block -
521
+ */
522
+ protected insert(block: number) {
523
+ let ptr = this._free;
524
+ let prev = 0;
525
+ while(ptr) {
526
+ if(block <= ptr) break;
527
+ prev = ptr;
528
+ ptr = this.blockNext(ptr);
529
+ }
530
+ if(prev) {
531
+ this.setBlockNext(prev, block);
532
+ } else {
533
+ this._free = block;
534
+ }
535
+ this.setBlockNext(block, ptr);
536
+ }
537
+ }
538
+
539
+ /**
540
+ * Returns a block's data address, based on given alignment.
541
+ *
542
+ * @param blockAddress -
543
+ */
544
+ const blockDataAddress = (blockAddress: number) => blockAddress > 0 ? blockAddress + SIZEOF_MEM_BLOCK : 0;
545
+
546
+ /**
547
+ * Returns block start address for given data address and alignment.
548
+ *
549
+ * @param dataAddress -
550
+ */
551
+ const blockSelfAddress = (dataAddress: number) => dataAddress > 0 ? dataAddress - SIZEOF_MEM_BLOCK : 0;
552
+
553
+ const align = (addr: number, size: number) => (size--, addr + size & ~size);
554
+ const SIZEOF = {
555
+ u8: 1,
556
+ u8c: 1,
557
+ i8: 1,
558
+ u16: 2,
559
+ i16: 2,
560
+ u32: 4,
561
+ i32: 4,
562
+ i64: 8,
563
+ u64: 8,
564
+ f32: 4,
565
+ f64: 8
566
+ };
567
+ type Type = 'u8' | 'u8c' | 'i8' | 'u16' | 'i16' | 'u32' | 'i32' | 'f32' | 'f64';
568
+
569
+ interface MemoryBufferConfig {
570
+ /**
571
+ * Backing ArrayBuffer (or SharedArrayBuffer). If not given, a new
572
+ * one will be created with given `size`.
573
+ */
574
+ buf: ArrayBufferLike;
575
+ /**
576
+ * Byte size for newly created ArrayBuffers (if `buf` is not given).
577
+ *
578
+ * @defaultValue 0x1000 (4KB)
579
+ */
580
+ size: number;
581
+ /**
582
+ * Anchor index (byte address) inside the array buffer. The MemPool
583
+ * stores its internal state from the given address and heap space
584
+ * starts at least 32 bytes later (depending on chosen `align`
585
+ * value). Unlike allocator state variables, `start`` cannot be
586
+ * saved inside the array buffer itself. If the ArrayBuffer is
587
+ * passed to other consumers they must use the same start value.
588
+ * MUST be multiple of 4.
589
+ *
590
+ * @defaultValue 0
591
+ */
592
+ start: number;
593
+ /**
594
+ * Byte address (+1) of the end of the memory region managed by the
595
+ * {@link MemPool}.
596
+ *
597
+ * @defaultValue end of the backing ArrayBuffer
598
+ */
599
+ end: number;
600
+ /**
601
+ * Number of bytes to align memory blocks to. MUST be a power of 2
602
+ * and >= 8. Use 16 if the pool is being used for allocating memory
603
+ * used in SIMD operations.
604
+ *
605
+ * @defaultValue 8
606
+ */
607
+ align: Pow2;
608
+ /**
609
+ * Flag to configure memory block compaction. If true,
610
+ * adjoining free blocks (in terms of address space) will be merged
611
+ * to minimize fragementation.
612
+ *
613
+ * @defaultValue true
614
+ */
615
+ compact: boolean;
616
+ /**
617
+ * Flag to configure memory block splitting. If true, and when the
618
+ * allocator is re-using a previously freed block larger than the
619
+ * requested size, the block will be split to minimize wasted/unused
620
+ * memory. The splitting behavior can further customized via the
621
+ * `minSplit` option.
622
+ *
623
+ * @defaultValue true
624
+ */
625
+ split: boolean;
626
+ /**
627
+ * Only used if `split` behavior is enabled. Defines min number of
628
+ * excess bytes available in a block for memory block splitting to
629
+ * occur.
630
+ *
631
+ * @defaultValue 16, MUST be > 8
632
+ */
633
+ minSplit: number;
634
+ /**
635
+ * Only needed when sharing the underlying ArrayBuffer. If true, the
636
+ * {@link MemPool} constructor will NOT initialize its internal state and
637
+ * assume the underlying ArrayBuffer has already been initialized by
638
+ * another {@link MemPool} instance. If this option is used, `buf` MUST be
639
+ * given.
640
+ *
641
+ * @defaultValue false
642
+ */
643
+ skipInitialization: boolean;
644
+ }
645
+ interface MemoryBufferStats {
646
+ /**
647
+ * Free block stats.
648
+ */
649
+ free: { count: number; size: number };
650
+ /**
651
+ * Used block stats.
652
+ */
653
+ used: { count: number; size: number };
654
+ /**
655
+ * Current top address.
656
+ */
657
+ top: number;
658
+ /**
659
+ * Bytes available
660
+ */
661
+ available: number;
662
+ /**
663
+ * Total pool size.
664
+ */
665
+ total: number;
666
+ }