@threadedstack/wmd-web 0.1.16 → 0.1.18

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/src/boot.js CHANGED
@@ -51,9 +51,11 @@ import {
51
51
  } from './host/ckpt.js'
52
52
  import { captureJit, jitSnapStats, modulesOf, ownersNeedingHub, restoreStore } from './host/jit-snap.js'
53
53
  export { CheckpointError, memoryStore, opfsStore } from './host/ckpt.js'
54
+ export { DiskError, deleteDisk, openDisk, sweepDisks } from './host/disk.js'
54
55
  export { NetSwitch } from './net/switch.js'
55
56
  export { ImageFiles, openImage, parseGroup, parsePasswd, resolveUser } from './image-files.js'
56
57
  export { P9_CACHE_DEFAULT, p9InitCmdline, p9MountOptions } from './host/p9mount.js'
58
+ export { loadImageTar } from './registry/load-tar.js'
57
59
 
58
60
  const PAGE = 65536
59
61
  /** A trap with the memory this close to its maximum is reported as out of memory. */
@@ -414,12 +416,30 @@ function toShared(bytes) {
414
416
  return sab
415
417
  }
416
418
 
419
+ /** vm.wasm's arguments for a machine with `disk`: --swap <MiB> and wmd.disk=data appended to --append. */
420
+ export function diskArgs(args, disk) {
421
+ if (args.includes('--swap')) throw new TypeError('--swap and a disk: give one')
422
+ const mib = disk.bytes / 2 ** 20
423
+ if (!Number.isInteger(mib) || mib < 1) throw new TypeError(`disk size ${disk.bytes} is not whole MiB`)
424
+ const out = [...args, '--swap', String(mib)]
425
+ const i = out.indexOf('--append')
426
+ if (i < 0 || i + 1 >= out.length) throw new TypeError('a disk needs --append (the kernel line gets wmd.disk=data)')
427
+ // the disk holds data: a wmd.disk= of any other value would let the guest init write a swap header over it
428
+ const given = out[i + 1].match(/(?:^| )wmd\.disk(?:=(\S*))?(?= |$)/g)
429
+ if (given && given.some(g => g.trim() !== 'wmd.disk=data')) throw new TypeError(`a disk needs wmd.disk=data on the kernel line, not ${given.map(g => g.trim()).join(' ')}`)
430
+ if (!given) out[i + 1] = `${out[i + 1]} wmd.disk=data`
431
+ return out
432
+ }
433
+
417
434
  /**
418
435
  * @param {object} o
419
436
  * @param {string|Uint8Array|ArrayBuffer} o.vmUrl URL of vm.wasm, or its bytes
420
437
  * @param {Record<string,string|Uint8Array>} o.files preopen contents: path → URL or bytes
421
438
  * @param {string[]} o.args vm.wasm arguments (after argv[0])
422
439
  * @param {string[]} [o.env] "K=V" (the runner passes only WMD_*)
440
+ * @param {object} [o.disk] a disk from openDisk (host/disk.js): the guest gets it as virtio-blk
441
+ * /dev/vda (vm.wasm's --swap MiB sizes the device) with wmd.disk=data on the kernel line, so guest init leaves
442
+ * it alone. A machine with a disk takes no checkpoints: guest memory and the disk would not match
423
443
  * @param {boolean} [o.heapReserve] W21 (default true, false in mobile WebKit: heapReserveDefault, D-516): vm.wasm grows its memory to the maximum
424
444
  * before any thread starts (WMD_HEAP_RESERVE=1), so memory never grows under running
425
445
  * vCPUs. Chromium checks memory.fill/copy against a per-Worker size that lags another
@@ -520,6 +540,10 @@ export async function bootWmd(o) {
520
540
  o.onStderr?.(text)
521
541
  }
522
542
  const preopen = o.preopen || '/g'
543
+ if (o.disk) {
544
+ if (o.checkpoint || o.restore || o.restoreLazy) throw new TypeError('a machine with a disk takes no checkpoints')
545
+ o = { ...o, args: diskArgs(o.args, o.disk) }
546
+ }
523
547
  const argv = ['vm.wasm', ...o.args]
524
548
  const env = vmEnv(o.env, o.heapReserve ?? heapReserveDefault())
525
549
  // the default Worker is started from a literal new URL(..., import.meta.url) so bundlers (Vite, webpack)
@@ -531,14 +555,12 @@ export async function bootWmd(o) {
531
555
  const flatRam = wantsFlatRam(o.args, o)
532
556
  if (!flatRam) ramMiB(o.args, o.allowLargeRam) // a bad or too big --ram is a clear error before anything is downloaded
533
557
  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
- }
558
+ if (flatRam) env.push('WMD_RAM_FLAT=1')
538
559
  const ramMem = flatRam ? createRamMemory(ramMiB(o.args, true)) : null
560
+ /** 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). */
561
+ const ramPages = ramMem ? ramMem.buffer.byteLength / CKPT_PAGE : 0
539
562
  // 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)
