@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.
Files changed (92) hide show
  1. package/README.md +2601 -2
  2. package/RELEASE.json +123 -0
  3. package/export-page/coi-sw.js +20 -0
  4. package/export-page/index.html +17 -0
  5. package/export-page/run.css +10 -0
  6. package/export-page/run.js +236 -0
  7. package/guest/arm64/Image +4 -0
  8. package/guest/arm64/Image-4k +4 -0
  9. package/guest/arm64/initramfs.cpio.gz +0 -0
  10. package/guest/arm64/vm.wasm +0 -0
  11. package/package.json +36 -4
  12. package/page/sw.js +381 -0
  13. package/relay/control.ts +139 -0
  14. package/relay/engine.ts +787 -0
  15. package/relay/hosted.ts +660 -0
  16. package/relay/keys.ts +53 -0
  17. package/relay/local-tls.ts +121 -0
  18. package/relay/lock.ts +81 -0
  19. package/relay/logfile.ts +138 -0
  20. package/relay/main.ts +642 -0
  21. package/relay/policy.ts +241 -0
  22. package/relay/relay.ts +266 -0
  23. package/relay/token.ts +116 -0
  24. package/relay/v1.ts +107 -0
  25. package/relay/wisp.ts +615 -0
  26. package/src/base.js +325 -0
  27. package/src/boot.js +2037 -0
  28. package/src/calib-worker.js +40 -0
  29. package/src/fs/9p/codec.js +303 -0
  30. package/src/fs/9p/server.js +521 -0
  31. package/src/fs/errno.js +43 -0
  32. package/src/fs/graph.js +380 -0
  33. package/src/fs/overlay.js +1018 -0
  34. package/src/fs/pack-write.js +165 -0
  35. package/src/fs/pack.js +427 -0
  36. package/src/fs/seed.js +22 -0
  37. package/src/fs/svc-snapshot.js +65 -0
  38. package/src/fs/upper-pack.js +112 -0
  39. package/src/host/base-cache.js +195 -0
  40. package/src/host/base-file.js +328 -0
  41. package/src/host/base-io.js +93 -0
  42. package/src/host/boot-events.js +209 -0
  43. package/src/host/chan-service.js +300 -0
  44. package/src/host/chan-services.js +689 -0
  45. package/src/host/chan.js +655 -0
  46. package/src/host/ckpt-file.js +395 -0
  47. package/src/host/ckpt-io.js +207 -0
  48. package/src/host/ckpt-point.js +160 -0
  49. package/src/host/ckpt-save.js +64 -0
  50. package/src/host/ckpt.js +434 -0
  51. package/src/host/clock.js +111 -0
  52. package/src/host/ctl.js +778 -0
  53. package/src/host/errno.js +18 -0
  54. package/src/host/fs.js +167 -0
  55. package/src/host/gcnudge.js +83 -0
  56. package/src/host/i64shim.js +81 -0
  57. package/src/host/jit-cache.js +341 -0
  58. package/src/host/jit-hub-core.js +208 -0
  59. package/src/host/jit-hub.js +93 -0
  60. package/src/host/jit-snap.js +226 -0
  61. package/src/host/jit.js +1903 -0
  62. package/src/host/layout.js +100 -0
  63. package/src/host/monitor.js +110 -0
  64. package/src/host/p9mount.js +27 -0
  65. package/src/host/ram-mem.js +125 -0
  66. package/src/host/stdio.js +143 -0
  67. package/src/host/threads.js +109 -0
  68. package/src/host/wasi.js +335 -0
  69. package/src/host/wmd-imports.js +71 -0
  70. package/src/host/yamux.js +387 -0
  71. package/src/image-files.js +154 -0
  72. package/src/net/dhcp-dns.js +126 -0
  73. package/src/net/net-worker.js +172 -0
  74. package/src/net/packet.js +227 -0
  75. package/src/net/peer.js +230 -0
  76. package/src/net/proto.js +189 -0
  77. package/src/net/relay-client.js +226 -0
  78. package/src/net/stack.js +867 -0
  79. package/src/net/switch.js +160 -0
  80. package/src/net/wisp-client.js +570 -0
  81. package/src/net/wisp-transport.js +196 -0
  82. package/src/net/wisp.js +511 -0
  83. package/src/registry/client.js +176 -0
  84. package/src/registry/converter.js +68 -0
  85. package/src/registry/hub.js +221 -0
  86. package/src/registry/oci.js +76 -0
  87. package/src/registry/pack-worker.js +70 -0
  88. package/src/registry/pull.js +258 -0
  89. package/src/registry/ref.js +110 -0
  90. package/src/registry/wasi-mem.js +355 -0
  91. package/src/worker.js +315 -0
  92. package/wmdpack.wasm +0 -0
@@ -0,0 +1,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
+ }