@threadedstack/wmd-web 0.0.0-stage → 0.1.12
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 +2601 -2
- package/RELEASE.json +123 -0
- package/export-page/coi-sw.js +20 -0
- package/export-page/index.html +17 -0
- package/export-page/run.css +10 -0
- package/export-page/run.js +236 -0
- package/guest/arm64/Image +4 -0
- package/guest/arm64/Image-4k +4 -0
- package/guest/arm64/initramfs.cpio.gz +0 -0
- package/guest/arm64/vm.wasm +0 -0
- package/package.json +36 -4
- package/page/sw.js +381 -0
- package/relay/control.ts +139 -0
- package/relay/engine.ts +787 -0
- package/relay/hosted.ts +660 -0
- package/relay/keys.ts +53 -0
- package/relay/local-tls.ts +121 -0
- package/relay/lock.ts +81 -0
- package/relay/logfile.ts +138 -0
- package/relay/main.ts +642 -0
- package/relay/policy.ts +241 -0
- package/relay/relay.ts +266 -0
- package/relay/token.ts +116 -0
- package/relay/v1.ts +107 -0
- package/relay/wisp.ts +615 -0
- package/src/base.js +325 -0
- package/src/boot.js +2037 -0
- package/src/calib-worker.js +40 -0
- package/src/fs/9p/codec.js +303 -0
- package/src/fs/9p/server.js +521 -0
- package/src/fs/errno.js +43 -0
- package/src/fs/graph.js +380 -0
- package/src/fs/overlay.js +1018 -0
- package/src/fs/pack-write.js +165 -0
- package/src/fs/pack.js +427 -0
- package/src/fs/seed.js +22 -0
- package/src/fs/svc-snapshot.js +65 -0
- package/src/fs/upper-pack.js +112 -0
- package/src/host/base-cache.js +195 -0
- package/src/host/base-file.js +328 -0
- package/src/host/base-io.js +93 -0
- package/src/host/boot-events.js +209 -0
- package/src/host/chan-service.js +300 -0
- package/src/host/chan-services.js +689 -0
- package/src/host/chan.js +655 -0
- package/src/host/ckpt-file.js +395 -0
- package/src/host/ckpt-io.js +207 -0
- package/src/host/ckpt-point.js +160 -0
- package/src/host/ckpt-save.js +64 -0
- package/src/host/ckpt.js +434 -0
- package/src/host/clock.js +111 -0
- package/src/host/ctl.js +778 -0
- package/src/host/errno.js +18 -0
- package/src/host/fs.js +167 -0
- package/src/host/gcnudge.js +83 -0
- package/src/host/i64shim.js +81 -0
- package/src/host/jit-cache.js +341 -0
- package/src/host/jit-hub-core.js +208 -0
- package/src/host/jit-hub.js +93 -0
- package/src/host/jit-snap.js +226 -0
- package/src/host/jit.js +1903 -0
- package/src/host/layout.js +100 -0
- package/src/host/monitor.js +110 -0
- package/src/host/p9mount.js +27 -0
- package/src/host/ram-mem.js +125 -0
- package/src/host/stdio.js +143 -0
- package/src/host/threads.js +109 -0
- package/src/host/wasi.js +335 -0
- package/src/host/wmd-imports.js +71 -0
- package/src/host/yamux.js +387 -0
- package/src/image-files.js +154 -0
- package/src/net/dhcp-dns.js +126 -0
- package/src/net/net-worker.js +172 -0
- package/src/net/packet.js +227 -0
- package/src/net/peer.js +230 -0
- package/src/net/proto.js +189 -0
- package/src/net/relay-client.js +226 -0
- package/src/net/stack.js +867 -0
- package/src/net/switch.js +160 -0
- package/src/net/wisp-client.js +570 -0
- package/src/net/wisp-transport.js +196 -0
- package/src/net/wisp.js +511 -0
- package/src/registry/client.js +176 -0
- package/src/registry/converter.js +68 -0
- package/src/registry/hub.js +221 -0
- package/src/registry/oci.js +76 -0
- package/src/registry/pack-worker.js +70 -0
- package/src/registry/pull.js +258 -0
- package/src/registry/ref.js +110 -0
- package/src/registry/wasi-mem.js +355 -0
- package/src/worker.js +315 -0
- package/wmdpack.wasm +0 -0
package/src/host/chan.js
ADDED
|
@@ -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
|
+
}
|