bun-shared-memory 0.1.7 → 0.2.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/CHANGELOG.md +76 -0
- package/README.md +18 -18
- package/package.json +2 -1
- package/src/growable-buffer.ts +198 -4
- package/src/memory.ts +404 -41
- package/src/refcount.ts +31 -8
- package/src/shared.ts +643 -168
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,80 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.0] - 2026-11-04
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Slab allocator for string values and arrays. Overwriting a value no longer
|
|
13
|
+
leaves the old block as dead space: freed blocks are pushed onto a shared
|
|
14
|
+
free list (one per table, in a `SharedArrayBuffer`) and reused by later
|
|
15
|
+
allocations, so the tables grow far less over time and defragmentation runs
|
|
16
|
+
less often. The free list is shared across workers via
|
|
17
|
+
`exportBuffers()`/`fromBuffers()`.
|
|
18
|
+
- Inline short strings. Strings up to 11 UTF-8 bytes are packed directly into
|
|
19
|
+
the 16-byte value slot (type + length in the top 4 bits of `c`, bytes in
|
|
20
|
+
`a`/`b`/`c`), avoiding a heap allocation and the string-table write on every
|
|
21
|
+
write. Longer strings still use the slab-allocated ArrayTable with in-place
|
|
22
|
+
reuse.
|
|
23
|
+
- A complete multi-worker todo-list example (`examples/todo-list/`) that
|
|
24
|
+
serves both the frontend and the API on a single port via Elysia workers.
|
|
25
|
+
|
|
26
|
+
### Performance
|
|
27
|
+
|
|
28
|
+
- Cache each object property getter's stable version-cell index (resolved
|
|
29
|
+
lazily on first read) and load the version directly via
|
|
30
|
+
`Atomics.load(mem.versionView, cellIndex)`, replacing the per-read
|
|
31
|
+
`index === null` check and the `getVersion` → `versionCellIndex` multiply +
|
|
32
|
+
divide. Object property reads improved a further 3–17%.
|
|
33
|
+
- Cache the top-level array ref and version-cell index on the `SharedRef`
|
|
34
|
+
target so index writes skip the per-write DataTable header reads, Map
|
|
35
|
+
lookups, and the generic encode path. `moveArray` now bumps the layout
|
|
36
|
+
version so the cached ref is invalidated on array growth. Closes the
|
|
37
|
+
remaining array-write gap: number[]/boolean[]/string[] index writes are now
|
|
38
|
+
at or above native speed.
|
|
39
|
+
- Cache the array proxy for an object's array property on the accessor getter
|
|
40
|
+
so it isn't recreated on every write (a write bumps the version and
|
|
41
|
+
invalidates the field cache), and cache the property slot offset + array ref
|
|
42
|
+
in the array proxy so index writes skip the per-write `getObjectLayout` Map
|
|
43
|
+
lookup, `properties.get`, and DataTable header reads. Object array-prop
|
|
44
|
+
index writes dropped from ~254× to ~33× slower than native.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- Raise the defragmentation trigger: a table now triggers a defrag pass when
|
|
49
|
+
its used length exceeds 80% of capacity (was: when free space fell below
|
|
50
|
+
10%), and only when at least 64 blocks have been freed. Because writes are
|
|
51
|
+
lock-free bump allocations, the used length grows monotonically even when
|
|
52
|
+
the live data set stays the same size; the new threshold defragments only
|
|
53
|
+
when the buffer is genuinely fragmented. During a defrag pass, lock-free
|
|
54
|
+
writes fall back to the mutex and in-flight writes are awaited before
|
|
55
|
+
compacting.
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
- Fix slab-allocator block corruption in `free()`. `alloc()` padded blocks to
|
|
60
|
+
`MIN_BLOCK_SIZE` internally but callers advanced the cursor by the unpadded
|
|
61
|
+
size, so blocks overlapped and `free()`'s metadata overwrote the neighbour
|
|
62
|
+
block. `alloc()` now returns the actual reserved (padded) size and all
|
|
63
|
+
callers advance by it. Free-list metadata was also reduced from 12 to 8
|
|
64
|
+
bytes (next pointer + size) and the magic marker dropped (double-frees are
|
|
65
|
+
now detected by walking the free list), lowering `MIN_BLOCK_SIZE` from 12 to
|
|
66
|
+
8 so small string blocks no longer inflate the ArrayTable during
|
|
67
|
+
defragmentation.
|
|
68
|
+
|
|
69
|
+
## [0.1.8] - 2026-11-02
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
|
|
73
|
+
- Share each `GrowableBuffer`'s append cursor across workers. The cursor was a
|
|
74
|
+
private per-instance field, so when a worker reconstructed `SharedMemory`
|
|
75
|
+
via `fromBuffers()` its cursor stayed `0` and the first write in that worker
|
|
76
|
+
wrote at offset `0`, overwriting existing data (e.g. a key string) and
|
|
77
|
+
corrupting the shared value. The cursor now lives in a `SharedArrayBuffer`
|
|
78
|
+
(as a `BigInt64Array`) read/written with `Atomics`, so every worker appends
|
|
79
|
+
at the correct offset. Reported while building the multi-worker todo-list
|
|
80
|
+
example.
|
|
81
|
+
|
|
8
82
|
## [0.1.7] - 2026-11-02
|
|
9
83
|
|
|
10
84
|
### Added
|
|
@@ -172,6 +246,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
172
246
|
- O(1) in-place `push`/`pop` with capacity-based array growth.
|
|
173
247
|
- Benchmarks for all data types vs native.
|
|
174
248
|
|
|
249
|
+
[0.2.0]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.8...v0.2.0
|
|
250
|
+
[0.1.8]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.7...v0.1.8
|
|
175
251
|
[0.1.7]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.6...v0.1.7
|
|
176
252
|
[0.1.6]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.5...v0.1.6
|
|
177
253
|
[0.1.5]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.4...v0.1.5
|
package/README.md
CHANGED
|
@@ -40,14 +40,14 @@ Measured on Linux (Bun 1.4.4, 100,000 requests per methodology, sorted by
|
|
|
40
40
|
speed; MySQL/Redis benchmarked over both TCP and Unix socket; each methodology
|
|
41
41
|
is the minimum over 3 runs × 5 samples to filter CPU-contention noise):
|
|
42
42
|
|
|
43
|
-
| Methodology | Transport | Requests | Elapsed (ms) | req/s
|
|
44
|
-
|
|
45
|
-
| `shared` | — | 100,000 | 21 | 4,876,492
|
|
46
|
-
| `sqlite` | — | 100,000 | 188 | 531,116
|
|
47
|
-
| `redis` | socket | 100,000 | 1,262 | 79,229
|
|
48
|
-
| `mysql` | socket | 100,000 | 1,605 | 62,322
|
|
49
|
-
| `redis` | tcp | 100,000 | 2,817 | 35,497
|
|
50
|
-
| `mysql` | tcp | 100,000 | 2,976 | 33,602
|
|
43
|
+
| Methodology | Transport | Requests | Elapsed (ms) | req/s | vs. shared |
|
|
44
|
+
| ----------- | --------- | -------- | ------------ | --------- | ------------- |
|
|
45
|
+
| `shared` | — | 100,000 | 21 | 4,876,492 | — |
|
|
46
|
+
| `sqlite` | — | 100,000 | 188 | 531,116 | 9.2× slower |
|
|
47
|
+
| `redis` | socket | 100,000 | 1,262 | 79,229 | 61.5× slower |
|
|
48
|
+
| `mysql` | socket | 100,000 | 1,605 | 62,322 | 78.2× slower |
|
|
49
|
+
| `redis` | tcp | 100,000 | 2,817 | 35,497 | 137.4× slower |
|
|
50
|
+
| `mysql` | tcp | 100,000 | 2,976 | 33,602 | 145.1× slower |
|
|
51
51
|
|
|
52
52
|
Shared memory is the fastest by a wide margin: ~9.2× faster than a live SQLite
|
|
53
53
|
read and ~61–145× faster than live MySQL/Redis reads. Unix sockets are
|
|
@@ -74,16 +74,16 @@ See [SPEC.md](./SPEC.md) for the full specification.
|
|
|
74
74
|
|
|
75
75
|
## 📊 Stats
|
|
76
76
|
|
|
77
|
-
|
|
|
78
|
-
|
|
79
|
-
| **Version**
|
|
80
|
-
| **Downloads / month** | [](https://www.npmjs.com/package/bun-shared-memory)
|
|
81
|
-
| **Stars**
|
|
82
|
-
| **Forks**
|
|
83
|
-
| **Open issues**
|
|
84
|
-
| **Open PRs**
|
|
85
|
-
| **License**
|
|
86
|
-
| **Language**
|
|
77
|
+
| | |
|
|
78
|
+
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
79
|
+
| **Version** | [](https://www.npmjs.com/package/bun-shared-memory) |
|
|
80
|
+
| **Downloads / month** | [](https://www.npmjs.com/package/bun-shared-memory) |
|
|
81
|
+
| **Stars** | [](https://github.com/Kapsonfire/bun-shared-memory) |
|
|
82
|
+
| **Forks** | [](https://github.com/Kapsonfire/bun-shared-memory) |
|
|
83
|
+
| **Open issues** | [](https://github.com/Kapsonfire/bun-shared-memory/issues) |
|
|
84
|
+
| **Open PRs** | [](https://github.com/Kapsonfire/bun-shared-memory/pulls) |
|
|
85
|
+
| **License** | [](https://github.com/Kapsonfire/bun-shared-memory/blob/main/LICENSE) |
|
|
86
|
+
| **Language** | [](https://www.typescriptlang.org) |
|
|
87
87
|
|
|
88
88
|
## Data Types
|
|
89
89
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bun-shared-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Share data between Bun workers/threads using SharedArrayBuffer (zero-copy, 4-buffer architecture, Atomics-based synchronization).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/bun": "latest",
|
|
50
|
+
"cofusion": "^0.0.1",
|
|
50
51
|
"elysia": "^1.4.30"
|
|
51
52
|
}
|
|
52
53
|
}
|
package/src/growable-buffer.ts
CHANGED
|
@@ -93,6 +93,16 @@ export function largestPowerOfTwo(n: number): number {
|
|
|
93
93
|
return 2 ** Math.floor(Math.log2(n));
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Minimum size of a slab-allocated block. The free-list metadata written by
|
|
98
|
+
* `free` (next pointer + size = 8 bytes) must fit inside the block, otherwise
|
|
99
|
+
* it would overwrite the following block. All blocks are padded up to at
|
|
100
|
+
* least this size.
|
|
101
|
+
*/
|
|
102
|
+
const MIN_BLOCK_SIZE = 8;
|
|
103
|
+
|
|
104
|
+
export { MIN_BLOCK_SIZE };
|
|
105
|
+
|
|
96
106
|
export class GrowableBuffer {
|
|
97
107
|
private chunks: Chunk[] = [];
|
|
98
108
|
private readonly maxByteLength: number;
|
|
@@ -102,7 +112,24 @@ export class GrowableBuffer {
|
|
|
102
112
|
/** log2(chunkSize), used for O(1) offset resolution via bit shifts. */
|
|
103
113
|
private readonly chunkShift: number;
|
|
104
114
|
private readonly budget?: SharedBudget;
|
|
105
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Shared append cursor (used length). Stored in a SharedArrayBuffer so all
|
|
117
|
+
* workers that reconstruct this buffer from `fromBuffers()` see the same
|
|
118
|
+
* used length and append at the correct offset instead of overwriting
|
|
119
|
+
* existing data at offset 0.
|
|
120
|
+
*/
|
|
121
|
+
private readonly cursor: BigInt64Array;
|
|
122
|
+
/**
|
|
123
|
+
* Shared head of the free list. Stored in a SharedArrayBuffer so all workers
|
|
124
|
+
* that reconstruct this buffer from `fromBuffers()` see the same free list
|
|
125
|
+
* and reuse freed blocks instead of appending at the end.
|
|
126
|
+
*
|
|
127
|
+
* The free list is a singly-linked list of freed blocks. Each freed block
|
|
128
|
+
* stores, in its first 4 bytes, the byte offset of the next free block (or
|
|
129
|
+
* -1 for the end of the list). The head is the offset of the first free
|
|
130
|
+
* block, or -1 when the list is empty.
|
|
131
|
+
*/
|
|
132
|
+
private readonly freeListHead: Int32Array;
|
|
106
133
|
/** Smallest chunk index that is not yet active (see `activate`). */
|
|
107
134
|
private nextChunkToActivate = 0;
|
|
108
135
|
|
|
@@ -113,9 +140,21 @@ export class GrowableBuffer {
|
|
|
113
140
|
chunkSize = 32 * 1024 * 1024, // 32 MiB per chunk
|
|
114
141
|
singleBuffer = false,
|
|
115
142
|
chargeInitial = true,
|
|
143
|
+
cursorBuffer?: SharedArrayBuffer,
|
|
144
|
+
freeListBuffer?: SharedArrayBuffer,
|
|
116
145
|
) {
|
|
117
146
|
this.singleBuffer = singleBuffer;
|
|
118
147
|
this.budget = budget;
|
|
148
|
+
this.cursor = new BigInt64Array(
|
|
149
|
+
cursorBuffer ?? new SharedArrayBuffer(BigInt64Array.BYTES_PER_ELEMENT),
|
|
150
|
+
);
|
|
151
|
+
this.freeListHead = new Int32Array(
|
|
152
|
+
freeListBuffer ?? new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT),
|
|
153
|
+
);
|
|
154
|
+
// Initialize the free list head to -1 (empty) on first construction.
|
|
155
|
+
if (freeListBuffer === undefined) {
|
|
156
|
+
Atomics.store(this.freeListHead, 0, -1);
|
|
157
|
+
}
|
|
119
158
|
if (singleBuffer && typeof initialSizeOrBuffer !== "number") {
|
|
120
159
|
const buffer = Array.isArray(initialSizeOrBuffer)
|
|
121
160
|
? initialSizeOrBuffer[0]
|
|
@@ -267,7 +306,17 @@ export class GrowableBuffer {
|
|
|
267
306
|
|
|
268
307
|
/** Number of bytes written so far (used length, not buffer capacity). */
|
|
269
308
|
get byteLength(): number {
|
|
270
|
-
return this.
|
|
309
|
+
return Number(Atomics.load(this.cursor, 0));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** The shared cursor buffer (for export to workers). */
|
|
313
|
+
get cursorBuffer(): SharedArrayBuffer {
|
|
314
|
+
return this.cursor.buffer as SharedArrayBuffer;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** The shared free-list head buffer (for export to workers). */
|
|
318
|
+
get freeListBuffer(): SharedArrayBuffer {
|
|
319
|
+
return this.freeListHead.buffer as SharedArrayBuffer;
|
|
271
320
|
}
|
|
272
321
|
|
|
273
322
|
/** Total logical capacity across all chunks (active or not). */
|
|
@@ -373,7 +422,149 @@ export class GrowableBuffer {
|
|
|
373
422
|
|
|
374
423
|
/** Mark `n` bytes as used, after writing them at the current offset. */
|
|
375
424
|
advance(n: number): void {
|
|
376
|
-
this.
|
|
425
|
+
Atomics.add(this.cursor, 0, BigInt(n));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Atomically reserve `size` bytes at the end of the buffer and return the
|
|
430
|
+
* start offset. This is the lock-free bump-allocation primitive: the shared
|
|
431
|
+
* cursor is incremented with a single `Atomics.add`, so concurrent workers
|
|
432
|
+
* each get a distinct, non-overlapping range without a mutex. The caller
|
|
433
|
+
* writes its data into `[offset, offset + size)`.
|
|
434
|
+
*
|
|
435
|
+
* Chunk growth is preserved: `ensureCapacity` activates any chunks needed to
|
|
436
|
+
* cover the reserved range. Because `SharedArrayBuffer.grow()` is visible
|
|
437
|
+
* across workers, activation needs no broadcast.
|
|
438
|
+
*
|
|
439
|
+
* Unlike `alloc`, this never consults the free list, so freed blocks are not
|
|
440
|
+
* reused on this path (dead space is reclaimed by defragmentation).
|
|
441
|
+
*/
|
|
442
|
+
reserve(size: number): number {
|
|
443
|
+
// Lock-free bump allocation with chunk-boundary alignment. The shared
|
|
444
|
+
// cursor is advanced with a compare-exchange loop so concurrent workers
|
|
445
|
+
// each get a distinct, non-overlapping range without a mutex. If the
|
|
446
|
+
// block would cross a chunk boundary, the offset is aligned up to the next
|
|
447
|
+
// chunk so the caller's typed writes (setInt32 etc.) never straddle two
|
|
448
|
+
// chunks (those accessors only write within a single chunk).
|
|
449
|
+
while (true) {
|
|
450
|
+
const current = Number(Atomics.load(this.cursor, 0));
|
|
451
|
+
let offset = current;
|
|
452
|
+
if (!this.singleBuffer) {
|
|
453
|
+
const within = current & (this.chunkSize - 1);
|
|
454
|
+
if (within + size > this.chunkSize) {
|
|
455
|
+
offset = current + (this.chunkSize - within);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const next = offset + size;
|
|
459
|
+
if (
|
|
460
|
+
Atomics.compareExchange(
|
|
461
|
+
this.cursor,
|
|
462
|
+
0,
|
|
463
|
+
BigInt(current),
|
|
464
|
+
BigInt(next),
|
|
465
|
+
) === BigInt(current)
|
|
466
|
+
) {
|
|
467
|
+
this.ensureCapacity(next);
|
|
468
|
+
return offset;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Allocate `size` bytes, reusing a freed block from the free list when one
|
|
475
|
+
* of at least `size` bytes is available (first-fit), otherwise appending at
|
|
476
|
+
* the end of the buffer.
|
|
477
|
+
*
|
|
478
|
+
* Returns the byte offset of the allocated block and the actual reserved
|
|
479
|
+
* (padded) size. The caller must write the block's data and then call
|
|
480
|
+
* `advance(reserved)` only when the block was appended (not reused), because
|
|
481
|
+
* a reused block is already counted as used.
|
|
482
|
+
*
|
|
483
|
+
* @returns the offset, the reserved size, and whether the block was freshly
|
|
484
|
+
* appended (true) or reused from the free list (false).
|
|
485
|
+
*/
|
|
486
|
+
alloc(size: number): { offset: number; appended: boolean; size: number } {
|
|
487
|
+
// Pad small blocks up to MIN_BLOCK_SIZE so the free-list metadata written
|
|
488
|
+
// by `free` always fits inside the block and never overwrites a neighbour.
|
|
489
|
+
size = Math.max(size, MIN_BLOCK_SIZE);
|
|
490
|
+
// First-fit search over the free list for a block of at least `size`.
|
|
491
|
+
let prev = -1;
|
|
492
|
+
let cur = Atomics.load(this.freeListHead, 0);
|
|
493
|
+
while (cur !== -1) {
|
|
494
|
+
const next = this.getInt32(cur);
|
|
495
|
+
if (this.blockSize(cur) >= size) {
|
|
496
|
+
// Unlink `cur` from the free list.
|
|
497
|
+
if (prev === -1) {
|
|
498
|
+
Atomics.store(this.freeListHead, 0, next);
|
|
499
|
+
} else {
|
|
500
|
+
this.setInt32(prev, next);
|
|
501
|
+
}
|
|
502
|
+
return { offset: cur, appended: false, size };
|
|
503
|
+
}
|
|
504
|
+
prev = cur;
|
|
505
|
+
cur = next;
|
|
506
|
+
}
|
|
507
|
+
// No reusable block; append at the end.
|
|
508
|
+
const offset = this.byteLength;
|
|
509
|
+
this.ensureCapacity(offset + size);
|
|
510
|
+
return { offset, appended: true, size };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Free a block previously allocated with `alloc`, pushing it onto the free
|
|
515
|
+
* list so it can be reused. Detects double frees: freeing a block that is
|
|
516
|
+
* already on the free list throws an Error.
|
|
517
|
+
*
|
|
518
|
+
* The block's first 8 bytes are overwritten with the free-list metadata
|
|
519
|
+
* (next pointer + size), so the caller must not read the block's data after
|
|
520
|
+
* freeing it.
|
|
521
|
+
*/
|
|
522
|
+
free(offset: number, size: number): void {
|
|
523
|
+
// Pad to MIN_BLOCK_SIZE to match the size `alloc` actually reserved, so
|
|
524
|
+
// `blockSize` reports the real block size for reuse decisions.
|
|
525
|
+
size = Math.max(size, MIN_BLOCK_SIZE);
|
|
526
|
+
// Double-free detection: a block already on the free list must not be
|
|
527
|
+
// freed again. Walk the list (free is not a hot path, so the O(n) scan is
|
|
528
|
+
// acceptable).
|
|
529
|
+
let cur = Atomics.load(this.freeListHead, 0);
|
|
530
|
+
while (cur !== -1) {
|
|
531
|
+
if (cur === offset) {
|
|
532
|
+
throw new Error(
|
|
533
|
+
`Double free detected at offset ${offset} (size ${size})`,
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
cur = this.getInt32(cur);
|
|
537
|
+
}
|
|
538
|
+
const head = Atomics.load(this.freeListHead, 0);
|
|
539
|
+
this.setInt32(offset, head);
|
|
540
|
+
this.setInt32(offset + 4, size);
|
|
541
|
+
Atomics.store(this.freeListHead, 0, offset);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Read the size of a freed block, stored in bytes [4..7] when it was
|
|
546
|
+
* freed. Used by `alloc` to check whether a freed block is large enough.
|
|
547
|
+
*/
|
|
548
|
+
private blockSize(offset: number): number {
|
|
549
|
+
return this.getInt32(offset + 4);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Walk the free list and return the number of freed blocks and the total
|
|
554
|
+
* bytes they occupy. Used to measure fragmentation: many small freed blocks
|
|
555
|
+
* scattered through the buffer indicate high fragmentation, which a
|
|
556
|
+
* defragmentation pass can compact.
|
|
557
|
+
*/
|
|
558
|
+
freeListStats(): { count: number; bytes: number } {
|
|
559
|
+
let count = 0;
|
|
560
|
+
let bytes = 0;
|
|
561
|
+
let cur = Atomics.load(this.freeListHead, 0);
|
|
562
|
+
while (cur !== -1) {
|
|
563
|
+
count++;
|
|
564
|
+
bytes += this.blockSize(cur);
|
|
565
|
+
cur = this.getInt32(cur);
|
|
566
|
+
}
|
|
567
|
+
return { count, bytes };
|
|
377
568
|
}
|
|
378
569
|
|
|
379
570
|
/**
|
|
@@ -383,7 +574,10 @@ export class GrowableBuffer {
|
|
|
383
574
|
* `releaseInactiveChunks()`.
|
|
384
575
|
*/
|
|
385
576
|
reset(n: number): void {
|
|
386
|
-
this.
|
|
577
|
+
Atomics.store(this.cursor, 0, BigInt(n));
|
|
578
|
+
// After compaction the free list's offsets are invalid (blocks moved), so
|
|
579
|
+
// clear it. Freed blocks are re-added lazily on the next `free`.
|
|
580
|
+
Atomics.store(this.freeListHead, 0, -1);
|
|
387
581
|
if (this.singleBuffer) return;
|
|
388
582
|
const neededChunks = Math.ceil(n / this.chunkSize);
|
|
389
583
|
for (let i = 0; i < this.chunks.length; i++) {
|