@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/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 { snapshot } from './tree.js'
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
- 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
+ })
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
- 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()
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.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
+ })
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 = 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
+ ]
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 = localFloor(m.sandboxes)
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
- * 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).
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 === undefined && avg > 0) {
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
- ? crowdShare(stat, cores)
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, and PATH our own behind
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: 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.
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