@threadedstack/wmd-web 0.1.18 → 0.1.20

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
@@ -192,6 +192,13 @@ export const WEBKIT_CODE_BUDGET_MIB = 144
192
192
  */
193
193
  export const WEBKIT_JIT_THRESHOLD = 1000
194
194
 
195
+ /**
196
+ * D-551: how deep compiled guest calls may nest in WebKit (vm.wasm's WMD_MIRROR_DEPTH; 32, and 64 for region
197
+ * calls, elsewhere). An amd64 go build in WebKit trapped with "Maximum call stack size exceeded" at about 33
198
+ * frames of block code, which killed the machine; Chromium and Firefox ran the same build at 32.
199
+ */
200
+ export const WEBKIT_MIRROR_DEPTH = 8
201
+
195
202
  const MiB = 1 << 20
196
203
 
197
204
  /**
@@ -342,7 +349,7 @@ export function fmaFuses() {
342
349
 
343
350
  /**
344
351
  * W32: the environment vm.wasm starts with in this browser: vmEnv's, plus
345
- * WMD_JIT_THRESHOLD=WEBKIT_JIT_THRESHOLD in WebKit, W30's WMD_TRACE_MAX for the engine and
352
+ * WMD_JIT_THRESHOLD=WEBKIT_JIT_THRESHOLD and D-551's WMD_MIRROR_DEPTH=WEBKIT_MIRROR_DEPTH in WebKit, W30's WMD_TRACE_MAX for the engine and
346
353
  * W33's WMD_RESCUE_MS, D-539's WMD_FMA_FUSED=1 where the engine fuses relaxed_madd (fmaFuses) and, in
347
354
  * Chromium, D-542's WMD_RGN_DUP=1 (regions copy sampled-hot compiled blocks of their page; Firefox's go ran
348
355
  * slower with it, WebKit did not move), each unless the caller sets it. Kept out of the boot key (bootKeyOf hashes vmEnv's list), so a checkpoint or base
@@ -355,6 +362,7 @@ export function browserEnv(env, nav = globalThis.navigator, fused = fmaFuses())
355
362
  const has = k => env.some(e => e.startsWith(`${k}=`))
356
363
  const add = []
357
364
  if (isWebKit(nav) && !has('WMD_JIT_THRESHOLD')) add.push(`WMD_JIT_THRESHOLD=${WEBKIT_JIT_THRESHOLD}`)
365
+ if (isWebKit(nav) && !has('WMD_MIRROR_DEPTH')) add.push(`WMD_MIRROR_DEPTH=${WEBKIT_MIRROR_DEPTH}`) // D-551
358
366
  if (!has('WMD_TRACE_MAX')) add.push(`WMD_TRACE_MAX=${BROWSER_TRACE_MAX[engineOf(nav)]}`)
359
367
  if (!has('WMD_RESCUE_MS')) add.push(`WMD_RESCUE_MS=${BROWSER_RESCUE_MS}`)
360
368
  if (!has('WMD_FMA_FUSED') && fused) add.push('WMD_FMA_FUSED=1') // D-539
@@ -6,7 +6,8 @@
6
6
  // the image back as a Docker tar on buildctl's stdout and converts it with loadImageTar. A Dockerfile with no RUN
7
7
  // is built in the page instead (fast.js), unless fast: false.
8
8
 
9
- import { bootWmd } from '../boot.js'
9
+ import { blobBytes } from '../blob.js'
10
+ import { bootWmd, isWebKit } from '../boot.js'
10
11
  import { MAIN_PROC_ID, containerBundle, fillRunRequest } from '../host/ctl.js'
11
12
  import { DiskError, deleteDisk, openDisk, sweepDisks } from '../host/disk.js'
12
13
  import { p9MountOptions } from '../host/p9mount.js'
@@ -15,6 +16,8 @@ import { packContext, readTarFile } from './context.js'
15
16
  import { BUILDER_PINS } from './pin.js'
16
17
  import { FastRefused, fastBuild, fastEligible } from './fast.js'
17
18
 
19
+ export { blobBytes } // D-554: read the tar Blob a build returns with it (WebKit cannot read 128 MiB or more whole)
20
+
18
21
  export const ARCHES = {
19
22
  arm64: {
20
23
  platform: 'linux/arm64',
@@ -41,13 +44,68 @@ const FULL_MIB = 64 // /bk with less free than this during a failed build: the b
41
44
  const enc = new TextEncoder()
42
45
  /** host/jit.js's line when the engine's code pool is full (D-515). */
