@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,1903 @@
1
+ // The JIT door: wmd.jit_load / jit_compile / jit_sync / jit_flush / jit_known / jit_note
2
+ // for vm.wasm in Web Workers (web/docs/host-interface.md §5; runner/src/jit.rs).
3
+ //
4
+ // The runner keeps one global, ordered slot list; slot k is at table index tableMin+k in
5
+ // every thread's table (tableMin: vm.wasm's table import minimum, tableMinOf). Here the list lives in SharedArrayBuffers so any Worker can
6
+ // read it synchronously (a Worker inside jit_sync cannot receive postMessage):
7
+ //
8
+ // store.ctl header cells (lock, flush generation, record count, ...) + counters
9
+ // store.recs one record per loaded module: where its bytes are in the log, its
10
+ // first slot, how many functions it exports, its content hash, timings
11
+ // store.log the module bytes. A flush never rewrites it, so a Worker that is reading
12
+ // bytes while another flushes reads valid bytes. Nothing in a machine run
13
+ // flushes, so in share mode (the hub below) the log is reclaimable (W31):
14
+ // the hub marks a record delivered once it has posted that record's Module
15
+ // to every port, and append() reuses the space of delivered records (see
16
+ // "Reclaiming the log"). Without sharing nothing is reclaimed. A Worker
17
+ // that cannot receive a Module the hub posted, or starts after its bytes
18
+ // were reused, compiles a copy from bytes the hub keeps (W31b).
19
+ //
20
+ // Each Worker compiles the bytes it has not compiled yet with the synchronous
21
+ // `new WebAssembly.Module` (allowed in Workers) and keeps the Module by log offset, so
22
+ // a module loaded twice, or again after a flush, is compiled once per Worker. Each
23
+ // thread (one vm.wasm instance + its own table) instantiates each module once per
24
+ // flush generation, with env.mem0/mem1 = the shared memory, env.tbl = its table and
25
+ // hst.h0..h16 = its own vm_helper_N exports (jit.rs instantiate_in).
26
+
27
+ export const TABLE_MIN = 155 // the default table import minimum (vm.wasm before W3.3); JIT slots start at the module's own, tableMinOf
28
+ export const HELPERS = 17 // vm_helper_0 .. vm_helper_16
29
+
30
+ // header (Int32 cells of store.ctl)
31
+ export const H_LOCK = 0
32
+ // The JS API's limit on a table's length (the same in V8, SpiderMonkey and JavaScriptCore).
33
+ export const TABLE_MAX_ENTRIES = 10_000_000
34
+ export const H_GEN = 1 // flush generation
35
+ export const H_NMOD = 2 // records published (monotonic over the whole session)
36
+ export const H_MOD_START = 3 // first record of the current generation
37
+ export const H_NSLOTS = 4 // slots in the current generation
38
+ export const H_LOG_USED = 5 // bytes of store.log in use
39
+ export const H_TABLE_MAX = 6 // largest table size any thread reached
40
+ export const H_FULL = 7 // set once an engine resource failure was seen: refuse new modules
41
+ export const H_LOG_TAIL = 8 // records below this own no live log bytes
42
+ export const H_LOG_LIM = 9 // end of the free run that starts at H_LOG_USED (0: up to the buffer's end)
43
+ export const H_DELIV = 10 // deliveries the hub has marked (a counter, only ever grows)
44
+ export const H_FAIL_D = 11 // H_DELIV + 1 at the last allocation that found no room (0: none)
45
+ export const H_FAIL_G = 12 // ... the generation then
46
+ export const H_FAIL_N = 13 // ... and the smallest size that failed
47
+ export const H_NORECLAIM = 14 // set (1) once a Worker could not receive a hub Module: the log is append-only from then on
48
+ export const JIT_OVER_BUDGET = -2 // jit_load: refused for the module budget (vm VM_JIT_OVER_BUDGET): the guest releases every module
49
+ export const JIT_LATER = -4 // D-515: jit_load: the engine takes no code now (vm VM_JIT_LATER): the guest interprets the blocks and asks again later
50
+ export const H_LIVE_KIB = 15 // KiB of module bytes loaded in the current generation (W34: the footprint budget)
51
+ export const H_SLOT0 = 16 // H_NSLOTS when the current generation started (W34: slots are never rewound); live slots = H_NSLOTS - H_SLOT0
52
+ export const H_BUDGET_KIB = 17 // D-511: a budget the engine taught us (0: none): live KiB well under where a compile ran out of code space
53
+ export const H_MARK = 18 // D-514: use marks read so far (markUse): a record's R_USED is the last one that saw its functions entered
54
+ export const H_MARK_KIB = 19 // D-514: KiB of modules loaded since the last markUse
55
+ export const H_DROPS = 20 // D-514: records in store.drops (every Worker clears their table entries when it syncs)
56
+ export const KNOWN_KEY_BYTES = 32 // W77: struct vm_known_rec's key (vm/cache.h; jit-cache.js KEY_BYTES)
57
+ export const KNOWN_NOTE_BYTES = 56 // ... and a whole record, as jit_note passes them
58
+ const MIN_LEARNED_KIB = 64 << 10
59
+ const COMPILE_RETRY_MAX_MS = 1600 // D-512: tries after 50, 100, ... 1600 ms: about 3 s in all
60
+ export const H_POOL_FULL = 21 // D-515: set while the engine's code pool is full (codePool): new modules wait
61
+ export const HDR_INTS = 22 // P-B9: jit-snap.js reads and writes the header
62
+ export const VM_CELLS = 1 << 21 // vm wasm_emit.h: the live flags and use marks are per table index modulo this
63
+ const MARK_PARTS = 8 // D-514: the use marks are read each time this part of the budget was loaded
64
+ export const KNOWN_DROPPED = -2 // D-514: jit_known's answer for a dropped block (vm cache.h VM_KNOWN_DROPPED): it is compiled at its next run
65
+ const EVICT_SPARSE = 1 / 2 // D-514: a module where fewer of its functions ran is dropped after the cold ones
66
+ const EVICT_KEEP = 5 / 8 // D-514: an eviction drops cold modules until the live ones fit in this part of the budget
67
+ const EVICT_MIN = 1 / 8 // ... and frees at least this part of it, or the guest releases every module instead
68
+ const POOL_CHECK_MS = 2000 // D-515: at most one code pool check a thread in this time
69
+ const POOL_PROBE_N = 1500000 // D-515: loop passes of the check: about 2 ms compiled, 40 ms interpreted
70
+ const POOL_SLOW_X = 6 // D-515: a check this many times slower than this thread's fastest means the pool is full
71
+ const POOL_LEARN = 3 / 4 // D-515: the budget from then on, of the modules live when the pool filled
72
+
73
+ // counters after the header: each a pair of Int32 cells (low word, unsigned; high word).
74
+ // D-419: not BigInt64 cells: Atomics.add on a BigInt64Array returns a new BigInt, garbage a
75
+ // vCPU Worker made at every jit_known (53,000 in a boot), and WebKit collects garbage late in a
76
+ // thread holding the guest's memory (D-419, gcnudge.js).
77
+ export const STATS = [
78
+ 'loads', // jit_load calls
79
+ 'compiles', // jit_compile calls (the compile-worker thread)
80
+ 'knownAsks', // jit_known calls
81
+ 'notes', // jit_note calls
82
+ 'appendCompiles', // modules compiled by the thread that loaded them
83
+ 'appendCompileNs',
84
+ 'validateNs', // WebAssembly.validate in jit_compile when it does not compile
85
+ 'syncs', // jit_sync passes (every jit_load/jit_flush/jit_sync and thread start)
86
+ 'syncNs', // total time inside sync, compiles and instantiations included
87
+ 'syncCompiles', // modules a thread compiled during sync (it had not seen the bytes)
88
+ 'syncCompileNs',
89
+ 'instantiates',
90
+ 'instantiateNs',
91
+ 'tableSets',
92
+ 'tableGrows',
93
+ 'bytes', // module bytes loaded (with duplicates)
94
+ 'logDedup', // loads whose bytes were already in the log
95
+ 'moduleCacheHits', // loads/syncs that found a compiled Module in this Worker
96
+ 'flushes',
97
+ 'failures', // jit_load/jit_compile that returned -1
98
+ 'hashNs',
99
+ 'shareSent', // Modules this door posted to the hub (compiled once, here)
100
+ 'shareRecv', // Modules received from the hub and installed (no compile)
101
+ 'shareWaits', // syncs that suspended to wait for a Module from the hub
102
+ 'shareWaitNs',
103
+ 'shareTimeouts', // waits that gave up and compiled a local copy
104
+ 'compileRetries', // a compile that threw and was tried again
105
+ 'outOfCode', // D-511: compiles the engine refused for code space: each releases every module and lowers the budget
106
+ 'refused', // jit_load/jit_compile refused after an engine resource failure (-1)
107
+ 'logReclaimedBytes', // log bytes released for reuse (the hub had delivered their Modules)
108
+ 'logWraps', // module bytes placed below the previous end of the log (reused space)
109
+ 'shareFreedWaits', // local-compile fallbacks that found the bytes delivered or released and waited for the hub instead
110
+ 'shareBytesWanted', // module bytes asked of the hub for a local copy (W31b)
111
+ 'shareBytesCompiles', // local copies compiled from bytes the hub sent
112
+ 'shareResends', // waits that ended without the Module and asked the hub to post it again (W57)
113
+ 'translated', // W77: functions jit_load/jit_compile got from the guest (blocks it translated)
114
+ 'cacheHits', // W77: jit_known answered with a function from the page's cache (jit-cache.js)
115
+ 'cacheMisses', // W77: jit_known for a key the cache's index does not have
116
+ 'cacheLoads', // W77: module bytes read from the cache (once per module and flush generation)
117
+ 'cacheLoadNs',
118
+ 'cacheFails', // W77: a key the index has whose module could not be read or loaded (the guest translates it)
119
+ 'cacheNotes', // W77: block keys sent to the page for the cache
120
+ 'knownHits', // D-513: jit_known answered from this session's notes (the same code at another physical page)
121
+ 'knownNotes', // D-513: block keys this session's table took
122
+ 'knownReloads', // D-513: modules of an older generation loaded again from the log for a jit_known hit
123
+ 'knownGone', // D-513: hits from an older generation whose module bytes the log had reused (the guest translates)
124
+ 'evictions', // D-514: budget refusals answered by dropping the modules no block entered lately
125
+ 'evictedModules', // D-514: modules those dropped
126
+ 'evictedKiB',
127
+ 'knownDropped', // D-514: hits on a dropped module: a miss, so the block is compiled again with the other blocks in use
128
+ 'marks', // D-514: passes over the use marks
129
+ 'poolChecks', // D-515: code pool checks (WebKit)
130
+ 'poolFull', // D-515: checks that found the code pool newly full
131
+ 'poolBack', // D-515: checks that found room in it again
132
+ 'poolLater', // D-515: loads answered JIT_LATER while it was full
133
+ ]
134
+ const S = Object.fromEntries(STATS.map((n, i) => [n, i]))
135
+ const TWO32 = 4294967296
136
+ /** Add v (a non-negative integer Number, up to 2^53) to counter i: low word, carry and high part. */
137
+ export function statAdd(st, i, v) {
138
+ const vlo = v % TWO32
139
+ const old = Atomics.add(st, 2 * i, vlo | 0) >>> 0
140
+ const hi = (v - vlo) / TWO32 + (old + vlo >= TWO32 ? 1 : 0)
141
+ if (hi) Atomics.add(st, 2 * i + 1, hi)
142
+ }
143
+ /**
144
+ * Counter i. Read again while the high word moved under the read; an adder between its low-word
145
+ * add and its carry (a few instructions) can still make one read 2^32 low, never the total: stats only.
146
+ */
147
+ export function statLoad(st, i) {
148
+ for (;;) {
149
+ const hi = Atomics.load(st, 2 * i + 1)
150
+ const lo = Atomics.load(st, 2 * i) >>> 0
151
+ if (Atomics.load(st, 2 * i + 1) === hi) return hi * TWO32 + lo
152
+ }
153
+ }
154
+ const STATS_OFF = HDR_INTS * 4
155
+ const CTL_BYTES = STATS_OFF + STATS.length * 8
156
+
157
+ // records (Int32 cells of store.recs)
158
+ export const REC_INTS = 17
159
+ export const R_LOG_OFF = 0
160
+ export const R_LEN = 1
161
+ export const R_SLOT_BASE = 2
162
+ export const R_NFUNCS = 3
163
+ export const R_HASH_LO = 4
164
+ export const R_HASH_HI = 5
165
+ export const R_APPEND_US = 6 // compile time in the loading thread (0: not compiled there)
166
+ export const R_SYNC_US = 7 // compile time in a thread that synced it (last one wins)
167
+ export const R_INST_US = 8 // instantiate time (last one wins)
168
+ export const R_KIND = 9 // 1 jit_load, 2 jit_compile, 3 a module from the page's cache (W77)
169
+ export const R_GEN = 10
170
+ export const R_OWNER = 11 // the record whose log allocation holds this module's bytes (itself, or the one it was de-duplicated against)
171
+ export const R_DELIVERED = 12 // set (1) only by the hub: it posted this record's Module to every port it had and keeps it (and its bytes) for later ones
172
+ export const R_FREED = 13 // set (1) only by append(), under H_LOCK: the record's log bytes may be overwritten
173
+ export const R_USED = 14 // D-514: the H_MARK that last saw one of its functions entered (the next one, when it was loaded)
174
+ export const R_DROPPED = 15 // D-514: set (1) under H_LOCK when evict dropped it: no Worker installs it, every one clears its slots
175
+ export const R_USEDN = 16 // D-514: how many of its functions were entered in the mark that set R_USED (all of them, when it was loaded)
176
+ const REC_BYTES = REC_INTS * 4
177
+
178
+ // D-513: this session's block keys (store.known): a header, KN_BUCKETS chain heads (entry + 1), then
179
+ // entries of KN_INTS: the key (8 ints), the generation, the table index, rlo, rlen, the span hash (2),
180
+ // the next entry + 1, the record that holds the function. Entries outlive a release: a hit from an
181
+ // older generation loads that record's module again from the log (store.reload: per owner record, the
182
+ // record it was loaded again as + 1 and that generation). Only read and written under vm.wasm's cache
183
+ // lock (jit_known, jit_note).
184
+ const KN_HDR = 4 // count
185
+ const KN_BUCKETS = 1 << 16
186
+ const KN_INTS = 16
187
+ const KN_E = KN_HDR + KN_BUCKETS
188
+ export const KN_MAX_ENTRIES = 1 << 20
189
+ const knBytes = n => (KN_E + n * KN_INTS) * 4
190
+
191
+ const now = () => performance.now()
192
+
193
+ /**
194
+ * D-515: JavaScriptCore stops compiling wasm once its executable pool is full, with no error: new
195
+ * code and code that becomes hot then stay in its interpreter, about 20 times slower, and such a
196
+ * module stays there for good, even once the pool has room (a new copy of the same bytes does not).
197
+ * Speed is the only sign. Returns the ms a fresh module's loop takes after it had the chance to be
198
+ * compiled: about 2 ms compiled, 40 ms interpreted.
199
+ */
200
+ export function poolProbe() {
201
+ const code = [1, 2, 0x7f, 0x03, 0x40, 0x20, 2, 0x41, 31, 0x6c, 0x20, 1, 0x6a, 0x41, (Math.random() * 63) | 0, 0x6a, 0x21, 2, 0x20, 1, 0x41, 1, 0x6a, 0x22, 1, 0x20, 0, 0x48, 0x0d, 0, 0x0b, 0x20, 2, 0x0b]
202
+ const bytes = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 6, 1, 0x60, 1, 0x7f, 1, 0x7f, 3, 2, 1, 0, 7, 5, 1, 1, 102, 0, 0, 10, code.length + 2, 1, code.length, ...code])
203
+ const f = new WebAssembly.Instance(new WebAssembly.Module(bytes)).exports.f
204
+ f(POOL_PROBE_N)
205
+ const t = now()
206
+ f(POOL_PROBE_N)
207
+ return now() - t
208
+ }
209
+ const ns = ms => Math.round(ms * 1e6)
210
+
211
+ /** The shared buffers of one machine's JIT door. Created once by the page. */
212
+ export function createJitStore({ logMax = 1 << 30, recMax = 1 << 20, logInitial = 1 << 20, recInitial = 4096, reclaim = true } = {}) {
213
+ if (typeof SharedArrayBuffer.prototype.grow !== 'function') {
214
+ throw new Error('the JIT door needs growable SharedArrayBuffers (SharedArrayBuffer.prototype.grow)')
215
+ }
216
+ const ctl = new SharedArrayBuffer(CTL_BYTES)
217
+ if (!reclaim) Atomics.store(new Int32Array(ctl, 0, HDR_INTS), H_NORECLAIM, 1) // append-only log from the start
218
+ return {
219
+ ctl,
220
+ recs: new SharedArrayBuffer(recInitial * REC_BYTES, { maxByteLength: recMax * REC_BYTES }),
221
+ log: new SharedArrayBuffer(logInitial, { maxByteLength: logMax }),
222
+ known: new SharedArrayBuffer(knBytes(4096), { maxByteLength: knBytes(KN_MAX_ENTRIES) }),
223
+ reload: new SharedArrayBuffer(recInitial * 8, { maxByteLength: recMax * 8 }),
224
+ drops: new SharedArrayBuffer(recInitial * 4, { maxByteLength: recMax * 4 }),
225
+ }
226
+ }
227
+
228
+ /** 2 x 32-bit content hash (FNV-1a / murmur-style mix over 4-byte words). */
229
+ export function hash64(b) {
230
+ let h1 = 0x811c9dc5 | 0
231
+ let h2 = 0x9747b28c | 0
232
+ const n = b.length
233
+ let i = 0
234
+ for (; i + 4 <= n; i += 4) {
235
+ const w = b[i] | (b[i + 1] << 8) | (b[i + 2] << 16) | (b[i + 3] << 24)
236
+ h1 = Math.imul(h1 ^ w, 0x01000193)
237
+ h2 = Math.imul(h2 ^ w, 0x5bd1e995)
238
+ h2 ^= h2 >>> 15
239
+ }
240
+ for (; i < n; i++) {
241
+ h1 = Math.imul(h1 ^ b[i], 0x01000193)
242
+ h2 = Math.imul(h2 ^ b[i], 0x5bd1e995)
243
+ }
244
+ return [h1 | 0, (h2 ^ n) | 0]
245
+ }
246
+
247
+ /**
248
+ * Names of the function exports of a wasm module, in export order, read from the
249
+ * bytes without compiling. null when the bytes are not a well-formed module header /
250
+ * section list. (Slot i of a module is its i-th function export: jit.rs load_slots,
251
+ * runner/src/batch.rs.)
252
+ */
253
+ export function funcExportNames(bytes) {
254
+ if (bytes.length < 8 || bytes[0] !== 0 || bytes[1] !== 0x61 || bytes[2] !== 0x73 || bytes[3] !== 0x6d) return null
255
+ let i = 8
256
+ const leb = () => {
257
+ let r = 0
258
+ let s = 0
259
+ for (;;) {
260
+ if (i >= bytes.length) throw new Error('truncated')
261
+ const x = bytes[i++]
262
+ r += (x & 0x7f) * 2 ** s
263
+ if (!(x & 0x80)) return r
264
+ s += 7
265
+ if (s > 35) throw new Error('bad leb')
266
+ }
267
+ }
268
+ try {
269
+ let names = []
270
+ while (i < bytes.length) {
271
+ const id = bytes[i++]
272
+ const len = leb()
273
+ const end = i + len
274
+ if (end > bytes.length) return null
275
+ if (id === 7) {
276
+ const count = leb()
277
+ for (let k = 0; k < count; k++) {
278
+ const nl = leb()
279
+ const name = new TextDecoder().decode(bytes.slice(i, i + nl))
280
+ i += nl
281
+ const kind = bytes[i++]
282
+ leb()
283
+ if (kind === 0) names.push(name)
284
+ }
285
+ if (i !== end) return null
286
+ }
287
+ i = end
288
+ }
289
+ return names
290
+ } catch {
291
+ return null
292
+ }
293
+ }
294
+
295
+ const moduleFuncNames = module =>
296
+ WebAssembly.Module.exports(module)
297
+ .filter(e => e.kind === 'function')
298
+ .map(e => e.name)
299
+
300
+ /**
301
+ * One Worker's side of the door.
302
+ *
303
+ * @param {object} o
304
+ * @param {{ctl:SharedArrayBuffer, recs:SharedArrayBuffer, log:SharedArrayBuffer}} o.store
305
+ * @param {WebAssembly.Memory} o.memory the machine's one shared memory
306
+ * @param {(msg:string)=>void} [o.log] host diagnostics (stderr)
307
+ * @param {'compile'|'validate'|'none'} [o.compileOnCompile] what jit_compile does with the
308
+ * bytes before publishing them: compile (so V8 can share the code with the vCPU's
309
+ * compile of the same bytes), validate only, or nothing
310
+ * @param {string} [o.brk] test mutation: 'load' makes jit_load return -1, 'compile'
311
+ * jit_compile, 'all' both; 'noclear' makes a flush leave old table entries in place
312
+ * (the P-B2 adversary's mutation; only door-level tests can catch it, since no
313
+ * guest run calls jit_flush)
314
+ * @param {(() => number)|null} [o.codePool] D-515: poolProbe where the engine's code pool can fill without an error (WebKit)
315
+ * @param {number} [o.maxLiveBytes] W34: refuse new modules (JIT_OVER_BUDGET: the guest interprets the block, then releases every module) once this many
316
+ * bytes of modules are live in the current generation (0: no budget). WebKit counts what the engine
317
+ * keeps per module (about 2x its bytes; never freed before a flush) toward its 8 GiB kill limit.
318
+ * @param {{port:{postMessage:Function, onmessage:any}, waitMs?:number}|null} [o.share]
319
+ * compile each module once per process and share the Module through a hub
320
+ * (src/host/jit-hub.js) instead of compiling a copy per Worker. Needs a caller
321
+ * that can wait: the imports then return a Promise when a Module has not arrived
322
+ * yet, so the Worker wraps them in WebAssembly.Suspending (JSPI) and calls the
323
+ * guest through WebAssembly.promising. See README "F1".
324
+ */
325
+ export function createJitDoor({ store, memory, ram = null, log = () => {}, compileOnCompile = 'validate', brk = '', share = null, tableMin = TABLE_MIN, maxLiveBytes = 0, cache = null, usedBase = 0, codePool = null, poolEvery = POOL_CHECK_MS }) {
326
+ const hdr = new Int32Array(store.ctl, 0, HDR_INTS)
327
+ const st = new Int32Array(store.ctl, STATS_OFF, STATS.length * 2)
328
+ const R = new Int32Array(store.recs) // length-tracking views over growable buffers
329
+ const L = new Uint8Array(store.log)
330
+ const KN = store.known ? new Int32Array(store.known) : null
331
+ const RL = store.reload ? new Int32Array(store.reload) : null
332
+ const DL = store.drops ? new Int32Array(store.drops) : null
333
+ const add = (k, v = 1) => statAdd(st, S[k], v)
334
+ let budgetLogged = false
335
+
336
+ // per Worker: compiled modules by log offset; content index for de-duplication.
337
+ // `modules` holds this generation's Modules only: the first sync (or flush) that
338
+ // sees a new generation drops the map, so after a flush nothing in this Worker
339
+ // (map, instances, table entries) references an old Module and the engine can free
340
+ // its code (P-B2 adversarial F1: a Worker used to keep every Module forever).
341
+ // Keys of `modules`, `instances`, the waiters and the hub messages (still named logOff
342
+ // there) are a module's OWNER record index, not its log offset: the log is reused, so an
343
+ // offset can hold a different module later, while an owner index never does.
344
+ let modules = new Map() // owner -> {module, names}
345
+ let modulesGen = 0
346
+ let hubBytes = new Map() // owner -> module bytes the hub sent on request (W31b; this generation)
347
+ let wanted = new Set() // owners whose bytes were asked of the hub (this generation)
348
+ let noBytes = new Set() // ... and that the hub answered 'lost' (it keeps no bytes for them)
349
+ const byHash = new Map() // "hi:lo:len" -> owner (the newest record that allocated those bytes)
350
+ let indexed = 0
351
+ function releaseOldModules(g) {
352
+ if (g === modulesGen) return
353
+ modules = new Map()
354
+ hubBytes = new Map()
355
+ wanted = new Set()
356
+ noBytes = new Set()
357
+ modulesGen = g
358
+ }
359
+
360
+ // An engine resource failure (Firefox: "out of memory" when its per-process wasm code
361
+ // space is used up) is not the module's fault: CompileError/LinkError are.
362
+ const isResource = err => !(err instanceof WebAssembly.CompileError || err instanceof WebAssembly.LinkError)
363
+
364
+ /**
365
+ * new WebAssembly.Module(bytes); on a resource failure, try again for a few seconds. D-512: right
366
+ * after a release, other Workers still hold the released Modules until each one syncs (an idle
367
+ * vCPU within a second, vm machine.c) and Firefox frees their code only then. Throws if every
368
+ * try fails.
369
+ */
370
+ const pause = new Int32Array(new SharedArrayBuffer(4))
371
+ function compileModule(bytes) {
372
+ for (let ms = 50; ; ms *= 2) {
373
+ try {
374
+ return new WebAssembly.Module(bytes)
375
+ } catch (err) {
376
+ if (!isResource(err) || ms > COMPILE_RETRY_MAX_MS) throw err
377
+ if (ms === 50) log(`wmd-web: jit: compile failed (${err && err.message ? err.message : err}); trying again for a few seconds`)
378
+ add('compileRetries')
379
+ Atomics.wait(pause, 0, 0, ms)
380
+ }
381
+ }
382
+ }
383
+
384
+ /** D-511: the budget in force: the page's, or lower, one the engine taught us (outOfCode). */
385
+ function liveBudget() {
386
+ const learned = Atomics.load(hdr, H_BUDGET_KIB) * 1024
387
+ return maxLiveBytes && learned ? Math.min(maxLiveBytes, learned) : maxLiveBytes || learned
388
+ }
389
+ /**
390
+ * D-511: the engine refused code (Firefox: "out of memory" when live and released modules fill its
391
+ * code space). Release every module and keep the live ones well under where this happened from now
392
+ * on: over budget, not -1, so the guest releases instead of compiling each block alone and the
393
+ * engine collects the released ones. If nothing fits after the release, the guest stops loading.
394
+ */
395
+ function outOfCode(err) {
396
+ const live = Atomics.load(hdr, H_LIVE_KIB)
397
+ const kib = Math.max(MIN_LEARNED_KIB, Math.floor(live * 0.6))
398
+ const cur = Atomics.load(hdr, H_BUDGET_KIB)
399
+ if (!cur || kib < cur) Atomics.store(hdr, H_BUDGET_KIB, kib)
400
+ add('outOfCode')
401
+ log(`wmd-web: jit: the engine refused more code at ${live} KiB of live modules (${err && err.message ? err.message : err}); releasing every module, budget ${Math.min(kib, cur || kib)} KiB from now on`)
402
+ return JIT_OVER_BUDGET
403
+ }
404
+
405
+ let poolNext = 0
406
+ let poolFast = Infinity
407
+ /**
408
+ * D-515: before a new module is compiled. While the engine's code pool is full (codePool, checked
409
+ * at most once each poolEvery) the answer is JIT_LATER: the guest interprets the blocks and asks
410
+ * again later, so no module is loaded only to run interpreted for good. A pool that fills with the
411
+ * live modules near the budget lowers the budget to POOL_LEARN of them and answers JIT_OVER_BUDGET
412
+ * (the guest drops cold modules); one that fills well under it holds code dropped lately that is not
413
+ * given back yet, and waiting is the cure. 0: compile.
414
+ */
415
+ function checkPool() {
416
+ if (!codePool) return 0
417
+ const t = now()
418
+ const was = Atomics.load(hdr, H_POOL_FULL)
419
+ if (t < poolNext) return was ? JIT_LATER : 0
420
+ poolNext = t + poolEvery
421
+ add('poolChecks')
422
+ let ms = codePool()
423
+ if (ms > poolFast * POOL_SLOW_X) ms = Math.min(ms, codePool()) // not a pause of this thread
424
+ poolFast = Math.min(poolFast, Math.max(ms, 0.05))
425
+ const full = ms > poolFast * POOL_SLOW_X
426
+ const at = () => new Date().toISOString().slice(11, 19)
427
+ if (!full) {
428
+ if (was && Atomics.compareExchange(hdr, H_POOL_FULL, 1, 0) === 1) {
429
+ add('poolBack')
430
+ log(`wmd-web: jit: the engine compiles code again (${at()})`)
431
+ }
432
+ return 0
433
+ }
434
+ if (was || Atomics.compareExchange(hdr, H_POOL_FULL, 0, 1) !== 0) return JIT_LATER
435
+ add('poolFull')
436
+ const live = Atomics.load(hdr, H_LIVE_KIB)
437
+ const budget = liveBudget()
438
+ const what = `${at()}, ${live} KiB of live modules, new code ${(ms / poolFast).toFixed(0)}x slower`
439
+ if (budget && live * 1024 < budget * POOL_LEARN) {
440
+ log(`wmd-web: jit: the engine's code pool is full (${what}); under the budget: it holds code dropped lately, new blocks wait`)
441
+ return JIT_LATER
442
+ }
443
+ const kib = Math.max(MIN_LEARNED_KIB, Math.floor(live * POOL_LEARN))
444
+ lock()
445
+ try {
446
+ const cur = Atomics.load(hdr, H_BUDGET_KIB)
447
+ if (!cur || kib < cur) Atomics.store(hdr, H_BUDGET_KIB, kib)
448
+ } finally {
449
+ unlock()
450
+ }
451
+ log(`wmd-web: jit: the engine's code pool is full (${what}); dropping cold modules, budget ${kib} KiB from now on`)
452
+ return JIT_OVER_BUDGET
453
+ }
454
+
455
+ // Sharing (o.share): Modules compiled by other Workers arrive here from the hub.
456
+ const waiters = new Map() // logOff -> resolve()
457
+ const forceLocal = new Set() // logOffs whose Module did not arrive in time: compile here
458
+ const lost = new Set() // logOffs the hub could not forward to this Worker
459
+ let deserErrors = 0 // 'messageerror' events on the hub port
460
+ const errorWaitMs = share?.errorWaitMs ?? 1500
461
+ const lostForGood = new Set() // ... being compiled locally: no Module will come
462
+ if (share) {
463
+ const wakeAll = () => {
464
+ for (const w of waiters.values()) w()
465
+ waiters.clear()
466
+ }
467
+ // A Module the engine could not deserialize here arrives as 'messageerror' with no data,
468
+ // so which one is lost is not known (P-B7: Firefox, past its per-process wasm code limit,
469
+ // failed the late Modules of a 190-Module boot; the sync then waited the whole waitMs).
470
+ // From then on a wait for a Module is short, and the wait that is open is cut short.
471
+ // The log may already have reused the lost Module's bytes (the hub counted it delivered when
472
+ // it posted it): the copy is then compiled from bytes the hub keeps (W31b, wantBytes).
473
+ share.port.onmessageerror = () => {
474
+ if (!deserErrors++) {
475
+ log(`wmd-web: jit: a Module from the hub could not be received here; this Worker asks the hub for the Modules it misses from now on`)
476
+ // W57: Firefox lost runs of the Modules the hub pushed (hundreds in a row) but delivered every one
477
+ // it posted in answer to this Worker's own request: stop the pushes, ask for what is missing
478
+ if (RESENDS) {
479
+ try {
480
+ share.port.postMessage({ t: 'pull' })
481
+ } catch {}
482
+ }
483
+ }
484
+ wakeAll()
485
+ }
486
+ share.port.onmessage = e => {
487
+ const m = e.data
488
+ // a flush elsewhere: whatever a sync here waits for may never come; let it re-sync
489
+ if (m.t === 'flush' || m.t === 'drop') return wakeAll()
490
+ if (m.t === 'lost') {
491
+ // the hub could not forward this Module to us: compile a copy instead of waiting;
492
+ // or it keeps no bytes for a Module we asked for (a flushed generation, or evicted)
493
+ if (wanted.has(m.logOff) && m.gen === modulesGen) noBytes.add(m.logOff)
494
+ lost.add(m.logOff)
495
+ const w = waiters.get(m.logOff)
496
+ if (w) {
497
+ waiters.delete(m.logOff)
498
+ w()
499
+ }
500
+ return
501
+ }
502
+ if (m.t === 'bytes') {
503
+ // the answer to wantBytes: a local copy of that Module can now be compiled
504
+ if (m.gen > modulesGen) releaseOldModules(m.gen)
505
+ if (m.gen === modulesGen) hubBytes.set(m.logOff, m.bytes)
506
+ const w = waiters.get(m.logOff)
507
+ if (w) {
508
+ waiters.delete(m.logOff)
509
+ w()
510
+ }
511
+ return
512
+ }
513
+ if (m.t !== 'mod') return
514
+ if (m.gen < Atomics.load(hdr, H_GEN)) return wakeAll() // a flushed generation's module
515
+ if (m.gen > modulesGen) releaseOldModules(m.gen)
516
+ if (m.gen === modulesGen && !modules.has(m.logOff)) {
517
+ try {
518
+ modules.set(m.logOff, { module: m.module, names: moduleFuncNames(m.module), shared: true })
519
+ } catch (err) {
520
+ log(`wmd-web: jit: a shared module could not be received (${err && err.message ? err.message : err})`)
521
+ }
522
+ }
523
+ const w = waiters.get(m.logOff)
524
+ if (w) {
525
+ waiters.delete(m.logOff)
526
+ w()
527
+ }
528
+ }
529
+ // W57: the hub posts nothing to this port before this: Firefox failed (as 'messageerror') every
530
+ // Module the hub posted while this Worker was still starting, the first 7 to 83 of a boot
531
+ try {
532
+ share.port.postMessage({ t: 'hello', pull: share.pull ?? true }) // D-512: a Module only when this door asks (askMissing)
533
+ } catch {}
534
+ }
535
+
536
+ const breakLoad = brk === 'load' || brk === 'all'
537
+ const breakCompile = brk === 'compile' || brk === 'all'
538
+ const breakClear = brk === 'noclear'
539
+
540
+ // per thread (reset by attach)
541
+ let table = null
542
+ let imports = null
543
+ let gen = -1
544
+ let applied = 0
545
+ let instances = new Map() // owner -> Instance (this generation)
546
+
547
+ function lock() {
548
+ for (let spins = 0; Atomics.compareExchange(hdr, H_LOCK, 0, 1) !== 0; spins++) {
549
+ if (spins > 100) {
550
+ try {
551
+ Atomics.wait(hdr, H_LOCK, 1, 1)
552
+ } catch {} // the main thread of a test runner may not wait
553
+ }
554
+ }
555
+ }
556
+ function unlock() {
557
+ Atomics.store(hdr, H_LOCK, 0)
558
+ Atomics.notify(hdr, H_LOCK, 1)
559
+ }
560
+
561
+ function rec(r) {
562
+ const b = r * REC_INTS
563
+ return {
564
+ logOff: Atomics.load(R, b + R_LOG_OFF),
565
+ len: Atomics.load(R, b + R_LEN),
566
+ slotBase: Atomics.load(R, b + R_SLOT_BASE),
567
+ nfuncs: Atomics.load(R, b + R_NFUNCS),
568
+ hl: Atomics.load(R, b + R_HASH_LO),
569
+ hh: Atomics.load(R, b + R_HASH_HI),
570
+ owner: Atomics.load(R, b + R_OWNER),
571
+ }
572
+ }
573
+
574
+ const hkey = (hl, hh, len) => `${hh}:${hl}:${len}`
575
+
576
+ const ownerGen = new Map()
577
+ function indexRecords() {
578
+ const n = Atomics.load(hdr, H_NMOD)
579
+ for (; indexed < n; indexed++) {
580
+ const x = rec(indexed)
581
+ // only records that own their bytes; the newest wins, so after the older copy's
582
+ // bytes were released a live copy of the same module is still found
583
+ if (x.owner === indexed) byHash.set(hkey(x.hl, x.hh, x.len), indexed)
584
+ ownerGen.set(x.owner, Atomics.load(R, indexed * REC_INTS + R_GEN)) // W34: the newest generation that uses the owner's bytes
585
+ }
586
+ }
587
+
588
+ function sameBytes(off, bytes) {
589
+ for (let i = 0; i < bytes.length; i++) if (L[off + i] !== bytes[i]) return false
590
+ return true
591
+ }
592
+
593
+ function ensure(sab, bytes) {
594
+ if (sab.byteLength >= bytes) return true
595
+ if (bytes > sab.maxByteLength) return false
596
+ sab.grow(Math.min(sab.maxByteLength, Math.max(bytes, sab.byteLength * 2)))
597
+ return true
598
+ }
599
+
600
+ // ---- Reclaiming the log (W31) ----
601
+ //
602
+ // Nothing in a machine run calls jit_flush (the only generation change), so the log only
603
+ // grew and a long build loop filled it ("module log full": every new block then stayed in
604
+ // vm.wasm's interpreter, about 10x slower).
605
+ //
606
+ // Only with sharing (o.share: a hub holds every Module of the generation). Without it
607
+ // every thread compiles from the log whenever it syncs, so any byte may be read at any
608
+ // time and nothing is ever released: append() refuses at logMax, as it always did.
609
+ //
610
+ // Delivery. The hub (jit-hub-core.js) sets R_DELIVERED of a record with Atomics.store
611
+ // only after it posted that record's Module to every port it had, and it keeps the
612
+ // Module, and a copy of its bytes, for ports that connect later. So a door never needs
613
+ // the log bytes: it gets the Module from the hub; when the hub could not post it
614
+ // ('lost'), from a local compile of the log, which the hub does not mark delivered; and
615
+ // when a posted Module could not be received ('messageerror', Firefox past its wasm code
616
+ // limit) or a replay comes after the bytes were reused, from a local compile of the
617
+ // bytes the hub sends on request (wantBytes, W31b). Door progress is not
618
+ // tracked at all, so an idle or finished thread cannot hold the log back, and a thread
619
+ // that attaches late gets every slot from the hub's replay.
620
+ //
621
+ // A record's bytes may be released when it owns them (R_OWNER is itself: a record
622
+ // de-duplicated against an older one owns nothing), and it is delivered or belongs to
623
+ // an older generation (a flush made the table forget it; every door re-syncs from
624
+ // H_MOD_START). Release = R_FREED := 1, set under H_LOCK just before the space can be
625
+ // overwritten. H_LOG_TAIL is the lowest record that may own live bytes.
626
+ //
627
+ // Placement. The live allocations are the owner records in [tail, nmod) that are not
628
+ // freed. A new module goes into the lowest gap among them that fits (so freed space at
629
+ // the front is reused first: a ring in the steady state), then after the last one,
630
+ // growing the buffer only if that does not fit; each module stays contiguous.
631
+ // H_LOG_USED is the end of the last placement and H_LOG_LIM the start of the next live
632
+ // allocation (0: none), so the common append is a bump. A release pass runs only when
633
+ // the free run at the head has no room, so a module logged twice stays de-duplicated
634
+ // for as long as nothing needed the space.
635
+ //
636
+ // Ordering (readers take no lock). The only readers of log bytes are a local-compile
637
+ // fallback (install) and the unlocked de-duplication compare. append() overwrites a
638
+ // range only after it stored R_FREED for every owner overlapping it (seq-cst, under the
639
+ // lock), and a record is freed only if the hub had delivered it. A reader reads the
640
+ // bytes first and then loads R_FREED: if that load sees 0 the store came later in the
641
+ // seq-cst order, so the overwrite (after the store) came after the reads, and they saw
642
+ // the module's own bytes; if it sees 1 the bytes are discarded unread (a fallback then
643
+ // waits for the hub instead). A fallback also refuses to start once R_DELIVERED is set.
644
+ // De-duplication takes a candidate without the lock and re-checks R_FREED under it:
645
+ // while it is 0 under the lock nothing has been overwritten, so the unlocked compare
646
+ // read intact bytes. The per-door Module maps are keyed by owner record index, never by
647
+ // offset, so a reused offset is never mistaken for the old module.
648
+ const align8 = n => (n + 7) & ~7
649
+ const isFreed = r => Atomics.load(R, r * REC_INTS + R_FREED) !== 0
650
+ // gone: released, or of a flushed generation (reclaimable at any time, undelivered or not).
651
+ // A de-duplication hit must be of the current generation: a new record that used an old
652
+ // generation's allocation would depend on bytes the flush made reclaimable.
653
+ const isDropped = r => Atomics.load(R, r * REC_INTS + R_DROPPED) !== 0
654
+ const isGone = r => isDropped(r) || (share && (isFreed(r) || Atomics.load(R, r * REC_INTS + R_GEN) !== Atomics.load(hdr, H_GEN)))
655
+
656
+ /** Release the bytes of delivered records (H_LOCK held; sharing only). */
657
+ function reclaim(nmod) {
658
+ const g = Atomics.load(hdr, H_GEN)
659
+ let freed = 0
660
+ let first = -1
661
+ for (let r = Atomics.load(hdr, H_LOG_TAIL); r < nmod; r++) {
662
+ const b = r * REC_INTS
663
+ if (Atomics.load(R, b + R_OWNER) !== r || Atomics.load(R, b + R_FREED)) continue
664
+ if (Atomics.load(R, b + R_DELIVERED) || Atomics.load(R, b + R_GEN) < g || Atomics.load(R, b + R_DROPPED)) {
665
+ Atomics.store(R, b + R_FREED, 1)
666
+ freed += align8(Atomics.load(R, b + R_LEN))
667
+ } else if (first < 0) first = r
668
+ }
669
+ Atomics.store(hdr, H_LOG_TAIL, first < 0 ? nmod : first)
670
+ if (freed) add('logReclaimedBytes', freed)
671
+ }
672
+
673
+ /** Room for `len` bytes in the log: its offset, or -1 when full (H_LOCK held). */
674
+ function allocLog(len) {
675
+ const n = align8(len)
676
+ const h = Atomics.load(hdr, H_LOG_USED)
677
+ if (!share || Atomics.load(hdr, H_NORECLAIM)) {
678
+ // today's behaviour: append only, grow, refuse at logMax
679
+ if (!ensure(store.log, h + n)) return -1
680
+ Atomics.store(hdr, H_LOG_USED, h + n)
681
+ return h
682
+ }
683
+ const take = (off, lim) => {
684
+ if (off < h) add('logWraps')
685
+ Atomics.store(hdr, H_LOG_USED, off + n)
686
+ Atomics.store(hdr, H_LOG_LIM, lim)
687
+ Atomics.store(hdr, H_FAIL_D, 0)
688
+ return off
689
+ }
690
+ const lim = Atomics.load(hdr, H_LOG_LIM)
691
+ if (h + n <= (lim || store.log.byteLength)) return take(h, lim)
692
+ const nmod = Atomics.load(hdr, H_NMOD)
693
+ // D-513: grow before reusing anything while under the budget (the buffer doubles, so up to twice
694
+ // it): jit_known loads a module of the generations before a release again from these bytes (knHit)
695
+ const keep = Math.min(store.log.maxByteLength, liveBudget())
696
+ if (keep && store.log.byteLength < keep) {
697
+ let top = h
698
+ for (let r = Atomics.load(hdr, H_LOG_TAIL); r < nmod; r++) {
699
+ const b = r * REC_INTS
700
+ if (Atomics.load(R, b + R_OWNER) !== r || Atomics.load(R, b + R_FREED)) continue
701
+ top = Math.max(top, Atomics.load(R, b + R_LOG_OFF) + align8(Atomics.load(R, b + R_LEN)))
702
+ }
703
+ if (top + n <= keep && ensure(store.log, top + n)) return take(top, 0)
704
+ }
705
+ const d = Atomics.load(hdr, H_DELIV) + 1
706
+ const g = Atomics.load(hdr, H_GEN)
707
+ // nothing was delivered or flushed since the last failure: a same or bigger module still does not fit
708
+ if (Atomics.load(hdr, H_FAIL_D) === d && Atomics.load(hdr, H_FAIL_G) === g && n >= Atomics.load(hdr, H_FAIL_N)) return -1
709
+ reclaim(nmod)
710
+ const iv = []
711
+ for (let r = Atomics.load(hdr, H_LOG_TAIL); r < nmod; r++) {
712
+ const b = r * REC_INTS
713
+ if (Atomics.load(R, b + R_OWNER) !== r || Atomics.load(R, b + R_FREED)) continue
714
+ const o = Atomics.load(R, b + R_LOG_OFF)
715
+ iv.push([o, o + align8(Atomics.load(R, b + R_LEN))])
716
+ }
717
+ iv.sort((a, b) => a[0] - b[0])
718
+ let prev = 0
719
+ for (const [o, e] of iv) {
720
+ if (o - prev >= n) return take(prev, o)
721
+ if (e > prev) prev = e
722
+ }
723
+ if (prev + n <= store.log.byteLength || ensure(store.log, prev + n)) return take(prev, 0)
724
+ const same = Atomics.load(hdr, H_FAIL_D) === d && Atomics.load(hdr, H_FAIL_G) === g
725
+ Atomics.store(hdr, H_FAIL_N, same ? Math.min(n, Atomics.load(hdr, H_FAIL_N)) : n)
726
+ Atomics.store(hdr, H_FAIL_G, g)
727
+ Atomics.store(hdr, H_FAIL_D, d)
728
+ return -1
729
+ }
730
+
731
+ /**
732
+ * A private copy of guest bytes [ptr, ptr+len). Both are wasm i32s, which JS receives
733
+ * signed: an address at or above 2 GiB arrives negative (W23: "Start offset
734
+ * -2147302880 is outside the bounds of the buffer" once vm.wasm's heap passed 2 GiB),
735
+ * so they are read as unsigned, as wasi.js and chan.js do and the runner's u32 params
736
+ * (jit.rs jit_load). null when the range is not inside the memory.
737
+ */
738
+ function readGuest(ptr, len) {
739
+ ptr >>>= 0
740
+ len >>>= 0
741
+ const buf = memory.buffer
742
+ if (ptr + len > buf.byteLength) return null
743
+ const out = new Uint8Array(len)
744
+ out.set(new Uint8Array(buf, ptr, len))
745
+ return out
746
+ }
747
+ const outside = (kind, ptr, len) => {
748
+ add('failures')
749
+ log(`wmd-web: jit_${kind}: module bytes at ${ptr >>> 0}+${len >>> 0} are outside the guest memory (${memory.buffer.byteLength} bytes)`)
750
+ return -1
751
+ }
752
+
753
+ // W77: this Worker's last appended guest modules, for jit_note (its table index -> module)
754
+ const RECENT = 64
755
+ const recent = []
756
+ let lastRec = -1 // the record the last successful append published
757
+ function recentFor(tidx) {
758
+ for (let i = recent.length - 1; i >= 0; i--) {
759
+ const x = recent[i]
760
+ if (tidx >= x.base && tidx < x.base + x.n) return x
761
+ }
762
+ return null
763
+ }
764
+ /** jit_known's answer: the span (rlo, rlen, span hash) at ptr+32, as jit.rs jit_known writes it. */
765
+ function writeSpan(ptr, e) {
766
+ const at = (ptr >>> 0) + KNOWN_KEY_BYTES
767
+ const buf = memory.buffer
768
+ if (at + 16 > buf.byteLength) return false
769
+ const dv = new DataView(buf, at, 16)
770
+ dv.setUint32(0, e[2] >>> 0, true)
771
+ dv.setUint32(4, e[3] >>> 0, true)
772
+ dv.setUint32(8, e[4] >>> 0, true)
773
+ dv.setUint32(12, e[5] >>> 0, true)
774
+ return true
775
+ }
776
+ /** After a hit: this thread's table must hold the function before the guest calls it. */
777
+ function hitDone(ptr, e, tidx, hit = 'cacheHits') {
778
+ if (!writeSpan(ptr, e)) {
779
+ add('cacheFails')
780
+ return -1
781
+ }
782
+ const failed = err => {
783
+ add('cacheFails')
784
+ log(`wmd-web: jit_known: ${err && err.message ? err.message : err}`)
785
+ return -1
786
+ }
787
+ try {
788
+ if (share) {
789
+ const p = syncShared()
790
+ if (p) return p.then(() => (add(hit), tidx), failed)
791
+ } else sync()
792
+ } catch (err) {
793
+ return failed(err)
794
+ }
795
+ add(hit)
796
+ return tidx
797
+ }
798
+
799
+ const knSlot = K => ((Math.imul(K[0] ^ K[2], 0x9e3779b1) ^ K[1] ^ K[3]) >>> 0) & (KN_BUCKETS - 1)
800
+ /** D-513: the entry of this session's table for key K, or -1. */
801
+ function knFind(K) {
802
+ for (let j = KN[KN_HDR + knSlot(K)] - 1; j >= 0; j = KN[KN_E + j * KN_INTS + 14] - 1) {
803
+ const o = KN_E + j * KN_INTS
804
+ let i = 0
805
+ while (i < 8 && KN[o + i] === K[i]) i++
806
+ if (i === 8) return j
807
+ }
808
+ return -1
809
+ }
810
+ /** D-513: key K (8 ints of `src` from `at`) names table index tidx with span v[0..3]; the newest note wins. */
811
+ function knNote(src, at, tidx, v) {
812
+ const r = recOfIndex(tidx)
813
+ if (r < 0) return
814
+ const K = src.subarray(at, at + 8)
815
+ let j = knFind(K)
816
+ if (j < 0) {
817
+ j = KN[0]
818
+ if (j >= KN_MAX_ENTRIES || !ensure(store.known, knBytes(j + 1))) return
819
+ const o = KN_E + j * KN_INTS
820
+ KN.set(K, o)
821
+ const h = KN_HDR + knSlot(K)
822
+ KN[o + 14] = KN[h]
823
+ KN[h] = j + 1
824
+ KN[0] = j + 1
825
+ add('knownNotes')
826
+ }
827
+ const o = KN_E + j * KN_INTS
828
+ KN[o + 8] = R[r * REC_INTS + R_GEN]
829
+ KN[o + 9] = tidx
830
+ KN.set(v, o + 10)
831
+ KN[o + 15] = r
832
+ }
833
+ /** The record whose slots hold table index tidx, or -1 (records take slots in order). */
834
+ function recOfIndex(tidx) {
835
+ const i = tidx - tableMin
836
+ let lo = 0
837
+ let hi = Atomics.load(hdr, H_NMOD) - 1
838
+ if (i < 0 || hi < 0) return -1
839
+ while (lo < hi) {
840
+ const m = (lo + hi + 1) >> 1
841
+ if (R[m * REC_INTS + R_SLOT_BASE] <= i) lo = m
842
+ else hi = m - 1
843
+ }
844
+ for (let r = lo; r >= 0; r--) {
845
+ const b = r * REC_INTS
846
+ const n = R[b + R_NFUNCS]
847
+ if (R[b + R_SLOT_BASE] > i) continue
848
+ return i < R[b + R_SLOT_BASE] + n ? r : -1
849
+ }
850
+ return -1
851
+ }
852
+ /** D-513: a hit on entry o: its function in this generation, the module loaded again from the log when it is older. */
853
+ function knHit(ptr, o) {
854
+ const g = Atomics.load(hdr, H_GEN)
855
+ const e = KN.slice(o + 8, o + 14)
856
+ const r = KN[o + 15]
857
+ // D-514: an eviction dropped the module because few of its functions ran; loading it again
858
+ // would bring the rest back, so the block is compiled again (at once: it was busy), with other busy ones
859
+ const dropped = () => {
860
+ add('knownDropped')
861
+ return KNOWN_DROPPED
862
+ }
863
+ if (KN[o + 8] === g) return isDropped(r) ? dropped() : hitDone(ptr, e, KN[o + 9], 'knownHits')
864
+ const owner = R[r * REC_INTS + R_OWNER]
865
+ const func = KN[o + 9] - tableMin - R[r * REC_INTS + R_SLOT_BASE]
866
+ const nr0 = RL.length > owner * 2 + 1 && Atomics.load(RL, owner * 2 + 1) === g ? Atomics.load(RL, owner * 2) - 1 : -1
867
+ if (nr0 >= 0 && isDropped(nr0)) return dropped()
868
+ let nr = nr0
869
+ if (nr < 0) {
870
+ const x = rec(owner)
871
+ const bytes = isFreed(owner) ? null : L.slice(x.logOff, x.logOff + x.len)
872
+ if (!bytes || isFreed(owner)) {
873
+ add('knownGone')
874
+ return -1
875
+ }
876
+ if (append(bytes, 3, 'compile') < 0 || !ensure(store.reload, (owner + 1) * 8)) return -1
877
+ nr = lastRec
878
+ Atomics.store(RL, owner * 2, nr + 1)
879
+ Atomics.store(RL, owner * 2 + 1, R[nr * REC_INTS + R_GEN])
880
+ add('knownReloads')
881
+ }
882
+ const b = nr * REC_INTS
883
+ if (func < 0 || func >= R[b + R_NFUNCS]) return -1
884
+ const tidx = tableMin + R[b + R_SLOT_BASE] + func
885
+ KN[o + 8] = e[0] = R[b + R_GEN]
886
+ KN[o + 9] = e[1] = tidx
887
+ KN[o + 15] = nr
888
+ return hitDone(ptr, e, tidx, 'knownHits')
889
+ }
890
+ /**
891
+ * W77: wmd.jit_known(ptr) over the page's cache (jit-cache.js): the table index of the
892
+ * function an earlier visit compiled for the block key at ptr, or -1. A miss answers at
893
+ * once; a hit on a module not loaded in this generation waits for its bytes (sharing only:
894
+ * the import is a JSPI Suspending then).
895
+ */
896
+ function known(ptr) {
897
+ const kb = readGuest(ptr, KNOWN_KEY_BYTES)
898
+ if (!kb) return -1
899
+ const K = new Int32Array(kb.buffer)
900
+ if (KN) {
901
+ const s = knFind(K)
902
+ if (s >= 0) return knHit(ptr, KN_E + s * KN_INTS)
903
+ }
904
+ if (!cache) return -1
905
+ const lk = cache.lookup
906
+ const j = lk.find(K)
907
+ if (j < 0) {
908
+ add('cacheMisses')
909
+ return -1
910
+ }
911
+ const e = lk.entry(j).slice()
912
+ const mod = e[0]
913
+ const func = e[1]
914
+ const g = Atomics.load(hdr, H_GEN)
915
+ const [lr, lg] = lk.loaded(mod)
916
+ if (lr && lg === g) {
917
+ const b = (lr - 1) * REC_INTS
918
+ if (Atomics.load(R, b + R_GEN) === g && func < R[b + R_NFUNCS]) return hitDone(ptr, e, tableMin + R[b + R_SLOT_BASE] + func)
919
+ }
920
+ if (!share) {
921
+ add('cacheFails') // no JSPI: this thread cannot wait for the bytes
922
+ return -1
923
+ }
924
+ const [hl, hh, len] = lk.mod(mod)
925
+ const t = now()
926
+ return cache.request(hl, hh, len).then(
927
+ bytes => {
928
+ add('cacheLoads')
929
+ add('cacheLoadNs', ns(now() - t))
930
+ if (!bytes || bytes.length !== len >>> 0) {
931
+ add('cacheFails')
932
+ return -1
933
+ }
934
+ const [a, b] = hash64(bytes)
935
+ if (a !== hl || b !== hh) {
936
+ add('cacheFails')
937
+ return -1
938
+ }
939
+ const base = append(bytes, 3, 'compile')
940
+ if (base < 0) {
941
+ add('cacheFails')
942
+ return -1
943
+ }
944
+ const r = lastRec
945
+ if (func >= R[r * REC_INTS + R_NFUNCS]) {
946
+ add('cacheFails')
947
+ return -1
948
+ }
949
+ lk.setLoaded(mod, r, R[r * REC_INTS + R_GEN])
950
+ return hitDone(ptr, e, base + func)
951
+ },
952
+ err => {
953
+ add('cacheFails')
954
+ log(`wmd-web: jit_known: cache read: ${err && err.message ? err.message : err}`)
955
+ return -1
956
+ },
957
+ )
958
+ }
959
+ /** W77: wmd.jit_note(ptr, n): the keys of blocks just compiled, to the page's cache with their modules. */
960
+ function note(ptr, n) {
961
+ n >>>= 0
962
+ if (!n) return
963
+ const b = readGuest(ptr, n * KNOWN_NOTE_BYTES)
964
+ if (!b) return
965
+ if (KN) {
966
+ const I = new Int32Array(b.buffer)
967
+ for (let i = 0; i < n; i++) {
968
+ const w = (i * KNOWN_NOTE_BYTES) >> 2
969
+ knNote(I, w, I[w + 12], I.subarray(w + 8, w + 12))
970
+ }
971
+ }
972
+ if (!cache) return
973
+ const dv = new DataView(b.buffer)
974
+ const mods = []
975
+ const modIx = new Map()
976
+ const ents = []
977
+ for (let i = 0; i < n; i++) {
978
+ const o = i * KNOWN_NOTE_BYTES
979
+ const tidx = dv.getInt32(o + 48, true)
980
+ const x = recentFor(tidx)
981
+ if (!x) continue
982
+ let mi = modIx.get(x)
983
+ if (mi === undefined) {
984
+ mi = mods.length
985
+ mods.push({ hl: x.hl, hh: x.hh, len: x.len, bytes: x.bytes || undefined })
986
+ x.bytes = null // sent once from this Worker
987
+ modIx.set(x, mi)
988
+ }
989
+ ents.push({ key: b.slice(o, o + KNOWN_KEY_BYTES), mod: mi, func: tidx - x.base, rlo: dv.getUint32(o + 32, true), rlen: dv.getUint32(o + 36, true), spanLo: dv.getInt32(o + 40, true), spanHi: dv.getInt32(o + 44, true) })
990
+ }
991
+ if (!ents.length) return
992
+ add('cacheNotes', ents.length)
993
+ cache.note({ mods, ents })
994
+ }
995
+
996
+ /** Register a module's bytes; returns the table index of its first function, or -1. */
997
+ function append(bytes, kind, how) {
998
+ for (;;) {
999
+ const r = appendOnce(bytes, kind, how)
1000
+ if (r !== AGAIN) return r
1001
+ }
1002
+ }
1003
+ function appendOnce(bytes, kind, how) {
1004
+ add('bytes', bytes.length)
1005
+ const th = now()
1006
+ const [hl, hh] = hash64(bytes)
1007
+ add('hashNs', ns(now() - th))
1008
+ indexRecords()
1009
+ // a de-duplication candidate: the record owning identical bytes that are still in the log
1010
+ let owner = byHash.get(hkey(hl, hh, bytes.length)) ?? -1
1011
+ if (owner >= 0 && (isGone(owner) || !sameBytes(rec(owner).logOff, bytes))) owner = -1
1012
+ let m = owner >= 0 ? modules.get(owner) : undefined
1013
+ let compileUs = 0
1014
+ let compiledHere = false
1015
+ if (m) add('moduleCacheHits')
1016
+ if (!m && Atomics.load(hdr, H_FULL)) {
1017
+ // The function table is full for good. Over budget, not -1: -1 makes the guest compile every
1018
+ // block of the batch alone, each refused again; a release that frees nothing stops its loads.
1019
+ add('refused')
1020
+ add('failures')
1021
+ return JIT_OVER_BUDGET
1022
+ }
1023
+ // W34: identical bytes already live in this generation add nothing an engine keeps (every Worker
1024
+ // instantiates an owner once per generation); bytes owned by an older generation are counted again
1025
+ const liveHere = owner >= 0 && ownerGen.get(owner) === Atomics.load(hdr, H_GEN)
1026
+ // D-512: a compile thread (jit_compile: it runs no blocks) keeps no Module. One waiting for work
1027
+ // would hold its last generation's for good, and Firefox frees code only once no Worker holds it.
1028
+ const keep = kind !== 2 || !share
1029
+ if (!m && liveHere && !keep) {
1030
+ const names = funcExportNames(bytes)
1031
+ if (names) {
1032
+ m = { module: null, names }
1033
+ add('moduleCacheHits')
1034
+ }
1035
+ }
1036
+ if (!m && !liveHere && how === 'compile') {
1037
+ const p = checkPool()
1038
+ if (p) {
1039
+ add(p === JIT_LATER ? 'poolLater' : 'refused')
1040
+ return p
1041
+ }
1042
+ }
1043
+ const budget = liveBudget()
1044
+ if (!m && !liveHere && budget && Atomics.load(hdr, H_LIVE_KIB) * 1024 + bytes.length > budget) {
1045
+ // W34: the engine keeps every live module; stay under the budget so the page does not pass WebKit's kill limit
1046
+ if (!budgetLogged) {
1047
+ budgetLogged = true
1048
+ log(`wmd-web: jit: module budget reached (${Atomics.load(hdr, H_LIVE_KIB)} KiB live of ${budget >> 10}); the guest drops its compiled blocks and compiles again`)
1049
+ }
1050
+ add('refused')
1051
+ add('failures')
1052
+ return JIT_OVER_BUDGET
1053
+ }
1054
+ if (!m) {
1055
+ try {
1056
+ if (how === 'compile') {
1057
+ const t = now()
1058
+ const module = compileModule(bytes)
1059
+ compiledHere = true
1060
+ const dt = now() - t
1061
+ compileUs = Math.max(1, Math.round(dt * 1000))
1062
+ add('appendCompiles')
1063
+ add('appendCompileNs', ns(dt))
1064
+ m = { module, names: moduleFuncNames(module) }
1065
+ } else {
1066
+ if (how === 'validate') {
1067
+ const t = now()
1068
+ const ok = WebAssembly.validate(bytes)
1069
+ add('validateNs', ns(now() - t))
1070
+ if (!ok) throw new Error('module does not validate')
1071
+ }
1072
+ const names = funcExportNames(bytes)
1073
+ if (!names) throw new Error('malformed module')
1074
+ m = { module: null, names }
1075
+ }
1076
+ } catch (err) {
1077
+ add('failures')
1078
+ if (how === 'compile' && isResource(err)) return outOfCode(err)
1079
+ log(`wmd-web: jit_${kind === 1 ? 'load' : 'compile'}: compile: ${err && err.message ? err.message : err}`)
1080
+ return -1
1081
+ }
1082
+ }
1083
+ let base
1084
+ let recGen = 0
1085
+ let key = owner
1086
+ lock()
1087
+ try {
1088
+ const r = Atomics.load(hdr, H_NMOD)
1089
+ // released since the unlocked check: the bytes may be reused, log the module again
1090
+ if (owner >= 0 && isGone(owner)) {
1091
+ if (!m.module) return AGAIN // D-512: no Module of its own to post: compile it after all
1092
+ owner = -1
1093
+ }
1094
+ let logOff
1095
+ if (owner < 0) {
1096
+ const off = allocLog(bytes.length)
1097
+ if (off < 0) {
1098
+ add('failures')
1099
+ log(`wmd-web: jit: module log full (${Atomics.load(hdr, H_LOG_USED)} bytes${share ? `; ${r - Atomics.load(hdr, H_LOG_TAIL)} records from #${Atomics.load(hdr, H_LOG_TAIL)} not delivered by the hub or flushed` : ''})`)
1100
+ return -1
1101
+ }
1102
+ L.set(bytes, off)
1103
+ logOff = off
1104
+ key = r
1105
+ } else {
1106
+ logOff = rec(owner).logOff
1107
+ key = owner
1108
+ add('logDedup')
1109
+ }
1110
+ if (!ensure(store.recs, (r + 1) * REC_BYTES)) {
1111
+ add('failures')
1112
+ log(`wmd-web: jit: module record list full (${r} records; records are not reclaimed, so this is the next limit after the module log)`)
1113
+ return -1
1114
+ }
1115
+ const slotBase = Atomics.load(hdr, H_NSLOTS)
1116
+ base = tableMin + slotBase
1117
+ if (base + m.names.length > TABLE_MAX_ENTRIES) {
1118
+ // Slots are never rewound (W34) and table.grow past the engines' limit throws
1119
+ // in every Worker's sync: refuse here instead, once and for good.
1120
+ add('failures')
1121
+ Atomics.store(hdr, H_FULL, 1)
1122
+ log(`wmd-web: jit: function table full (${TABLE_MAX_ENTRIES} entries); new blocks stay in the guest's interpreter from now on`)
1123
+ return -1
1124
+ }
1125
+ const b = r * REC_INTS
1126
+ R[b + R_LOG_OFF] = logOff
1127
+ R[b + R_LEN] = bytes.length
1128
+ if (owner < 0 || ownerGen.get(owner) !== Atomics.load(hdr, H_GEN)) {
1129
+ Atomics.add(hdr, H_LIVE_KIB, Math.ceil(bytes.length / 1024))
1130
+ const kib = Atomics.add(hdr, H_MARK_KIB, Math.ceil(bytes.length / 1024)) + Math.ceil(bytes.length / 1024)
1131
+ if (budget && kib * 1024 >= budget / MARK_PARTS) markUse()
1132
+ }
1133
+ R[b + R_SLOT_BASE] = slotBase
1134
+ R[b + R_NFUNCS] = m.names.length
1135
+ R[b + R_HASH_LO] = hl
1136
+ R[b + R_HASH_HI] = hh
1137
+ R[b + R_APPEND_US] = compileUs
1138
+ R[b + R_SYNC_US] = 0
1139
+ R[b + R_INST_US] = 0
1140
+ R[b + R_KIND] = kind
1141
+ recGen = Atomics.load(hdr, H_GEN)
1142
+ R[b + R_GEN] = recGen
1143
+ R[b + R_OWNER] = key
1144
+ R[b + R_USED] = Atomics.load(hdr, H_MARK) + 1 // D-514: new: not cold before the next mark has looked
1145
+ R[b + R_DROPPED] = 0
1146
+ R[b + R_USEDN] = R[b + R_NFUNCS]
1147
+ // A new owner record whose Module came from this Worker's cache (the de-duplication owner
1148
+ // was released between the unlocked check and the lock) must be posted too, or the other
1149
+ // vCPUs wait waitMs for it before compiling from the log.
1150
+ if (share && m.module && (compiledHere || key === r)) {
1151
+ // Post before publishing: once the record is visible other vCPUs wait for this
1152
+ // Module, so a post that fails (Firefox past its code limit can fail to
1153
+ // serialize one) must leave the module unpublished; -1 is what the guest handles.
1154
+ try {
1155
+ share.port.postMessage({ t: 'mod', logOff: key, gen: recGen, module: m.module, bytes }) // the hub keeps the bytes for copies (W31b)
1156
+ } catch (err) {
1157
+ add('failures')
1158
+ return outOfCode(err)
1159
+ }
1160
+ add('shareSent')
1161
+ }
1162
+ Atomics.store(hdr, H_NSLOTS, slotBase + m.names.length)
1163
+ Atomics.store(hdr, H_NMOD, r + 1) // publish (seq-cst store after the plain writes)
1164
+ lastRec = r
1165
+ } finally {
1166
+ unlock()
1167
+ }
1168
+ if (kind !== 3) {
1169
+ add('translated', m.names.length)
1170
+ // W77: the notes for these functions follow from this thread (cache.c cp_publish)
1171
+ if (cache) {
1172
+ recent.push({ base, n: m.names.length, hl, hh, len: bytes.length, bytes })
1173
+ if (recent.length > RECENT) recent.shift()
1174
+ }
1175
+ }
1176
+ // Keep it for this generation. The record may be the first this Worker sees of a
1177
+ // new generation (a flush ran since its last sync): move to it now, or the sync
1178
+ // that follows would drop the Module and wait for it from the hub, which does not
1179
+ // send a Module back to the Worker that posted it.
1180
+ if (recGen > modulesGen) releaseOldModules(recGen)
1181
+ if (keep && m.module && recGen === modulesGen && !modules.has(key)) modules.set(key, m)
1182
+ return base
1183
+ }
1184
+
1185
+ const AGAIN = Symbol('again')
1186
+ class Missing {
1187
+ constructor(logOff) {
1188
+ this.logOff = logOff
1189
+ }
1190
+ }
1191
+
1192
+ /** Compile this Worker's own copy of record r's module from `bytes` and keep it. */
1193
+ function compileLocal(r, bytes) {
1194
+ const owner = Atomics.load(R, r * REC_INTS + R_OWNER)
1195
+ const t = now()
1196
+ const module = compileModule(bytes)
1197
+ const dt = now() - t
1198
+ add('syncCompiles')
1199
+ add('syncCompileNs', ns(dt))
1200
+ Atomics.store(R, r * REC_INTS + R_SYNC_US, Math.max(1, Math.round(dt * 1000)))
1201
+ const m = { module, names: moduleFuncNames(module) }
1202
+ modules.set(owner, m)
1203
+ forceLocal.delete(owner)
1204
+ return m
1205
+ }
1206
+
1207
+ /** Ask the hub for the bytes of `owner` (once per generation); they arrive as {t:'bytes'}. */
1208
+ function wantBytes(owner) {
1209
+ if (!share || wanted.has(owner)) return
1210
+ wanted.add(owner)
1211
+ add('shareBytesWanted')
1212
+ try {
1213
+ share.port.postMessage({ t: 'want', logOff: owner, gen: modulesGen })
1214
+ } catch {}
1215
+ }
1216
+
1217
+ function install(r, mayWait) {
1218
+ if (isDropped(r)) return // D-514
1219
+ const x = rec(r)
1220
+ let m = modules.get(x.owner)
1221
+ if (m && !m.module) m = undefined
1222
+ if (m) {
1223
+ if (m.shared && !m.counted) {
1224
+ m.counted = true
1225
+ add('shareRecv')
1226
+ } else add('moduleCacheHits')
1227
+ } else if (hubBytes.has(x.owner)) {
1228
+ // W31b: the hub sent this Module's bytes (the Module itself could not be received here,
1229
+ // and the log may have reused its bytes): compile the copy from them
1230
+ m = compileLocal(r, hubBytes.get(x.owner))
1231
+ hubBytes.delete(x.owner)
1232
+ add('shareBytesCompiles')
1233
+ } else {
1234
+ // Sharing: the Module was compiled by the Worker that loaded it and is on its way
1235
+ // through the hub; wait for it rather than compile another copy.
1236
+ if (mayWait && !forceLocal.has(x.owner)) throw new Missing(x.owner)
1237
+ // sharing: a record the hub delivered (or whose bytes were released) is the hub's to
1238
+ // supply: ask it for the bytes and wait, never read bytes that may be reused
1239
+ const freedWait = () => {
1240
+ add('shareFreedWaits')
1241
+ wantBytes(x.owner)
1242
+ throw new Missing(x.owner)
1243
+ }
1244
+ if (share && ((!Atomics.load(hdr, H_NORECLAIM) && Atomics.load(R, x.owner * REC_INTS + R_DELIVERED)) || isGone(x.owner))) freedWait()
1245
+ const bytes = L.slice(x.logOff, x.logOff + x.len) // a plain (non-shared) copy
1246
+ if (share && isGone(x.owner)) freedWait() // released while we copied: the copy may be torn
1247
+ m = compileLocal(r, bytes)
1248
+ }
1249
+ if (m.names.length !== x.nfuncs) throw new Error(`jit module ${r} exports ${m.names.length} functions, record says ${x.nfuncs}`)
1250
+ let inst = instances.get(x.owner)
1251
+ if (!inst) {
1252
+ const t = now()
1253
+ inst = new WebAssembly.Instance(m.module, imports)
1254
+ const dt = now() - t
1255
+ add('instantiates')
1256
+ add('instantiateNs', ns(dt))
1257
+ Atomics.store(R, r * REC_INTS + R_INST_US, Math.max(1, Math.round(dt * 1000)))
1258
+ instances.set(x.owner, inst)
1259
+ }
1260
+ const first = tableMin + x.slotBase
1261
+ growTable(first + x.nfuncs)
1262
+ for (let i = 0; i < x.nfuncs; i++) table.set(first + i, inst.exports[m.names[i]])
1263
+ add('tableSets', x.nfuncs)
1264
+ }
1265
+
1266
+ function growTable(need) {
1267
+ if (table.length < need) {
1268
+ // Exactly to `need`, as jit.rs does: the guest's dispatcher treats an index at or
1269
+ // past table.size as "not synced yet" (IR_EXIT_SYNC_MISS, wasm_emit.c) and calls
1270
+ // jit_sync, so the table must never be larger than what this thread installed.
1271
+ table.grow(need - table.length)
1272
+ add('tableGrows')
1273
+ for (let cur = Atomics.load(hdr, H_TABLE_MAX); cur < table.length; cur = Atomics.load(hdr, H_TABLE_MAX)) {
1274
+ if (Atomics.compareExchange(hdr, H_TABLE_MAX, cur, table.length) === cur) break
1275
+ }
1276
+ }
1277
+ }
1278
+
1279
+ // W34: slots are never rewound, so nothing below the first slot of the generation a
1280
+ // clear starts is set again; each clear covers only what may have been set since (a
1281
+ // guest that releases often would otherwise clear its whole, ever longer, table every
1282
+ // time). Not past that first slot: a sync can install the new generation's first
1283
+ // records before it sees the flush, and installs them again after this clear.
1284
+ let clearedTo = tableMin
1285
+ function clearTable(slot0) {
1286
+ if (breakClear) return // test mutation (brk='noclear'): the door-level flush tests must catch it
1287
+ for (let i = clearedTo; i < table.length; i++) table.set(i, null)
1288
+ clearedTo = Math.min(table.length, tableMin + slot0)
1289
+ }
1290
+
1291
+ /**
1292
+ * Bring this thread's table up to date with the global slot list (jit.rs
1293
+ * sync_store). With `mayWait` (sharing), returns the log offset of a Module that has
1294
+ * not arrived yet instead of compiling it; the caller waits and calls again (the
1295
+ * thread's progress, `applied`, is kept). Returns undefined when up to date.
1296
+ */
1297
+ function sync(mayWait = false) {
1298
+ if (!table) throw new Error('jit_sync before the thread was attached')
1299
+ const t = now()
1300
+ add('syncs')
1301
+ try {
1302
+ syncLoop(mayWait)
1303
+ } catch (err) {
1304
+ if (err instanceof Missing) return err.logOff
1305
+ throw err
1306
+ } finally {
1307
+ add('syncNs', ns(now() - t))
1308
+ }
1309
+ }
1310
+ function syncLoop(mayWait) {
1311
+ for (;;) {
1312
+ const g = Atomics.load(hdr, H_GEN)
1313
+ if (g !== gen) {
1314
+ const start = Atomics.load(hdr, H_MOD_START)
1315
+ const slot0 = Atomics.load(hdr, H_SLOT0)
1316
+ if (Atomics.load(hdr, H_GEN) !== g) continue
1317
+ if (gen !== -1) clearTable(slot0)
1318
+ releaseOldModules(g)
1319
+ instances = new Map()
1320
+ applied = start
1321
+ gen = g
1322
+ }
1323
+ applyDrops()
1324
+ const n = Atomics.load(hdr, H_NMOD)
1325
+ for (; applied < n; ) {
1326
+ install(applied, mayWait)
1327
+ applied++
1328
+ }
1329
+ if (Atomics.load(hdr, H_GEN) === gen) break // else a flush ran meanwhile: redo
1330
+ }
1331
+ }
1332
+
1333
+ // How long a vCPU waits for a Module before it compiles its own copy. Every published
1334
+ // module was compiled and posted by its loader, so the wait normally ends in well
1335
+ // under a millisecond; the copy is only for a hub that stopped answering.
1336
+ const waitMs = share?.waitMs ?? 3000
1337
+ /** Wait (the Worker's event loop runs) until `logOff` arrives, or waitMs passes. */
1338
+ function arrival(logOff) {
1339
+ return new Promise(resolve => {
1340
+ const timer = setTimeout(() => {
1341
+ if (waiters.get(logOff) === done) waiters.delete(logOff)
1342
+ resolve(false)
1343
+ }, deserErrors || timeouts.has(logOff) ? Math.min(waitMs, errorWaitMs) : waitMs)
1344
+ const done = () => {
1345
+ clearTimeout(timer)
1346
+ resolve(true)
1347
+ }
1348
+ waiters.set(logOff, done)
1349
+ })
1350
+ }
1351
+
1352
+ /** sync for a caller that can wait: undefined when done at once, else a Promise. */
1353
+ const LOCAL_TRIES = 3 // local copies that may fail with a resource error, and timeouts for one Module, before the thread traps
1354
+ // W57: waits for one Module that end by asking the hub to post it again, before a local copy. A Module
1355
+ // Firefox failed to deliver ('messageerror', seen with its code space far from full) arrives when posted
1356
+ // again; a copy per vCPU instead filled the code space and trapped the guest.
1357
+ const RESENDS = share?.resends ?? 4
1358
+ const asked = new Set() // "gen:owner" asked of the hub in a batch (the port had failed a message)
1359
+ function resend(logOff) {
1360
+ add('shareResends')
1361
+ try {
1362
+ share.port.postMessage({ t: 'resend', logOffs: [logOff], gen: modulesGen })
1363
+ } catch {}
1364
+ }
1365
+ /** W57: ask the hub, in one message, for `miss` and every later Module this thread lacks (not asked yet). */
1366
+ function askMissing(miss, again = false) {
1367
+ const want = [miss]
1368
+ asked.add(`${modulesGen}:${miss}`)
1369
+ const n = Atomics.load(hdr, H_NMOD)
1370
+ for (let r = applied; r < n && want.length < 1024; r++) {
1371
+ const o = Atomics.load(R, r * REC_INTS + R_OWNER)
1372
+ const k = `${modulesGen}:${o}`
1373
+ if (modules.has(o) || o === miss || (asked.has(k) && !again) || isDropped(r)) continue
1374
+ asked.add(k)
1375
+ want.push(o)
1376
+ }
1377
+ add('shareResends', want.length)
1378
+ try {
1379
+ share.port.postMessage({ t: 'resend', logOffs: want, gen: modulesGen })
1380
+ } catch {}
1381
+ }
1382
+ const localFails = new Map() // logOff -> failed local compiles
1383
+ const timeouts = new Map() // logOff -> waits that ended without the Module
1384
+ const localTried = new Set() // logOffs a local copy was tried for
1385
+ let portLossy = false // a wait ended without its Module, asked again too: this port does not deliver reliably, so a missing Module is compiled at once
1386
+ let resendLogged = 0
1387
+ function syncShared() {
1388
+ let miss = sync(true)
1389
+ if (miss === undefined) return undefined
1390
+ return (async () => {
1391
+ while (miss !== undefined) {
1392
+ // nothing can supply it: the log reused its bytes and the hub has none (W31b)
1393
+ if (noBytes.has(miss) && !modules.has(miss) && Atomics.load(hdr, H_GEN) === modulesGen) {
1394
+ throw new Error(`jit: the Module at log offset ${miss} (generation ${modulesGen}) could not be received from the hub${deserErrors ? ' (messageerror)' : ''}, its bytes in the module log were reused and the hub keeps no copy of them (evicted past its byte limit)`)
1395
+ }
1396
+ const t = now()
1397
+ add('shareWaits')
1398
+ let ok = modules.has(miss) || lost.has(miss)
1399
+ // On a lossy port a missing Module is compiled here at once, once: waiting for each of the
1400
+ // 190 Modules of a boot one after another took minutes (P-B7 review, Firefox). If the copy
1401
+ // cannot be made (its log bytes are released) the Module is the hub's to supply: wait.
1402
+ const now_ = !ok && portLossy && !localTried.has(miss)
1403
+ // D-512: the hub posts a Module only when asked (hello pull), so a missing one is asked for at once
1404
+ if (!ok && !now_ && RESENDS && !asked.has(`${modulesGen}:${miss}`)) askMissing(miss)
1405
+ if (!ok && !now_) ok = await arrival(miss)
1406
+ add('shareWaitNs', ns(now() - t))
1407
+ if (now_) {
1408
+ localTried.add(miss)
1409
+ forceLocal.add(miss)
1410
+ } else if (lost.has(miss) && !modules.has(miss)) {
1411
+ lost.delete(miss)
1412
+ lostForGood.add(miss)
1413
+ forceLocal.add(miss) // a local copy is the only way left; if it fails, the thread traps
1414
+ } else if (!ok && !modules.has(miss)) {
1415
+ add('shareTimeouts')
1416
+ const n = (timeouts.get(miss) || 0) + 1
1417
+ timeouts.set(miss, n)
1418
+ const ms = deserErrors || n > 1 ? Math.min(waitMs, errorWaitMs) : waitMs
1419
+ if (n <= RESENDS) {
1420
+ if (!resendLogged++) log(`wmd-web: jit: module at log offset ${miss} (generation ${modulesGen}) did not arrive from the hub in ${ms} ms; asking the hub to post it again`)
1421
+ // a lost batch is asked again as a batch: one wait per Module of it cost minutes (W57 review)
1422
+ if (deserErrors) askMissing(miss, true)
1423
+ else resend(miss)
1424
+ continue
1425
+ }
1426
+ if (n - RESENDS >= LOCAL_TRIES) {
1427
+ throw new Error(`jit: the Module at log offset ${miss} (generation ${modulesGen}) did not arrive from the hub in ${n} waits of ${ms} ms${deserErrors ? ' (a hub message could not be deserialized here)' : ''} and no local copy could be made (its bytes in the module log were released after the hub delivered it)`)
1428
+ }
1429
+ log(`wmd-web: jit: module at log offset ${miss} (generation ${modulesGen}) did not arrive from the hub in ${ms} ms; compiling a copy${portLossy ? '' : '; every later missing Module is compiled at once (this hub port does not deliver reliably)'}`)
1430
+ portLossy = true
1431
+ localTried.add(miss)
1432
+ forceLocal.add(miss)
1433
+ }
1434
+ try {
1435
+ miss = sync(true)
1436
+ } catch (err) {
1437
+ // The copy did not compile (the engine is out of code space). If the Module was only
1438
+ // late it will still come (its loader compiled and posted it), so wait again; but not
1439
+ // forever, and not at all when the port loses Modules.
1440
+ if (!forceLocal.has(miss) || !isResource(err) || lostForGood.has(miss)) throw err
1441
+ forceLocal.delete(miss)
1442
+ const n = (localFails.get(miss) || 0) + 1
1443
+ localFails.set(miss, n)
1444
+ const why = err && err.message ? err.message : String(err)
1445
+ if (n >= LOCAL_TRIES || deserErrors) {
1446
+ throw new Error(`jit: the Module at log offset ${miss} could not be received from the hub${deserErrors ? ' (messageerror)' : ''} and a local copy could not be compiled (${why}; ${n} tries): the engine is out of code space for it`)
1447
+ }
1448
+ log(`wmd-web: jit: the local copy of the Module at log offset ${miss} failed (${why}); waiting for the hub again (${n}/${LOCAL_TRIES})`)
1449
+ }
1450
+ }
1451
+ })()
1452
+ }
1453
+
1454
+ // ---- Dropping cold and sparse modules (D-514) ----
1455
+ //
1456
+ // WebKit gives a page about 350 MB of executable memory; a Go build's modules want about 1 GB of
1457
+ // it. A release (flush) drops every module, so the code in use starts again in WebKit's
1458
+ // interpreter, and the old copies hold the memory until each Worker lets go of them. Instead the
1459
+ // guest's blocks mark their table index each time one is entered through its live flag (vm.wasm's
1460
+ // use marks, at usedBase in memory 0). markUse moves those marks to the records; evict picks the
1461
+ // modules no block entered lately, then the ones where few blocks were entered (a module holds
1462
+ // about a hundred blocks compiled together, and one busy block would keep all of them); the guest
1463
+ // turns their functions off, and drop() takes them out of every Worker (its table entries,
1464
+ // instances and Modules) and out of the hub. The busy blocks of a dropped module are compiled
1465
+ // again (knHit answers a miss for them), together, so the code in use ends up in fewer modules.
1466
+
1467
+ /** H_LOCK held: records whose functions were entered since the last mark get the new mark and the count. */
1468
+ function markUse() {
1469
+ Atomics.store(hdr, H_MARK_KIB, 0)
1470
+ const m = Atomics.add(hdr, H_MARK, 1) + 1
1471
+ add('marks')
1472
+ if (!usedBase) return m
1473
+ const U = new Uint8Array(memory.buffer)
1474
+ const n = Atomics.load(hdr, H_NMOD)
1475
+ for (let r = Atomics.load(hdr, H_MOD_START); r < n; r++) {
1476
+ const b = r * REC_INTS
1477
+ if (R[b + R_DROPPED]) continue
1478
+ const lo = tableMin + R[b + R_SLOT_BASE]
1479
+ let nu = 0
1480
+ for (let i = 0, nf = R[b + R_NFUNCS]; i < nf; i++) {
1481
+ const k = usedBase + ((lo + i) & (VM_CELLS - 1))
1482
+ if (U[k]) {
1483
+ nu++
1484
+ U[k] = 0
1485
+ }
1486
+ }
1487
+ if (nu) {
1488
+ R[b + R_USED] = m
1489
+ R[b + R_USEDN] = nu
1490
+ }
1491
+ }
1492
+ return m
1493
+ }
1494
+
1495
+ let evictPending = null // records evict(0) picked, for evict(1)
1496
+ /**
1497
+ * wmd.jit_evict (vm cache.h jit_evict). mode 0: pick modules until the live ones fit in
1498
+ * EVICT_KEEP of the budget: first the ones no block entered in the last mark, least recently
1499
+ * used first, then the ones where fewer than EVICT_SPARSE of the functions were entered, the
1500
+ * emptiest first. Write their table index ranges [lo, hi) at ptr (sorted, at most cap) and return
1501
+ * how many, or 0 when that frees less than EVICT_MIN of it (the guest then releases every
1502
+ * module). Never the module at table index keep (the dispatcher). mode 1: drop what mode 0 picked.
1503
+ */
1504
+ function evict(mode, keep, ptr, cap) {
1505
+ if (mode === 1) return drop()
1506
+ evictPending = null
1507
+ const budget = liveBudget()
1508
+ if (!usedBase || !budget || !DL) return 0
1509
+ lock()
1510
+ try {
1511
+ const m = markUse()
1512
+ const g = Atomics.load(hdr, H_GEN)
1513
+ const n = Atomics.load(hdr, H_NMOD)
1514
+ const groups = new Map() // owner -> {used, fns, busy, recs, kib}
1515
+ for (let r = Atomics.load(hdr, H_MOD_START); r < n; r++) {
1516
+ const b = r * REC_INTS
1517
+ if (R[b + R_DROPPED] || R[b + R_GEN] !== g) continue
1518
+ const o = R[b + R_OWNER]
1519
+ let x = groups.get(o)
1520
+ if (!x) groups.set(o, (x = { o, used: 0, fns: 0, busy: 0, recs: [], kib: o === r || R[o * REC_INTS + R_GEN] !== g ? Math.ceil(R[b + R_LEN] / 1024) : 0 }))
1521
+ const lo = tableMin + R[b + R_SLOT_BASE]
1522
+ const nf = R[b + R_NFUNCS]
1523
+ x.used = Math.max(x.used, keep >= lo && keep < lo + nf ? Infinity : R[b + R_USED])
1524
+ x.fns += nf
1525
+ if (R[b + R_USED] >= m) x.busy += R[b + R_USEDN]
1526
+ x.recs.push(r)
1527
+ }
1528
+ const all = [...groups.values()]
1529
+ const cold = all.filter(x => x.used < m).sort((a, b) => a.used - b.used || a.o - b.o)
1530
+ const sparse = all
1531
+ .filter(x => x.used === m && x.busy < x.fns * EVICT_SPARSE)
1532
+ .sort((a, b) => a.busy / a.fns - b.busy / b.fns || a.o - b.o)
1533
+ let live = Atomics.load(hdr, H_LIVE_KIB)
1534
+ const keepKiB = Math.floor((budget * EVICT_KEEP) / 1024)
1535
+ const pick = []
1536
+ let ranges = 0
1537
+ let freed = 0
1538
+ for (const x of [...cold, ...sparse]) {
1539
+ if (live <= keepKiB || ranges + x.recs.length > cap) break
1540
+ pick.push(x)
1541
+ ranges += x.recs.length
1542
+ live -= x.kib
1543
+ freed += x.kib
1544
+ }
1545
+ if (!pick.length || freed * 1024 < budget * EVICT_MIN) return 0
1546
+ const iv = []
1547
+ for (const x of pick) for (const r of x.recs) iv.push([tableMin + R[r * REC_INTS + R_SLOT_BASE], R[r * REC_INTS + R_NFUNCS]])
1548
+ iv.sort((a, b) => a[0] - b[0])
1549
+ const out = new DataView(memory.buffer, ptr >>> 0, iv.length * 8)
1550
+ iv.forEach(([lo, nf], i) => {
1551
+ out.setUint32(i * 8, lo, true)
1552
+ out.setUint32(i * 8 + 4, lo + nf, true)
1553
+ })
1554
+ evictPending = { gen: g, pick, freed }
1555
+ return iv.length
1556
+ } finally {
1557
+ unlock()
1558
+ }
1559
+ }
1560
+
1561
+ /** evict(1): the guest turned the picked modules' functions off; drop them everywhere. */
1562
+ function drop() {
1563
+ const p = evictPending
1564
+ evictPending = null
1565
+ if (!p) return 0
1566
+ const owners = []
1567
+ lock()
1568
+ try {
1569
+ if (Atomics.load(hdr, H_GEN) !== p.gen) return 0
1570
+ let k = Atomics.load(hdr, H_DROPS)
1571
+ for (const x of p.pick) {
1572
+ for (const r of x.recs) {
1573
+ if (!ensure(store.drops, (k + 1) * 4)) break
1574
+ Atomics.store(R, r * REC_INTS + R_DROPPED, 1)
1575
+ DL[k++] = r
1576
+ }
1577
+ owners.push(x.o)
1578
+ }
1579
+ Atomics.sub(hdr, H_LIVE_KIB, Math.min(p.freed, Atomics.load(hdr, H_LIVE_KIB)))
1580
+ Atomics.store(hdr, H_DROPS, k)
1581
+ } finally {
1582
+ unlock()
1583
+ }
1584
+ add('evictions')
1585
+ add('evictedModules', owners.length)
1586
+ add('evictedKiB', p.freed)
1587
+ if (share) share.port.postMessage({ t: 'drop', gen: p.gen, owners })
1588
+ return owners.length
1589
+ }
1590
+
1591
+ // per thread: drops this thread's table has taken
1592
+ let dropsApplied = 0
1593
+ /** Clear the table entries, instances and Modules of the records dropped since this thread last looked. */
1594
+ function applyDrops() {
1595
+ if (!DL) return
1596
+ const nd = Atomics.load(hdr, H_DROPS)
1597
+ for (; dropsApplied < nd; dropsApplied++) {
1598
+ const r = DL[dropsApplied]
1599
+ const b = r * REC_INTS
1600
+ if (Atomics.load(R, b + R_GEN) !== gen) continue
1601
+ const o = Atomics.load(R, b + R_OWNER)
1602
+ if (r < applied) {
1603
+ const first = tableMin + Atomics.load(R, b + R_SLOT_BASE)
1604
+ for (let i = 0, nf = Atomics.load(R, b + R_NFUNCS); i < nf && first + i < table.length; i++) table.set(first + i, null)
1605
+ }
1606
+ instances.delete(o)
1607
+ modules.delete(o)
1608
+ hubBytes.delete(o)
1609
+ }
1610
+ }
1611
+
1612
+ function flush() {
1613
+ let g
1614
+ lock()
1615
+ try {
1616
+ // H_MOD_START and H_SLOT0 before H_GEN, so a reader that sees the new generation
1617
+ // also sees where it starts (sync re-reads H_GEN after H_MOD_START).
1618
+ // H_NSLOTS is not rewound (W34): the guest flushes while other vCPUs run, and a
1619
+ // vCPU whose table is not synced yet may still call a slot index it read before
1620
+ // the flush. With slots only ever growing, that index is either an old entry
1621
+ // (still the old, correct function until its thread syncs) or past its table
1622
+ // (IR_EXIT_SYNC_MISS); a rewound index could name another block's new function.
1623
+ Atomics.store(hdr, H_MOD_START, Atomics.load(hdr, H_NMOD))
1624
+ Atomics.store(hdr, H_SLOT0, Atomics.load(hdr, H_NSLOTS))
1625
+ g = Atomics.add(hdr, H_GEN, 1) + 1
1626
+ Atomics.store(hdr, H_LIVE_KIB, 0)
1627
+ } finally {
1628
+ unlock()
1629
+ }
1630
+ if (share) share.port.postMessage({ t: 'flush', gen: g }) // the hub drops older Modules
1631
+ add('flushes')
1632
+ }
1633
+
1634
+
1635
+ const loadFailed = err => {
1636
+ log(`wmd-web: jit_load: ${err && err.message ? err.message : err}`)
1637
+ return -1
1638
+ }
1639
+ const imp = {
1640
+ load(ptr, len) {
1641
+ add('loads')
1642
+ if (breakLoad) return -1
1643
+ const bytes = readGuest(ptr, len)
1644
+ if (!bytes) return outside('load', ptr, len)
1645
+ const base = append(bytes, 1, 'compile')
1646
+ if (base < 0) return base // -1, or JIT_OVER_BUDGET (-2) for the budget refusal
1647
+ try {
1648
+ if (share) {
1649
+ const p = syncShared()
1650
+ if (p) return p.then(() => base, loadFailed)
1651
+ } else sync()
1652
+ } catch (err) {
1653
+ return loadFailed(err)
1654
+ }
1655
+ return base
1656
+ },
1657
+ compile(ptr, len) {
1658
+ add('compiles')
1659
+ if (breakCompile) return -1
1660
+ // sharing: the compile thread compiles (once for the process) and shares it
1661
+ const bytes = readGuest(ptr, len)
1662
+ if (!bytes) return outside('compile', ptr, len)
1663
+ return append(bytes, 2, share ? 'compile' : compileOnCompile)
1664
+ },
1665
+ sync() {
1666
+ // failure is fatal, as in the runner (exit 4): the throw traps the thread
1667
+ if (share) return syncShared()
1668
+ sync()
1669
+ },
1670
+ flush() {
1671
+ flush()
1672
+ if (share) return syncShared()
1673
+ sync()
1674
+ },
1675
+ known(ptr) {
1676
+ add('knownAsks')
1677
+ if (!cache && !KN) return -1
1678
+ return known(ptr)
1679
+ },
1680
+ note(ptr, n) {
1681
+ add('notes')
1682
+ if (cache || KN) note(ptr, n)
1683
+ },
1684
+ evict(mode, keep, ptr, cap) {
1685
+ return evict(mode, keep, ptr, cap)
1686
+ },
1687
+ }
1688
+
1689
+ return {
1690
+ imports: imp,
1691
+ /** A new vm.wasm instance (a new thread) on this Worker: its table and helpers. */
1692
+ attach(instance, tbl) {
1693
+ table = tbl
1694
+ const ub = instance.exports.vm_jit_used_base
1695
+ if (typeof ub === 'function' && !usedBase) usedBase = ub() >>> 0
1696
+ const hst = {}
1697
+ for (let i = 0; i < HELPERS; i++) {
1698
+ const f = instance.exports[`vm_helper_${i}`]
1699
+ if (typeof f !== 'function') throw new Error(`vm.wasm has no export vm_helper_${i}`)
1700
+ hst[`h${i}`] = f
1701
+ }
1702
+ imports = { env: ram ? { mem0: memory, mem1: memory, win: ram, tbl } : { mem0: memory, mem1: memory, tbl }, hst }
1703
+ gen = -1
1704
+ applied = 0
1705
+ clearedTo = tableMin
1706
+ instances = new Map()
1707
+ dropsApplied = Atomics.load(hdr, H_DROPS) // install skips what is dropped already
1708
+ },
1709
+ /** For tests: attach with an explicit helper list instead of a vm.wasm instance. */
1710
+ attachRaw(tbl, helpers) {
1711
+ table = tbl
1712
+ const hst = {}
1713
+ helpers.forEach((f, i) => (hst[`h${i}`] = f))
1714
+ imports = { env: ram ? { mem0: memory, mem1: memory, win: ram, tbl } : { mem0: memory, mem1: memory, tbl }, hst }
1715
+ gen = -1
1716
+ applied = 0
1717
+ clearedTo = tableMin
1718
+ instances = new Map()
1719
+ dropsApplied = Atomics.load(hdr, H_DROPS) // install skips what is dropped already
1720
+ },
1721
+ sync: () => {
1722
+ if (share) throw new Error('jit door: sharing is on, use syncAsync()')
1723
+ sync()
1724
+ },
1725
+ /** For JS callers that can wait (a new thread before wasi_thread_start). */
1726
+ syncAsync: async () => {
1727
+ if (share) await syncShared()
1728
+ else sync()
1729
+ },
1730
+ get compiledModules() {
1731
+ return modules.size
1732
+ },
1733
+ }
1734
+ }
1735
+
1736
+ function pct(sorted, p) {
1737
+ if (!sorted.length) return 0
1738
+ return sorted[Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length))]
1739
+ }
1740
+
1741
+ /** Counters and per-module compile cost, read from the store (page side). */
1742
+ export function readJitStats(store) {
1743
+ const hdr = new Int32Array(store.ctl, 0, HDR_INTS)
1744
+ const st = new Int32Array(store.ctl, STATS_OFF, STATS.length * 2)
1745
+ const R = new Int32Array(store.recs)
1746
+ const out = {}
1747
+ STATS.forEach((k, i) => {
1748
+ const v = statLoad(st, i)
1749
+ out[k] = k.endsWith('Ns') ? Math.round(v / 1e4) / 100 : v // ...Ns reported in ms
1750
+ })
1751
+ for (const k of Object.keys(out)) {
1752
+ if (k.endsWith('Ns')) {
1753
+ out[`${k.slice(0, -2)}Ms`] = out[k]
1754
+ delete out[k]
1755
+ }
1756
+ }
1757
+ const n = Atomics.load(hdr, H_NMOD)
1758
+ const append = []
1759
+ const sync = []
1760
+ const inst = []
1761
+ let funcs = 0
1762
+ let maxLen = 0
1763
+ for (let r = 0; r < n; r++) {
1764
+ const b = r * REC_INTS
1765
+ funcs += R[b + R_NFUNCS]
1766
+ maxLen = Math.max(maxLen, R[b + R_LEN])
1767
+ if (R[b + R_APPEND_US]) append.push(R[b + R_APPEND_US] / 1000)
1768
+ if (R[b + R_SYNC_US]) sync.push(R[b + R_SYNC_US] / 1000)
1769
+ if (R[b + R_INST_US]) inst.push(R[b + R_INST_US] / 1000)
1770
+ }
1771
+ const summary = a => {
1772
+ a.sort((x, y) => x - y)
1773
+ const sum = a.reduce((s, v) => s + v, 0)
1774
+ return { n: a.length, totalMs: +sum.toFixed(2), p50Ms: +pct(a, 50).toFixed(3), p99Ms: +pct(a, 99).toFixed(3), maxMs: +(a.at(-1) ?? 0).toFixed(3) }
1775
+ }
1776
+ out.modules = n
1777
+ out.functions = funcs
1778
+ out.maxModuleBytes = maxLen
1779
+ out.flushGen = Atomics.load(hdr, H_GEN)
1780
+ out.liveModules = n - Atomics.load(hdr, H_MOD_START)
1781
+ out.liveSlots = Atomics.load(hdr, H_NSLOTS) - Atomics.load(hdr, H_SLOT0)
1782
+ out.slots = Atomics.load(hdr, H_NSLOTS)
1783
+ out.liveKiB = Atomics.load(hdr, H_LIVE_KIB) // W34: what the module budget counts
1784
+ out.tableMax = Atomics.load(hdr, H_TABLE_MAX)
1785
+ out.logBytes = Atomics.load(hdr, H_LOG_USED)
1786
+ out.logCapacity = store.log.byteLength
1787
+ out.logTail = Atomics.load(hdr, H_LOG_TAIL) // owner records below it have had their bytes released
1788
+ let live = 0
1789
+ let delivered = 0
1790
+ for (let r = out.logTail; r < n; r++) {
1791
+ if (R[r * REC_INTS + R_OWNER] !== r || R[r * REC_INTS + R_FREED]) continue
1792
+ live++
1793
+ if (R[r * REC_INTS + R_DELIVERED]) delivered++
1794
+ }
1795
+ out.logLiveRecords = live // owners whose bytes are still in the log
1796
+ out.logLiveDelivered = delivered // ... of which the hub delivered (released at the next release pass)
1797
+ out.recordsUsed = n
1798
+ out.recordsMax = Math.floor(store.recs.maxByteLength / REC_BYTES) // records are not reclaimed: the next limit after the log
1799
+ out.recordBytes = store.recs.byteLength
1800
+ out.compileInLoader = summary(append) // per module, in the thread that loaded it
1801
+ out.compileInSync = summary(sync) // per module, in a thread that synced it
1802
+ out.instantiate = summary(inst)
1803
+ return out
1804
+ }
1805
+
1806
+ /**
1807
+ * The minimum of the table a wasm module imports (vm.wasm's env.__indirect_function_table),
1808
+ * read from the binary's import section, as the runner reads it from the module's import
1809
+ * type (host.rs table_min). The JS API does not expose import limits. vm.wasm's minimum
1810
+ * grows when it gains functions taken by address (155 before W3.3, 158 after), and the
1811
+ * JIT slots start right after it. null when the module imports no table.
1812
+ *
1813
+ * With more than one table import, env.__indirect_function_table is the one (it is the
1814
+ * only table the browser host provides, worker.js); without it, the last table import
1815
+ * (host.rs keeps the last). Custom sections anywhere, any LEB128 length (non-minimal
1816
+ * encodings included), table64/memory64/shared/custom-page-size limits, typed function
1817
+ * references ((ref null? ht), 0x63/0x64 + a heap type) in table and global imports, and
1818
+ * exact function imports (0x20) are parsed. Throws on what is not a module or is cut short.
1819
+ * @param {ArrayBuffer|SharedArrayBuffer|ArrayBufferView} bytes
1820
+ * @returns {number|null}
1821
+ */
1822
+ export function tableMinOf(bytes) {
1823
+ const b = ArrayBuffer.isView(bytes) ? new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength) : new Uint8Array(bytes)
1824
+ let p = 8
1825
+ let end = b.length
1826
+ const byte = () => {
1827
+ if (p >= end) throw new Error('tableMinOf: truncated module')
1828
+ return b[p++]
1829
+ }
1830
+ const u = () => {
1831
+ // unsigned LEB128 (u32 or, for table64/memory64 limits, u64: a Number, exact to 2^53)
1832
+ let r = 0
1833
+ let mul = 1
1834
+ for (let n = 0; ; n++) {
1835
+ if (n >= 10) throw new Error('tableMinOf: LEB128 too long')
1836
+ const x = byte()
1837
+ r += (x & 0x7f) * mul
1838
+ if (!(x & 0x80)) return r
1839
+ mul *= 128
1840
+ }
1841
+ }
1842
+ const s33 = () => {
1843
+ // a heap type: a negative abbreviation (one byte) or a type index (s33 LEB)
1844
+ for (let n = 0; n < 5; n++) if (!(byte() & 0x80)) return
1845
+ throw new Error('tableMinOf: LEB128 too long')
1846
+ }
1847
+ const name = () => {
1848
+ const n = u()
1849
+ if (p + n > end) throw new Error('tableMinOf: truncated module')
1850
+ const s = new TextDecoder().decode(b.subarray(p, p + n))
1851
+ p += n
1852
+ return s
1853
+ }
1854
+ const reftype = t => {
1855
+ if (t === 0x63 || t === 0x64) s33() // (ref null ht) / (ref ht)
1856
+ }
1857
+ const valtype = () => reftype(byte())
1858
+ const limits = () => {
1859
+ // bit 0: has a maximum; bit 1: shared; bit 2: 64-bit index; bit 3: custom page size
1860
+ const flags = byte()
1861
+ if (flags & ~0x0f) throw new Error(`tableMinOf: limits flags ${flags}`)
1862
+ const min = u()
1863
+ if (flags & 1) u()
1864
+ if (flags & 8) u()
1865
+ return min
1866
+ }
1867
+ if (b.length < 8 || b[0] !== 0 || b[1] !== 0x61 || b[2] !== 0x73 || b[3] !== 0x6d) throw new Error('tableMinOf: not a wasm module')
1868
+ while (p < b.length) {
1869
+ end = b.length
1870
+ const id = byte()
1871
+ const size = u()
1872
+ const next = p + size
1873
+ if (next > b.length) throw new Error('tableMinOf: truncated module')
1874
+ if (id !== 2) {
1875
+ p = next
1876
+ continue
1877
+ }
1878
+ end = next // the import section's own end
1879
+ let last = null
1880
+ let named = null
1881
+ for (let n = u(); n > 0; n--) {
1882
+ const mod = name()
1883
+ const isIft = name() === '__indirect_function_table' && mod === 'env' // both names are read
1884
+ const kind = byte()
1885
+ if (kind === 0 || kind === 0x20) u() // function (or exact function): type index
1886
+ else if (kind === 1) {
1887
+ reftype(byte())
1888
+ const min = limits()
1889
+ last = min
1890
+ if (isIft) named = min
1891
+ } else if (kind === 2) limits() // memory
1892
+ else if (kind === 3) {
1893
+ valtype()
1894
+ byte() // mutability
1895
+ } else if (kind === 4) {
1896
+ byte() // tag: attribute
1897
+ u()
1898
+ } else throw new Error(`tableMinOf: import kind ${kind}`)
1899
+ }
1900
+ return named ?? last
1901
+ }
1902
+ return null
1903
+ }