@vzn/vx-dte 0.0.652 → 0.0.653
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 +135 -62
- package/package.json +3 -3
- package/src/dispatch.ts +39 -4
- package/src/github.ts +0 -3
- package/src/index.ts +1 -0
- package/src/init.ts +9 -10
- package/src/local.ts +112 -28
- package/src/messages.ts +14 -3
- package/src/orchestrator.ts +265 -33
- package/src/planner.ts +183 -55
- package/src/plugin.ts +209 -30
- package/src/seal.ts +159 -43
- package/src/server-fit.ts +80 -0
- package/src/tree.ts +215 -7
- package/src/usage.ts +65 -3
- package/src/warm-ups.ts +72 -6
- package/src/wire.ts +84 -42
- package/src/worker.ts +148 -15
package/src/worker.ts
CHANGED
|
@@ -45,7 +45,8 @@ import {
|
|
|
45
45
|
type WiredResult,
|
|
46
46
|
} from './messages.js'
|
|
47
47
|
import { leftUnder, names, outputFiles } from './outputs.js'
|
|
48
|
-
import {
|
|
48
|
+
import { serverFit } from './server-fit.js'
|
|
49
|
+
import { snapshot, unfilter } from './tree.js'
|
|
49
50
|
import { sealWorker, type Proof } from './seal.js'
|
|
50
51
|
import { join, ORCHESTRATOR_SILENT, RelayError, unsent as unsentBy } from './wire.js'
|
|
51
52
|
|
|
@@ -66,6 +67,8 @@ export async function runWorker(opts: {
|
|
|
66
67
|
// The tracked files as they are, as the run snapshots its own: HEAD's
|
|
67
68
|
// tree let in a checkout whose prepare rewrote a tracked file, and the
|
|
68
69
|
// task built from it was saved under the clean key (2026-10-25).
|
|
70
|
+
// Its files first as the commit holds them, as the run's key read them.
|
|
71
|
+
await unfilter(opts.root)
|
|
69
72
|
const base = await snapshot(opts.root)
|
|
70
73
|
const tree = base.tree
|
|
71
74
|
// Never an output, as core never cleans one: the checkout is the run's
|
|
@@ -132,7 +135,40 @@ export async function runWorker(opts: {
|
|
|
132
135
|
}
|
|
133
136
|
let tasksCpuMs = 0
|
|
134
137
|
const trees = treeMeter()
|
|
135
|
-
|
|
138
|
+
// Its servers' trees, and which dependants each serves now: their CPU is
|
|
139
|
+
// charged to those tasks, and each base reported (`serverFit`).
|
|
140
|
+
const serverTrees = treeMeter()
|
|
141
|
+
const fit = serverFit()
|
|
142
|
+
const frame = (): Load => {
|
|
143
|
+
const servers = fit.bases()
|
|
144
|
+
return {
|
|
145
|
+
...load.now(),
|
|
146
|
+
tasks: trees.now(),
|
|
147
|
+
...(Object.keys(servers).length === 0 ? {} : { servers }),
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const usersOf = new Map<string, Set<string>>()
|
|
151
|
+
let sampledAt = performance.now()
|
|
152
|
+
const sampler = setInterval(() => {
|
|
153
|
+
const at = performance.now()
|
|
154
|
+
const ms = at - sampledAt
|
|
155
|
+
sampledAt = at
|
|
156
|
+
if (usersOf.size === 0) return
|
|
157
|
+
const capped = load.now().cpu >= cores * FULL
|
|
158
|
+
for (const [server, users] of usersOf)
|
|
159
|
+
fit.sample(server, [...users], serverTrees.coresOf(server), ms, capped)
|
|
160
|
+
}, TREE_EVERY_MS)
|
|
161
|
+
sampler.unref?.()
|
|
162
|
+
const newFloor = (sandboxes: Parameters<typeof localFloor>[0] = []): LocalFloor =>
|
|
163
|
+
localFloor(sandboxes, (id, pid) => {
|
|
164
|
+
if (pid === undefined) {
|
|
165
|
+
serverTrees.drop(id)
|
|
166
|
+
usersOf.delete(id)
|
|
167
|
+
} else {
|
|
168
|
+
serverTrees.watch(id, pid)
|
|
169
|
+
usersOf.set(id, usersOf.get(id) ?? new Set())
|
|
170
|
+
}
|
|
171
|
+
})
|
|
136
172
|
/**
|
|
137
173
|
* Entries whose files are here, restored or run here, by key: tasks
|
|
138
174
|
* started together wait on one restore, never run before it.
|
|
@@ -445,6 +481,7 @@ export async function runWorker(opts: {
|
|
|
445
481
|
m.nested.map((n) => path.join(opts.root, n)),
|
|
446
482
|
undefined,
|
|
447
483
|
(m.sandbox.reads ?? []).map((r) => path.join(opts.root, r)),
|
|
484
|
+
(m.sandbox.reported ?? []).map((r) => path.join(opts.root, r)),
|
|
448
485
|
)
|
|
449
486
|
const output = new BoundedCapture()
|
|
450
487
|
const req: ExecuteRequest = {
|
|
@@ -453,7 +490,7 @@ export async function runWorker(opts: {
|
|
|
453
490
|
command: m.command,
|
|
454
491
|
forwardArgs: [],
|
|
455
492
|
cwd: path.join(opts.root, m.cwd),
|
|
456
|
-
env: localEnv(m.env, root, opts.root),
|
|
493
|
+
env: localEnv(m.env, root, opts.root, m.unset),
|
|
457
494
|
envDefine: {},
|
|
458
495
|
// Both streams as one capture, in the order written, as core keeps a
|
|
459
496
|
// run's: kept apart, a hit replayed every stdout line before the
|
|
@@ -487,16 +524,40 @@ export async function runWorker(opts: {
|
|
|
487
524
|
})),
|
|
488
525
|
}),
|
|
489
526
|
}
|
|
490
|
-
|
|
527
|
+
// An untracked file under its project is one the orchestrator's key
|
|
528
|
+
// never read (it refuses one it keys), yet the command may: one a
|
|
529
|
+
// checkout left built into the output saved under the clean key, and
|
|
530
|
+
// every later run restored it (2026-11-07). Listed before the command,
|
|
531
|
+
// so a file the task writes is not taken for one it read.
|
|
532
|
+
const strays = await untrackedUnder(opts.root, m, [names(m.project, m.outputs), ...upstream])
|
|
533
|
+
floor ??= newFloor()
|
|
491
534
|
ui.print('out', ui.status('info', m.task, ui.dim('running')))
|
|
492
535
|
const crowd = load.mark()
|
|
536
|
+
const serves = (m.servers ?? []).map((sv) => sv.task)
|
|
537
|
+
// Counted a user once its server is up: `onServer` makes the set.
|
|
538
|
+
const using = (): void => {
|
|
539
|
+
for (const sv of serves) usersOf.get(sv)?.add(m.task)
|
|
540
|
+
}
|
|
493
541
|
const {
|
|
494
542
|
outputs: _,
|
|
495
543
|
stdout: _o,
|
|
496
544
|
stderr: _e,
|
|
497
545
|
...res
|
|
498
|
-
} = await floor
|
|
546
|
+
} = await floor
|
|
547
|
+
.execute({
|
|
548
|
+
...req,
|
|
549
|
+
onSpawn: (pid) => {
|
|
550
|
+
using()
|
|
551
|
+
req.onSpawn?.(pid)
|
|
552
|
+
},
|
|
553
|
+
})
|
|
554
|
+
.finally(() => {
|
|
555
|
+
trees.drop(m.task)
|
|
556
|
+
for (const sv of serves) usersOf.get(sv)?.delete(m.task)
|
|
557
|
+
})
|
|
499
558
|
tasksCpuMs += res.cpuMs ?? 0
|
|
559
|
+
// What its servers did for it: its CPU as history books it.
|
|
560
|
+
const served = fit.take(m.task)
|
|
500
561
|
const cpuShare = load.shareSince(
|
|
501
562
|
crowd,
|
|
502
563
|
res.cpuMs === undefined || !(res.durationMs > 0) ? undefined : res.cpuMs / res.durationMs,
|
|
@@ -539,7 +600,10 @@ export async function runWorker(opts: {
|
|
|
539
600
|
// it; this entry is withheld for any. Nor for an additive task that
|
|
540
601
|
// removed an upstream's file: a hit left the file in place, and core
|
|
541
602
|
// saves none once this machine removes it too (2026-10-24).
|
|
542
|
-
const changed =
|
|
603
|
+
const changed = [
|
|
604
|
+
...(await changedTracked(opts.root, base.commit, m, names(m.project, m.outputs))),
|
|
605
|
+
...strays,
|
|
606
|
+
]
|
|
543
607
|
const now = await stamps([...found.keys()])
|
|
544
608
|
const others = unchanged(found, now)
|
|
545
609
|
const removed = [...found.keys()]
|
|
@@ -558,6 +622,7 @@ export async function runWorker(opts: {
|
|
|
558
622
|
: m.runKey
|
|
559
623
|
await reply(m, key, others, {
|
|
560
624
|
...res,
|
|
625
|
+
...(served > 0 ? { cpuMs: (res.cpuMs ?? 0) + served } : {}),
|
|
561
626
|
output: output.chunks(),
|
|
562
627
|
violations: back,
|
|
563
628
|
...(cpuShare === undefined ? {} : { cpuShare }),
|
|
@@ -616,13 +681,28 @@ export async function runWorker(opts: {
|
|
|
616
681
|
ahead.set(m.task, [...(ahead.get(m.task) ?? []), ...m.deps])
|
|
617
682
|
} else if (m.type === 'cancel') {
|
|
618
683
|
running.get(m.task)?.abort()
|
|
684
|
+
} else if (m.type === 'unserve') {
|
|
685
|
+
// Not awaited: its grace must not hold the next order.
|
|
686
|
+
const task = m.task
|
|
687
|
+
void floor
|
|
688
|
+
?.stop(task)
|
|
689
|
+
.catch((err: unknown) =>
|
|
690
|
+
ui.print(
|
|
691
|
+
'err',
|
|
692
|
+
ui.status(
|
|
693
|
+
'warn',
|
|
694
|
+
task,
|
|
695
|
+
`server not stopped: ${err instanceof Error ? err.message : String(err)}`,
|
|
696
|
+
),
|
|
697
|
+
),
|
|
698
|
+
)
|
|
619
699
|
} else if (m.type === 'batch') {
|
|
620
700
|
// Every batch of a run names the same orchestrator root.
|
|
621
701
|
root = m.root
|
|
622
702
|
realRoot = m.realRoot
|
|
623
703
|
batch = m.n
|
|
624
704
|
if (m.sandboxes !== undefined) {
|
|
625
|
-
if (floor === undefined) floor =
|
|
705
|
+
if (floor === undefined) floor = newFloor(m.sandboxes)
|
|
626
706
|
else await floor.rearm(m.sandboxes)
|
|
627
707
|
}
|
|
628
708
|
for (const t of m.tasks) {
|
|
@@ -660,6 +740,8 @@ export async function runWorker(opts: {
|
|
|
660
740
|
process.off('SIGINT', onInt)
|
|
661
741
|
process.off('SIGTERM', onTerm)
|
|
662
742
|
trees.stop()
|
|
743
|
+
serverTrees.stop()
|
|
744
|
+
clearInterval(sampler)
|
|
663
745
|
leaving.abort()
|
|
664
746
|
for (const c of running.values()) c.abort()
|
|
665
747
|
await Promise.all(work)
|
|
@@ -716,6 +798,13 @@ function cpuTimes(): [number, number] {
|
|
|
716
798
|
return [total - idle, total]
|
|
717
799
|
}
|
|
718
800
|
|
|
801
|
+
function selfAvg(from: NodeJS.CpuUsage, since: number): { self?: number } {
|
|
802
|
+
const ms = performance.now() - since
|
|
803
|
+
if (ms <= 0) return {}
|
|
804
|
+
const u = process.cpuUsage(from)
|
|
805
|
+
return { self: Math.round(((u.user + u.system) / 1000 / ms) * 1000) / 1000 }
|
|
806
|
+
}
|
|
807
|
+
|
|
719
808
|
/**
|
|
720
809
|
* The share of a core each runnable thread gets now: the machine's cores
|
|
721
810
|
* over the threads Linux has runnable (`procs_running`, less the one
|
|
@@ -740,7 +829,9 @@ export function crowdShare(stat: string, cores: number): number | undefined {
|
|
|
740
829
|
* more than the threads that wait for a core: a GitHub macOS runner
|
|
741
830
|
* read 49 with two test shards on 3 cores and a core idle, and the run
|
|
742
831
|
* learned each shard needed the whole machine (main 8fa3c98). So a
|
|
743
|
-
*
|
|
832
|
+
* machine whose cores were not all busy since the last look is not
|
|
833
|
+
* crowded: Linux's run queue read a share of 0.67 on machines 83% busy,
|
|
834
|
+
* and the run booked tasks 1.5 times the CPU they used (2026-11-07).
|
|
744
835
|
*/
|
|
745
836
|
export function loadMeter(
|
|
746
837
|
read: () => string | undefined = procStat,
|
|
@@ -755,6 +846,8 @@ export function loadMeter(
|
|
|
755
846
|
} {
|
|
756
847
|
let last = times()
|
|
757
848
|
const first = last
|
|
849
|
+
const self = process.cpuUsage()
|
|
850
|
+
const since = performance.now()
|
|
758
851
|
let cpu = 0
|
|
759
852
|
const timer = setInterval(() => {
|
|
760
853
|
const next = times()
|
|
@@ -770,14 +863,16 @@ export function loadMeter(
|
|
|
770
863
|
const stat = read()
|
|
771
864
|
const avg = stat === undefined ? average() : 0
|
|
772
865
|
let full = true
|
|
773
|
-
if (stat
|
|
866
|
+
if (stat !== undefined || avg > 0) {
|
|
774
867
|
const now = times()
|
|
775
868
|
full = now[1] <= looked[1] || (now[0] - looked[0]) / (now[1] - looked[1]) >= FULL
|
|
776
869
|
looked = now
|
|
777
870
|
}
|
|
778
871
|
const share =
|
|
779
872
|
stat !== undefined
|
|
780
|
-
?
|
|
873
|
+
? full
|
|
874
|
+
? crowdShare(stat, cores)
|
|
875
|
+
: 1
|
|
781
876
|
: avg > 0
|
|
782
877
|
? full
|
|
783
878
|
? Math.min(1, cores / avg)
|
|
@@ -797,6 +892,7 @@ export function loadMeter(
|
|
|
797
892
|
cpu: Math.round(cpu * 100) / 100,
|
|
798
893
|
memory: os.totalmem() - available(),
|
|
799
894
|
...(total > 0 ? { avg: Math.round(((t[0] - first[0]) / total) * cores * 100) / 100 } : {}),
|
|
895
|
+
...selfAvg(self, since),
|
|
800
896
|
}
|
|
801
897
|
},
|
|
802
898
|
mark: () => ({ ...crowd, times: times() }),
|
|
@@ -840,6 +936,8 @@ export function treeMeter(
|
|
|
840
936
|
watch: (task: string, pid: number) => void
|
|
841
937
|
drop: (task: string) => void
|
|
842
938
|
now: () => number
|
|
939
|
+
/** One watched tree's cores over the last look. */
|
|
940
|
+
coresOf: (task: string) => number
|
|
843
941
|
stop: () => void
|
|
844
942
|
} {
|
|
845
943
|
const watched = new Map<string, { pid: number; at: number; cpuMs: number; cores: number }>()
|
|
@@ -887,6 +985,7 @@ export function treeMeter(
|
|
|
887
985
|
for (const w of watched.values()) cores += w.cores
|
|
888
986
|
return Math.round(cores * 100) / 100
|
|
889
987
|
},
|
|
988
|
+
coresOf: (task) => watched.get(task)?.cores ?? 0,
|
|
890
989
|
stop,
|
|
891
990
|
}
|
|
892
991
|
}
|
|
@@ -978,6 +1077,38 @@ export async function changedTracked(
|
|
|
978
1077
|
.filter((f) => f !== '' && !output(f) && !m.nested.some((n) => f.startsWith(`${n}/`)))
|
|
979
1078
|
}
|
|
980
1079
|
|
|
1080
|
+
/**
|
|
1081
|
+
* Untracked files git does not ignore under the task's project,
|
|
1082
|
+
* workspace-relative, those `held` matches (its outputs, its dependencies')
|
|
1083
|
+
* and nested projects' aside.
|
|
1084
|
+
*/
|
|
1085
|
+
async function untrackedUnder(
|
|
1086
|
+
root: string,
|
|
1087
|
+
m: RunOrder,
|
|
1088
|
+
held: readonly ((file: string) => boolean)[],
|
|
1089
|
+
): Promise<string[]> {
|
|
1090
|
+
const p = Bun.spawn(
|
|
1091
|
+
[
|
|
1092
|
+
'git',
|
|
1093
|
+
'ls-files',
|
|
1094
|
+
'--others',
|
|
1095
|
+
'--exclude-standard',
|
|
1096
|
+
'-z',
|
|
1097
|
+
'--',
|
|
1098
|
+
m.project === '' ? '.' : `:(literal)${m.project}`,
|
|
1099
|
+
],
|
|
1100
|
+
{ cwd: root, stdin: 'ignore', stdout: 'pipe', stderr: 'ignore' },
|
|
1101
|
+
)
|
|
1102
|
+
const [out, exit] = await Promise.all([new Response(p.stdout).text(), p.exited])
|
|
1103
|
+
// A git that could not say is no proof there are none.
|
|
1104
|
+
if (exit !== 0) return [m.project === '' ? '.' : m.project]
|
|
1105
|
+
return out
|
|
1106
|
+
.split('\0')
|
|
1107
|
+
.filter(
|
|
1108
|
+
(f) => f !== '' && !held.some((h) => h(f)) && !m.nested.some((n) => f.startsWith(`${n}/`)),
|
|
1109
|
+
)
|
|
1110
|
+
}
|
|
1111
|
+
|
|
981
1112
|
/** The checkout's tracked files, relative to `root`; none outside git. */
|
|
982
1113
|
export function trackedFiles(root: string): Set<string> {
|
|
983
1114
|
const r = Bun.spawnSync(['git', 'ls-files', '-z'], { cwd: root, stderr: 'pipe' })
|
|
@@ -1007,23 +1138,25 @@ function available(): number {
|
|
|
1007
1138
|
|
|
1008
1139
|
/**
|
|
1009
1140
|
* The orchestrator's env for a task, made this machine's: its checkout root
|
|
1010
|
-
* swapped for ours, host names from our own env
|
|
1011
|
-
* the workspace's bin directories.
|
|
1141
|
+
* swapped for ours, host names from our own env but those its key folds as
|
|
1142
|
+
* `unset`, and PATH our own behind the workspace's bin directories.
|
|
1012
1143
|
*/
|
|
1013
1144
|
export function localEnv(
|
|
1014
1145
|
env: Readonly<Record<string, string>>,
|
|
1015
1146
|
remoteRoot: string,
|
|
1016
1147
|
root: string,
|
|
1148
|
+
unset: readonly string[] = [],
|
|
1017
1149
|
): Record<string, string | undefined> {
|
|
1018
1150
|
// By path: a plain replace moved `/ws2` and `/private/ws` with `/ws`.
|
|
1019
1151
|
const out: Record<string, string | undefined> = swapRoots({ ...env }, [[remoteRoot, root]])
|
|
1020
|
-
// Sent only when the task's `env.define` names it
|
|
1152
|
+
// Sent only when the task's `env.define` names it or its `inputs.env`
|
|
1153
|
+
// keys it: its value, as keyed.
|
|
1021
1154
|
// Else this machine's manager, as core sets it on a local task.
|
|
1022
|
-
if (!('npm_execpath' in out)) {
|
|
1155
|
+
if (!('npm_execpath' in out) && !unset.includes('npm_execpath')) {
|
|
1023
1156
|
const manager = packageManagerPath(root)
|
|
1024
1157
|
if (manager !== null) out['npm_execpath'] = manager
|
|
1025
1158
|
}
|
|
1026
|
-
for (const k of HOST_ENV) if (!(k in out)) out[k] = process.env[k]
|
|
1159
|
+
for (const k of HOST_ENV) if (!(k in out) && !unset.includes(k)) out[k] = process.env[k]
|
|
1027
1160
|
const bins = (out['PATH'] ?? '').split(':').filter((p) => p === root || p.startsWith(`${root}/`))
|
|
1028
1161
|
out['PATH'] = [...bins, process.env['PATH'] ?? ''].join(':')
|
|
1029
1162
|
return out
|