563
+ const ckptOn = !!o.checkpoint || !!o.restore || !!o.restoreLazy
542
564
  let restoreIn = o.restore ? parseCheckpoint(o.restore) : null
543
565
  // P-B6: a base snapshot that is still downloading ({header: Promise, ckpt: Promise, abort()}, src/base.js openBase()). The
544
566
  // 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 +762,7 @@ export async function bootWmd(o) {
740
762
  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
763
  ckptT.baseWaitMs = absMs() - tw // the time the boot waited for the snapshot after everything else was in
742
764
  const parsed = parseCheckpoint(got)
743
- checkCheckpointFits(parsed.header, { vmKey, bootKey, bootDesc, maxPages: maximum, hasJit: o.jit !== false, fmaFused: fmaFuses() })
765
+ checkCheckpointFits(parsed.header, { vmKey, bootKey, bootDesc, maxPages: maximum, ramPages, hasJit: o.jit !== false, fmaFused: fmaFuses() })
744
766
  checkPacksFit(parsed.header.packs, packDigests)
745
767
  restoreIn = parsed
746
768
  lazyIn = null // baseLazy stays: accept() when the restore has succeeded, reject() if it fails
@@ -749,7 +771,7 @@ export async function bootWmd(o) {
749
771
  baseRefused(err)
750
772
  }
751
773
  }
752
- if (restoreIn) checkCheckpointFits(restoreIn.header, { vmKey, bootKey, bootDesc, maxPages: maximum, hasJit: o.jit !== false, fmaFused: fmaFuses() })
774
+ if (restoreIn) checkCheckpointFits(restoreIn.header, { vmKey, bootKey, bootDesc, maxPages: maximum, ramPages, hasJit: o.jit !== false, fmaFused: fmaFuses() })
753
775
  }
754
776
  if (signal?.aborted) throw abortError()
755
777
  } catch (err) {
@@ -812,6 +834,26 @@ export async function bootWmd(o) {
812
834
  kept[p] = 1
813
835
  }
814
836
  }
837
+ // the flat layout: the guest's RAM, into its own memory (a fresh one: all zero but for these pages)
838
+ if (restoreIn.ram) {
839
+ const ram = ramMem.buffer
840
+ const win = 16384 // pages per view: a RAM memory can be past the length of one typed array view
841
+ let at = -1
842
+ let v = null
843
+ let r = 0
844
+ for (const c of restoreIn.ram.chunks) {
845
+ for (let off = 0; off < c.length; off += CKPT_PAGE) {
846
+ const p = restoreIn.ram.index[r++]
847
+ const lo = p - (p % win)
848
+ if (lo !== at) {
849
+ at = lo
850
+ v = new Uint8Array(ram, lo * CKPT_PAGE, Math.min(win, ramPages - lo) * CKPT_PAGE)
851
+ }
852
+ v.set(c.subarray(off, off + CKPT_PAGE), (p - lo) * CKPT_PAGE)
853
+ }
854
+ }
855
+ ckptT.ramPagesRestored = r
856
+ }
815
857
  // pages the instance's start function wrote that were zero at the checkpoint (the runner's zero_absent): the static region
816
858
  let zeroed = 0
817
859
  const w = new Int32Array(memory.buffer, 0, Math.min(restoreIn.header.memPages, VM_MIN_PAGES) * (CKPT_PAGE / 4))
@@ -845,6 +887,7 @@ export async function bootWmd(o) {
845
887
  be.now('restore', { t: 'boot', stage: 'restore', state: 'done', used: true, bytes: baseBytes, total: baseTotal, source: baseSource, tookMs: Math.round(ckptT.pagesWrittenAt) })
846
888
  restoreIn.memChunks = null
847
889
  restoreIn.index = null
890
+ restoreIn.ram = null
848
891
  restoreIn.jit = null
849
892
  restoreIn.svc = null
850
893
  restoreIn.ctl = null
@@ -1531,6 +1574,7 @@ export async function bootWmd(o) {
1531
1574
  tableMin,
1532
1575
  memory,
1533
1576
  ram: ramMem,
1577
+ blk: o.disk?.mailbox || null,
1534
1578
  ctlSab,
1535
1579
  files,
1536
1580
  preopen,
@@ -1854,7 +1898,7 @@ export async function bootWmd(o) {
1854
1898
  try {
1855
1899
  // everything that can fail without harming the running machine comes first: the checkpoint parses and is this boot's
1856
1900
  const parsed = parseCheckpoint(ckpt)
1857
- checkCheckpointFits(parsed.header, { vmKey, bootKey, bootDesc, maxPages: maximum, hasJit: o.jit !== false, fmaFused: fmaFuses() })
1901
+ checkCheckpointFits(parsed.header, { vmKey, bootKey, bootDesc, maxPages: maximum, ramPages, hasJit: o.jit !== false, fmaFused: fmaFuses() })
1858
1902
  if (!heldAssets) throw new CheckpointError('not-ready', 'this machine was booted without checkpoint support: it has no kept boot files to restore with')
1859
1903
  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
1904
  // the boot's own bytes (vm.wasm and its compiled Module, the kernel, the packs, the keys) are reused: nothing is