@threadedstack/wmd-web 0.1.15 → 0.1.17
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 +4 -2
- package/RELEASE.json +40 -11
- package/guest/arm64/vm.wasm +0 -0
- package/package.json +3 -3
- package/src/boot.js +28 -9
- package/src/host/ckpt-point.js +55 -10
- package/src/host/ckpt.js +35 -2
- package/src/worker.js +3 -1
- package/wmdpack.wasm +0 -0
package/README.md
CHANGED
|
@@ -2594,8 +2594,10 @@ engines' memory64 limit), passes `WMD_RAM_FLAT=1` to vm.wasm and hands the memor
|
|
|
2594
2594
|
slack (`flatMaxPages`), whatever the guest's RAM. Leave `maxPages` out for such a boot.
|
|
2595
2595
|
- `flatRam: true|false` forces the layout on or off; unset, it is chosen for `--ram` above 1536 when the engine
|
|
2596
2596
|
has memory64 and `allowLargeRam` is not set. Safari has no memory64: the old limit and its message stay.
|
|
2597
|
-
-
|
|
2598
|
-
|
|
2597
|
+
- Checkpoints and restores work as for any guest: a checkpoint also holds the RAM memory's written pages (sections
|
|
2598
|
+
`ram-index` and `ram`, header `ramPages`) and restores only into a boot whose RAM memory has the same size.
|
|
2599
|
+
- A checkpoint, flat or not, reads only the RAM pages the guest has written (vm.wasm's `vm_touched_map`, a bit per
|
|
2600
|
+
64 KiB set before the first write): a page never written is not read, so the browser never backs it with memory.
|
|
2599
2601
|
|
|
2600
2602
|
Measured in Chromium (page/b7.js, `ram=`): 4096 shows `Mem: 3976`; 8192 with 4 vCPUs held 5366 MB in six
|
|
2601
2603
|
processes with every byte checked; the same work at 1536 (small) and 4096 (flat) took 35 s and 39 s.
|
package/RELEASE.json
CHANGED
|
@@ -1,9 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wmd-web",
|
|
3
3
|
"version": "0.3.2",
|
|
4
|
-
"commit": "
|
|
5
|
-
"previous":
|
|
4
|
+
"commit": "fa2b21cfd0fd0bf679498e6feeefc26c33f17372",
|
|
5
|
+
"previous": null,
|
|
6
6
|
"decisions": [
|
|
7
|
+
"D-388",
|
|
8
|
+
"D-395",
|
|
9
|
+
"D-408",
|
|
10
|
+
"D-415",
|
|
11
|
+
"D-416",
|
|
12
|
+
"D-417",
|
|
13
|
+
"D-419",
|
|
14
|
+
"D-423",
|
|
15
|
+
"D-428",
|
|
16
|
+
"D-430",
|
|
17
|
+
"D-432",
|
|
18
|
+
"D-433",
|
|
19
|
+
"D-438",
|
|
20
|
+
"D-442",
|
|
21
|
+
"D-443",
|
|
22
|
+
"D-444",
|
|
23
|
+
"D-445",
|
|
24
|
+
"D-446",
|
|
25
|
+
"D-447",
|
|
26
|
+
"D-448",
|
|
27
|
+
"D-449",
|
|
28
|
+
"D-451",
|
|
29
|
+
"D-453",
|
|
30
|
+
"D-454",
|
|
31
|
+
"D-468",
|
|
32
|
+
"D-476",
|
|
33
|
+
"D-477",
|
|
34
|
+
"D-481",
|
|
35
|
+
"D-482",
|
|
7
36
|
"D-486",
|
|
8
37
|
"D-488",
|
|
9
38
|
"D-494",
|
|
@@ -22,13 +51,13 @@
|
|
|
22
51
|
"D-544",
|
|
23
52
|
"D-545"
|
|
24
53
|
],
|
|
25
|
-
"vmSources": "
|
|
54
|
+
"vmSources": "c7f2793941170d96db37d9a12a8a5ad072fda7de",
|
|
26
55
|
"guest": {
|
|
27
56
|
"arch": "arm64",
|
|
28
57
|
"files": {
|
|
29
58
|
"vm.wasm": {
|
|
30
|
-
"sha256": "
|
|
31
|
-
"bytes":
|
|
59
|
+
"sha256": "2504f4071537e205d78766f2c84219fe49ea4709314becff3a314765733e7fdf",
|
|
60
|
+
"bytes": 1561314
|
|
32
61
|
},
|
|
33
62
|
"Image": {
|
|
34
63
|
"sha256": "a6b3209cf69d5bc1dacf0f3ed67f92c535dbb4108d67b47091c2aef87c6d7b04",
|
|
@@ -45,7 +74,7 @@
|
|
|
45
74
|
}
|
|
46
75
|
},
|
|
47
76
|
"files": {
|
|
48
|
-
"README.md": "
|
|
77
|
+
"README.md": "6873b42beaf44e02eded67fa54aaa2c13d766597048fbc8633909b6d77b049e0",
|
|
49
78
|
"export-page/coi-sw.js": "0300e670b413cb2e0aae35843fc583546bbf6687df971ab6df41cfa3b0d494d7",
|
|
50
79
|
"export-page/index.html": "f5450c89e63601bbbbffef421f03491b231d1f143070012c1c5315d4b0059f43",
|
|
51
80
|
"export-page/run.css": "b57adf1e76553549c1f89ff157b7973409dedc21889cd3932948cd05b51adc6f",
|
|
@@ -53,10 +82,10 @@
|
|
|
53
82
|
"guest/arm64/Image": "a6b3209cf69d5bc1dacf0f3ed67f92c535dbb4108d67b47091c2aef87c6d7b04",
|
|
54
83
|
"guest/arm64/Image-4k": "8ff6e338a0b42d9f774b35423e0e7f9d7f779c635446e8de2a3beb83d3518f8a",
|
|
55
84
|
"guest/arm64/initramfs.cpio.gz": "a2260db90e5d8acdf12a2123f696b4512a1465856d5653e20a85504069a3480c",
|
|
56
|
-
"guest/arm64/vm.wasm": "
|
|
85
|
+
"guest/arm64/vm.wasm": "2504f4071537e205d78766f2c84219fe49ea4709314becff3a314765733e7fdf",
|
|
57
86
|
"package.json": "17aa3a8406e8d2d85b647c426438d3fd75edcb59d83e257882227e7b93cccbc0",
|
|
58
87
|
"src/base.js": "982f8e46dbb727ec542a494ebdb884ccd19a31ff4e091425b2d1fc01b0d4754a",
|
|
59
|
-
"src/boot.js": "
|
|
88
|
+
"src/boot.js": "911a40e01fa4833249743653b7698e8fed217d52a34056a92524562f3f8daacc",
|
|
60
89
|
"src/calib-worker.js": "3d610591a1b3b58f59c3a2ab457f43c90727d95973cd43055b3b73b76180611d",
|
|
61
90
|
"src/fs/9p/codec.js": "69711eb4ebc84eff39256138169fe18f5a5066d6cd9e4ff2daf61c65380d7ef7",
|
|
62
91
|
"src/fs/9p/server.js": "da0ba00ebb5522161bb0ef7cd94bfc9616e342bf865633dfef0ba5bc67627d7a",
|
|
@@ -77,9 +106,9 @@
|
|
|
77
106
|
"src/host/chan.js": "43104dcdd4d17d13f55a71906b398bdb52d9e96c6fca239310f2ee1ccbb4d4ad",
|
|
78
107
|
"src/host/ckpt-file.js": "da7938fa640a062e77bdfb59caad5b3b1fcbf6150e3220de3be23f253f4c4497",
|
|
79
108
|
"src/host/ckpt-io.js": "3fbe74f0254c597f10d088c85e8ba72725502d86ae4dd8c6d2bf0821a93c873f",
|
|
80
|
-
"src/host/ckpt-point.js": "
|
|
109
|
+
"src/host/ckpt-point.js": "42bc16b3f754de3cd4e39f37a0feca2ffd8b4231cd72ec32bd6e1ea734c3c8c7",
|
|
81
110
|
"src/host/ckpt-save.js": "e222571033ae74a4439596c5e90e03b2aa9a5516665dd290f8dd243712294523",
|
|
82
|
-
"src/host/ckpt.js": "
|
|
111
|
+
"src/host/ckpt.js": "2155b5c8e6f609478805eb8a0b5d5210d47cdf35a80f79e0b9ae42b2c3170923",
|
|
83
112
|
"src/host/clock.js": "aba6af3a8b1a985ae340892222e4b482d33afe20f5c8b3a4c7403ce87d1e247c",
|
|
84
113
|
"src/host/ctl.js": "654c55c64efa3df4df8b7b28024a10bd0e7ed3afb89004f8a583b9e62f17f58f",
|
|
85
114
|
"src/host/errno.js": "26021d85962084be2c43c98fb3accafc2a8f11fbf3c8a5b8d484bd6a088ae4db",
|
|
@@ -120,6 +149,6 @@
|
|
|
120
149
|
"src/registry/pull.js": "66edf4602aca8d41cc68393d01a2da01c4667de7c493af5ae400e8f249d4e3ae",
|
|
121
150
|
"src/registry/ref.js": "24038e0fbcb6ac143cb8e8f7b9fac4dd0803d0891961786dc88dba01a4402e7b",
|
|
122
151
|
"src/registry/wasi-mem.js": "a7f684343010892460db27ee8c1e38e47e797c93dc4ebe4c30185dca6ca56ee2",
|
|
123
|
-
"src/worker.js": "
|
|
152
|
+
"src/worker.js": "fe23f1b99acedc65e56f92f7246923fdbdbefd085bae427b57cbe1402b4f5161"
|
|
124
153
|
}
|
|
125
154
|
}
|
package/guest/arm64/vm.wasm
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@threadedstack/wmd-web",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"homepage": "https://github.com/threadedstack/wmd-releases",
|
|
6
6
|
"repository": {
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
},
|
|
10
10
|
"description": "Browser host for wmd's guest vm.wasm (Web Workers + shared memory + WASI preview1)",
|
|
11
11
|
"wmd": {
|
|
12
|
-
"tag": "v0.1.
|
|
13
|
-
"commit": "
|
|
12
|
+
"tag": "v0.1.17",
|
|
13
|
+
"commit": "fa2b21cfd0fd0bf679498e6feeefc26c33f17372",
|
|
14
14
|
"web": "0.3.2"
|
|
15
15
|
},
|
|
16
16
|
"type": "module",
|
package/src/boot.js
CHANGED
|
@@ -531,14 +531,12 @@ export async function bootWmd(o) {
|
|
|
531
531
|
const flatRam = wantsFlatRam(o.args, o)
|
|
532
532
|
if (!flatRam) ramMiB(o.args, o.allowLargeRam) // a bad or too big --ram is a clear error before anything is downloaded
|
|
533
533
|
const maximum = o.maxPages || (flatRam ? flatMaxPages(o.args) : memoryMaxPages(o.args, 1024, o.allowLargeRam))
|
|
534
|
-
if (flatRam)
|
|
535
|
-
if (o.restore || o.restoreLazy) throw new Error(`--ram ${ramMiB(o.args, true)}: a guest with more than ${MAX_BROWSER_RAM_MIB} MiB cannot start from a checkpoint yet`)
|
|
536
|
-
env.push('WMD_RAM_FLAT=1')
|
|
537
|
-
}
|
|
534
|
+
if (flatRam) env.push('WMD_RAM_FLAT=1')
|
|
538
535
|
const ramMem = flatRam ? createRamMemory(ramMiB(o.args, true)) : null
|
|
536
|
+
/** The RAM memory's pages (0: the RAM is in memory 0). A checkpoint of a flat guest also holds its RAM memory's pages (sections ram-index, ram). */
|
|
537
|
+
const ramPages = ramMem ? ramMem.buffer.byteLength / CKPT_PAGE : 0
|
|
539
538
|
// P-B9: checkpoints. restoreIn: the checkpoint's parts (parseCheckpoint refuses a malformed one).
|
|
540
|
-
|
|
541
|
-
const ckptOn = !flatRam && (!!o.checkpoint || !!o.restore || !!o.restoreLazy)
|
|
539
|
+
const ckptOn = !!o.checkpoint || !!o.restore || !!o.restoreLazy
|
|
542
540
|
let restoreIn = o.restore ? parseCheckpoint(o.restore) : null
|
|
543
541
|
// P-B6: a base snapshot that is still downloading ({header: Promise, ckpt: Promise, abort()}, src/base.js openBase()). The
|
|
544
542
|
// boot downloads its own files meanwhile; the snapshot's header is checked against this boot's vm.wasm and boot key as soon
|
|
@@ -740,7 +738,7 @@ export async function bootWmd(o) {
|
|
|
740
738
|
const got = typeof box.take === 'function' ? box.take() : box // a box hands the snapshot over once: the opener keeps no reference to its 0.5 GB
|
|
741
739
|
ckptT.baseWaitMs = absMs() - tw // the time the boot waited for the snapshot after everything else was in
|
|
742
740
|
const parsed = parseCheckpoint(got)
|
|
743
|
-
checkCheckpointFits(parsed.header, { vmKey, bootKey, bootDesc, maxPages: maximum, hasJit: o.jit !== false, fmaFused: fmaFuses() })
|
|
741
|
+
checkCheckpointFits(parsed.header, { vmKey, bootKey, bootDesc, maxPages: maximum, ramPages, hasJit: o.jit !== false, fmaFused: fmaFuses() })
|
|
744
742
|
checkPacksFit(parsed.header.packs, packDigests)
|
|
745
743
|
restoreIn = parsed
|
|
746
744
|
lazyIn = null // baseLazy stays: accept() when the restore has succeeded, reject() if it fails
|
|
@@ -749,7 +747,7 @@ export async function bootWmd(o) {
|
|
|
749
747
|
baseRefused(err)
|
|
750
748
|
}
|
|
751
749
|
}
|
|
752
|
-
if (restoreIn) checkCheckpointFits(restoreIn.header, { vmKey, bootKey, bootDesc, maxPages: maximum, hasJit: o.jit !== false, fmaFused: fmaFuses() })
|
|
750
|
+
if (restoreIn) checkCheckpointFits(restoreIn.header, { vmKey, bootKey, bootDesc, maxPages: maximum, ramPages, hasJit: o.jit !== false, fmaFused: fmaFuses() })
|
|
753
751
|
}
|
|
754
752
|
if (signal?.aborted) throw abortError()
|
|
755
753
|
} catch (err) {
|
|
@@ -812,6 +810,26 @@ export async function bootWmd(o) {
|
|
|
812
810
|
kept[p] = 1
|
|
813
811
|
}
|
|
814
812
|
}
|
|
813
|
+
// the flat layout: the guest's RAM, into its own memory (a fresh one: all zero but for these pages)
|
|
814
|
+
if (restoreIn.ram) {
|
|
815
|
+
const ram = ramMem.buffer
|
|
816
|
+
const win = 16384 // pages per view: a RAM memory can be past the length of one typed array view
|
|
817
|
+
let at = -1
|
|
818
|
+
let v = null
|
|
819
|
+
let r = 0
|
|
820
|
+
for (const c of restoreIn.ram.chunks) {
|
|
821
|
+
for (let off = 0; off < c.length; off += CKPT_PAGE) {
|
|
822
|
+
const p = restoreIn.ram.index[r++]
|
|
823
|
+
const lo = p - (p % win)
|
|
824
|
+
if (lo !== at) {
|
|
825
|
+
at = lo
|
|
826
|
+
v = new Uint8Array(ram, lo * CKPT_PAGE, Math.min(win, ramPages - lo) * CKPT_PAGE)
|
|
827
|
+
}
|
|
828
|
+
v.set(c.subarray(off, off + CKPT_PAGE), (p - lo) * CKPT_PAGE)
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
ckptT.ramPagesRestored = r
|
|
832
|
+
}
|
|
815
833
|
// pages the instance's start function wrote that were zero at the checkpoint (the runner's zero_absent): the static region
|
|
816
834
|
let zeroed = 0
|
|
817
835
|
const w = new Int32Array(memory.buffer, 0, Math.min(restoreIn.header.memPages, VM_MIN_PAGES) * (CKPT_PAGE / 4))
|
|
@@ -845,6 +863,7 @@ export async function bootWmd(o) {
|
|
|
845
863
|
be.now('restore', { t: 'boot', stage: 'restore', state: 'done', used: true, bytes: baseBytes, total: baseTotal, source: baseSource, tookMs: Math.round(ckptT.pagesWrittenAt) })
|
|
846
864
|
restoreIn.memChunks = null
|
|
847
865
|
restoreIn.index = null
|
|
866
|
+
restoreIn.ram = null
|
|
848
867
|
restoreIn.jit = null
|
|
849
868
|
restoreIn.svc = null
|
|
850
869
|
restoreIn.ctl = null
|
|
@@ -1854,7 +1873,7 @@ export async function bootWmd(o) {
|
|
|
1854
1873
|
try {
|
|
1855
1874
|
// everything that can fail without harming the running machine comes first: the checkpoint parses and is this boot's
|
|
1856
1875
|
const parsed = parseCheckpoint(ckpt)
|
|
1857
|
-
checkCheckpointFits(parsed.header, { vmKey, bootKey, bootDesc, maxPages: maximum, hasJit: o.jit !== false, fmaFused: fmaFuses() })
|
|
1876
|
+
checkCheckpointFits(parsed.header, { vmKey, bootKey, bootDesc, maxPages: maximum, ramPages, hasJit: o.jit !== false, fmaFused: fmaFuses() })
|
|
1858
1877
|
if (!heldAssets) throw new CheckpointError('not-ready', 'this machine was booted without checkpoint support: it has no kept boot files to restore with')
|
|
1859
1878
|
validateFdTable(parsed.ctl, heldAssets.files.length) // the fd table is checked against this boot's files now, not after the old machine is gone
|
|
1860
1879
|
// the boot's own bytes (vm.wasm and its compiled Module, the kernel, the packs, the keys) are reused: nothing is
|
package/src/host/ckpt-point.js
CHANGED
|
@@ -30,27 +30,44 @@ export const REQ_CKPT = VM_POINT_CKPT
|
|
|
30
30
|
|
|
31
31
|
/** Pages per chunk of the memory section (32 MiB). */
|
|
32
32
|
export const CHUNK_PAGES = 512
|
|
33
|
+
/** Pages per view while a memory is scanned (1 GiB). */
|
|
34
|
+
export const WINDOW_PAGES = 16384
|
|
33
35
|
|
|
34
36
|
/**
|
|
35
37
|
* Non-zero pages of memory: {index: Uint32Array of page numbers, chunks: ArrayBuffer[] (pages
|
|
36
38
|
* back to back, CHUNK_PAGES each), pages (total), nonzero, scanMs, copyMs}. Reads the memory
|
|
37
39
|
* without atomics: the machine is frozen.
|
|
38
40
|
*/
|
|
39
|
-
export function scanMemory(memory, { now = () => performance.now(), chunkPages = CHUNK_PAGES, limitBytes = Infinity, verifyAbove = false } = {}) {
|
|
41
|
+
export function scanMemory(memory, { now = () => performance.now(), chunkPages = CHUNK_PAGES, limitBytes = Infinity, verifyAbove = false, skip = null } = {}) {
|
|
40
42
|
const buf = memory.buffer
|
|
41
43
|
// limitBytes: pages from here up are taken to be zero and are not read. Reading an untouched page of a shared
|
|
42
44
|
// memory commits it in WebKit (W34: a full walk of a 3968 MiB memory added about 2.2 GB of footprint), so a caller
|
|
43
45
|
// that knows where the guest's heap ends should say so; the page count of the checkpoint stays the memory's.
|
|
44
46
|
const total = buf.byteLength / PAGE
|
|
45
47
|
const pages = Math.min(total, Math.ceil(limitBytes / PAGE))
|
|
46
|
-
const w = new Int32Array(buf)
|
|
47
|
-
const u8 = new Uint8Array(buf)
|
|
48
48
|
const per = PAGE / 4
|
|
49
|
+
// views of at most WINDOW_PAGES pages: a guest RAM memory64 can be 16 GiB, past the length of one typed array view
|
|
50
|
+
let wLo = -1
|
|
51
|
+
let w = null
|
|
52
|
+
const words = p => {
|
|
53
|
+
const at = p - (p % WINDOW_PAGES)
|
|
54
|
+
if (at !== wLo) {
|
|
55
|
+
wLo = at
|
|
56
|
+
w = new Int32Array(buf, at * PAGE, Math.min(WINDOW_PAGES, total - at) * per)
|
|
57
|
+
}
|
|
58
|
+
return (p - at) * per
|
|
59
|
+
}
|
|
49
60
|
const t0 = now()
|
|
50
61
|
let idx = new Uint32Array(Math.min(pages, 1 << 14))
|
|
51
62
|
let n = 0
|
|
63
|
+
let skipped = 0
|
|
52
64
|
for (let p = 0; p < pages; p++) {
|
|
53
|
-
|
|
65
|
+
// a page the guest never wrote is zero: it is not read (reading it would make the host back it with memory)
|
|
66
|
+
if (skip && skip(p)) {
|
|
67
|
+
skipped++
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
const lo = words(p)
|
|
54
71
|
const hi = lo + per
|
|
55
72
|
let nz = false
|
|
56
73
|
for (let i = lo; i < hi; i += 8) {
|
|
@@ -72,7 +89,7 @@ export function scanMemory(memory, { now = () => performance.now(), chunkPages =
|
|
|
72
89
|
let aboveNonzero = 0
|
|
73
90
|
if (verifyAbove) {
|
|
74
91
|
for (let p = pages; p < total; p++) {
|
|
75
|
-
const lo = p
|
|
92
|
+
const lo = words(p)
|
|
76
93
|
for (let i = lo; i < lo + per; i += 8) {
|
|
77
94
|
if ((w[i] | w[i + 1] | w[i + 2] | w[i + 3] | w[i + 4] | w[i + 5] | w[i + 6] | w[i + 7]) !== 0) {
|
|
78
95
|
aboveNonzero++
|
|
@@ -86,10 +103,32 @@ export function scanMemory(memory, { now = () => performance.now(), chunkPages =
|
|
|
86
103
|
for (let k = 0; k < n; k += chunkPages) {
|
|
87
104
|
const m = Math.min(chunkPages, n - k)
|
|
88
105
|
const c = new Uint8Array(m * PAGE)
|
|
89
|
-
for (let j = 0; j < m; j++) c.set(
|
|
106
|
+
for (let j = 0; j < m; j++) c.set(new Uint8Array(buf, idx[k + j] * PAGE, PAGE), j * PAGE)
|
|
90
107
|
chunks.push(c.buffer)
|
|
91
108
|
}
|
|
92
|
-
return { index: idx.slice(0, n), chunks, pages: total, scannedPages: pages, aboveNonzero, nonzero: n, scanMs: t1 - t0, copyMs: now() - t1 }
|
|
109
|
+
return { index: idx.slice(0, n), chunks, pages: total, scannedPages: pages - skipped, skippedPages: skipped, aboveNonzero, nonzero: n, scanMs: t1 - t0, copyMs: now() - t1 }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The pages a checkpoint need not read, from vm.wasm's vm_touched_map (guest/vm/include/vm/ram.h): the RAM pages the guest never
|
|
114
|
+
* wrote. `desc` is the address of its six u64s: [bitmap address, bitmap pages, memory-0 offset of RAM (or of the flat layout's boot
|
|
115
|
+
* stand-in), its length, RAM bytes, flat]. Returns {mem0(p), ram(p)}: whether page p of memory 0, or of the RAM memory, is skipped.
|
|
116
|
+
*/
|
|
117
|
+
export function touchedSkips(memory, desc) {
|
|
118
|
+
const d = new BigUint64Array(memory.buffer, desc, 6)
|
|
119
|
+
const bits = new Uint8Array(memory.buffer, Number(d[0]), Number(d[1]) / 8)
|
|
120
|
+
const nbits = Number(d[1])
|
|
121
|
+
const lowOff = Number(d[2])
|
|
122
|
+
const lowLen = Number(d[3])
|
|
123
|
+
const flat = d[5] !== 0n
|
|
124
|
+
const unset = i => i >= nbits || !(bits[i >> 3] & (1 << (i & 7)))
|
|
125
|
+
const first = Math.ceil(lowOff / PAGE)
|
|
126
|
+
const end = Math.floor((lowOff + lowLen) / PAGE) // pages wholly inside the range
|
|
127
|
+
return {
|
|
128
|
+
// memory 0: a page inside guest RAM that was never written; with the flat layout the boot stand-in is never read again
|
|
129
|
+
mem0: p => p >= first && p < end && (flat || unset(p - lowOff / PAGE)),
|
|
130
|
+
ram: p => unset(p),
|
|
131
|
+
}
|
|
93
132
|
}
|
|
94
133
|
|
|
95
134
|
/**
|
|
@@ -97,7 +136,7 @@ export function scanMemory(memory, { now = () => performance.now(), chunkPages =
|
|
|
97
136
|
* message to the page; goTimeoutMs: how long vCPU 0 waits for the page (a page that is gone
|
|
98
137
|
* makes the point fail: the guest goes on, as it does for any failed point).
|
|
99
138
|
*/
|
|
100
|
-
export function createCkptImports({ memory, ctl, post, scanLimit = null, verifyBound = false, useHeapTop = false, heapTop = () => null, goTimeoutMs = 60_000, now = () => performance.timeOrigin + performance.now() }) {
|
|
139
|
+
export function createCkptImports({ memory, ram = null, touched = () => null, ctl, post, scanLimit = null, verifyBound = false, useHeapTop = false, heapTop = () => null, goTimeoutMs = 60_000, now = () => performance.timeOrigin + performance.now() }) {
|
|
101
140
|
return {
|
|
102
141
|
ctl_attach(wordAddr) {
|
|
103
142
|
Atomics.store(ctl, CTL_CK_HOSTREQ, wordAddr >>> 0)
|
|
@@ -124,7 +163,11 @@ export function createCkptImports({ memory, ctl, post, scanLimit = null, verifyB
|
|
|
124
163
|
// verifyBound found no non-zero page above it
|
|
125
164
|
const top = scanLimit ?? (useHeapTop ? heapTop() : null)
|
|
126
165
|
const limit = typeof top === 'number' && top > 0 ? Math.min(top, memory.buffer.byteLength) : Infinity
|
|
127
|
-
|
|
166
|
+
const desc = touched()
|
|
167
|
+
const skips = desc ? touchedSkips(memory, desc) : null
|
|
168
|
+
// RAM in memory 0 starts at a 64 KiB boundary (memory.grow), so a skipped page is all RAM
|
|
169
|
+
if (skips && (Number(new BigUint64Array(memory.buffer, desc, 6)[2]) % PAGE) !== 0) throw new Error('guest RAM is not page-aligned in memory 0')
|
|
170
|
+
cap = scanMemory(memory, { now, limitBytes: limit, verifyAbove: verifyBound, skip: skips ? skips.mem0 : null })
|
|
128
171
|
cap.limitBytes = Number.isFinite(limit) ? limit : null
|
|
129
172
|
// the live g_rsv {brk, end} of vm.wasm 50c6495e / 3aef93fc (linear address 4251160), next to what vm_heap_top() says
|
|
130
173
|
try {
|
|
@@ -133,13 +176,15 @@ export function createCkptImports({ memory, ctl, post, scanLimit = null, verifyB
|
|
|
133
176
|
cap.endLive = Number(g[1])
|
|
134
177
|
} catch {}
|
|
135
178
|
cap.heapTopRet = heapTop()
|
|
179
|
+
// the flat layout: guest RAM is a memory of its own (host/ram-mem.js), every page of it the guest's
|
|
180
|
+
if (ram) cap.ram = scanMemory(ram, { now, skip: skips ? skips.ram : null })
|
|
136
181
|
} catch (err) {
|
|
137
182
|
post({ t: 'ckpt-mem-error', message: String(err && err.message ? err.message : err) })
|
|
138
183
|
Atomics.wait(ctl, CTL_CK_GO, 0, goTimeoutMs)
|
|
139
184
|
Atomics.store(ctl, CTL_CK_GO, 0)
|
|
140
185
|
return -12
|
|
141
186
|
}
|
|
142
|
-
post({ t: 'ckpt-mem', index: cap.index, chunks: cap.chunks, pages: cap.pages, scannedPages: cap.scannedPages, aboveNonzero: cap.aboveNonzero, brkLive: cap.brkLive, endLive: cap.endLive, heapTopRet: cap.heapTopRet, limitBytes: cap.limitBytes, nonzero: cap.nonzero, scanMs: cap.scanMs, copyMs: cap.copyMs, doneAt: now() }, [cap.index.buffer, ...cap.chunks])
|
|
187
|
+
post({ t: 'ckpt-mem', index: cap.index, chunks: cap.chunks, pages: cap.pages, scannedPages: cap.scannedPages, skippedPages: cap.skippedPages, ramScannedPages: cap.ram?.scannedPages, ramSkippedPages: cap.ram?.skippedPages, aboveNonzero: cap.aboveNonzero, brkLive: cap.brkLive, endLive: cap.endLive, heapTopRet: cap.heapTopRet, limitBytes: cap.limitBytes, nonzero: cap.nonzero, scanMs: cap.scanMs, copyMs: cap.copyMs, ram: cap.ram ? { index: cap.ram.index, chunks: cap.ram.chunks, pages: cap.ram.pages, nonzero: cap.ram.nonzero, scanMs: cap.ram.scanMs, copyMs: cap.ram.copyMs } : null, doneAt: now() }, [cap.index.buffer, ...cap.chunks, ...(cap.ram ? [cap.ram.index.buffer, ...cap.ram.chunks] : [])])
|
|
143
188
|
const r = Atomics.wait(ctl, CTL_CK_GO, 0, goTimeoutMs)
|
|
144
189
|
const go = Atomics.load(ctl, CTL_CK_GO)
|
|
145
190
|
Atomics.store(ctl, CTL_CK_GO, 0)
|
package/src/host/ckpt.js
CHANGED
|
@@ -154,6 +154,11 @@ export function assembleCheckpoint(p) {
|
|
|
154
154
|
const sections = []
|
|
155
155
|
sections.push({ name: 'mem-index', chunks: [new Uint8Array(p.mem.index.buffer, p.mem.index.byteOffset, p.mem.index.byteLength)] })
|
|
156
156
|
sections.push({ name: 'mem', chunks: p.mem.chunks.map(c => new Uint8Array(c)) })
|
|
157
|
+
// the flat layout: the guest's RAM is a memory of its own, saved as its non-zero pages the same way
|
|
158
|
+
if (p.mem.ram) {
|
|
159
|
+
sections.push({ name: 'ram-index', chunks: [new Uint8Array(p.mem.ram.index.buffer, p.mem.ram.index.byteOffset, p.mem.ram.index.byteLength)] })
|
|
160
|
+
sections.push({ name: 'ram', chunks: p.mem.ram.chunks.map(c => new Uint8Array(c)) })
|
|
161
|
+
}
|
|
157
162
|
const header = {
|
|
158
163
|
...p.meta,
|
|
159
164
|
format: FORMAT_VERSION,
|
|
@@ -164,6 +169,10 @@ export function assembleCheckpoint(p) {
|
|
|
164
169
|
guestHdr: b64(p.hdr),
|
|
165
170
|
svcPorts: Object.keys(p.svc).map(Number),
|
|
166
171
|
}
|
|
172
|
+
if (p.mem.ram) {
|
|
173
|
+
header.ramPages = p.mem.ram.pages
|
|
174
|
+
header.ramNonzero = p.mem.ram.nonzero
|
|
175
|
+
}
|
|
167
176
|
if (p.jit) {
|
|
168
177
|
sections.push({ name: 'jit-recs', chunks: [new Uint8Array(p.jit.snap.recs.buffer, p.jit.snap.recs.byteOffset, p.jit.snap.recs.byteLength)] })
|
|
169
178
|
sections.push({ name: 'jit-log', chunks: concatChunks([p.jit.snap.log], 32 << 20) })
|
|
@@ -198,6 +207,7 @@ export function parseCheckpoint(ck) {
|
|
|
198
207
|
if (index[i] >= h.memPages) throw new CheckpointError('malformed', `page ${index[i]} is past the ${h.memPages} pages of memory`)
|
|
199
208
|
if (i && index[i] <= index[i - 1]) throw new CheckpointError('malformed', `the page list is not strictly increasing at entry ${i} (page ${index[i]} after ${index[i - 1]}): a duplicate or unordered page`)
|
|
200
209
|
}
|
|
210
|
+
const ram = h.ramPages ? pagesOf(get, 'ram', h.ramPages) : null
|
|
201
211
|
let jit = null
|
|
202
212
|
if (h.jit) {
|
|
203
213
|
const recs = concat(get('jit-recs'))
|
|
@@ -213,7 +223,26 @@ export function parseCheckpoint(ck) {
|
|
|
213
223
|
const svc = {}
|
|
214
224
|
for (const port of h.svcPorts || []) svc[port] = concat(get(`svc-${port}`))
|
|
215
225
|
const ctlBytes = concat(get('ctl'))
|
|
216
|
-
return { header: h, index, memChunks, jit, svc, ctl: new Uint8Array(ctlBytes), names: parseNames(ck) }
|
|
226
|
+
return { header: h, index, memChunks, ram, jit, svc, ctl: new Uint8Array(ctlBytes), names: parseNames(ck) }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** The flat layout's RAM sections (`<name>-index` and `<name>`): {index, chunks}, checked as the memory's are. */
|
|
230
|
+
function pagesOf(get, name, total) {
|
|
231
|
+
if (!Number.isInteger(total) || total <= 0) throw new CheckpointError('malformed', `${name}Pages is ${total}`)
|
|
232
|
+
const idxBytes = concat(get(`${name}-index`))
|
|
233
|
+
if (idxBytes.length % 4) throw new CheckpointError('malformed', `${name}-index is not a list of 32-bit page numbers`)
|
|
234
|
+
const index = new Uint32Array(idxBytes.buffer.slice(idxBytes.byteOffset, idxBytes.byteOffset + idxBytes.length))
|
|
235
|
+
const chunks = get(name)
|
|
236
|
+
const have = chunks.reduce((n, c) => n + c.length, 0)
|
|
237
|
+
if (have !== index.length * PAGE) throw new CheckpointError('malformed', `the ${name} section holds ${have} bytes for ${index.length} pages`)
|
|
238
|
+
chunks.forEach((c, i) => {
|
|
239
|
+
if (c.length % PAGE) throw new CheckpointError('malformed', `${name} chunk ${i} is ${c.length} bytes, not a whole number of ${PAGE}-byte pages`)
|
|
240
|
+
})
|
|
241
|
+
for (let i = 0; i < index.length; i++) {
|
|
242
|
+
if (index[i] >= total) throw new CheckpointError('malformed', `${name} page ${index[i]} is past its ${total} pages`)
|
|
243
|
+
if (i && index[i] <= index[i - 1]) throw new CheckpointError('malformed', `the ${name} page list is not strictly increasing at entry ${i}`)
|
|
244
|
+
}
|
|
245
|
+
return { pages: total, index, chunks }
|
|
217
246
|
}
|
|
218
247
|
|
|
219
248
|
// no leading zeros: "198.18.07.1" and "198.18.7.1" are one address, and the map keeps each address once
|
|
@@ -266,7 +295,7 @@ export function checkVmKeyFits(h, vmKey) {
|
|
|
266
295
|
}
|
|
267
296
|
}
|
|
268
297
|
|
|
269
|
-
export function checkCheckpointFits(h, { vmKey, bootKey, bootDesc, maxPages, hasJit, fmaFused = true }) {
|
|
298
|
+
export function checkCheckpointFits(h, { vmKey, bootKey, bootDesc, maxPages, ramPages = 0, hasJit, fmaFused = true }) {
|
|
270
299
|
if (h.format !== FORMAT_VERSION) throw new CheckpointError('version', `checkpoint format ${h.format}; this build restores format ${FORMAT_VERSION}`)
|
|
271
300
|
if (h.vmKey !== vmKey) {
|
|
272
301
|
throw new CheckpointError('stale', `this checkpoint was taken with a different vm.wasm (${String(h.vmKey).slice(0, 12)}, this boot has ${String(vmKey).slice(0, 12)}): its memory image is the old emulator's and cannot be restored; take a new one`)
|
|
@@ -276,6 +305,10 @@ export function checkCheckpointFits(h, { vmKey, bootKey, bootDesc, maxPages, has
|
|
|
276
305
|
throw new CheckpointError('stale', `this checkpoint was taken by a boot with a different kernel, initramfs, command line, environment, image layers or bundle files than this one; restore it only into the same configuration${diff ? ` (differs: ${diff})` : ''}`)
|
|
277
306
|
}
|
|
278
307
|
if (h.memPages > maxPages) throw new CheckpointError('stale', `this checkpoint's memory is ${h.memPages} pages and this boot's maximum is ${maxPages}`)
|
|
308
|
+
// the flat layout: the guest's RAM is restored into a memory of exactly its size
|
|
309
|
+
if ((h.ramPages || 0) !== ramPages) {
|
|
310
|
+
throw new CheckpointError('stale', h.ramPages ? `this checkpoint holds ${h.ramPages / 16} MiB of guest RAM in a memory of its own and this boot ${ramPages ? `has ${ramPages / 16} MiB` : 'keeps its RAM in memory 0'}` : 'this checkpoint keeps the guest\'s RAM in memory 0 and this boot keeps it in a memory of its own')
|
|
311
|
+
}
|
|
279
312
|
if (h.jit && !hasJit) throw new CheckpointError('stale', 'this checkpoint carries JIT state and this boot has the JIT door off (jit: false)')
|
|
280
313
|
if (!h.jit && hasJit) throw new CheckpointError('stale', 'this checkpoint was taken without the JIT door and this boot has it on')
|
|
281
314
|
// D-539: its JIT blocks may use relaxed_madd, which this engine does not fuse or parse
|
package/src/worker.js
CHANGED
|
@@ -166,8 +166,9 @@ async function run(m) {
|
|
|
166
166
|
// P-B3: channels over the chan block's rings, when the boot has channel services.
|
|
167
167
|
const chan = m.chan ? createChanImports({ sab: m.chan, memory, exiting: () => Atomics.load(ctl, CTL_EXIT) !== 0 }) : {}
|
|
168
168
|
let heapTopFn = null // vm.wasm's vm_heap_top export, once the main thread has instantiated (null: a full scan)
|
|
169
|
+
let touchedFn = null // vm.wasm's vm_touched_map export: the RAM pages the guest wrote (null: every page is read)
|
|
169
170
|
// P-B9: checkpoint support: wmd.ctl_attach and wmd.point (host/ckpt-point.js)
|
|
170
|
-
const ckpt = m.ckpt ? createCkptImports({ memory, ctl, scanLimit: m.ckptScanLimit ?? null, verifyBound: !!m.ckptVerifyBound, useHeapTop: !!m.ckptUseHeapTop, heapTop: () => heapTopFn?.() ?? null, post: (msg, transfer) => self.postMessage(msg, transfer || []) }) : null
|
|
171
|
+
const ckpt = m.ckpt ? createCkptImports({ memory, ram: m.ram || null, ctl, scanLimit: m.ckptScanLimit ?? null, verifyBound: !!m.ckptVerifyBound, useHeapTop: !!m.ckptUseHeapTop, heapTop: () => heapTopFn?.() ?? null, touched: () => touchedFn?.() ?? null, post: (msg, transfer) => self.postMessage(msg, transfer || []) }) : null
|
|
171
172
|
// the flat layout: guest RAM is the page's memory64 (host/ram-mem.js)
|
|
172
173
|
const ramImports = m.ram ? createRamImports({ memory, ram: m.ram }) : null
|
|
173
174
|
const wmd = createWmdImports({
|
|
@@ -250,6 +251,7 @@ async function run(m) {
|
|
|
250
251
|
if (role === 'main') {
|
|
251
252
|
const inst = instantiate()
|
|
252
253
|
if (typeof inst.exports.vm_heap_top === 'function') heapTopFn = () => Number(inst.exports.vm_heap_top())
|
|
254
|
+
if (typeof inst.exports.vm_touched_map === 'function') touchedFn = () => inst.exports.vm_touched_map() >>> 0
|
|
253
255
|
self.postMessage({ t: 'ready', role, at: performance.timeOrigin + performance.now() })
|
|
254
256
|
try {
|
|
255
257
|
if (m.resume) {
|
package/wmdpack.wasm
CHANGED
|
Binary file
|