@threadedstack/wmd-web 0.0.0-stage → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2601 -2
- package/RELEASE.json +123 -0
- package/export-page/coi-sw.js +20 -0
- package/export-page/index.html +17 -0
- package/export-page/run.css +10 -0
- package/export-page/run.js +236 -0
- package/guest/arm64/Image +4 -0
- package/guest/arm64/Image-4k +4 -0
- package/guest/arm64/initramfs.cpio.gz +0 -0
- package/guest/arm64/vm.wasm +0 -0
- package/package.json +36 -4
- package/page/sw.js +381 -0
- package/relay/control.ts +139 -0
- package/relay/engine.ts +787 -0
- package/relay/hosted.ts +660 -0
- package/relay/keys.ts +53 -0
- package/relay/local-tls.ts +121 -0
- package/relay/lock.ts +81 -0
- package/relay/logfile.ts +138 -0
- package/relay/main.ts +642 -0
- package/relay/policy.ts +241 -0
- package/relay/relay.ts +266 -0
- package/relay/token.ts +116 -0
- package/relay/v1.ts +107 -0
- package/relay/wisp.ts +615 -0
- package/src/base.js +325 -0
- package/src/boot.js +2037 -0
- package/src/calib-worker.js +40 -0
- package/src/fs/9p/codec.js +303 -0
- package/src/fs/9p/server.js +521 -0
- package/src/fs/errno.js +43 -0
- package/src/fs/graph.js +380 -0
- package/src/fs/overlay.js +1018 -0
- package/src/fs/pack-write.js +165 -0
- package/src/fs/pack.js +427 -0
- package/src/fs/seed.js +22 -0
- package/src/fs/svc-snapshot.js +65 -0
- package/src/fs/upper-pack.js +112 -0
- package/src/host/base-cache.js +195 -0
- package/src/host/base-file.js +328 -0
- package/src/host/base-io.js +93 -0
- package/src/host/boot-events.js +209 -0
- package/src/host/chan-service.js +300 -0
- package/src/host/chan-services.js +689 -0
- package/src/host/chan.js +655 -0
- package/src/host/ckpt-file.js +395 -0
- package/src/host/ckpt-io.js +207 -0
- package/src/host/ckpt-point.js +160 -0
- package/src/host/ckpt-save.js +64 -0
- package/src/host/ckpt.js +434 -0
- package/src/host/clock.js +111 -0
- package/src/host/ctl.js +778 -0
- package/src/host/errno.js +18 -0
- package/src/host/fs.js +167 -0
- package/src/host/gcnudge.js +83 -0
- package/src/host/i64shim.js +81 -0
- package/src/host/jit-cache.js +341 -0
- package/src/host/jit-hub-core.js +208 -0
- package/src/host/jit-hub.js +93 -0
- package/src/host/jit-snap.js +226 -0
- package/src/host/jit.js +1903 -0
- package/src/host/layout.js +100 -0
- package/src/host/monitor.js +110 -0
- package/src/host/p9mount.js +27 -0
- package/src/host/ram-mem.js +125 -0
- package/src/host/stdio.js +143 -0
- package/src/host/threads.js +109 -0
- package/src/host/wasi.js +335 -0
- package/src/host/wmd-imports.js +71 -0
- package/src/host/yamux.js +387 -0
- package/src/image-files.js +154 -0
- package/src/net/dhcp-dns.js +126 -0
- package/src/net/net-worker.js +172 -0
- package/src/net/packet.js +227 -0
- package/src/net/peer.js +230 -0
- package/src/net/proto.js +189 -0
- package/src/net/relay-client.js +226 -0
- package/src/net/stack.js +867 -0
- package/src/net/switch.js +160 -0
- package/src/net/wisp-client.js +570 -0
- package/src/net/wisp-transport.js +196 -0
- package/src/net/wisp.js +511 -0
- package/src/registry/client.js +176 -0
- package/src/registry/converter.js +68 -0
- package/src/registry/hub.js +221 -0
- package/src/registry/oci.js +76 -0
- package/src/registry/pack-worker.js +70 -0
- package/src/registry/pull.js +258 -0
- package/src/registry/ref.js +110 -0
- package/src/registry/wasi-mem.js +355 -0
- package/src/worker.js +315 -0
- package/wmdpack.wasm +0 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// WASI preview1 errno values (wasi_snapshot_preview1 `errno` enum).
|
|
2
|
+
export const E = Object.freeze({
|
|
3
|
+
SUCCESS: 0,
|
|
4
|
+
AGAIN: 6,
|
|
5
|
+
BADF: 8,
|
|
6
|
+
FAULT: 21,
|
|
7
|
+
INVAL: 28,
|
|
8
|
+
IO: 29,
|
|
9
|
+
ISDIR: 31,
|
|
10
|
+
NFILE: 41,
|
|
11
|
+
NOENT: 44,
|
|
12
|
+
NOSYS: 52,
|
|
13
|
+
NOTDIR: 54,
|
|
14
|
+
NOTSUP: 58,
|
|
15
|
+
ROFS: 69,
|
|
16
|
+
SPIPE: 70,
|
|
17
|
+
NOTCAPABLE: 76,
|
|
18
|
+
})
|
package/src/host/fs.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// A read-only, in-memory filesystem behind one WASI preopen, shared by all Workers.
|
|
2
|
+
//
|
|
3
|
+
// The file bytes live in SharedArrayBuffers (one per file) so every Worker reads the
|
|
4
|
+
// same memory without copies. The fd table is process-wide like the runner's
|
|
5
|
+
// (runner/src/wasi.rs FileTable behind a Mutex): it lives in the control buffer, so a
|
|
6
|
+
// file opened on one thread can be read on another. fds 0-2 are stdio, fd 3 is the
|
|
7
|
+
// preopen, files get 4.. (the runner hands out the next free number the same way).
|
|
8
|
+
|
|
9
|
+
import { CTL_FD_LOCK, FD_FILE, FD_KIND, FD_MAX, FD_POS_HI, FD_POS_LO, fdIndex } from './layout.js'
|
|
10
|
+
import { E } from './errno.js'
|
|
11
|
+
|
|
12
|
+
export const FILETYPE_CHARACTER_DEVICE = 2
|
|
13
|
+
export const FILETYPE_DIRECTORY = 3
|
|
14
|
+
export const FILETYPE_REGULAR_FILE = 4
|
|
15
|
+
|
|
16
|
+
export const PREOPEN_FD = 3
|
|
17
|
+
const FIRST_FILE_FD = 4
|
|
18
|
+
|
|
19
|
+
// Normalise a path relative to the preopen. Returns null when it climbs out.
|
|
20
|
+
export function normalizePath(p) {
|
|
21
|
+
const out = []
|
|
22
|
+
for (const part of p.split('/')) {
|
|
23
|
+
if (part === '' || part === '.') continue
|
|
24
|
+
if (part === '..') {
|
|
25
|
+
if (!out.length) return null
|
|
26
|
+
out.pop()
|
|
27
|
+
continue
|
|
28
|
+
}
|
|
29
|
+
out.push(part)
|
|
30
|
+
}
|
|
31
|
+
return out.join('/')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {object} o
|
|
36
|
+
* @param {SharedArrayBuffer} o.ctlSab control buffer (holds the fd table)
|
|
37
|
+
* @param {string} o.preopen guest name of the preopened directory, e.g. "/g"
|
|
38
|
+
* @param {{path:string, buffer:SharedArrayBuffer|ArrayBuffer}[]} o.files
|
|
39
|
+
*/
|
|
40
|
+
export function createFs({ ctlSab, preopen, files }) {
|
|
41
|
+
const ctl = new Int32Array(ctlSab)
|
|
42
|
+
const list = files.map(f => ({ path: normalizePath(f.path), bytes: new Uint8Array(f.buffer) }))
|
|
43
|
+
const byPath = new Map(list.map((f, i) => [f.path, i]))
|
|
44
|
+
const dirs = new Set([''])
|
|
45
|
+
for (const f of list) {
|
|
46
|
+
const parts = f.path.split('/')
|
|
47
|
+
for (let i = 1; i < parts.length; i++) dirs.add(parts.slice(0, i).join('/'))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function lock() {
|
|
51
|
+
while (Atomics.compareExchange(ctl, CTL_FD_LOCK, 0, 1) !== 0) {
|
|
52
|
+
/* spin: held for a few loads/stores only */
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const unlock = () => Atomics.store(ctl, CTL_FD_LOCK, 0)
|
|
56
|
+
|
|
57
|
+
const slot = fd => fd - FIRST_FILE_FD
|
|
58
|
+
const isFile = fd =>
|
|
59
|
+
fd >= FIRST_FILE_FD &&
|
|
60
|
+
slot(fd) < FD_MAX &&
|
|
61
|
+
Atomics.load(ctl, fdIndex(slot(fd), FD_KIND)) === 1
|
|
62
|
+
const getPos = s =>
|
|
63
|
+
(Atomics.load(ctl, fdIndex(s, FD_POS_HI)) >>> 0) * 2 ** 32 +
|
|
64
|
+
(Atomics.load(ctl, fdIndex(s, FD_POS_LO)) >>> 0)
|
|
65
|
+
const setPos = (s, pos) => {
|
|
66
|
+
Atomics.store(ctl, fdIndex(s, FD_POS_LO), pos % 2 ** 32 | 0)
|
|
67
|
+
Atomics.store(ctl, fdIndex(s, FD_POS_HI), Math.floor(pos / 2 ** 32) | 0)
|
|
68
|
+
}
|
|
69
|
+
const fileOf = fd => list[Atomics.load(ctl, fdIndex(slot(fd), FD_FILE))]
|
|
70
|
+
|
|
71
|
+
/** WASI filetype of an fd, or -1 when it is not open. */
|
|
72
|
+
function fdType(fd) {
|
|
73
|
+
if (fd >= 0 && fd <= 2) return FILETYPE_CHARACTER_DEVICE
|
|
74
|
+
if (fd === PREOPEN_FD) return FILETYPE_DIRECTORY
|
|
75
|
+
if (isFile(fd)) return FILETYPE_REGULAR_FILE
|
|
76
|
+
return -1
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function prestatName(fd) {
|
|
80
|
+
return fd === PREOPEN_FD ? preopen : null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** @returns {{errno:number, fd?:number}} */
|
|
84
|
+
function open(dirfd, path, oflags) {
|
|
85
|
+
if (dirfd !== PREOPEN_FD) return { errno: isFile(dirfd) || dirfd <= 2 ? E.NOTDIR : E.BADF }
|
|
86
|
+
const rel = normalizePath(path)
|
|
87
|
+
if (rel === null) return { errno: E.NOTCAPABLE }
|
|
88
|
+
const idx = byPath.get(rel)
|
|
89
|
+
if (idx === undefined) return { errno: dirs.has(rel) ? E.ISDIR : E.NOENT }
|
|
90
|
+
// Same as the runner: O_DIRECTORY (oflags bit 1) on a regular file is ENOTDIR.
|
|
91
|
+
if (oflags & 0x2) return { errno: E.NOTDIR }
|
|
92
|
+
// O_TRUNC / O_CREAT|O_EXCL on a read-only fs.
|
|
93
|
+
if (oflags & 0x8 || (oflags & 0x5) === 0x5) return { errno: E.ROFS }
|
|
94
|
+
lock()
|
|
95
|
+
try {
|
|
96
|
+
for (let s = 0; s < FD_MAX; s++) {
|
|
97
|
+
if (Atomics.load(ctl, fdIndex(s, FD_KIND)) !== 0) continue
|
|
98
|
+
Atomics.store(ctl, fdIndex(s, FD_FILE), idx)
|
|
99
|
+
setPos(s, 0)
|
|
100
|
+
Atomics.store(ctl, fdIndex(s, FD_KIND), 1)
|
|
101
|
+
return { errno: E.SUCCESS, fd: s + FIRST_FILE_FD }
|
|
102
|
+
}
|
|
103
|
+
return { errno: E.NFILE }
|
|
104
|
+
} finally {
|
|
105
|
+
unlock()
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function close(fd) {
|
|
110
|
+
if (fd >= 0 && fd <= PREOPEN_FD) return E.SUCCESS
|
|
111
|
+
if (!isFile(fd)) return E.BADF
|
|
112
|
+
Atomics.store(ctl, fdIndex(slot(fd), FD_KIND), 0)
|
|
113
|
+
return E.SUCCESS
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Copy up to dest.length bytes from the fd's position into dest.
|
|
118
|
+
* @returns {number} bytes copied, or -errno
|
|
119
|
+
*/
|
|
120
|
+
function read(fd, dest) {
|
|
121
|
+
if (fd === 0) return 0 // stdin: always EOF
|
|
122
|
+
if (!isFile(fd)) return -E.BADF
|
|
123
|
+
const s = slot(fd)
|
|
124
|
+
const f = fileOf(fd)
|
|
125
|
+
lock()
|
|
126
|
+
try {
|
|
127
|
+
const pos = getPos(s)
|
|
128
|
+
const n = Math.max(0, Math.min(dest.length, f.bytes.length - pos))
|
|
129
|
+
if (n) dest.set(f.bytes.subarray(pos, pos + n))
|
|
130
|
+
setPos(s, pos + n)
|
|
131
|
+
return n
|
|
132
|
+
} finally {
|
|
133
|
+
unlock()
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** @returns {{errno:number, pos?:number}} */
|
|
138
|
+
function seek(fd, offset, whence) {
|
|
139
|
+
if (fd >= 0 && fd <= PREOPEN_FD) return { errno: E.SPIPE }
|
|
140
|
+
if (!isFile(fd)) return { errno: E.BADF }
|
|
141
|
+
const s = slot(fd)
|
|
142
|
+
const size = fileOf(fd).bytes.length
|
|
143
|
+
lock()
|
|
144
|
+
try {
|
|
145
|
+
let base
|
|
146
|
+
if (whence === 0) base = 0
|
|
147
|
+
else if (whence === 1) base = getPos(s)
|
|
148
|
+
else if (whence === 2) base = size
|
|
149
|
+
else return { errno: E.INVAL }
|
|
150
|
+
const pos = base + Number(offset)
|
|
151
|
+
if (pos < 0 || !Number.isSafeInteger(pos)) return { errno: E.INVAL }
|
|
152
|
+
setPos(s, pos)
|
|
153
|
+
return { errno: E.SUCCESS, pos }
|
|
154
|
+
} finally {
|
|
155
|
+
unlock()
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** @returns {{type:number,size:number}|null} */
|
|
160
|
+
function stat(fd) {
|
|
161
|
+
const type = fdType(fd)
|
|
162
|
+
if (type < 0) return null
|
|
163
|
+
return { type, size: type === FILETYPE_REGULAR_FILE ? fileOf(fd).bytes.length : 0 }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { fdType, prestatName, open, close, read, seek, stat }
|
|
167
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// D-419: make WebKit collect the garbage of a thread that holds the guest's shared memory.
|
|
2
|
+
//
|
|
3
|
+
// JavaScriptCore sizes how much a heap may allocate before its next collection from the heap's
|
|
4
|
+
// size, and that size counts the shared WebAssembly.Memory the thread holds (gigabytes for
|
|
5
|
+
// vm.wasm). Small objects then never reach it: a Worker making 26,000 short-lived arrays a
|
|
6
|
+
// second grew the content process 0.95 MiB/s for ten minutes with no collection, whether the
|
|
7
|
+
// big Memory was the instance's or only kept alive beside it, and whether or not the Worker
|
|
8
|
+
// returned to its event loop; with 16 pages of memory it was flat. Every host call that
|
|
9
|
+
// allocates (a console write, a JIT compile, the page's own work) added to it for good.
|
|
10
|
+
//
|
|
11
|
+
// An ArrayBuffer's size counts toward the next collection the moment it is made. One of 2 GiB
|
|
12
|
+
// every 5 s, released at once (transfer(0)) and never written, so it costs no memory and about
|
|
13
|
+
// 0.02 ms, makes the collection happen: the same Worker stayed flat at vm.wasm's 3.9 GiB.
|
|
14
|
+
// Only JavaScriptCore needs it. Chromium's V8 collects such garbage on its own (per-Worker heaps
|
|
15
|
+
// stayed at 1-6 MiB) and refuses a 2 GiB ArrayBuffer.
|
|
16
|
+
|
|
17
|
+
export const NUDGE_BYTES = 2 ** 31
|
|
18
|
+
export const NUDGE_MS = 5000
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Whether this engine is JavaScriptCore: WebKit's own user agent (Safari, and every browser
|
|
22
|
+
* on iOS). Blink keeps AppleWebKit/537.36 in its user agent; Gecko has no AppleWebKit.
|
|
23
|
+
*/
|
|
24
|
+
export function needsGcNudge(ua = globalThis.navigator?.userAgent || '') {
|
|
25
|
+
return /AppleWebKit\//.test(ua) && !/AppleWebKit\/537\.36/.test(ua)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** One nudge. A refused allocation (no address space left) only skips this one. */
|
|
29
|
+
export function gcNudge() {
|
|
30
|
+
try {
|
|
31
|
+
const b = new ArrayBuffer(NUDGE_BYTES)
|
|
32
|
+
b.transfer?.(0) // release the reservation now, not at the collection
|
|
33
|
+
} catch {}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* For a thread that runs vm.wasm (it seldom returns to its event loop): a function to call on every host call,
|
|
38
|
+
* which nudges once NUDGE_MS has passed. Returns null where no nudge is needed.
|
|
39
|
+
* @param {{on?: boolean, now?: () => number, nudge?: () => void}} [o]
|
|
40
|
+
*/
|
|
41
|
+
export function gcNudger({ on = needsGcNudge(), now = () => performance.now(), nudge = gcNudge } = {}) {
|
|
42
|
+
if (!on) return null
|
|
43
|
+
let n = 0
|
|
44
|
+
let next = now() + NUDGE_MS
|
|
45
|
+
return () => {
|
|
46
|
+
if ((++n & 15) !== 0) return // a clock read every 16 calls is plenty: idle vCPUs call thousands of times a second
|
|
47
|
+
const t = now()
|
|
48
|
+
if (t < next) return
|
|
49
|
+
next = t + NUDGE_MS
|
|
50
|
+
nudge()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** For a thread with an event loop: nudge every NUDGE_MS. Returns a stop function. */
|
|
55
|
+
export function gcNudgeEvery({ on = needsGcNudge(), nudge = gcNudge } = {}) {
|
|
56
|
+
if (!on) return () => {}
|
|
57
|
+
const id = setInterval(nudge, NUDGE_MS)
|
|
58
|
+
return () => clearInterval(id)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* W32: the wmd.jit_yield import for a thread that runs vm.wasm under JSPI (JIT sharing: its
|
|
63
|
+
* entry points are promising; wrap the result in WebAssembly.Suspending). JavaScriptCore gives a
|
|
64
|
+
* dead Instance's code back only once this thread's heap is swept, and the sweep runs from the
|
|
65
|
+
* thread's event loop, which a vCPU or service thread inside vm.wasm never reaches: in WebKit,
|
|
66
|
+
* 83 MB of released JIT code stayed pinned while three idle vCPUs sat in wasm, nudged or not; a
|
|
67
|
+
* 0 ms yield once per second or once per 5 s gave it back (93 -> 2 MB, test/features/w32,
|
|
68
|
+
* docs/plans/w32-holder-results.md). vm.wasm calls it only where no JIT block is on its stack: a
|
|
69
|
+
* JSPI suspension under a block chain traps in WebKit ("JSPI stack scan failed: unrecognized type
|
|
70
|
+
* of Wasm frame: RestoreFrameMode"). Once per `every` ms a call nudges the collector and returns a
|
|
71
|
+
* Promise settled from a 0 ms timer, so the event loop runs once; every other call returns at once.
|
|
72
|
+
* @param {{every?: number, now?: () => number, nudge?: () => void, delay?: (fn: () => void) => void}} [o]
|
|
73
|
+
*/
|
|
74
|
+
export function jitYielder({ every = 2000, now = () => performance.now(), nudge = gcNudge, delay = fn => setTimeout(fn, 0) } = {}) {
|
|
75
|
+
let next = now() + every
|
|
76
|
+
return () => {
|
|
77
|
+
const t = now()
|
|
78
|
+
if (t < next) return
|
|
79
|
+
next = t + every
|
|
80
|
+
nudge()
|
|
81
|
+
return new Promise(r => delay(r))
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// D-419: vm.wasm's imports with i64 parameters, called through a small wasm module that hands
|
|
2
|
+
// JS each i64 as an f64 (f64.convert_i64_s), so no call makes a BigInt.
|
|
3
|
+
//
|
|
4
|
+
// The engine makes a BigInt for every i64 argument of a wasm-to-JS call, and WebKit collects
|
|
5
|
+
// garbage late in a thread holding a large shared memory (gigabytes, as vm.wasm's; gcnudge.js):
|
|
6
|
+
// 26,000 calls a second of an import with an i64 parameter grew the content process 0.62 MiB/s,
|
|
7
|
+
// with 64 KiB of shared memory or an i32-only import 0.0. An idle guest calls clock_time_get
|
|
8
|
+
// thousands of times a second. A wasm-to-wasm call makes nothing, and the f64 reaches JS as a
|
|
9
|
+
// Number.
|
|
10
|
+
//
|
|
11
|
+
// Every value these parameters carry is exact as an f64: clock precision (ignored), seek
|
|
12
|
+
// offsets (fs.seek wants a safe integer anyway), rights masks (under 2^30) and timeouts in ns
|
|
13
|
+
// (-1 or less than 2^53). Signed conversion: -1 stays -1.
|
|
14
|
+
|
|
15
|
+
/** name -> [module, params, results], the signatures vm.wasm imports (i = i32, I = i64). */
|
|
16
|
+
export const I64_IMPORTS = {
|
|
17
|
+
clock_time_get: ['wasi_snapshot_preview1', 'iIi', 'i'],
|
|
18
|
+
fd_seek: ['wasi_snapshot_preview1', 'iIii', 'i'],
|
|
19
|
+
path_open: ['wasi_snapshot_preview1', 'iiiiiIIii', 'i'],
|
|
20
|
+
chan_recv: ['wmd', 'iiiI', 'i'],
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const leb = n => {
|
|
24
|
+
const o = []
|
|
25
|
+
do {
|
|
26
|
+
let b = n & 0x7f
|
|
27
|
+
n >>>= 7
|
|
28
|
+
if (n) b |= 0x80
|
|
29
|
+
o.push(b)
|
|
30
|
+
} while (n)
|
|
31
|
+
return o
|
|
32
|
+
}
|
|
33
|
+
const vec = items => [...leb(items.length), ...items.flat()]
|
|
34
|
+
const str = s => vec([...new TextEncoder().encode(s)].map(b => [b]))
|
|
35
|
+
const sec = (id, body) => [id, ...leb(body.length), ...body]
|
|
36
|
+
const TY = { i: 0x7f, I: 0x7e, F: 0x7c }
|
|
37
|
+
|
|
38
|
+
/** The shim's bytes for these names: imports j.<name> (i64 as f64), exports <name> (as vm.wasm imports it). */
|
|
39
|
+
export function i64ShimBytes(names) {
|
|
40
|
+
const types = []
|
|
41
|
+
const imports = []
|
|
42
|
+
const funcs = []
|
|
43
|
+
const exports = []
|
|
44
|
+
const codes = []
|
|
45
|
+
names.forEach((name, k) => {
|
|
46
|
+
const [, params, results] = I64_IMPORTS[name]
|
|
47
|
+
const ft = (ps, rs) => [0x60, ...vec([...ps].map(c => [TY[c]])), ...vec([...rs].map(c => [TY[c]]))]
|
|
48
|
+
types.push(ft(params.replaceAll('I', 'F'), results), ft(params, results))
|
|
49
|
+
imports.push([...str('j'), ...str(name), 0x00, ...leb(2 * k)])
|
|
50
|
+
funcs.push(leb(2 * k + 1))
|
|
51
|
+
exports.push([...str(name), 0x00, ...leb(names.length + k)])
|
|
52
|
+
const body = [0] // no locals
|
|
53
|
+
;[...params].forEach((c, i) => body.push(0x20, ...leb(i), ...(c === 'I' ? [0xb9] : []))) // local.get; f64.convert_i64_s
|
|
54
|
+
body.push(0x10, ...leb(k), 0x0b) // call j.<name>; end
|
|
55
|
+
codes.push([...leb(body.length), ...body])
|
|
56
|
+
})
|
|
57
|
+
return new Uint8Array([
|
|
58
|
+
0, 0x61, 0x73, 0x6d, 1, 0, 0, 0,
|
|
59
|
+
...sec(1, vec(types)), ...sec(2, vec(imports)), ...sec(3, vec(funcs)), ...sec(7, vec(exports)), ...sec(10, vec(codes)),
|
|
60
|
+
])
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The import objects with each I64_IMPORTS function replaced by the shim's export, which calls
|
|
65
|
+
* the given JS function with Numbers. Modules and names not given are left as they are.
|
|
66
|
+
* @param {{[module: string]: {[name: string]: Function}}} imports
|
|
67
|
+
*/
|
|
68
|
+
export function shimI64Imports(imports) {
|
|
69
|
+
// a WebAssembly.Suspending import (W32: clock_time_get under JSPI) goes through the shim too
|
|
70
|
+
const isImport = f => typeof f === 'function' || (typeof WebAssembly.Suspending === 'function' && f instanceof WebAssembly.Suspending)
|
|
71
|
+
const names = Object.keys(I64_IMPORTS).filter(n => isImport(imports[I64_IMPORTS[n][0]]?.[n]))
|
|
72
|
+
if (!names.length) return imports
|
|
73
|
+
const j = Object.fromEntries(names.map(n => [n, imports[I64_IMPORTS[n][0]][n]]))
|
|
74
|
+
const { exports } = new WebAssembly.Instance(new WebAssembly.Module(i64ShimBytes(names)), { j })
|
|
75
|
+
const out = { ...imports }
|
|
76
|
+
for (const n of names) {
|
|
77
|
+
const m = I64_IMPORTS[n][0]
|
|
78
|
+
out[m] = { ...out[m], [n]: exports[n] }
|
|
79
|
+
}
|
|
80
|
+
return out
|
|
81
|
+
}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
// W77: the JIT's module bytes kept across visits, in a store the page supplies
|
|
2
|
+
// (bootWmd jit.cache: {get(key) -> Promise<Uint8Array|null>, put(key, bytes)}).
|
|
3
|
+
//
|
|
4
|
+
// The runner keeps the same thing on disk (D-295, runner/src/jit.rs known/note): vm.wasm
|
|
5
|
+
// asks wmd.jit_known(key) for every block it decodes and, on a hit, takes the function an
|
|
6
|
+
// earlier run compiled for that key instead of interpreting the block jit_threshold times
|
|
7
|
+
// and translating it again; wmd.jit_note tells the host which function each newly
|
|
8
|
+
// compiled block got. A block key (32 bytes, struct vm_known_rec) is the hash of the
|
|
9
|
+
// block's guest bytes, its pc, stage, mode, length and a hash of the emitter's settings;
|
|
10
|
+
// vm.wasm checks the span hash it gets back (ir_adopt), so a stale entry is only a miss.
|
|
11
|
+
//
|
|
12
|
+
// A compiled WebAssembly.Module cannot be stored (IndexedDB refuses it in Chromium,
|
|
13
|
+
// Firefox and WebKit: DataCloneError), so the store holds bytes, under two kinds of key,
|
|
14
|
+
// both under the vm.wasm build's sha256 (code from another build is never used):
|
|
15
|
+
//
|
|
16
|
+
// jit1/<vm sha256>/idx the index: every block key noted so far, with
|
|
17
|
+
// its module, function and span (encodeIndex)
|
|
18
|
+
// jit1/<vm sha256>/m/<hash64>-<len> one module's bytes, as jit_load/jit_compile got them
|
|
19
|
+
//
|
|
20
|
+
// jit_known runs for every decoded block (53,000 times in a boot) inside vm.wasm's cache
|
|
21
|
+
// lock, so a miss must answer at once: the page reads the index before the guest starts
|
|
22
|
+
// and hands every Worker a read-only table of it in a SharedArrayBuffer (buildKnown,
|
|
23
|
+
// knownLookup). Only a hit waits (JSPI, as jit_sync does for the hub): the Worker asks
|
|
24
|
+
// the page for that module's bytes once, and appends them as jit_load would; every later
|
|
25
|
+
// hit on the module in the same flush generation uses the record the first one made
|
|
26
|
+
// (a cell per module in the table, shared by the Workers).
|
|
27
|
+
//
|
|
28
|
+
// A module is put when a note first names one of its functions (jit_load modules that no
|
|
29
|
+
// block key names are not kept); the index is put a few seconds after new notes and when
|
|
30
|
+
// the machine ends (bootWmd waits for that last put, up to SAVE_WAIT_MS).
|
|
31
|
+
|
|
32
|
+
import { hash64 } from './jit.js'
|
|
33
|
+
|
|
34
|
+
export const CACHE_PREFIX = 'jit1'
|
|
35
|
+
export const INDEX_MAGIC = 0x3143_4a57 // "WJC1"
|
|
36
|
+
export const KEY_BYTES = 32 // struct vm_known_rec's key: hash, pc (u64), stage, mode, len, fp (u32)
|
|
37
|
+
export const NOTE_BYTES = 56 // a jit_note record: key, rlo, rlen, span (u64), tidx, pad
|
|
38
|
+
export const ENT_INTS = 6 // mod, func, rlo, rlen, spanLo, spanHi
|
|
39
|
+
export const MOD_INTS = 5 // hl, hh, len, loaded record + 1, its generation
|
|
40
|
+
export const M_LOADED = 3
|
|
41
|
+
export const M_GEN = 4
|
|
42
|
+
export const MAX_ENTRIES = 1 << 20 // index entries kept (past it new notes are dropped)
|
|
43
|
+
export const SAVE_DELAY_MS = 5000 // index put this long after the last new note
|
|
44
|
+
export const SAVE_WAIT_MS = 10_000 // bootWmd waits this long for the last puts when the machine ends
|
|
45
|
+
|
|
46
|
+
const hex8 = v => (v >>> 0).toString(16).padStart(8, '0')
|
|
47
|
+
export const indexKey = tag => `${CACHE_PREFIX}/${tag}/idx`
|
|
48
|
+
export const moduleKey = (tag, hl, hh, len) => `${CACHE_PREFIX}/${tag}/m/${hex8(hh)}${hex8(hl)}-${len >>> 0}`
|
|
49
|
+
const modId = (hl, hh, len) => `${hl >>> 0}:${hh >>> 0}:${len >>> 0}`
|
|
50
|
+
const keyHex = k => {
|
|
51
|
+
let s = ''
|
|
52
|
+
for (let i = 0; i < KEY_BYTES; i++) s += k[i].toString(16).padStart(2, '0')
|
|
53
|
+
return s
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The index blob: u32 magic, u32 module count M, u32 entry count N, M x (i32 hl, hh, len),
|
|
58
|
+
* N x (32-byte key, i32 mod, func, rlo, rlen, spanLo, spanHi). Little endian.
|
|
59
|
+
* @param {{mods:number[][], ents:{key:Uint8Array, v:number[]}[]}} ix
|
|
60
|
+
*/
|
|
61
|
+
export function encodeIndex(ix) {
|
|
62
|
+
const M = ix.mods.length
|
|
63
|
+
const N = ix.ents.length
|
|
64
|
+
const out = new Uint8Array(12 + M * 12 + N * (KEY_BYTES + ENT_INTS * 4))
|
|
65
|
+
const dv = new DataView(out.buffer)
|
|
66
|
+
dv.setUint32(0, INDEX_MAGIC, true)
|
|
67
|
+
dv.setUint32(4, M, true)
|
|
68
|
+
dv.setUint32(8, N, true)
|
|
69
|
+
let o = 12
|
|
70
|
+
for (const m of ix.mods) {
|
|
71
|
+
for (let i = 0; i < 3; i++) dv.setInt32(o + i * 4, m[i] | 0, true)
|
|
72
|
+
o += 12
|
|
73
|
+
}
|
|
74
|
+
for (const e of ix.ents) {
|
|
75
|
+
out.set(e.key, o)
|
|
76
|
+
o += KEY_BYTES
|
|
77
|
+
for (let i = 0; i < ENT_INTS; i++) dv.setInt32(o + i * 4, e.v[i] | 0, true)
|
|
78
|
+
o += ENT_INTS * 4
|
|
79
|
+
}
|
|
80
|
+
return out
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The inverse of encodeIndex; throws on a blob that is not one (the caller starts empty). */
|
|
84
|
+
export function decodeIndex(bytes) {
|
|
85
|
+
const b = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)
|
|
86
|
+
const dv = new DataView(b.buffer, b.byteOffset, b.byteLength)
|
|
87
|
+
if (b.byteLength < 12 || dv.getUint32(0, true) !== INDEX_MAGIC) throw new Error('not a JIT cache index')
|
|
88
|
+
const M = dv.getUint32(4, true)
|
|
89
|
+
const N = dv.getUint32(8, true)
|
|
90
|
+
if (b.byteLength !== 12 + M * 12 + N * (KEY_BYTES + ENT_INTS * 4)) throw new Error('JIT cache index: bad length')
|
|
91
|
+
const mods = []
|
|
92
|
+
let o = 12
|
|
93
|
+
for (let j = 0; j < M; j++, o += 12) mods.push([dv.getInt32(o, true), dv.getInt32(o + 4, true), dv.getInt32(o + 8, true)])
|
|
94
|
+
const ents = []
|
|
95
|
+
for (let j = 0; j < N; j++) {
|
|
96
|
+
const key = b.slice(o, o + KEY_BYTES)
|
|
97
|
+
o += KEY_BYTES
|
|
98
|
+
const v = []
|
|
99
|
+
for (let i = 0; i < ENT_INTS; i++, o += 4) v.push(dv.getInt32(o, true))
|
|
100
|
+
if (v[0] < 0 || v[0] >= M) throw new Error('JIT cache index: bad module number')
|
|
101
|
+
ents.push({ key, v })
|
|
102
|
+
}
|
|
103
|
+
return { mods, ents }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const slotOf = (k, i, mask) => {
|
|
107
|
+
// the key starts with the 64-bit hash of the block's bytes: its words are spread enough
|
|
108
|
+
const h = Math.imul(k[i] ^ k[i + 2], 0x9e3779b1) ^ k[i + 1] ^ k[i + 3]
|
|
109
|
+
return (h >>> 0) & mask
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The Workers' read-only table of an index: one SharedArrayBuffer with a header
|
|
114
|
+
* (capacity, entries, modules), the keys, the entries, the open-addressing slots
|
|
115
|
+
* (entry + 1) and one cell per module (MOD_INTS: hl, hh, len, and the record the first
|
|
116
|
+
* hit of this boot loaded it as, + 1, with that record's flush generation).
|
|
117
|
+
*/
|
|
118
|
+
export function buildKnown(ix) {
|
|
119
|
+
const N = ix.ents.length
|
|
120
|
+
const M = ix.mods.length
|
|
121
|
+
let cap = 16
|
|
122
|
+
while (cap < N * 2) cap *= 2
|
|
123
|
+
const ints = 4 + N * (KEY_BYTES / 4) + N * ENT_INTS + cap + M * MOD_INTS
|
|
124
|
+
const sab = new SharedArrayBuffer(ints * 4)
|
|
125
|
+
const I = new Int32Array(sab)
|
|
126
|
+
I[0] = cap
|
|
127
|
+
I[1] = N
|
|
128
|
+
I[2] = M
|
|
129
|
+
const kOff = 4
|
|
130
|
+
const eOff = kOff + N * (KEY_BYTES / 4)
|
|
131
|
+
const sOff = eOff + N * ENT_INTS
|
|
132
|
+
const mOff = sOff + cap
|
|
133
|
+
const K = new Int32Array(KEY_BYTES / 4)
|
|
134
|
+
const K8 = new Uint8Array(K.buffer)
|
|
135
|
+
for (let j = 0; j < N; j++) {
|
|
136
|
+
const e = ix.ents[j]
|
|
137
|
+
K8.set(e.key)
|
|
138
|
+
I.set(K, kOff + j * (KEY_BYTES / 4))
|
|
139
|
+
I.set(e.v, eOff + j * ENT_INTS)
|
|
140
|
+
let s = slotOf(K, 0, cap - 1)
|
|
141
|
+
while (I[sOff + s]) s = (s + 1) & (cap - 1)
|
|
142
|
+
I[sOff + s] = j + 1
|
|
143
|
+
}
|
|
144
|
+
for (let j = 0; j < M; j++) I.set(ix.mods[j], mOff + j * MOD_INTS)
|
|
145
|
+
return sab
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** A Worker's view of buildKnown's table. find(key Int32Array(8)) -> entry or -1. */
|
|
149
|
+
export function knownLookup(sab) {
|
|
150
|
+
const I = new Int32Array(sab)
|
|
151
|
+
const cap = I[0]
|
|
152
|
+
const N = I[1]
|
|
153
|
+
const M = I[2]
|
|
154
|
+
const kOff = 4
|
|
155
|
+
const eOff = kOff + N * (KEY_BYTES / 4)
|
|
156
|
+
const sOff = eOff + N * ENT_INTS
|
|
157
|
+
const mOff = sOff + cap
|
|
158
|
+
return {
|
|
159
|
+
entries: N,
|
|
160
|
+
modules: M,
|
|
161
|
+
find(K) {
|
|
162
|
+
if (!N) return -1
|
|
163
|
+
for (let s = slotOf(K, 0, cap - 1); ; s = (s + 1) & (cap - 1)) {
|
|
164
|
+
const j = I[sOff + s] - 1
|
|
165
|
+
if (j < 0) return -1
|
|
166
|
+
const b = kOff + j * (KEY_BYTES / 4)
|
|
167
|
+
let same = true
|
|
168
|
+
for (let i = 0; i < KEY_BYTES / 4; i++) {
|
|
169
|
+
if (I[b + i] !== K[i]) {
|
|
170
|
+
same = false
|
|
171
|
+
break
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (same) return j
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
/** [mod, func, rlo, rlen, spanLo, spanHi] */
|
|
178
|
+
entry: j => I.subarray(eOff + j * ENT_INTS, eOff + (j + 1) * ENT_INTS),
|
|
179
|
+
mod: m => I.subarray(mOff + m * MOD_INTS, mOff + m * MOD_INTS + 3),
|
|
180
|
+
/** the record + 1 and generation a hit of this boot loaded module m as (0 when none) */
|
|
181
|
+
loaded: m => [Atomics.load(I, mOff + m * MOD_INTS + M_LOADED), Atomics.load(I, mOff + m * MOD_INTS + M_GEN)],
|
|
182
|
+
setLoaded(m, rec, gen) {
|
|
183
|
+
Atomics.store(I, mOff + m * MOD_INTS + M_GEN, gen)
|
|
184
|
+
Atomics.store(I, mOff + m * MOD_INTS + M_LOADED, rec + 1)
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Checks the bytes a store returned for module m of the index before a Worker loads them. */
|
|
190
|
+
export function moduleBytesFit(bytes, hl, hh, len) {
|
|
191
|
+
if (!(bytes instanceof Uint8Array) || bytes.length !== len >>> 0) return false
|
|
192
|
+
const [a, b] = hash64(bytes)
|
|
193
|
+
return a === hl && b === hh
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The page's side: reads the index before the guest starts, answers the Workers' module
|
|
198
|
+
* reads, takes their notes and puts the modules they name and the index.
|
|
199
|
+
* @param {{cache:{get:Function, put:Function}, tag:string, log?:(s:string)=>void, saveDelayMs?:number}} o
|
|
200
|
+
*/
|
|
201
|
+
export function createJitCacheHost({ cache, tag, log = () => {}, saveDelayMs = SAVE_DELAY_MS }) {
|
|
202
|
+
if (!cache || typeof cache.get !== 'function' || typeof cache.put !== 'function') throw new Error('jit.cache needs get(key) and put(key, bytes)')
|
|
203
|
+
const ix = { mods: [], ents: [] }
|
|
204
|
+
const modIds = new Map() // modId -> number in ix.mods
|
|
205
|
+
const entIds = new Map() // keyHex -> number in ix.ents
|
|
206
|
+
const stored = new Set() // modIds the store has (from the index, or put by this boot)
|
|
207
|
+
const stats = { indexEntries: 0, indexModules: 0, indexBytes: 0, indexLoadMs: 0, gets: 0, getMisses: 0, getBytes: 0, puts: 0, putBytes: 0, notes: 0, newEntries: 0, indexPuts: 0, dropped: 0, errors: 0 }
|
|
208
|
+
let dirty = false
|
|
209
|
+
let timer = null
|
|
210
|
+
let chain = Promise.resolve() // puts in order: a module before the index that names it
|
|
211
|
+
let errLogged = false
|
|
212
|
+
const fail = (what, err) => {
|
|
213
|
+
stats.errors++
|
|
214
|
+
if (!errLogged) {
|
|
215
|
+
errLogged = true
|
|
216
|
+
log(`wmd-web: jit cache: ${what}: ${err && err.message ? err.message : err}`)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const queue = f => (chain = chain.then(f).catch(err => fail('put', err)))
|
|
220
|
+
|
|
221
|
+
function addMod(hl, hh, len) {
|
|
222
|
+
const id = modId(hl, hh, len)
|
|
223
|
+
let m = modIds.get(id)
|
|
224
|
+
if (m === undefined) {
|
|
225
|
+
m = ix.mods.length
|
|
226
|
+
ix.mods.push([hl | 0, hh | 0, len | 0])
|
|
227
|
+
modIds.set(id, m)
|
|
228
|
+
}
|
|
229
|
+
return m
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function saveSoon() {
|
|
233
|
+
dirty = true
|
|
234
|
+
if (timer || !saveDelayMs) return
|
|
235
|
+
timer = setTimeout(() => {
|
|
236
|
+
timer = null
|
|
237
|
+
saveIndex()
|
|
238
|
+
}, saveDelayMs)
|
|
239
|
+
}
|
|
240
|
+
function saveIndex() {
|
|
241
|
+
if (!dirty) return chain
|
|
242
|
+
dirty = false
|
|
243
|
+
const bytes = encodeIndex(ix)
|
|
244
|
+
return queue(async () => {
|
|
245
|
+
await cache.put(indexKey(tag), bytes)
|
|
246
|
+
stats.indexPuts++
|
|
247
|
+
stats.putBytes += bytes.length
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
stats,
|
|
253
|
+
/** Read the index; a missing or unreadable one is an empty cache. The table for the Workers. */
|
|
254
|
+
async load() {
|
|
255
|
+
const t = performance.now()
|
|
256
|
+
try {
|
|
257
|
+
const b = await cache.get(indexKey(tag))
|
|
258
|
+
if (b) {
|
|
259
|
+
const got = decodeIndex(b)
|
|
260
|
+
stats.indexBytes = b.byteLength
|
|
261
|
+
for (const m of got.mods) {
|
|
262
|
+
addMod(m[0], m[1], m[2])
|
|
263
|
+
stored.add(modId(m[0], m[1], m[2]))
|
|
264
|
+
}
|
|
265
|
+
for (const e of got.ents) {
|
|
266
|
+
entIds.set(keyHex(e.key), ix.ents.length)
|
|
267
|
+
ix.ents.push(e)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
} catch (err) {
|
|
271
|
+
fail('index', err)
|
|
272
|
+
}
|
|
273
|
+
stats.indexEntries = ix.ents.length
|
|
274
|
+
stats.indexModules = ix.mods.length
|
|
275
|
+
stats.indexLoadMs = Math.round(performance.now() - t)
|
|
276
|
+
return buildKnown(ix)
|
|
277
|
+
},
|
|
278
|
+
/** A Worker's read of module m's bytes (null: the store has none). */
|
|
279
|
+
async get(hl, hh, len) {
|
|
280
|
+
stats.gets++
|
|
281
|
+
try {
|
|
282
|
+
const b = await cache.get(moduleKey(tag, hl, hh, len))
|
|
283
|
+
if (!b) {
|
|
284
|
+
stats.getMisses++
|
|
285
|
+
return null
|
|
286
|
+
}
|
|
287
|
+
const copy = new Uint8Array(b) // the store's buffer is not ours to transfer
|
|
288
|
+
stats.getBytes += copy.length
|
|
289
|
+
return copy
|
|
290
|
+
} catch (err) {
|
|
291
|
+
fail('get', err)
|
|
292
|
+
return null
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
/**
|
|
296
|
+
* A Worker's notes: {mods:[{hl,hh,len,bytes?}], ents:[{key, mod (number in mods), func, rlo, rlen, spanLo, spanHi}]}.
|
|
297
|
+
* A module's bytes come with the first note that names it from that Worker.
|
|
298
|
+
*/
|
|
299
|
+
note({ mods, ents }) {
|
|
300
|
+
stats.notes += ents.length
|
|
301
|
+
const ids = mods.map(m => {
|
|
302
|
+
const id = modId(m.hl, m.hh, m.len)
|
|
303
|
+
if (m.bytes && !stored.has(id)) {
|
|
304
|
+
stored.add(id)
|
|
305
|
+
const bytes = m.bytes
|
|
306
|
+
queue(async () => {
|
|
307
|
+
await cache.put(moduleKey(tag, m.hl, m.hh, m.len), bytes)
|
|
308
|
+
stats.puts++
|
|
309
|
+
stats.putBytes += bytes.length
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
return stored.has(id) ? addMod(m.hl, m.hh, m.len) : -1
|
|
313
|
+
})
|
|
314
|
+
for (const e of ents) {
|
|
315
|
+
const m = ids[e.mod]
|
|
316
|
+
if (m === undefined || m < 0) continue // its bytes never reached the store
|
|
317
|
+
const k = keyHex(e.key)
|
|
318
|
+
const v = [m, e.func, e.rlo, e.rlen, e.spanLo, e.spanHi]
|
|
319
|
+
const j = entIds.get(k)
|
|
320
|
+
if (j !== undefined) {
|
|
321
|
+
ix.ents[j].v = v // a block compiled again (after a stale hit): the newest function
|
|
322
|
+
} else if (ix.ents.length >= MAX_ENTRIES) {
|
|
323
|
+
stats.dropped++
|
|
324
|
+
continue
|
|
325
|
+
} else {
|
|
326
|
+
entIds.set(k, ix.ents.length)
|
|
327
|
+
ix.ents.push({ key: e.key, v })
|
|
328
|
+
stats.newEntries++
|
|
329
|
+
}
|
|
330
|
+
saveSoon()
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
/** The machine ended: put the index (if it changed); resolves when every put is done or after waitMs. */
|
|
334
|
+
save(waitMs = SAVE_WAIT_MS) {
|
|
335
|
+
clearTimeout(timer)
|
|
336
|
+
timer = null
|
|
337
|
+
const done = saveIndex()
|
|
338
|
+
return Promise.race([done, new Promise(r => setTimeout(r, waitMs))])
|
|
339
|
+
},
|
|
340
|
+
}
|
|
341
|
+
}
|