@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/boot.js
ADDED
|
@@ -0,0 +1,2037 @@
|
|
|
1
|
+
// Page-side entry point of wmd-web: download vm.wasm and the boot files, compile,
|
|
2
|
+
// build the shared memory and the Worker pool, run vm.wasm's _start in a Worker.
|
|
3
|
+
//
|
|
4
|
+
// const vm = await bootWmd({ vmUrl, files: { Image: url, ... }, args: [...] })
|
|
5
|
+
// const { code, timings } = await vm.exited
|
|
6
|
+
//
|
|
7
|
+
// Requires a cross-origin isolated page (SharedArrayBuffer, shared wasm memory).
|
|
8
|
+
|
|
9
|
+
import { absMs, markStart } from './host/clock.js'
|
|
10
|
+
import {
|
|
11
|
+
CTL_EXIT,
|
|
12
|
+
CTL_EXIT_CODE,
|
|
13
|
+
CTL_TRAPS,
|
|
14
|
+
CTL_STALL_DONE,
|
|
15
|
+
CTL_STALL_MS,
|
|
16
|
+
CTL_STALL_TAKEN,
|
|
17
|
+
CTL_STALL_TID,
|
|
18
|
+
CTL_STALL_WAKE,
|
|
19
|
+
F64_STALL_AT,
|
|
20
|
+
F64_STALL_END,
|
|
21
|
+
CTL_LIVE_THREADS,
|
|
22
|
+
CTL_NEXT_TID,
|
|
23
|
+
CTL_POOL_N,
|
|
24
|
+
CTL_CK_ATTACHES,
|
|
25
|
+
CTL_CK_GO,
|
|
26
|
+
CTL_CK_RESTORED,
|
|
27
|
+
CTL_SPAWN_FAIL,
|
|
28
|
+
POOL_MAX,
|
|
29
|
+
SLOT_STATE,
|
|
30
|
+
ST_DEAD,
|
|
31
|
+
ST_IDLE,
|
|
32
|
+
ST_STARTING,
|
|
33
|
+
createControl,
|
|
34
|
+
hostCallCounts,
|
|
35
|
+
slotIndex,
|
|
36
|
+
} from './host/layout.js'
|
|
37
|
+
import { gcNudgeEvery } from './host/gcnudge.js'
|
|
38
|
+
import { createRamMemory, haveMemory64 } from './host/ram-mem.js'
|
|
39
|
+
import { bootFileName, cachedFromTiming, createBootEvents, createWorkerList, readBody, timingCount, vcpusOf } from './host/boot-events.js'
|
|
40
|
+
import { createJitStore, readJitStats, tableMinOf } from './host/jit.js'
|
|
41
|
+
import { createJitCacheHost } from './host/jit-cache.js'
|
|
42
|
+
import { createPanicScanner, createThrottleMeter, createWatchdog, watchdogDefaultMs } from './host/monitor.js'
|
|
43
|
+
import { BOOT_RING, createStdio, stdioReader, stdioText } from './host/stdio.js'
|
|
44
|
+
import {
|
|
45
|
+
CONSOLE_IN_LIMIT, chanCounters, chanView, consoleInputGate, createChanBlock, markGuestGone, markServiceDead, ringBell, ringRead,
|
|
46
|
+
} from './host/chan.js'
|
|
47
|
+
import { ControlClient } from './host/ctl.js'
|
|
48
|
+
import { PAGE as CKPT_PAGE, requestCheckpoint } from './host/ckpt-point.js'
|
|
49
|
+
import {
|
|
50
|
+
CheckpointError, assembleCheckpoint, bootKeyOf, checkCheckpointFits, checkPacksFit, checkVmKeyFits, packDigestsOf, fdTableBytes, parseCheckpoint, restoreFdTable, resumePorts, sha256Hex, validateFdTable,
|
|
51
|
+
} from './host/ckpt.js'
|
|
52
|
+
import { captureJit, jitSnapStats, modulesOf, ownersNeedingHub, restoreStore } from './host/jit-snap.js'
|
|
53
|
+
export { CheckpointError, memoryStore, opfsStore } from './host/ckpt.js'
|
|
54
|
+
export { NetSwitch } from './net/switch.js'
|
|
55
|
+
export { ImageFiles, openImage, parseGroup, parsePasswd, resolveUser } from './image-files.js'
|
|
56
|
+
export { P9_CACHE_DEFAULT, p9InitCmdline, p9MountOptions } from './host/p9mount.js'
|
|
57
|
+
|
|
58
|
+
const PAGE = 65536
|
|
59
|
+
/** A trap with the memory this close to its maximum is reported as out of memory. */
|
|
60
|
+
export const OOM_MARGIN = 64 << 20
|
|
61
|
+
/** vm.wasm's stderr when it grew its memory to the maximum up front (W21, WMD_HEAP_RESERVE). */
|
|
62
|
+
export const HEAP_RESERVE_RE = /vm\.wasm: heap reserve \d+ MiB at /
|
|
63
|
+
/** vm.wasm's stderr when an allocation found the reserve used up (W21). */
|
|
64
|
+
export const HEAP_USED_UP_RE = /vm\.wasm: heap reserve used up \(W21\): out of memory/
|
|
65
|
+
/**
|
|
66
|
+
* vm.wasm's stderr when it stops on a failed allocation, reserve or not (W21 review A3), and the
|
|
67
|
+
* JIT encoder's own line (guest/vm/src/wasm_enc.c enc_alloc), which a vm.wasm built before
|
|
68
|
+
* vm_out_of_memory printed before it aborted (a trap): W29, a trap whose only word was this line.
|
|
69
|
+
*/
|
|
70
|
+
export const VM_OOM_RE = /vm\.wasm: out of memory: |wasm_enc: out of memory allocating \d+ bytes/
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Whether vm.wasm's environment asks for W21's heap reserve: WMD_HEAP_RESERVE=1 exactly
|
|
74
|
+
* (guest/vm/src/heap_reserve.c reads it so). bootWmd adds it unless heapReserve is false.
|
|
75
|
+
*/
|
|
76
|
+
export const heapReserveAsked = env => env.includes('WMD_HEAP_RESERVE=1')
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Whether a trap is vm.wasm out of memory, and the evidence: vm.wasm's own "heap reserve
|
|
80
|
+
* used up" line, or, when it runs without a reserve, the memory within OOM_MARGIN of its
|
|
81
|
+
* maximum. With a reserve (asked for in vm.wasm's environment, or reported by vm.wasm)
|
|
82
|
+
* the memory is at the maximum from the start, so the size says nothing.
|
|
83
|
+
* Returns null, 'heap reserve used up', 'vm.wasm out of memory' (its own line, no reserve)
|
|
84
|
+
* or 'memory at its maximum'.
|
|
85
|
+
*/
|
|
86
|
+
export function isOutOfMemory({ reserve = false, usedUp = false, said = false, memoryBytes, memoryMaxBytes }) {
|
|
87
|
+
if (usedUp) return 'heap reserve used up'
|
|
88
|
+
if (said) return 'vm.wasm out of memory'
|
|
89
|
+
if (!reserve && memoryBytes > memoryMaxBytes - OOM_MARGIN) return 'memory at its maximum'
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
const VM_MIN_PAGES = 6144 // vm.wasm's memory import minimum (384 MiB)
|
|
93
|
+
|
|
94
|
+
const haveJspi = () => typeof WebAssembly.Suspending === 'function' && typeof WebAssembly.promising === 'function'
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Whether JIT module sharing is on by default (jit.share 'auto'): wherever JSPI exists
|
|
98
|
+
* (Playwright's Chromium 153, Firefox 155 and WebKit 26.6 all have it). Firefox needs
|
|
99
|
+
* it: with a compiled copy per Worker it ran out of wasm code space at ~16,200 copies
|
|
100
|
+
* per process (P-B2 adversarial F1). In Chromium and WebKit it was as fast or faster
|
|
101
|
+
* and used no more memory in the measured boots (README "F1").
|
|
102
|
+
*/
|
|
103
|
+
export function jitShareDefault() {
|
|
104
|
+
return haveJspi()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** WebKit (Safari, Playwright's WebKit) kills a content process whose memory footprint passes 8 GiB (W34). */
|
|
108
|
+
export const WEBKIT_KILL_MIB = 8192
|
|
109
|
+
/** Footprint the page itself needs besides the machine's memory and the JIT's modules (files, packs, rings), MiB (W34, measured: 0.75 GiB with a 220 MiB pack). */
|
|
110
|
+
const PAGE_BASE_MIB = 768
|
|
111
|
+
/** Footprint WebKit keeps per byte of a live JIT module (W34, measured with the heap reserve on: 2.2 GiB for 560 MiB of modules, about 4x; 2.2x with it off). */
|
|
112
|
+
const MODULE_FOOTPRINT_X = 4
|
|
113
|
+
|
|
114
|
+
export function isWebKit(nav = globalThis.navigator) {
|
|
115
|
+
const ua = nav?.userAgent || ''
|
|
116
|
+
return /AppleWebKit/.test(ua) && !/Chrome|Chromium|Firefox|Android/.test(ua) // desktop Edge says Chrome too; iOS Edge (EdgiOS), Chrome (CriOS) and Firefox (FxiOS) are WebKit
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function isFirefox(nav = globalThis.navigator) {
|
|
120
|
+
return /Firefox\//.test(nav?.userAgent || '') && !isWebKit(nav)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* D-511: Firefox keeps all compiled code of a process, wasm and JavaScript, in one 2044 MiB pool
|
|
125
|
+
* (SpiderMonkey MaxCodeBytesPerProcess); our modules make about 2.7x their bytes of it, so new ones
|
|
126
|
+
* stop compiling at 768 MiB live. Every Worker instantiates every module and counts its code, and
|
|
127
|
+
* past 80% of the pool (~614 MiB of modules) each Worker runs back-to-back full GCs that discard its
|
|
128
|
+
* JavaScript code and free nothing (TOO_MUCH_JIT_CODE: 166 a second in a Go start-up at 700 MiB,
|
|
129
|
+
* vCPUs 93-97% in the collector, the channel service starved). A release's modules stay allocated
|
|
130
|
+
* until every Worker drops them, so live plus one released generation must fit: 2 x 320 < 768.
|
|
131
|
+
*/
|
|
132
|
+
export const FIREFOX_CODE_BUDGET_MIB = 320
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* D-516 (wmn#67): mobile WebKit, the engine of every browser on iPhone and iPad. iOS kills its tab when the tab's memory
|
|
136
|
+
* passes a limit far below a Mac's (about 1.5 GiB, up to about 3 GiB on some phones), and mobile WebKit counts every
|
|
137
|
+
* byte a shared memory or growable SharedArrayBuffer grows by at once, written or not; desktop WebKit counts only the
|
|
138
|
+
* pages written. An iPad in desktop mode says Macintosh; it is told apart by its touch points.
|
|
139
|
+
*/
|
|
140
|
+
export function isMobileWebKit(nav = globalThis.navigator) {
|
|
141
|
+
if (!isWebKit(nav)) return false
|
|
142
|
+
const ua = nav?.userAgent || ''
|
|
143
|
+
return /\b(iPhone|iPad|iPod)\b/.test(ua) || (/Macintosh/.test(ua) && (nav?.maxTouchPoints || 0) > 1)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* D-516: whether bootWmd asks for W21's heap reserve when the caller does not say. Not in mobile WebKit: there the
|
|
148
|
+
* reserve (the memory grown to its maximum before boot) costs the whole maximum at once, 2.4 GiB with a 1 GiB guest,
|
|
149
|
+
* and iOS kills the tab while the machine boots. The reserve is for V8's stale memory size (W21); WebKit checks the
|
|
150
|
+
* live size, so it does not need it.
|
|
151
|
+
*/
|
|
152
|
+
export function heapReserveDefault(nav = globalThis.navigator) {
|
|
153
|
+
return !isMobileWebKit(nav)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* W34: the bytes of live JIT modules a WebKit page can hold, so that the machine's memory at its
|
|
158
|
+
* maximum + the page's own files + the engine's copies of the modules stay under 5.5 GiB (2.5 GiB
|
|
159
|
+
* below WebKit's kill limit). Firefox: FIREFOX_CODE_BUDGET_MIB (D-511). 0 (no budget) in Chromium,
|
|
160
|
+
* whose wasm code space held 1.7 GB of modules in a Go start-up.
|
|
161
|
+
* At least 128 MiB, so a big memory maximum slows the JIT's reach rather than switching it off.
|
|
162
|
+
*/
|
|
163
|
+
export function jitBudgetDefault(maxBytes, nav = globalThis.navigator) {
|
|
164
|
+
if (isFirefox(nav)) return FIREFOX_CODE_BUDGET_MIB * MiB
|
|
165
|
+
if (!isWebKit(nav)) return 0
|
|
166
|
+
const room = (WEBKIT_KILL_MIB - 2560 - PAGE_BASE_MIB) * MiB - maxBytes
|
|
167
|
+
return Math.min(WEBKIT_CODE_BUDGET_MIB * MiB, Math.max(128 * MiB, Math.floor(room / MODULE_FOOTPRINT_X)))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* W32: JavaScriptCore's executable pool (512 MiB) stops taking new code at about 343 MB of
|
|
172
|
+
* "JS JIT generated code"; from then on new blocks run at interpreter speed (b8 in WebKit: 6000
|
|
173
|
+
* rounds took 55-63 s after a build instead of 0.55 s). It holds about 2.1x a module's bytes, so
|
|
174
|
+
* the budget releases every module before 144 MiB of them are live. A release is only a backstop:
|
|
175
|
+
* WebKit gives the code back 30-40 s later, so a workload that keeps reaching it builds slowly
|
|
176
|
+
* (b8 build at 144 MiB without a raised threshold: 80 s instead of 14 s, compute fast).
|
|
177
|
+
*/
|
|
178
|
+
export const WEBKIT_CODE_BUDGET_MIB = 144
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* W32: runs a block makes in vm.wasm's interpreter before it is compiled, in WebKit (vm.wasm's
|
|
182
|
+
* default is 50, D-274). Code that runs a few hundred times stays interpreted, so a build's
|
|
183
|
+
* modules fit the executable pool: b8 in WebKit at 300: build 17.9-18.7 s (14 s at 50), compute
|
|
184
|
+
* after it 0.53-0.55 s (55-63 s at 50), 117 MiB of modules, 249 MB of code at most; at 200 the
|
|
185
|
+
* code reached 326 MB, at 500 the build took 24.7 s, at 2000 56 s.
|
|
186
|
+
* D-505: at 300 a long Claude Code session still filled the pool (about 345 MB) and ran
|
|
187
|
+
* interpreted after it. With vm.wasm batching blocks above jit_hot again, 1000 is the pick:
|
|
188
|
+
* b8 build 25.5-26.3 s (21.2-22.1 at 300, 40.6 at 3000), `claude -p` 32.9 s (57.3 at 300),
|
|
189
|
+
* 2000 terminal lines 4.2 s (31.7 at 300), 161 MB of code.
|
|
190
|
+
*/
|
|
191
|
+
export const WEBKIT_JIT_THRESHOLD = 1000
|
|
192
|
+
|
|
193
|
+
const MiB = 1 << 20
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The default cap on a 9P service's upper (the guest's writes, held in the service
|
|
197
|
+
* Worker's memory): an eighth of the device's memory as navigator.deviceMemory reports it
|
|
198
|
+
* (Chromium: 0.25-8 GiB, rounded), from 256 MiB to 1 GiB; 512 MiB where the browser does
|
|
199
|
+
* not say (Firefox, Safari). Returns {bytes, source}.
|
|
200
|
+
*/
|
|
201
|
+
export function upperCapDefault(nav = globalThis.navigator) {
|
|
202
|
+
const gib = nav?.deviceMemory
|
|
203
|
+
if (typeof gib === 'number' && Number.isFinite(gib) && gib > 0) {
|
|
204
|
+
return { bytes: Math.min(1024 * MiB, Math.max(256 * MiB, Math.floor((gib * 1024 * MiB) / 8))), source: `navigator.deviceMemory ${gib} GiB / 8` }
|
|
205
|
+
}
|
|
206
|
+
return { bytes: 512 * MiB, source: 'default (navigator.deviceMemory unknown)' }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const fmtMiB = n => (Number.isFinite(n) ? `${Math.round((n / MiB) * 10) / 10} MiB` : 'no cap')
|
|
210
|
+
|
|
211
|
+
// Host-side endings (vm.exited code < 0; guest exit codes are >= 0).
|
|
212
|
+
export const EXIT_STOPPED = -1 // reason 'stopped' (vm.stop()) or 'aborted' (the AbortSignal)
|
|
213
|
+
export const EXIT_NO_PROGRESS = -2 // the progress watchdog fired
|
|
214
|
+
export const EXIT_PANIC = -3 // the console showed "Kernel panic - not syncing"
|
|
215
|
+
export const EXIT_SERVICE_DIED = -4 // the channel service Worker ended while the guest ran (P-B3)
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The most --ram (MiB) a browser boot accepts unless the caller opts in (W28). The desktop engine
|
|
219
|
+
* now boots guests up to 3 GiB, but memory 0 is a shared wasm32 memory of at most 4 GiB that also
|
|
220
|
+
* holds vm.wasm's heap (translator IR, module buffers, ~384 MiB initial): --ram 3072 leaves about
|
|
221
|
+
* 0.6 GiB of heap, and a tab commits the whole 4 GiB (W21 reserve). 1536 leaves 2 GiB or more.
|
|
222
|
+
*/
|
|
223
|
+
export const MAX_BROWSER_RAM_MIB = 1536
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* --ram's value in MiB; throws an error naming --ram when it is not a whole number, is 0, or when it is
|
|
227
|
+
* above MAX_BROWSER_RAM_MIB and allowLargeRam is not true.
|
|
228
|
+
*/
|
|
229
|
+
export function ramMiB(args, allowLargeRam = false) {
|
|
230
|
+
const i = args.indexOf('--ram')
|
|
231
|
+
if (i < 0) return args.includes('--machine') ? 256 : 0
|
|
232
|
+
const v = args[i + 1]
|
|
233
|
+
if (typeof v !== 'string' || !/^[0-9]+$/.test(v)) {
|
|
234
|
+
throw new Error(`--ram ${v === undefined ? '(missing)' : JSON.stringify(v)}: expected a whole number of MiB, e.g. --ram 256`)
|
|
235
|
+
}
|
|
236
|
+
const mib = Number(v)
|
|
237
|
+
if (mib < 1) throw new Error(`--ram ${v}: a guest needs at least 1 MiB, e.g. --ram 256`)
|
|
238
|
+
if (mib > MAX_BROWSER_RAM_MIB && allowLargeRam !== true) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
`--ram ${v}: more than ${MAX_BROWSER_RAM_MIB} MiB is not supported in a browser by default: the shared memory is at most 4 GiB ` +
|
|
241
|
+
`and also holds the emulator's heap, so a bigger guest leaves it too little room. ` +
|
|
242
|
+
`Use ${MAX_BROWSER_RAM_MIB} or less, or pass allowLargeRam: true (bootWmd option; memoryMaxPages' third argument) to accept the risk.`,
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
return mib
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Memory 0 of a flat guest besides vm.wasm's own minimum: the boot stand-in for the start of RAM (vm/ram.h VM_RAM_LOW_FLAT). */
|
|
249
|
+
export const FLAT_STAGE_MIB = 512
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Whether a boot with these args keeps guest RAM in a memory64 of its own (host/ram-mem.js): o.flatRam when it
|
|
253
|
+
* is a boolean, else when --ram is above MAX_BROWSER_RAM_MIB, allowLargeRam is not set and the engine has memory64.
|
|
254
|
+
*/
|
|
255
|
+
export function wantsFlatRam(args, o = {}) {
|
|
256
|
+
if (o.flatRam === false) return false
|
|
257
|
+
let mib = 0
|
|
258
|
+
try {
|
|
259
|
+
mib = ramMiB(args, true)
|
|
260
|
+
} catch {
|
|
261
|
+
return false // ramMiB reports the bad value
|
|
262
|
+
}
|
|
263
|
+
if (!mib) return false
|
|
264
|
+
if (o.flatRam === true) {
|
|
265
|
+
if (!haveMemory64()) throw new Error('flatRam: this browser has no shared 64-bit memory (WebAssembly memory64)')
|
|
266
|
+
return true
|
|
267
|
+
}
|
|
268
|
+
return mib > MAX_BROWSER_RAM_MIB && o.allowLargeRam !== true && haveMemory64()
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** The memory 0 maximum, in pages, of a flat guest: vm.wasm's minimum, the boot stand-in and heap slack. */
|
|
272
|
+
export function flatMaxPages(args, slackMiB = 1536) {
|
|
273
|
+
const stage = Math.min(ramMiB(args, true), FLAT_STAGE_MIB)
|
|
274
|
+
return Math.min(65536, VM_MIN_PAGES + Math.ceil(((stage + slackMiB) * 1024 * 1024) / PAGE))
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* vm.wasm's environment: the caller's entries plus WMD_HEAP_RESERVE=1 (W21) unless
|
|
279
|
+
* heapReserve is false or the caller sets WMD_HEAP_RESERVE itself.
|
|
280
|
+
*/
|
|
281
|
+
export function vmEnv(env = [], heapReserve = true) {
|
|
282
|
+
const out = [...env]
|
|
283
|
+
if (heapReserve !== false && !out.some(e => e.startsWith('WMD_HEAP_RESERVE='))) out.push('WMD_HEAP_RESERVE=1')
|
|
284
|
+
return out
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** The browser's wasm engine: 'webkit' (JavaScriptCore), 'gecko' (SpiderMonkey) or 'blink' (V8, the rest). */
|
|
288
|
+
export function engineOf(nav = globalThis.navigator) {
|
|
289
|
+
if (isWebKit(nav)) return 'webkit'
|
|
290
|
+
return /Firefox\//.test(nav?.userAgent || '') ? 'gecko' : 'blink'
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* W30: the most members a trace (D-303) records in each engine (vm.wasm's WMD_TRACE_MAX; 1024
|
|
295
|
+
* natively). A trace is one looping wasm function; the browsers' compilers run a big one slower
|
|
296
|
+
* than the blocks it replaces (Chromium awk 29 s without traces, 37-52 s with traces of up to 1024
|
|
297
|
+
* members, 19-21 s with up to 32; Firefox 39 s against 67-99 s at 1024).
|
|
298
|
+
*/
|
|
299
|
+
export const BROWSER_TRACE_MAX = { blink: 32, gecko: 32, webkit: 32 }
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* W33: a browser can stop one vCPU's Worker for seconds. When it stops in the vCPU's idle wait
|
|
303
|
+
* with an interrupt due (on the 6.1 guest kernel, RCU's grace-period timer), vm.wasm's rescue
|
|
304
|
+
* thread runs that vCPU after this many ms, so exec on the other vCPUs does not wait for it.
|
|
305
|
+
*/
|
|
306
|
+
export const BROWSER_RESCUE_MS = 20
|
|
307
|
+
|
|
308
|
+
// D-539: a module with f64x2.relaxed_madd and f32x4.relaxed_madd, each on one lane: d(a, b, c) and s(a, b, c)
|
|
309
|
+
const FMA_PROBE = new Uint8Array([
|
|
310
|
+
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
|
|
311
|
+
0x01, 0x0f, 0x02, 0x60, 0x03, 0x7c, 0x7c, 0x7c, 0x01, 0x7c, 0x60, 0x03, 0x7d, 0x7d, 0x7d, 0x01, 0x7d,
|
|
312
|
+
0x03, 0x03, 0x02, 0x00, 0x01,
|
|
313
|
+
0x07, 0x09, 0x02, 0x01, 0x64, 0x00, 0x00, 0x01, 0x73, 0x00, 0x01,
|
|
314
|
+
0x0a, 0x2b, 0x02,
|
|
315
|
+
0x14, 0x00, 0x20, 0x00, 0xfd, 0x14, 0x20, 0x01, 0xfd, 0x14, 0x20, 0x02, 0xfd, 0x14, 0xfd, 0x87, 0x02, 0xfd, 0x21, 0x00, 0x0b,
|
|
316
|
+
0x14, 0x00, 0x20, 0x00, 0xfd, 0x13, 0x20, 0x01, 0xfd, 0x13, 0x20, 0x02, 0xfd, 0x13, 0xfd, 0x85, 0x02, 0xfd, 0x1f, 0x00, 0x0b,
|
|
317
|
+
])
|
|
318
|
+
let fmaFused = null
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* D-539: whether this engine parses relaxed SIMD and its relaxed_madd is fused (one rounding), in f64 and f32:
|
|
322
|
+
* (1 + 2^-30)(1 - 2^-30) - 1 is -2^-60 fused and 0 unfused (2^-12 and -2^-24 for f32). Where it is, vm.wasm
|
|
323
|
+
* gets WMD_FMA_FUSED=1 (browserEnv) and its JIT blocks do a guest FMA with relaxed_madd (D-472); Safari
|
|
324
|
+
* rejects relaxed SIMD, so there it stays the exact plain-f64 FMA (D-535). Asked once per page.
|
|
325
|
+
*/
|
|
326
|
+
export function fmaFuses() {
|
|
327
|
+
if (fmaFused !== null) return fmaFused
|
|
328
|
+
fmaFused = false
|
|
329
|
+
try {
|
|
330
|
+
if (WebAssembly.validate(FMA_PROBE)) {
|
|
331
|
+
const { d, s } = new WebAssembly.Instance(new WebAssembly.Module(FMA_PROBE)).exports
|
|
332
|
+
fmaFused = d(1 + 2 ** -30, 1 - 2 ** -30, -1) === -(2 ** -60) &&
|
|
333
|
+
s(Math.fround(1 + 2 ** -12), Math.fround(1 - 2 ** -12), -1) === -(2 ** -24)
|
|
334
|
+
}
|
|
335
|
+
} catch {
|
|
336
|
+
fmaFused = false
|
|
337
|
+
}
|
|
338
|
+
return fmaFused
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* W32: the environment vm.wasm starts with in this browser: vmEnv's, plus
|
|
343
|
+
* WMD_JIT_THRESHOLD=WEBKIT_JIT_THRESHOLD in WebKit, W30's WMD_TRACE_MAX for the engine and
|
|
344
|
+
* W33's WMD_RESCUE_MS, D-539's WMD_FMA_FUSED=1 where the engine fuses relaxed_madd (fmaFuses) and, in
|
|
345
|
+
* Chromium, D-542's WMD_RGN_DUP=1 (regions copy sampled-hot compiled blocks of their page; Firefox's go ran
|
|
346
|
+
* slower with it, WebKit did not move), each unless the caller sets it. Kept out of the boot key (bootKeyOf hashes vmEnv's list), so a checkpoint or base
|
|
347
|
+
* snapshot made in one engine restores in another; a restored machine keeps the settings it started with, but
|
|
348
|
+
* its FMA follows the restoring page (machine_fma_env). A checkpoint taken with WMD_FMA_FUSED=1 records
|
|
349
|
+
* fmaFused, and an engine that does not fuse refuses it (checkCheckpointFits), as its blocks may hold
|
|
350
|
+
* relaxed_madd.
|
|
351
|
+
*/
|
|
352
|
+
export function browserEnv(env, nav = globalThis.navigator, fused = fmaFuses()) {
|
|
353
|
+
const has = k => env.some(e => e.startsWith(`${k}=`))
|
|
354
|
+
const add = []
|
|
355
|
+
if (isWebKit(nav) && !has('WMD_JIT_THRESHOLD')) add.push(`WMD_JIT_THRESHOLD=${WEBKIT_JIT_THRESHOLD}`)
|
|
356
|
+
if (!has('WMD_TRACE_MAX')) add.push(`WMD_TRACE_MAX=${BROWSER_TRACE_MAX[engineOf(nav)]}`)
|
|
357
|
+
if (!has('WMD_RESCUE_MS')) add.push(`WMD_RESCUE_MS=${BROWSER_RESCUE_MS}`)
|
|
358
|
+
if (!has('WMD_FMA_FUSED') && fused) add.push('WMD_FMA_FUSED=1') // D-539
|
|
359
|
+
if (engineOf(nav) === 'blink' && !has('WMD_RGN_DUP')) add.push('WMD_RGN_DUP=1') // D-542: proven in Chromium only
|
|
360
|
+
return add.length ? [...env, ...add] : env
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Pages the shared memory must be able to grow to for a given --ram (MiB). */
|
|
364
|
+
export function memoryMaxPages(args, slackMiB = 1024, allowLargeRam = false) {
|
|
365
|
+
const ramMiB_ = ramMiB(args, allowLargeRam)
|
|
366
|
+
// RAM + vCPU arena (a few MiB per vCPU) + heap growth slack.
|
|
367
|
+
const pages = VM_MIN_PAGES + Math.ceil((ramMiB_ * 1024 * 1024) / PAGE) + Math.ceil((slackMiB * 1024 * 1024) / PAGE)
|
|
368
|
+
return Math.min(65536, pages)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Time the throttle detector's calibration loop n times now (in its own Worker) and
|
|
373
|
+
* resolve with the fastest run, in ms. A page can call this while it is in the
|
|
374
|
+
* foreground and pass the result as bootWmd({throttle: {baselineMs}}), so a boot that
|
|
375
|
+
* starts in a background window is still recognised as throttled.
|
|
376
|
+
*/
|
|
377
|
+
export function calibrate(n = 3) {
|
|
378
|
+
return new Promise((resolve, reject) => {
|
|
379
|
+
const w = new Worker(new URL('./calib-worker.js', import.meta.url), { type: 'module', name: 'wmd-calib' })
|
|
380
|
+
let best = Infinity
|
|
381
|
+
let got = 0
|
|
382
|
+
w.onmessage = e => {
|
|
383
|
+
best = Math.min(best, e.data.workMs)
|
|
384
|
+
if (++got === n) {
|
|
385
|
+
w.terminate()
|
|
386
|
+
resolve(best)
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
w.onerror = err => {
|
|
390
|
+
w.terminate()
|
|
391
|
+
reject(new Error(`calibration Worker failed: ${err.message || err}`))
|
|
392
|
+
}
|
|
393
|
+
w.postMessage({ t: 'once', n })
|
|
394
|
+
})
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async function fetchShared(url, signal, onBytes, shared = true, onResponse) {
|
|
398
|
+
const r = await fetch(url, { signal, cache: 'no-store' })
|
|
399
|
+
if (!r.ok) throw new Error(`GET ${url}: ${r.status} ${r.statusText}`)
|
|
400
|
+
onResponse?.(r)
|
|
401
|
+
return readBody(r, { shared, onBytes })
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// W71: a pack the page made in shared memory (src/registry/pull.js) is used in place, not copied
|
|
405
|
+
// (packs are read only; a file in o.files may not be, so files are always copied)
|
|
406
|
+
function packShared(bytes) {
|
|
407
|
+
if (typeof SharedArrayBuffer !== 'undefined' && bytes.buffer instanceof SharedArrayBuffer && bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength) return bytes.buffer
|
|
408
|
+
return toShared(bytes)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function toShared(bytes) {
|
|
412
|
+
const sab = new SharedArrayBuffer(bytes.byteLength)
|
|
413
|
+
new Uint8Array(sab).set(bytes)
|
|
414
|
+
return sab
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* @param {object} o
|
|
419
|
+
* @param {string|Uint8Array|ArrayBuffer} o.vmUrl URL of vm.wasm, or its bytes
|
|
420
|
+
* @param {Record<string,string|Uint8Array>} o.files preopen contents: path → URL or bytes
|
|
421
|
+
* @param {string[]} o.args vm.wasm arguments (after argv[0])
|
|
422
|
+
* @param {string[]} [o.env] "K=V" (the runner passes only WMD_*)
|
|
423
|
+
* @param {boolean} [o.heapReserve] W21 (default true, false in mobile WebKit: heapReserveDefault, D-516): vm.wasm grows its memory to the maximum
|
|
424
|
+
* before any thread starts (WMD_HEAP_RESERVE=1), so memory never grows under running
|
|
425
|
+
* vCPUs. Chromium checks memory.fill/copy against a per-Worker size that lags another
|
|
426
|
+
* Worker's grow, and trapped (README "W21"). An env entry WMD_HEAP_RESERVE=... wins.
|
|
427
|
+
* @param {string} [o.preopen] guest name of the preopen (default "/g")
|
|
428
|
+
* @param {number} [o.pool] Workers started up front (default 4)
|
|
429
|
+
* @param {number} [o.spare] idle Workers kept in reserve (default 2)
|
|
430
|
+
* @param {boolean} [o.allowLargeRam] accept --ram above MAX_BROWSER_RAM_MIB (1536; W28)
|
|
431
|
+
* @param {number} [o.maxPages] shared memory maximum, in 64 KiB pages
|
|
432
|
+
* @param {URL|string} [o.workerUrl]
|
|
433
|
+
* @param {(text:string)=>void} [o.onStdout]
|
|
434
|
+
* @param {(text:string)=>void} [o.onStderr]
|
|
435
|
+
* @param {(ev:object)=>void} [o.onEvent] diagnostics: thread start/end, pool growth, ..., and W24's start-up events
|
|
436
|
+
* ({t:'boot', stage, state, atMs, tookMs?}, {t:'heap'}, {t:'layer'}, {t:'exit'}: src/host/boot-events.js, README "W24")
|
|
437
|
+
* @param {{name:string, re:RegExp}[]} [o.marks] stdout regexes whose first match time is recorded
|
|
438
|
+
* @param {AbortSignal} [o.signal] abort before the Workers start: bootWmd rejects with an
|
|
439
|
+
* AbortError; after: vm.exited gives {code:-1, reason:'aborted'}
|
|
440
|
+
* @param {{compileOnCompile?:'compile'|'validate'|'none', brk?:string, share?:'auto'|boolean, shareWaitMs?:number, maxLiveBytes?:number, codePool?:boolean, cache?:{get:(key:string)=>Promise<Uint8Array|null>, put:(key:string, bytes:Uint8Array)=>Promise<void>}}|false} [o.jit]
|
|
441
|
+
* false: no JIT door (the stubbed jit_* imports return -1 and are counted in hostCalls).
|
|
442
|
+
* JIT door options. share: compile each JIT module once per process and hand the
|
|
443
|
+
* Module to every vCPU Worker through a hub Worker (src/host/jit-hub.js), instead
|
|
444
|
+
* of one compiled copy per Worker. Needs JSPI. 'auto' (default): on where
|
|
445
|
+
* WebAssembly.Suspending exists (jitShareDefault). With sharing, jit_compile
|
|
446
|
+
* always compiles (compileOnCompile applies only without it).
|
|
447
|
+
* cache (W77): {get(key) -> Promise<Uint8Array|null>, put(key, bytes) -> Promise}, a byte store the
|
|
448
|
+
* page keeps across visits (IndexedDB, say): the JIT's translated modules and an index of the
|
|
449
|
+
* guest blocks they hold, so a later boot of the same vm.wasm skips translating them again
|
|
450
|
+
* (host/jit-cache.js). Needs sharing. Counters in timings.jit (cacheHits, cacheMisses,
|
|
451
|
+
* translated, ...) and timings.jitCache; the index is put before vm.exited resolves (up to 10 s).
|
|
452
|
+
* @param {number} [o.stdioRingBytes] stdout/stderr ring per fd (power of two; default
|
|
453
|
+
* BOOT_RING, 32 MiB). If the page falls a whole ring behind, the skipped bytes are
|
|
454
|
+
* replaced in the delivered text by a "[wmd-web: N bytes of stdout were dropped]" line.
|
|
455
|
+
* @param {number} [o.watchdogMs] end the boot (code -2) when the guest writes nothing and
|
|
456
|
+
* spawns no thread for this long after it starts. 0 = off.
|
|
457
|
+
* Default: watchdogDefaultMs(args) (60 s arm64, 300 s x86)
|
|
458
|
+
* @param {{intervalMs?:number, threshold?:number, baselineMs?:number}|false} [o.throttle]
|
|
459
|
+
* the throttle detector (calib-worker.js); false = off
|
|
460
|
+
* @param {object} [o.channels] P-B3: wmd.chan_* over shared-memory rings, served by a
|
|
461
|
+
* service Worker (src/host/chan-service.js). {services: [{port, kind: '9p'|'console'|'control'|'net',
|
|
462
|
+
* layers?: (url|Uint8Array)[] (packs, top first), at?, files?: {path: string|Uint8Array},
|
|
463
|
+
* maxUpperBytes?, yamux?}], ringBytes? (each direction, default 1 MiB), nslots? (8), spinUs? (5000: the
|
|
464
|
+
* service's longest adaptive doorbell spin, W51)}.
|
|
465
|
+
* A file is its bytes or text, or {data, mode?, uid?, gid?}; {dir: true, mode?, uid?, gid?} makes a
|
|
466
|
+
* directory (W65: an image's WORKDIR). A control service with yamux: true is
|
|
467
|
+
* wmd's control session (P-B11): vm.control drives it (src/host/ctl.js).
|
|
468
|
+
* Pass the ports to vm.wasm yourself (--p9 PORT, --console PORT, ...). A port with no service
|
|
469
|
+
* refuses (-111), as nothing listening does natively. collect?: [{port, path}]: files read
|
|
470
|
+
* out of the 9P trees when the machine ends, into timings.chanFiles {"port:path": text|null}.
|
|
471
|
+
* maxUpperBytes?: the cap on each 9P service's upper (bytes of file contents the guest's
|
|
472
|
+
* writes hold in the service Worker) for services that do not set their own; default
|
|
473
|
+
* upperCapDefault() (navigator.deviceMemory); Infinity for none. Reported in
|
|
474
|
+
* timings.chanUppers; when a write hits it the guest gets ENOSPC and onStderr says so.
|
|
475
|
+
* consoleInputBytes?: console input (vm.consoleWrite) queued for the guest at most
|
|
476
|
+
* (default CONSOLE_IN_LIMIT, 4 MiB).
|
|
477
|
+
* The guest should mount the 9P root with p9MountOptions() (cache=mmap): under
|
|
478
|
+
* cache=loose a full upper cannot fail write(2) (src/host/p9mount.js).
|
|
479
|
+
* P-B10: a net service with relay: {url, token?, tokenUrl?} ({port, kind: 'net', relay,
|
|
480
|
+
* config?, maxConns?}) is the guest's network: a network Worker (src/net/net-worker.js) runs
|
|
481
|
+
* a TCP/IP stack for the guest's virtio-net frames and carries its TCP connections and DNS
|
|
482
|
+
* through the relay (web/relay). Pass --net PORT (and --mac) to vm.wasm. vm.net has its
|
|
483
|
+
* stats; timings.net has them at the end.
|
|
484
|
+
* W56: {port, kind: 'net', switch, name, ip?, relay?}: the guest is on an in-page network
|
|
485
|
+
* (new NetSwitch(), src/net/switch.js) with the other guests of the page on the same switch:
|
|
486
|
+
* they reach each other by address and by name (name, name.wmd.internal), with or without a
|
|
487
|
+
* relay; the guest's address is `ip`, else the one its name had on the switch, else the next
|
|
488
|
+
* free one (switch.address(name) tells it before the boot, for the bundle's /etc/hosts).
|
|
489
|
+
* A restored guest must come back with the same address (same name, or the same ip). Without `ip` the
|
|
490
|
+
* address follows the order names first reach the switch: to restore after a page reload, give `ip`.
|
|
491
|
+
* @param {(text:string)=>void} [o.onConsole] text the guest wrote to the console channel (hvc0)
|
|
492
|
+
* @param {boolean} [o.checkpoint] P-B9: the guest gets its checkpoint thread (wmd.ctl_attach) and point import
|
|
493
|
+
* (wmd.point), the boot gets a key (the vm.wasm, kernel, command line, packs it ran), and the handle gets
|
|
494
|
+
* checkpoint(): memory 0, the 9P service's files and fids, the JIT slot list and the fd table, taken while the
|
|
495
|
+
* guest is stopped at its checkpoint point. Needs channels with a 9P service, and the same options to restore.
|
|
496
|
+
* @param {boolean} [o.checkpointHeapTop] P-B9: bound the scan at vm.wasm's vm_heap_top() (default on, W58: false reads all of memory 0)
|
|
497
|
+
* @param {number} [o.checkpointScanLimit] P-B9: bytes of memory 0 a checkpoint reads (default all). Above it pages are taken as zero:
|
|
498
|
+
* only safe with the guest's real heap top (WebKit commits every page a scan reads, W34)
|
|
499
|
+
* @param {object} [o.restore] P-B9: a checkpoint ({header, sections}, from vm.checkpoint() or a store's load())
|
|
500
|
+
* to continue from instead of booting: the same options as the boot that took it (a different vm.wasm, kernel,
|
|
501
|
+
* command line, packs or bundle is refused with a CheckpointError, code 'stale'). The guest comes back as it was;
|
|
502
|
+
* its console and control ports are reset (the init opens the control port again), network flows are gone.
|
|
503
|
+
* @param {boolean} [o.stallHook] P-B8 test hook: vm.stallThread(tid, ms) stops one guest
|
|
504
|
+
* thread's Worker at its next host call (default off; when
|
|
505
|
+
* off the Workers do not look at the stall cells)
|
|
506
|
+
*/
|
|
507
|
+
export async function bootWmd(o) {
|
|
508
|
+
if (!globalThis.crossOriginIsolated) {
|
|
509
|
+
throw new Error('page is not cross-origin isolated (needs COOP same-origin + COEP require-corp)')
|
|
510
|
+
}
|
|
511
|
+
const t0 = absMs()
|
|
512
|
+
const timings = { t0 }
|
|
513
|
+
const ev = e => o.onEvent?.(e)
|
|
514
|
+
// W24: the start-up events (host/boot-events.js); be.close() at the end
|
|
515
|
+
const be = createBootEvents({ emit: ev, t0, now: absMs })
|
|
516
|
+
// the emulator's last stderr text, for the error a wait on a dead machine gives
|
|
517
|
+
let errTail = ''
|
|
518
|
+
const emitErr = text => {
|
|
519
|
+
errTail = (errTail + text).slice(-4096)
|
|
520
|
+
o.onStderr?.(text)
|
|
521
|
+
}
|
|
522
|
+
const preopen = o.preopen || '/g'
|
|
523
|
+
const argv = ['vm.wasm', ...o.args]
|
|
524
|
+
const env = vmEnv(o.env, o.heapReserve ?? heapReserveDefault())
|
|
525
|
+
// the default Worker is started from a literal new URL(..., import.meta.url) so bundlers (Vite, webpack)
|
|
526
|
+
// see it and bundle worker.js with its imports; a variable URL is copied as a plain asset (2.15)
|
|
527
|
+
const workerUrl = o.workerUrl
|
|
528
|
+
// The flat layout (host/ram-mem.js): a guest with more RAM than memory 0 can hold gets its RAM in a memory64 of its
|
|
529
|
+
// own. Chosen when --ram is above MAX_BROWSER_RAM_MIB and the engine has memory64 (o.flatRam forces it on or off);
|
|
530
|
+
// without it such a --ram is the error it was.
|
|
531
|
+
const flatRam = wantsFlatRam(o.args, o)
|
|
532
|
+
if (!flatRam) ramMiB(o.args, o.allowLargeRam) // a bad or too big --ram is a clear error before anything is downloaded
|
|
533
|
+
const maximum = o.maxPages || (flatRam ? flatMaxPages(o.args) : memoryMaxPages(o.args, 1024, o.allowLargeRam))
|
|
534
|
+
if (flatRam) {
|
|
535
|
+
if (o.restore || o.restoreLazy) throw new Error(`--ram ${ramMiB(o.args, true)}: a guest with more than ${MAX_BROWSER_RAM_MIB} MiB cannot start from a checkpoint yet`)
|
|
536
|
+
env.push('WMD_RAM_FLAT=1')
|
|
537
|
+
}
|
|
538
|
+
const ramMem = flatRam ? createRamMemory(ramMiB(o.args, true)) : null
|
|
539
|
+
// P-B9: checkpoints. restoreIn: the checkpoint's parts (parseCheckpoint refuses a malformed one).
|
|
540
|
+
// A flat guest takes none yet: a checkpoint holds memory 0 only.
|
|
541
|
+
const ckptOn = !flatRam && (!!o.checkpoint || !!o.restore || !!o.restoreLazy)
|
|
542
|
+
let restoreIn = o.restore ? parseCheckpoint(o.restore) : null
|
|
543
|
+
// P-B6: a base snapshot that is still downloading ({header: Promise, ckpt: Promise, abort()}, src/base.js openBase()). The
|
|
544
|
+
// boot downloads its own files meanwhile; the snapshot's header is checked against this boot's vm.wasm and boot key as soon
|
|
545
|
+
// as each is known. With o.restoreFallback a snapshot that is refused (other vm.wasm or boot key, cut, damaged, absent)
|
|
546
|
+
// does not fail the boot: it goes on cold, and timings.baseFallback says why.
|
|
547
|
+
let lazyIn = o.restoreLazy || null
|
|
548
|
+
let baseLazy = lazyIn // kept until the restore has succeeded (accept) or failed (reject)
|
|
549
|
+
// The handle must not keep the checkpoint it came from alive (about 500 MB): vm.restore() builds the next machine from
|
|
550
|
+
// `o`, so `o` carries no restore and no assets. (A caller that holds the checkpoint object holds it for its own reasons.)
|
|
551
|
+
const adoptIn = o._adopt || null // W57: the replaced machine's JIT hub, kept running by vm.restore() on the same handle
|
|
552
|
+
// W59: the network's identity across vm.restore(): the machine's instance id (one per bootWmd handle chain; the relay
|
|
553
|
+
// replaces this instance's old session) and its placeholder map (names the guest looked up -> 198.18.0.0/15 addresses).
|
|
554
|
+
// The checkpoint's own map comes first; the handle's names it does not conflict with are kept too (never recycled).
|
|
555
|
+
const netIn = o._net || null
|
|
556
|
+
const netInstance = netIn?.instance || Array.from(crypto.getRandomValues(new Uint8Array(16)), b => b.toString(16).padStart(2, '0')).join('')
|
|
557
|
+
const netNames = new Map()
|
|
558
|
+
const netIps = new Set()
|
|
559
|
+
for (const [n, ip] of [...(restoreIn?.names || []), ...(netIn?.names || [])]) {
|
|
560
|
+
if (netNames.has(n) || netIps.has(ip)) continue
|
|
561
|
+
netNames.set(n, ip)
|
|
562
|
+
netIps.add(ip)
|
|
563
|
+
}
|
|
564
|
+
let netStatus = null
|
|
565
|
+
const netStatusCbs = new Set()
|
|
566
|
+
const netNameWaiters = new Map()
|
|
567
|
+
o = { ...o, restore: undefined, restoreLazy: undefined, assets: o.assets && undefined, _assets: o.assets, _adopt: undefined, _net: undefined }
|
|
568
|
+
let heldAssets = null // what vm.restore() hands the next boot: bytes, Module, boot files, packs, keys
|
|
569
|
+
let restoring = false
|
|
570
|
+
let ck = null // the checkpoint being taken (declared early: finish() looks at it)
|
|
571
|
+
const ckptT = {}
|
|
572
|
+
let vmKey = null
|
|
573
|
+
let bootKey = null
|
|
574
|
+
let bootDesc = null
|
|
575
|
+
let packDigests = null
|
|
576
|
+
// W24: where the restored snapshot came from ('opfs' | 'network' for a base snapshot, 'memory' for vm.restore()), and its size
|
|
577
|
+
let baseSource = null
|
|
578
|
+
let baseTotal = null
|
|
579
|
+
let baseBytes = null
|
|
580
|
+
|
|
581
|
+
// Abort: listen from the start. Before the Workers exist an abort rejects every await
|
|
582
|
+
// below at once (raceAbort); afterwards it ends the machine (finish).
|
|
583
|
+
const signal = o.signal
|
|
584
|
+
let onAbortAfterStart = null
|
|
585
|
+
let rejectAbort
|
|
586
|
+
const abortP = new Promise((_, rej) => (rejectAbort = rej))
|
|
587
|
+
abortP.catch(() => {})
|
|
588
|
+
const abortError = () => new DOMException('boot aborted', 'AbortError')
|
|
589
|
+
const onAbort = () => (onAbortAfterStart ? onAbortAfterStart() : rejectAbort(abortError()))
|
|
590
|
+
if (signal?.aborted) throw abortError()
|
|
591
|
+
signal?.addEventListener('abort', onAbort)
|
|
592
|
+
const raceAbort = p => {
|
|
593
|
+
p.catch(() => {}) // once aborted nobody awaits p; its own failure must not go unhandled
|
|
594
|
+
return Promise.race([p, abortP])
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// 1. Downloads (in parallel). vm.wasm is compiled from its bytes so the compile
|
|
598
|
+
// time is measured on its own.
|
|
599
|
+
const names = Object.keys(o.files)
|
|
600
|
+
// P-B9: o.assets is what an earlier boot of this handle already had (vm.restore passes it): vm.wasm's bytes and compiled
|
|
601
|
+
// Module, the boot files and the packs, and the boot's keys. Nothing is downloaded, hashed or compiled again, and the
|
|
602
|
+
// old machine is not stopped until the new one's inputs are in hand.
|
|
603
|
+
const A = o._assets || null
|
|
604
|
+
o._assets = undefined // the handle keeps no reference of its own to the previous boot's assets
|
|
605
|
+
// W24: one download per file, with its bytes as they come. A file the caller passed as bytes, or one a restore reuses
|
|
606
|
+
// (o.assets), is reported once, done and cached.
|
|
607
|
+
const download = (file, url, shared, extra = {}) => {
|
|
608
|
+
const kind = `download:${file}`
|
|
609
|
+
const base = { t: 'boot', stage: 'download', file, ...extra }
|
|
610
|
+
let finalUrl = url
|
|
611
|
+
const before = timingCount(url)
|
|
612
|
+
const ts = absMs()
|
|
613
|
+
return fetchShared(url, o.signal, (bytes, total) => be.progress(kind, { ...base, state: 'progress', bytes, total, cached: null }), shared, r => (finalUrl = r.url || url)).then(async buf => {
|
|
614
|
+
const tookMs = Math.round(absMs() - ts)
|
|
615
|
+
// the browser adds a response's Resource Timing entry a little after its body is read (Chromium: a small file was
|
|
616
|
+
// read before its entry existed): wait for it, at most 200 ms
|
|
617
|
+
for (let i = 0; i < 20 && timingCount(url) <= before && !o.signal?.aborted; i++) await new Promise(r => setTimeout(r, 10))
|
|
618
|
+
const n = buf.byteLength
|
|
619
|
+
be.now(kind, { ...base, state: 'done', bytes: n, total: n, cached: cachedFromTiming(url, { bytes: n, finalUrl, before }), tookMs })
|
|
620
|
+
return buf
|
|
621
|
+
})
|
|
622
|
+
}
|
|
623
|
+
// reused: vm.restore() hands this boot the last one's bytes (nothing fetched: cached); bytes the caller passed came from
|
|
624
|
+
// wherever the caller got them, which wmd-web cannot see (cached null, given true)
|
|
625
|
+
const given = (file, buf, extra = {}) => {
|
|
626
|
+
be.now(`download:${file}`, { t: 'boot', stage: 'download', file, ...extra, state: 'done', bytes: buf.byteLength, total: buf.byteLength, cached: A ? true : null, ...(A ? {} : { given: true }), tookMs: 0 })
|
|
627
|
+
return buf
|
|
628
|
+
}
|
|
629
|
+
let lastDownloadAt = t0
|
|
630
|
+
const downloaded = x => ((lastDownloadAt = Math.max(lastDownloadAt, absMs())), x)
|
|
631
|
+
const vmP = A
|
|
632
|
+
? Promise.resolve(given('vm.wasm', A.vmBytes))
|
|
633
|
+
: o.vmUrl instanceof Uint8Array || o.vmUrl instanceof ArrayBuffer
|
|
634
|
+
? Promise.resolve(given('vm.wasm', o.vmUrl instanceof ArrayBuffer ? o.vmUrl : o.vmUrl.slice().buffer)) // 2.7: bytes the page already checked
|
|
635
|
+
: download('vm.wasm', o.vmUrl, false).then(b => {
|
|
636
|
+
timings.vmDownloadMs = absMs() - t0
|
|
637
|
+
return downloaded(b)
|
|
638
|
+
})
|
|
639
|
+
const fileName = n => bootFileName(n, o.args, preopen)
|
|
640
|
+
const filesP = A
|
|
641
|
+
? Promise.resolve(A.files.map(f => given(fileName(f.path), f.buffer)))
|
|
642
|
+
: Promise.all(names.map(n => (typeof o.files[n] === 'string' ? download(fileName(n), o.files[n], true) : Promise.resolve(given(fileName(n), toShared(o.files[n])))))).then(bufs => {
|
|
643
|
+
timings.filesDownloadMs = absMs() - t0
|
|
644
|
+
return downloaded(bufs)
|
|
645
|
+
})
|
|
646
|
+
filesP.catch(() => {}) // awaited below; must not go unhandled if vm.wasm fails first
|
|
647
|
+
// P-B3: the channel services' packs (fetched in parallel with the rest); pack:<n> numbers them over all services
|
|
648
|
+
const chanSvcs = o.channels?.services || []
|
|
649
|
+
let packN = 0
|
|
650
|
+
const packIds = chanSvcs.map(svc => (svc.layers || []).map(() => packN++))
|
|
651
|
+
const layersP = A
|
|
652
|
+
? Promise.resolve(A.chanLayers.map((ls, i) => ls.map((b, j) => given(`pack:${packIds[i][j]}`, b, { port: chanSvcs[i].port }))))
|
|
653
|
+
: Promise.all(
|
|
654
|
+
chanSvcs.map((svc, i) =>
|
|
655
|
+
Promise.all((svc.layers || []).map((l, j) => (typeof l === 'string' ? download(`pack:${packIds[i][j]}`, l, true, { port: svc.port }) : Promise.resolve(given(`pack:${packIds[i][j]}`, packShared(l), { port: svc.port }))))),
|
|
656
|
+
),
|
|
657
|
+
).then(downloaded)
|
|
658
|
+
layersP.catch(() => {})
|
|
659
|
+
let module, files, chanLayers, tableMin, vmBytes
|
|
660
|
+
function baseRefused(err) {
|
|
661
|
+
const lazy = lazyIn || baseLazy
|
|
662
|
+
lazyIn = null
|
|
663
|
+
baseLazy = null
|
|
664
|
+
try {
|
|
665
|
+
lazy?.abort?.(err)
|
|
666
|
+
} catch {}
|
|
667
|
+
if (!o.restoreFallback) throw err
|
|
668
|
+
timings.baseFallback = { code: err && err.code ? err.code : 'error', message: err && err.message ? err.message : String(err) }
|
|
669
|
+
// W24: the restore stage ends here, unused; the kernel stage follows
|
|
670
|
+
be.now('restore', { t: 'boot', stage: 'restore', state: 'done', used: false, fallback: timings.baseFallback.code, bytes: baseBytes, total: baseTotal, source: baseSource })
|
|
671
|
+
emitErr(`\n[wmd-web] base snapshot not used (${timings.baseFallback.code}): ${timings.baseFallback.message}\n[wmd-web] booting the kernel instead\n`)
|
|
672
|
+
}
|
|
673
|
+
try {
|
|
674
|
+
vmBytes = await raceAbort(vmP)
|
|
675
|
+
// the vm key first (1.4 MB of hashing): a snapshot of another vm.wasm is refused from its header before anything else is spent on it
|
|
676
|
+
if (ckptOn) vmKey = A ? A.keys.vmKey : await raceAbort(sha256Hex(new Uint8Array(vmBytes)))
|
|
677
|
+
if (lazyIn) {
|
|
678
|
+
try {
|
|
679
|
+
const h = await raceAbort(lazyIn.header)
|
|
680
|
+
checkVmKeyFits(h, vmKey)
|
|
681
|
+
lazyIn.proceed?.() // the reader decodes only from here: a refused header costs what was queued, not the file
|
|
682
|
+
} catch (err) {
|
|
683
|
+
if (signal?.aborted) throw err
|
|
684
|
+
baseRefused(err)
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
const tc = absMs()
|
|
688
|
+
be.now('compile', { t: 'boot', stage: 'compile', state: 'start', moduleBytes: vmBytes.byteLength, reused: !!A })
|
|
689
|
+
module = A ? A.module : await raceAbort(WebAssembly.compile(vmBytes))
|
|
690
|
+
timings.compileMs = absMs() - tc
|
|
691
|
+
be.now('compile', { t: 'boot', stage: 'compile', state: 'done', moduleBytes: vmBytes.byteLength, reused: !!A, tookMs: Math.round(timings.compileMs) })
|
|
692
|
+
tableMin = tableMinOf(vmBytes) ?? undefined
|
|
693
|
+
timings.tableMin = tableMin
|
|
694
|
+
const bufs = await raceAbort(filesP)
|
|
695
|
+
timings.downloadMs = Math.max(timings.vmDownloadMs || 0, timings.filesDownloadMs || 0)
|
|
696
|
+
timings.fileBytes = bufs.reduce((n, b) => n + b.byteLength, 0) + vmBytes.byteLength
|
|
697
|
+
files = names.map((n, i) => ({ path: n, buffer: bufs[i] }))
|
|
698
|
+
chanLayers = await raceAbort(layersP)
|
|
699
|
+
timings.layerBytes = chanLayers.flat().reduce((n, b) => n + b.byteLength, 0)
|
|
700
|
+
timings.allDownloadMs = lastDownloadAt - t0 // vm.wasm, the boot files and the packs all in
|
|
701
|
+
be.now('download', { t: 'boot', stage: 'download', state: 'done', fileBytes: timings.fileBytes, layerBytes: timings.layerBytes, tookMs: Math.round(timings.allDownloadMs) })
|
|
702
|
+
if (ckptOn) {
|
|
703
|
+
// the boot's identity: a checkpoint restores only into the same one (research/10 R11)
|
|
704
|
+
const tk = absMs()
|
|
705
|
+
let packs
|
|
706
|
+
if (A) {
|
|
707
|
+
bootKey = A.keys.bootKey
|
|
708
|
+
bootDesc = A.keys.bootDesc
|
|
709
|
+
packs = A.keys.packs
|
|
710
|
+
} else {
|
|
711
|
+
// the boot key samples each pack (head and tail); packs is the full digest of every layer (a SHA-256 tree over 8 MiB pieces), recorded in
|
|
712
|
+
// every checkpoint this boot takes and checked against a base snapshot's before it is used
|
|
713
|
+
const [k, pk] = await raceAbort(Promise.all([bootKeyOf({ vmKey, argv, env, preopen, files, services: chanSvcs.map((s, i) => ({ ...s, layers: chanLayers[i] })), maxPages: maximum }), packDigestsOf(chanLayers)]))
|
|
714
|
+
bootKey = k.key
|
|
715
|
+
bootDesc = k.desc
|
|
716
|
+
packs = pk
|
|
717
|
+
}
|
|
718
|
+
ckptT.keyMs = absMs() - tk
|
|
719
|
+
heldAssets = { vmBytes, module, tableMin, files, chanLayers, keys: { vmKey, bootKey, bootDesc, packs } }
|
|
720
|
+
packDigests = packs
|
|
721
|
+
if (lazyIn) {
|
|
722
|
+
const tw = absMs()
|
|
723
|
+
const info = lazyIn.info
|
|
724
|
+
const restoreProgress = () => {
|
|
725
|
+
if (!info) return
|
|
726
|
+
const att = info.attempts?.[info.attempts.length - 1]
|
|
727
|
+
if (!att) return
|
|
728
|
+
baseSource = att.source
|
|
729
|
+
baseTotal = att.size ?? info.entry?.bytes ?? null
|
|
730
|
+
be.progress('restore', { t: 'boot', stage: 'restore', state: 'progress', bytes: info.netBytes, total: baseTotal, source: baseSource })
|
|
731
|
+
}
|
|
732
|
+
restoreProgress()
|
|
733
|
+
const restoreTimer = setInterval(restoreProgress, 250)
|
|
734
|
+
try {
|
|
735
|
+
const box = await raceAbort(lazyIn.ckpt).finally(() => {
|
|
736
|
+
clearInterval(restoreTimer)
|
|
737
|
+
restoreProgress()
|
|
738
|
+
baseBytes = info?.netBytes ?? null
|
|
739
|
+
})
|
|
740
|
+
const got = typeof box.take === 'function' ? box.take() : box // a box hands the snapshot over once: the opener keeps no reference to its 0.5 GB
|
|
741
|
+
ckptT.baseWaitMs = absMs() - tw // the time the boot waited for the snapshot after everything else was in
|
|
742
|
+
const parsed = parseCheckpoint(got)
|
|
743
|
+
checkCheckpointFits(parsed.header, { vmKey, bootKey, bootDesc, maxPages: maximum, hasJit: o.jit !== false, fmaFused: fmaFuses() })
|
|
744
|
+
checkPacksFit(parsed.header.packs, packDigests)
|
|
745
|
+
restoreIn = parsed
|
|
746
|
+
lazyIn = null // baseLazy stays: accept() when the restore has succeeded, reject() if it fails
|
|
747
|
+
} catch (err) {
|
|
748
|
+
if (signal?.aborted) throw err
|
|
749
|
+
baseRefused(err)
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (restoreIn) checkCheckpointFits(restoreIn.header, { vmKey, bootKey, bootDesc, maxPages: maximum, hasJit: o.jit !== false, fmaFused: fmaFuses() })
|
|
753
|
+
}
|
|
754
|
+
if (signal?.aborted) throw abortError()
|
|
755
|
+
} catch (err) {
|
|
756
|
+
signal?.removeEventListener('abort', onAbort)
|
|
757
|
+
be.close()
|
|
758
|
+
// whatever ended the boot (the caller's signal, a failed download, a compile error): the snapshot download, the decoded
|
|
759
|
+
// snapshot and the pending cache copy are dropped here, not left for the garbage collector
|
|
760
|
+
const lazy = lazyIn || baseLazy
|
|
761
|
+
lazyIn = null
|
|
762
|
+
baseLazy = null
|
|
763
|
+
try {
|
|
764
|
+
lazy?.abort?.(err)
|
|
765
|
+
} catch {}
|
|
766
|
+
throw signal?.aborted ? abortError() : err
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// 2. Shared memory and control block.
|
|
770
|
+
const tMem = absMs()
|
|
771
|
+
let memory = new WebAssembly.Memory({ initial: VM_MIN_PAGES, maximum, shared: true })
|
|
772
|
+
const memoryInitialBytes = memory.buffer.byteLength
|
|
773
|
+
const ctlSab = createControl()
|
|
774
|
+
const ctl = new Int32Array(ctlSab)
|
|
775
|
+
if (restoreIn) {
|
|
776
|
+
// The fd table now; the memory pages when the main thread has made its instance (restoreMemory, from the
|
|
777
|
+
// 'resume-instantiated' message): vm.wasm's start function must see an untouched memory (worker.js).
|
|
778
|
+
restoreFdTable(ctl, restoreIn.ctl, files.length)
|
|
779
|
+
ckptT.pagesRestored = restoreIn.index.length
|
|
780
|
+
}
|
|
781
|
+
function restoreMemory() {
|
|
782
|
+
try {
|
|
783
|
+
restoreMemoryNow()
|
|
784
|
+
// the snapshot restored: its cache copy may be committed now (not before: a snapshot that fails to restore must not stay cached)
|
|
785
|
+
const lazy = baseLazy
|
|
786
|
+
baseLazy = null
|
|
787
|
+
lazy?.accept?.()
|
|
788
|
+
} catch (err) {
|
|
789
|
+
const lazy = baseLazy
|
|
790
|
+
baseLazy = null
|
|
791
|
+
try {
|
|
792
|
+
lazy?.reject?.(err) // the cache entry this snapshot came from is removed: the next load downloads again
|
|
793
|
+
} catch {}
|
|
794
|
+
// the main thread waits for this (worker.js): tell it, instead of leaving it to its 120 s timeout
|
|
795
|
+
Atomics.store(ctl, CTL_CK_RESTORED, 2)
|
|
796
|
+
Atomics.notify(ctl, CTL_CK_RESTORED)
|
|
797
|
+
o.onStderr?.(`\n[wmd-web] restoring the checkpoint's memory failed: ${err && err.message ? err.message : err}\n`)
|
|
798
|
+
finish(4, `restore failed: ${err && err.message ? err.message : err}`)
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function restoreMemoryNow() {
|
|
802
|
+
const tm = absMs()
|
|
803
|
+
const have = memory.buffer.byteLength / CKPT_PAGE
|
|
804
|
+
if (restoreIn.header.memPages > have) memory.grow(restoreIn.header.memPages - have)
|
|
805
|
+
const u8 = new Uint8Array(memory.buffer)
|
|
806
|
+
const kept = new Uint8Array(restoreIn.header.memPages)
|
|
807
|
+
let k = 0
|
|
808
|
+
for (const c of restoreIn.memChunks) {
|
|
809
|
+
for (let off = 0; off < c.length; off += CKPT_PAGE) {
|
|
810
|
+
const p = restoreIn.index[k++]
|
|
811
|
+
u8.set(c.subarray(off, off + CKPT_PAGE), p * CKPT_PAGE)
|
|
812
|
+
kept[p] = 1
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
// pages the instance's start function wrote that were zero at the checkpoint (the runner's zero_absent): the static region
|
|
816
|
+
let zeroed = 0
|
|
817
|
+
const w = new Int32Array(memory.buffer, 0, Math.min(restoreIn.header.memPages, VM_MIN_PAGES) * (CKPT_PAGE / 4))
|
|
818
|
+
for (let p = 0; p * (CKPT_PAGE / 4) < w.length; p++) {
|
|
819
|
+
if (kept[p]) continue
|
|
820
|
+
const lo = p * (CKPT_PAGE / 4)
|
|
821
|
+
let nz = false
|
|
822
|
+
for (let i = lo; i < lo + CKPT_PAGE / 4; i += 8) {
|
|
823
|
+
if ((w[i] | w[i + 1] | w[i + 2] | w[i + 3] | w[i + 4] | w[i + 5] | w[i + 6] | w[i + 7]) !== 0) {
|
|
824
|
+
nz = true
|
|
825
|
+
break
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
if (nz) {
|
|
829
|
+
w.fill(0, lo, lo + CKPT_PAGE / 4)
|
|
830
|
+
zeroed++
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
ckptT.instantiatedMs = tm - t0 // boot start to the main thread's instance made (key, download, replay, Worker start included)
|
|
834
|
+
ckptT.memoryMs = absMs() - tm
|
|
835
|
+
ckptT.zeroedAfterInstantiate = zeroed
|
|
836
|
+
ckptT.pagesWrittenAt = absMs() - t0
|
|
837
|
+
Atomics.store(ctl, CTL_CK_RESTORED, 1)
|
|
838
|
+
Atomics.notify(ctl, CTL_CK_RESTORED)
|
|
839
|
+
// everything of the checkpoint has been copied into this machine (memory, fd table, the 9P state and the JIT store went
|
|
840
|
+
// earlier): do not pin it for the machine's life
|
|
841
|
+
if (baseSource === null) {
|
|
842
|
+
baseSource = 'memory' // vm.restore() or bootWmd({restore}): the checkpoint was in the page's memory
|
|
843
|
+
baseBytes = baseTotal = restoreIn.memChunks.reduce((n, c) => n + c.length, 0)
|
|
844
|
+
}
|
|
845
|
+
be.now('restore', { t: 'boot', stage: 'restore', state: 'done', used: true, bytes: baseBytes, total: baseTotal, source: baseSource, tookMs: Math.round(ckptT.pagesWrittenAt) })
|
|
846
|
+
restoreIn.memChunks = null
|
|
847
|
+
restoreIn.index = null
|
|
848
|
+
restoreIn.jit = null
|
|
849
|
+
restoreIn.svc = null
|
|
850
|
+
restoreIn.ctl = null
|
|
851
|
+
}
|
|
852
|
+
// o.jit === false: no JIT door; vm.wasm gets the stubbed jit_* imports (-1, counted
|
|
853
|
+
// in the host-call counters), as before P-B2. Otherwise the door (src/host/jit.js).
|
|
854
|
+
// The log is reclaimed while sharing. A Worker that cannot receive a Module from the hub ('messageerror':
|
|
855
|
+
// Firefox past its per-process wasm code limit), or that starts after the log reused the bytes of early
|
|
856
|
+
// Modules, compiles its copy from bytes the hub keeps (W31b), so every browser reclaims. o.jitReclaim overrides.
|
|
857
|
+
const jitStore = o.jit === false ? null : restoreIn?.jit ? restoreStore(restoreIn.jit, { reclaim: o.jitReclaim ?? true }) : createJitStore({ reclaim: o.jitReclaim ?? true })
|
|
858
|
+
const stdio = createStdio(o.stdioRingBytes ?? BOOT_RING)
|
|
859
|
+
let memTook = absMs() - tMem // the memory, the control block, the JIT store and the stdio rings (the channel block is added below)
|
|
860
|
+
const out = stdioReader(stdio)
|
|
861
|
+
const outText = stdioText(out)
|
|
862
|
+
timings.stdioRingBytes = out.ringBytes
|
|
863
|
+
// W77: the cache is the page's object (functions): it stays here, the Workers get ports
|
|
864
|
+
const { cache: jitCacheStore, ...jitRest } = o.jit || {}
|
|
865
|
+
const jitOpts = { compileOnCompile: 'validate', ...jitRest }
|
|
866
|
+
// W34: WebKit keeps every live module until a flush and kills the page at 8 GiB. Past the budget the
|
|
867
|
+
// door answers JIT_OVER_BUDGET and the guest releases every module (one flush) and compiles again.
|
|
868
|
+
if (jitOpts.maxLiveBytes === undefined) jitOpts.maxLiveBytes = jitBudgetDefault(maximum * 65536)
|
|
869
|
+
timings.jitMaxLiveBytes = jitOpts.maxLiveBytes
|
|
870
|
+
const share = !jitStore ? false : jitOpts.share === undefined || jitOpts.share === 'auto' ? jitShareDefault() : !!jitOpts.share
|
|
871
|
+
if (share && !haveJspi()) {
|
|
872
|
+
signal?.removeEventListener('abort', onAbort)
|
|
873
|
+
be.close()
|
|
874
|
+
throw new Error('jit.share needs JSPI (WebAssembly.Suspending / promising)')
|
|
875
|
+
}
|
|
876
|
+
timings.jitShare = share
|
|
877
|
+
// W77: the JIT's module cache (host/jit-cache.js): the index is read before any Worker starts
|
|
878
|
+
let jitCache = null
|
|
879
|
+
let knownSab = null
|
|
880
|
+
if (jitStore && jitCacheStore) {
|
|
881
|
+
if (!share) {
|
|
882
|
+
timings.jitCache = { on: false, why: 'the JIT cache needs module sharing (JSPI)' }
|
|
883
|
+
} else {
|
|
884
|
+
const tag = vmKey ?? (await raceAbort(sha256Hex(new Uint8Array(vmBytes))))
|
|
885
|
+
jitCache = createJitCacheHost({ cache: jitCacheStore, tag, log: s => emitErr(`\n[${s}]\n`) })
|
|
886
|
+
knownSab = await raceAbort(jitCache.load())
|
|
887
|
+
timings.jitCache = { on: true, ...jitCache.stats }
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// P-B3: the channel block and its service Worker.
|
|
892
|
+
let chanSab = null
|
|
893
|
+
let chanSvc = null
|
|
894
|
+
let chanDied = null
|
|
895
|
+
// P-B10: the network Worker, when a net service has a relay
|
|
896
|
+
let netWorker = null
|
|
897
|
+
let netStats = null
|
|
898
|
+
let netMember = null // W56: this guest on an in-page switch (switch.js attach())
|
|
899
|
+
const netWaiters = new Map()
|
|
900
|
+
let chanLock = null
|
|
901
|
+
let chanStats = null
|
|
902
|
+
let chanBytesSeen = 0
|
|
903
|
+
let controlUp = false
|
|
904
|
+
let collectDone = null
|
|
905
|
+
const upperWaiters = new Map() // W62: export-upper id -> {res, rej}
|
|
906
|
+
// P-B11: wmd's control session (a control service with yamux: true), driven from the page
|
|
907
|
+
const ctlService = chanSvcs.find(s => (s.kind === 'control' || s.kind === 3) && s.yamux)
|
|
908
|
+
const control = ctlService
|
|
909
|
+
? new ControlClient({
|
|
910
|
+
post: m => {
|
|
911
|
+
if (!chanSvc || done || chanDied) {
|
|
912
|
+
if (m.op === 'open') queueMicrotask(() => control.onEvents([{ ev: 'closed', k: m.k, why: 'the machine has ended' }]))
|
|
913
|
+
return
|
|
914
|
+
}
|
|
915
|
+
if (m.bytes) {
|
|
916
|
+
const b = m.bytes instanceof Uint8Array ? m.bytes.slice() : new Uint8Array(m.bytes)
|
|
917
|
+
chanSvc.postMessage({ ...m, bytes: b }, [b.buffer])
|
|
918
|
+
} else chanSvc.postMessage(m)
|
|
919
|
+
},
|
|
920
|
+
})
|
|
921
|
+
: null
|
|
922
|
+
// W24: the Workers stage. Every Worker started before the vCPU (the channel service, the network Worker, the JIT hub,
|
|
923
|
+
// the first pool Workers and the main one) ready = the stage is done; later changes are progress events.
|
|
924
|
+
const wl = createWorkerList(be.at)
|
|
925
|
+
const vcpus = vcpusOf(o.args)
|
|
926
|
+
let workersDone = false
|
|
927
|
+
let workersStarted = false // set once the main Worker has been started: before that the list is not whole
|
|
928
|
+
let tWorkers = null
|
|
929
|
+
const workersEv = () => {
|
|
930
|
+
const e = { t: 'boot', stage: 'workers', workers: wl.list(), vcpus, jitShared: share }
|
|
931
|
+
if (!workersDone && workersStarted && wl.allReady()) {
|
|
932
|
+
workersDone = true
|
|
933
|
+
be.now('workers', { ...e, state: 'done', tookMs: Math.round(absMs() - tWorkers) })
|
|
934
|
+
} else be.progress('workers', { ...e, state: 'progress' })
|
|
935
|
+
}
|
|
936
|
+
const workerState = (id, role, state) => {
|
|
937
|
+
if (tWorkers === null) tWorkers = absMs()
|
|
938
|
+
wl.set(id, role, state)
|
|
939
|
+
workersEv()
|
|
940
|
+
}
|
|
941
|
+
const consoleDec = new TextDecoder()
|
|
942
|
+
const consoleBuf = new Uint8Array(1 << 20)
|
|
943
|
+
timings.consoleBytes = 0
|
|
944
|
+
let consoleGate = null
|
|
945
|
+
let consoleFull = false
|
|
946
|
+
if (o.channels) {
|
|
947
|
+
// the cap on each 9P upper (F2: none before, so a guest could grow the Worker without bound)
|
|
948
|
+
const capDefault = o.channels.maxUpperBytes !== undefined ? { bytes: o.channels.maxUpperBytes, source: 'channels.maxUpperBytes' } : upperCapDefault()
|
|
949
|
+
timings.chanUppers = chanSvcs
|
|
950
|
+
.filter(s => s.kind === '9p' || s.kind === 2)
|
|
951
|
+
.map(s => (s.maxUpperBytes !== undefined ? { port: s.port, maxUpperBytes: s.maxUpperBytes, source: 'service maxUpperBytes' } : { port: s.port, maxUpperBytes: capDefault.bytes, source: capDefault.source }))
|
|
952
|
+
try {
|
|
953
|
+
const tc = absMs()
|
|
954
|
+
chanSab = createChanBlock({
|
|
955
|
+
ports: chanSvcs.map(s => ({ port: s.port, kind: s.kind })),
|
|
956
|
+
nslots: o.channels.nslots ?? 8,
|
|
957
|
+
ringBytes: o.channels.ringBytes ?? 1 << 20,
|
|
958
|
+
})
|
|
959
|
+
memTook += absMs() - tc
|
|
960
|
+
// W56: on an in-page switch the guest gets its address there (the DHCP lease) and a port for its peers;
|
|
961
|
+
// taken here, before any Worker exists, so a refused name (taken, invalid) leaves nothing behind
|
|
962
|
+
const sw = chanSvcs.find(s => (s.kind === 'net' || s.kind === 1) && s.switch)
|
|
963
|
+
if (sw) netMember = sw.switch.attach({ name: sw.name, ip: sw.ip })
|
|
964
|
+
} catch (err) {
|
|
965
|
+
signal?.removeEventListener('abort', onAbort)
|
|
966
|
+
be.close()
|
|
967
|
+
throw err // (no Worker exists yet)
|
|
968
|
+
}
|
|
969
|
+
const lockName = `wmd-chan-${Math.random().toString(36).slice(2)}-${Date.now()}`
|
|
970
|
+
chanSvc = new Worker(new URL('./host/chan-service.js', import.meta.url), { type: 'module', name: 'wmd-chan' })
|
|
971
|
+
workerState('chan-service', 'chan-service', 'starting')
|
|
972
|
+
chanSvc.onmessage = e => {
|
|
973
|
+
const m = e.data
|
|
974
|
+
if (m.t === 'stats') {
|
|
975
|
+
chanStats = m.stats
|
|
976
|
+
layerEvents(m.stats)
|
|
977
|
+
}
|
|
978
|
+
else if (m.t === 'upper-full') {
|
|
979
|
+
emitErr(
|
|
980
|
+
`\n[wmd-web] the guest's files on 9P port ${m.port} are full: the upper holds ${fmtMiB(m.used)} of its ${fmtMiB(m.max)} cap ` +
|
|
981
|
+
`(channels.maxUpperBytes); the guest's writes get ENOSPC (No space left on device)\n`,
|
|
982
|
+
)
|
|
983
|
+
ev({ t: 'chan', ev: 'upper-full', port: m.port, used: m.used, max: m.max })
|
|
984
|
+
}
|
|
985
|
+
else if (m.t === 'snap') ckptSvc(m)
|
|
986
|
+
else if (m.t === 'collected') collectDone?.(m.files)
|
|
987
|
+
else if (m.t === 'upper') {
|
|
988
|
+
const w = upperWaiters.get(m.id)
|
|
989
|
+
upperWaiters.delete(m.id)
|
|
990
|
+
if (w && m.error) w.rej(new Error(`exportUpper: ${m.error}`))
|
|
991
|
+
else if (w) w.res({ bytes: new Uint8Array(m.bytes.buffer || m.bytes), entries: m.entries, files: m.files, whiteouts: m.whiteouts, opaque: m.opaque, dataBytes: m.dataBytes, ms: m.ms })
|
|
992
|
+
}
|
|
993
|
+
else if (m.t === 'ctl') {
|
|
994
|
+
if (m.evs.some(e => e.ev === 'data')) watchdog.progress()
|
|
995
|
+
if (!controlUp && vcpuAt !== null && m.evs.some(e => e.ev === 'session' && e.up)) {
|
|
996
|
+
controlUp = true // W24: the first time the guest's control session came up
|
|
997
|
+
be.now('control', { t: 'boot', stage: 'control', state: 'done', tookMs: Math.round(absMs() - vcpuAt) })
|
|
998
|
+
}
|
|
999
|
+
control?.onEvents(m.evs)
|
|
1000
|
+
}
|
|
1001
|
+
else if (m.t === 'error') serviceDied(`the channel service failed: ${m.message.split('\n')[0]}`)
|
|
1002
|
+
else if (m.t === 'locked' && navigator.locks?.request) {
|
|
1003
|
+
// Granted only when the service Worker has ended, however it ended.
|
|
1004
|
+
chanLock = new AbortController()
|
|
1005
|
+
navigator.locks
|
|
1006
|
+
.request(lockName, { signal: chanLock.signal }, () => serviceDied('the channel service Worker ended'))
|
|
1007
|
+
.catch(() => {})
|
|
1008
|
+
} else if (m.t === 'ready') {
|
|
1009
|
+
ev({ t: 'chan', ev: 'ready' })
|
|
1010
|
+
workerState('chan-service', 'chan-service', 'ready')
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
chanSvc.onerror = err => {
|
|
1014
|
+
if (!done) workerState('chan-service', 'chan-service', 'failed')
|
|
1015
|
+
serviceDied(`the channel service Worker failed: ${err.message || err}`)
|
|
1016
|
+
}
|
|
1017
|
+
const netSvc = chanSvcs.find(s => (s.kind === 'net' || s.kind === 1) && (s.relay || s.switch))
|
|
1018
|
+
let netPort = null
|
|
1019
|
+
if (netSvc) {
|
|
1020
|
+
const mc = new MessageChannel()
|
|
1021
|
+
netPort = mc.port1
|
|
1022
|
+
netWorker = new Worker(new URL('./net/net-worker.js', import.meta.url), { type: 'module', name: 'wmd-net' })
|
|
1023
|
+
workerState('net', 'net', 'starting')
|
|
1024
|
+
netWorker.onmessage = e => {
|
|
1025
|
+
const m = e.data
|
|
1026
|
+
if (m.t === 'ready') workerState('net', 'net', 'ready')
|
|
1027
|
+
else if (m.t === 'stats') {
|
|
1028
|
+
netStats = m.stats
|
|
1029
|
+
const w = netWaiters.get(m.id)
|
|
1030
|
+
if (w) {
|
|
1031
|
+
netWaiters.delete(m.id)
|
|
1032
|
+
w(m.stats)
|
|
1033
|
+
}
|
|
1034
|
+
} else if (m.t === 'log') emitErr(`[wmd-web] ${m.line}\n`)
|
|
1035
|
+
else if (m.t === 'name') {
|
|
1036
|
+
if (!netNames.has(m.name) && !netIps.has(m.ip)) {
|
|
1037
|
+
netNames.set(m.name, m.ip)
|
|
1038
|
+
netIps.add(m.ip)
|
|
1039
|
+
}
|
|
1040
|
+
} else if (m.t === 'status') {
|
|
1041
|
+
netStatus = m.status
|
|
1042
|
+
for (const cb of netStatusCbs) {
|
|
1043
|
+
try {
|
|
1044
|
+
cb(m.status)
|
|
1045
|
+
} catch {}
|
|
1046
|
+
}
|
|
1047
|
+
} else if (m.t === 'names') {
|
|
1048
|
+
const w = netNameWaiters.get(m.id)
|
|
1049
|
+
if (w) {
|
|
1050
|
+
netNameWaiters.delete(m.id)
|
|
1051
|
+
w(m.names)
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
netWorker.onerror = err => {
|
|
1056
|
+
if (!done) workerState('net', 'net', 'failed')
|
|
1057
|
+
emitErr(`\n[wmd-web] the network Worker failed: ${err.message || err}\n`)
|
|
1058
|
+
}
|
|
1059
|
+
const r = netSvc.relay
|
|
1060
|
+
const relay = r ? { url: r.url, protocol: r.protocol, token: r.token, key: r.key, tokenUrl: r.tokenUrl && new URL(r.tokenUrl, location.href).href } : null
|
|
1061
|
+
const config = netMember ? { ...netSvc.config, ip: netMember.ip } : netSvc.config
|
|
1062
|
+
const transfer = netMember ? [mc.port2, netMember.port] : [mc.port2]
|
|
1063
|
+
netWorker.postMessage({ t: 'init', port: mc.port2, relay, instance: netInstance, names: [...netNames], switchPort: netMember?.port, config, maxConns: netSvc.maxConns }, transfer)
|
|
1064
|
+
}
|
|
1065
|
+
chanSvc.postMessage({
|
|
1066
|
+
t: 'init',
|
|
1067
|
+
sab: chanSab,
|
|
1068
|
+
lockName,
|
|
1069
|
+
spinUs: o.channels.spinUs,
|
|
1070
|
+
consoleInputBytes: o.channels.consoleInputBytes ?? CONSOLE_IN_LIMIT,
|
|
1071
|
+
services: chanSvcs.map((s, i) => ({
|
|
1072
|
+
port: s.port,
|
|
1073
|
+
kind: s.kind,
|
|
1074
|
+
at: s.at,
|
|
1075
|
+
files: s.files,
|
|
1076
|
+
yamux: !!s.yamux,
|
|
1077
|
+
maxUpperBytes: timings.chanUppers.find(u => u.port === s.port)?.maxUpperBytes,
|
|
1078
|
+
layers: chanLayers[i],
|
|
1079
|
+
restore: restoreIn?.svc[s.port], // P-B9: the 9P tree and fids of the checkpoint (copied: the checkpoint stays usable)
|
|
1080
|
+
})),
|
|
1081
|
+
netPort,
|
|
1082
|
+
}, netPort ? [netPort] : [])
|
|
1083
|
+
consoleGate = consoleInputGate(chanSab, o.channels.consoleInputBytes ?? CONSOLE_IN_LIMIT)
|
|
1084
|
+
}
|
|
1085
|
+
be.now('memory', { t: 'boot', stage: 'memory', state: 'done', initialBytes: memoryInitialBytes, maxBytes: maximum * PAGE, ringBytes: stdio.byteLength + (chanSab ? chanSab.byteLength : 0), tookMs: Math.round(memTook) })
|
|
1086
|
+
// W24: each 9P service's writable layer (its upper), from the service's stats (every 250 ms while they change); an
|
|
1087
|
+
// event when it is first seen and on each change of 1 MiB or more
|
|
1088
|
+
const layerSeen = new Map()
|
|
1089
|
+
function layerEvents(st) {
|
|
1090
|
+
for (const [port, u] of Object.entries(st?.uppers || {})) {
|
|
1091
|
+
const last = layerSeen.get(port)
|
|
1092
|
+
if (last !== undefined && Math.abs(u.used - last) < 1 << 20) continue
|
|
1093
|
+
layerSeen.set(port, u.used)
|
|
1094
|
+
be.progress(`layer:${port}`, { t: 'layer', port: Number(port), usedBytes: u.used, capBytes: u.max, kind: 'memory' })
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
function serviceDied(why) {
|
|
1098
|
+
if (done || chanDied) return
|
|
1099
|
+
chanDied = why
|
|
1100
|
+
markServiceDead(chanSab)
|
|
1101
|
+
emitErr(`\n[wmd-web] ${why}: every channel is closed\n`)
|
|
1102
|
+
control?.fail(`${why}: every channel is closed${errTail.trim() ? `; the emulator's last output:\n${errTail.trim().split('\n').slice(-12).join('\n')}` : ''}`)
|
|
1103
|
+
ev({ t: 'chan', ev: 'service-died', why })
|
|
1104
|
+
failUpperWaiters(why)
|
|
1105
|
+
}
|
|
1106
|
+
function failUpperWaiters(why) {
|
|
1107
|
+
for (const w of upperWaiters.values()) w.rej(new Error(`exportUpper: ${why}`))
|
|
1108
|
+
upperWaiters.clear()
|
|
1109
|
+
}
|
|
1110
|
+
const chanV = chanSab && chanView(chanSab)
|
|
1111
|
+
function drainConsole() {
|
|
1112
|
+
if (!chanV) return
|
|
1113
|
+
const v = chanV
|
|
1114
|
+
let text = ''
|
|
1115
|
+
for (;;) {
|
|
1116
|
+
const n = ringRead(v, v.pipe, consoleBuf)
|
|
1117
|
+
if (!n) break
|
|
1118
|
+
timings.consoleBytes += n
|
|
1119
|
+
text += consoleDec.decode(consoleBuf.subarray(0, n), { stream: true })
|
|
1120
|
+
}
|
|
1121
|
+
if (text) {
|
|
1122
|
+
ringBell(v) // the service may be waiting for room in the pipe
|
|
1123
|
+
watchdog.progress()
|
|
1124
|
+
o.onConsole?.(text)
|
|
1125
|
+
}
|
|
1126
|
+
// channel traffic is progress too (a guest busy on 9P may print nothing for a while)
|
|
1127
|
+
const c = v.f64
|
|
1128
|
+
let bytes = 0
|
|
1129
|
+
for (let s = 0; s < v.nslots; s++) bytes += c[s * 4] + c[s * 4 + 1]
|
|
1130
|
+
if (bytes !== chanBytesSeen) {
|
|
1131
|
+
chanBytesSeen = bytes
|
|
1132
|
+
watchdog.progress()
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
let hub = share ? new Worker(new URL('./host/jit-hub.js', import.meta.url), { type: 'module', name: 'wmd-jit-hub' }) : null
|
|
1136
|
+
if (hub && jitStore) hub.postMessage({ t: 'store', recs: jitStore.recs, ctl: jitStore.ctl }) // the hub marks records delivered (W31)
|
|
1137
|
+
// P-B9: a checkpoint needs the bytes of modules the log released; the hub keeps them (up to this much)
|
|
1138
|
+
if (hub && ckptOn) hub.postMessage({ t: 'config', bytesMax: o.ckptHubBytes ?? 1 << 30 })
|
|
1139
|
+
let hubUp = false
|
|
1140
|
+
let replayDone = null // P-B9/W57: the hub's answer to a restore's replay
|
|
1141
|
+
// W57: vm.restore() keeps this machine's hub past its end, for the next machine's hub to take its Modules from
|
|
1142
|
+
let keepHub = false
|
|
1143
|
+
let keptHub = null
|
|
1144
|
+
if (hub) {
|
|
1145
|
+
// W24: the hub's first answer (to a stats request) says its script runs
|
|
1146
|
+
workerState('jit-hub', 'jit-hub', 'starting')
|
|
1147
|
+
hub.onmessage = e => {
|
|
1148
|
+
if (e.data.t === 'bytes-of') ckptOn && ckptHub(e.data)
|
|
1149
|
+
else if (e.data.t === 'replayed') replayDone?.(e.data)
|
|
1150
|
+
else if (e.data.t === 'stats' && !hubUp && !done) {
|
|
1151
|
+
hubUp = true
|
|
1152
|
+
workerState('jit-hub', 'jit-hub', 'ready')
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
hub.onerror = () => !done && workerState('jit-hub', 'jit-hub', 'failed')
|
|
1156
|
+
hub.postMessage({ t: 'stats' })
|
|
1157
|
+
}
|
|
1158
|
+
/** A port to the hub for one Worker (null when not sharing). */
|
|
1159
|
+
const hubPort = () => {
|
|
1160
|
+
if (!hub) return null
|
|
1161
|
+
const ch = new MessageChannel()
|
|
1162
|
+
hub.postMessage({ t: 'port', port: ch.port1 }, [ch.port1])
|
|
1163
|
+
return ch.port2
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// 3. Workers.
|
|
1167
|
+
const workers = []
|
|
1168
|
+
const stdoutLines = { buf: '' }
|
|
1169
|
+
const marks = (o.marks || []).map(m => ({ ...m, hit: false }))
|
|
1170
|
+
let done = false
|
|
1171
|
+
const jitCachePorts = [] // W77: the page's ends of the Workers' cache ports
|
|
1172
|
+
let resolveExit
|
|
1173
|
+
const exited = new Promise(r => (resolveExit = r))
|
|
1174
|
+
let vcpuAt = null // W24: when vm.wasm's _start began (the main Worker's instance made)
|
|
1175
|
+
let kernelDone = false
|
|
1176
|
+
let lastHeap = -1
|
|
1177
|
+
function heapEvent() {
|
|
1178
|
+
const bytes = memory ? memory.buffer.byteLength : 0
|
|
1179
|
+
if (bytes === lastHeap) return
|
|
1180
|
+
lastHeap = bytes
|
|
1181
|
+
be.progress('heap', { t: 'heap', bytes, maxBytes: maximum * PAGE })
|
|
1182
|
+
}
|
|
1183
|
+
const spare = o.spare ?? 2
|
|
1184
|
+
|
|
1185
|
+
// vm.wasm's own lines about its heap (W21's reserve, branch w21-trap): with the reserve
|
|
1186
|
+
// the memory is at its maximum from the start, so only vm.wasm can say it ran out.
|
|
1187
|
+
const heap = { reserve: false, usedUp: false, said: false, carry: '' }
|
|
1188
|
+
const watchHeap = text => {
|
|
1189
|
+
const t = heap.carry + text
|
|
1190
|
+
if (HEAP_RESERVE_RE.test(t)) heap.reserve = true
|
|
1191
|
+
if (HEAP_USED_UP_RE.test(t)) heap.usedUp = true
|
|
1192
|
+
if (VM_OOM_RE.test(t)) heap.said = true
|
|
1193
|
+
heap.carry = t.slice(-160)
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function pump() {
|
|
1197
|
+
const got = outText.drain() // text, with a marker line wherever bytes were skipped
|
|
1198
|
+
if (got[1] || got[2]) watchdog.progress()
|
|
1199
|
+
if (timings.firstByteMs === undefined && out.firstStdoutAt()) timings.firstByteMs = out.firstStdoutAt() - t0
|
|
1200
|
+
if (!kernelDone && vcpuAt !== null && out.firstStdoutAt()) {
|
|
1201
|
+
kernelDone = true // W24: the kernel stage ends at the first console byte (a restored machine has no kernel stage)
|
|
1202
|
+
if (!restoreIn) be.now('kernel', { t: 'boot', stage: 'kernel', state: 'done', tookMs: Math.round(Math.max(0, out.firstStdoutAt() - vcpuAt)) })
|
|
1203
|
+
}
|
|
1204
|
+
if (got[1]) onStdoutText(got[1])
|
|
1205
|
+
if (got[2]) {
|
|
1206
|
+
watchHeap(got[2])
|
|
1207
|
+
emitErr(got[2])
|
|
1208
|
+
}
|
|
1209
|
+
drainConsole()
|
|
1210
|
+
}
|
|
1211
|
+
const pumpTimer = setInterval(pump, 16)
|
|
1212
|
+
|
|
1213
|
+
let lateTrap = null
|
|
1214
|
+
function finish(code, reason) {
|
|
1215
|
+
if (done) return
|
|
1216
|
+
done = true
|
|
1217
|
+
if (baseLazy) {
|
|
1218
|
+
// the machine ended before its restore succeeded (stopped, failed to start): the snapshot's cache copy is not committed
|
|
1219
|
+
const lazy = baseLazy
|
|
1220
|
+
baseLazy = null
|
|
1221
|
+
try {
|
|
1222
|
+
lazy.abort?.(new Error(`the machine ended (${reason}) before the base snapshot was restored`))
|
|
1223
|
+
} catch {}
|
|
1224
|
+
}
|
|
1225
|
+
if (ck) ckptFail(new CheckpointError('ended', `the machine ended (${reason}) before the checkpoint was complete`)) // also stops its poll timer
|
|
1226
|
+
timings.totalMs = absMs() - t0
|
|
1227
|
+
timings.spawnFailures = Atomics.load(ctl, CTL_SPAWN_FAIL)
|
|
1228
|
+
timings.threadsSpawned = Atomics.load(ctl, CTL_NEXT_TID) - 1
|
|
1229
|
+
timings.jit = jitStore ? readJitStats(jitStore) : null
|
|
1230
|
+
timings.memoryBytes = memory.buffer.byteLength
|
|
1231
|
+
timings.memoryMaxBytes = maximum * PAGE
|
|
1232
|
+
const trapped = String(reason).startsWith('trap')
|
|
1233
|
+
if (trapped) {
|
|
1234
|
+
// stop every guest thread now: after a trap the guest's memory may hold garbage
|
|
1235
|
+
// (the others also stop at their next host call, worker.js guard)
|
|
1236
|
+
for (const w of workers) w.terminate()
|
|
1237
|
+
}
|
|
1238
|
+
if (trapped || reason === 'exit') pump() // vm.wasm's last stderr lines (the heap reserve's "used up")
|
|
1239
|
+
// W21 review A3: vm.wasm now stops itself on a failed allocation (exit 4 after its "out
|
|
1240
|
+
// of memory" line) instead of trapping; that end is reported the same way.
|
|
1241
|
+
// Only exit status 4, the one vm_out_of_memory uses: a user-mode guest can print the same
|
|
1242
|
+
// lines on its own stderr (the joint W21/W23 review's oomspoof: exit 1 was taken for OOM).
|
|
1243
|
+
if (trapped || (reason === 'exit' && code === 4 && (heap.usedUp || heap.said))) {
|
|
1244
|
+
// A trap because vm.wasm ran out of room for its own heap: its malloc failed and it
|
|
1245
|
+
// aborted (P-B11 problem 1, review finding 4). With W21's heap reserve vm.wasm says
|
|
1246
|
+
// so on stderr, and the memory is at its maximum whatever happens, so the size
|
|
1247
|
+
// only counts without a reserve.
|
|
1248
|
+
const oom = isOutOfMemory({ ...heap, reserve: heap.reserve || heapReserveAsked(env), memoryBytes: timings.memoryBytes, memoryMaxBytes: timings.memoryMaxBytes })
|
|
1249
|
+
if (oom) {
|
|
1250
|
+
if (!trapped) for (const w of workers) w.terminate()
|
|
1251
|
+
timings.oom = oom
|
|
1252
|
+
timings.trap = reason
|
|
1253
|
+
reason =
|
|
1254
|
+
`out of memory: the emulator used ${fmtMiB(timings.memoryBytes)} of its ${fmtMiB(timings.memoryMaxBytes)} memory maximum and stopped ` +
|
|
1255
|
+
`(${reason}); give it more room above the guest's RAM (bootWmd maxPages, memoryMaxPages(args, slackMiB))`
|
|
1256
|
+
emitErr(`\n[wmd-web] ${reason}\n`)
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
let collected = Promise.resolve()
|
|
1260
|
+
if (chanSab) {
|
|
1261
|
+
markGuestGone(chanSab)
|
|
1262
|
+
chanLock?.abort() // before the service ends, so its end is not taken for a death
|
|
1263
|
+
chanSvc?.postMessage({ t: 'stats' })
|
|
1264
|
+
netWorker?.postMessage({ t: 'stats', id: 'final' })
|
|
1265
|
+
// files the caller wants out of the 9P trees (bundle/exitcode, a runc log, ...)
|
|
1266
|
+
const want = o.channels.collect || []
|
|
1267
|
+
if (want.length && chanSvc && !chanDied) {
|
|
1268
|
+
collected = new Promise(res => {
|
|
1269
|
+
collectDone = files => {
|
|
1270
|
+
timings.chanFiles = files
|
|
1271
|
+
res()
|
|
1272
|
+
}
|
|
1273
|
+
setTimeout(res, 2000)
|
|
1274
|
+
})
|
|
1275
|
+
chanSvc.postMessage({ t: 'collect', files: want })
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
// Give threads a moment to finish their last writes, drain, then stop every Worker.
|
|
1279
|
+
const settle = new Promise(r => setTimeout(r, 50))
|
|
1280
|
+
Promise.all([settle, collected]).then(async () => {
|
|
1281
|
+
for (const w of workers) w.terminate()
|
|
1282
|
+
// W77: the notes the Workers sent are in; put the index (bounded wait) before the end is reported
|
|
1283
|
+
if (jitCache) {
|
|
1284
|
+
await jitCache.save()
|
|
1285
|
+
for (const p of jitCachePorts) p.close()
|
|
1286
|
+
timings.jitCache = { on: true, ...jitCache.stats }
|
|
1287
|
+
}
|
|
1288
|
+
if (keepHub && hub) keptHub = hub
|
|
1289
|
+
else hub?.terminate()
|
|
1290
|
+
chanSvc?.terminate()
|
|
1291
|
+
failUpperWaiters('the machine has ended (its files ended with it)')
|
|
1292
|
+
netWorker?.terminate()
|
|
1293
|
+
netMember?.detach() // W56: its peers' streams to it fail; the name keeps its address
|
|
1294
|
+
if (netWorker) timings.net = netStats
|
|
1295
|
+
if (chanSab) timings.chan = { ...chanCounters(chanSab), service: chanStats, died: chanDied }
|
|
1296
|
+
if (control) {
|
|
1297
|
+
control.fail(`the machine ended (${reason}, exit code ${code}) before the control channel was ready${errTail.trim() ? `; the emulator's last output:\n${errTail.trim().split('\n').slice(-12).join('\n')}` : ''}`)
|
|
1298
|
+
// every stream still open ends (a process gets exit 125), as when the engine's
|
|
1299
|
+
// control connection breaks
|
|
1300
|
+
const evs = [...control.streams.keys()].map(k => ({ ev: 'closed', k, why: `the machine ended (${reason})` }))
|
|
1301
|
+
evs.push({ ev: 'session', up: false, epoch: control.session.epoch, why: 'the machine ended' })
|
|
1302
|
+
control.onEvents(evs)
|
|
1303
|
+
}
|
|
1304
|
+
clearInterval(pumpTimer)
|
|
1305
|
+
pump()
|
|
1306
|
+
timings.outputDropped = { stdout: out.dropped[1], stderr: out.dropped[2] }
|
|
1307
|
+
timings.outputDropMarkers = { stdout: outText.skips[1], stderr: outText.skips[2] }
|
|
1308
|
+
timings.hostCalls = hostCallCounts(ctl)
|
|
1309
|
+
timings.throttle = throttle ? throttle.state() : null
|
|
1310
|
+
// A kernel panic the guest turns into a clean exit (PSCI reset -> 0) is a failure.
|
|
1311
|
+
if (panic.line && (reason === 'exit' || reason === 'kernel panic')) {
|
|
1312
|
+
timings.guestExitCode = reason === 'exit' ? code : null
|
|
1313
|
+
code = EXIT_PANIC
|
|
1314
|
+
reason = `kernel panic: ${panic.line.replace(/^\[[^\]]*\]\s*/, '')}`
|
|
1315
|
+
} else if (chanDied && reason === 'exit') {
|
|
1316
|
+
// The guest powers off when its 9P or net channel closes (chan_pump.c host_gone),
|
|
1317
|
+
// with exit 0: a host failure must not look like a clean run.
|
|
1318
|
+
timings.guestExitCode = code
|
|
1319
|
+
code = EXIT_SERVICE_DIED
|
|
1320
|
+
reason = `channel service died: ${chanDied}`
|
|
1321
|
+
}
|
|
1322
|
+
// W21 review A2: a trap counted up to here (the 50 ms settle) turns an exit 0 into 4. A
|
|
1323
|
+
// trap later than that is not seen; the joint W21/W23 review found none in 15 proof boots,
|
|
1324
|
+
// and user mode now drains its threads before vm.wasm exits (main_wasm.c).
|
|
1325
|
+
timings.traps = Atomics.load(ctl, CTL_TRAPS)
|
|
1326
|
+
if (timings.traps && reason === 'exit' && code === 0) {
|
|
1327
|
+
timings.guestExitCode = code
|
|
1328
|
+
code = 4
|
|
1329
|
+
reason = lateTrap || `trap: a thread trapped after the exit (${timings.traps})`
|
|
1330
|
+
}
|
|
1331
|
+
// W24: every Worker has ended; the end, by number
|
|
1332
|
+
wl.endAll()
|
|
1333
|
+
be.now('workers', { t: 'boot', stage: 'workers', state: 'progress', workers: wl.list(), vcpus, jitShared: share })
|
|
1334
|
+
be.now('exit', { t: 'exit', code, reason, oom: !!timings.oom })
|
|
1335
|
+
be.close()
|
|
1336
|
+
release()
|
|
1337
|
+
resolveExit({ code, reason, timings })
|
|
1338
|
+
})
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// Drop everything that holds the shared memory, the file buffers or a Worker, so a
|
|
1342
|
+
// finished boot can be collected even while the page keeps its handle (vm.memory
|
|
1343
|
+
// stays readable for post-mortem until vm.dispose()).
|
|
1344
|
+
function release() {
|
|
1345
|
+
clearInterval(tickTimer)
|
|
1346
|
+
stopGcNudge()
|
|
1347
|
+
clearTimeout(panicTimer)
|
|
1348
|
+
signal?.removeEventListener('abort', onAbort)
|
|
1349
|
+
for (const w of workers) w.onmessage = w.onerror = null
|
|
1350
|
+
workers.length = 0
|
|
1351
|
+
calib?.terminate()
|
|
1352
|
+
if (calib) calib.onmessage = null
|
|
1353
|
+
calib = null
|
|
1354
|
+
hub = null // terminated in finish()
|
|
1355
|
+
if (chanSvc) chanSvc.onmessage = chanSvc.onerror = null
|
|
1356
|
+
chanSvc = null
|
|
1357
|
+
if (netWorker) netWorker.onmessage = netWorker.onerror = null
|
|
1358
|
+
netWorker = null
|
|
1359
|
+
netMember?.detach()
|
|
1360
|
+
module = files = null
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function onStdoutText(text) {
|
|
1364
|
+
o.onStdout?.(text)
|
|
1365
|
+
if (panic.push(text)) onPanic()
|
|
1366
|
+
if (!marks.length) return
|
|
1367
|
+
stdoutLines.buf += text
|
|
1368
|
+
let nl
|
|
1369
|
+
while ((nl = stdoutLines.buf.indexOf('\n')) >= 0) {
|
|
1370
|
+
const line = stdoutLines.buf.slice(0, nl).replace(/\r$/, '') // the x86 8250 console sends CRLF
|
|
1371
|
+
stdoutLines.buf = stdoutLines.buf.slice(nl + 1)
|
|
1372
|
+
for (const m of marks) {
|
|
1373
|
+
if (!m.hit && m.re.test(line)) {
|
|
1374
|
+
m.hit = true
|
|
1375
|
+
timings[m.name] = absMs() - t0
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
function onMessage(e) {
|
|
1382
|
+
const m = e.data
|
|
1383
|
+
switch (m.t) {
|
|
1384
|
+
case 'ready':
|
|
1385
|
+
loaded.add(m.role === 'main' ? 'main' : m.slot)
|
|
1386
|
+
if (!done) workerState(wid(m.role === 'main' ? 'main' : m.slot), m.role === 'main' ? 'vcpu' : 'pool', 'ready')
|
|
1387
|
+
if (m.role === 'main') {
|
|
1388
|
+
timings.instantiatedMs = m.at - t0
|
|
1389
|
+
watchdog.start()
|
|
1390
|
+
ev({ t: 'phase', phase: 'running' })
|
|
1391
|
+
vcpuAt = m.at
|
|
1392
|
+
if (!restoreIn) be.now('kernel', { t: 'boot', stage: 'kernel', state: 'start' })
|
|
1393
|
+
heapEvent()
|
|
1394
|
+
}
|
|
1395
|
+
break
|
|
1396
|
+
case 'need':
|
|
1397
|
+
topUp(m.why)
|
|
1398
|
+
break
|
|
1399
|
+
case 'thread':
|
|
1400
|
+
watchdog.progress()
|
|
1401
|
+
ev(m)
|
|
1402
|
+
if (!done && wl.tid(wid(m.slot), m.ev === 'start' ? m.tid : null)) workersEv()
|
|
1403
|
+
break
|
|
1404
|
+
case 'exit':
|
|
1405
|
+
finish(m.code, 'exit')
|
|
1406
|
+
break
|
|
1407
|
+
case 'resume-instantiated':
|
|
1408
|
+
restoreMemory()
|
|
1409
|
+
break
|
|
1410
|
+
case 'ckpt-point':
|
|
1411
|
+
case 'ckpt-mem':
|
|
1412
|
+
case 'ckpt-failed':
|
|
1413
|
+
case 'ckpt-mem-error':
|
|
1414
|
+
ckptEvent(m)
|
|
1415
|
+
break
|
|
1416
|
+
case 'trap':
|
|
1417
|
+
emitErr(`\n[wmd-web] thread ${m.tid} trapped: ${m.message}\n${m.stack}\n`)
|
|
1418
|
+
// W21 review A2: a thread that traps after another called proc_exit (the trap lost
|
|
1419
|
+
// the race for CTL_EXIT) still fails the run; finish() turns an exit 0 into 4
|
|
1420
|
+
if (done) lateTrap ??= `trap: ${m.message} (thread ${m.tid}, after the exit)`
|
|
1421
|
+
finish(4, `trap: ${m.message}`)
|
|
1422
|
+
break
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// P-B7: a Worker whose script failed to load (an error before its 'ready') is made again,
|
|
1427
|
+
// up to LOAD_TRIES times, after a delay that differs per Worker. WebKit, offline behind the
|
|
1428
|
+
// pack host's service worker, failed the module imports of all but one of several Workers
|
|
1429
|
+
// started at once from one script (README P-B7, problem 1); started 30 ms apart they load.
|
|
1430
|
+
const LOAD_TRIES = 3
|
|
1431
|
+
const loaded = new Set() // 'main' and pool slots that posted 'ready'
|
|
1432
|
+
const retryDelay = (key, attempt) => 40 * attempt * (1 + (key === 'main' ? 0 : key + 1))
|
|
1433
|
+
const wid = key => (key === 'main' ? 'vcpu-0' : `pool-${key}`)
|
|
1434
|
+
function startWorker(key, name, role, slot, attempt, onFail) {
|
|
1435
|
+
const w = workerUrl
|
|
1436
|
+
? new Worker(workerUrl, { type: 'module', name })
|
|
1437
|
+
: new Worker(new URL('./worker.js', import.meta.url), { type: 'module', name })
|
|
1438
|
+
workerState(wid(key), key === 'main' ? 'vcpu' : 'pool', 'starting')
|
|
1439
|
+
w.onmessage = onMessage
|
|
1440
|
+
w.onerror = err => {
|
|
1441
|
+
if (!done && !loaded.has(key) && attempt < LOAD_TRIES) {
|
|
1442
|
+
err.preventDefault?.()
|
|
1443
|
+
w.terminate()
|
|
1444
|
+
const i = workers.indexOf(w)
|
|
1445
|
+
if (i >= 0) workers.splice(i, 1)
|
|
1446
|
+
ev({ t: 'pool', ev: 'worker-retry', key, attempt })
|
|
1447
|
+
timings.workerLoadRetries = (timings.workerLoadRetries || 0) + 1
|
|
1448
|
+
setTimeout(() => !done && startWorker(key, name, role, slot, attempt + 1, onFail), retryDelay(key, attempt))
|
|
1449
|
+
return
|
|
1450
|
+
}
|
|
1451
|
+
if (!done) workerState(wid(key), key === 'main' ? 'vcpu' : 'pool', 'failed')
|
|
1452
|
+
onFail(err)
|
|
1453
|
+
}
|
|
1454
|
+
postInit(w, role, slot)
|
|
1455
|
+
workers.push(w)
|
|
1456
|
+
return w
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
function addPoolWorker() {
|
|
1460
|
+
const slot = Atomics.load(ctl, CTL_POOL_N)
|
|
1461
|
+
if (slot >= POOL_MAX) return false
|
|
1462
|
+
Atomics.store(ctl, slotIndex(slot, SLOT_STATE), ST_STARTING)
|
|
1463
|
+
Atomics.store(ctl, CTL_POOL_N, slot + 1)
|
|
1464
|
+
startWorker(slot, `wmd-pool-${slot}`, 'pool', slot, 1, err => {
|
|
1465
|
+
Atomics.store(ctl, slotIndex(slot, SLOT_STATE), ST_DEAD)
|
|
1466
|
+
emitErr(`\n[wmd-web] pool worker ${slot} failed to load: ${err.message || err}\n`)
|
|
1467
|
+
ev({ t: 'pool', ev: 'worker-error', slot })
|
|
1468
|
+
})
|
|
1469
|
+
return true
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
function topUp(why) {
|
|
1473
|
+
if (done) return
|
|
1474
|
+
const n = Atomics.load(ctl, CTL_POOL_N)
|
|
1475
|
+
let idleOrStarting = 0
|
|
1476
|
+
for (let i = 0; i < n; i++) {
|
|
1477
|
+
const s = Atomics.load(ctl, slotIndex(i, SLOT_STATE))
|
|
1478
|
+
if (s === ST_IDLE || s === ST_STARTING) idleOrStarting++
|
|
1479
|
+
}
|
|
1480
|
+
let added = 0
|
|
1481
|
+
while (idleOrStarting + added < spare + (why === 'spawned' ? 0 : 1)) {
|
|
1482
|
+
if (!addPoolWorker()) {
|
|
1483
|
+
emitErr(`\n[wmd-web] thread pool exhausted: ${POOL_MAX} Workers in use (${why})\n`)
|
|
1484
|
+
ev({ t: 'pool', ev: 'exhausted' })
|
|
1485
|
+
return
|
|
1486
|
+
}
|
|
1487
|
+
added++
|
|
1488
|
+
}
|
|
1489
|
+
if (why === 'exhausted') {
|
|
1490
|
+
emitErr(`\n[wmd-web] thread-spawn found no idle Worker in time; pthread_create failed (EAGAIN)\n`)
|
|
1491
|
+
ev({ t: 'pool', ev: 'spawn-failed' })
|
|
1492
|
+
}
|
|
1493
|
+
if (added) ev({ t: 'pool', ev: 'grow', added, total: Atomics.load(ctl, CTL_POOL_N), why })
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
function postInit(w, role, slot) {
|
|
1497
|
+
const msg = initMsg(role, slot)
|
|
1498
|
+
msg.jitPort = hubPort()
|
|
1499
|
+
const transfer = msg.jitPort ? [msg.jitPort] : []
|
|
1500
|
+
if (jitCache) {
|
|
1501
|
+
msg.jitCache = { sab: knownSab, port: jitCachePort() }
|
|
1502
|
+
transfer.push(msg.jitCache.port)
|
|
1503
|
+
}
|
|
1504
|
+
w.postMessage(msg, transfer)
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
/** W77: a Worker's port to the JIT cache: module reads ({t:'get'}) and notes ({t:'note'}). */
|
|
1508
|
+
function jitCachePort() {
|
|
1509
|
+
const ch = new MessageChannel()
|
|
1510
|
+
const p = ch.port1
|
|
1511
|
+
p.onmessage = e => {
|
|
1512
|
+
const m = e.data
|
|
1513
|
+
if (m.t === 'get') {
|
|
1514
|
+
jitCache.get(m.hl, m.hh, m.len).then(bytes => {
|
|
1515
|
+
try {
|
|
1516
|
+
p.postMessage({ id: m.id, bytes }, bytes ? [bytes.buffer] : [])
|
|
1517
|
+
} catch {}
|
|
1518
|
+
})
|
|
1519
|
+
} else if (m.t === 'note') jitCache.note(m)
|
|
1520
|
+
}
|
|
1521
|
+
jitCachePorts.push(p)
|
|
1522
|
+
return ch.port2
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
function initMsg(role, slot) {
|
|
1526
|
+
return {
|
|
1527
|
+
t: 'init',
|
|
1528
|
+
role,
|
|
1529
|
+
slot,
|
|
1530
|
+
module,
|
|
1531
|
+
tableMin,
|
|
1532
|
+
memory,
|
|
1533
|
+
ram: ramMem,
|
|
1534
|
+
ctlSab,
|
|
1535
|
+
files,
|
|
1536
|
+
preopen,
|
|
1537
|
+
args: argv,
|
|
1538
|
+
env: browserEnv(env),
|
|
1539
|
+
jit: jitStore,
|
|
1540
|
+
jitOpts,
|
|
1541
|
+
stdio,
|
|
1542
|
+
chan: chanSab,
|
|
1543
|
+
stallHook: !!o.stallHook,
|
|
1544
|
+
ckpt: ckptOn,
|
|
1545
|
+
ckptScanLimit: o.checkpointScanLimit,
|
|
1546
|
+
ckptVerifyBound: !!o.checkpointVerifyBound,
|
|
1547
|
+
ckptUseHeapTop: o.checkpointHeapTop !== false, // W58: reading untouched pages commits them in WebKit
|
|
1548
|
+
resume: role === 'main' && restoreIn ? { ports: resumePorts(o.args), stats: o.args.includes('--stats') } : null,
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
// Monitors: kernel panic, progress watchdog, throttling, stub host-call counters.
|
|
1553
|
+
const panic = createPanicScanner()
|
|
1554
|
+
let panicTimer = 0
|
|
1555
|
+
function onPanic() {
|
|
1556
|
+
ev({ t: 'panic', line: panic.line })
|
|
1557
|
+
emitErr(`\n[wmd-web] kernel panic on the console: ${panic.line}\n`)
|
|
1558
|
+
// CONFIG_PANIC_TIMEOUT=-1 reboots at once (exit 0); a kernel that hangs instead is
|
|
1559
|
+
// stopped after a few seconds, once the panic trace has been printed.
|
|
1560
|
+
panicTimer = setTimeout(() => finish(EXIT_PANIC, 'kernel panic'), 5000)
|
|
1561
|
+
}
|
|
1562
|
+
const TICK_MS = 250
|
|
1563
|
+
const watchdogMs = o.watchdogMs ?? watchdogDefaultMs(o.args)
|
|
1564
|
+
const watchdog = createWatchdog({ limitMs: watchdogMs, tickMs: TICK_MS })
|
|
1565
|
+
const throttleOpts = o.throttle === false ? null : o.throttle || {}
|
|
1566
|
+
const throttle = throttleOpts && createThrottleMeter({ threshold: throttleOpts.threshold ?? 2, baselineMs: throttleOpts.baselineMs })
|
|
1567
|
+
let calib = null
|
|
1568
|
+
if (throttle) {
|
|
1569
|
+
calib = new Worker(new URL('./calib-worker.js', import.meta.url), { type: 'module', name: 'wmd-calib' })
|
|
1570
|
+
calib.onmessage = e => {
|
|
1571
|
+
if (done || e.data.t !== 'calib') return
|
|
1572
|
+
const s = throttle.sample(e.data.workMs)
|
|
1573
|
+
if (s.changed) {
|
|
1574
|
+
const { changed, ...st } = s
|
|
1575
|
+
ev({ t: 'throttle', ...st, waitLateMs: e.data.waitLateMs })
|
|
1576
|
+
if (st.throttled)
|
|
1577
|
+
emitErr(`\n[wmd-web] the guest is running about ${st.ratio}x slower than this browser can run it (throttled: background window or tab?)\n`)
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
calib.postMessage({ t: 'start', intervalMs: throttleOpts.intervalMs ?? 2000 })
|
|
1581
|
+
}
|
|
1582
|
+
let lastCalls = ''
|
|
1583
|
+
function onTick() {
|
|
1584
|
+
if (done) return
|
|
1585
|
+
if (vcpuAt !== null) heapEvent()
|
|
1586
|
+
const calls = hostCallCounts(ctl)
|
|
1587
|
+
const key = JSON.stringify(calls)
|
|
1588
|
+
if (key !== lastCalls) {
|
|
1589
|
+
lastCalls = key
|
|
1590
|
+
ev({ t: 'host-calls', counts: calls }) // at most once per tick, however often the guest calls
|
|
1591
|
+
}
|
|
1592
|
+
if (watchdog.check(throttle && throttle.state().throttled ? throttle.state().ratio : 1)) {
|
|
1593
|
+
const s = Math.round(watchdog.idleMs() / 1000)
|
|
1594
|
+
emitErr(`\n[wmd-web] guest made no progress: no console output and no new thread for ${s} s after the vCPU started; stopping\n`)
|
|
1595
|
+
finish(EXIT_NO_PROGRESS, `no progress: the guest wrote nothing for ${s} s`)
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
const tickTimer = setInterval(onTick, TICK_MS)
|
|
1599
|
+
// D-419: the page holds the guest's memory too, so WebKit would collect its garbage late
|
|
1600
|
+
const stopGcNudge = gcNudgeEvery()
|
|
1601
|
+
|
|
1602
|
+
if (restoreIn?.jit && hub) {
|
|
1603
|
+
// the slot list's modules, compiled again by the hub before any vCPU starts. Not here: a Module the page compiles
|
|
1604
|
+
// keeps its code until the page collects its garbage, and Firefox's per-process code limit then fills with the
|
|
1605
|
+
// code of machines already gone (W57); the hub's code goes with its Worker when the machine ends.
|
|
1606
|
+
const tr = absMs()
|
|
1607
|
+
const mods = modulesOf(restoreIn.jit).map(m => ({ owner: m.owner, bytes: m.bytes.slice() }))
|
|
1608
|
+
if (adoptIn) {
|
|
1609
|
+
// the old machine's hub (still running) posts its Modules and their bytes to this one, Worker to Worker; the
|
|
1610
|
+
// replay takes each whose bytes are the same instead of compiling them again
|
|
1611
|
+
const ch = new MessageChannel()
|
|
1612
|
+
adoptIn.postMessage({ t: 'handoff', port: ch.port1 }, [ch.port1])
|
|
1613
|
+
hub.postMessage({ t: 'adopt', port: ch.port2 }, [ch.port2])
|
|
1614
|
+
}
|
|
1615
|
+
const r = await Promise.race([
|
|
1616
|
+
new Promise(res => {
|
|
1617
|
+
replayDone = res
|
|
1618
|
+
hub.postMessage({ t: 'replay', id: 1, gen: restoreIn.jit.gen, mods }, mods.map(m => m.bytes.buffer))
|
|
1619
|
+
}),
|
|
1620
|
+
exited.then(() => ({ ended: true })), // the hub ends with the machine: the handle is returned, already ended
|
|
1621
|
+
])
|
|
1622
|
+
replayDone = null
|
|
1623
|
+
if (r.error && !done) throw new Error(`restore: the JIT hub could not compile the checkpoint's modules: ${r.error}`)
|
|
1624
|
+
ckptT.replayModules = r.n ?? 0
|
|
1625
|
+
ckptT.replayReused = r.reused ?? 0
|
|
1626
|
+
ckptT.replayMs = absMs() - tr
|
|
1627
|
+
}
|
|
1628
|
+
if (restoreIn) timings.restore = ckptT
|
|
1629
|
+
|
|
1630
|
+
const poolSize = o.pool ?? 4
|
|
1631
|
+
for (let i = 0; i < poolSize; i++) addPoolWorker()
|
|
1632
|
+
|
|
1633
|
+
markStart(ctlSab)
|
|
1634
|
+
timings.startMs = absMs() - t0
|
|
1635
|
+
startWorker('main', 'wmd-main', 'main', -1, 1, err => {
|
|
1636
|
+
emitErr(`\n[wmd-web] main worker failed: ${err.message || err}\n`)
|
|
1637
|
+
finish(4, 'main worker error')
|
|
1638
|
+
})
|
|
1639
|
+
workersStarted = true
|
|
1640
|
+
|
|
1641
|
+
onAbortAfterStart = () => finish(EXIT_STOPPED, 'aborted')
|
|
1642
|
+
if (signal?.aborted) onAbortAfterStart()
|
|
1643
|
+
|
|
1644
|
+
|
|
1645
|
+
// ---- P-B9: checkpoints -------------------------------------------------------------------
|
|
1646
|
+
//
|
|
1647
|
+
// vm.checkpoint(): the page stores REQ_CKPT in the guest's host_req word (the address the guest's ctl thread
|
|
1648
|
+
// gave wmd.ctl_attach). The guest stops its vCPUs at a block boundary, drains the file share, stops its
|
|
1649
|
+
// service threads and calls wmd.point on vCPU 0 (host/ckpt-point.js), which tells the page and copies memory
|
|
1650
|
+
// 0's non-zero pages. At that same moment the page asks the channel service Worker for the 9P state and
|
|
1651
|
+
// the JIT hub for the modules' bytes the log released, and reads the JIT slot list and the fd table (nothing
|
|
1652
|
+
// else runs). When all four are in, the page lets vCPU 0 go (CTL_CK_GO) and the guest continues.
|
|
1653
|
+
const gnow = () => performance.timeOrigin + performance.now()
|
|
1654
|
+
const hdrNs = (hdr, off) => Number(new DataView(hdr.buffer, hdr.byteOffset, hdr.byteLength).getBigUint64(off, true)) / 1e6
|
|
1655
|
+
|
|
1656
|
+
function ckptFail(err) {
|
|
1657
|
+
const c = ck
|
|
1658
|
+
if (!c) return
|
|
1659
|
+
ck = null
|
|
1660
|
+
clearTimeout(c.timer)
|
|
1661
|
+
clearInterval(c.poll)
|
|
1662
|
+
if (c.atPoint && !c.went) {
|
|
1663
|
+
Atomics.store(ctl, CTL_CK_GO, 2) // the guest's vCPU 0 is waiting for us: tell it the checkpoint failed
|
|
1664
|
+
Atomics.notify(ctl, CTL_CK_GO)
|
|
1665
|
+
}
|
|
1666
|
+
c.reject(err)
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
function ckptEvent(m) {
|
|
1670
|
+
const c = ck
|
|
1671
|
+
if (!c) return // a point nobody asked for (or one that timed out): nothing to do
|
|
1672
|
+
if (m.t === 'ckpt-failed') return ckptFail(new CheckpointError('point', `the guest could not take the checkpoint: ${m.why} (the machine goes on)`))
|
|
1673
|
+
if (m.t === 'ckpt-mem-error') return ckptFail(new CheckpointError('memory', `copying the guest's memory failed: ${m.message}`))
|
|
1674
|
+
if (m.t === 'ckpt-point') {
|
|
1675
|
+
c.atPoint = true
|
|
1676
|
+
c.hdr = m.hdr
|
|
1677
|
+
c.pointAt = m.at
|
|
1678
|
+
if (chanSvc) chanSvc.postMessage({ t: 'snap' })
|
|
1679
|
+
else c.svc = {}
|
|
1680
|
+
try {
|
|
1681
|
+
c.ctlBytes = fdTableBytes(ctl)
|
|
1682
|
+
if (!jitStore) c.jitDone = true
|
|
1683
|
+
else {
|
|
1684
|
+
const need = ownersNeedingHub(jitStore)
|
|
1685
|
+
if (!need.length) {
|
|
1686
|
+
const t = absMs()
|
|
1687
|
+
c.jitSnap = captureJit(jitStore, new Map())
|
|
1688
|
+
c.jitMs = absMs() - t
|
|
1689
|
+
c.jitDone = true
|
|
1690
|
+
} else if (!hub) {
|
|
1691
|
+
throw new CheckpointError('jit', `${need.length} module records were released from the module log and there is no hub to ask`)
|
|
1692
|
+
} else {
|
|
1693
|
+
c.jitAskedAt = absMs()
|
|
1694
|
+
hub.postMessage({ t: 'bytes-of', id: 1, owners: need })
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
} catch (err) {
|
|
1698
|
+
return ckptFail(err)
|
|
1699
|
+
}
|
|
1700
|
+
} else if (m.t === 'ckpt-mem') {
|
|
1701
|
+
c.mem = m
|
|
1702
|
+
}
|
|
1703
|
+
ckptMaybeGo()
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
function ckptSvc(m) {
|
|
1707
|
+
const c = ck
|
|
1708
|
+
if (!c || !c.atPoint) return
|
|
1709
|
+
if (m.error) return ckptFail(new CheckpointError('service', `the file service could not save its state: ${m.error}`))
|
|
1710
|
+
for (const d of Object.values(m.ports)) d.segments = d.segments.map(b => new Uint8Array(b))
|
|
1711
|
+
c.svc = m.ports
|
|
1712
|
+
c.svcMs = m.ms
|
|
1713
|
+
ckptMaybeGo()
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
function ckptHub(m) {
|
|
1717
|
+
const c = ck
|
|
1718
|
+
if (!c || !c.atPoint) return
|
|
1719
|
+
try {
|
|
1720
|
+
if (m.missing.length) {
|
|
1721
|
+
throw new CheckpointError('jit', `the JIT hub no longer holds the bytes of ${m.missing.length} released module records (first: ${m.missing[0]}); raise bootWmd({ckptHubBytes})`)
|
|
1722
|
+
}
|
|
1723
|
+
c.jitSnap = captureJit(jitStore, new Map(m.got))
|
|
1724
|
+
c.jitMs = absMs() - c.jitAskedAt
|
|
1725
|
+
c.jitDone = true
|
|
1726
|
+
} catch (err) {
|
|
1727
|
+
return ckptFail(err)
|
|
1728
|
+
}
|
|
1729
|
+
ckptMaybeGo()
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
function ckptMaybeGo() {
|
|
1733
|
+
const c = ck
|
|
1734
|
+
if (!c || c.went || !c.mem || !c.svc || !c.jitDone) return
|
|
1735
|
+
c.went = true
|
|
1736
|
+
c.goAt = gnow()
|
|
1737
|
+
Atomics.store(ctl, CTL_CK_GO, 1)
|
|
1738
|
+
Atomics.notify(ctl, CTL_CK_GO, 1)
|
|
1739
|
+
// the guest runs again when its ctl thread attaches once more (the end of the pause)
|
|
1740
|
+
c.poll = setInterval(() => {
|
|
1741
|
+
if (Atomics.load(ctl, CTL_CK_ATTACHES) > c.attachesBefore || gnow() - c.goAt > 15000) {
|
|
1742
|
+
clearInterval(c.poll)
|
|
1743
|
+
c.resumeAt = gnow()
|
|
1744
|
+
ckptFinish(c)
|
|
1745
|
+
}
|
|
1746
|
+
}, 2)
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
/** W59: the network Worker's whole placeholder map (a superset of what the guest has seen), else the page's copy. */
|
|
1750
|
+
function netNameList() {
|
|
1751
|
+
if (!netWorker || netSvcRelayProtocol() !== 'wisp') return Promise.resolve([...netNames])
|
|
1752
|
+
return new Promise(res => {
|
|
1753
|
+
const id = Math.random()
|
|
1754
|
+
const t = setTimeout(() => netNameWaiters.delete(id) && res([...netNames]), 2000)
|
|
1755
|
+
netNameWaiters.set(id, names => {
|
|
1756
|
+
clearTimeout(t)
|
|
1757
|
+
for (const [n, ip] of names) {
|
|
1758
|
+
if (netNames.has(n) || netIps.has(ip)) continue
|
|
1759
|
+
netNames.set(n, ip)
|
|
1760
|
+
netIps.add(ip)
|
|
1761
|
+
}
|
|
1762
|
+
res([...netNames])
|
|
1763
|
+
})
|
|
1764
|
+
netWorker.postMessage({ t: 'names', id })
|
|
1765
|
+
})
|
|
1766
|
+
}
|
|
1767
|
+
function netSvcRelayProtocol() {
|
|
1768
|
+
return chanSvcs?.find(s => (s.kind === 'net' || s.kind === 1) && s.relay)?.relay?.protocol || ''
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
function ckptFinish(c) {
|
|
1772
|
+
if (ck !== c) return
|
|
1773
|
+
ck = null
|
|
1774
|
+
clearTimeout(c.timer)
|
|
1775
|
+
netNameList().then(names => ckptAssemble(c, names))
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
function ckptAssemble(c, names) {
|
|
1779
|
+
try {
|
|
1780
|
+
const t = absMs()
|
|
1781
|
+
const stats = {
|
|
1782
|
+
stopLatencyMs: c.pointAt - c.t0, // request to the vCPUs stopped and the point entered (includes the guest's world stop, share drain and service stop)
|
|
1783
|
+
worldStopMs: hdrNs(c.hdr, 312 - 0),
|
|
1784
|
+
p9DrainMs: hdrNs(c.hdr, 320),
|
|
1785
|
+
svcStopMs: hdrNs(c.hdr, 328),
|
|
1786
|
+
scanMs: c.mem.scanMs,
|
|
1787
|
+
copyMs: c.mem.copyMs,
|
|
1788
|
+
memWaitMs: c.goAt - c.pointAt, // point entered to released: scan, copy, the page's collection of the rest
|
|
1789
|
+
svcMs: c.svcMs,
|
|
1790
|
+
jitMs: c.jitMs ?? 0,
|
|
1791
|
+
resumeMs: c.resumeAt - c.goAt, // released to the guest's ctl thread attached again
|
|
1792
|
+
nonzeroPages: c.mem.nonzero,
|
|
1793
|
+
memPages: c.mem.pages,
|
|
1794
|
+
scannedPages: c.mem.scannedPages,
|
|
1795
|
+
scanLimitBytes: c.mem.limitBytes,
|
|
1796
|
+
heapTopPage: c.mem.heapTopRet ? Math.ceil(c.mem.heapTopRet / CKPT_PAGE) : null,
|
|
1797
|
+
pagesAtOrAboveHeapTop: c.mem.heapTopRet ? Array.from(c.mem.index).filter(p => p >= Math.ceil(c.mem.heapTopRet / CKPT_PAGE)).length : null, // non-zero pages the checkpoint holds from the heap top up: 0 proves the heap-top bound
|
|
1798
|
+
aboveNonzeroPages: c.mem.aboveNonzero,
|
|
1799
|
+
brkLive: c.mem.brkLive,
|
|
1800
|
+
endLive: c.mem.endLive,
|
|
1801
|
+
heapTopRet: c.mem.heapTopRet,
|
|
1802
|
+
}
|
|
1803
|
+
// the guest's view of the pause: stopping the world, draining the share, stopping its threads, then everything from the point to its restart
|
|
1804
|
+
stats.pauseMs = stats.worldStopMs + stats.p9DrainMs + stats.svcStopMs + (c.resumeAt - c.pointAt)
|
|
1805
|
+
const meta = {
|
|
1806
|
+
label: c.label || '',
|
|
1807
|
+
vmKey,
|
|
1808
|
+
bootKey,
|
|
1809
|
+
bootDesc,
|
|
1810
|
+
packs: packDigests,
|
|
1811
|
+
ports: resumePorts(o.args),
|
|
1812
|
+
args: o.args,
|
|
1813
|
+
jitShare: share,
|
|
1814
|
+
fmaFused: env.includes('WMD_FMA_FUSED=1') || (!env.some(e => e.startsWith('WMD_FMA_FUSED=')) && fmaFuses()), // D-539: as browserEnv; its blocks may hold relaxed_madd
|
|
1815
|
+
stats,
|
|
1816
|
+
}
|
|
1817
|
+
const parts = { meta, hdr: c.hdr, mem: c.mem, jit: c.jitSnap ? { snap: c.jitSnap } : null, svc: c.svc, ctl: c.ctlBytes, names }
|
|
1818
|
+
const ckpt = assembleCheckpoint(parts)
|
|
1819
|
+
ckpt.header.stats.assembleMs = absMs() - t
|
|
1820
|
+
ckpt.header.svcStats = Object.fromEntries(Object.entries(c.svc).map(([port, d]) => [port, { bytes: d.bytes, fids: d.fids }]))
|
|
1821
|
+
if (c.jitSnap) ckpt.header.jitStats = jitSnapStats(c.jitSnap)
|
|
1822
|
+
c.resolve(ckpt)
|
|
1823
|
+
} catch (err) {
|
|
1824
|
+
c.reject(err)
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
/**
|
|
1829
|
+
* P-B9: take a checkpoint. Resolves with {header, sections}: the guest's memory pages, the JIT slot list, the 9P
|
|
1830
|
+
* service's state and the fd table, as captured at one instant while the guest was stopped. header.stats has the
|
|
1831
|
+
* times. It stays usable (restore it again as often as wanted) until the caller drops it; to survive a reload save
|
|
1832
|
+
* it with a store (opfsStore().save(name, ckpt)).
|
|
1833
|
+
*/
|
|
1834
|
+
function checkpoint({ label = '', timeoutMs = 60_000 } = {}) {
|
|
1835
|
+
if (!ckptOn) return Promise.reject(new Error('this machine was booted without checkpoint support: bootWmd({checkpoint: true})'))
|
|
1836
|
+
if (restoring) return Promise.reject(new CheckpointError('busy', 'a restore is running: this machine is about to end, and a checkpoint taken now would be lost'))
|
|
1837
|
+
if (done || chanDied) return Promise.reject(new Error('the machine has ended'))
|
|
1838
|
+
if (ck) return Promise.reject(new Error('a checkpoint is already being taken'))
|
|
1839
|
+
return new Promise((resolve, reject) => {
|
|
1840
|
+
const c = (ck = { label, t0: gnow(), resolve, reject, attachesBefore: Atomics.load(ctl, CTL_CK_ATTACHES), timer: null })
|
|
1841
|
+
c.timer = setTimeout(() => ckptFail(new CheckpointError('timeout', `the guest did not reach its checkpoint point in ${timeoutMs} ms`)), timeoutMs)
|
|
1842
|
+
if (!requestCheckpoint(memory, ctl)) ckptFail(new CheckpointError('not-ready', 'the guest has not started its checkpoint thread yet (it does that once the kernel has booted); try again after the control session is up'))
|
|
1843
|
+
})
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
/**
|
|
1847
|
+
* P-B9: end this machine and continue from a checkpoint in a new one (same page, same options). Resolves with the
|
|
1848
|
+
* new machine's handle. The old machine's Workers are gone when this returns; anything that was running in it (a
|
|
1849
|
+
* block, a process the guest started after the checkpoint) is gone with them.
|
|
1850
|
+
*/
|
|
1851
|
+
async function restore(ckpt, over = {}) {
|
|
1852
|
+
if (restoring) throw new CheckpointError('busy', 'a restore is already running on this machine: wait for it (it returns the new machine)')
|
|
1853
|
+
restoring = true
|
|
1854
|
+
try {
|
|
1855
|
+
// everything that can fail without harming the running machine comes first: the checkpoint parses and is this boot's
|
|
1856
|
+
const parsed = parseCheckpoint(ckpt)
|
|
1857
|
+
checkCheckpointFits(parsed.header, { vmKey, bootKey, bootDesc, maxPages: maximum, hasJit: o.jit !== false, fmaFused: fmaFuses() })
|
|
1858
|
+
if (!heldAssets) throw new CheckpointError('not-ready', 'this machine was booted without checkpoint support: it has no kept boot files to restore with')
|
|
1859
|
+
validateFdTable(parsed.ctl, heldAssets.files.length) // the fd table is checked against this boot's files now, not after the old machine is gone
|
|
1860
|
+
// the boot's own bytes (vm.wasm and its compiled Module, the kernel, the packs, the keys) are reused: nothing is
|
|
1861
|
+
// downloaded or hashed again, so the running machine is stopped only when the next one's inputs are in hand. After a
|
|
1862
|
+
// page reload (bootWmd({restore})) they are fetched, as for any boot.
|
|
1863
|
+
const reuse = !['vmUrl', 'files', 'channels', 'args', 'env', 'maxPages'].some(k => k in over)
|
|
1864
|
+
const assets = reuse ? heldAssets : null
|
|
1865
|
+
if (ck) ckptFail(new CheckpointError('restore', 'a restore began: the checkpoint in progress was abandoned'))
|
|
1866
|
+
// W57: this machine's hub outlives it until the next one's hub has taken the Modules it can reuse (the same
|
|
1867
|
+
// code: Firefox would otherwise hold two copies of the guest's code, and two guests' restores filled its 2 GiB)
|
|
1868
|
+
keepHub = !!(hub && parsed.jit)
|
|
1869
|
+
let oldHub = null
|
|
1870
|
+
try {
|
|
1871
|
+
finish(EXIT_STOPPED, 'stopped')
|
|
1872
|
+
await exited
|
|
1873
|
+
memory = null
|
|
1874
|
+
oldHub = keptHub
|
|
1875
|
+
keptHub = null
|
|
1876
|
+
const next = await bootWmd({ ...o, ...over, checkpoint: true, restore: ckpt, assets, _adopt: oldHub, _net: { instance: netInstance, names: [...netNames] } })
|
|
1877
|
+
heldAssets = null // the new machine holds them
|
|
1878
|
+
return next
|
|
1879
|
+
} finally {
|
|
1880
|
+
keepHub = false
|
|
1881
|
+
;(oldHub || keptHub)?.terminate() // the replay is over (or never came): the new hub holds what it took
|
|
1882
|
+
keptHub = null
|
|
1883
|
+
}
|
|
1884
|
+
} finally {
|
|
1885
|
+
restoring = false
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
return {
|
|
1890
|
+
exited,
|
|
1891
|
+
timings,
|
|
1892
|
+
/** The shared memory, for post-mortem reads; null after dispose(). */
|
|
1893
|
+
get memory() {
|
|
1894
|
+
return memory
|
|
1895
|
+
},
|
|
1896
|
+
stop: () => finish(EXIT_STOPPED, 'stopped'),
|
|
1897
|
+
checkpoint,
|
|
1898
|
+
restore,
|
|
1899
|
+
/** P-B9: this boot's identity keys ({vmKey, bootKey}; null without checkpoint support). */
|
|
1900
|
+
keys: () => (ckptOn ? { vmKey, bootKey } : null),
|
|
1901
|
+
/**
|
|
1902
|
+
* P-B11: wmd's control session (src/host/ctl.js ControlClient), when a channel service
|
|
1903
|
+
* {kind: 'control', yamux: true} was given: run / startExec / attach / kill / procs /
|
|
1904
|
+
* simple over the guest init's yamux server. null otherwise.
|
|
1905
|
+
*/
|
|
1906
|
+
control,
|
|
1907
|
+
/**
|
|
1908
|
+
* Bytes (or text) for the guest's console channel (hvc0); queued until it is open.
|
|
1909
|
+
* Returns how many bytes were taken: at most consoleSpace(), so the queue for a guest
|
|
1910
|
+
* that does not read stays under channels.consoleInputBytes (4 MiB). The rest is not
|
|
1911
|
+
* sent; write it again once consoleSpace() > 0. 0 without channels or after the end.
|
|
1912
|
+
*/
|
|
1913
|
+
consoleWrite: data => {
|
|
1914
|
+
if (!chanSvc || !consoleGate || done || chanDied) return 0
|
|
1915
|
+
const all = typeof data === 'string' ? new TextEncoder().encode(data) : new Uint8Array(data)
|
|
1916
|
+
const n = consoleGate.take(all.length)
|
|
1917
|
+
if (n < all.length) {
|
|
1918
|
+
if (!consoleFull) {
|
|
1919
|
+
consoleFull = true
|
|
1920
|
+
emitErr(
|
|
1921
|
+
`\n[wmd-web] console input is full: the guest has not read the ${fmtMiB(consoleGate.limit)} already queued ` +
|
|
1922
|
+
`(channels.consoleInputBytes); vm.consoleWrite took ${n} of ${all.length} bytes\n`,
|
|
1923
|
+
)
|
|
1924
|
+
ev({ t: 'chan', ev: 'console-full', took: n, of: all.length })
|
|
1925
|
+
}
|
|
1926
|
+
} else consoleFull = false
|
|
1927
|
+
if (!n) return 0
|
|
1928
|
+
const bytes = all.slice(0, n) // our own copy: the caller's buffer is not transferred
|
|
1929
|
+
chanSvc.postMessage({ t: 'console', bytes }, [bytes.buffer])
|
|
1930
|
+
return n
|
|
1931
|
+
},
|
|
1932
|
+
/** Bytes vm.consoleWrite can take now (0 without channels or after the end). */
|
|
1933
|
+
consoleSpace: () => (consoleGate && chanSvc && !done && !chanDied ? consoleGate.space() : 0),
|
|
1934
|
+
/**
|
|
1935
|
+
* P-B10: the guest network (null without a net service with a relay or a switch): stats() asks the
|
|
1936
|
+
* network Worker for its stack and relay stats; dropRelay() drops the relay WebSocket
|
|
1937
|
+
* as a network failure would (test hook).
|
|
1938
|
+
*/
|
|
1939
|
+
net: netWorker
|
|
1940
|
+
? {
|
|
1941
|
+
stats: () =>
|
|
1942
|
+
new Promise(res => {
|
|
1943
|
+
if (!netWorker) return res(netStats)
|
|
1944
|
+
const id = Math.random()
|
|
1945
|
+
netWaiters.set(id, res)
|
|
1946
|
+
netWorker.postMessage({ t: 'stats', id })
|
|
1947
|
+
setTimeout(() => netWaiters.delete(id) && res(netStats), 2000)
|
|
1948
|
+
}),
|
|
1949
|
+
dropRelay: () => netWorker?.postMessage({ t: 'drop-relay' }),
|
|
1950
|
+
/** W59: the relay's state for the page ({state, relay, reason, words}); returns a function that stops the calls */
|
|
1951
|
+
onStatus: cb => {
|
|
1952
|
+
netStatusCbs.add(cb)
|
|
1953
|
+
return () => netStatusCbs.delete(cb)
|
|
1954
|
+
},
|
|
1955
|
+
/** W59: the last status (null before the first) */
|
|
1956
|
+
status: () => netStatus,
|
|
1957
|
+
/** W56: the guest's name and address on its switch (null without one) */
|
|
1958
|
+
member: netMember ? { name: netMember.name, ip: netMember.ip } : null,
|
|
1959
|
+
}
|
|
1960
|
+
: null,
|
|
1961
|
+
/**
|
|
1962
|
+
* W62: the guest's writable layer on 9P service `port` (the files it made, changed and removed
|
|
1963
|
+
* under the service's `at`) as one pack: Promise<{bytes (Uint8Array, a pack), entries, files,
|
|
1964
|
+
* whiteouts, opaque, dataBytes, ms}>. Booted again with `layers: [bytes, ...the image's layers]`
|
|
1965
|
+
* the guest finds the tree it left (a build committed once, cached between visits). The tree
|
|
1966
|
+
* is read as it is at that moment: have the guest finish its writes (`sync`) first. Rejects
|
|
1967
|
+
* after the machine has ended, as the service's tree ends with it.
|
|
1968
|
+
*/
|
|
1969
|
+
exportUpper: port =>
|
|
1970
|
+
new Promise((res, rej) => {
|
|
1971
|
+
if (!chanSvc || done || chanDied) return rej(new Error('exportUpper: the machine has ended (its files ended with it)'))
|
|
1972
|
+
const svc = chanSvcs.find(s => s.port === port && (s.kind === '9p' || s.kind === 2))
|
|
1973
|
+
if (!svc) return rej(new Error(`exportUpper: no 9P service on port ${port}`))
|
|
1974
|
+
const id = Math.random()
|
|
1975
|
+
upperWaiters.set(id, { res, rej })
|
|
1976
|
+
chanSvc.postMessage({ t: 'export-upper', id, port })
|
|
1977
|
+
}),
|
|
1978
|
+
/** P-B3 channel counters, and the service's latest stats (refreshed every 250 ms). */
|
|
1979
|
+
chanStats: () => (chanSab ? { ...chanCounters(chanSab), service: chanStats, died: chanDied } : null),
|
|
1980
|
+
/**
|
|
1981
|
+
* Test hooks for the channel service: 'terminate' (the page terminates it), 'die' (it
|
|
1982
|
+
* calls self.close(): only its lock tells the page), 'crash' (an exception in its loop),
|
|
1983
|
+
* ['stall', ms] (busy for ms).
|
|
1984
|
+
*/
|
|
1985
|
+
chanTest: (what, arg) => {
|
|
1986
|
+
if (!chanSvc) return false
|
|
1987
|
+
if (what === 'terminate') {
|
|
1988
|
+
chanSvc.terminate()
|
|
1989
|
+
serviceDied('the channel service Worker was terminated')
|
|
1990
|
+
} else chanSvc.postMessage({ t: what, ms: arg })
|
|
1991
|
+
return true
|
|
1992
|
+
},
|
|
1993
|
+
/**
|
|
1994
|
+
* P-B8 test hook (needs bootWmd({stallHook: true})): the Worker running guest thread `tid`
|
|
1995
|
+
* (1 = _start, vCPU 0) blocks at its next host call for `ms`, as if the browser had stopped
|
|
1996
|
+
* it. One stall at a time: false if the hook is off, the machine has ended, or a stall is
|
|
1997
|
+
* still armed. vm.stallThread(0) ends a running stall early. vm.stats().stall reports it.
|
|
1998
|
+
*/
|
|
1999
|
+
stallThread: (tid, ms = 0) => {
|
|
2000
|
+
if (!o.stallHook || done) return false
|
|
2001
|
+
if (!tid) {
|
|
2002
|
+
Atomics.store(ctl, CTL_STALL_TID, 0)
|
|
2003
|
+
Atomics.add(ctl, CTL_STALL_WAKE, 1)
|
|
2004
|
+
Atomics.notify(ctl, CTL_STALL_WAKE)
|
|
2005
|
+
return true
|
|
2006
|
+
}
|
|
2007
|
+
Atomics.store(ctl, CTL_STALL_MS, Math.max(0, Math.min(2 ** 31 - 1, Math.round(ms))))
|
|
2008
|
+
return Atomics.compareExchange(ctl, CTL_STALL_TID, 0, tid) === 0
|
|
2009
|
+
},
|
|
2010
|
+
/** Stop if running and drop the last reference this handle holds (the memory). */
|
|
2011
|
+
dispose: () => {
|
|
2012
|
+
finish(EXIT_STOPPED, 'stopped')
|
|
2013
|
+
memory = null
|
|
2014
|
+
},
|
|
2015
|
+
stats: () => ({
|
|
2016
|
+
exited: Atomics.load(ctl, CTL_EXIT),
|
|
2017
|
+
exitCode: Atomics.load(ctl, CTL_EXIT_CODE),
|
|
2018
|
+
liveThreads: Atomics.load(ctl, CTL_LIVE_THREADS),
|
|
2019
|
+
poolWorkers: Atomics.load(ctl, CTL_POOL_N),
|
|
2020
|
+
spawnFailures: Atomics.load(ctl, CTL_SPAWN_FAIL),
|
|
2021
|
+
memoryPages: memory ? memory.buffer.byteLength / PAGE : null,
|
|
2022
|
+
jit: jitStore ? readJitStats(jitStore) : null,
|
|
2023
|
+
hostCalls: hostCallCounts(ctl),
|
|
2024
|
+
throttle: throttle ? throttle.state() : null,
|
|
2025
|
+
noProgressMs: done ? null : Math.round(watchdog.idleMs()),
|
|
2026
|
+
stall: o.stallHook
|
|
2027
|
+
? {
|
|
2028
|
+
armed: Atomics.load(ctl, CTL_STALL_TID),
|
|
2029
|
+
taken: Atomics.load(ctl, CTL_STALL_TAKEN),
|
|
2030
|
+
done: Atomics.load(ctl, CTL_STALL_DONE),
|
|
2031
|
+
at: new Float64Array(ctlSab)[F64_STALL_AT / 8],
|
|
2032
|
+
end: new Float64Array(ctlSab)[F64_STALL_END / 8],
|
|
2033
|
+
}
|
|
2034
|
+
: null,
|
|
2035
|
+
}),
|
|
2036
|
+
}
|
|
2037
|
+
}
|