@threadedstack/wmd-web 0.0.0-stage → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2601 -2
- package/RELEASE.json +123 -0
- package/export-page/coi-sw.js +20 -0
- package/export-page/index.html +17 -0
- package/export-page/run.css +10 -0
- package/export-page/run.js +236 -0
- package/guest/arm64/Image +4 -0
- package/guest/arm64/Image-4k +4 -0
- package/guest/arm64/initramfs.cpio.gz +0 -0
- package/guest/arm64/vm.wasm +0 -0
- package/package.json +36 -4
- package/page/sw.js +381 -0
- package/relay/control.ts +139 -0
- package/relay/engine.ts +787 -0
- package/relay/hosted.ts +660 -0
- package/relay/keys.ts +53 -0
- package/relay/local-tls.ts +121 -0
- package/relay/lock.ts +81 -0
- package/relay/logfile.ts +138 -0
- package/relay/main.ts +642 -0
- package/relay/policy.ts +241 -0
- package/relay/relay.ts +266 -0
- package/relay/token.ts +116 -0
- package/relay/v1.ts +107 -0
- package/relay/wisp.ts +615 -0
- package/src/base.js +325 -0
- package/src/boot.js +2037 -0
- package/src/calib-worker.js +40 -0
- package/src/fs/9p/codec.js +303 -0
- package/src/fs/9p/server.js +521 -0
- package/src/fs/errno.js +43 -0
- package/src/fs/graph.js +380 -0
- package/src/fs/overlay.js +1018 -0
- package/src/fs/pack-write.js +165 -0
- package/src/fs/pack.js +427 -0
- package/src/fs/seed.js +22 -0
- package/src/fs/svc-snapshot.js +65 -0
- package/src/fs/upper-pack.js +112 -0
- package/src/host/base-cache.js +195 -0
- package/src/host/base-file.js +328 -0
- package/src/host/base-io.js +93 -0
- package/src/host/boot-events.js +209 -0
- package/src/host/chan-service.js +300 -0
- package/src/host/chan-services.js +689 -0
- package/src/host/chan.js +655 -0
- package/src/host/ckpt-file.js +395 -0
- package/src/host/ckpt-io.js +207 -0
- package/src/host/ckpt-point.js +160 -0
- package/src/host/ckpt-save.js +64 -0
- package/src/host/ckpt.js +434 -0
- package/src/host/clock.js +111 -0
- package/src/host/ctl.js +778 -0
- package/src/host/errno.js +18 -0
- package/src/host/fs.js +167 -0
- package/src/host/gcnudge.js +83 -0
- package/src/host/i64shim.js +81 -0
- package/src/host/jit-cache.js +341 -0
- package/src/host/jit-hub-core.js +208 -0
- package/src/host/jit-hub.js +93 -0
- package/src/host/jit-snap.js +226 -0
- package/src/host/jit.js +1903 -0
- package/src/host/layout.js +100 -0
- package/src/host/monitor.js +110 -0
- package/src/host/p9mount.js +27 -0
- package/src/host/ram-mem.js +125 -0
- package/src/host/stdio.js +143 -0
- package/src/host/threads.js +109 -0
- package/src/host/wasi.js +335 -0
- package/src/host/wmd-imports.js +71 -0
- package/src/host/yamux.js +387 -0
- package/src/image-files.js +154 -0
- package/src/net/dhcp-dns.js +126 -0
- package/src/net/net-worker.js +172 -0
- package/src/net/packet.js +227 -0
- package/src/net/peer.js +230 -0
- package/src/net/proto.js +189 -0
- package/src/net/relay-client.js +226 -0
- package/src/net/stack.js +867 -0
- package/src/net/switch.js +160 -0
- package/src/net/wisp-client.js +570 -0
- package/src/net/wisp-transport.js +196 -0
- package/src/net/wisp.js +511 -0
- package/src/registry/client.js +176 -0
- package/src/registry/converter.js +68 -0
- package/src/registry/hub.js +221 -0
- package/src/registry/oci.js +76 -0
- package/src/registry/pack-worker.js +70 -0
- package/src/registry/pull.js +258 -0
- package/src/registry/ref.js +110 -0
- package/src/registry/wasi-mem.js +355 -0
- package/src/worker.js +315 -0
- package/wmdpack.wasm +0 -0
package/src/host/jit.js
ADDED
|
@@ -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
|
+
}
|