@nxtedition/shared 4.0.3 → 4.0.5
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 +22 -21
- package/lib/index.d.ts +2 -1
- package/lib/index.js +25 -31
- package/lib/index.test.d.ts +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -183,7 +183,7 @@ Immediately publishes the pending write position to the reader. Unlike `uncork`,
|
|
|
183
183
|
|
|
184
184
|
## Benchmarks
|
|
185
185
|
|
|
186
|
-
Measured on AMD EPYC 9355P (4.
|
|
186
|
+
Measured on AMD EPYC 9355P (4.29 GHz), Node.js 25.6.1, 8 MiB ring buffer, Docker (x64-linux).
|
|
187
187
|
|
|
188
188
|
Each benchmark writes batches of fixed-size messages from the main thread and
|
|
189
189
|
reads them in a worker thread. The shared ring buffer is compared against
|
|
@@ -193,38 +193,39 @@ Node.js `postMessage` (structured clone).
|
|
|
193
193
|
|
|
194
194
|
| Size | shared (buffer) | shared (string) | postMessage (buffer) | postMessage (string) |
|
|
195
195
|
| -----: | --------------: | --------------: | -------------------: | -------------------: |
|
|
196
|
-
| 64 B | **
|
|
197
|
-
| 256 B | **2.
|
|
198
|
-
| 1 KiB | **4.
|
|
199
|
-
| 4 KiB |
|
|
200
|
-
| 16 KiB |
|
|
201
|
-
| 64 KiB |
|
|
196
|
+
| 64 B | **838 MiB/s** | 388 MiB/s | 24 MiB/s | 42 MiB/s |
|
|
197
|
+
| 256 B | **2.65 GiB/s** | 1.46 GiB/s | 89 MiB/s | 168 MiB/s |
|
|
198
|
+
| 1 KiB | **4.95 GiB/s** | 4.86 GiB/s | 339 MiB/s | 525 MiB/s |
|
|
199
|
+
| 4 KiB | 8.42 GiB/s | **15.11 GiB/s** | 1.12 GiB/s | 1.86 GiB/s |
|
|
200
|
+
| 16 KiB | 12.02 GiB/s | **33.27 GiB/s** | 4.12 GiB/s | 6.02 GiB/s |
|
|
201
|
+
| 64 KiB | 12.96 GiB/s | **43.66 GiB/s** | 9.33 GiB/s | 14.73 GiB/s |
|
|
202
202
|
|
|
203
203
|
### Message rate
|
|
204
204
|
|
|
205
205
|
| Size | shared (buffer) | shared (string) | postMessage (buffer) | postMessage (string) |
|
|
206
206
|
| -----: | --------------: | --------------: | -------------------: | -------------------: |
|
|
207
|
-
| 64 B | **
|
|
208
|
-
| 256 B | **11.
|
|
209
|
-
| 1 KiB | **5.
|
|
210
|
-
| 4 KiB |
|
|
211
|
-
| 16 KiB |
|
|
212
|
-
| 64 KiB |
|
|
207
|
+
| 64 B | **13.73 M/s** | 6.35 M/s | 391 K/s | 693 K/s |
|
|
208
|
+
| 256 B | **11.14 M/s** | 6.14 M/s | 366 K/s | 689 K/s |
|
|
209
|
+
| 1 KiB | **5.19 M/s** | 5.09 M/s | 348 K/s | 538 K/s |
|
|
210
|
+
| 4 KiB | 2.21 M/s | **3.96 M/s** | 295 K/s | 488 K/s |
|
|
211
|
+
| 16 KiB | 788 K/s | **2.18 M/s** | 270 K/s | 395 K/s |
|
|
212
|
+
| 64 KiB | 212 K/s | **715 K/s** | 153 K/s | 241 K/s |
|
|
213
213
|
|
|
214
214
|
### Key findings
|
|
215
215
|
|
|
216
216
|
- **Small messages (64–256 B):** The shared ring buffer with `Buffer.set` delivers
|
|
217
|
-
**
|
|
218
|
-
**
|
|
217
|
+
**13.7–11.1 M msg/s** — up to **35x faster** than `postMessage` (buffer) and
|
|
218
|
+
**20x faster** than `postMessage` (string). Per-message overhead dominates at
|
|
219
219
|
these sizes, and avoiding structured cloning makes the biggest difference.
|
|
220
220
|
|
|
221
|
-
- **Medium
|
|
222
|
-
|
|
223
|
-
|
|
221
|
+
- **Medium messages (1 KiB):** `Buffer.set` and string are nearly identical
|
|
222
|
+
(**4.95 vs 4.86 GiB/s**), both **~9x faster** than the best `postMessage`
|
|
223
|
+
variant.
|
|
224
224
|
|
|
225
|
-
- **
|
|
226
|
-
|
|
227
|
-
|
|
225
|
+
- **Large messages (4–64 KiB):** Shared string overtakes `Buffer.set` and
|
|
226
|
+
scales to **43.7 GiB/s** at 64 KiB — **3.4x faster** than `Buffer.set` and
|
|
227
|
+
**3.0x faster** than `postMessage` (string). At every size, the shared ring
|
|
228
|
+
buffer outperforms `postMessage`.
|
|
228
229
|
|
|
229
230
|
- **Caveat:** The string benchmark uses ASCII-only content. Multi-byte UTF-8
|
|
230
231
|
strings will not hit V8's vectorized fast path and will be significantly slower.
|
package/lib/index.d.ts
CHANGED
|
@@ -26,7 +26,8 @@ export declare class SharedStateBuffer extends SharedArrayBuffer {
|
|
|
26
26
|
export declare class Reader {
|
|
27
27
|
#private;
|
|
28
28
|
constructor(sharedBuffer: SharedArrayBuffer);
|
|
29
|
-
readSome
|
|
29
|
+
readSome(next: (data: BufferRegion) => void | boolean): number;
|
|
30
|
+
readSome<U>(next: (data: BufferRegion, opaque: U) => void | boolean, opaque: U): number;
|
|
30
31
|
}
|
|
31
32
|
/**
|
|
32
33
|
* Writer for the ring buffer.
|
package/lib/index.js
CHANGED
|
@@ -16,6 +16,8 @@ const STATE_BYTES = 128
|
|
|
16
16
|
const HWM_BYTES = 256 * 1024 // 256 KiB
|
|
17
17
|
const HWM_COUNT = 1024 // 1024 items
|
|
18
18
|
|
|
19
|
+
const isProduction = process.env.NODE_ENV === 'production'
|
|
20
|
+
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
|
|
@@ -59,7 +61,6 @@ export class Reader {
|
|
|
59
61
|
#size
|
|
60
62
|
#int32
|
|
61
63
|
#data
|
|
62
|
-
#readPos
|
|
63
64
|
|
|
64
65
|
constructor(sharedBuffer ) {
|
|
65
66
|
const size = sharedBuffer.byteLength - STATE_BYTES
|
|
@@ -77,13 +78,11 @@ export class Reader {
|
|
|
77
78
|
byteOffset: 0,
|
|
78
79
|
byteLength: 0,
|
|
79
80
|
}
|
|
80
|
-
|
|
81
|
-
// Local copy of the pointer. The `| 0` is a hint to the V8 JIT
|
|
82
|
-
// compiler that this is a 32-bit integer, enabling optimizations.
|
|
83
|
-
this.#readPos = Atomics.load(this.#state, READ_INDEX) | 0
|
|
84
81
|
}
|
|
85
82
|
|
|
86
|
-
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
readSome (next , opaque ) {
|
|
87
86
|
let count = 0
|
|
88
87
|
let bytes = 0
|
|
89
88
|
|
|
@@ -91,14 +90,9 @@ export class Reader {
|
|
|
91
90
|
const int32 = this.#int32
|
|
92
91
|
const size = this.#size
|
|
93
92
|
const data = this.#data
|
|
94
|
-
let readPos = this.#readPos
|
|
95
|
-
let writePos = state[WRITE_INDEX] | 0
|
|
96
93
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (readPos === writePos) {
|
|
100
|
-
writePos = Atomics.load(state, WRITE_INDEX) | 0
|
|
101
|
-
}
|
|
94
|
+
let readPos = state[READ_INDEX] | 0
|
|
95
|
+
let writePos = state[WRITE_INDEX] | 0
|
|
102
96
|
|
|
103
97
|
// Process messages in a batch to minimize loop and atomic operation overhead.
|
|
104
98
|
while (count < HWM_COUNT && bytes < HWM_BYTES && readPos !== writePos) {
|
|
@@ -113,7 +107,7 @@ export class Reader {
|
|
|
113
107
|
readPos = 0
|
|
114
108
|
// After wrapping, we must re-check against the writer's position.
|
|
115
109
|
// It's possible the writer is now at a position > 0.
|
|
116
|
-
writePos =
|
|
110
|
+
writePos = state[WRITE_INDEX] | 0
|
|
117
111
|
} else {
|
|
118
112
|
if (dataLen < 0) {
|
|
119
113
|
throw new Error('Invalid data length')
|
|
@@ -140,12 +134,10 @@ export class Reader {
|
|
|
140
134
|
}
|
|
141
135
|
}
|
|
142
136
|
|
|
143
|
-
this.#readPos = readPos
|
|
144
|
-
|
|
145
137
|
// IMPORTANT: The reader only updates its shared `readPos` after a batch
|
|
146
|
-
// is processed. This significantly reduces
|
|
138
|
+
// is processed. This significantly reduces shared memory overhead.
|
|
147
139
|
if (bytes > 0) {
|
|
148
|
-
|
|
140
|
+
state[READ_INDEX] = readPos | 0
|
|
149
141
|
}
|
|
150
142
|
|
|
151
143
|
return count
|
|
@@ -192,8 +184,8 @@ export class Writer {
|
|
|
192
184
|
|
|
193
185
|
// Local copies of the pointers. The `| 0` is a hint to the V8 JIT
|
|
194
186
|
// compiler that these are 32-bit integers, enabling optimizations.
|
|
195
|
-
this.#readPos =
|
|
196
|
-
this.#writePos =
|
|
187
|
+
this.#readPos = this.#state[READ_INDEX] | 0
|
|
188
|
+
this.#writePos = this.#state[WRITE_INDEX] | 0
|
|
197
189
|
|
|
198
190
|
this.#yielding = 0
|
|
199
191
|
this.#corked = 0
|
|
@@ -231,12 +223,11 @@ export class Writer {
|
|
|
231
223
|
if (delay > 0) {
|
|
232
224
|
Atomics.wait(this.#state, READ_INDEX, this.#readPos, delay)
|
|
233
225
|
} else {
|
|
234
|
-
// @ts-expect-error Atomics.pause is Stage 3, available in Node.js 25+
|
|
235
226
|
Atomics.pause()
|
|
236
227
|
}
|
|
237
228
|
|
|
238
229
|
// After waking up, refresh the local view of the reader's position.
|
|
239
|
-
this.#readPos =
|
|
230
|
+
this.#readPos = this.#state[READ_INDEX] | 0
|
|
240
231
|
}
|
|
241
232
|
|
|
242
233
|
/**
|
|
@@ -260,12 +251,12 @@ export class Writer {
|
|
|
260
251
|
}
|
|
261
252
|
|
|
262
253
|
this.#readPos = state[READ_INDEX] | 0
|
|
263
|
-
if (this.#readPos
|
|
254
|
+
if (this.#readPos < 4) {
|
|
264
255
|
this.#yield(0)
|
|
265
256
|
}
|
|
266
257
|
|
|
267
258
|
// Not enough space at the end. Check if there's space at the beginning.
|
|
268
|
-
if (this.#readPos
|
|
259
|
+
if (this.#readPos < 4) {
|
|
269
260
|
// Reader is at the beginning, so no space to wrap around into.
|
|
270
261
|
return false
|
|
271
262
|
}
|
|
@@ -276,13 +267,13 @@ export class Writer {
|
|
|
276
267
|
// Reset writer position to the beginning.
|
|
277
268
|
this.#writePos = 0
|
|
278
269
|
|
|
279
|
-
if (this.#writePos + 4 > size) {
|
|
270
|
+
if (!isProduction && this.#writePos + 4 > size) {
|
|
280
271
|
// assertion
|
|
281
272
|
throw new Error(
|
|
282
273
|
`Write position ${this.#writePos} with next header exceeds buffer size ${size}`,
|
|
283
274
|
)
|
|
284
275
|
}
|
|
285
|
-
if (this.#writePos === this.#readPos) {
|
|
276
|
+
if (!isProduction && this.#writePos === this.#readPos) {
|
|
286
277
|
// assertion
|
|
287
278
|
throw new Error(
|
|
288
279
|
`Write position ${this.#writePos} cannot equal read position ${this.#readPos}`,
|
|
@@ -290,6 +281,7 @@ export class Writer {
|
|
|
290
281
|
}
|
|
291
282
|
|
|
292
283
|
Atomics.store(state, WRITE_INDEX, this.#writePos)
|
|
284
|
+
this.#pending = 0
|
|
293
285
|
}
|
|
294
286
|
|
|
295
287
|
// Case 2: The writer has wrapped around. [ 0 ... W - R ... s ]
|
|
@@ -329,7 +321,7 @@ export class Writer {
|
|
|
329
321
|
}
|
|
330
322
|
|
|
331
323
|
const size = this.#size
|
|
332
|
-
if (dataPos + dataLen > size) {
|
|
324
|
+
if (!isProduction && dataPos + dataLen > size) {
|
|
333
325
|
// assertion
|
|
334
326
|
throw new Error(`Data position ${dataPos} with length ${dataLen} exceeds buffer size ${size}`)
|
|
335
327
|
}
|
|
@@ -337,11 +329,11 @@ export class Writer {
|
|
|
337
329
|
const alignedLen = (dataLen + 3) & ~3
|
|
338
330
|
const nextPos = this.#writePos + 4 + alignedLen
|
|
339
331
|
|
|
340
|
-
if (nextPos + 4 > size) {
|
|
332
|
+
if (!isProduction && nextPos + 4 > size) {
|
|
341
333
|
// assertion
|
|
342
334
|
throw new Error(`Write position ${nextPos} with next header exceeds buffer size ${size}`)
|
|
343
335
|
}
|
|
344
|
-
if (nextPos === this.#readPos) {
|
|
336
|
+
if (!isProduction && nextPos === this.#readPos) {
|
|
345
337
|
// assertion
|
|
346
338
|
throw new Error(`Write position ${nextPos} cannot equal read position ${this.#readPos}`)
|
|
347
339
|
}
|
|
@@ -413,7 +405,8 @@ export class Writer {
|
|
|
413
405
|
|
|
414
406
|
this.#write(len, fn, opaque)
|
|
415
407
|
|
|
416
|
-
if (this.#writePos === this.#readPos) {
|
|
408
|
+
if (!isProduction && this.#writePos === this.#readPos) {
|
|
409
|
+
// assertion
|
|
417
410
|
throw new Error(
|
|
418
411
|
`Write position ${this.#writePos} cannot equal read position ${this.#readPos}`,
|
|
419
412
|
)
|
|
@@ -447,7 +440,8 @@ export class Writer {
|
|
|
447
440
|
|
|
448
441
|
this.#write(len, fn, opaque)
|
|
449
442
|
|
|
450
|
-
if (this.#writePos === this.#readPos) {
|
|
443
|
+
if (!isProduction && this.#writePos === this.#readPos) {
|
|
444
|
+
// assertion
|
|
451
445
|
throw new Error(
|
|
452
446
|
`Write position ${this.#writePos} cannot equal read position ${this.#readPos}`,
|
|
453
447
|
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxtedition/shared",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -27,5 +27,5 @@
|
|
|
27
27
|
"rimraf": "^6.1.3",
|
|
28
28
|
"typescript": "^5.9.3"
|
|
29
29
|
},
|
|
30
|
-
"gitHead": "
|
|
30
|
+
"gitHead": "f6592f0be62fecc161237610ae6454ec6585548b"
|
|
31
31
|
}
|