@nxtedition/shared 4.0.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -56
- package/lib/index.d.ts +2 -2
- package/lib/index.js +4 -4
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -61,6 +61,15 @@ w.cork(() => {
|
|
|
61
61
|
// All writes flushed atomically when cork returns
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
Or manually:
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
w.cork()
|
|
68
|
+
w.writeSync(buf1.length, writeFn, buf1)
|
|
69
|
+
w.writeSync(buf2.length, writeFn, buf2)
|
|
70
|
+
w.uncork() // publishes all writes to the reader
|
|
71
|
+
```
|
|
72
|
+
|
|
64
73
|
### Non-blocking writes with tryWrite
|
|
65
74
|
|
|
66
75
|
```js
|
|
@@ -92,11 +101,10 @@ const w = new Writer(state)
|
|
|
92
101
|
|
|
93
102
|
```js
|
|
94
103
|
// reader-worker.js
|
|
95
|
-
import {
|
|
104
|
+
import { Reader } from '@nxtedition/shared'
|
|
96
105
|
import { workerData } from 'node:worker_threads'
|
|
97
106
|
|
|
98
|
-
const
|
|
99
|
-
const r = new Reader(state)
|
|
107
|
+
const r = new Reader(workerData)
|
|
100
108
|
|
|
101
109
|
function poll() {
|
|
102
110
|
const count = r.readSome((data) => {
|
|
@@ -116,108 +124,110 @@ Allocates or wraps a shared memory buffer for the ring buffer. The first 128 byt
|
|
|
116
124
|
- **size** — Data capacity in bytes (must be a positive integer, max ~2 GB)
|
|
117
125
|
- **buffer** — An existing `SharedArrayBuffer` to wrap
|
|
118
126
|
|
|
119
|
-
|
|
127
|
+
#### `state.buffer`
|
|
120
128
|
|
|
121
|
-
|
|
129
|
+
The underlying `SharedArrayBuffer`. Pass this to a worker thread to share the ring buffer.
|
|
122
130
|
|
|
123
|
-
|
|
131
|
+
### `new Reader(state: State | SharedArrayBuffer)`
|
|
124
132
|
|
|
125
|
-
|
|
133
|
+
Creates a reader for the ring buffer. Accepts a `State` instance or a `SharedArrayBuffer` directly (shorthand for `new Reader(new State(buf))`).
|
|
134
|
+
|
|
135
|
+
#### `reader.readSome(next, opaque?)`
|
|
136
|
+
|
|
137
|
+
Reads a batch of messages. Calls `next(data, opaque)` for each message, where `data` has:
|
|
126
138
|
|
|
127
139
|
- `buffer: Buffer` — The underlying shared buffer
|
|
128
140
|
- `view: DataView` — A DataView over the shared buffer
|
|
129
141
|
- `byteOffset: number` — Start offset of the message payload
|
|
130
142
|
- `byteLength: number` — Length of the message payload in bytes
|
|
131
143
|
|
|
144
|
+
- **opaque** — Optional user-provided context object passed through to the callback. Useful for avoiding closures on hot paths.
|
|
145
|
+
|
|
132
146
|
Return `false` from the callback to stop reading early. Returns the number of messages processed.
|
|
133
147
|
|
|
134
148
|
Messages are batched: up to 1024 items or 256 KiB per call.
|
|
135
149
|
|
|
136
|
-
### `new Writer(state: State, options?)`
|
|
150
|
+
### `new Writer(state: State | SharedArrayBuffer, options?)`
|
|
137
151
|
|
|
138
|
-
Creates a writer for the ring buffer.
|
|
152
|
+
Creates a writer for the ring buffer. Accepts a `State` instance or a `SharedArrayBuffer` directly (shorthand for `new Writer(new State(buf))`).
|
|
139
153
|
|
|
140
154
|
**Options:**
|
|
141
155
|
|
|
142
156
|
- `yield?: () => void` — Called when the writer must wait for the reader to catch up. Useful to prevent deadlocks when the writer thread also drives the reader.
|
|
143
157
|
- `logger?: { warn(obj, msg): void }` — Logger for yield warnings (pino-compatible).
|
|
144
158
|
|
|
145
|
-
#### `writer.writeSync(len, fn)`
|
|
159
|
+
#### `writer.writeSync(len, fn, opaque?)`
|
|
146
160
|
|
|
147
161
|
Synchronously writes a message. Blocks (via `Atomics.wait`) until buffer space is available.
|
|
148
162
|
|
|
149
163
|
- **len** — Maximum payload size in bytes. Writing beyond `len` bytes in the callback is undefined behavior.
|
|
150
|
-
- **fn(data) → number** — Write callback. Write payload into `data.buffer` starting at `data.byteOffset`. **Must return the end position** (`data.byteOffset + bytesWritten`), not the byte count.
|
|
164
|
+
- **fn(data, opaque) → number** — Write callback. Write payload into `data.buffer` starting at `data.byteOffset`. **Must return the end position** (`data.byteOffset + bytesWritten`), not the byte count.
|
|
165
|
+
- **opaque** — Optional user-provided context object passed through to the callback. Useful for avoiding closures on hot paths.
|
|
151
166
|
|
|
152
167
|
Throws on timeout (default: 60000 ms).
|
|
153
168
|
|
|
154
|
-
#### `writer.tryWrite(len, fn)`
|
|
169
|
+
#### `writer.tryWrite(len, fn, opaque?)`
|
|
170
|
+
|
|
171
|
+
Non-blocking write attempt. Returns `false` if the buffer is full. The `fn` and `opaque` parameters follow the same contract as `writeSync`.
|
|
155
172
|
|
|
156
|
-
|
|
173
|
+
#### `writer.cork(callback?)`
|
|
157
174
|
|
|
158
|
-
|
|
175
|
+
Batches multiple writes. The write pointer is only published to the reader when the cork is released, reducing atomic operation overhead.
|
|
159
176
|
|
|
160
|
-
|
|
177
|
+
When called with a callback, uncork is called automatically when the callback returns. When called without a callback, you must call `uncork()` manually.
|
|
178
|
+
|
|
179
|
+
#### `writer.uncork()`
|
|
180
|
+
|
|
181
|
+
Decrements the cork counter. When it reaches zero, publishes the pending write position to the reader. Safe to call when not corked (no-op).
|
|
182
|
+
|
|
183
|
+
#### `writer.flushSync()`
|
|
184
|
+
|
|
185
|
+
Immediately publishes the pending write position to the reader. Unlike `uncork`, this does not interact with the cork counter — it forces a flush regardless.
|
|
161
186
|
|
|
162
187
|
## Benchmarks
|
|
163
188
|
|
|
164
|
-
Measured on
|
|
189
|
+
Measured on AMD EPYC 9355P (4.28 GHz), Node.js 25.6.0, 8 MiB ring buffer, Docker (x64-linux).
|
|
165
190
|
|
|
166
191
|
Each benchmark writes batches of fixed-size messages from the main thread and
|
|
167
192
|
reads them in a worker thread. The shared ring buffer is compared against
|
|
168
|
-
Node.js `postMessage` (structured clone).
|
|
169
|
-
collected with [`@mitata/counters`](https://github.com/evanwashere/mitata).
|
|
193
|
+
Node.js `postMessage` (structured clone).
|
|
170
194
|
|
|
171
195
|
### Throughput
|
|
172
196
|
|
|
173
197
|
| Size | shared (buffer) | shared (string) | postMessage (buffer) | postMessage (string) |
|
|
174
198
|
| -----: | --------------: | --------------: | -------------------: | -------------------: |
|
|
175
|
-
| 64 B |
|
|
176
|
-
| 256 B | **
|
|
177
|
-
| 1 KiB |
|
|
178
|
-
| 4 KiB |
|
|
179
|
-
| 16 KiB |
|
|
180
|
-
| 64 KiB |
|
|
199
|
+
| 64 B | **901 MiB/s** | 410 MiB/s | 25 MiB/s | 42 MiB/s |
|
|
200
|
+
| 256 B | **2.67 GiB/s** | 896 MiB/s | 88 MiB/s | 158 MiB/s |
|
|
201
|
+
| 1 KiB | **4.88 GiB/s** | 1.26 GiB/s | 328 MiB/s | 498 MiB/s |
|
|
202
|
+
| 4 KiB | **9.22 GiB/s** | 1.50 GiB/s | 1.14 GiB/s | 1.70 GiB/s |
|
|
203
|
+
| 16 KiB | **10.90 GiB/s** | 1.56 GiB/s | 4.29 GiB/s | 6.27 GiB/s |
|
|
204
|
+
| 64 KiB | 13.03 GiB/s | 1.55 GiB/s | 10.10 GiB/s | **15.18 GiB/s** |
|
|
181
205
|
|
|
182
206
|
### Message rate
|
|
183
207
|
|
|
184
208
|
| Size | shared (buffer) | shared (string) | postMessage (buffer) | postMessage (string) |
|
|
185
209
|
| -----: | --------------: | --------------: | -------------------: | -------------------: |
|
|
186
|
-
| 64 B | **
|
|
187
|
-
| 256 B | **
|
|
188
|
-
| 1 KiB |
|
|
189
|
-
| 4 KiB |
|
|
190
|
-
| 16 KiB |
|
|
191
|
-
| 64 KiB |
|
|
192
|
-
|
|
193
|
-
### CPU efficiency (instructions per cycle)
|
|
194
|
-
|
|
195
|
-
| Size | shared (buffer) | shared (string) | postMessage (buffer) | postMessage (string) |
|
|
196
|
-
| -----: | --------------: | --------------: | -------------------: | -------------------: |
|
|
197
|
-
| 64 B | 5.45 | 5.62 | 3.89 | 3.35 |
|
|
198
|
-
| 256 B | 4.52 | 5.76 | 3.82 | 3.18 |
|
|
199
|
-
| 1 KiB | 4.18 | **6.25** | 3.65 | 3.16 |
|
|
200
|
-
| 4 KiB | 3.79 | **6.65** | 3.51 | 2.92 |
|
|
201
|
-
| 16 KiB | 3.73 | **6.12** | 2.91 | 2.63 |
|
|
202
|
-
| 64 KiB | 3.93 | **4.30** | 2.44 | 2.92 |
|
|
210
|
+
| 64 B | **14.76 M/s** | 6.72 M/s | 405 K/s | 688 K/s |
|
|
211
|
+
| 256 B | **11.20 M/s** | 3.67 M/s | 360 K/s | 648 K/s |
|
|
212
|
+
| 1 KiB | **5.12 M/s** | 1.32 M/s | 336 K/s | 510 K/s |
|
|
213
|
+
| 4 KiB | **2.42 M/s** | 394 K/s | 298 K/s | 445 K/s |
|
|
214
|
+
| 16 KiB | **714 K/s** | 102 K/s | 281 K/s | 411 K/s |
|
|
215
|
+
| 64 KiB | 213 K/s | 25 K/s | 165 K/s | **249 K/s** |
|
|
203
216
|
|
|
204
217
|
### Key findings
|
|
205
218
|
|
|
206
|
-
- **Small messages (64
|
|
207
|
-
|
|
208
|
-
Per-message overhead dominates at
|
|
209
|
-
|
|
210
|
-
27.5 M msg/s at 64 B with 5.45 IPC.
|
|
219
|
+
- **Small messages (64–256 B):** The shared ring buffer with `Buffer.set` delivers
|
|
220
|
+
**14.8–11.2 M msg/s** — up to **36x faster** than `postMessage` (buffer) and
|
|
221
|
+
**21x faster** than `postMessage` (string). Per-message overhead dominates at
|
|
222
|
+
these sizes, and avoiding structured cloning makes the biggest difference.
|
|
211
223
|
|
|
212
|
-
- **
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
(6-7 IPC on Apple M3 Pro), which explains why string writes outperform raw
|
|
216
|
-
`Buffer.copy` at larger sizes.
|
|
224
|
+
- **Medium to large messages (1–16 KiB):** `Buffer.set` via the ring buffer
|
|
225
|
+
maintains its lead, reaching **10.9 GiB/s** at 16 KiB — **1.7–5.4x faster**
|
|
226
|
+
than the best `postMessage` variant.
|
|
217
227
|
|
|
218
|
-
- **
|
|
219
|
-
|
|
220
|
-
|
|
228
|
+
- **Very large messages (64 KiB):** `postMessage` (string) overtakes the shared
|
|
229
|
+
buffer at **15.2 GiB/s** vs **13.0 GiB/s**. At this size, structured cloning
|
|
230
|
+
overhead is amortized and the kernel's optimized `memcpy` dominates.
|
|
221
231
|
|
|
222
232
|
- **Caveat:** The string benchmark uses ASCII-only content. Multi-byte UTF-8
|
|
223
233
|
strings will not hit V8's vectorized fast path and will be significantly slower.
|
|
@@ -225,8 +235,7 @@ collected with [`@mitata/counters`](https://github.com/evanwashere/mitata).
|
|
|
225
235
|
### Running the benchmark
|
|
226
236
|
|
|
227
237
|
```sh
|
|
228
|
-
|
|
229
|
-
sudo node --allow-natives-syntax packages/shared/src/bench.mjs
|
|
238
|
+
node --allow-natives-syntax packages/shared/src/bench.mjs
|
|
230
239
|
```
|
|
231
240
|
|
|
232
241
|
## License
|
package/lib/index.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export declare class State {
|
|
|
25
25
|
*/
|
|
26
26
|
export declare class Reader {
|
|
27
27
|
#private;
|
|
28
|
-
constructor(state: State);
|
|
28
|
+
constructor(state: State | SharedArrayBuffer);
|
|
29
29
|
readSome<U>(next: (data: BufferRegion, opaque?: U) => void | boolean, opaque?: U): number;
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
@@ -33,7 +33,7 @@ export declare class Reader {
|
|
|
33
33
|
*/
|
|
34
34
|
export declare class Writer {
|
|
35
35
|
#private;
|
|
36
|
-
constructor(state: State, { yield: onYield, logger }?: WriterOptions);
|
|
36
|
+
constructor(state: State | SharedArrayBuffer, { yield: onYield, logger }?: WriterOptions);
|
|
37
37
|
/**
|
|
38
38
|
* Synchronously writes a message. Blocks (via `Atomics.wait`) until buffer space is available.
|
|
39
39
|
* Writing more than "len" bytes in the callback will cause undefined behavior.
|
package/lib/index.js
CHANGED
|
@@ -74,8 +74,8 @@ export class Reader {
|
|
|
74
74
|
#data
|
|
75
75
|
#readPos
|
|
76
76
|
|
|
77
|
-
constructor(state
|
|
78
|
-
const sharedBuffer = state.buffer
|
|
77
|
+
constructor(state ) {
|
|
78
|
+
const sharedBuffer = state instanceof SharedArrayBuffer ? state : state.buffer
|
|
79
79
|
const size = sharedBuffer.byteLength - STATE_BYTES
|
|
80
80
|
|
|
81
81
|
this.#state = new Int32Array(sharedBuffer, 0, STATE_BYTES >> 2)
|
|
@@ -183,8 +183,8 @@ export class Writer {
|
|
|
183
183
|
#logger
|
|
184
184
|
#uncorkBound
|
|
185
185
|
|
|
186
|
-
constructor(state
|
|
187
|
-
const sharedBuffer = state.buffer
|
|
186
|
+
constructor(state , { yield: onYield, logger } = {}) {
|
|
187
|
+
const sharedBuffer = state instanceof SharedArrayBuffer ? state : state.buffer
|
|
188
188
|
|
|
189
189
|
if (onYield != null && typeof onYield !== 'function') {
|
|
190
190
|
throw new TypeError('onYield must be a function')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxtedition/shared",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -26,6 +26,5 @@
|
|
|
26
26
|
"oxlint-tsgolint": "^0.13.0",
|
|
27
27
|
"rimraf": "^6.1.3",
|
|
28
28
|
"typescript": "^5.9.3"
|
|
29
|
-
}
|
|
30
|
-
"gitHead": "18d5bdc8855ad9012882b03cab144a9625b8c4b8"
|
|
29
|
+
}
|
|
31
30
|
}
|