@threadedstack/wmd-web 0.0.0-stage → 0.1.13

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.
Files changed (92) hide show
  1. package/README.md +2601 -2
  2. package/RELEASE.json +123 -0
  3. package/export-page/coi-sw.js +20 -0
  4. package/export-page/index.html +17 -0
  5. package/export-page/run.css +10 -0
  6. package/export-page/run.js +236 -0
  7. package/guest/arm64/Image +4 -0
  8. package/guest/arm64/Image-4k +4 -0
  9. package/guest/arm64/initramfs.cpio.gz +0 -0
  10. package/guest/arm64/vm.wasm +0 -0
  11. package/package.json +36 -4
  12. package/page/sw.js +381 -0
  13. package/relay/control.ts +139 -0
  14. package/relay/engine.ts +787 -0
  15. package/relay/hosted.ts +660 -0
  16. package/relay/keys.ts +53 -0
  17. package/relay/local-tls.ts +121 -0
  18. package/relay/lock.ts +81 -0
  19. package/relay/logfile.ts +138 -0
  20. package/relay/main.ts +642 -0
  21. package/relay/policy.ts +241 -0
  22. package/relay/relay.ts +266 -0
  23. package/relay/token.ts +116 -0
  24. package/relay/v1.ts +107 -0
  25. package/relay/wisp.ts +615 -0
  26. package/src/base.js +325 -0
  27. package/src/boot.js +2037 -0
  28. package/src/calib-worker.js +40 -0
  29. package/src/fs/9p/codec.js +303 -0
  30. package/src/fs/9p/server.js +521 -0
  31. package/src/fs/errno.js +43 -0
  32. package/src/fs/graph.js +380 -0
  33. package/src/fs/overlay.js +1018 -0
  34. package/src/fs/pack-write.js +165 -0
  35. package/src/fs/pack.js +427 -0
  36. package/src/fs/seed.js +22 -0
  37. package/src/fs/svc-snapshot.js +65 -0
  38. package/src/fs/upper-pack.js +112 -0
  39. package/src/host/base-cache.js +195 -0
  40. package/src/host/base-file.js +328 -0
  41. package/src/host/base-io.js +93 -0
  42. package/src/host/boot-events.js +209 -0
  43. package/src/host/chan-service.js +300 -0
  44. package/src/host/chan-services.js +689 -0
  45. package/src/host/chan.js +655 -0
  46. package/src/host/ckpt-file.js +395 -0
  47. package/src/host/ckpt-io.js +207 -0
  48. package/src/host/ckpt-point.js +160 -0
  49. package/src/host/ckpt-save.js +64 -0
  50. package/src/host/ckpt.js +434 -0
  51. package/src/host/clock.js +111 -0
  52. package/src/host/ctl.js +778 -0
  53. package/src/host/errno.js +18 -0
  54. package/src/host/fs.js +167 -0
  55. package/src/host/gcnudge.js +83 -0
  56. package/src/host/i64shim.js +81 -0
  57. package/src/host/jit-cache.js +341 -0
  58. package/src/host/jit-hub-core.js +208 -0
  59. package/src/host/jit-hub.js +93 -0
  60. package/src/host/jit-snap.js +226 -0
  61. package/src/host/jit.js +1903 -0
  62. package/src/host/layout.js +100 -0
  63. package/src/host/monitor.js +110 -0
  64. package/src/host/p9mount.js +27 -0
  65. package/src/host/ram-mem.js +125 -0
  66. package/src/host/stdio.js +143 -0
  67. package/src/host/threads.js +109 -0
  68. package/src/host/wasi.js +335 -0
  69. package/src/host/wmd-imports.js +71 -0
  70. package/src/host/yamux.js +387 -0
  71. package/src/image-files.js +154 -0
  72. package/src/net/dhcp-dns.js +126 -0
  73. package/src/net/net-worker.js +172 -0
  74. package/src/net/packet.js +227 -0
  75. package/src/net/peer.js +230 -0
  76. package/src/net/proto.js +189 -0
  77. package/src/net/relay-client.js +226 -0
  78. package/src/net/stack.js +867 -0
  79. package/src/net/switch.js +160 -0
  80. package/src/net/wisp-client.js +570 -0
  81. package/src/net/wisp-transport.js +196 -0
  82. package/src/net/wisp.js +511 -0
  83. package/src/registry/client.js +176 -0
  84. package/src/registry/converter.js +68 -0
  85. package/src/registry/hub.js +221 -0
  86. package/src/registry/oci.js +76 -0
  87. package/src/registry/pack-worker.js +70 -0
  88. package/src/registry/pull.js +258 -0
  89. package/src/registry/ref.js +110 -0
  90. package/src/registry/wasi-mem.js +355 -0
  91. package/src/worker.js +315 -0
  92. package/wmdpack.wasm +0 -0
