@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.
- 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
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Layout of the control SharedArrayBuffer every Worker of one wmd machine shares.
|
|
2
|
+
//
|
|
3
|
+
// All cells are Int32 so Atomics.wait/notify work on them. The control block is
|
|
4
|
+
// tiny and created once per boot by boot.js; Workers get it in their init message
|
|
5
|
+
// and never use postMessage for anything that has to be synchronous (thread spawn,
|
|
6
|
+
// the fd table, the clock base, the exit flag).
|
|
7
|
+
|
|
8
|
+
export const CTL_NEXT_TID = 0 // last tid handed out; the _start thread is tid 1, first spawn gets 2
|
|
9
|
+
export const CTL_EXIT = 1 // 0 running, 1 proc_exit called (or a thread trapped)
|
|
10
|
+
export const CTL_EXIT_CODE = 2
|
|
11
|
+
export const CTL_POOL_GEN = 3 // bumped by the page every time pool slots are added
|
|
12
|
+
export const CTL_FD_LOCK = 4 // spin lock over the fd table
|
|
13
|
+
export const CTL_POOL_N = 5 // number of pool slots in use (registered Workers)
|
|
14
|
+
export const CTL_SPAWN_FAIL = 6 // count of thread-spawn calls that found no idle Worker
|
|
15
|
+
export const CTL_LIVE_THREADS = 7 // spawned threads currently running
|
|
16
|
+
export const CTL_TRAPS = 8 // threads that trapped (W21 review A2: a trap after an exit 0 still fails the run)
|
|
17
|
+
// P-B8 test hook (bootWmd({stallHook: true}), vm.stallThread): the thread with tid CTL_STALL_TID
|
|
18
|
+
// blocks for CTL_STALL_MS at its next host call, as if the browser had stopped its Worker.
|
|
19
|
+
export const CTL_STALL_TID = 9 // 0: none armed
|
|
20
|
+
export const CTL_STALL_MS = 10
|
|
21
|
+
export const CTL_STALL_TAKEN = 11 // stalls started
|
|
22
|
+
export const CTL_STALL_DONE = 12 // stalls ended
|
|
23
|
+
export const CTL_STALL_WAKE = 13 // the stalled thread waits on this cell; bumped to end a stall early
|
|
24
|
+
export const CTL_MONO_LOCK = 14 // spin lock over F64_LAST_MONO (clock.js)
|
|
25
|
+
|
|
26
|
+
// Float64 cells (byte offsets, 8-aligned) inside the same buffer.
|
|
27
|
+
export const F64_START_MS = 64 // absolute ms (timeOrigin + now) when the machine started
|
|
28
|
+
export const F64_LAST_MONO = 72 // highest monotonic ns any Worker has returned (a Number, under CTL_MONO_LOCK)
|
|
29
|
+
|
|
30
|
+
// Counters of calls to the stub wmd.* imports (wmd-imports.js), one Int32 cell per
|
|
31
|
+
// name. A guest in --mode jit calls jit_compile thousands of times a second: counting
|
|
32
|
+
// in shared memory instead of one postMessage per call keeps the page responsive. The
|
|
33
|
+
// page reads them on demand (vm.stats().hostCalls) and in a batched event.
|
|
34
|
+
export const HOST_CALL_NAMES = ['jit_load', 'jit_compile', 'jit_known', 'jit_sync', 'jit_note', 'jit_flush', 'snapshot', 'chan_open']
|
|
35
|
+
export const HOST_CALL_BASE = 20 // Int32 cells 20..27 (bytes 80..111, between F64_LAST_MONO and the pool)
|
|
36
|
+
// P-B8 stall hook: when the last stall started and ended (absolute ms, Float64; bytes 112..127)
|
|
37
|
+
export const F64_STALL_AT = 112
|
|
38
|
+
export const F64_STALL_END = 120
|
|
39
|
+
const HOST_CALL_INDEX = new Map(HOST_CALL_NAMES.map((n, i) => [n, HOST_CALL_BASE + i]))
|
|
40
|
+
|
|
41
|
+
export function noteHostCall(ctl, name) {
|
|
42
|
+
const i = HOST_CALL_INDEX.get(name)
|
|
43
|
+
if (i !== undefined) Atomics.add(ctl, i, 1)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** {name: count} of the stub calls made so far (only names called at least once). */
|
|
47
|
+
export function hostCallCounts(ctl) {
|
|
48
|
+
const out = {}
|
|
49
|
+
for (const [n, i] of HOST_CALL_INDEX) {
|
|
50
|
+
const c = Atomics.load(ctl, i)
|
|
51
|
+
if (c) out[n] = c
|
|
52
|
+
}
|
|
53
|
+
return out
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Pool slots: SLOT_INTS Int32 cells each.
|
|
57
|
+
export const POOL_BASE = 32
|
|
58
|
+
export const POOL_MAX = 128
|
|
59
|
+
export const SLOT_INTS = 4
|
|
60
|
+
export const SLOT_STATE = 0
|
|
61
|
+
export const SLOT_TID = 1
|
|
62
|
+
export const SLOT_ARG = 2
|
|
63
|
+
export const SLOT_RUNS = 3 // how many threads this Worker has run
|
|
64
|
+
|
|
65
|
+
export const ST_IDLE = 0 // Worker is waiting for work
|
|
66
|
+
export const ST_CLAIMED = 1 // a spawner owns the slot and is filling tid/arg
|
|
67
|
+
export const ST_GO = 2 // tid/arg written; Worker should start
|
|
68
|
+
export const ST_RUNNING = 3
|
|
69
|
+
export const ST_DEAD = 4 // Worker failed to start or was terminated
|
|
70
|
+
export const ST_STARTING = 5 // slot assigned, Worker not parked yet
|
|
71
|
+
|
|
72
|
+
// The process-wide WASI fd table: FD_MAX entries of FD_INTS cells.
|
|
73
|
+
export const FD_BASE = POOL_BASE + POOL_MAX * SLOT_INTS
|
|
74
|
+
export const FD_MAX = 64
|
|
75
|
+
export const FD_INTS = 4
|
|
76
|
+
export const FD_KIND = 0 // 0 free, 1 regular file (fds 0-2 are stdio, 3 the preopen; not stored)
|
|
77
|
+
export const FD_FILE = 1 // index into the file list
|
|
78
|
+
export const FD_POS_LO = 2
|
|
79
|
+
export const FD_POS_HI = 3
|
|
80
|
+
|
|
81
|
+
// P-B9 (checkpoints): after the fd table.
|
|
82
|
+
export const CKPT_BASE = FD_BASE + FD_MAX * FD_INTS
|
|
83
|
+
export const CTL_CK_HOSTREQ = CKPT_BASE // guest address of the machine's host_req word (wmd.ctl_attach); 0: not attached yet
|
|
84
|
+
export const CTL_CK_ATTACHES = CKPT_BASE + 1 // wmd.ctl_attach calls so far (one more after each checkpoint: the vCPUs run again)
|
|
85
|
+
export const CTL_CK_GO = CKPT_BASE + 2 // page -> vCPU 0 inside wmd.point: 0 wait, 1 return 0 (taken), 2 return -1 (failed)
|
|
86
|
+
export const CTL_CK_POINTS = CKPT_BASE + 3 // wmd.point calls so far
|
|
87
|
+
export const CTL_CK_RESTORED = CKPT_BASE + 4 // restore: the page has written the checkpoint's memory; the main thread may call vm_resume (1)
|
|
88
|
+
|
|
89
|
+
export const CTL_INTS = CKPT_BASE + 8
|
|
90
|
+
export const CTL_BYTES = CTL_INTS * 4
|
|
91
|
+
|
|
92
|
+
export const slotIndex = (slot, field) => POOL_BASE + slot * SLOT_INTS + field
|
|
93
|
+
export const fdIndex = (fd, field) => FD_BASE + fd * FD_INTS + field
|
|
94
|
+
|
|
95
|
+
export function createControl() {
|
|
96
|
+
const sab = new SharedArrayBuffer(CTL_BYTES)
|
|
97
|
+
const ctl = new Int32Array(sab)
|
|
98
|
+
ctl[CTL_NEXT_TID] = 1
|
|
99
|
+
return sab
|
|
100
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Page-side monitors of a running machine (no wasm, no Workers; unit-tested):
|
|
2
|
+
//
|
|
3
|
+
// createPanicScanner finds "Kernel panic - not syncing" in the console, so a panic
|
|
4
|
+
// the guest maps to exit 0 (PSCI SYSTEM_RESET) is reported as a failure.
|
|
5
|
+
// createWatchdog "the guest made no progress": no console output and no new
|
|
6
|
+
// threads for limitMs after the vCPU started.
|
|
7
|
+
// createThrottleMeter compares a fixed calibration loop (run in its own Worker,
|
|
8
|
+
// calib-worker.js) against the fastest run seen: when the browser
|
|
9
|
+
// deprioritises the page's process (a minimised window), every
|
|
10
|
+
// Worker slows down and so does the guest.
|
|
11
|
+
|
|
12
|
+
const PANIC_RE = /Kernel panic - not syncing/
|
|
13
|
+
|
|
14
|
+
export function createPanicScanner() {
|
|
15
|
+
let buf = ''
|
|
16
|
+
const s = {
|
|
17
|
+
line: null,
|
|
18
|
+
/** Feed console text; returns the panic line the first time one completes. */
|
|
19
|
+
push(text) {
|
|
20
|
+
if (s.line) return null
|
|
21
|
+
buf += text
|
|
22
|
+
let nl
|
|
23
|
+
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
24
|
+
const l = buf.slice(0, nl).replace(/\r$/, '')
|
|
25
|
+
buf = buf.slice(nl + 1)
|
|
26
|
+
if (PANIC_RE.test(l)) {
|
|
27
|
+
s.line = l
|
|
28
|
+
buf = ''
|
|
29
|
+
return l
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (buf.length > 4096) buf = buf.slice(-256) // a very long line: keep a tail only
|
|
33
|
+
return null
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
return s
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Default no-progress limit for a boot's argv. Interpreter boots of the arm64 kernel
|
|
41
|
+
* write to the console at least every few seconds; the amd64 decompressor is silent for
|
|
42
|
+
* about 75 s in Chromium (more in Firefox), so x86 gets 300 s. User-mode programs may
|
|
43
|
+
* legitimately run silent: off.
|
|
44
|
+
*/
|
|
45
|
+
export function watchdogDefaultMs(args) {
|
|
46
|
+
if (!args.includes('--machine')) return 0
|
|
47
|
+
const i = args.indexOf('--arch')
|
|
48
|
+
return i >= 0 && args[i + 1] === 'x86' ? 300_000 : 60_000
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {{limitMs:number, tickMs?:number, now?:()=>number}} o
|
|
53
|
+
* check(ratio) is called every tickMs; returns true once nothing has progressed for
|
|
54
|
+
* limitMs * max(1, ratio). A check that comes much later than tickMs means the page's
|
|
55
|
+
* own thread was stalled (messages from the Workers were not delivered either), so
|
|
56
|
+
* the silence count restarts.
|
|
57
|
+
*/
|
|
58
|
+
export function createWatchdog({ limitMs, tickMs = 250, now = () => performance.now() }) {
|
|
59
|
+
let armed = false
|
|
60
|
+
let last = 0
|
|
61
|
+
let lastCheck = 0
|
|
62
|
+
return {
|
|
63
|
+
start() {
|
|
64
|
+
armed = limitMs > 0
|
|
65
|
+
last = lastCheck = now()
|
|
66
|
+
},
|
|
67
|
+
progress() {
|
|
68
|
+
last = now()
|
|
69
|
+
},
|
|
70
|
+
idleMs: () => now() - last,
|
|
71
|
+
check(ratio = 1) {
|
|
72
|
+
if (!armed) return false
|
|
73
|
+
const t = now()
|
|
74
|
+
const gap = t - lastCheck
|
|
75
|
+
lastCheck = t
|
|
76
|
+
if (gap > 4 * tickMs + 1000) last = t // page stalled: give the guest the benefit of the doubt
|
|
77
|
+
return t - last > limitMs * Math.max(1, ratio)
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {{threshold?:number, window?:number, baselineMs?:number}} o
|
|
84
|
+
* sample(ms) takes one calibration time. ratio = median of the last `window` samples /
|
|
85
|
+
* the best (lowest) time seen, including an optional baseline from an earlier run.
|
|
86
|
+
* Returns {changed, ...state}; state().throttled flips when the ratio crosses threshold.
|
|
87
|
+
*/
|
|
88
|
+
export function createThrottleMeter({ threshold = 2, window = 3, baselineMs } = {}) {
|
|
89
|
+
let best = baselineMs > 0 ? baselineMs : Infinity
|
|
90
|
+
const recent = []
|
|
91
|
+
const st = { throttled: false, ratio: 1, maxRatio: 1, sampleMs: null, baselineMs: null, samples: 0, throttledSamples: 0 }
|
|
92
|
+
return {
|
|
93
|
+
sample(ms) {
|
|
94
|
+
best = Math.min(best, ms)
|
|
95
|
+
recent.push(ms)
|
|
96
|
+
if (recent.length > window) recent.shift()
|
|
97
|
+
const med = [...recent].sort((a, b) => a - b)[recent.length >> 1]
|
|
98
|
+
st.samples++
|
|
99
|
+
st.sampleMs = ms
|
|
100
|
+
st.baselineMs = best
|
|
101
|
+
st.ratio = Math.round((med / best) * 100) / 100
|
|
102
|
+
st.maxRatio = Math.max(st.maxRatio, st.ratio)
|
|
103
|
+
const was = st.throttled
|
|
104
|
+
st.throttled = recent.length >= Math.min(window, 2) && st.ratio >= threshold
|
|
105
|
+
if (st.throttled) st.throttledSamples++
|
|
106
|
+
return { changed: was !== st.throttled, ...st }
|
|
107
|
+
},
|
|
108
|
+
state: () => ({ ...st }),
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// The 9P mount options the browser guest uses for its root (P-B3/P-B4 adversarial F1).
|
|
2
|
+
//
|
|
3
|
+
// cache=mmap, not the native engine's cache=loose. Linux 6.1's v9fs with cache=loose
|
|
4
|
+
// writes into the page cache and sends the data later, one 4 KiB page per Twrite
|
|
5
|
+
// (fs/9p/vfs_addr.c v9fs_vfs_writepage). When the server refuses that write (the upper's
|
|
6
|
+
// memory cap: ENOSPC, or any other error but EAGAIN) the page is dropped, the error is
|
|
7
|
+
// only recorded for a later fsync(2)/close(2) (mapping_set_error), and write(2) never
|
|
8
|
+
// fails: a writer that does not fsync loses its data silently, and one past the dirty
|
|
9
|
+
// limit sits in balance_dirty_pages while writeback drains the failing pages one round
|
|
10
|
+
// at a time. The cap therefore cannot be enforced at write(2) under cache=loose. With
|
|
11
|
+
// cache=mmap (and cache=none) write(2) is a Twrite of up to msize bytes
|
|
12
|
+
// (v9fs_file_write_iter), so ENOSPC reaches the writer at once; mmap keeps shared
|
|
13
|
+
// writable mappings working (cache=none refuses them with EINVAL, wmd D-062).
|
|
14
|
+
// The measurements behind the choice are in README "P-B3/P-B4 adversarial review".
|
|
15
|
+
|
|
16
|
+
export const P9_CACHE_DEFAULT = 'mmap'
|
|
17
|
+
export const P9_MSIZE_DEFAULT = 262144
|
|
18
|
+
|
|
19
|
+
/** "trans=virtio,version=9p2000.L,msize=...,cache=..." for mount -t 9p. */
|
|
20
|
+
export function p9MountOptions({ msize = P9_MSIZE_DEFAULT, cache = P9_CACHE_DEFAULT } = {}) {
|
|
21
|
+
return `trans=virtio,version=9p2000.L,msize=${msize},cache=${cache}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Kernel command line words for wmd's Go init (cmdline.go wmd.p9msize / wmd.p9cache). */
|
|
25
|
+
export function p9InitCmdline({ msize = P9_MSIZE_DEFAULT, cache = P9_CACHE_DEFAULT } = {}) {
|
|
26
|
+
return `wmd.p9msize=${msize} wmd.p9cache=${cache}`
|
|
27
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Guest RAM outside memory 0: the flat layout (guest/vm/include/vm/ram.h).
|
|
2
|
+
//
|
|
3
|
+
// vm.wasm's memory 0 is a wasm32 memory: 4 GiB at most, for guest RAM and the emulator's own
|
|
4
|
+
// heap, so a browser guest stopped at 1536 MiB (boot.js MAX_BROWSER_RAM_MIB). With a memory64
|
|
5
|
+
// the page makes a second shared memory that holds all of guest RAM. Translated code imports it
|
|
6
|
+
// as env.win and reads it with 64-bit addresses; vm.wasm's C code, which can only address memory
|
|
7
|
+
// 0, reaches it through wmd.ram_rw and wmd.ram_cas. Those two are exports of a small wasm module
|
|
8
|
+
// made here (memory.copy between the two memories, a compare-and-swap on the RAM memory): a
|
|
9
|
+
// wasm-to-wasm call, so no i64 argument becomes a BigInt (D-419) and a page-table walk pays a
|
|
10
|
+
// call, not a trip through JS.
|
|
11
|
+
|
|
12
|
+
const PAGE = 65536
|
|
13
|
+
|
|
14
|
+
/** Whether this engine makes a shared 64-bit memory (Chrome 133, Firefox 134; not Safari). */
|
|
15
|
+
export function haveMemory64() {
|
|
16
|
+
try {
|
|
17
|
+
const m = new WebAssembly.Memory({ address: 'i64', initial: 1n, maximum: 1n, shared: true })
|
|
18
|
+
return m.buffer instanceof SharedArrayBuffer && m.buffer.byteLength === PAGE
|
|
19
|
+
} catch {
|
|
20
|
+
return false
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The shared memory64 that holds a guest's RAM of `mib` MiB. Throws when the engine cannot make it. */
|
|
25
|
+
export function createRamMemory(mib) {
|
|
26
|
+
const pages = BigInt(mib) * 16n
|
|
27
|
+
try {
|
|
28
|
+
return new WebAssembly.Memory({ address: 'i64', initial: pages, maximum: pages, shared: true })
|
|
29
|
+
} catch (err) {
|
|
30
|
+
throw new Error(`--ram ${mib}: the browser could not make a ${mib} MiB memory for the guest (${err && err.message ? err.message : err})`)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const leb = n => {
|
|
35
|
+
// unsigned LEB128 of a Number (exact to 2^53)
|
|
36
|
+
const o = []
|
|
37
|
+
do {
|
|
38
|
+
let b = n % 128
|
|
39
|
+
n = Math.floor(n / 128)
|
|
40
|
+
if (n) b |= 0x80
|
|
41
|
+
o.push(b)
|
|
42
|
+
} while (n)
|
|
43
|
+
return o
|
|
44
|
+
}
|
|
45
|
+
const vec = items => [...leb(items.length), ...items.flat()]
|
|
46
|
+
const str = s => vec([...new TextEncoder().encode(s)].map(b => [b]))
|
|
47
|
+
const sec = (id, body) => [id, ...leb(body.length), ...body]
|
|
48
|
+
const I32 = 0x7f
|
|
49
|
+
const I64 = 0x7e
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The helper module's bytes, for a RAM memory of `pages` 64 KiB pages:
|
|
53
|
+
* (import "env" "memory" (memory 0 65536 shared))
|
|
54
|
+
* (import "env" "ram" (memory i64 pages pages shared))
|
|
55
|
+
* (func (export "ram_rw") (param $off i64) (param $ptr i32) (param $len i32) (param $write i32) (result i32))
|
|
56
|
+
* (func (export "ram_cas") (param $off i64) (param $size i32) (param $expect i64) (param $desired i64) (result i64))
|
|
57
|
+
*/
|
|
58
|
+
export function ramHelperBytes(pages) {
|
|
59
|
+
const get = i => [0x20, i]
|
|
60
|
+
const copy = (dst, src) => [0xfc, 0x0a, dst, src]
|
|
61
|
+
const rw = [
|
|
62
|
+
0x00, // no locals
|
|
63
|
+
...get(3), 0x04, 0x40, // if write
|
|
64
|
+
...get(0), ...get(1), ...get(2), ...copy(1, 0),
|
|
65
|
+
0x05, // else
|
|
66
|
+
...get(1), ...get(0), ...get(2), ...copy(0, 1),
|
|
67
|
+
0x0b,
|
|
68
|
+
0x41, 0x00, // i32.const 0
|
|
69
|
+
0x0b,
|
|
70
|
+
]
|
|
71
|
+
// cmpxchg on memory 1: opcode, then memarg (align | 0x40: a memory index follows), index, offset
|
|
72
|
+
const cx = (op, align) => [...get(0), ...get(2), ...get(3), 0xfe, op, align | 0x40, 0x01, 0x00]
|
|
73
|
+
const is = n => [...get(1), 0x41, n, 0x46, 0x04, I64] // if (size == n) (result i64)
|
|
74
|
+
const cas = [
|
|
75
|
+
0x00,
|
|
76
|
+
...is(8), ...cx(0x49, 3),
|
|
77
|
+
0x05, ...is(4), ...cx(0x4e, 2),
|
|
78
|
+
0x05, ...is(2), ...cx(0x4d, 1),
|
|
79
|
+
0x05, ...cx(0x4c, 0),
|
|
80
|
+
0x0b, 0x0b, 0x0b,
|
|
81
|
+
0x0b,
|
|
82
|
+
]
|
|
83
|
+
const body = b => [...leb(b.length), ...b]
|
|
84
|
+
return new Uint8Array([
|
|
85
|
+
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
|
|
86
|
+
...sec(1, vec([
|
|
87
|
+
[0x60, ...vec([[I64], [I32], [I32], [I32]]), ...vec([[I32]])],
|
|
88
|
+
[0x60, ...vec([[I64], [I32], [I64], [I64]]), ...vec([[I64]])],
|
|
89
|
+
])),
|
|
90
|
+
...sec(2, vec([
|
|
91
|
+
[...str('env'), ...str('memory'), 0x02, 0x03, ...leb(0), ...leb(65536)],
|
|
92
|
+
[...str('env'), ...str('ram'), 0x02, 0x07, ...leb(pages), ...leb(pages)],
|
|
93
|
+
])),
|
|
94
|
+
...sec(3, vec([[0], [1]])),
|
|
95
|
+
...sec(7, vec([
|
|
96
|
+
[...str('ram_rw'), 0x00, 0],
|
|
97
|
+
[...str('ram_cas'), 0x00, 1],
|
|
98
|
+
])),
|
|
99
|
+
...sec(10, vec([body(rw), body(cas)])),
|
|
100
|
+
])
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let helperModule = null // one compile per thread; the page size is in the bytes
|
|
104
|
+
let helperPages = -1
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* vm.wasm's wmd.ram_map2, wmd.ram_rw and wmd.ram_cas for a machine whose RAM is `ram` (a shared
|
|
108
|
+
* memory64, createRamMemory) and whose memory 0 is `memory`. ram_rw and ram_cas are wasm
|
|
109
|
+
* functions: hand them to vm.wasm as they are, not through a JS wrapper.
|
|
110
|
+
*/
|
|
111
|
+
export function createRamImports({ memory, ram }) {
|
|
112
|
+
const bytes = ram.buffer.byteLength
|
|
113
|
+
const pages = bytes / PAGE
|
|
114
|
+
if (!helperModule || helperPages !== pages) {
|
|
115
|
+
helperModule = new WebAssembly.Module(ramHelperBytes(pages))
|
|
116
|
+
helperPages = pages
|
|
117
|
+
}
|
|
118
|
+
const { exports } = new WebAssembly.Instance(helperModule, { env: { memory, ram } })
|
|
119
|
+
return {
|
|
120
|
+
// 1: the RAM memory exists and memory 0 does not show it (the flat layout); -1: a size vm.wasm did not get
|
|
121
|
+
ram_map2: (_off, _lowLen, total) => (Number(total) === bytes ? 1 : -1),
|
|
122
|
+
ram_rw: exports.ram_rw,
|
|
123
|
+
ram_cas: exports.ram_cas,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// stdout/stderr from every Worker to the page through one SharedArrayBuffer: a byte ring
|
|
2
|
+
// per fd. Writers (any thread, inside fd_write) copy under a spin lock and move the
|
|
3
|
+
// fd's write position; the page drains on a timer. No postMessage per write: a guest
|
|
4
|
+
// that writes megabytes a second (vm.wasm dumping the IR of every block whose compile
|
|
5
|
+
// failed) cannot flood the page's event loop. If the page falls a full ring behind, it
|
|
6
|
+
// skips to the newest ring's worth of bytes and counts what it skipped; stdioText()
|
|
7
|
+
// (what bootWmd delivers) then puts a visible marker line into the text at that point.
|
|
8
|
+
//
|
|
9
|
+
// The ring size is a power of two per fd, set by createStdio(ringBytes) and stored in
|
|
10
|
+
// the header, so writers and the reader agree on it. RING (4 MiB) is the library
|
|
11
|
+
// default; bootWmd uses BOOT_RING (32 MiB, see there).
|
|
12
|
+
|
|
13
|
+
export const RING = 1 << 22 // default bytes per fd for createStdio()
|
|
14
|
+
// bootWmd's default. The guest console runs at about 0.33 MB/s in JIT mode, so 32 MiB
|
|
15
|
+
// covers a page that serves no events for about 100 s: a 10-25 s main-thread stall, or
|
|
16
|
+
// Chromium's intensive throttling of hidden tabs (timers run once a minute after 5
|
|
17
|
+
// minutes hidden). It costs 64 MiB of SharedArrayBuffer (pages are touched only as
|
|
18
|
+
// the ring fills), against the guest's 640 MiB shared memory.
|
|
19
|
+
export const BOOT_RING = 1 << 25
|
|
20
|
+
const H_LOCK = 0
|
|
21
|
+
const H_W1 = 1 // bytes ever written to fd 1, mod 2^32
|
|
22
|
+
const H_W2 = 2 // ... fd 2
|
|
23
|
+
const H_RING = 3 // ring bytes per fd
|
|
24
|
+
const F_FIRST = 2 // Float64 index (byte 16): absolute ms of the first stdout byte
|
|
25
|
+
const HDR = 32
|
|
26
|
+
|
|
27
|
+
/** The text put into a delivered stream where `n` bytes were skipped. */
|
|
28
|
+
export const dropMarker = (n, fd) => `\n[wmd-web: ${n} bytes of ${fd === 2 ? 'stderr' : 'stdout'} were dropped]\n`
|
|
29
|
+
/** Matches a dropMarker line (boot gates fail on it). */
|
|
30
|
+
export const DROP_MARKER_RE = /\[wmd-web: (\d+) bytes of (stdout|stderr) were dropped\]/
|
|
31
|
+
|
|
32
|
+
export function createStdio(ringBytes = RING) {
|
|
33
|
+
if (!(ringBytes >= 4096 && ringBytes <= 1 << 30 && (ringBytes & (ringBytes - 1)) === 0)) {
|
|
34
|
+
throw new Error(`stdio ring size must be a power of two from 4 KiB to 1 GiB, got ${ringBytes}`)
|
|
35
|
+
}
|
|
36
|
+
const sab = new SharedArrayBuffer(HDR + 2 * ringBytes)
|
|
37
|
+
new Int32Array(sab, 0, HDR / 4)[H_RING] = ringBytes
|
|
38
|
+
return sab
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const ringOf = sab => new Int32Array(sab, 0, HDR / 4)[H_RING] || RING
|
|
42
|
+
|
|
43
|
+
/** Writer side (a Worker): returns write(fd, bytes) for fds 1 and 2. */
|
|
44
|
+
export function stdioWriter(sab, now = () => performance.timeOrigin + performance.now()) {
|
|
45
|
+
const hdr = new Int32Array(sab, 0, HDR / 4)
|
|
46
|
+
const f64 = new Float64Array(sab, 0, HDR / 8)
|
|
47
|
+
const RING = ringOf(sab)
|
|
48
|
+
const rings = { 1: new Uint8Array(sab, HDR, RING), 2: new Uint8Array(sab, HDR + RING, RING) }
|
|
49
|
+
return function write(fd, bytes) {
|
|
50
|
+
const ring = rings[fd]
|
|
51
|
+
if (!ring || !bytes.length) return
|
|
52
|
+
const src = bytes.length > RING ? bytes.subarray(bytes.length - RING) : bytes
|
|
53
|
+
const skipped = bytes.length - src.length
|
|
54
|
+
while (Atomics.compareExchange(hdr, H_LOCK, 0, 1) !== 0) {}
|
|
55
|
+
try {
|
|
56
|
+
const cell = fd === 1 ? H_W1 : H_W2
|
|
57
|
+
const w = (Atomics.load(hdr, cell) >>> 0) + skipped
|
|
58
|
+
const at = w % RING
|
|
59
|
+
const first = Math.min(src.length, RING - at)
|
|
60
|
+
ring.set(src.subarray(0, first), at)
|
|
61
|
+
if (first < src.length) ring.set(src.subarray(first), 0)
|
|
62
|
+
if (fd === 1 && f64[F_FIRST] === 0) f64[F_FIRST] = now()
|
|
63
|
+
Atomics.store(hdr, cell, (w + src.length) | 0)
|
|
64
|
+
} finally {
|
|
65
|
+
Atomics.store(hdr, H_LOCK, 0)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Reader side (the page): drain() returns {1: Uint8Array, 2: Uint8Array} of new bytes. */
|
|
71
|
+
export function stdioReader(sab) {
|
|
72
|
+
const hdr = new Int32Array(sab, 0, HDR / 4)
|
|
73
|
+
const f64 = new Float64Array(sab, 0, HDR / 8)
|
|
74
|
+
const RING = ringOf(sab)
|
|
75
|
+
const rings = { 1: new Uint8Array(sab, HDR, RING), 2: new Uint8Array(sab, HDR + RING, RING) }
|
|
76
|
+
const read = { 1: 0, 2: 0 }
|
|
77
|
+
const dropped = { 1: 0, 2: 0 }
|
|
78
|
+
function take(fd) {
|
|
79
|
+
const w = Atomics.load(hdr, fd === 1 ? H_W1 : H_W2) >>> 0
|
|
80
|
+
let avail = (w - read[fd]) >>> 0
|
|
81
|
+
if (!avail) return null
|
|
82
|
+
if (avail > RING) {
|
|
83
|
+
dropped[fd] += avail - RING
|
|
84
|
+
avail = RING
|
|
85
|
+
}
|
|
86
|
+
const start = (w - avail) >>> 0
|
|
87
|
+
const at = start % RING
|
|
88
|
+
const out = new Uint8Array(avail)
|
|
89
|
+
const first = Math.min(avail, RING - at)
|
|
90
|
+
out.set(rings[fd].subarray(at, at + first), 0)
|
|
91
|
+
if (first < avail) out.set(rings[fd].subarray(0, avail - first), first)
|
|
92
|
+
// a writer may have wrapped over what we just copied: re-check and drop that part
|
|
93
|
+
const w2 = Atomics.load(hdr, fd === 1 ? H_W1 : H_W2) >>> 0
|
|
94
|
+
const over = ((w2 - start) >>> 0) - RING
|
|
95
|
+
read[fd] = w
|
|
96
|
+
if (over >= avail) {
|
|
97
|
+
dropped[fd] += avail
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
if (over > 0) {
|
|
101
|
+
dropped[fd] += over
|
|
102
|
+
return out.subarray(over)
|
|
103
|
+
}
|
|
104
|
+
return out
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
drain: () => ({ 1: take(1), 2: take(2) }),
|
|
108
|
+
firstStdoutAt: () => f64[F_FIRST] || undefined,
|
|
109
|
+
dropped,
|
|
110
|
+
ringBytes: RING,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Text side of a reader (what bootWmd delivers): drain() returns {1: string, 2: string}
|
|
116
|
+
* decoded with streaming TextDecoders. Where the reader skipped bytes, the text has a
|
|
117
|
+
* dropMarker line at that point, so the loss is visible in the stream itself (a gate
|
|
118
|
+
* line can never vanish silently). `skips` counts the marker lines per fd.
|
|
119
|
+
*/
|
|
120
|
+
export function stdioText(reader) {
|
|
121
|
+
const dec = { 1: new TextDecoder(), 2: new TextDecoder() }
|
|
122
|
+
const seen = { 1: 0, 2: 0 }
|
|
123
|
+
const skips = { 1: 0, 2: 0 }
|
|
124
|
+
const one = (fd, bytes) => {
|
|
125
|
+
let text = ''
|
|
126
|
+
const lost = reader.dropped[fd] - seen[fd]
|
|
127
|
+
if (lost > 0) {
|
|
128
|
+
seen[fd] = reader.dropped[fd]
|
|
129
|
+
skips[fd]++
|
|
130
|
+
// what the decoder held belonged to bytes before the skip: end that sequence
|
|
131
|
+
text = dec[fd].decode() + dropMarker(lost, fd)
|
|
132
|
+
}
|
|
133
|
+
if (bytes) text += dec[fd].decode(bytes, { stream: true })
|
|
134
|
+
return text
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
drain() {
|
|
138
|
+
const got = reader.drain()
|
|
139
|
+
return { 1: one(1, got[1]), 2: one(2, got[2]) }
|
|
140
|
+
},
|
|
141
|
+
skips,
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// wasi.thread-spawn over a pool of pre-started Workers (host-interface.md §6).
|
|
2
|
+
//
|
|
3
|
+
// thread-spawn is called from inside wasm on a Worker that cannot return to its
|
|
4
|
+
// event loop, and it must return the new tid synchronously. So the page starts
|
|
5
|
+
// Workers ahead of time; each one has the compiled vm.wasm Module and parks on its
|
|
6
|
+
// slot in the control buffer with Atomics.wait. Spawning claims an idle slot with a
|
|
7
|
+
// compare-exchange, writes tid and start_arg, flips the slot to GO and notifies.
|
|
8
|
+
//
|
|
9
|
+
// If no slot is idle the spawner asks the page for more Workers (postMessage is
|
|
10
|
+
// fine from a busy Worker: the page's event loop is free) and waits on the pool
|
|
11
|
+
// generation cell for up to `waitMs`; if still none it returns -11 (EAGAIN), which
|
|
12
|
+
// pthread_create reports as a failure, and the page shows "thread pool exhausted".
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
CTL_EXIT,
|
|
16
|
+
CTL_LIVE_THREADS,
|
|
17
|
+
CTL_NEXT_TID,
|
|
18
|
+
CTL_POOL_GEN,
|
|
19
|
+
CTL_POOL_N,
|
|
20
|
+
CTL_SPAWN_FAIL,
|
|
21
|
+
SLOT_ARG,
|
|
22
|
+
SLOT_RUNS,
|
|
23
|
+
SLOT_STATE,
|
|
24
|
+
SLOT_TID,
|
|
25
|
+
ST_CLAIMED,
|
|
26
|
+
ST_GO,
|
|
27
|
+
ST_IDLE,
|
|
28
|
+
ST_RUNNING,
|
|
29
|
+
slotIndex,
|
|
30
|
+
} from './layout.js'
|
|
31
|
+
|
|
32
|
+
export const EAGAIN = -11
|
|
33
|
+
|
|
34
|
+
/** Count slots in a given state (used by the page to keep spare Workers). */
|
|
35
|
+
export function countSlots(ctl, state) {
|
|
36
|
+
const n = Atomics.load(ctl, CTL_POOL_N)
|
|
37
|
+
let c = 0
|
|
38
|
+
for (let i = 0; i < n; i++) if (Atomics.load(ctl, slotIndex(i, SLOT_STATE)) === state) c++
|
|
39
|
+
return c
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function claimIdle(ctl) {
|
|
43
|
+
const n = Atomics.load(ctl, CTL_POOL_N)
|
|
44
|
+
for (let i = 0; i < n; i++) {
|
|
45
|
+
if (Atomics.compareExchange(ctl, slotIndex(i, SLOT_STATE), ST_IDLE, ST_CLAIMED) === ST_IDLE) return i
|
|
46
|
+
}
|
|
47
|
+
return -1
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The `wasi.thread-spawn` import.
|
|
52
|
+
* @param {Int32Array} ctl
|
|
53
|
+
* @param {{ requestWorkers:(why:string)=>void, waitMs?:number }} o
|
|
54
|
+
*/
|
|
55
|
+
export function createThreadSpawn(ctl, { requestWorkers, waitMs = 10000 }) {
|
|
56
|
+
return function threadSpawn(startArg) {
|
|
57
|
+
let slot = claimIdle(ctl)
|
|
58
|
+
if (slot < 0) {
|
|
59
|
+
const deadline = Date.now() + waitMs
|
|
60
|
+
requestWorkers('no idle worker')
|
|
61
|
+
while (slot < 0 && Date.now() < deadline && !Atomics.load(ctl, CTL_EXIT)) {
|
|
62
|
+
const gen = Atomics.load(ctl, CTL_POOL_GEN)
|
|
63
|
+
slot = claimIdle(ctl)
|
|
64
|
+
if (slot >= 0) break
|
|
65
|
+
Atomics.wait(ctl, CTL_POOL_GEN, gen, Math.max(1, deadline - Date.now()))
|
|
66
|
+
slot = claimIdle(ctl)
|
|
67
|
+
}
|
|
68
|
+
if (slot < 0) {
|
|
69
|
+
Atomics.add(ctl, CTL_SPAWN_FAIL, 1)
|
|
70
|
+
requestWorkers('exhausted')
|
|
71
|
+
return EAGAIN
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const tid = Atomics.add(ctl, CTL_NEXT_TID, 1) + 1
|
|
75
|
+
Atomics.store(ctl, slotIndex(slot, SLOT_TID), tid)
|
|
76
|
+
Atomics.store(ctl, slotIndex(slot, SLOT_ARG), startArg)
|
|
77
|
+
Atomics.add(ctl, CTL_LIVE_THREADS, 1)
|
|
78
|
+
Atomics.store(ctl, slotIndex(slot, SLOT_STATE), ST_GO)
|
|
79
|
+
Atomics.notify(ctl, slotIndex(slot, SLOT_STATE))
|
|
80
|
+
// Keep spares: the page tops the pool up asynchronously.
|
|
81
|
+
requestWorkers('spawned')
|
|
82
|
+
return tid
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Park a pool Worker until a spawn request arrives. Returns {tid, arg}, or null once
|
|
88
|
+
* the machine has exited. Wakes every `pollMs` to notice an exit; after `maxPolls` waits
|
|
89
|
+
* with no work it returns undefined.
|
|
90
|
+
*/
|
|
91
|
+
export function waitForWork(ctl, slot, pollMs = 1000, maxPolls = Infinity) {
|
|
92
|
+
const st = slotIndex(slot, SLOT_STATE)
|
|
93
|
+
for (let n = 0; ; n++) {
|
|
94
|
+
const s = Atomics.load(ctl, st)
|
|
95
|
+
if (s === ST_GO) break
|
|
96
|
+
if (Atomics.load(ctl, CTL_EXIT)) return null
|
|
97
|
+
if (n >= maxPolls) return undefined // no work yet (W32: the caller yields to its event loop, then waits again)
|
|
98
|
+
Atomics.wait(ctl, st, s, pollMs)
|
|
99
|
+
}
|
|
100
|
+
Atomics.store(ctl, st, ST_RUNNING)
|
|
101
|
+
Atomics.add(ctl, slotIndex(slot, SLOT_RUNS), 1)
|
|
102
|
+
return { tid: Atomics.load(ctl, slotIndex(slot, SLOT_TID)), arg: Atomics.load(ctl, slotIndex(slot, SLOT_ARG)) }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A pool Worker finished a thread: make the slot reusable. */
|
|
106
|
+
export function threadDone(ctl, slot) {
|
|
107
|
+
Atomics.sub(ctl, CTL_LIVE_THREADS, 1)
|
|
108
|
+
Atomics.store(ctl, slotIndex(slot, SLOT_STATE), ST_IDLE)
|
|
109
|
+
}
|