@amritk/nish-x86_64-linux 0.28.0 → 0.30.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/std/secret.ts CHANGED
@@ -48,7 +48,7 @@ export const secret = <T>(value: T): Secret<T> => new Secret<T>(value)
48
48
  /** `f(value)`: the one way to compute with what a `Secret` holds. */
49
49
  export const expose = <T, R>(s: Secret<T>, f: (value: T) => R): R => f(s.value)
50
50
 
51
- /** `f(value, arg)`: `expose` with one more argument, because an arrow captures nothing. */
51
+ /** `f(value, arg)`: `expose` with one more argument, because the arrow `expose` runs captures nothing. */
52
52
  export const exposeWith = <T, A, R>(s: Secret<T>, arg: A, f: (value: T, arg: A) => R): R => f(s.value, arg)
53
53
 
54
54
  /**
@@ -0,0 +1,587 @@
1
+ /**
2
+ * `nish/sync/ring` — bounded lock-free queues of byte records in a shared
3
+ * region, for handing work between processes on different cores.
4
+ *
5
+ * import { createRegion, unlinkRegion } from "nish/sync/shm"
6
+ * import { initSpscRing, attachSpscRing, spscRingBytes } from "nish/sync/ring"
7
+ *
8
+ * const region = createRegion("jobs", spscRingBytes(65536)).expect("region")
9
+ * const ring = initSpscRing(region, 0, 65536, true)
10
+ * ring.tryPush(job, 0, job.length) // false when full
11
+ *
12
+ * // The other process, with the region opened by name:
13
+ * const ring = attachSpscRing(region, 0).expect("ring")
14
+ * const n = ring.popWait(buf, 0, 1000) // the record's length, or RING_EMPTY
15
+ *
16
+ * **Two rings.** `SpscRing` has one producer and one consumer, and holds
17
+ * records of any length up to its capacity back to back: a record costs its
18
+ * length rounded up to 4, plus 4. `MpscRing` takes any number of producers
19
+ * and one consumer, in fixed slots of `slotBytes` each (Vyukov's bounded
20
+ * queue: a producer claims a slot with a compare-exchange on the enqueue
21
+ * position, and each slot's sequence number says whose turn it is). Neither
22
+ * takes a lock, so a push or a pop in a busy ring is a handful of loads and
23
+ * stores and no system call.
24
+ *
25
+ * **A ring lives at `base` in a `SharedRegion`.** `initSpscRing` and
26
+ * `initMpscRing` write its header there, which the process that creates the
27
+ * region does once, before anyone attaches; `attachSpscRing` and
28
+ * `attachMpscRing` read it in any other process (or the same one) and refuse
29
+ * memory with no ring of that kind at `base`. `spscRingBytes` and
30
+ * `mpscRingBytes` say how many bytes a ring takes from `base`, which is a
31
+ * multiple of 64 so the header's counters each have a cache line. Several
32
+ * rings may share a region at different bases.
33
+ *
34
+ * **Who may call what is not checked.** An `SpscRing` with two producers, or
35
+ * either ring with two consumers, loses or repeats records; it never reads or
36
+ * writes outside its region, because every access is a checked `SharedRegion`
37
+ * access, and a length a broken peer wrote that cannot be a record's panics.
38
+ * An `MpscRing` producer that dies between claiming a slot and filling it
39
+ * stalls the consumer at that slot for good: a supervisor that restarts
40
+ * producers makes a fresh ring.
41
+ *
42
+ * **The doorbell.** A ring made with `doorbell` true lets the consumer sleep
43
+ * instead of spinning: `popWait` spins a little, then says it is sleeping in
44
+ * the header and sleeps on Linux's futex, and a push rings it only when that
45
+ * flag is set (one `SEQ_CST` store and load on each side, so neither can miss
46
+ * the other). A busy ring makes no system calls; an idle one makes two per
47
+ * record, the consumer's wait and the producer's wake. Without the doorbell a
48
+ * push is a release store and `popWait` answers `SHM_UNSUPPORTED`, as it does
49
+ * off Linux, where the consumer polls `tryPop`. `popWait` reads the monotonic
50
+ * clock for its deadline, so a program that imports this module reaches the
51
+ * `clock` capability as well as `ffi`.
52
+ *
53
+ * Private helpers share the importing program's flat symbol namespace
54
+ * (`docs/wp26-stdlib.md` §3e), which is why each one carries the module's name.
55
+ */
56
+
57
+ import { ACQUIRE, RELAXED, RELEASE, SEQ_CST, SHM_UNSUPPORTED, SharedRegion } from "nish/sync/shm"
58
+ import { monotonicNanos } from "nish:process"
59
+
60
+ /** `tryPop`, `popWait`: the ring is empty (or the wait ran out). */
61
+ export const RING_EMPTY: i32 = -1
62
+ /** `tryPop`, `popWait`: the next record is longer than the room after `off` in `dst`; it stays queued. */
63
+ export const RING_TOO_SMALL: i32 = -2
64
+
65
+ // The header, from `base`. Each counter has a cache line of its own, so the
66
+ // producer's stores and the consumer's never contend for one.
67
+ const RING_MAGIC_AT: i32 = 0
68
+ const RING_SIZE_AT: i32 = 8 // SPSC: the data's bytes; MPSC: the slots
69
+ const RING_SLOT_AT: i32 = 12 // MPSC: each slot's payload bytes
70
+ const RING_BELL_AT: i32 = 16 // 1 when the ring has a doorbell
71
+ const RING_TAIL_AT: i32 = 64 // the producers' position (i64)
72
+ const RING_HEAD_AT: i32 = 128 // the consumer's position (i64)
73
+ const RING_SLEEP_AT: i32 = 192 // 1 while the consumer sleeps or is about to
74
+ const RING_DATA_AT: i32 = 256
75
+ const RING_SPSC_MAGIC: i32 = 0x6e727331 // "nrs1"
76
+ const RING_MPSC_MAGIC: i32 = 0x6e726d31 // "nrm1"
77
+ // Tries before popWait sleeps: several microseconds of polling, longer than
78
+ // the round trip a busy peer answers within, before paying for a futex wake
79
+ // of tens of microseconds. 256 put a ping-pong's p99 at 35 µs; 2048 put it at
80
+ // 1.4 µs, and 8192 bought nothing more (docs/wp47-shared-memory.md).
81
+ const RING_SPINS: i32 = 2048
82
+ // An MPSC slot: [seq i64][len i32][4 unused][payload].
83
+ const RING_SLOT_HEAD: i32 = 16
84
+ // The consumer's sleep flag at RING_SLEEP_AT.
85
+ const RING_AWAKE: i32 = 0
86
+ const RING_ASLEEP: i32 = 1
87
+
88
+ const ringIsPowerOfTwo = (n: i32): boolean => n > 0 && (n & (n - 1)) === 0
89
+
90
+ /** The bytes an SPSC record of `len` takes: its length word and the payload, rounded up to 4. */
91
+ const ringRecordBytes = (len: i32): i32 => ((len + 3) & ~3) + 4
92
+
93
+ /** A ring's header is 64-aligned and the ring fits its region from `base`. */
94
+ const ringCheckPlace = (region: SharedRegion, base: i32, bytes: i64, what: string): void => {
95
+ if (base < 0 || base % 64 !== 0) {
96
+ panic(`nish/sync/ring: ${what} at ${base}: a ring's base is a multiple of 64`)
97
+ }
98
+ if (toI64(base) + bytes > toI64(region.size())) {
99
+ panic(`nish/sync/ring: ${what} at ${base} needs ${bytes} bytes; the region has ${region.size()}`)
100
+ }
101
+ }
102
+
103
+ /** The bytes an `SpscRing` of `capacity` data bytes takes from its base. */
104
+ export const spscRingBytes = (capacity: i32): i32 => RING_DATA_AT + capacity
105
+
106
+ /** The bytes an `MpscRing` of `slots` slots of `slotBytes` takes from its base. */
107
+ export const mpscRingBytes = (slots: i32, slotBytes: i32): i32 =>
108
+ RING_DATA_AT + slots * (RING_SLOT_HEAD + slotBytes)
109
+
110
+ /**
111
+ * Wakes a consumer that said it is sleeping. The caller has published its
112
+ * record with a `SEQ_CST` store, so this load and the consumer's `SEQ_CST`
113
+ * store of the flag cannot both miss each other; the exchange makes one
114
+ * producer of many the one that pays for the wake.
115
+ */
116
+ const ringRing = (region: SharedRegion, base: i32): void => {
117
+ const at: i32 = base + RING_SLEEP_AT
118
+ if (
119
+ region.loadI32(at, SEQ_CST) !== RING_AWAKE &&
120
+ region.exchangeI32(at, RING_AWAKE, SEQ_CST) !== RING_AWAKE
121
+ ) {
122
+ region.wake(at, toI32(1))
123
+ }
124
+ }
125
+
126
+ /** `monotonicNanos()` past which a wait of `timeoutMs` gives up; 0 for no limit. */
127
+ const ringDeadline = (timeoutMs: i32): i64 =>
128
+ timeoutMs < 0 ? toI64(0) : monotonicNanos() + toI64(timeoutMs) * 1000000
129
+
130
+ /** The milliseconds left before `deadline`, rounded up: -1 for no limit, 0 when it has passed. */
131
+ const ringLeftMs = (deadline: i64): i32 => {
132
+ if (deadline === toI64(0)) {
133
+ return -1
134
+ }
135
+ const left: i64 = deadline - monotonicNanos()
136
+ return left <= toI64(0) ? 0 : toI32((left + 999999) / 1000000)
137
+ }
138
+
139
+ /**
140
+ * The consumer's half of the doorbell, after it has stored 1 in the flag with
141
+ * `SEQ_CST` and looked once more (`ready` answers whether a record arrived):
142
+ * sleep at most `waitMs` if none did. False when there is no futex here.
143
+ */
144
+ const ringSleep = (region: SharedRegion, base: i32, ready: boolean, waitMs: i32): boolean => {
145
+ const at: i32 = base + RING_SLEEP_AT
146
+ if (ready) {
147
+ region.storeI32(at, RING_AWAKE, RELAXED)
148
+ return true
149
+ }
150
+ const rc: i32 = region.waitI32(at, RING_ASLEEP, waitMs)
151
+ region.storeI32(at, RING_AWAKE, RELAXED)
152
+ return rc !== SHM_UNSUPPORTED
153
+ }
154
+
155
+ /**
156
+ * One producer, one consumer, records of any length. Make it with
157
+ * `initSpscRing` or `attachSpscRing`; each object keeps its own copy of the
158
+ * position it owns and a cache of the other side's, so a push reads the
159
+ * consumer's counter only when the ring looks full, and a pop the producer's
160
+ * only when it looks empty.
161
+ */
162
+ export class SpscRing {
163
+ /** The region the ring lives in. */
164
+ region: SharedRegion
165
+ /** Its header's offset in the region. */
166
+ base: i32
167
+ /** Data bytes, a power of two. */
168
+ capacity: i32
169
+ /** Whether a push rings the consumer's doorbell. */
170
+ doorbell: boolean
171
+ /** The producer's position: bytes ever pushed (this object's, as a producer). */
172
+ tail: i64
173
+ /** The consumer's position: bytes ever popped (this object's, as a consumer). */
174
+ head: i64
175
+ /** The other side's position, as last loaded. */
176
+ seenHead: i64
177
+ seenTail: i64
178
+
179
+ constructor(region: SharedRegion, base: i32) {
180
+ this.region = region
181
+ this.base = base
182
+ this.capacity = region.loadI32(base + RING_SIZE_AT, RELAXED)
183
+ this.doorbell = region.loadI32(base + RING_BELL_AT, RELAXED) !== 0
184
+ this.tail = region.loadI64(base + RING_TAIL_AT, ACQUIRE)
185
+ this.head = region.loadI64(base + RING_HEAD_AT, ACQUIRE)
186
+ this.seenHead = this.head
187
+ this.seenTail = this.tail
188
+ }
189
+
190
+ /**
191
+ * Appends `src[off, off + len)` as one record: false when the ring has no
192
+ * room for it now. A record longer than the ring could ever hold panics.
193
+ */
194
+ tryPush(src: readonly u8[], off: i32, len: i32): boolean {
195
+ const need: i32 = ringRecordBytes(len)
196
+ if (len < 0 || need > this.capacity) {
197
+ panic(`nish/sync/ring: a record of ${len} bytes does not fit a ring of ${this.capacity}`)
198
+ }
199
+ const end: i64 = this.tail + toI64(need)
200
+ if (end - this.seenHead > toI64(this.capacity)) {
201
+ this.seenHead = this.region.loadI64(this.base + RING_HEAD_AT, ACQUIRE)
202
+ if (end - this.seenHead > toI64(this.capacity)) {
203
+ return false
204
+ }
205
+ }
206
+ const mask: i32 = this.capacity - 1
207
+ const data: i32 = this.base + RING_DATA_AT
208
+ const at: i32 = toI32(this.tail & toI64(mask))
209
+ this.region.storeI32(data + at, len, RELAXED)
210
+ const from: i32 = (at + 4) & mask
211
+ const first: i32 = len < this.capacity - from ? len : this.capacity - from
212
+ this.region.write(data + from, src, off, first)
213
+ if (first < len) {
214
+ this.region.write(data, src, off + first, len - first)
215
+ }
216
+ this.tail = end
217
+ if (this.doorbell) {
218
+ this.region.storeI64(this.base + RING_TAIL_AT, end, SEQ_CST)
219
+ ringRing(this.region, this.base)
220
+ } else {
221
+ this.region.storeI64(this.base + RING_TAIL_AT, end, RELEASE)
222
+ }
223
+ return true
224
+ }
225
+
226
+ /** Whether a record is queued, loading the producer's position if the cache says none. */
227
+ private ready(order: i32): boolean {
228
+ if (this.head !== this.seenTail) {
229
+ return true
230
+ }
231
+ this.seenTail = this.region.loadI64(this.base + RING_TAIL_AT, order)
232
+ return this.head !== this.seenTail
233
+ }
234
+
235
+ /** The length of the record at the consumer's position, checked against the ring. */
236
+ private lengthAt(pos: i64, avail: i64): i32 {
237
+ const mask: i32 = this.capacity - 1
238
+ const len: i32 = this.region.loadI32(this.base + RING_DATA_AT + toI32(pos & toI64(mask)), RELAXED)
239
+ // The capacity first: a length a broken producer wrote near 2^31 would
240
+ // overflow the rounding below and panic with the wrong reason.
241
+ if (len < 0 || len > this.capacity - 4 || toI64(ringRecordBytes(len)) > avail) {
242
+ panic(`nish/sync/ring: a record of length ${len} at position ${pos}: the ring is corrupt`)
243
+ }
244
+ return len
245
+ }
246
+
247
+ /** Copies the record at `pos`, `len` bytes, into `dst` at `off`. */
248
+ private copyOut(pos: i64, len: i32, dst: u8[], off: i32): void {
249
+ const mask: i32 = this.capacity - 1
250
+ const data: i32 = this.base + RING_DATA_AT
251
+ const from: i32 = toI32((pos + 4) & toI64(mask))
252
+ const first: i32 = len < this.capacity - from ? len : this.capacity - from
253
+ this.region.read(data + from, dst, off, first)
254
+ if (first < len) {
255
+ this.region.read(data, dst, off + first, len - first)
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Moves the next record into `dst` from `off` and answers its length:
261
+ * `RING_EMPTY` when there is none, `RING_TOO_SMALL` (keeping it queued)
262
+ * when it is longer than `dst.length - off`.
263
+ */
264
+ tryPop(dst: u8[], off: i32): i32 {
265
+ if (!this.ready(ACQUIRE)) {
266
+ return RING_EMPTY
267
+ }
268
+ const len: i32 = this.lengthAt(this.head, this.seenTail - this.head)
269
+ if (len > toI32(dst.length) - off) {
270
+ return RING_TOO_SMALL
271
+ }
272
+ this.copyOut(this.head, len, dst, off)
273
+ this.head = this.head + toI64(ringRecordBytes(len))
274
+ this.region.storeI64(this.base + RING_HEAD_AT, this.head, RELEASE)
275
+ return len
276
+ }
277
+
278
+ /**
279
+ * Moves up to `lens.length` records into `dst`, back to back from 0, while
280
+ * they fit, and sets `lens[i]` to each one's length: how many it moved. One
281
+ * load of the producer's position and one store of the consumer's, however
282
+ * many records it moves.
283
+ */
284
+ popBatch(dst: u8[], lens: i32[]): i32 {
285
+ // Always the producer's position now, not the cache: a cache that still
286
+ // holds a record would hide every one pushed since it was loaded.
287
+ this.seenTail = this.region.loadI64(this.base + RING_TAIL_AT, ACQUIRE)
288
+ let count: i32 = 0
289
+ let used: i32 = 0
290
+ let pos: i64 = this.head
291
+ while (count < toI32(lens.length) && pos !== this.seenTail) {
292
+ const len: i32 = this.lengthAt(pos, this.seenTail - pos)
293
+ if (len > toI32(dst.length) - used) {
294
+ break
295
+ }
296
+ this.copyOut(pos, len, dst, used)
297
+ if (count >= 0 && count < toI32(lens.length)) {
298
+ lens[count] = len
299
+ }
300
+ count = count + 1
301
+ used = used + len
302
+ pos = pos + toI64(ringRecordBytes(len))
303
+ }
304
+ if (count > 0) {
305
+ this.head = pos
306
+ this.region.storeI64(this.base + RING_HEAD_AT, pos, RELEASE)
307
+ }
308
+ return count
309
+ }
310
+
311
+ /**
312
+ * `tryPop`, but on an empty ring it polls briefly and then sleeps until a
313
+ * push rings the doorbell, for at most `timeoutMs` in all (negative: no
314
+ * limit): `RING_EMPTY` only once that has passed. `SHM_UNSUPPORTED` for a
315
+ * ring without a doorbell, or off Linux, where the caller polls `tryPop`.
316
+ */
317
+ popWait(dst: u8[], off: i32, timeoutMs: i32): i32 {
318
+ const deadline: i64 = ringDeadline(timeoutMs)
319
+ while (true) {
320
+ for (let i: i32 = 0; i < RING_SPINS; i++) {
321
+ const n: i32 = this.tryPop(dst, off)
322
+ if (n !== RING_EMPTY) {
323
+ return n
324
+ }
325
+ }
326
+ if (!this.doorbell) {
327
+ return SHM_UNSUPPORTED
328
+ }
329
+ const waitMs: i32 = ringLeftMs(deadline)
330
+ if (waitMs === 0) {
331
+ return RING_EMPTY
332
+ }
333
+ this.region.storeI32(this.base + RING_SLEEP_AT, RING_ASLEEP, SEQ_CST)
334
+ if (!ringSleep(this.region, this.base, this.ready(SEQ_CST), waitMs)) {
335
+ return SHM_UNSUPPORTED
336
+ }
337
+ }
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Writes an `SpscRing`'s header at `base` and answers the ring: `capacity`
343
+ * data bytes, a power of two from 8, and `doorbell` for `popWait`. The
344
+ * memory is the creator's to give; a ring already there is reset.
345
+ */
346
+ export const initSpscRing = (region: SharedRegion, base: i32, capacity: i32, doorbell: boolean): SpscRing => {
347
+ if (capacity < 8 || !ringIsPowerOfTwo(capacity)) {
348
+ panic(`nish/sync/ring: an SPSC ring's capacity is a power of two from 8, not ${capacity}`)
349
+ }
350
+ ringCheckPlace(region, base, toI64(spscRingBytes(capacity)), "an SPSC ring")
351
+ region.storeI32(base + RING_SIZE_AT, capacity, RELAXED)
352
+ region.storeI32(base + RING_BELL_AT, doorbell ? toI32(1) : toI32(0), RELAXED)
353
+ region.storeI64(base + RING_TAIL_AT, toI64(0), RELAXED)
354
+ region.storeI64(base + RING_HEAD_AT, toI64(0), RELAXED)
355
+ region.storeI32(base + RING_SLEEP_AT, RING_AWAKE, RELAXED)
356
+ region.storeI32(base + RING_MAGIC_AT, RING_SPSC_MAGIC, RELEASE)
357
+ return new SpscRing(region, base)
358
+ }
359
+
360
+ /** The `SpscRing` an `initSpscRing` wrote at `base`; an error when there is none. */
361
+ export const attachSpscRing = (region: SharedRegion, base: i32): Result<SpscRing, string> => {
362
+ ringCheckPlace(region, base, toI64(RING_DATA_AT), "an SPSC ring")
363
+ if (region.loadI32(base + RING_MAGIC_AT, ACQUIRE) !== RING_SPSC_MAGIC) {
364
+ return Err(`no SPSC ring at ${base}`)
365
+ }
366
+ const capacity: i32 = region.loadI32(base + RING_SIZE_AT, RELAXED)
367
+ if (capacity < 8 || !ringIsPowerOfTwo(capacity)) {
368
+ return Err(`the SPSC ring at ${base} has capacity ${capacity}`)
369
+ }
370
+ ringCheckPlace(region, base, toI64(spscRingBytes(capacity)), "an SPSC ring")
371
+ return Ok(new SpscRing(region, base))
372
+ }
373
+
374
+ /**
375
+ * Any number of producers, one consumer, records of at most `slotBytes`.
376
+ * Make it with `initMpscRing` or `attachMpscRing`.
377
+ */
378
+ export class MpscRing {
379
+ /** The region the ring lives in. */
380
+ region: SharedRegion
381
+ /** Its header's offset in the region. */
382
+ base: i32
383
+ /** Slots, a power of two. */
384
+ slots: i32
385
+ /** Each slot's payload bytes, a multiple of 8. */
386
+ slotBytes: i32
387
+ /** Whether a push rings the consumer's doorbell. */
388
+ doorbell: boolean
389
+ /** The consumer's position: records ever popped (this object's, as the consumer). */
390
+ head: i64
391
+
392
+ constructor(region: SharedRegion, base: i32) {
393
+ this.region = region
394
+ this.base = base
395
+ this.slots = region.loadI32(base + RING_SIZE_AT, RELAXED)
396
+ this.slotBytes = region.loadI32(base + RING_SLOT_AT, RELAXED)
397
+ this.doorbell = region.loadI32(base + RING_BELL_AT, RELAXED) !== 0
398
+ this.head = region.loadI64(base + RING_HEAD_AT, ACQUIRE)
399
+ }
400
+
401
+ /** The offset of position `pos`'s slot. */
402
+ private slotAt(pos: i64): i32 {
403
+ const index: i32 = toI32(pos & toI64(this.slots - 1))
404
+ return this.base + RING_DATA_AT + index * (RING_SLOT_HEAD + this.slotBytes)
405
+ }
406
+
407
+ /**
408
+ * Queues `src[off, off + len)` as one record: false when every slot is
409
+ * taken now. A record longer than a slot panics.
410
+ */
411
+ tryPush(src: readonly u8[], off: i32, len: i32): boolean {
412
+ if (len < 0 || len > this.slotBytes) {
413
+ panic(`nish/sync/ring: a record of ${len} bytes does not fit a slot of ${this.slotBytes}`)
414
+ }
415
+ const tailAt: i32 = this.base + RING_TAIL_AT
416
+ let pos: i64 = this.region.loadI64(tailAt, RELAXED)
417
+ while (true) {
418
+ const seq: i64 = this.region.loadI64(this.slotAt(pos), ACQUIRE)
419
+ if (seq === pos) {
420
+ const seen: i64 = this.region.compareExchangeI64(tailAt, pos, pos + 1, RELAXED)
421
+ if (seen === pos) {
422
+ break
423
+ }
424
+ pos = seen
425
+ } else if (seq < pos) {
426
+ return false
427
+ } else {
428
+ pos = this.region.loadI64(tailAt, RELAXED)
429
+ }
430
+ }
431
+ const slot: i32 = this.slotAt(pos)
432
+ this.region.storeI32(slot + 8, len, RELAXED)
433
+ this.region.write(slot + RING_SLOT_HEAD, src, off, len)
434
+ if (this.doorbell) {
435
+ this.region.storeI64(slot, pos + 1, SEQ_CST)
436
+ ringRing(this.region, this.base)
437
+ } else {
438
+ this.region.storeI64(slot, pos + 1, RELEASE)
439
+ }
440
+ return true
441
+ }
442
+
443
+ /** Whether the slot at the consumer's position is filled. */
444
+ private ready(order: i32): boolean {
445
+ return this.region.loadI64(this.slotAt(this.head), order) === this.head + 1
446
+ }
447
+
448
+ /** Empties the consumer's slot for the producer one lap on. */
449
+ private release(slot: i32): void {
450
+ this.region.storeI64(slot, this.head + toI64(this.slots), RELEASE)
451
+ this.head = this.head + 1
452
+ this.region.storeI64(this.base + RING_HEAD_AT, this.head, RELAXED)
453
+ }
454
+
455
+ /** The length in the consumer's slot, checked against the slot. */
456
+ private lengthAt(slot: i32): i32 {
457
+ const len: i32 = this.region.loadI32(slot + 8, RELAXED)
458
+ if (len < 0 || len > this.slotBytes) {
459
+ panic(`nish/sync/ring: a record of length ${len} at position ${this.head}: the ring is corrupt`)
460
+ }
461
+ return len
462
+ }
463
+
464
+ /**
465
+ * Moves the next record into `dst` from `off` and answers its length:
466
+ * `RING_EMPTY` when there is none, `RING_TOO_SMALL` (keeping it queued)
467
+ * when it is longer than `dst.length - off`.
468
+ */
469
+ tryPop(dst: u8[], off: i32): i32 {
470
+ if (!this.ready(ACQUIRE)) {
471
+ return RING_EMPTY
472
+ }
473
+ const slot: i32 = this.slotAt(this.head)
474
+ const len: i32 = this.lengthAt(slot)
475
+ if (len > toI32(dst.length) - off) {
476
+ return RING_TOO_SMALL
477
+ }
478
+ this.region.read(slot + RING_SLOT_HEAD, dst, off, len)
479
+ this.release(slot)
480
+ return len
481
+ }
482
+
483
+ /**
484
+ * Moves up to `lens.length` records into `dst`, back to back from 0, while
485
+ * they fit, and sets `lens[i]` to each one's length: how many it moved.
486
+ */
487
+ popBatch(dst: u8[], lens: i32[]): i32 {
488
+ let count: i32 = 0
489
+ let used: i32 = 0
490
+ while (count < toI32(lens.length) && this.ready(ACQUIRE)) {
491
+ const slot: i32 = this.slotAt(this.head)
492
+ const len: i32 = this.lengthAt(slot)
493
+ if (len > toI32(dst.length) - used) {
494
+ break
495
+ }
496
+ this.region.read(slot + RING_SLOT_HEAD, dst, used, len)
497
+ this.release(slot)
498
+ if (count >= 0 && count < toI32(lens.length)) {
499
+ lens[count] = len
500
+ }
501
+ count = count + 1
502
+ used = used + len
503
+ }
504
+ return count
505
+ }
506
+
507
+ /** `SpscRing.popWait`, for this ring. */
508
+ popWait(dst: u8[], off: i32, timeoutMs: i32): i32 {
509
+ const deadline: i64 = ringDeadline(timeoutMs)
510
+ while (true) {
511
+ for (let i: i32 = 0; i < RING_SPINS; i++) {
512
+ const n: i32 = this.tryPop(dst, off)
513
+ if (n !== RING_EMPTY) {
514
+ return n
515
+ }
516
+ }
517
+ if (!this.doorbell) {
518
+ return SHM_UNSUPPORTED
519
+ }
520
+ const waitMs: i32 = ringLeftMs(deadline)
521
+ if (waitMs === 0) {
522
+ return RING_EMPTY
523
+ }
524
+ this.region.storeI32(this.base + RING_SLEEP_AT, RING_ASLEEP, SEQ_CST)
525
+ if (!ringSleep(this.region, this.base, this.ready(SEQ_CST), waitMs)) {
526
+ return SHM_UNSUPPORTED
527
+ }
528
+ }
529
+ }
530
+ }
531
+
532
+ /**
533
+ * Writes an `MpscRing`'s header at `base` and answers the ring: `slots`
534
+ * slots, a power of two from 2, of `slotBytes` each, a multiple of 8 from 8,
535
+ * and `doorbell` for `popWait`. A ring already there is reset.
536
+ */
537
+ export const initMpscRing = (
538
+ region: SharedRegion,
539
+ base: i32,
540
+ slots: i32,
541
+ slotBytes: i32,
542
+ doorbell: boolean
543
+ ): MpscRing => {
544
+ if (slots < 2 || !ringIsPowerOfTwo(slots) || slotBytes < 8 || slotBytes % 8 !== 0) {
545
+ panic(
546
+ `nish/sync/ring: an MPSC ring has a power of two from 2 slots of a multiple of 8 bytes, not ${slots} of ${slotBytes}`
547
+ )
548
+ }
549
+ ringCheckPlace(
550
+ region,
551
+ base,
552
+ toI64(RING_DATA_AT) + toI64(slots) * toI64(RING_SLOT_HEAD + slotBytes),
553
+ "an MPSC ring"
554
+ )
555
+ region.storeI32(base + RING_SIZE_AT, slots, RELAXED)
556
+ region.storeI32(base + RING_SLOT_AT, slotBytes, RELAXED)
557
+ region.storeI32(base + RING_BELL_AT, doorbell ? toI32(1) : toI32(0), RELAXED)
558
+ region.storeI64(base + RING_TAIL_AT, toI64(0), RELAXED)
559
+ region.storeI64(base + RING_HEAD_AT, toI64(0), RELAXED)
560
+ region.storeI32(base + RING_SLEEP_AT, RING_AWAKE, RELAXED)
561
+ const stride: i32 = RING_SLOT_HEAD + slotBytes
562
+ for (let i: i32 = 0; i < slots; i++) {
563
+ region.storeI64(base + RING_DATA_AT + i * stride, toI64(i), RELAXED)
564
+ }
565
+ region.storeI32(base + RING_MAGIC_AT, RING_MPSC_MAGIC, RELEASE)
566
+ return new MpscRing(region, base)
567
+ }
568
+
569
+ /** The `MpscRing` an `initMpscRing` wrote at `base`; an error when there is none. */
570
+ export const attachMpscRing = (region: SharedRegion, base: i32): Result<MpscRing, string> => {
571
+ ringCheckPlace(region, base, toI64(RING_DATA_AT), "an MPSC ring")
572
+ if (region.loadI32(base + RING_MAGIC_AT, ACQUIRE) !== RING_MPSC_MAGIC) {
573
+ return Err(`no MPSC ring at ${base}`)
574
+ }
575
+ const slots: i32 = region.loadI32(base + RING_SIZE_AT, RELAXED)
576
+ const slotBytes: i32 = region.loadI32(base + RING_SLOT_AT, RELAXED)
577
+ if (slots < 2 || !ringIsPowerOfTwo(slots) || slotBytes < 8 || slotBytes % 8 !== 0) {
578
+ return Err(`the MPSC ring at ${base} has ${slots} slots of ${slotBytes} bytes`)
579
+ }
580
+ ringCheckPlace(
581
+ region,
582
+ base,
583
+ toI64(RING_DATA_AT) + toI64(slots) * toI64(RING_SLOT_HEAD + slotBytes),
584
+ "an MPSC ring"
585
+ )
586
+ return Ok(new MpscRing(region, base))
587
+ }