@nxtedition/shared 4.0.4 → 4.0.6
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.js +22 -30
- package/lib/index.test.d.ts +1 -0
- package/package.json +5 -3
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.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,10 +78,6 @@ 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
|
|
|
@@ -93,14 +90,9 @@ export class Reader {
|
|
|
93
90
|
const int32 = this.#int32
|
|
94
91
|
const size = this.#size
|
|
95
92
|
const data = this.#data
|
|
96
|
-
let readPos = this.#readPos
|
|
97
|
-
let writePos = state[WRITE_INDEX] | 0
|
|
98
93
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (readPos === writePos) {
|
|
102
|
-
writePos = Atomics.load(state, WRITE_INDEX) | 0
|
|
103
|
-
}
|
|
94
|
+
let readPos = state[READ_INDEX] | 0
|
|
95
|
+
let writePos = state[WRITE_INDEX] | 0
|
|
104
96
|
|
|
105
97
|
// Process messages in a batch to minimize loop and atomic operation overhead.
|
|
106
98
|
while (count < HWM_COUNT && bytes < HWM_BYTES && readPos !== writePos) {
|
|
@@ -115,7 +107,7 @@ export class Reader {
|
|
|
115
107
|
readPos = 0
|
|
116
108
|
// After wrapping, we must re-check against the writer's position.
|
|
117
109
|
// It's possible the writer is now at a position > 0.
|
|
118
|
-
writePos =
|
|
110
|
+
writePos = state[WRITE_INDEX] | 0
|
|
119
111
|
} else {
|
|
120
112
|
if (dataLen < 0) {
|
|
121
113
|
throw new Error('Invalid data length')
|
|
@@ -142,12 +134,10 @@ export class Reader {
|
|
|
142
134
|
}
|
|
143
135
|
}
|
|
144
136
|
|
|
145
|
-
this.#readPos = readPos
|
|
146
|
-
|
|
147
137
|
// IMPORTANT: The reader only updates its shared `readPos` after a batch
|
|
148
|
-
// is processed. This significantly reduces
|
|
138
|
+
// is processed. This significantly reduces shared memory overhead.
|
|
149
139
|
if (bytes > 0) {
|
|
150
|
-
|
|
140
|
+
state[READ_INDEX] = readPos | 0
|
|
151
141
|
}
|
|
152
142
|
|
|
153
143
|
return count
|
|
@@ -194,8 +184,8 @@ export class Writer {
|
|
|
194
184
|
|
|
195
185
|
// Local copies of the pointers. The `| 0` is a hint to the V8 JIT
|
|
196
186
|
// compiler that these are 32-bit integers, enabling optimizations.
|
|
197
|
-
this.#readPos =
|
|
198
|
-
this.#writePos =
|
|
187
|
+
this.#readPos = this.#state[READ_INDEX] | 0
|
|
188
|
+
this.#writePos = this.#state[WRITE_INDEX] | 0
|
|
199
189
|
|
|
200
190
|
this.#yielding = 0
|
|
201
191
|
this.#corked = 0
|
|
@@ -233,12 +223,11 @@ export class Writer {
|
|
|
233
223
|
if (delay > 0) {
|
|
234
224
|
Atomics.wait(this.#state, READ_INDEX, this.#readPos, delay)
|
|
235
225
|
} else {
|
|
236
|
-
// @ts-expect-error Atomics.pause is Stage 3, available in Node.js 25+
|
|
237
226
|
Atomics.pause()
|
|
238
227
|
}
|
|
239
228
|
|
|
240
229
|
// After waking up, refresh the local view of the reader's position.
|
|
241
|
-
this.#readPos =
|
|
230
|
+
this.#readPos = this.#state[READ_INDEX] | 0
|
|
242
231
|
}
|
|
243
232
|
|
|
244
233
|
/**
|
|
@@ -262,12 +251,12 @@ export class Writer {
|
|
|
262
251
|
}
|
|
263
252
|
|
|
264
253
|
this.#readPos = state[READ_INDEX] | 0
|
|
265
|
-
if (this.#readPos
|
|
254
|
+
if (this.#readPos < 4) {
|
|
266
255
|
this.#yield(0)
|
|
267
256
|
}
|
|
268
257
|
|
|
269
258
|
// Not enough space at the end. Check if there's space at the beginning.
|
|
270
|
-
if (this.#readPos
|
|
259
|
+
if (this.#readPos < 4) {
|
|
271
260
|
// Reader is at the beginning, so no space to wrap around into.
|
|
272
261
|
return false
|
|
273
262
|
}
|
|
@@ -278,13 +267,13 @@ export class Writer {
|
|
|
278
267
|
// Reset writer position to the beginning.
|
|
279
268
|
this.#writePos = 0
|
|
280
269
|
|
|
281
|
-
if (this.#writePos + 4 > size) {
|
|
270
|
+
if (!isProduction && this.#writePos + 4 > size) {
|
|
282
271
|
// assertion
|
|
283
272
|
throw new Error(
|
|
284
273
|
`Write position ${this.#writePos} with next header exceeds buffer size ${size}`,
|
|
285
274
|
)
|
|
286
275
|
}
|
|
287
|
-
if (this.#writePos === this.#readPos) {
|
|
276
|
+
if (!isProduction && this.#writePos === this.#readPos) {
|
|
288
277
|
// assertion
|
|
289
278
|
throw new Error(
|
|
290
279
|
`Write position ${this.#writePos} cannot equal read position ${this.#readPos}`,
|
|
@@ -292,6 +281,7 @@ export class Writer {
|
|
|
292
281
|
}
|
|
293
282
|
|
|
294
283
|
Atomics.store(state, WRITE_INDEX, this.#writePos)
|
|
284
|
+
this.#pending = 0
|
|
295
285
|
}
|
|
296
286
|
|
|
297
287
|
// Case 2: The writer has wrapped around. [ 0 ... W - R ... s ]
|
|
@@ -331,7 +321,7 @@ export class Writer {
|
|
|
331
321
|
}
|
|
332
322
|
|
|
333
323
|
const size = this.#size
|
|
334
|
-
if (dataPos + dataLen > size) {
|
|
324
|
+
if (!isProduction && dataPos + dataLen > size) {
|
|
335
325
|
// assertion
|
|
336
326
|
throw new Error(`Data position ${dataPos} with length ${dataLen} exceeds buffer size ${size}`)
|
|
337
327
|
}
|
|
@@ -339,11 +329,11 @@ export class Writer {
|
|
|
339
329
|
const alignedLen = (dataLen + 3) & ~3
|
|
340
330
|
const nextPos = this.#writePos + 4 + alignedLen
|
|
341
331
|
|
|
342
|
-
if (nextPos + 4 > size) {
|
|
332
|
+
if (!isProduction && nextPos + 4 > size) {
|
|
343
333
|
// assertion
|
|
344
334
|
throw new Error(`Write position ${nextPos} with next header exceeds buffer size ${size}`)
|
|
345
335
|
}
|
|
346
|
-
if (nextPos === this.#readPos) {
|
|
336
|
+
if (!isProduction && nextPos === this.#readPos) {
|
|
347
337
|
// assertion
|
|
348
338
|
throw new Error(`Write position ${nextPos} cannot equal read position ${this.#readPos}`)
|
|
349
339
|
}
|
|
@@ -415,7 +405,8 @@ export class Writer {
|
|
|
415
405
|
|
|
416
406
|
this.#write(len, fn, opaque)
|
|
417
407
|
|
|
418
|
-
if (this.#writePos === this.#readPos) {
|
|
408
|
+
if (!isProduction && this.#writePos === this.#readPos) {
|
|
409
|
+
// assertion
|
|
419
410
|
throw new Error(
|
|
420
411
|
`Write position ${this.#writePos} cannot equal read position ${this.#readPos}`,
|
|
421
412
|
)
|
|
@@ -449,7 +440,8 @@ export class Writer {
|
|
|
449
440
|
|
|
450
441
|
this.#write(len, fn, opaque)
|
|
451
442
|
|
|
452
|
-
if (this.#writePos === this.#readPos) {
|
|
443
|
+
if (!isProduction && this.#writePos === this.#readPos) {
|
|
444
|
+
// assertion
|
|
453
445
|
throw new Error(
|
|
454
446
|
`Write position ${this.#writePos} cannot equal read position ${this.#readPos}`,
|
|
455
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.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -18,14 +18,16 @@
|
|
|
18
18
|
"prepublishOnly": "yarn build",
|
|
19
19
|
"typecheck": "tsc --noEmit",
|
|
20
20
|
"test": "node --test",
|
|
21
|
-
"test:ci": "node --test"
|
|
21
|
+
"test:ci": "node --test",
|
|
22
|
+
"test:types": "tsd"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
25
|
"@types/node": "^25.2.3",
|
|
25
26
|
"amaroc": "^1.0.1",
|
|
26
27
|
"oxlint-tsgolint": "^0.13.0",
|
|
27
28
|
"rimraf": "^6.1.3",
|
|
29
|
+
"tsd": "^0.33.0",
|
|
28
30
|
"typescript": "^5.9.3"
|
|
29
31
|
},
|
|
30
|
-
"gitHead": "
|
|
32
|
+
"gitHead": "9b8156711c1909480df222a003871e2d9cded24c"
|
|
31
33
|
}
|