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