@vzn/vx-dte 0.0.651 → 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/src/worker.ts CHANGED
@@ -36,6 +36,7 @@ import {
36
36
  HOST_ENV,
37
37
  PART_LISTS,
38
38
  type DepOrder,
39
+ type WiredDep,
39
40
  type Load,
40
41
  type WorkerInfo,
41
42
  type RunOrder,
@@ -44,7 +45,8 @@ import {
44
45
  type WiredResult,
45
46
  } from './messages.js'
46
47
  import { leftUnder, names, outputFiles } from './outputs.js'
47
- import { snapshot } from './tree.js'
48
+ import { serverFit } from './server-fit.js'
49
+ import { snapshot, unfilter } from './tree.js'
48
50
  import { sealWorker, type Proof } from './seal.js'
49
51
  import { join, ORCHESTRATOR_SILENT, RelayError, unsent as unsentBy } from './wire.js'
50
52
 
@@ -65,6 +67,8 @@ export async function runWorker(opts: {
65
67
  // The tracked files as they are, as the run snapshots its own: HEAD's
66
68
  // tree let in a checkout whose prepare rewrote a tracked file, and the
67
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)
68
72
  const base = await snapshot(opts.root)
69
73
  const tree = base.tree
70
74
  // Never an output, as core never cleans one: the checkout is the run's
@@ -114,7 +118,9 @@ export async function runWorker(opts: {
114
118
  const artifacts = opts.artifacts(() => facts)
115
119
  const running = new Map<string, AbortController>()
116
120
  /** Each order's deps sent ahead of its batch, until the batch comes. */
117
- const ahead = new Map<string, DepOrder[]>()
121
+ const ahead = new Map<string, WiredDep[]>()
122
+ /** Each dependency sent whole, by its task: an order after names it alone (`WiredDep`). */
123
+ const known = new Map<string, DepOrder>()
118
124
  const work = new Set<Promise<void>>()
119
125
  let root = opts.root
120
126
  let realRoot = opts.root
@@ -129,7 +135,40 @@ export async function runWorker(opts: {
129
135
  }
130
136
  let tasksCpuMs = 0
131
137
  const trees = treeMeter()
132
- const frame = (): Load => ({ ...load.now(), tasks: trees.now() })
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
+ })
133
172
  /**
134
173
  * Entries whose files are here, restored or run here, by key: tasks
135
174
  * started together wait on one restore, never run before it.
@@ -440,6 +479,9 @@ export async function runWorker(opts: {
440
479
  ? undefined
441
480
  : new Set(m.sandbox.keyed.map((k) => path.join(opts.root, k))),
442
481
  m.nested.map((n) => path.join(opts.root, n)),
482
+ undefined,
483
+ (m.sandbox.reads ?? []).map((r) => path.join(opts.root, r)),
484
+ (m.sandbox.reported ?? []).map((r) => path.join(opts.root, r)),
443
485
  )
444
486
  const output = new BoundedCapture()
445
487
  const req: ExecuteRequest = {
@@ -448,7 +490,7 @@ export async function runWorker(opts: {
448
490
  command: m.command,
449
491
  forwardArgs: [],
450
492
  cwd: path.join(opts.root, m.cwd),
451
- env: localEnv(m.env, root, opts.root),
493
+ env: localEnv(m.env, root, opts.root, m.unset),
452
494
  envDefine: {},
453
495
  // Both streams as one capture, in the order written, as core keeps a
454
496
  // run's: kept apart, a hit replayed every stdout line before the
@@ -482,16 +524,40 @@ export async function runWorker(opts: {
482
524
  })),
483
525
  }),
484
526
  }
485
- floor ??= localFloor()
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()
486
534
  ui.print('out', ui.status('info', m.task, ui.dim('running')))
487
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
+ }
488
541
  const {
489
542
  outputs: _,
490
543
  stdout: _o,
491
544
  stderr: _e,
492
545
  ...res
493
- } = await floor.execute(req).finally(() => trees.drop(m.task))
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
+ })
494
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)
495
561
  const cpuShare = load.shareSince(
496
562
  crowd,
497
563
  res.cpuMs === undefined || !(res.durationMs > 0) ? undefined : res.cpuMs / res.durationMs,
@@ -534,7 +600,10 @@ export async function runWorker(opts: {
534
600
  // it; this entry is withheld for any. Nor for an additive task that
535
601
  // removed an upstream's file: a hit left the file in place, and core
536
602
  // saves none once this machine removes it too (2026-10-24).
537
- const changed = await changedTracked(opts.root, base.commit, m, names(m.project, m.outputs))
603
+ const changed = [
604
+ ...(await changedTracked(opts.root, base.commit, m, names(m.project, m.outputs))),
605
+ ...strays,
606
+ ]
538
607
  const now = await stamps([...found.keys()])
539
608
  const others = unchanged(found, now)
540
609
  const removed = [...found.keys()]
@@ -553,6 +622,7 @@ export async function runWorker(opts: {
553
622
  : m.runKey
554
623
  await reply(m, key, others, {
555
624
  ...res,
625
+ ...(served > 0 ? { cpuMs: (res.cpuMs ?? 0) + served } : {}),
556
626
  output: output.chunks(),
557
627
  violations: back,
558
628
  ...(cpuShare === undefined ? {} : { cpuShare }),
@@ -611,21 +681,53 @@ export async function runWorker(opts: {
611
681
  ahead.set(m.task, [...(ahead.get(m.task) ?? []), ...m.deps])
612
682
  } else if (m.type === 'cancel') {
613
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
+ )
614
699
  } else if (m.type === 'batch') {
615
700
  // Every batch of a run names the same orchestrator root.
616
701
  root = m.root
617
702
  realRoot = m.realRoot
618
703
  batch = m.n
619
704
  if (m.sandboxes !== undefined) {
620
- if (floor === undefined) floor = localFloor(m.sandboxes)
705
+ if (floor === undefined) floor = newFloor(m.sandboxes)
621
706
  else await floor.rearm(m.sandboxes)
622
707
  }
623
708
  for (const t of m.tasks) {
709
+ const sent = ahead.get(t.task)
710
+ ahead.delete(t.task)
711
+ // Learned in the order sent, a task run already too: the run counts them sent.
712
+ let missing: string | undefined
713
+ const deps = (sent === undefined ? t.deps : [...sent, ...t.deps]).map((d): DepOrder => {
714
+ if (typeof d !== 'string') {
715
+ known.set(d.task, d)
716
+ return d
717
+ }
718
+ const got = known.get(d)
719
+ if (got === undefined) missing ??= d
720
+ return got!
721
+ })
624
722
  // One it runs already is not run twice.
625
723
  if (running.has(t.task)) continue
626
- const deps = ahead.get(t.task)
627
- ahead.delete(t.task)
628
- start(deps === undefined ? t : { ...t, deps: [...deps, ...t.deps] })
724
+ if (missing !== undefined) {
725
+ const reason = `was sent ${missing} by name, never whole`
726
+ ui.print('err', ui.status('error', t.task, `could not run: ${reason}`))
727
+ await say({ type: 'fault', task: t.task, reason }).catch(unsent('fault'))
728
+ continue
729
+ }
730
+ start({ ...t, deps })
629
731
  }
630
732
  }
631
733
  }
@@ -638,6 +740,8 @@ export async function runWorker(opts: {
638
740
  process.off('SIGINT', onInt)
639
741
  process.off('SIGTERM', onTerm)
640
742
  trees.stop()
743
+ serverTrees.stop()
744
+ clearInterval(sampler)
641
745
  leaving.abort()
642
746
  for (const c of running.values()) c.abort()
643
747
  await Promise.all(work)
@@ -694,6 +798,13 @@ function cpuTimes(): [number, number] {
694
798
  return [total - idle, total]
695
799
  }
696
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
+
697
808
  /**
698
809
  * The share of a core each runnable thread gets now: the machine's cores
699
810
  * over the threads Linux has runnable (`procs_running`, less the one
@@ -718,7 +829,9 @@ export function crowdShare(stat: string, cores: number): number | undefined {
718
829
  * more than the threads that wait for a core: a GitHub macOS runner
719
830
  * read 49 with two test shards on 3 cores and a core idle, and the run
720
831
  * learned each shard needed the whole machine (main 8fa3c98). So a
721
- * Mac whose cores were not all busy since the last look is not crowded.
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).
722
835
  */
723
836
  export function loadMeter(
724
837
  read: () => string | undefined = procStat,
@@ -733,6 +846,8 @@ export function loadMeter(
733
846
  } {
734
847
  let last = times()
735
848
  const first = last
849
+ const self = process.cpuUsage()
850
+ const since = performance.now()
736
851
  let cpu = 0
737
852
  const timer = setInterval(() => {
738
853
  const next = times()
@@ -748,14 +863,16 @@ export function loadMeter(
748
863
  const stat = read()
749
864
  const avg = stat === undefined ? average() : 0
750
865
  let full = true
751
- if (stat === undefined && avg > 0) {
866
+ if (stat !== undefined || avg > 0) {
752
867
  const now = times()
753
868
  full = now[1] <= looked[1] || (now[0] - looked[0]) / (now[1] - looked[1]) >= FULL
754
869
  looked = now
755
870
  }
756
871
  const share =
757
872
  stat !== undefined
758
- ? crowdShare(stat, cores)
873
+ ? full
874
+ ? crowdShare(stat, cores)
875
+ : 1
759
876
  : avg > 0
760
877
  ? full
761
878
  ? Math.min(1, cores / avg)
@@ -775,6 +892,7 @@ export function loadMeter(
775
892
  cpu: Math.round(cpu * 100) / 100,
776
893
  memory: os.totalmem() - available(),
777
894
  ...(total > 0 ? { avg: Math.round(((t[0] - first[0]) / total) * cores * 100) / 100 } : {}),
895
+ ...selfAvg(self, since),
778
896
  }
779
897
  },
780
898
  mark: () => ({ ...crowd, times: times() }),
@@ -818,6 +936,8 @@ export function treeMeter(
818
936
  watch: (task: string, pid: number) => void
819
937
  drop: (task: string) => void
820
938
  now: () => number
939
+ /** One watched tree's cores over the last look. */
940
+ coresOf: (task: string) => number
821
941
  stop: () => void
822
942
  } {
823
943
  const watched = new Map<string, { pid: number; at: number; cpuMs: number; cores: number }>()
@@ -865,6 +985,7 @@ export function treeMeter(
865
985
  for (const w of watched.values()) cores += w.cores
866
986
  return Math.round(cores * 100) / 100
867
987
  },
988
+ coresOf: (task) => watched.get(task)?.cores ?? 0,
868
989
  stop,
869
990
  }
870
991
  }
@@ -956,6 +1077,38 @@ export async function changedTracked(
956
1077
  .filter((f) => f !== '' && !output(f) && !m.nested.some((n) => f.startsWith(`${n}/`)))
957
1078
  }
958
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
+
959
1112
  /** The checkout's tracked files, relative to `root`; none outside git. */
960
1113
  export function trackedFiles(root: string): Set<string> {
961
1114
  const r = Bun.spawnSync(['git', 'ls-files', '-z'], { cwd: root, stderr: 'pipe' })
@@ -985,23 +1138,25 @@ function available(): number {
985
1138
 
986
1139
  /**
987
1140
  * The orchestrator's env for a task, made this machine's: its checkout root
988
- * swapped for ours, host names from our own env, and PATH our own behind
989
- * 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.
990
1143
  */
991
1144
  export function localEnv(
992
1145
  env: Readonly<Record<string, string>>,
993
1146
  remoteRoot: string,
994
1147
  root: string,
1148
+ unset: readonly string[] = [],
995
1149
  ): Record<string, string | undefined> {
996
1150
  // By path: a plain replace moved `/ws2` and `/private/ws` with `/ws`.
997
1151
  const out: Record<string, string | undefined> = swapRoots({ ...env }, [[remoteRoot, root]])
998
- // Sent only when the task's `env.define` names it: its value, as keyed.
1152
+ // Sent only when the task's `env.define` names it or its `inputs.env`
1153
+ // keys it: its value, as keyed.
999
1154
  // Else this machine's manager, as core sets it on a local task.
1000
- if (!('npm_execpath' in out)) {
1155
+ if (!('npm_execpath' in out) && !unset.includes('npm_execpath')) {
1001
1156
  const manager = packageManagerPath(root)
1002
1157
  if (manager !== null) out['npm_execpath'] = manager
1003
1158
  }
1004
- 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]
1005
1160
  const bins = (out['PATH'] ?? '').split(':').filter((p) => p === root || p.startsWith(`${root}/`))
1006
1161
  out['PATH'] = [...bins, process.env['PATH'] ?? ''].join(':')
1007
1162
  return out