43
46
  export const POOL_FULL_RE = /wmd-web: jit: the engine's code pool is full/
47
+ const JIT_LINE_RE = /^(?:vm\.wasm: JIT|wmd-web: jit:)/
48
+ const JIT_LINES = 60 // guest/host JIT messages kept (a ring)
49
+ const JIT_EVENTS = 200 // 'jit' events sent in a builder's life: the vm repeats some of them at every release
50
+ const JIT_KINDS = [
51
+ [/the engine's code pool is full/, 'pool-full'],
52
+ [/the engine compiles code again/, 'pool-back'],
53
+ [/blocks run in the interpreter from now on/, 'interpreter-from-now-on'], // vm cache_release: release_dead
54
+ [/waiting before releasing again/, 'release-deferred'], // D-445 grace window
55
+ [/JIT release/, 'release'],
56
+ [/module budget reached|budget \d+ KiB from now on|refused more code/, 'budget'],
57
+ ]
58
+ /** What a JIT message line says, or null for a line that is not one. */
59
+ export function jitKind(line) {
60
+ if (!JIT_LINE_RE.test(line)) return null
61
+ return JIT_KINDS.find(([re]) => re.test(line))?.[1] || 'other'
62
+ }
63
+ /**
64
+ * The guest's stderr into whole lines: the JIT's messages are kept in a ring with the time (performance.now, ms)
65
+ * and sent as {t:'jit', what, line} events, so a build that crawls in the interpreter can be told from one that hangs.
66
+ */
67
+ export function jitWatch({ onEvent = () => {}, max = JIT_LINES, maxEvents = JIT_EVENTS, now = () => Math.round(performance.now()) } = {}) {
68
+ const ring = []
69
+ let buf = ''
70
+ let events = 0
71
+ let skipped = 0
72
+ const line = l => {
73
+ const what = jitKind(l)
74
+ if (!what) return
75
+ const at = now()
76
+ ring.push({ at, what, line: l.slice(0, 400) })
77
+ if (ring.length > max) ring.shift()
78
+ if (what === 'other') return
79
+ if (events < maxEvents || what === 'interpreter-from-now-on') {
80
+ events++
81
+ onEvent({ t: 'jit', what, line: l.slice(0, 400) })
82
+ } else skipped++
83
+ }
84
+ return {
85
+ push(text) {
86
+ buf += text
87
+ let i
88
+ while ((i = buf.indexOf('\n')) >= 0) {
89
+ line(buf.slice(0, i))
90
+ buf = buf.slice(i + 1)
91
+ }
92
+ if (buf.length > 4096) buf = buf.slice(-4096)
93
+ },
94
+ lines: () => ring.slice(),
95
+ skipped: () => skipped,
96
+ }
97
+ }
44
98
 
45
99
  export class BuildError extends Error {
46
- constructor(message, { step = null, tail = '', reason = null } = {}) {
100
+ constructor(message, { step = null, tail = '', reason = null, jit = null, machine = null } = {}) {
47
101
  super(message)
48
102
  this.name = 'BuildError'
49
103
  this.step = step
50
104
  this.tail = tail
105
+ this.jit = jit // the builder's last JIT messages ([{at, what, line}]), when it kept them
106
+ // when buildctl's stream ended without an exit (code 125): why it closed, whether the machine ended, the
107
+ // machine's stderr tail and the start script's last lines
108
+ this.machine = machine
51
109
  this.reason = reason // 'no-space' | 'quota' | 'disk' | 'timeout' | 'pin' | 'secret' | null
52
110
  }
53
111
  }
@@ -220,7 +278,7 @@ async function indexDigest(index) {
220
278
  async function bytesOf(x, fetchFn) {
221
279
  if (x instanceof Uint8Array) return x
222
280
  if (x instanceof ArrayBuffer) return new Uint8Array(x)
223
- if (typeof Blob !== 'undefined' && x instanceof Blob) return new Uint8Array(await x.arrayBuffer())
281
+ if (typeof Blob !== 'undefined' && x instanceof Blob) return blobBytes(x)
224
282
  const r = await fetchFn(x)
225
283
  if (!r.ok) throw new Error(`${x}: ${r.status} ${r.statusText}`)
226
284
  return new Uint8Array(await r.arrayBuffer())
@@ -348,10 +406,88 @@ function aborted(signal) {
348
406
  * @param {string[]} [o.registryMirrors] Docker Hub mirrors (host[:port], in order) BuildKit tries before docker.io; list a plain-http one
349
407
  * in httpRegistries too
350
408
  * @param {Record<string,string>} [o.registryCAs] host -> PEM: CAs BuildKit checks that registry's certificate against
409
+ * @param {boolean} [o.nodeInterpreted] D-553: node in RUN steps runs its JavaScript in the interpreter (a preload turns
410
+ * off V8's compilers; WebAssembly stays). Default: on in WebKit, off elsewhere.
411
+ * Without it, WebKit cannot keep up with the code node's JIT writes, and an npm
412
+ * install of 1,600 packages does not end in 30 minutes.
351
413
  * @param {Function} [o.onEvent]
352
414
  * @param {AbortSignal} [o.signal]
353
415
  * @param {object} [o.boot] extra bootWmd options (tests)
354
416
  */
417
+ /**
418
+ * D-553: the preload node runs in a RUN step (NODE_OPTIONS --require). It turns off V8's compilers, so node runs its
419
+ * JavaScript in the interpreter and writes no new machine code; WebAssembly stays (node-gyp's undici needs it, which
420
+ * --jitless removed). Each flag only where that V8 has it (V8 prints an error for an unknown one): measured on node
421
+ * 12, 14, 16, 18, 20, 22 and 24.
422
+ */
423
+ export const NODE_PRELOAD = `try {
424
+ var v = process.versions.v8.split('.').map(Number), m = v[0] * 100 + v[1], f = ['--no-opt']
425
+ if (m >= 708) f.push('--regexp-interpret-all')
426
+ if (m >= 904) f.push('--no-sparkplug', '--liftoff-only')
427
+ if (m >= 1100) f.push('--no-maglev')
428
+ require('v8').setFlagsFromString(f.join(' '))
429
+ } catch (e) {}
430
+ `
431
+
432
+ /**
433
+ * D-553: buildkit-runc that makes node in a RUN step load NODE_PRELOAD (runc ... --bundle <dir> ...). BuildKit writes
434
+ * config.json on one line (json.Encoder). awk walks it as JSON, strings and escapes included (inside a string a quote
435
+ * is \", so the keys it looks for cannot occur there):
436
+ * - it appends a read-only bind of /usr/local/lib/wmd-node.js at /dev/.wmd-node.js to the end of "mounts", after
437
+ * BuildKit's /dev tmpfs, so the file is in that tmpfs and never in the step's layer;
438
+ * - then it adds --require=/dev/.wmd-node.js to the first "env":[ (process.env)'s NODE_OPTIONS, or a NODE_OPTIONS
439
+ * element at the front.
440
+ * Without a "mounts" array it changes nothing (a --require of a missing file would stop node).
441
+ */
442
+ export const RUNC_NODE = String.raw`#!/bin/sh
443
+ b=
444
+ p=
445
+ for a in "$@"; do
446
+ case "$a" in --bundle=*) b=$(printf %s "$a" | cut -c10-) ;; esac
447
+ case "$p" in --bundle|-b) b=$a ;; esac
448
+ p=$a
449
+ done
450
+ c="$b/config.json"
451
+ if [ -n "$b" ] && [ -f "$c" ]; then
452
+ awk -v M='{"destination":"/dev/.wmd-node.js","type":"bind","source":"/usr/local/lib/wmd-node.js","options":["rbind","ro"]}' -v R='--require=/dev/.wmd-node.js' '
453
+ function shut(s, st, n, i, ch, ins, esc, d) {
454
+ n = length(s); ins = 0; esc = 0; d = 0
455
+ for (i = st; i <= n; i++) {
456
+ ch = substr(s, i, 1)
457
+ if (ins) { if (esc) esc = 0; else if (ch == "\\") esc = 1; else if (ch == "\"") ins = 0 }
458
+ else if (ch == "\"") ins = 1
459
+ else if (ch == "[" || ch == "{") d++
460
+ else if (ch == "]" || ch == "}") { if (!d) return i; d-- }
461
+ }
462
+ return 0
463
+ }
464
+ {
465
+ s = $0
466
+ k = index(s, ",\"mounts\":["); if (!k) k = index(s, "{\"mounts\":[")
467
+ if (!k || index(s, "\"destination\":\"/dev/.wmd-node.js\"")) { print s; next }
468
+ st = k + 11; e = shut(s, st); if (!e) { print s; next }
469
+ s = substr(s, 1, e - 1) (e == st ? "" : ",") M substr(s, e)
470
+ k = index(s, "\"env\":["); if (!k) { print s; next }
471
+ st = k + 7; n = length(s); ins = 0; esc = 0; found = 0; q = 0; L = length(R) + 1
472
+ for (i = st; i <= n; i++) {
473
+ ch = substr(s, i, 1)
474
+ if (ins) {
475
+ if (esc) esc = 0
476
+ else if (ch == "\\") esc = 1
477
+ else if (ch == "\"") {
478
+ ins = 0
479
+ if (substr(s, q, 14) == "\"NODE_OPTIONS=") { found = 1; s = substr(s, 1, i - 1) " " R substr(s, i); i += L; n += L }
480
+ }
481
+ } else if (ch == "\"") { ins = 1; q = i }
482
+ else if (ch == "]") break
483
+ }
484
+ if (!found) s = substr(s, 1, st - 1) "\"NODE_OPTIONS=" R "\"" (substr(s, st, 1) == "]" ? "" : ",") substr(s, st)
485
+ print s
486
+ }' "$c" > "$c.wmd" && mv "$c.wmd" "$c"
487
+ fi
488
+ exec /usr/bin/buildkit-runc "$@"
489
+ `
490
+
355
491
  export async function createBuilder(o) {
356
492
  const archName = o.arch || 'arm64'
357
493
  const arch = ARCHES[archName]
@@ -359,11 +495,13 @@ export async function createBuilder(o) {
359
495
  if (o.disk !== false) checkNamespace(o.disk?.namespace)
360
496
  if (!o.relay?.url) throw new TypeError('relay: {url}: the builder pulls base images through it')
361
497
  const fetchFn = o.fetch || globalThis.fetch.bind(globalThis)
498
+ // every event carries `at`: performance.now() in ms (`ms` is a duration in some events)
362
499
  const emit = ev => {
363
500
  try {
364
- o.onEvent?.(ev)
501
+ o.onEvent?.({ at: Math.round(performance.now()), ...ev })
365
502
  } catch {}
366
503
  }
504
+ const jit = jitWatch({ onEvent: emit })
367
505
  const ram = o.ram || 1536
368
506
  const cpus = o.cpus || 2
369
507
  const t0 = performance.now()
@@ -398,7 +536,16 @@ export async function createBuilder(o) {
398
536
  ...(o.registryMirrors?.length ? [`WMD_BK_MIRRORS=${o.registryMirrors.map(checkHost).join(',')}`] : []),
399
537
  ...(o.registryCAs && Object.keys(o.registryCAs).length ? [`WMD_BK_CA=${Object.entries(o.registryCAs).map(([h, pem]) => `${checkHost(h)}=${b64utf8(pem)}`).join(' ')}`] : []),
400
538
  ]
401
- const argv = ['/usr/local/bin/wmd-buildkitd']
539
+ const interp = o.nodeInterpreted ?? isWebKit()
540
+ // D-553: BuildKit starts each RUN step with buildkit-runc from PATH; a wrapper earlier in PATH makes node in the step
541
+ // load a preload that turns off V8's compilers, then runs the real one. The image and the cache keys stay as they are.
542
+ const argv = interp
543
+ ? ['/bin/sh', '-c', 'mkdir -p /usr/local/lib && printf %s "$WMD_NODE_PRELOAD" > /usr/local/lib/wmd-node.js && printf %s "$WMD_RUNC_WRAP" > /usr/local/bin/buildkit-runc && chmod 0755 /usr/local/bin/buildkit-runc && exec /usr/local/bin/wmd-buildkitd']
544
+ : ['/usr/local/bin/wmd-buildkitd']
545
+ if (interp) {
546
+ env.push(`WMD_RUNC_WRAP=${RUNC_NODE}`, `WMD_NODE_PRELOAD=${NODE_PRELOAD}`)
547
+ emit({ t: 'node-interpreted', message: "node in RUN steps runs its JavaScript in the interpreter: WebKit's code pool cannot keep up with node's JIT" })
548
+ }
402
549
  const b = containerBundle({ args: argv, env, cwd: '/', hostname: 'buildkitsandbox', networkDisabled: false, ip: '192.168.127.2', gateway: '192.168.127.1', privileged: true })
403
550
  const append = [...arch.console, 'rdinit=/sbin/init', 'loglevel=4', 'norandmaps', 'wmd.init', 'wmd.p9tag=wmdroot', 'wmd.control=vport', 'wmd.exit'].join(' ')
404
551
  const args = [
@@ -425,6 +572,7 @@ export async function createBuilder(o) {
425
572
  ...(disk ? { disk } : {}),
426
573
  onStderr: s => {
427
574
  stderr = (stderr + s).slice(-16384)
575
+ jit.push(s)
428
576
  // WebKit: the engine's code pool filled, so new code waits and the guest runs slower (D-515; plan 5.10)
429
577
  if (!slowSaid && POOL_FULL_RE.test(s)) {
430
578
  slowSaid = true
@@ -482,6 +630,8 @@ export async function createBuilder(o) {
482
630
  throw e
483
631
  }
484
632
  emit({ t: 'ready', ms: Math.round(performance.now() - t0), platform: arch.platform })
633
+ let endedWith = null
634
+ ended.then(r => (endedWith = { code: r?.code ?? null, reason: r?.reason ?? null }), () => {})
485
635
 
486
636
  let seq = 0
487
637
  let busy = false
@@ -539,14 +689,14 @@ export async function createBuilder(o) {
539
689
  clearTimeout(killTimer)
540
690
  signal?.removeEventListener('abort', kill)
541
691
  }
542
- if (!binary) return { code: r.code ?? null, out, err }
692
+ if (!binary) return { code: r.code ?? null, out, err, error: r.error ?? null }
543
693
  const bytes = new Uint8Array(size)
544
694
  let at = 0
545
695
  for (const c of chunks) {
546
696
  bytes.set(c, at)
547
697
  at += c.length
548
698
  }
549
- return { code: r.code ?? null, out: bytes, err }
699
+ return { code: r.code ?? null, out: bytes, err, error: r.error ?? null }
550
700
  })()
551
701
  }
552
702
  const sh = (cmd, opts) => inGuest(['/bin/sh', '-c', cmd], opts)
@@ -563,6 +713,15 @@ export async function createBuilder(o) {
563
713
  inGuest,
564
714
  sh,
565
715
  log: () => mainLog.lines,
716
+ jitLines: () => jit.lines(),
717
+ /** The machine's state for a failure report: {ended ({code, reason} or null), stderr tail, start script's tail}. */
718
+ machineState: () => ({ ended: endedWith, stderr: stderr.slice(-4000), main: mainLog.tail.slice(-3000) }),
719
+ /** The guest network's counters and its last 40 events (resets, refusals, D-551 redials), or null. */
720
+ netStats: async () => {
721
+ const n = await vm.net?.stats?.().catch(() => null)
722
+ if (!n?.stack) return n ?? null
723
+ return { stack: { ...n.stack, events: (n.stack.events || []).slice(-40) }, relay: n.relay ?? null }
724
+ },
566
725
  /** Free MiB and free inodes on /bk. */
567
726
  async room() {
568
727
  const r = await sh("df -Pm /bk | awk 'NR==2{print $4}'; df -Pi /bk | awk 'NR==2{print $4, $2}'")
@@ -609,6 +768,7 @@ async function keepBuilt(store, index, packs) {
609
768
  async function runBuild(builder, req, emit) {
610
769
  const t0 = performance.now()
611
770
  const onEvent = ev => {
771
+ ev = { at: Math.round(performance.now()), ...ev }
612
772
  emit(ev)
613
773
  try {
614
774
  req.onEvent?.(ev)
@@ -650,7 +810,18 @@ async function runBuild(builder, req, emit) {
650
810
  const dir = `/run/ctx/${id}`
651
811
  // req.signal and timeoutMs cover everything from here: the context, the upload, buildctl and the conversion
652
812
  const ac = new AbortController()
653
- const timer = req.timeoutMs ? setTimeout(() => ac.abort(new BuildError(`the build ran past ${req.timeoutMs} ms`, { reason: 'timeout' })), req.timeoutMs) : null
813
+ let lastStep = null
814
+ const timer = req.timeoutMs
815
+ ? setTimeout(
816
+ () => {
817
+ const e = new BuildError(`the build ran past ${req.timeoutMs} ms${lastStep ? `, in the step ${lastStep}` : ''}`, { reason: 'timeout', step: lastStep, tail: log.tail.slice(-4000), jit: builder.jitLines?.() ?? null })
818
+ ac.abort(e)
819
+ // the network's counters join the error while buildctl is stopped (SIGINT, then 5 s), before it is thrown
820
+ builder.netStats?.().then(n => (e.net = n), () => {})
821
+ },
822
+ req.timeoutMs,
823
+ )
824
+ : null
654
825
  const onAbort = () => ac.abort(new BuildCancelled())
655
826
  req.signal?.addEventListener('abort', onAbort, { once: true })
656
827
  const check = () => {
@@ -698,7 +869,6 @@ async function runBuild(builder, req, emit) {
698
869
  if (req.push?.auth) xenv.push(`DOCKER_CONFIG=${dir}/auth`)
699
870
  args.push('--output', `type=docker,${req.tag ? `name=${req.tag},` : ''}dest=-`)
700
871
  if (req.push) args.push('--output', `type=image,name=${req.push.name},push=true${req.push.insecure ? ',registry.insecure=true' : ''}`)
701
- let lastStep = null
702
872
  const errLines = [] // buildctl's own error lines (no step), not the steps' output
703
873
  const pr = progressReader({
704
874
  onStep: ev => {
@@ -737,8 +907,22 @@ async function runBuild(builder, req, emit) {
737
907
  if (builder.disk?.events?.slice?.(diskMark).some(e => e.reason === 'quota')) reason = 'quota'
738
908
  if (!reason && /input\/output error|read-only file system/i.test(tail)) reason = 'disk'
739
909
  const failed = [...pr.vertexes.values()].find(v => v.error)
740
- const why = failed?.error || tail.split('\n').filter(Boolean).pop() || `buildctl exited ${r.code}`
741
- throw new BuildError(`the build failed${failed ? ` at ${failed.name}` : ''}: ${why.slice(0, 500)}`, { step: failed?.name || lastStep, tail, reason })
910
+ const why = failed?.error || tail.split('\n').filter(Boolean).pop() || `buildctl exited ${r.code}${r.error ? ` (${r.error})` : ''}`
911
+ // a stream that ended with no exit frame: the machine's side says why
912
+ let machine = null
913
+ if (r.code === 125 && r.error) {
914
+ await new Promise(res => setTimeout(res, 2000)) // let a machine that is going down report it
915
+ machine = { stream: r.error, ...(builder.machineState?.() ?? {}) }
916
+ }
917
+ const err = new BuildError(`the build failed${failed ? ` at ${failed.name}` : ''}: ${why.slice(0, 500)}`, { step: failed?.name || lastStep, tail, reason, jit: builder.jitLines?.() ?? null, machine })
918
+ err.net = (await builder.netStats?.().catch(() => null)) ?? null
919
+ // buildkitd's side (a session it closed, a worker error): its last log lines, when the machine still answers
920
+ if (r.code !== 125) {
921
+ const d = await Promise.race([builder.sh('tail -n 40 /run/buildkitd.log').catch(() => null), new Promise(res => setTimeout(res, 10_000, null))])
922
+ err.daemonLog = d?.out ? d.out.slice(-6000) : null
923
+ }
924
+ check() // a cancel or timeout during these waits wins
925
+ throw err
742
926
  }
743
927
  onEvent({ t: 'export', bytes: r.out.length })
744
928
  const t1 = performance.now()
@@ -784,7 +968,8 @@ async function ensureRoom(builder, onEvent, signal) {
784
968
  export async function buildImage(o) {
785
969
  const builder = await createBuilder(o)
786
970
  try {
787
- return await builder.build(o)
971
+ // o.onEvent already hears every build event through the builder: given to build() too, each would come twice
972
+ return await builder.build({ ...o, onEvent: undefined })
788
973
  } finally {
789
974
  await builder.close()
790
975
  }
@@ -1,6 +1,7 @@
1
1
  // The build context (docs/plans/web-buildkit-impl.md 5.6): turn a file source, a plain object or a tar into the
2
2
  // one tar the page sends to the builder. .dockerignore is applied here with a port of moby's patternmatcher, so
3
3
  // ignored files are never read; BuildKit applies it again and stays the source of truth.
4
+ import { blobBytes } from '../blob.js'
4
5
  import { matcher, parseDockerignore } from './dockerignore.js'
5
6
  import { decodeText, scanTar, tarWriter } from './tar.js'
6
7
 
@@ -108,7 +109,7 @@ function tarReader(input) {
108
109
  const u8 = input instanceof ArrayBuffer ? new Uint8Array(input) : input
109
110
  const total = blob ? input.size : u8.length
110
111
  const read = blob
111
- ? async (o, n) => new Uint8Array(await input.slice(o, o + n).arrayBuffer())
112
+ ? (o, n) => blobBytes(input.slice(o, o + n))
112
113
  : (o, n) => u8.subarray(o, o + n)
113
114
  return { blob, u8, total, read }
114
115
  }
@@ -175,7 +176,7 @@ async function packTar(input, dockerfile, lim) {
175
176
  if (err instanceof ContextError) throw err
176
177
  throw new ContextError('bad-source', '', `build context: ${err.message}`)
177
178
  }
178
- const tar = blob ? new Uint8Array(await input.arrayBuffer()) : u8
179
+ const tar = blob ? await blobBytes(input) : u8
179
180
  return { tar, files, bytes, ignored: 0, dockerfile }
180
181
  }
181
182
 
@@ -71,7 +71,14 @@ function serve(mailbox, disk, closeFiles) {
71
71
  let ops = 0
72
72
  const post = () => self.postMessage({ t: 'stats', stats: { ...stats, ...disk.usage() } })
73
73
  for (;;) {
74
- if (Atomics.load(i32, 5) === 1) break
74
+ const ctl = Atomics.load(i32, 5)
75
+ if (ctl === 1) break
76
+ if (ctl === 2) {
77
+ // tests (gate 9): the Worker ends with no word, as a crashed one does; a vCPU waiting on it gives up after WAIT_MS
78
+ closeFiles()
79
+ self.close()
80
+ return
81
+ }
75
82
  const s = Atomics.load(i32, 1)
76
83
  if (s === STATE.GONE) break
77
84
  if (s !== STATE.POSTED) {
package/src/host/disk.js CHANGED
@@ -10,7 +10,7 @@
10
10
  // i32[2] op 0 read, 1 write, 2 flush, 3 discard (blk.c's BLK_OP_*)
11
11
  // i32[3] len bytes (u32)
12
12
  // i32[4] result 0 or a negative errno
13
- // i32[5] control 1: the page asks the Worker to flush and close
13
+ // i32[5] control 1: the page asks the Worker to flush and close; 2 (tests): it ends at once and answers nothing
14
14
  // f64[3] off byte offset (bytes 24..31)
15
15
  // data from byte 64, DATA bytes; longer reads and writes go in pieces
16
16
  //
package/src/host/jit.js CHANGED
@@ -69,6 +69,8 @@ const POOL_CHECK_MS = 2000 // D-515: at most one code pool check a thread in thi
69
69
  const POOL_PROBE_N = 1500000 // D-515: loop passes of the check: about 2 ms compiled, 40 ms interpreted
70
70
  const POOL_SLOW_X = 6 // D-515: a check this many times slower than this thread's fastest means the pool is full
71
71
  const POOL_LEARN = 3 / 4 // D-515: the budget from then on, of the modules live when the pool filled
72
+ const POOL_RECOVER_N = 5 // a learned budget grows again after this many checks in a row that found room (10 s at POOL_CHECK_MS)
73
+ const POOL_RECOVER_X = 4 / 3 // ... by this factor each time (the inverse of POOL_LEARN), never past maxLiveBytes
72
74
 
73
75
  // counters after the header: each a pair of Int32 cells (low word, unsigned; high word).
74
76
  // D-419: not BigInt64 cells: Atomics.add on a BigInt64Array returns a new BigInt, garbage a
@@ -404,6 +406,38 @@ export function createJitDoor({ store, memory, ram = null, log = () => {}, compi
404
406
 
405
407
  let poolNext = 0
406
408
  let poolFast = Infinity
409
+ let poolRoom = 0 // checks in a row that found room, with the shared budget unchanged
410
+ let budgetSeen = -1 // the shared budget at the last check: a change (another Worker grew or lowered it) starts the count again
411
+ /**
412
+ * The engine has had room for POOL_RECOVER_N checks in a row: a budget learned while released code
413
+ * was not yet freed (WebKit frees it 30-40 s late, D-445) grows back by POOL_RECOVER_X, and is
414
+ * dropped once it reaches maxLiveBytes (the original cap). Without a cap there is nothing to
415
+ * return to.
416
+ */
417
+ function recoverBudget() {
418
+ if (!maxLiveBytes) return
419
+ // the budget is shared and every Worker checks: without this, N Workers would grow it N times as fast
420
+ const seen = Atomics.load(hdr, H_BUDGET_KIB)
421
+ if (seen !== budgetSeen) {
422
+ budgetSeen = seen
423
+ poolRoom = 0
424
+ }
425
+ if (++poolRoom < POOL_RECOVER_N) return
426
+ poolRoom = 0
427
+ if (!seen) return
428
+ lock()
429
+ try {
430
+ const cur = Atomics.load(hdr, H_BUDGET_KIB)
431
+ if (!cur) return
432
+ const next = Math.ceil(cur * POOL_RECOVER_X)
433
+ const done = next * 1024 >= maxLiveBytes
434
+ Atomics.store(hdr, H_BUDGET_KIB, done ? 0 : next)
435
+ budgetSeen = done ? 0 : next
436
+ log(`wmd-web: jit: the engine has had room for ${POOL_RECOVER_N} checks; budget ${done ? `back to its cap of ${maxLiveBytes >> 10} KiB` : `${next} KiB`}`)
437
+ } finally {
438
+ unlock()
439
+ }
440
+ }
407
441
  /**
408
442
  * D-515: before a new module is compiled. While the engine's code pool is full (codePool, checked
409
443
  * at most once each poolEvery) the answer is JIT_LATER: the guest interprets the blocks and asks
@@ -423,8 +457,10 @@ export function createJitDoor({ store, memory, ram = null, log = () => {}, compi
423
457
  if (ms > poolFast * POOL_SLOW_X) ms = Math.min(ms, codePool()) // not a pause of this thread
424
458
  poolFast = Math.min(poolFast, Math.max(ms, 0.05))
425
459
  const full = ms > poolFast * POOL_SLOW_X
460
+ if (full) poolRoom = 0
426
461
  const at = () => new Date().toISOString().slice(11, 19)
427
462
  if (!full) {
463
+ recoverBudget()
428
464
  if (was && Atomics.compareExchange(hdr, H_POOL_FULL, 1, 0) === 1) {
429
465
  add('poolBack')
430
466
  log(`wmd-web: jit: the engine compiles code again (${at()})`)
@@ -4,6 +4,7 @@
4
4
  // `USER name`, devcontainer `remoteUser` or `--chown=app:app` (resolveUser). W66: another image's files,
5
5
  // for `COPY --from` and multi-stage builds (walk).
6
6
 
7
+ import { blobBytes } from './blob.js'
7
8
  import { ENOENT, FsError } from './fs/errno.js'
8
9
  import { Overlay } from './fs/overlay.js'
9
10
  import { Pack, bytesSource } from './fs/pack.js'
@@ -19,7 +20,7 @@ async function layerBytes(l, i, fetchFn) {
19
20
  }
20
21
  if (l instanceof Uint8Array) return l
21
22
  if (l instanceof ArrayBuffer || (typeof SharedArrayBuffer !== 'undefined' && l instanceof SharedArrayBuffer)) return new Uint8Array(l)
22
- if (typeof Blob !== 'undefined' && l instanceof Blob) return new Uint8Array(await l.arrayBuffer())
23
+ if (typeof Blob !== 'undefined' && l instanceof Blob) return blobBytes(l)
23
24
  throw new TypeError(`layer ${i}: a URL, bytes or a Blob, not ${Object.prototype.toString.call(l)}`)
24
25
  }
25
26