@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/README.md +24 -3
- package/RELEASE.json +28 -44
- package/guest/arm64/Image +1 -1
- package/guest/arm64/Image-4k +1 -1
- package/guest/arm64/initramfs.cpio.gz +0 -0
- package/guest/arm64/vm.wasm +0 -0
- package/package.json +4 -4
- package/relay/hosted.ts +32 -18
- package/relay/main.ts +1 -1
- package/relay/relay.ts +0 -1
- package/relay/token.ts +5 -34
- package/relay/wisp.ts +5 -37
- package/src/blob.js +12 -0
- package/src/boot.js +9 -1
- package/src/build/builder.js +197 -12
- package/src/build/context.js +3 -2
- package/src/host/disk-worker.js +8 -1
- package/src/host/disk.js +1 -1
- package/src/host/jit.js +36 -0
- package/src/image-files.js +2 -1
- package/src/net/stack.js +153 -1
- package/src/registry/load-tar.js +2 -1
- package/wmdpack.wasm +0 -0
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
|
package/src/build/builder.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/build/context.js
CHANGED
|
@@ -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
|
-
?
|
|
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 ?
|
|
179
|
+
const tar = blob ? await blobBytes(input) : u8
|
|
179
180
|
return { tar, files, bytes, ignored: 0, dockerfile }
|
|
180
181
|
}
|
|
181
182
|
|
package/src/host/disk-worker.js
CHANGED
|
@@ -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
|
-
|
|
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()})`)
|
package/src/image-files.js
CHANGED
|
@@ -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
|
|
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
|
|