@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 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
- - No checkpoint yet: `checkpoint: true` is ignored and `restore` / `restoreLazy` are refused with a message
2598
- (a checkpoint holds memory 0 only).
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": "b591b664091fb3105d0cd0f341c56fe04131ff2c",
5
- "previous": "wmd-web-v0.1.0",
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": "f638fbdada4ccae26c12334b851e12244c1985d8",
54
+ "vmSources": "c7f2793941170d96db37d9a12a8a5ad072fda7de",
26
55
  "guest": {
27
56
  "arch": "arm64",
28
57
  "files": {
29
58
  "vm.wasm": {
30
- "sha256": "f7affc474658fdfdc50e96c29a224eed1520ea69b426d6427fbc058eb141091a",
31
- "bytes": 1548267
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": "b9edbf3db9576f7016df93dee8e2b197071655518f90363fe2dafe2a97b03a3d",
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": "f7affc474658fdfdc50e96c29a224eed1520ea69b426d6427fbc058eb141091a",
85
+ "guest/arm64/vm.wasm": "2504f4071537e205d78766f2c84219fe49ea4709314becff3a314765733e7fdf",
57
86
  "package.json": "17aa3a8406e8d2d85b647c426438d3fd75edcb59d83e257882227e7b93cccbc0",
58
87
  "src/base.js": "982f8e46dbb727ec542a494ebdb884ccd19a31ff4e091425b2d1fc01b0d4754a",
59
- "src/boot.js": "114f9cf0561bca17b042c8db2e72e6984f7a55fd8d19700730895935f226cc77",
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": "3cc9ec778f131fbfd17a788709f01f096719f3ef81490a7be36a7fc1a422aef4",
109
+ "src/host/ckpt-point.js": "42bc16b3f754de3cd4e39f37a0feca2ffd8b4231cd72ec32bd6e1ea734c3c8c7",
81
110
  "src/host/ckpt-save.js": "e222571033ae74a4439596c5e90e03b2aa9a5516665dd290f8dd243712294523",
82
- "src/host/ckpt.js": "17c3e3f39c7e4ab9b51a4d121d1017d943257f95357330d8f21140011562d0db",
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": "dda8e216ff774fc6d31ccb55c265f1c560243f872b57bc0affe3075b94bd5b4f"
152
+ "src/worker.js": "fe23f1b99acedc65e56f92f7246923fdbdbefd085bae427b57cbe1402b4f5161"
124
153
  }
125
154
  }
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadedstack/wmd-web",
3
- "version": "0.1.15",
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.15",
13
- "commit": "b591b664091fb3105d0cd0f341c56fe04131ff2c",
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
- // A flat guest takes none yet: a checkpoint holds memory 0 only.
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
@@ -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
- const lo = p * per
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 * per
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(u8.subarray(idx[k + j] * PAGE, (idx[k + j] + 1) * PAGE), j * PAGE)
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
- cap = scanMemory(memory, { now, limitBytes: limit, verifyAbove: verifyBound })
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