bun-shared-memory 0.1.5 → 0.1.7
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 +100 -0
- package/README.md +41 -28
- package/package.json +1 -1
- package/src/growable-buffer.ts +441 -45
- package/src/memory.ts +306 -166
- package/src/refcount.ts +19 -4
- package/src/shared.ts +5 -5
- package/src/worker.ts +60 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,9 +5,99 @@ 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.1.7] - 2026-11-02
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Keep the Index Table in a single growable `SharedArrayBuffer` (capped at
|
|
13
|
+
4 GiB) instead of the chunked logic, so it stays contiguous and cheap to
|
|
14
|
+
address. `SharedMemoryOptions.indexTableMaxByteLength` configures its cap
|
|
15
|
+
(default 4 GiB).
|
|
16
|
+
- `maxTotalBytes` now caps the combined physical size of DataTable,
|
|
17
|
+
StringTable, and ArrayTable only (the Index Table is excluded). The budget
|
|
18
|
+
is enforced by an atomic `SharedBudget` counter shared across workers that
|
|
19
|
+
reserves memory before each chunk growth, so the three tables cannot exceed
|
|
20
|
+
the limit even when multiple workers write concurrently.
|
|
21
|
+
- Enforce size limits regardless of the configured values: `chunkSize` is at
|
|
22
|
+
least 4 MiB, `maxTotalBytes`/`maxByteLength` are at least 32 MiB, and
|
|
23
|
+
`chunkSize` is raised as needed so no table needs more than 1024 chunks.
|
|
24
|
+
`SharedMemory.maxByteLength` and `SharedMemory.maxTotalBytes` are now public
|
|
25
|
+
read-only fields.
|
|
26
|
+
|
|
27
|
+
### Performance
|
|
28
|
+
|
|
29
|
+
- Reclaim memory from inactive chunks after defragmentation. A
|
|
30
|
+
`SharedArrayBuffer` cannot be shrunk, so `GrowableBuffer` replaces each
|
|
31
|
+
inactive chunk with a fresh 4-byte `SharedArrayBuffer`, releasing the old
|
|
32
|
+
full-size buffer so its memory is freed. `defragment()` returns the new
|
|
33
|
+
chunk lists and the main thread broadcasts them to workers via a
|
|
34
|
+
`__chunkUpdate` message; workers apply them with `applyChunkUpdate()`. This
|
|
35
|
+
only runs during defragmentation (rare), so the hot path is unaffected.
|
|
36
|
+
- Optimize the budget write hot path: `GrowableBuffer` tracks
|
|
37
|
+
`nextChunkToActivate` (the first not-yet-active chunk) so `ensureCapacity`
|
|
38
|
+
only activates new chunks instead of re-scanning every already-active chunk
|
|
39
|
+
on each write (was O(n) per write as the dataset grew). Writes ~9% faster.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- Stabilize the real-world benchmark against CPU contention. The shared-memory
|
|
44
|
+
methodology finishes in tens of milliseconds, so it is sensitive to load
|
|
45
|
+
from other processes (IDE, language servers). Each worker now runs several
|
|
46
|
+
measured samples (default 5) and reports its fastest; each methodology is
|
|
47
|
+
run several times (default 3) and the fastest run is kept. Because
|
|
48
|
+
contention can only slow a run down, the minimum is the best estimate of
|
|
49
|
+
true throughput.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- Resolve chunk offsets with an unsigned right shift (`offset >>> chunkShift`)
|
|
54
|
+
instead of a signed one. A signed 32-bit shift turns offsets at or above
|
|
55
|
+
2 GiB negative, so reads/writes past 2 GiB (e.g. in a >4 GiB chunked buffer)
|
|
56
|
+
hit the wrong chunk or threw. The Index Table and the Data/String/Array
|
|
57
|
+
tables can now be addressed at any offset below the 4 GiB single-SAB cap.
|
|
58
|
+
Reported by [mizulu](https://github.com/mizulu).
|
|
59
|
+
|
|
60
|
+
## [0.1.6] - 2026-11-02
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
|
|
64
|
+
- Support shared memory larger than 4 GiB. The single-`SharedArrayBuffer`
|
|
65
|
+
per-buffer architecture is replaced with a chunked `GrowableBuffer`: each
|
|
66
|
+
buffer is backed by multiple equal-size chunks (32 MiB default, power of
|
|
67
|
+
two), so a buffer can exceed the 4 GiB single-SAB limit. Offsets resolve
|
|
68
|
+
O(1) via bit shifts (`offset >> chunkShift`).
|
|
69
|
+
- `SharedMemoryOptions.chunkSize` to configure the chunk size (the minimum
|
|
70
|
+
growth step).
|
|
71
|
+
- Chunks are pre-allocated at construction (4 bytes each, with
|
|
72
|
+
`maxByteLength = chunkSize`) and activated on first access via
|
|
73
|
+
`SharedArrayBuffer.grow()`. Because `grow()` is visible across all workers
|
|
74
|
+
sharing a buffer, a worker can grow a chunk and every other worker sees it
|
|
75
|
+
immediately — no postMessage broadcast is needed for chunk activation.
|
|
76
|
+
|
|
77
|
+
### Changed
|
|
78
|
+
|
|
79
|
+
- `exportBuffers()`/`fromBuffers()` now pass chunk lists instead of a single
|
|
80
|
+
buffer; `fromBuffers` reuses the shared `SharedArrayBuffer`s directly (no
|
|
81
|
+
copy) so writes stay visible across workers. The chunk size is derived from
|
|
82
|
+
each buffer's `maxByteLength` (preserved across `postMessage`), not its
|
|
83
|
+
current 4-byte length.
|
|
84
|
+
- Remove the 4 GiB cap on the per-buffer maximum in the `SharedMemory`
|
|
85
|
+
constructor (the chunked `GrowableBuffer` already bypasses the single-SAB
|
|
86
|
+
limit).
|
|
87
|
+
|
|
88
|
+
### Performance
|
|
89
|
+
|
|
90
|
+
- After defragmentation, unused chunks are marked inactive:
|
|
91
|
+
`GrowableBuffer.reset()` deactivates chunks beyond the compacted region so
|
|
92
|
+
the GC can reclaim their storage.
|
|
93
|
+
- The chunked accessor adds an indirect `chunks[index].view` lookup on the
|
|
94
|
+
write hot path (the price of >4 GiB support): writes ~-15%, array writes
|
|
95
|
+
~-20%. Reads are unchanged (lock-free cache path).
|
|
96
|
+
|
|
8
97
|
## [0.1.5] - 2026-11-02
|
|
9
98
|
|
|
10
99
|
### Changed
|
|
100
|
+
|
|
11
101
|
- Replace the `Proxy` used for object `SharedRef`s with hard-baked
|
|
12
102
|
`Object.defineProperty` accessor getters/setters, so property reads skip the
|
|
13
103
|
Proxy trap on the hot path (~3× faster object property reads). Methods
|
|
@@ -24,6 +114,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
24
114
|
## [0.1.4] - 2026-11-01
|
|
25
115
|
|
|
26
116
|
### Changed
|
|
117
|
+
|
|
27
118
|
- Declare `"sideEffects": false` in `package.json` so bundlers can tree-shake
|
|
28
119
|
the library (the modules have no side effects on import).
|
|
29
120
|
- Remove the `typescript` peer dependency. The library ships as TypeScript
|
|
@@ -33,6 +124,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
33
124
|
## [0.1.3] - 2026-11-01
|
|
34
125
|
|
|
35
126
|
### Added
|
|
127
|
+
|
|
36
128
|
- Real-world benchmark (`bench/real-world/`) comparing shared memory against
|
|
37
129
|
live SQLite, MySQL, and Redis via an Elysia mini API.
|
|
38
130
|
- Redis as a fourth benchmark methodology (in Docker).
|
|
@@ -41,6 +133,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
41
133
|
- Real-world benchmark results and methodology documented in the README.
|
|
42
134
|
|
|
43
135
|
### Changed
|
|
136
|
+
|
|
44
137
|
- MySQL/Redis connections are established before the benchmark (not per
|
|
45
138
|
request) so connection setup is not measured as overhead.
|
|
46
139
|
- The benchmark always runs both TCP and Unix-socket transports; the socket
|
|
@@ -49,16 +142,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
49
142
|
## [0.1.2] - 2026-11-01
|
|
50
143
|
|
|
51
144
|
### Changed
|
|
145
|
+
|
|
52
146
|
- Unref the maintenance timer so the process can exit naturally.
|
|
53
147
|
- Document `init()` as the first call and how to configure memory via `init()`.
|
|
54
148
|
|
|
55
149
|
## [0.1.1] - 2026-11-01
|
|
56
150
|
|
|
57
151
|
### Added
|
|
152
|
+
|
|
58
153
|
- Repository metadata and npm publishing preparation.
|
|
59
154
|
- README with package name and install instructions.
|
|
60
155
|
|
|
61
156
|
### Changed
|
|
157
|
+
|
|
62
158
|
- Optimize the read hot path: direct cache on the proxy target.
|
|
63
159
|
- Optimize `Mutex`: skip notify when no worker is waiting.
|
|
64
160
|
- Start the refcount coordinator/tracker automatically in `init()`.
|
|
@@ -67,6 +163,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
67
163
|
## [0.1.0] - 2026-11-01
|
|
68
164
|
|
|
69
165
|
### Added
|
|
166
|
+
|
|
70
167
|
- Initial release: `Shared<T>` Proxy-based API over a 4-buffer shared memory
|
|
71
168
|
architecture (Index Table, DataTable, append-only StringTable, ArrayTable).
|
|
72
169
|
- Atomics-based synchronization (`Mutex`, `Notifier`).
|
|
@@ -75,6 +172,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
75
172
|
- O(1) in-place `push`/`pop` with capacity-based array growth.
|
|
76
173
|
- Benchmarks for all data types vs native.
|
|
77
174
|
|
|
175
|
+
[0.1.7]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.6...v0.1.7
|
|
176
|
+
[0.1.6]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.5...v0.1.6
|
|
177
|
+
[0.1.5]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.4...v0.1.5
|
|
78
178
|
[0.1.4]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.3...v0.1.4
|
|
79
179
|
[0.1.3]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.2...v0.1.3
|
|
80
180
|
[0.1.2]: https://github.com/Kapsonfire/bun-shared-memory/compare/v0.1.1...v0.1.2
|
package/README.md
CHANGED
|
@@ -37,20 +37,21 @@ is benchmarked sequentially with 4 workers. See
|
|
|
37
37
|
[`bench/real-world/README.md`](bench/real-world/README.md) for setup.
|
|
38
38
|
|
|
39
39
|
Measured on Linux (Bun 1.4.4, 100,000 requests per methodology, sorted by
|
|
40
|
-
speed; MySQL/Redis benchmarked over both TCP and Unix socket
|
|
40
|
+
speed; MySQL/Redis benchmarked over both TCP and Unix socket; each methodology
|
|
41
|
+
is the minimum over 3 runs × 5 samples to filter CPU-contention noise):
|
|
41
42
|
|
|
42
43
|
| Methodology | Transport | Requests | Elapsed (ms) | req/s | vs. shared |
|
|
43
44
|
|-------------|-----------|----------|--------------|------------|------------|
|
|
44
|
-
| `shared` | — | 100,000 |
|
|
45
|
-
| `sqlite` | — | 100,000 |
|
|
46
|
-
| `redis` | socket | 100,000 | 1,
|
|
47
|
-
| `mysql` | socket | 100,000 | 1,
|
|
48
|
-
| `redis` | tcp | 100,000 | 2,
|
|
49
|
-
| `mysql` | tcp | 100,000 |
|
|
50
|
-
|
|
51
|
-
Shared memory is the fastest by a wide margin: ~
|
|
52
|
-
read and ~
|
|
53
|
-
significantly faster than TCP for MySQL/Redis (~1.
|
|
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
|
+
|
|
52
|
+
Shared memory is the fastest by a wide margin: ~9.2× faster than a live SQLite
|
|
53
|
+
read and ~61–145× faster than live MySQL/Redis reads. Unix sockets are
|
|
54
|
+
significantly faster than TCP for MySQL/Redis (~1.8×), but both remain far
|
|
54
55
|
slower than shared memory.
|
|
55
56
|
|
|
56
57
|
## ✨ Features
|
|
@@ -305,22 +306,24 @@ A complete runnable example is in [`examples/worker/`](examples/worker/).
|
|
|
305
306
|
|
|
306
307
|
## Memory budget
|
|
307
308
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
memory can be several times that.
|
|
309
|
+
The Index Table (including each key's version counter) uses one growable
|
|
310
|
+
`SharedArrayBuffer`, capped at **4 GiB** by default (configurable via
|
|
311
|
+
`indexTableMaxByteLength`). The DataTable, StringTable, and ArrayTable use
|
|
312
|
+
chunks and can individually exceed 4 GiB. By default each table grows up to
|
|
313
|
+
its own `maxByteLength`, so the _total_ memory can be several times that.
|
|
313
314
|
|
|
314
315
|
Configure the buffers when calling `init()` — pass `initialSize` (each
|
|
315
|
-
|
|
316
|
-
(each
|
|
317
|
-
`maxTotalBytes
|
|
316
|
+
table's starting size, avoiding a grow on first use) and `maxByteLength`
|
|
317
|
+
(each table's cap). To cap the combined physical memory of DataTable,
|
|
318
|
+
StringTable, and ArrayTable, pass `maxTotalBytes`. The Index Table is excluded
|
|
319
|
+
from this budget. Its growth and chunk growth in workers reserve memory from
|
|
320
|
+
the same atomic budget before allocating, so the three tables cannot exceed it.
|
|
318
321
|
|
|
319
322
|
```ts
|
|
320
323
|
import { init } from "bun-shared-memory";
|
|
321
324
|
|
|
322
|
-
//
|
|
323
|
-
//
|
|
325
|
+
// The Index Table grows up to 4 GiB independently. DataTable, StringTable, and
|
|
326
|
+
// ArrayTable together are capped at 4 GiB.
|
|
324
327
|
await init({
|
|
325
328
|
initialSize: 64 * 1024,
|
|
326
329
|
maxByteLength: 4 * 1024 * 1024 * 1024,
|
|
@@ -328,14 +331,24 @@ await init({
|
|
|
328
331
|
});
|
|
329
332
|
```
|
|
330
333
|
|
|
331
|
-
`maxTotalBytes` caps the **sum** of
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
`maxByteLength`.
|
|
334
|
+
`maxTotalBytes` caps the **sum** of the physical sizes of DataTable,
|
|
335
|
+
StringTable, and ArrayTable, including across workers. If a write would exceed
|
|
336
|
+
the budget, a `RangeError` is thrown before the chunk grows. Without
|
|
337
|
+
`maxTotalBytes`, each table simply grows up to its own `maxByteLength`.
|
|
335
338
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
+
### Size limits
|
|
340
|
+
|
|
341
|
+
The following limits are enforced regardless of the configured values:
|
|
342
|
+
|
|
343
|
+
- **`chunkSize` is at least 4 MiB.** A smaller value is raised to 4 MiB.
|
|
344
|
+
- **`maxTotalBytes` / `maxByteLength` are at least 32 MiB.** A smaller value
|
|
345
|
+
is raised to 32 MiB.
|
|
346
|
+
- **No table uses more than 1024 chunks.** `chunkSize` is raised as needed so
|
|
347
|
+
that `ceil(maxByteLength / chunkSize) ≤ 1024` (e.g. an 8 GiB table with a
|
|
348
|
+
4 MiB chunk would need 2048 chunks, so the chunk size is raised to 8 MiB).
|
|
349
|
+
|
|
350
|
+
These keep the chunk count bounded (so the pre-allocated chunk list stays
|
|
351
|
+
small) and avoid pathological tiny chunks.
|
|
339
352
|
|
|
340
353
|
## Array capacity
|
|
341
354
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bun-shared-memory",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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,
|