@amritk/nish-x86_64-linux 0.27.2 → 0.29.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/INSTALL.md +16 -8
- package/bin/nish +0 -0
- package/docs/AI.md +118 -26
- package/package.json +1 -1
- package/runtime/core/host.d.ts +2 -1
- package/runtime/core/host.mjs +4 -0
- package/runtime/core.d.ts +47 -0
- package/runtime/core.mjs +67 -0
- package/runtime/nish.d.ts +176 -0
- package/runtime/nish.h +120 -6
- package/runtime/nish.mjs +24 -4
- package/runtime/runtime-checksum.c +270 -0
- package/runtime/runtime-net.c +386 -5
- package/runtime/runtime-os.c +59 -8
- package/runtime/runtime-parallel.c +81 -5
- package/runtime/runtime-sandbox.c +479 -0
- package/runtime/runtime-sha256.c +239 -0
- package/runtime/runtime-shm.c +541 -0
- package/runtime/runtime.c +827 -791
- package/scripts/build.sh +79 -28
- package/std/README.md +25 -13
- package/std/collections.ts +5 -5
- package/std/compress/checksum.ts +88 -75
- package/std/compress/deflate.ts +224 -0
- package/std/compress/gzip.ts +52 -0
- package/std/crypto/bcrypt.ts +816 -280
- package/std/crypto/hmac.ts +305 -2
- package/std/crypto/jwt.ts +1661 -0
- package/std/json-equal.ts +335 -0
- package/std/json-format.ts +1070 -0
- package/std/json-number.ts +428 -0
- package/std/json-pointer.ts +245 -0
- package/std/json-typed.ts +34 -0
- package/std/json-value.ts +510 -17
- package/std/json.ts +35 -6
- package/std/net/handler.ts +1348 -0
- package/std/net/http-client.ts +97 -16
- package/std/net/http-compress.ts +301 -16
- package/std/net/http1-client.ts +104 -19
- package/std/net/http1-server.ts +127 -24
- package/std/net/http1.ts +24 -4
- package/std/net/outbound.ts +409 -0
- package/std/net/postgres.ts +405 -0
- package/std/net/prefork.ts +344 -20
- package/std/net/websocket.ts +295 -14
- package/std/regex.ts +613 -52
- package/std/secret.ts +1 -1
- package/std/sync/ring.ts +587 -0
- package/std/sync/shm.ts +284 -0
- package/std/term-io.ts +35 -8
- package/std/term-rate.ts +13 -1
- package/std/threads.ts +35 -3
- package/std/yaml-json.ts +841 -0
- /package/runtime/{LICENSE-ryu → LICENSE-zmij} +0 -0
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
|
|
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
|
/**
|
package/std/sync/ring.ts
ADDED
|
@@ -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
|
+
}
|