@@ -0,0 +1,655 @@
1
+ // P-B3: wmd.chan_* over shared-memory rings (web/docs/host-interface.md §3).
2
+ //
3
+ // The runner's channels are byte streams to a TCP or Unix socket (runner/src/chan.rs).
4
+ // Here a channel is a duplex pair of byte rings in one SharedArrayBuffer (the "chan
5
+ // block"), with a service Worker (chan-service.js) on the host side. The guest threads
6
+ // call the imports synchronously and block in Atomics.wait; the service never blocks
7
+ // (Atomics.waitAsync on one doorbell cell), so it can still receive messages.
8
+ //
9
+ // Semantics, as chan.rs:
10
+ // chan_open(kind, port) -22 port not in 1..65535; -111 no service on that port (or
11
+ // the service is dead); else the lowest free handle. The port
12
+ // decides the service; kind is only recorded.
13
+ // chan_send(h, ptr, len) all `len` bytes, blocking while the ring is full; len, or -14
14
+ // bad range, -9 bad handle, -32 when the peer is gone or the
15
+ // channel was shut down for writing. One message is never
16
+ // interleaved with another thread's: senders hold the ring's
17
+ // producer lock for the whole call (a socket does not promise
18
+ // that; the guest relies on it for 9P anyway).
19
+ // chan_recv(h, ptr, len, t) t >= 0: wait up to t ns (rounded up to ms, as chan.rs), 0 if
20
+ // nothing came; t < 0: block. Bytes read (a partial read is
21
+ // normal), 0 on timeout, -1 at EOF or when the peer is gone.
22
+ // Bytes already in the ring are delivered before EOF.
23
+ // chan_wake(h) wake-on-stop: ends one wait in chan_recv(h) now. A latched
24
+ // one-shot flag (S_WAKE): the woken recv, or the next one if none
25
+ // is waiting, returns the bytes in the ring or 0 at once; wakes
26
+ // before that collapse into one. 0, or -9 bad handle.
27
+ // chan_shutdown(h) half-close: the host side sees EOF after the bytes in the ring.
28
+ // chan_close(h) free the handle. Threads blocked on it return -1 / -32. The
29
+ // slot is reused only after the service has dropped its session,
30
+ // so a reply in flight can never land in a new channel.
31
+ //
32
+ // A peer that dies cannot hang the guest: the page (or the service itself) marks the
33
+ // service dead (markServiceDead), which wakes every waiter, and every blocking loop also
34
+ // re-checks the machine's exit flag at least once a second. When the machine ends, the
35
+ // page marks the guest gone (markGuestGone), which wakes every waiter at once too.
36
+ //
37
+ // Layout (Int32 cells, then the ring data):
38
+ // header HDR_INTS cells: doorbell, service state, guest-gone flag, port registry, counters
39
+ // slots nslots x SLOT_INTS: state, kind, port, generation, then two rings' cells
40
+ // (guest->host and host->guest)
41
+ // pipe one more ring's cells: the console's path from the service to the page
42
+ // stats Float64: per slot bytes each way; the guest-side 9P round-trip histogram
43
+ // data 2 rings per slot + the pipe, ringBytes each (a power of two)
44
+
45
+ export const CHAN_NET = 1
46
+ export const CHAN_P9 = 2
47
+ export const CHAN_CONTROL = 3
48
+ export const CHAN_CONSOLE = 4
49
+ export const KIND_NAMES = { 1: 'net', 2: '9p', 3: 'control', 4: 'console' }
50
+ export const KIND_OF = { net: 1, '9p': 2, control: 3, console: 4 }
51
+
52
+ export const EBADF = -9
53
+ export const EFAULT = -14
54
+ export const EINVAL = -22
55
+ export const EMFILE = -24
56
+ export const EPIPE = -32
57
+ export const ECONNREFUSED = -111
58
+
59
+ // Header cells.
60
+ const H_MAGIC = 0
61
+ const H_NSLOTS = 1
62
+ const H_RING = 2
63
+ export const H_DOORBELL = 3 // bumped (and notified) by anything the service must look at
64
+ export const H_SVC = 4 // service state
65
+ export const H_GONE = 5 // the guest machine has ended: the service stops
66
+ const H_NPORTS = 6
67
+ export const H_OPENS = 7
68
+ export const H_REFUSED = 8
69
+ export const H_CLOSES = 9
70
+ const H_PIPE_RING = 10 // bytes of the page pipe ring
71
+ export const H_CIN_TAKEN = 11 // console input bytes the service has put into a guest ring (or dropped), mod 2^32
72
+ export const H_PORTS = 16 // MAX_PORTS x [port, kind]
73
+ export const MAX_PORTS = 16
74
+ const HDR_INTS = 64
75
+ const MAGIC = 0x77636831 // "wch1"
76
+
77
+ export const SVC_STARTING = 0
78
+ export const SVC_RUNNING = 1
79
+ export const SVC_DEAD = 2
80
+
81
+ // Slot cells.
82
+ export const SLOT_INTS = 32
83
+ export const S_STATE = 0
84
+ export const S_KIND = 1
85
+ export const S_PORT = 2
86
+ export const S_GEN = 3
87
+ export const S_WAKE = 4 // chan_wake latched: the next recv that would wait returns 0
88
+ export const G2H = 8 // guest -> host ring cells
89
+ export const H2G = 16 // host -> guest ring cells
90
+
91
+ export const ST_FREE = 0
92
+ export const ST_OPEN = 1
93
+ export const ST_CLOSING = 2 // chan_close called; the service frees the slot
94
+ const ST_OPENING = 3
95
+
96
+ // Ring cells (relative to the ring's base cell).
97
+ export const R_W = 0 // bytes ever written, mod 2^32
98
+ export const R_R = 1 // bytes ever read, mod 2^32
99
+ export const R_DSEQ = 2 // bumped on data written / writer done / torn down: consumers wait here
100
+ export const R_SSEQ = 3 // bumped on data read / reader gone / torn down: producers wait here
101
+ export const R_SHUT = 4 // the writer is done: EOF once the ring is empty
102
+ export const R_GONE = 5 // torn down: no more reads or writes
103
+ const R_PLOCK = 6 // producer lock (held for a whole send)
104
+ const R_CLOCK = 7 // consumer lock (held for one copy)
105
+
106
+ // 9P round-trip histogram (guest side: chan_send of a T-message -> its R-message
107
+ // delivered by chan_recv). Buckets: 0..999 us by 1 us, 1,000..9,990 us by 10 us,
108
+ // then one bucket for 10 ms and more.
109
+ export const RTT_BUCKETS = 1000 + 900 + 1
110
+ export function rttBucket(us) {
111
+ if (us < 0) us = 0
112
+ if (us < 1000) return Math.floor(us)
113
+ if (us < 10000) return 1000 + Math.floor((us - 1000) / 10)
114
+ return 1900
115
+ }
116
+ export function bucketUs(b) {
117
+ if (b < 1000) return b + 0.5
118
+ if (b < 1900) return 1000 + (b - 1000) * 10 + 5
119
+ return 10000
120
+ }
121
+
122
+ /** p50/p90/p99/max (us) and count from a bucket array (Int32Array or number[]). */
123
+ export function histSummary(buckets, extra = {}) {
124
+ let n = 0
125
+ for (let i = 0; i < buckets.length; i++) n += buckets[i]
126
+ const out = { n, ...extra }
127
+ if (!n) return out
128
+ const at = q => {
129
+ const want = Math.max(1, Math.ceil(n * q))
130
+ let c = 0
131
+ for (let i = 0; i < buckets.length; i++) {
132
+ c += buckets[i]
133
+ if (c >= want) return bucketUs(i)
134
+ }
135
+ return bucketUs(buckets.length - 1)
136
+ }
137
+ out.p50Us = at(0.5)
138
+ out.p90Us = at(0.9)
139
+ out.p99Us = at(0.99)
140
+ let last = 0
141
+ for (let i = 0; i < buckets.length; i++) if (buckets[i]) last = i
142
+ out.maxBucketUs = bucketUs(last)
143
+ return out
144
+ }
145
+
146
+ /** W51: one stage histogram (see layoutOf). Concurrent writers are fine for a diagnostic. */
147
+ export function stageNote(st, us) {
148
+ if (!(us >= 0)) return
149
+ Atomics.add(st.hist, rttBucket(us), 1)
150
+ st.f64[0] += us
151
+ st.f64[1] += 1
152
+ if (us > st.f64[2]) st.f64[2] = us
153
+ }
154
+ const stageSummary = st => histSummary(st.hist, { meanUs: st.f64[1] ? st.f64[0] / st.f64[1] : 0, maxUs: st.f64[2] })
155
+
156
+ const align = (n, a) => Math.ceil(n / a) * a
157
+ const STAGE_BYTES = align(RTT_BUCKETS * 4, 8) + 4 * 8 // W51: one stage histogram + sum, n, max, spare
158
+
159
+ function layoutOf(nslots, ringBytes, pipeBytes) {
160
+ const ints = HDR_INTS + (nslots + 1) * SLOT_INTS
161
+ const statsOff = align(ints * 4, 64)
162
+ const statsF64 = nslots * 4 + 4 // per slot: bytes g2h, bytes h2g, send waits, send wait ms; then rtt sum, n, max, spare
163
+ const rttOff = align(statsOff + statsF64 * 8, 64)
164
+ const tagOff = align(rttOff + RTT_BUCKETS * 4, 64) // Float64 send time per 9P tag
165
+ // W51: the stage split of a 9P round trip. Float64 reply-written time per tag (the
166
+ // service), then two histograms with their sum/n/max: request picked up by the service
167
+ // (send -> parse) and reply read by the guest (reply written -> chan_recv returns).
168
+ const replyOff = tagOff + 65536 * 8
169
+ const stageOff = align(replyOff + 65536 * 8, 64)
170
+ const dataOff = align(stageOff + 2 * STAGE_BYTES, 4096)
171
+ const total = dataOff + nslots * 2 * ringBytes + pipeBytes
172
+ return { statsOff, rttOff, tagOff, replyOff, stageOff, dataOff, total }
173
+ }
174
+
175
+ const pow2 = n => n >= 64 && n <= 1 << 30 && (n & (n - 1)) === 0
176
+
177
+ /**
178
+ * The chan block. ports: [{port, kind}] the services listen on (kind: 1..4 or a name).
179
+ * ringBytes: each direction of each channel (power of two, >= 64).
180
+ */
181
+ export function createChanBlock({ ports = [], nslots = 8, ringBytes = 1 << 20, pipeBytes = 1 << 23 } = {}) {
182
+ if (!pow2(ringBytes)) throw new Error(`channel ring size must be a power of two from 64 bytes to 1 GiB, got ${ringBytes}`)
183
+ if (!pow2(pipeBytes)) throw new Error(`console pipe size must be a power of two, got ${pipeBytes}`)
184
+ if (ports.length > MAX_PORTS) throw new Error(`at most ${MAX_PORTS} channel services`)
185
+ if (!(nslots >= 1 && nslots <= 64)) throw new Error('nslots must be 1..64')
186
+ const L = layoutOf(nslots, ringBytes, pipeBytes)
187
+ const sab = new SharedArrayBuffer(L.total)
188
+ const i32 = new Int32Array(sab)
189
+ i32[H_MAGIC] = MAGIC
190
+ i32[H_NSLOTS] = nslots
191
+ i32[H_RING] = ringBytes
192
+ i32[H_PIPE_RING] = pipeBytes
193
+ const seen = new Set()
194
+ ports.forEach((p, i) => {
195
+ const kind = typeof p.kind === 'string' ? KIND_OF[p.kind] : p.kind
196
+ if (!(p.port >= 1 && p.port <= 65535)) throw new Error(`channel port ${p.port}: must be 1..65535`)
197
+ if (!kind) throw new Error(`channel port ${p.port}: unknown kind ${p.kind}`)
198
+ if (seen.has(p.port)) throw new Error(`channel port ${p.port} given twice`)
199
+ seen.add(p.port)
200
+ i32[H_PORTS + 2 * i] = p.port
201
+ i32[H_PORTS + 2 * i + 1] = kind
202
+ })
203
+ i32[H_NPORTS] = ports.length
204
+ return sab
205
+ }
206
+
207
+ /** Views and ring handles over a chan block, for any thread (guest, service, page). */
208
+ export function chanView(sab) {
209
+ const i32 = new Int32Array(sab)
210
+ if (i32[H_MAGIC] !== MAGIC) throw new Error('not a chan block')
211
+ const nslots = i32[H_NSLOTS]
212
+ const ringBytes = i32[H_RING]
213
+ const pipeBytes = i32[H_PIPE_RING]
214
+ const L = layoutOf(nslots, ringBytes, pipeBytes)
215
+ const u8 = new Uint8Array(sab)
216
+ const f64 = new Float64Array(sab, L.statsOff, nslots * 4 + 4)
217
+ const rtt = new Int32Array(sab, L.rttOff, RTT_BUCKETS)
218
+ const tagTimes = new Float64Array(sab, L.tagOff, 65536)
219
+ const replyTimes = new Float64Array(sab, L.replyOff, 65536)
220
+ const stage = k => ({
221
+ hist: new Int32Array(sab, L.stageOff + k * STAGE_BYTES, RTT_BUCKETS),
222
+ f64: new Float64Array(sab, L.stageOff + k * STAGE_BYTES + align(RTT_BUCKETS * 4, 8), 4),
223
+ })
224
+ const pickup = stage(0), readback = stage(1)
225
+ const slotBase = s => HDR_INTS + s * SLOT_INTS
226
+ const ring = (s, dir) => ({
227
+ c: slotBase(s) + dir,
228
+ data: L.dataOff + (2 * s + (dir === G2H ? 0 : 1)) * ringBytes,
229
+ size: ringBytes,
230
+ })
231
+ const pipe = { c: slotBase(nslots) + G2H, data: L.dataOff + nslots * 2 * ringBytes, size: pipeBytes }
232
+ return { sab, i32, u8, f64, rtt, tagTimes, replyTimes, pickup, readback, nslots, ringBytes, pipeBytes, slotBase, ring, pipe }
233
+ }
234
+
235
+ // ---- ring primitives (non-blocking; callers do the waiting) ----------------------------
236
+
237
+ export const ringAvail = (v, r) => (Atomics.load(v.i32, r.c + R_W) - Atomics.load(v.i32, r.c + R_R)) >>> 0
238
+ export const ringSpace = (v, r) => r.size - ringAvail(v, r)
239
+
240
+ /** Copy up to n bytes of src[off..] into the ring. One producer at a time. Returns bytes written. */
241
+ export function ringWrite(v, r, src, off = 0, n = src.length - off) {
242
+ const i32 = v.i32
243
+ const w = Atomics.load(i32, r.c + R_W) >>> 0
244
+ const rd = Atomics.load(i32, r.c + R_R) >>> 0
245
+ const space = r.size - ((w - rd) >>> 0)
246
+ n = Math.min(n, space)
247
+ if (n <= 0) return 0
248
+ const at = w & (r.size - 1)
249
+ const first = Math.min(n, r.size - at)
250
+ v.u8.set(src.subarray(off, off + first), r.data + at)
251
+ if (first < n) v.u8.set(src.subarray(off + first, off + n), r.data)
252
+ Atomics.store(i32, r.c + R_W, (w + n) | 0)
253
+ Atomics.add(i32, r.c + R_DSEQ, 1)
254
+ Atomics.notify(i32, r.c + R_DSEQ)
255
+ return n
256
+ }
257
+
258
+ /** Copy up to n bytes out of the ring into dst[off..]. One consumer at a time. Returns bytes read. */
259
+ export function ringRead(v, r, dst, off = 0, n = dst.length - off) {
260
+ const i32 = v.i32
261
+ const w = Atomics.load(i32, r.c + R_W) >>> 0
262
+ const rd = Atomics.load(i32, r.c + R_R) >>> 0
263
+ n = Math.min(n, (w - rd) >>> 0)
264
+ if (n <= 0) return 0
265
+ const at = rd & (r.size - 1)
266
+ const first = Math.min(n, r.size - at)
267
+ dst.set(v.u8.subarray(r.data + at, r.data + at + first), off)
268
+ if (first < n) dst.set(v.u8.subarray(r.data, r.data + n - first), off + first)
269
+ Atomics.store(i32, r.c + R_R, (rd + n) | 0)
270
+ Atomics.add(i32, r.c + R_SSEQ, 1)
271
+ Atomics.notify(i32, r.c + R_SSEQ)
272
+ return n
273
+ }
274
+
275
+ /** Wake everything waiting on a ring (after a flag change). */
276
+ export function ringKick(v, r) {
277
+ Atomics.add(v.i32, r.c + R_DSEQ, 1)
278
+ Atomics.add(v.i32, r.c + R_SSEQ, 1)
279
+ Atomics.notify(v.i32, r.c + R_DSEQ)
280
+ Atomics.notify(v.i32, r.c + R_SSEQ)
281
+ }
282
+
283
+ export function ringBell(v) {
284
+ Atomics.add(v.i32, H_DOORBELL, 1)
285
+ Atomics.notify(v.i32, H_DOORBELL)
286
+ }
287
+
288
+ /** Reset a ring for a new channel. Sequence cells keep counting, so no waiter sees an old value come back. */
289
+ function ringReset(v, r) {
290
+ const i32 = v.i32
291
+ Atomics.store(i32, r.c + R_W, 0)
292
+ Atomics.store(i32, r.c + R_R, 0)
293
+ Atomics.store(i32, r.c + R_SHUT, 0)
294
+ Atomics.store(i32, r.c + R_GONE, 0)
295
+ Atomics.store(i32, r.c + R_PLOCK, 0)
296
+ Atomics.store(i32, r.c + R_CLOCK, 0)
297
+ }
298
+
299
+ /**
300
+ * Mark the service dead: every waiter wakes, chan_open refuses, sends fail with -32, and
301
+ * receives return what is left in their ring and then -1. Safe from any thread, any
302
+ * number of times.
303
+ */
304
+ export function markServiceDead(sab) {
305
+ const v = chanView(sab)
306
+ Atomics.store(v.i32, H_SVC, SVC_DEAD)
307
+ for (let s = 0; s < v.nslots; s++) {
308
+ const g = v.ring(s, G2H)
309
+ const h = v.ring(s, H2G)
310
+ Atomics.store(v.i32, g.c + R_GONE, 1)
311
+ Atomics.store(v.i32, h.c + R_SHUT, 1)
312
+ ringKick(v, g)
313
+ ringKick(v, h)
314
+ }
315
+ ringKick(v, v.pipe)
316
+ ringBell(v)
317
+ }
318
+
319
+ /**
320
+ * The guest machine has ended: the service drops every session and stops, and every
321
+ * guest thread still blocked in chan_send / chan_recv returns now (-32 / -1) instead of
322
+ * at the end of its current wait slice.
323
+ */
324
+ export function markGuestGone(sab) {
325
+ const v = chanView(sab)
326
+ Atomics.store(v.i32, H_GONE, 1)
327
+ for (let s = 0; s < v.nslots; s++) {
328
+ ringKick(v, v.ring(s, G2H))
329
+ ringKick(v, v.ring(s, H2G))
330
+ }
331
+ ringKick(v, v.pipe)
332
+ ringBell(v)
333
+ }
334
+
335
+ export const serviceDead = sab => Atomics.load(new Int32Array(sab), H_SVC) === SVC_DEAD
336
+
337
+ // ---- guest side: the five imports -------------------------------------------------------
338
+
339
+ function lock(i32, i, abort) {
340
+ if (Atomics.compareExchange(i32, i, 0, 1) === 0) return true
341
+ for (;;) {
342
+ if (Atomics.exchange(i32, i, 2) === 0) return true
343
+ if (abort()) return false
344
+ Atomics.wait(i32, i, 2, 100)
345
+ }
346
+ }
347
+
348
+ function unlock(i32, i) {
349
+ if (Atomics.sub(i32, i, 1) !== 1) {
350
+ Atomics.store(i32, i, 0)
351
+ Atomics.notify(i32, i, 1)
352
+ }
353
+ }
354
+
355
+ const WAIT_SLICE_MS = 1000 // longest single Atomics.wait: flags are re-checked at least this often
356
+
357
+ /**
358
+ * wmd.chan_* for one guest Worker.
359
+ * @param {object} o
360
+ * @param {SharedArrayBuffer} o.sab the chan block
361
+ * @param {WebAssembly.Memory} o.memory the guest's shared memory
362
+ * @param {() => boolean} [o.exiting] the machine is ending (CTL_EXIT)
363
+ * @param {() => number} [o.now] ms clock comparable across Workers
364
+ */
365
+ export function createChanImports({ sab, memory, exiting = () => false, now = () => performance.timeOrigin + performance.now() }) {
366
+ const v = chanView(sab)
367
+ const i32 = v.i32
368
+ let buf = memory.buffer
369
+ let mem = new Uint8Array(buf)
370
+ const views = () => {
371
+ const b = memory.buffer
372
+ if (b !== buf || b.byteLength !== mem.length) {
373
+ buf = b
374
+ mem = new Uint8Array(b)
375
+ }
376
+ }
377
+ const dead = () => Atomics.load(i32, H_SVC) === SVC_DEAD
378
+ const ending = () => exiting() || Atomics.load(i32, H_GONE) !== 0 // the machine is over
379
+ const valid = h => Number.isInteger(h) && h >= 0 && h < v.nslots && Atomics.load(i32, v.slotBase(h) + S_STATE) === ST_OPEN
380
+ // 9P framing state of the replies this thread receives, per handle (only the pump
381
+ // thread receives; this is for the round-trip histogram, it never changes the bytes).
382
+ const rx = new Map()
383
+
384
+ function open(kind, port) {
385
+ if (!(port >= 1 && port <= 65535)) return EINVAL
386
+ if (dead() || Atomics.load(i32, H_GONE)) {
387
+ Atomics.add(i32, H_REFUSED, 1)
388
+ return ECONNREFUSED
389
+ }
390
+ const n = Atomics.load(i32, H_NPORTS)
391
+ let found = false
392
+ for (let i = 0; i < n; i++) if (Atomics.load(i32, H_PORTS + 2 * i) === port) found = true
393
+ if (!found) {
394
+ Atomics.add(i32, H_REFUSED, 1)
395
+ return ECONNREFUSED
396
+ }
397
+ for (let s = 0; s < v.nslots; s++) {
398
+ const b = v.slotBase(s)
399
+ if (Atomics.compareExchange(i32, b + S_STATE, ST_FREE, ST_OPENING) !== ST_FREE) continue
400
+ ringReset(v, v.ring(s, G2H))
401
+ ringReset(v, v.ring(s, H2G))
402
+ v.f64.fill(0, s * 4, s * 4 + 4)
403
+ Atomics.store(i32, b + S_KIND, kind)
404
+ Atomics.store(i32, b + S_PORT, port)
405
+ Atomics.store(i32, b + S_WAKE, 0)
406
+ Atomics.add(i32, b + S_GEN, 1)
407
+ Atomics.store(i32, b + S_STATE, ST_OPEN)
408
+ Atomics.add(i32, H_OPENS, 1)
409
+ rx.delete(s)
410
+ ringBell(v)
411
+ return s
412
+ }
413
+ return EMFILE
414
+ }
415
+
416
+ function send(h, ptr, len) {
417
+ ptr >>>= 0
418
+ len >>>= 0
419
+ views()
420
+ if (ptr + len > mem.length) return EFAULT
421
+ if (!valid(h)) return EBADF
422
+ const b = v.slotBase(h)
423
+ const gen = Atomics.load(i32, b + S_GEN)
424
+ const r = v.ring(h, G2H)
425
+ const broken = () =>
426
+ dead() || Atomics.load(i32, r.c + R_GONE) !== 0 || Atomics.load(i32, r.c + R_SHUT) !== 0 ||
427
+ Atomics.load(i32, b + S_STATE) !== ST_OPEN || Atomics.load(i32, b + S_GEN) !== gen
428
+ if (broken()) return EPIPE
429
+ if (!lock(i32, r.c + R_PLOCK, () => broken() || ending())) return EPIPE
430
+ try {
431
+ if (Atomics.load(i32, b + S_KIND) === CHAN_P9 && len >= 7) {
432
+ // T-message: stamp its tag for the round-trip histogram
433
+ v.tagTimes[mem[ptr + 5] | (mem[ptr + 6] << 8)] = now()
434
+ }
435
+ let done = 0
436
+ while (done < len) {
437
+ if (broken()) return EPIPE
438
+ const seq = Atomics.load(i32, r.c + R_SSEQ)
439
+ views()
440
+ const n = ringWrite(v, r, mem, ptr + done, len - done)
441
+ if (n > 0) {
442
+ done += n
443
+ ringBell(v)
444
+ continue
445
+ }
446
+ if (ending()) return EPIPE
447
+ // the ring is full: the host is behind (counted, so a test can tell)
448
+ const tw = now()
449
+ Atomics.wait(i32, r.c + R_SSEQ, seq, WAIT_SLICE_MS)
450
+ v.f64[h * 4 + 2] += 1
451
+ v.f64[h * 4 + 3] += now() - tw
452
+ }
453
+ v.f64[h * 4] += len
454
+ return len
455
+ } finally {
456
+ unlock(i32, r.c + R_PLOCK)
457
+ }
458
+ }
459
+
460
+ function noteRx(h, kind, src, off, n) {
461
+ if (kind !== CHAN_P9) return
462
+ const gen = Atomics.load(i32, v.slotBase(h) + S_GEN)
463
+ let st = rx.get(h)
464
+ if (!st || st.gen !== gen) rx.set(h, (st = { gen, hdr: new Uint8Array(7), got: 0, left: 0, tag: 0 }))
465
+ let i = off
466
+ const end = off + n
467
+ while (i < end) {
468
+ if (st.left === 0) {
469
+ // in a header
470
+ st.hdr[st.got++] = src[i++]
471
+ if (st.got === 7) {
472
+ const size = (st.hdr[0] | (st.hdr[1] << 8) | (st.hdr[2] << 16) | (st.hdr[3] << 24)) >>> 0
473
+ st.tag = st.hdr[5] | (st.hdr[6] << 8)
474
+ st.got = 0
475
+ st.left = Math.max(0, size - 7)
476
+ if (st.left === 0) done(st.tag)
477
+ }
478
+ } else {
479
+ const k = Math.min(st.left, end - i)
480
+ i += k
481
+ st.left -= k
482
+ if (st.left === 0) done(st.tag)
483
+ }
484
+ }
485
+ function done(tag) {
486
+ const t0 = v.tagTimes[tag]
487
+ if (!(t0 > 0)) return
488
+ const t = now()
489
+ const tr = v.replyTimes[tag]
490
+ if (tr > 0) stageNote(v.readback, (t - tr) * 1000)
491
+ const us = (t - t0) * 1000
492
+ Atomics.add(v.rtt, rttBucket(us), 1)
493
+ const base = v.nslots * 4
494
+ v.f64[base] += us
495
+ v.f64[base + 1] += 1
496
+ if (us > v.f64[base + 2]) v.f64[base + 2] = us
497
+ }
498
+ }
499
+
500
+ function recv(h, ptr, len, timeoutNs) {
501
+ ptr >>>= 0
502
+ len >>>= 0
503
+ views()
504
+ if (ptr + len > mem.length) return EFAULT
505
+ if (!valid(h)) return EBADF
506
+ const b = v.slotBase(h)
507
+ const gen = Atomics.load(i32, b + S_GEN)
508
+ const kind = Atomics.load(i32, b + S_KIND)
509
+ const r = v.ring(h, H2G)
510
+ // D-419: a Number from the i64 shim (a BigInt from tests); no BigInt arithmetic per call
511
+ const t = Math.trunc(Number(timeoutNs) || 0)
512
+ const ms = t < 0 ? Infinity : Math.ceil(t / 1_000_000) // rounded up to ms, as chan.rs
513
+ const deadline = now() + ms
514
+ for (;;) {
515
+ const seq = Atomics.load(i32, r.c + R_DSEQ)
516
+ if (ringAvail(v, r) > 0) {
517
+ if (len === 0) return 0
518
+ if (!lock(i32, r.c + R_CLOCK, ending)) return -1
519
+ let n = 0
520
+ try {
521
+ views()
522
+ n = ringRead(v, r, mem, ptr, len)
523
+ } finally {
524
+ unlock(i32, r.c + R_CLOCK)
525
+ }
526
+ if (n > 0) {
527
+ ringBell(v) // the service may be waiting for room to write the rest of a reply
528
+ v.f64[h * 4 + 1] += n
529
+ noteRx(h, kind, mem, ptr, n)
530
+ return n
531
+ }
532
+ continue // another reader took it
533
+ }
534
+ if (
535
+ Atomics.load(i32, r.c + R_SHUT) !== 0 || Atomics.load(i32, r.c + R_GONE) !== 0 || dead() ||
536
+ Atomics.load(i32, b + S_STATE) !== ST_OPEN || Atomics.load(i32, b + S_GEN) !== gen || exiting() ||
537
+ Atomics.load(i32, H_GONE) !== 0
538
+ ) {
539
+ return -1
540
+ }
541
+ if (Atomics.exchange(i32, b + S_WAKE, 0) !== 0) return 0 // chan_wake
542
+ const rem = deadline - now()
543
+ if (rem <= 0) return 0
544
+ Atomics.wait(i32, r.c + R_DSEQ, seq, Math.min(rem, WAIT_SLICE_MS))
545
+ }
546
+ }
547
+
548
+ function wake(h) {
549
+ if (!valid(h)) return EBADF
550
+ Atomics.store(i32, v.slotBase(h) + S_WAKE, 1)
551
+ const r = v.ring(h, H2G)
552
+ Atomics.add(i32, r.c + R_DSEQ, 1)
553
+ Atomics.notify(i32, r.c + R_DSEQ)
554
+ return 0
555
+ }
556
+
557
+ function shutdown(h) {
558
+ if (!valid(h)) return EBADF
559
+ const r = v.ring(h, G2H)
560
+ Atomics.store(i32, r.c + R_SHUT, 1)
561
+ ringKick(v, r)
562
+ ringBell(v)
563
+ return 0
564
+ }
565
+
566
+ function close(h) {
567
+ if (!valid(h)) return EBADF
568
+ const b = v.slotBase(h)
569
+ if (Atomics.compareExchange(i32, b + S_STATE, ST_OPEN, ST_CLOSING) !== ST_OPEN) return EBADF
570
+ for (const dir of [G2H, H2G]) {
571
+ const r = v.ring(h, dir)
572
+ Atomics.store(i32, r.c + R_GONE, 1)
573
+ ringKick(v, r)
574
+ }
575
+ rx.delete(h)
576
+ Atomics.add(i32, H_CLOSES, 1)
577
+ ringBell(v)
578
+ return 0
579
+ }
580
+
581
+ return { open, send, recv, wake, shutdown, close }
582
+ }
583
+
584
+ /** Counters and the guest-side 9P round-trip histogram, for the page. */
585
+ export function chanCounters(sab) {
586
+ const v = chanView(sab)
587
+ const i32 = v.i32
588
+ const slots = []
589
+ for (let s = 0; s < v.nslots; s++) {
590
+ const b = v.slotBase(s)
591
+ const state = Atomics.load(i32, b + S_STATE)
592
+ const gen = Atomics.load(i32, b + S_GEN)
593
+ if (!gen) continue
594
+ slots.push({
595
+ handle: s,
596
+ state: ['free', 'open', 'closing', 'opening'][state],
597
+ kind: KIND_NAMES[Atomics.load(i32, b + S_KIND)] || '?',
598
+ port: Atomics.load(i32, b + S_PORT),
599
+ opens: gen,
600
+ bytesOut: v.f64[s * 4],
601
+ bytesIn: v.f64[s * 4 + 1],
602
+ sendWaits: v.f64[s * 4 + 2], // chan_send found the ring full and waited
603
+ sendWaitMs: Math.round(v.f64[s * 4 + 3] * 10) / 10,
604
+ })
605
+ }
606
+ const base = v.nslots * 4
607
+ return {
608
+ service: ['starting', 'running', 'dead'][Atomics.load(i32, H_SVC)],
609
+ opens: Atomics.load(i32, H_OPENS),
610
+ refused: Atomics.load(i32, H_REFUSED),
611
+ closes: Atomics.load(i32, H_CLOSES),
612
+ slots,
613
+ p9Rtt: histSummary(v.rtt, { meanUs: v.f64[base + 1] ? v.f64[base] / v.f64[base + 1] : 0, maxUs: v.f64[base + 2] }),
614
+ // W51: send -> the service parses it, and reply written -> the guest's chan_recv returns
615
+ p9Pickup: stageSummary(v.pickup),
616
+ p9Readback: stageSummary(v.readback),
617
+ }
618
+ }
619
+
620
+ /** Ports registered in the block: [{port, kind}]. */
621
+ export function chanPorts(sab) {
622
+ const i32 = new Int32Array(sab)
623
+ const out = []
624
+ for (let i = 0; i < i32[H_NPORTS]; i++) out.push({ port: i32[H_PORTS + 2 * i], kind: i32[H_PORTS + 2 * i + 1] })
625
+ return out
626
+ }
627
+
628
+ // ---- page side: console input with backpressure ------------------------------------------
629
+
630
+ /** Console input bytes the page may have queued for the guest that it has not taken yet. */
631
+ export const CONSOLE_IN_LIMIT = 4 << 20
632
+
633
+ /**
634
+ * The page's side of console input (page -> guest). The page posts bytes to the service
635
+ * Worker; the service counts in H_CIN_TAKEN the bytes it has put into the guest's ring.
636
+ * Bytes posted and not yet taken (in flight, or queued in the service) never exceed
637
+ * `limit`: take(n) says how many of n bytes may be posted now (0 when the guest has not
638
+ * read what is already queued). Counters are mod 2^32, so `limit` must stay far below it.
639
+ */
640
+ export function consoleInputGate(sab, limit = CONSOLE_IN_LIMIT) {
641
+ const i32 = new Int32Array(sab)
642
+ let sent = 0
643
+ const queued = () => ((sent >>> 0) - (Atomics.load(i32, H_CIN_TAKEN) >>> 0)) >>> 0
644
+ const space = () => Math.max(0, limit - queued())
645
+ return {
646
+ limit,
647
+ queued,
648
+ space,
649
+ take(n) {
650
+ const k = Math.min(n, space())
651
+ sent += k
652
+ return k
653
+ },
654
+ }
655
+ }