@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
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// The JIT door's module hub logic (P-B2 adversarial F1), apart from the Worker shell in
|
|
2
|
+
// jit-hub.js so tests can run it in-process. Each vCPU Worker has a port to the hub. The
|
|
3
|
+
// Worker that compiles a module (jit_load / jit_compile) posts the WebAssembly.Module here
|
|
4
|
+
// once; the hub forwards it to every other port and keeps it for ports that connect later.
|
|
5
|
+
// A posted Module shares its compiled code (it is not compiled again), so the process holds
|
|
6
|
+
// one copy of each module instead of one per vCPU Worker; Firefox has a per-process limit on
|
|
7
|
+
// wasm code. A flush (a newer generation) drops the old ones.
|
|
8
|
+
//
|
|
9
|
+
// W31: the hub is the authority on delivery. Once it has posted a record's Module to every
|
|
10
|
+
// port it has (the poster excepted: it has the Module) and keeps the Module and a copy of
|
|
11
|
+
// the module's bytes for ports to come, it sets the record's R_DELIVERED cell in store.recs
|
|
12
|
+
// (Atomics.store, after the posts) and counts it in H_DELIV. append() may then reuse that
|
|
13
|
+
// record's log bytes. A post that fails (a 'lost' message) leaves the cell clear: the door
|
|
14
|
+
// that could not get the Module compiles a local copy from the log bytes, which stay.
|
|
15
|
+
//
|
|
16
|
+
// W31b: a post can also fail on the receiving side, after the hub counted it delivered
|
|
17
|
+
// (Firefox past its per-process wasm code limit cannot deserialize a Module: 'messageerror'
|
|
18
|
+
// at the door, which cannot tell which Module it lost), and a Worker that starts late gets
|
|
19
|
+
// the replay of Modules whose log bytes are long reused. Either door asks for the bytes
|
|
20
|
+
// ({t:'want'}) and compiles its own copy from them, so the log never has to keep them.
|
|
21
|
+
//
|
|
22
|
+
// ports: objects with postMessage(msg); the owner feeds their messages to receive(port, msg).
|
|
23
|
+
// The bytes kept are capped (bytesMax, oldest evicted first; the Modules themselves are not):
|
|
24
|
+
// a 'want' for evicted bytes is answered 'lost', and that door then fails at once with a
|
|
25
|
+
// clear error (only a Worker that also lost the Module itself needs them).
|
|
26
|
+
//
|
|
27
|
+
// in: {t:'mod', logOff, gen, module, bytes} | {t:'flush', gen} (forwarded, without bytes)
|
|
28
|
+
// {t:'want', logOff, gen} a door needs the bytes of that record
|
|
29
|
+
// {t:'drop', gen, owners} D-514: forget these Modules (forwarded, to wake syncs)
|
|
30
|
+
// {t:'resend', logOff, gen} W57: a door did not get that Module: post it again (no answer when the
|
|
31
|
+
// hub has none of that generation yet: the loader's post is still coming)
|
|
32
|
+
// out: {t:'mod', logOff, gen, module} | {t:'flush', gen} ({..., resent: true} when it answers 'resend')
|
|
33
|
+
// {t:'lost', logOff, gen} a Module could not be posted, or bytes are not here
|
|
34
|
+
// {t:'bytes', logOff, gen, bytes} the answer to 'want'
|
|
35
|
+
import { H_DELIV, R_DELIVERED, REC_INTS } from './jit.js'
|
|
36
|
+
|
|
37
|
+
export function createHubCore({ bytesMax: bytesMax0 = 256 << 20 } = {}) {
|
|
38
|
+
let bytesMax = bytesMax0
|
|
39
|
+
const ports = []
|
|
40
|
+
const starting = new Set() // W57: ports whose door has not said hello yet: nothing is posted to them
|
|
41
|
+
const pulling = new Set() // W57: ports whose door asks for each Module ({t:'resend'}): no pushes
|
|
42
|
+
const owed = new Map() // W57: pulling port -> logOffs it asked for before the hub had them (sent when they come)
|
|
43
|
+
let gen = 0
|
|
44
|
+
let mods = new Map() // logOff (the owner record index) -> Module (generation `gen`)
|
|
45
|
+
let bytes = new Map() // logOff -> the module's bytes (Uint8Array, generation `gen`), oldest first
|
|
46
|
+
let bytesHeld = 0
|
|
47
|
+
let answered = 0
|
|
48
|
+
let evicted = 0
|
|
49
|
+
let forwarded = 0
|
|
50
|
+
let resent = 0
|
|
51
|
+
let dropped = 0
|
|
52
|
+
let R = null
|
|
53
|
+
let hdr = null
|
|
54
|
+
|
|
55
|
+
function newGen(g) {
|
|
56
|
+
if (g <= gen) return
|
|
57
|
+
gen = g
|
|
58
|
+
mods = new Map()
|
|
59
|
+
bytes = new Map()
|
|
60
|
+
bytesHeld = 0
|
|
61
|
+
owed.clear()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// A post can fail (Firefox past its code limit may fail to serialize a Module): tell
|
|
65
|
+
// that Worker, so a sync waiting for the Module compiles a copy instead of waiting.
|
|
66
|
+
function send(p, m) {
|
|
67
|
+
try {
|
|
68
|
+
p.postMessage(m)
|
|
69
|
+
forwarded++
|
|
70
|
+
return true
|
|
71
|
+
} catch {
|
|
72
|
+
try {
|
|
73
|
+
p.postMessage({ t: 'lost', logOff: m.logOff, gen: m.gen })
|
|
74
|
+
} catch {}
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function receive(from, m) {
|
|
80
|
+
if (m.t === 'hello') {
|
|
81
|
+
// W57: the door is listening: now its replay (every Module of the generation, which includes
|
|
82
|
+
// the ones forwarded while it was starting), then the new ones
|
|
83
|
+
if (!starting.delete(from)) return
|
|
84
|
+
// D-512: a door that asks for each Module gets none unasked. A Worker that never reads its port (a
|
|
85
|
+
// compile thread, a helper waiting for work) kept every Module posted to it alive: Firefox frees a
|
|
86
|
+
// Module's code only once no Worker's queue holds it, and ran out of code space after a release.
|
|
87
|
+
if (m.pull) return void pulling.add(from)
|
|
88
|
+
for (const [logOff, module] of mods) send(from, { t: 'mod', logOff, gen, module })
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
if (m.t === 'flush') {
|
|
92
|
+
newGen(m.gen)
|
|
93
|
+
for (const p of ports) if (p !== from && !starting.has(p)) p.postMessage(m) // wakes syncs waiting for old modules
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
if (m.t === 'drop') {
|
|
97
|
+
// D-514: the guest dropped these cold modules: let their code go (a door that needs one again loads its bytes anew)
|
|
98
|
+
if (m.gen !== gen) return
|
|
99
|
+
for (const o of m.owners) {
|
|
100
|
+
mods.delete(o)
|
|
101
|
+
const b = bytes.get(o)
|
|
102
|
+
if (b) {
|
|
103
|
+
bytes.delete(o)
|
|
104
|
+
bytesHeld -= b.byteLength
|
|
105
|
+
}
|
|
106
|
+
for (const set of owed.values()) set.delete(o)
|
|
107
|
+
}
|
|
108
|
+
dropped += m.owners.length
|
|
109
|
+
for (const p of ports) if (p !== from && !starting.has(p)) p.postMessage(m) // wakes syncs waiting for one of them
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
if (m.t === 'want') {
|
|
113
|
+
const b = m.gen === gen ? bytes.get(m.logOff) : undefined
|
|
114
|
+
try {
|
|
115
|
+
from.postMessage(b ? { t: 'bytes', logOff: m.logOff, gen, bytes: b } : { t: 'lost', logOff: m.logOff, gen: m.gen })
|
|
116
|
+
if (b) answered++
|
|
117
|
+
} catch {}
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
if (m.t === 'pull') {
|
|
121
|
+
pulling.add(from)
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
if (m.t === 'resend') {
|
|
125
|
+
// W57: Firefox lost runs of pushed Module messages ('messageerror', its code space far from full)
|
|
126
|
+
// but delivered the ones posted in answer to the door's request. Without this the door compiled its
|
|
127
|
+
// own copy, every vCPU did, and the copies used up the code space the shared Modules were saving.
|
|
128
|
+
// A Module the hub does not have yet (the loader's post is still on its way) is sent to a pulling
|
|
129
|
+
// port when it comes: nothing else is pushed to that port.
|
|
130
|
+
if (m.gen !== gen) return
|
|
131
|
+
for (const logOff of m.logOffs || [m.logOff]) {
|
|
132
|
+
const mod = mods.get(logOff)
|
|
133
|
+
if (mod) {
|
|
134
|
+
if (send(from, { t: 'mod', logOff, gen, module: mod, resent: true })) resent++
|
|
135
|
+
} else if (pulling.has(from)) {
|
|
136
|
+
let o = owed.get(from)
|
|
137
|
+
if (!o) owed.set(from, (o = new Set()))
|
|
138
|
+
o.add(logOff)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
if (m.t !== 'mod' || m.gen < gen) return
|
|
144
|
+
newGen(m.gen)
|
|
145
|
+
if (mods.has(m.logOff)) return
|
|
146
|
+
mods.set(m.logOff, m.module)
|
|
147
|
+
const kept = m.bytes instanceof Uint8Array
|
|
148
|
+
if (kept) {
|
|
149
|
+
bytes.set(m.logOff, m.bytes)
|
|
150
|
+
bytesHeld += m.bytes.byteLength
|
|
151
|
+
for (const [k, b] of bytes) {
|
|
152
|
+
if (bytesHeld <= bytesMax || k === m.logOff) break
|
|
153
|
+
bytes.delete(k)
|
|
154
|
+
bytesHeld -= b.byteLength
|
|
155
|
+
evicted++
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const fwd = { t: 'mod', logOff: m.logOff, gen: m.gen, module: m.module }
|
|
159
|
+
let all = true
|
|
160
|
+
for (const p of ports) if (p !== from && !starting.has(p) && !pulling.has(p) && !send(p, fwd)) all = false // a starting port gets it in its replay, a pulling one when it asks
|
|
161
|
+
for (const [p, o] of owed) if (o.delete(m.logOff) && send(p, { ...fwd, resent: true })) resent++
|
|
162
|
+
if (all && kept && R) {
|
|
163
|
+
Atomics.store(R, m.logOff * REC_INTS + R_DELIVERED, 1) // only now: after every post, with the Module and its bytes kept
|
|
164
|
+
Atomics.add(hdr, H_DELIV, 1)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
/** The store whose records this hub marks delivered (the page's, from createJitStore). */
|
|
170
|
+
setStore(store) {
|
|
171
|
+
R = new Int32Array(store.recs)
|
|
172
|
+
hdr = new Int32Array(store.ctl, 0, 16)
|
|
173
|
+
},
|
|
174
|
+
/**
|
|
175
|
+
* A new port: once its door says hello it gets every Module of the generation (a replay), then the
|
|
176
|
+
* new ones. W57: nothing before that; Firefox delivered the Modules posted to a Worker that was still
|
|
177
|
+
* starting as 'messageerror' (each vCPU then compiled its own copies and ran out of code space).
|
|
178
|
+
*/
|
|
179
|
+
addPort(port) {
|
|
180
|
+
ports.push(port)
|
|
181
|
+
starting.add(port)
|
|
182
|
+
},
|
|
183
|
+
receive,
|
|
184
|
+
/** P-B9: the bytes the hub keeps for these owner records of the current generation (copies), and the ones it does not. */
|
|
185
|
+
bytesFor(owners) {
|
|
186
|
+
const got = []
|
|
187
|
+
const missing = []
|
|
188
|
+
for (const o of owners) {
|
|
189
|
+
const b = bytes.get(o)
|
|
190
|
+
if (b) got.push([o, b.slice()])
|
|
191
|
+
else missing.push(o)
|
|
192
|
+
}
|
|
193
|
+
return { gen, got, missing }
|
|
194
|
+
},
|
|
195
|
+
/** W57: [Module, bytes] of each Module of the generation whose bytes the hub still keeps. */
|
|
196
|
+
*kept() {
|
|
197
|
+
for (const [logOff, module] of mods) {
|
|
198
|
+
const b = bytes.get(logOff)
|
|
199
|
+
if (b) yield [module, b]
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
/** P-B9: a checkpointing machine keeps every module's bytes: raise the limit (bytes held). */
|
|
203
|
+
setBytesMax(n) {
|
|
204
|
+
if (n > bytesMax) bytesMax = n
|
|
205
|
+
},
|
|
206
|
+
stats: () => ({ ports: ports.length, modules: mods.size, gen, forwarded, answered, bytesHeld, evicted, resent, dropped }),
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// The JIT door's module hub (P-B2 adversarial F1): one Worker that never runs guest
|
|
2
|
+
// code, so it always answers messages. The logic is jit-hub-core.js.
|
|
3
|
+
//
|
|
4
|
+
// in (self): {t:'store', recs, ctl} the JIT store (once, before any port)
|
|
5
|
+
// {t:'port', port} a new Worker's port (from the page)
|
|
6
|
+
// {t:'bytes-of', id, owners} P-B9: copies of those records' bytes -> {t:'bytes-of', id, gen, got:[[owner, bytes]], missing}
|
|
7
|
+
// {t:'config', bytesMax} P-B9: raise the limit on the bytes kept
|
|
8
|
+
// {t:'replay', id, gen, mods:[{owner, bytes}]} P-B9/W57: a restore's modules, compiled here (not in the
|
|
9
|
+
// page: a Module the page compiled keeps its code alive until the page collects
|
|
10
|
+
// its garbage, so in Firefox each restored machine's code outlived it) and kept
|
|
11
|
+
// as if a door had posted them -> {t:'replayed', id, n, reused} | {t:'replayed', id, error}
|
|
12
|
+
// {t:'handoff', port} W57: post every Module of this generation with its bytes on port ({t:'mods',
|
|
13
|
+
// list:[{key, module, bytes}]}), for the hub of the machine a restore makes
|
|
14
|
+
// {t:'adopt', port} W57: the next replay takes Modules from that port's {t:'mods'} instead of
|
|
15
|
+
// compiling the same bytes again (Firefox holds two copies of a guest's code
|
|
16
|
+
// otherwise, the old machine's until its Workers are collected, and its 2 GiB
|
|
17
|
+
// code space ran out when two guests in one page were restored). No answer
|
|
18
|
+
// in 30 s, or one that cannot be received here: the replay compiles.
|
|
19
|
+
import { gcNudgeEvery } from './gcnudge.js'
|
|
20
|
+
import { createHubCore } from './jit-hub-core.js'
|
|
21
|
+
import { moduleKey, replayModuleList, sameBytes } from './jit-snap.js'
|
|
22
|
+
|
|
23
|
+
// W32: WebKit gives a released Module's code back only after every thread that held the
|
|
24
|
+
// Module has collected it. The hub holds every Module and allocates little, so without the
|
|
25
|
+
// nudge (D-419, gcnudge.js) a flush never gave the code back: in WebKit, 93 MB of JIT code
|
|
26
|
+
// stayed after a flush with the page and the vCPUs nudging, 2 MB with the hub nudging too
|
|
27
|
+
// (docs/plans/w32-holder-results.md).
|
|
28
|
+
gcNudgeEvery()
|
|
29
|
+
|
|
30
|
+
const core = createHubCore()
|
|
31
|
+
let adopted = null // W57: a Promise of [{key, module, bytes}] from the machine a restore replaces
|
|
32
|
+
|
|
33
|
+
self.onmessage = e => {
|
|
34
|
+
const m = e.data
|
|
35
|
+
if (m.t === 'store') {
|
|
36
|
+
core.setStore(m)
|
|
37
|
+
} else if (m.t === 'port') {
|
|
38
|
+
const port = m.port
|
|
39
|
+
port.onmessage = ev => core.receive(port, ev.data)
|
|
40
|
+
core.addPort(port)
|
|
41
|
+
} else if (m.t === 'bytes-of') {
|
|
42
|
+
// P-B9: the bytes of module records the log released (a checkpoint needs them)
|
|
43
|
+
const r = core.bytesFor(m.owners)
|
|
44
|
+
self.postMessage({ t: 'bytes-of', id: m.id, ...r }, r.got.map(([, b]) => b.buffer))
|
|
45
|
+
} else if (m.t === 'replay') {
|
|
46
|
+
const from = { postMessage() {} } // not a port: every port gets the Modules (none yet: they are kept for the vCPUs)
|
|
47
|
+
const take = adopted || Promise.resolve([])
|
|
48
|
+
adopted = null
|
|
49
|
+
let reused = 0
|
|
50
|
+
take
|
|
51
|
+
.then(list => {
|
|
52
|
+
const have = new Map(list.map(x => [x.key, x]))
|
|
53
|
+
const reuse = b => {
|
|
54
|
+
const x = have.get(moduleKey(b))
|
|
55
|
+
if (!x || !sameBytes(x.bytes, b)) return null
|
|
56
|
+
reused++
|
|
57
|
+
return x.module
|
|
58
|
+
}
|
|
59
|
+
return replayModuleList(m.mods, m.gen, mod => core.receive(from, mod), { reuse })
|
|
60
|
+
})
|
|
61
|
+
.then(
|
|
62
|
+
n => self.postMessage({ t: 'replayed', id: m.id, n, reused }),
|
|
63
|
+
err => self.postMessage({ t: 'replayed', id: m.id, error: String(err && err.message ? err.message : err) }),
|
|
64
|
+
)
|
|
65
|
+
} else if (m.t === 'handoff') {
|
|
66
|
+
const list = []
|
|
67
|
+
for (const [module, b] of core.kept()) list.push({ key: moduleKey(b), module, bytes: b })
|
|
68
|
+
try {
|
|
69
|
+
m.port.postMessage({ t: 'mods', list })
|
|
70
|
+
} catch {
|
|
71
|
+
m.port.postMessage({ t: 'mods', list: [] }) // a Module that cannot be posted: the new hub compiles them all
|
|
72
|
+
}
|
|
73
|
+
} else if (m.t === 'adopt') {
|
|
74
|
+
const port = m.port
|
|
75
|
+
adopted = new Promise(res => {
|
|
76
|
+
let over = false
|
|
77
|
+
const done = list => {
|
|
78
|
+
if (over) return
|
|
79
|
+
over = true
|
|
80
|
+
port.onmessage = port.onmessageerror = null
|
|
81
|
+
port.close()
|
|
82
|
+
res(list)
|
|
83
|
+
}
|
|
84
|
+
port.onmessage = e => done(e.data?.list || [])
|
|
85
|
+
port.onmessageerror = () => done([]) // not deliverable here (Firefox past its code limit): compile instead
|
|
86
|
+
setTimeout(() => done([]), 30_000)
|
|
87
|
+
})
|
|
88
|
+
} else if (m.t === 'config') {
|
|
89
|
+
core.setBytesMax(m.bytesMax)
|
|
90
|
+
} else if (m.t === 'stats') {
|
|
91
|
+
self.postMessage({ t: 'stats', ...core.stats() })
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// P-B9: the JIT door's state in a checkpoint, and back.
|
|
2
|
+
//
|
|
3
|
+
// What compiled code a restored guest needs. The guest's block cache (in memory 0, so in the
|
|
4
|
+
// checkpoint) names compiled blocks by table index, and a table index is a slot of the door's
|
|
5
|
+
// slot list: slot k = tableMin + k, in every thread's table. A restored guest that finds an
|
|
6
|
+
// index with nothing behind it does not recover: the dispatcher bails out with SYNC_MISS, the
|
|
7
|
+
// guest retries the same block and gets the same answer (cache.c; the guest has no "restore
|
|
8
|
+
// without slots" resume yet, research/10 5.8). So the slot list has to come back exactly, and
|
|
9
|
+
// that needs each live module's bytes.
|
|
10
|
+
//
|
|
11
|
+
// What is kept: the current generation's records (a flush makes the older ones dead: nothing in
|
|
12
|
+
// a machine run calls jit_flush today) with their slot bases and function counts, and the bytes
|
|
13
|
+
// of each module that owns its bytes. The bytes are in the module log, or, when the hub
|
|
14
|
+
// delivered the Module and the log reused the space (W31 reclaim), in the hub's copy (W31b),
|
|
15
|
+
// which the page asks for first (collectBytes).
|
|
16
|
+
//
|
|
17
|
+
// What is not kept: compiled code. The restoring page compiles every module again
|
|
18
|
+
// (WebAssembly.compile in parallel, in the page) and hands the Modules to the hub, as the
|
|
19
|
+
// loading door did in the first run, before any vCPU starts. That compile is the cost of a
|
|
20
|
+
// restore on the browser: it is measured in the proof.
|
|
21
|
+
import { H_GEN, H_LIVE_KIB, H_LOG_USED, H_MOD_START, H_NMOD, H_NSLOTS, H_SLOT0, HDR_INTS, REC_INTS, R_DELIVERED, R_DROPPED, R_FREED, R_GEN, R_LEN, R_LOG_OFF, R_NFUNCS, R_OWNER, R_SLOT_BASE, createJitStore, hash64 } from './jit.js'
|
|
22
|
+
|
|
23
|
+
const align8 = n => (n + 7) & ~7
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The owners (record indexes) of the current generation whose bytes the module log no longer
|
|
27
|
+
* holds: the page asks the hub for these before capture().
|
|
28
|
+
*/
|
|
29
|
+
export function ownersNeedingHub(store) {
|
|
30
|
+
const hdr = new Int32Array(store.ctl, 0, HDR_INTS)
|
|
31
|
+
const R = new Int32Array(store.recs)
|
|
32
|
+
const out = []
|
|
33
|
+
const n = Atomics.load(hdr, H_NMOD)
|
|
34
|
+
for (let r = Atomics.load(hdr, H_MOD_START); r < n; r++) {
|
|
35
|
+
const b = r * REC_INTS
|
|
36
|
+
if (Atomics.load(R, b + R_OWNER) === r && Atomics.load(R, b + R_FREED) && !Atomics.load(R, b + R_DROPPED)) out.push(r)
|
|
37
|
+
}
|
|
38
|
+
return out
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Reads the store. The guest must be stopped (compile workers included): the store is read
|
|
43
|
+
* without its lock. hubBytes: Map owner -> Uint8Array for the owners ownersNeedingHub named.
|
|
44
|
+
* Returns {gen, nslots, nrec, recs: Int32Array, log: Uint8Array} in the store's own layout, but
|
|
45
|
+
* dense: records renumbered from 0, the log holding each owner's bytes once.
|
|
46
|
+
*/
|
|
47
|
+
export function captureJit(store, hubBytes = new Map()) {
|
|
48
|
+
const hdr = new Int32Array(store.ctl, 0, HDR_INTS)
|
|
49
|
+
const R = new Int32Array(store.recs)
|
|
50
|
+
const L = new Uint8Array(store.log)
|
|
51
|
+
const gen = Atomics.load(hdr, H_GEN)
|
|
52
|
+
const first = Atomics.load(hdr, H_MOD_START)
|
|
53
|
+
const n = Atomics.load(hdr, H_NMOD)
|
|
54
|
+
// D-514: dropped modules are left out (the guest's table names none of their functions)
|
|
55
|
+
const kept = []
|
|
56
|
+
for (let r = first; r < n; r++) if (!Atomics.load(R, r * REC_INTS + R_DROPPED)) kept.push(r)
|
|
57
|
+
const nrec = kept.length
|
|
58
|
+
const recs = new Int32Array(nrec * REC_INTS)
|
|
59
|
+
const index = new Map() // old owner -> new index
|
|
60
|
+
let total = 0
|
|
61
|
+
for (const r of kept) {
|
|
62
|
+
const b = r * REC_INTS
|
|
63
|
+
if (Atomics.load(R, b + R_OWNER) === r) total += align8(Atomics.load(R, b + R_LEN))
|
|
64
|
+
}
|
|
65
|
+
const log = new Uint8Array(total)
|
|
66
|
+
let off = 0
|
|
67
|
+
for (let i = 0; i < nrec; i++) {
|
|
68
|
+
const r = kept[i]
|
|
69
|
+
const b = r * REC_INTS
|
|
70
|
+
const o = Atomics.load(R, b + R_OWNER)
|
|
71
|
+
const nb = i * REC_INTS
|
|
72
|
+
for (let k = 0; k < REC_INTS; k++) recs[nb + k] = Atomics.load(R, b + k)
|
|
73
|
+
recs[nb + R_GEN] = gen
|
|
74
|
+
recs[nb + R_DELIVERED] = 0 // the restoring hub marks it
|
|
75
|
+
recs[nb + R_FREED] = 0
|
|
76
|
+
if (o === r) {
|
|
77
|
+
const len = Atomics.load(R, b + R_LEN)
|
|
78
|
+
let bytes
|
|
79
|
+
if (Atomics.load(R, b + R_FREED)) {
|
|
80
|
+
bytes = hubBytes.get(r)
|
|
81
|
+
if (!bytes || bytes.length !== len) {
|
|
82
|
+
throw new Error(`jit snapshot: the bytes of module record ${r} (${len} bytes) were released from the module log and the hub does not hold them (evicted past its limit)`)
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
const lo = Atomics.load(R, b + R_LOG_OFF)
|
|
86
|
+
bytes = L.subarray(lo, lo + len)
|
|
87
|
+
// released while we copy would mean a vCPU is still running: the caller's contract
|
|
88
|
+
if (Atomics.load(R, b + R_FREED)) throw new Error(`jit snapshot: record ${r} was released while it was read (the guest is not stopped)`)
|
|
89
|
+
}
|
|
90
|
+
log.set(bytes, off)
|
|
91
|
+
recs[nb + R_LOG_OFF] = off
|
|
92
|
+
index.set(r, i)
|
|
93
|
+
off += align8(len)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (let i = 0; i < nrec; i++) {
|
|
97
|
+
const r = kept[i]
|
|
98
|
+
const nb = i * REC_INTS
|
|
99
|
+
const o = Atomics.load(R, r * REC_INTS + R_OWNER)
|
|
100
|
+
const mapped = index.get(o)
|
|
101
|
+
if (mapped === undefined) throw new Error(`jit snapshot: record ${r} is de-duplicated against record ${o}, which is not in this generation`)
|
|
102
|
+
recs[nb + R_OWNER] = mapped
|
|
103
|
+
if (o !== r) recs[nb + R_LOG_OFF] = recs[mapped * REC_INTS + R_LOG_OFF]
|
|
104
|
+
}
|
|
105
|
+
return { gen, nslots: Atomics.load(hdr, H_NSLOTS), slot0: Atomics.load(hdr, H_SLOT0), nrec, recs, log }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** D-514: records saved with an older, narrower layout (14 cells before D-514) widened to REC_INTS. */
|
|
109
|
+
export function widenRecs(recs, nrec) {
|
|
110
|
+
if (!nrec || recs.length === nrec * REC_INTS || recs.length % nrec) return recs
|
|
111
|
+
const w = recs.length / nrec
|
|
112
|
+
if (w > REC_INTS) return recs
|
|
113
|
+
const out = new Int32Array(nrec * REC_INTS)
|
|
114
|
+
for (let r = 0; r < nrec; r++) out.set(recs.subarray(r * w, r * w + w), r * REC_INTS)
|
|
115
|
+
return out
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** A fresh store holding a captured slot list (no Module yet: replay() makes those). */
|
|
119
|
+
export function restoreStore(snap, opts = {}) {
|
|
120
|
+
const logMax = Math.max(opts.logMax ?? 1 << 30, snap.log.length)
|
|
121
|
+
const store = createJitStore({ ...opts, logMax, logInitial: Math.min(logMax, Math.max(opts.logInitial ?? 1 << 20, snap.log.length)), recInitial: Math.max(opts.recInitial ?? 4096, snap.nrec) })
|
|
122
|
+
const hdr = new Int32Array(store.ctl, 0, HDR_INTS)
|
|
123
|
+
const R = new Int32Array(store.recs)
|
|
124
|
+
const L = new Uint8Array(store.log)
|
|
125
|
+
R.set(snap.recs)
|
|
126
|
+
L.set(snap.log)
|
|
127
|
+
Atomics.store(hdr, H_GEN, snap.gen)
|
|
128
|
+
Atomics.store(hdr, H_MOD_START, 0)
|
|
129
|
+
Atomics.store(hdr, H_NMOD, snap.nrec)
|
|
130
|
+
Atomics.store(hdr, H_NSLOTS, snap.nslots)
|
|
131
|
+
// W34: the generation's first slot (slots are never rewound) and the KiB its modules hold
|
|
132
|
+
// against the budget (each owner record counted once, as jit_load adds it). A snapshot from
|
|
133
|
+
// before W34 has no slot0: its generation starts at its lowest slot base.
|
|
134
|
+
let slot0 = snap.nslots
|
|
135
|
+
let liveKiB = 0
|
|
136
|
+
for (let r = 0; r < snap.nrec; r++) {
|
|
137
|
+
const b = r * REC_INTS
|
|
138
|
+
slot0 = Math.min(slot0, snap.recs[b + R_SLOT_BASE])
|
|
139
|
+
if (snap.recs[b + R_OWNER] === r) liveKiB += Math.ceil(snap.recs[b + R_LEN] / 1024)
|
|
140
|
+
}
|
|
141
|
+
Atomics.store(hdr, H_SLOT0, snap.slot0 ?? slot0)
|
|
142
|
+
Atomics.store(hdr, H_LIVE_KIB, liveKiB)
|
|
143
|
+
Atomics.store(hdr, H_LOG_USED, snap.log.length)
|
|
144
|
+
return store
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The owner records of a captured snapshot with their bytes: [{owner, bytes}]. */
|
|
148
|
+
export function modulesOf(snap) {
|
|
149
|
+
const out = []
|
|
150
|
+
for (let r = 0; r < snap.nrec; r++) {
|
|
151
|
+
const b = r * REC_INTS
|
|
152
|
+
if (snap.recs[b + R_OWNER] !== r) continue
|
|
153
|
+
const lo = snap.recs[b + R_LOG_OFF]
|
|
154
|
+
out.push({ owner: r, bytes: snap.log.subarray(lo, lo + snap.recs[b + R_LEN]), nfuncs: snap.recs[b + R_NFUNCS], slotBase: snap.recs[b + R_SLOT_BASE] })
|
|
155
|
+
}
|
|
156
|
+
return out
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Compiles every module of a snapshot (WebAssembly.compile: the engine uses its background
|
|
161
|
+
* threads) and gives each to post({t:'mod', logOff: owner, gen, module, bytes}), the message a
|
|
162
|
+
* loading door sends the hub. Resolves when all are posted. concurrency bounds the compiles
|
|
163
|
+
* in flight (memory: a Module's code is held from here on).
|
|
164
|
+
*/
|
|
165
|
+
export async function replayModules(snap, post, { concurrency = 16, onProgress } = {}) {
|
|
166
|
+
return replayModuleList(modulesOf(snap), snap.gen, post, { concurrency, onProgress })
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** A module's identity by content: its length and a 64-bit hash of its bytes (W57: a restore's Modules found by content). */
|
|
170
|
+
export function moduleKey(bytes) {
|
|
171
|
+
const [a, b] = hash64(bytes)
|
|
172
|
+
return `${bytes.length}:${a >>> 0}:${b >>> 0}`
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function sameBytes(a, b) {
|
|
176
|
+
if (a.length !== b.length) return false
|
|
177
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false
|
|
178
|
+
return true
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* replayModules for a list of {owner, bytes} of generation gen (the hub's replay: W57). reuse(bytes) may return a
|
|
183
|
+
* Module already compiled from the same bytes (the machine a restore replaces), which is posted instead of a compile.
|
|
184
|
+
*/
|
|
185
|
+
export async function replayModuleList(mods, gen, post, { concurrency = 16, onProgress, reuse } = {}) {
|
|
186
|
+
let next = 0
|
|
187
|
+
let done = 0
|
|
188
|
+
async function lane() {
|
|
189
|
+
for (;;) {
|
|
190
|
+
const i = next++
|
|
191
|
+
if (i >= mods.length) return
|
|
192
|
+
const m = mods[i]
|
|
193
|
+
const module = reuse?.(m.bytes) || (await WebAssembly.compile(m.bytes))
|
|
194
|
+
post({ t: 'mod', logOff: m.owner, gen, module, bytes: m.bytes.slice() })
|
|
195
|
+
onProgress?.(++done, mods.length)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, mods.length) }, lane))
|
|
199
|
+
return mods.length
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export const jitSnapStats = snap => ({ records: snap.nrec, slots: snap.nslots, modules: modulesOf(snap).length, logBytes: snap.log.length, recBytes: snap.recs.byteLength })
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Refuses a snapshot whose records do not describe its own log: a checkpoint is read from a file, and a record that points
|
|
207
|
+
* outside the log, at an owner that is not an owner or past the slot count would make the replay read garbage.
|
|
208
|
+
*/
|
|
209
|
+
export function validateSnap(snap) {
|
|
210
|
+
const bad = why => {
|
|
211
|
+
throw new Error(`jit snapshot is damaged: ${why}`)
|
|
212
|
+
}
|
|
213
|
+
if (snap.recs.length !== snap.nrec * REC_INTS) bad(`${snap.recs.length} record cells for ${snap.nrec} records`)
|
|
214
|
+
if (!Number.isInteger(snap.nslots) || snap.nslots < 0 || snap.nslots > 0x7fffffff) bad(`slot count ${snap.nslots}`)
|
|
215
|
+
for (let r = 0; r < snap.nrec; r++) {
|
|
216
|
+
const b = r * REC_INTS
|
|
217
|
+
const owner = snap.recs[b + R_OWNER]
|
|
218
|
+
const off = snap.recs[b + R_LOG_OFF]
|
|
219
|
+
const len = snap.recs[b + R_LEN]
|
|
220
|
+
const base = snap.recs[b + R_SLOT_BASE]
|
|
221
|
+
const nf = snap.recs[b + R_NFUNCS]
|
|
222
|
+
if (owner < 0 || owner > r || snap.recs[owner * REC_INTS + R_OWNER] !== owner) bad(`record ${r} names owner ${owner}`)
|
|
223
|
+
if (off < 0 || len <= 0 || off + len > snap.log.length) bad(`record ${r} lies outside the ${snap.log.length}-byte log (${off}+${len})`)
|
|
224
|
+
if (base < 0 || nf < 0 || base + nf > snap.nslots) bad(`record ${r} has slots ${base}..${base + nf} past the slot count ${snap.nslots}`)
|
|
225
|
+
}
|
|
226
|
+
}
|