@vzn/vx 0.0.615 → 0.0.617

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.615",
3
+ "version": "0.0.617",
4
4
  "description": "The fastest task runner and build cache for JavaScript monorepos.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -21,10 +21,10 @@
21
21
  "node": ">=18"
22
22
  },
23
23
  "optionalDependencies": {
24
- "@vzn/vx-linux-x64": "0.0.615",
25
- "@vzn/vx-linux-arm64": "0.0.615",
26
- "@vzn/vx-darwin-x64": "0.0.615",
27
- "@vzn/vx-darwin-arm64": "0.0.615"
24
+ "@vzn/vx-linux-x64": "0.0.617",
25
+ "@vzn/vx-linux-arm64": "0.0.617",
26
+ "@vzn/vx-darwin-x64": "0.0.617",
27
+ "@vzn/vx-darwin-arm64": "0.0.617"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -24,7 +24,17 @@ export function tieredReverseDepCount(
24
24
  ): Map<string, number> {
25
25
  const exec = new Map<string, TaskNode>()
26
26
  for (const [id, node] of nodes) if (!restoreTier.has(id)) exec.set(id, node)
27
- const counts = computeReverseDepCount(exec)
27
+ // An exec tier with no edge inside it (a warm run's groups) counts 0
28
+ // everywhere: no closure to build.
29
+ let linked = false
30
+ for (const node of exec.values()) {
31
+ if (node.deps.some((d) => exec.has(d))) {
32
+ linked = true
33
+ break
34
+ }
35
+ }
36
+ const counts = linked ? computeReverseDepCount(exec) : new Map<string, number>()
37
+ if (!linked) for (const id of exec.keys()) counts.set(id, 0)
28
38
  let feeds = false
29
39
  for (const node of exec.values()) {
30
40
  const weight = 1 + counts.get(node.id)!
@@ -324,6 +324,9 @@ export interface ScheduleOptions {
324
324
  class ReadyHeap {
325
325
  private readonly ids: string[] = []
326
326
  private readonly seq: number[] = []
327
+ // Each slot's priority, read once at push: a map lookup per comparison
328
+ // was two per heap level on every push and pop.
329
+ private readonly prio: number[] = []
327
330
  private next = 0
328
331
  constructor(private readonly priority: ReadonlyMap<string, number>) {}
329
332
  get size(): number {
@@ -331,8 +334,8 @@ class ReadyHeap {
331
334
  }
332
335
  /** True if slot i outranks slot j (higher priority, or equal priority + earlier seq). */
333
336
  private higher(i: number, j: number): boolean {
334
- const pi = this.priority.get(this.ids[i]!) ?? 0
335
- const pj = this.priority.get(this.ids[j]!) ?? 0
337
+ const pi = this.prio[i]!
338
+ const pj = this.prio[j]!
336
339
  return pi !== pj ? pi > pj : this.seq[i]! < this.seq[j]!
337
340
  }
338
341
  private swap(i: number, j: number): void {
@@ -342,6 +345,9 @@ class ReadyHeap {
342
345
  const si = this.seq[i]!
343
346
  this.seq[i] = this.seq[j]!
344
347
  this.seq[j] = si
348
+ const pi = this.prio[i]!
349
+ this.prio[i] = this.prio[j]!
350
+ this.prio[j] = pi
345
351
  }
346
352
  /**
347
353
  * `seq` defaults to a fresh monotonic counter. A parked-then-repushed
@@ -361,6 +367,7 @@ class ReadyHeap {
361
367
  push(id: string, seq: number = this.next++): void {
362
368
  this.ids.push(id)
363
369
  this.seq.push(seq)
370
+ this.prio.push(this.priority.get(id) ?? 0)
364
371
  let i = this.ids.length - 1
365
372
  while (i > 0) {
366
373
  const parent = (i - 1) >> 1
@@ -379,9 +386,11 @@ class ReadyHeap {
379
386
  const top = this.ids[0]!
380
387
  const lastId = this.ids.pop()!
381
388
  const lastSeq = this.seq.pop()!
389
+ const lastPrio = this.prio.pop()!
382
390
  if (n > 1) {
383
391
  this.ids[0] = lastId
384
392
  this.seq[0] = lastSeq
393
+ this.prio[0] = lastPrio
385
394
  let i = 0
386
395
  for (;;) {
387
396
  const l = 2 * i + 1
@@ -701,14 +710,18 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
701
710
  // could fail anyway.
702
711
  const aborted = (): boolean => options.signal?.aborted === true
703
712
  const serverDied = options.serverDied ?? ((): boolean => false)
704
- const deadServerVia = (id: string): string | undefined =>
705
- deadServerBehind(nodes, serverDied, id)
706
713
  const servers =
707
714
  options.serverDied === undefined
708
715
  ? []
709
716
  : [...nodes.values()]
710
717
  .filter((n) => n.config.exec?.persistent !== undefined)
711
718
  .map((n) => n.id)
719
+ // Only a server can die: with none in the graph, no walk (a stack and
720
+ // a set per dependency of every exec-tier task, X-171).
721
+ const deadServerVia =
722
+ servers.length === 0
723
+ ? (): undefined => undefined
724
+ : (id: string): string | undefined => deadServerBehind(nodes, serverDied, id)
712
725
  const willSkip = (id: string): boolean => {
713
726
  if (failFastTripped || aborted()) return true
714
727
  if (continueMode === 'never' && servers.some(serverDied)) {
@@ -76,12 +76,13 @@ export { restoreHit, type RestoreHitArgs } from './hit-restore.js'
76
76
  import type { DeferredOutputs } from './deferred-outputs.js'
77
77
  import type { Logger } from './logger.js'
78
78
  import {
79
- computeGroupKey,
79
+ computeGroupHash,
80
80
  computeTaskHash,
81
81
  describeTaskInputs,
82
82
  type HashCache,
83
83
  movedInput,
84
84
  type TaskInputComponent,
85
+ type UpfrontGroupKey,
85
86
  } from './task-hash.js'
86
87
  import { getContext } from './remote-prefetch.js'
87
88
  import { expandGroupUpstream, filterUpstreamHashes } from './upstream.js'
@@ -190,6 +191,11 @@ export interface ExecuteArgs {
190
191
  * used verbatim instead of deriving the same key again.
191
192
  */
192
193
  upfrontKey?: string
194
+ /**
195
+ * An unkeyed group's up-front key (`deriveStableKeys`' `groupKeys`): used
196
+ * verbatim when the live upstream folds the same ids and keys.
197
+ */
198
+ upfrontGroupKey?: UpfrontGroupKey
193
199
  /**
194
200
  * Start the sandbox runtime, on the first task that executes inside one
195
201
  * (run.ts, `prepareSandbox`). Absent when no task in the run declares a
@@ -263,17 +269,25 @@ export async function executeTask(args: ExecuteArgs): Promise<TaskOutcome> {
263
269
  */
264
270
  async function executeGroupTask(args: ExecuteArgs): Promise<TaskOutcome> {
265
271
  const wallclockNs = process.hrtime.bigint() - args.runStartHrTimeNs
266
- const hash = await computeGroupKey({
267
- node: args.node,
268
- upstream: args.upstream,
269
- workspaceRoot: args.workspaceRoot,
270
- workspaceFingerprint: args.workspaceFingerprint,
271
- cache: args.cache,
272
- forwardArgs: args.forwardArgs,
273
- nestedProjectDirs: args.nestedProjectDirs,
274
- ...(args.gitFilesCache !== undefined ? { gitFilesCache: args.gitFilesCache } : {}),
275
- ...(args.hashCache !== undefined ? { hashCache: args.hashCache } : {}),
276
- })
272
+ // `computeGroupKey`: an unkeyed group's key is its upstream's alone, so
273
+ // the up-front one stands when that upstream is unchanged.
274
+ const upfront = args.upfrontGroupKey
275
+ const hash =
276
+ args.node.config.cache === undefined
277
+ ? upfront !== undefined && sameUpstream(args.upstream, upfront.upstream)
278
+ ? upfront.key
279
+ : computeGroupHash(args.upstream)
280
+ : await computeTaskHash({
281
+ node: args.node,
282
+ upstream: args.upstream,
283
+ workspaceRoot: args.workspaceRoot,
284
+ workspaceFingerprint: args.workspaceFingerprint,
285
+ cache: args.cache,
286
+ forwardArgs: args.forwardArgs,
287
+ nestedProjectDirs: args.nestedProjectDirs,
288
+ ...(args.gitFilesCache !== undefined ? { gitFilesCache: args.gitFilesCache } : {}),
289
+ ...(args.hashCache !== undefined ? { hashCache: args.hashCache } : {}),
290
+ })
277
291
  return {
278
292
  node: args.node,
279
293
  status: 'success',
@@ -288,6 +302,17 @@ async function executeGroupTask(args: ExecuteArgs): Promise<TaskOutcome> {
288
302
  }
289
303
  }
290
304
 
305
+ /** The same ids with the same keys, in order: what `computeGroupHash` reads. */
306
+ function sameUpstream(live: TaskOutcome[], upfront: TaskOutcome[]): boolean {
307
+ if (live.length !== upfront.length) return false
308
+ for (let i = 0; i < live.length; i++) {
309
+ const a = live[i]
310
+ const b = upfront[i]!
311
+ if (a === undefined || a.node.id !== b.node.id || a.hash !== b.hash) return false
312
+ }
313
+ return true
314
+ }
315
+
291
316
  /**
292
317
  * Persistent task: dev server / file watcher / daemon. Spawn, wait
293
318
  * for ready (regex match or immediate), stash the subprocess in the
@@ -45,7 +45,7 @@ import { normalizeGlob, relPosix, span, staticPrefix } from '../util/index.js'
45
45
  import type { CacheEntry, CacheLayer, GitFilesCache } from '../cache/index.js'
46
46
  import type { TaskNode } from '../graph/index.js'
47
47
  import { deriveStableKeys, workspaceInputsReach } from './stable-keys.js'
48
- import type { HashCache } from './task-hash.js'
48
+ import type { HashCache, UpfrontGroupKey } from './task-hash.js'
49
49
  import { getContext } from './remote-prefetch.js'
50
50
 
51
51
  export interface ShortCircuitArgs {
@@ -91,6 +91,8 @@ export interface ShortCircuit {
91
91
  * again (`deriveStableKeys`' `uncachedKeys`).
92
92
  */
93
93
  uncachedKeys: Map<string, string>
94
+ /** Unkeyed group id → its up-front key (`deriveStableKeys`' `groupKeys`). */
95
+ groupKeys: Map<string, UpfrontGroupKey>
94
96
  }
95
97
 
96
98
  /**
@@ -105,11 +107,17 @@ export interface ShortCircuit {
105
107
  export async function startLocalShortCircuit(args: ShortCircuitArgs): Promise<ShortCircuit> {
106
108
  let stableKeys
107
109
  const uncachedKeys = new Map<string, string>()
110
+ const groupKeys = new Map<string, UpfrontGroupKey>()
108
111
  const endKeys = span('stable keys')
109
112
  try {
110
- stableKeys = await deriveStableKeys({ ...args, uncachedKeys })
113
+ stableKeys = await deriveStableKeys({ ...args, uncachedKeys, groupKeys })
111
114
  } catch {
112
- return { preProbed: new Map(), restoreTier: new Set(), uncachedKeys: new Map() }
115
+ return {
116
+ preProbed: new Map(),
117
+ restoreTier: new Set(),
118
+ uncachedKeys: new Map(),
119
+ groupKeys: new Map(),
120
+ }
113
121
  } finally {
114
122
  endKeys()
115
123
  }
@@ -120,14 +128,14 @@ export async function startLocalShortCircuit(args: ShortCircuitArgs): Promise<Sh
120
128
  const preProbed = new Map<string, ProbedEntry>()
121
129
  const restoreTier = new Set<string>()
122
130
  if (candidates.length === 0 && uncachedKeys.size === 0) {
123
- return { preProbed, restoreTier, uncachedKeys }
131
+ return { preProbed, restoreTier, uncachedKeys, groupKeys }
124
132
  }
125
133
 
126
134
  const keptOut = restoreTierExclusions(args.nodes, args.workspaceRoot)
127
135
  // A root-anchored output may land in a project no edge leads from, so an
128
136
  // uncached key there is derived again once the writer may have run.
129
137
  for (const id of keptOut) uncachedKeys.delete(id)
130
- if (candidates.length === 0) return { preProbed, restoreTier, uncachedKeys }
138
+ if (candidates.length === 0) return { preProbed, restoreTier, uncachedKeys, groupKeys }
131
139
 
132
140
  // Bounded pool over the stable candidates: probe local ONCE each. A
133
141
  // confirmed hit becomes restore-tier (unless workspace outputs disable
@@ -153,7 +161,7 @@ export async function startLocalShortCircuit(args: ShortCircuitArgs): Promise<Sh
153
161
  preProbed.set(node.id, { hash, hit })
154
162
  if (hit !== null && !keptOut.has(node.id)) restoreTier.add(node.id)
155
163
  }
156
- return { preProbed, restoreTier, uncachedKeys }
164
+ return { preProbed, restoreTier, uncachedKeys, groupKeys }
157
165
  } catch {
158
166
  // Fall through to the per-hash pool, which isolates a failing probe
159
167
  // to its own task.
@@ -176,7 +184,7 @@ export async function startLocalShortCircuit(args: ShortCircuitArgs): Promise<Sh
176
184
  }
177
185
  await Promise.all(Array.from({ length: workers }, () => pump()))
178
186
 
179
- return { preProbed, restoreTier, uncachedKeys }
187
+ return { preProbed, restoreTier, uncachedKeys, groupKeys }
180
188
  }
181
189
 
182
190
  /**
@@ -54,6 +54,7 @@ import { isDefaultBuild } from './projects.js'
54
54
  import { prepareSandbox } from './sandbox-request.js'
55
55
  import type { OutputDirSnapshot, SaveFacts } from './miss-save.js'
56
56
  import { admitTasks, taintTracker } from './admission.js'
57
+ import type { ExecuteArgs } from './execute-task.js'
57
58
  import { excludedTaint } from './excluded-keys.js'
58
59
  import { keyUpstream } from './upstream.js'
59
60
  import { busLogger, createEventBus, terminalSubscriber, type EventBus } from './events.js'
@@ -116,6 +117,7 @@ const emptyShortCircuit = (): ShortCircuit => ({
116
117
  preProbed: new Map(),
117
118
  restoreTier: new Set(),
118
119
  uncachedKeys: new Map(),
120
+ groupKeys: new Map(),
119
121
  })
120
122
 
121
123
  /**
@@ -938,9 +940,19 @@ async function runOnBus(
938
940
  // The scheduler's fail-fast stop, as the retry loop of a task already
939
941
  // in flight sees it.
940
942
  const failFast = new AbortController()
941
- const buildExecuteArgs = (node: TaskNode, upstream: TaskOutcome[], reuseProbe = true) => {
943
+ // A literal of the fields every task carries, then each optional one
944
+ // assigned only when present: twelve conditional spreads per task cost
945
+ // ~18 ms of object copying on a 1,090-package warm run (X-149).
946
+ const track = telemetry?.track
947
+ const armSandbox = sandboxArmer !== null ? () => sandboxArmer.arm() : undefined
948
+ const buildExecuteArgs = (
949
+ node: TaskNode,
950
+ upstream: TaskOutcome[],
951
+ reuseProbe = true,
952
+ ): ExecuteArgs => {
942
953
  const probe = reuseProbe ? shortCircuit.preProbed.get(node.id) : undefined
943
954
  const upfrontKey = shortCircuit.uncachedKeys.get(node.id)
955
+ const upfrontGroupKey = shortCircuit.groupKeys.get(node.id)
944
956
  if (
945
957
  options.continueMode === 'always' &&
946
958
  node.deps.some((d) => deadServerBehind(nodes, serverDied, d) !== undefined)
@@ -948,7 +960,7 @@ async function runOnBus(
948
960
  taintSeeds.add(node.id)
949
961
  const tainted = taint.judge(node, upstream)
950
962
  if (tainted) taintedRan.add(node.id)
951
- return {
963
+ const a: ExecuteArgs = {
952
964
  node,
953
965
  upstream,
954
966
  workspaceRoot,
@@ -956,31 +968,18 @@ async function runOnBus(
956
968
  cache,
957
969
  cachePolicy: policy,
958
970
  forwardArgs: options.forwardArgs,
959
- ...(options.retries !== undefined ? { retries: options.retries } : {}),
960
- ...(taskTimeoutDefault !== undefined ? { timeout: taskTimeoutDefault } : {}),
961
971
  log,
962
972
  executor: placements.executors.get(node.id) ?? UNPLACED_EXECUTOR,
963
- ...(download.modeOf.get(node.id) === 'deferred' ? { download: 'deferred' as const } : {}),
964
973
  deferred: deferredOutputs,
965
- ...(placements.remoteOnlyNoop.has(node.id) ? { remoteOnlyNoop: true } : {}),
966
- ...(placements.remoteOnly.has(node.id) ? { remoteOnly: true } : {}),
967
974
  nestedProjectDirs: nestedDirsByProject.get(node.projectName) ?? [],
968
975
  cacheDir,
969
976
  runStartHrTimeNs,
970
977
  persistentRegistry,
971
978
  liveChildren,
972
- ...(telemetry?.track !== undefined ? { track: telemetry.track } : {}),
973
- ...(explainMiss !== undefined ? { explainMiss } : {}),
974
979
  gitFilesCache,
975
980
  hashCache,
976
981
  probesAfterWrites: lateProbes,
977
- ...(probe !== undefined ? { preProbed: probe } : {}),
978
- ...(upfrontKey !== undefined ? { upfrontKey } : {}),
979
- ...(tainted ? { taintedUpstream: true } : {}),
980
- ...(dependedOn.has(node.id) ? {} : { noDependants: true as const }),
981
- ...(holders.has(node.id) ? { terminal: true as const } : {}),
982
982
  fingerprintWatch,
983
- ...(sandboxArmer !== null ? { armSandbox: () => sandboxArmer.arm() } : {}),
984
983
  keyedProjects: keyed,
985
984
  outputDirSnapshots,
986
985
  deferSave: saveLane.defer,
@@ -988,6 +987,21 @@ async function runOnBus(
988
987
  stopSignal: stopRun.signal,
989
988
  failFast: failFast.signal,
990
989
  }
990
+ if (options.retries !== undefined) a.retries = options.retries
991
+ if (taskTimeoutDefault !== undefined) a.timeout = taskTimeoutDefault
992
+ if (download.modeOf.get(node.id) === 'deferred') a.download = 'deferred'
993
+ if (placements.remoteOnlyNoop.has(node.id)) a.remoteOnlyNoop = true
994
+ if (placements.remoteOnly.has(node.id)) a.remoteOnly = true
995
+ if (track !== undefined) a.track = track
996
+ if (explainMiss !== undefined) a.explainMiss = explainMiss
997
+ if (probe !== undefined) a.preProbed = probe
998
+ if (upfrontKey !== undefined) a.upfrontKey = upfrontKey
999
+ if (upfrontGroupKey !== undefined) a.upfrontGroupKey = upfrontGroupKey
1000
+ if (tainted) a.taintedUpstream = true
1001
+ if (!dependedOn.has(node.id)) a.noDependants = true
1002
+ if (holders.has(node.id)) a.terminal = true
1003
+ if (armSandbox !== undefined) a.armSandbox = armSandbox
1004
+ return a
991
1005
  }
992
1006
 
993
1007
  const executeWithDedup = admitTasks({
@@ -16,7 +16,12 @@ import type { CacheLayer, GitFilesCache } from '../cache/index.js'
16
16
  import { isLiteralPattern, normalizeGlob, relPosix } from '../util/index.js'
17
17
  import { commandWriteReach, mayWriteFingerprint, undeclaredWriteReach } from './sandbox-request.js'
18
18
  import { foldedDeps } from './keyed-projects.js'
19
- import { computeGroupKey, computeTaskHash, type HashCache } from './task-hash.js'
19
+ import {
20
+ computeGroupHash,
21
+ computeTaskHash,
22
+ type HashCache,
23
+ type UpfrontGroupKey,
24
+ } from './task-hash.js'
20
25
  import { filterUpstreamHashes, keyedOutcome, keyUpstream } from './upstream.js'
21
26
 
22
27
  export interface DeriveStableKeysArgs {
@@ -33,6 +38,11 @@ export interface DeriveStableKeysArgs {
33
38
  * that key reads, so execute-task need not derive it a second time.
34
39
  */
35
40
  uncachedKeys?: Map<string, string>
41
+ /**
42
+ * Filled with each unkeyed group's key and the upstream it folded:
43
+ * execute-task reuses the key when the live upstream matches.
44
+ */
45
+ groupKeys?: Map<string, UpfrontGroupKey>
36
46
  }
37
47
 
38
48
  export interface StableKey {
@@ -217,20 +227,27 @@ export async function deriveStableKeys(args: DeriveStableKeysArgs): Promise<Stab
217
227
  // dependents that filter inputs.tasks through the group still
218
228
  // cascade (and forward their producers, above). They inherit
219
229
  // instability from any unstable member.
220
- keyById.set(
221
- id,
222
- await computeGroupKey({
223
- node,
224
- upstream,
225
- workspaceRoot: args.workspaceRoot,
226
- workspaceFingerprint: args.workspaceFingerprint,
227
- cache: args.cache,
228
- forwardArgs: args.forwardArgs,
229
- nestedProjectDirs: args.nestedDirsByProject.get(node.projectName) ?? [],
230
- gitFilesCache: args.gitFilesCache,
231
- hashCache: args.hashCache,
232
- }),
233
- )
230
+ // `computeGroupKey`, with the unkeyed branch kept synchronous.
231
+ if (node.config.cache === undefined) {
232
+ const key = computeGroupHash(upstream)
233
+ keyById.set(id, key)
234
+ args.groupKeys?.set(id, { key, upstream })
235
+ } else {
236
+ keyById.set(
237
+ id,
238
+ await computeTaskHash({
239
+ node,
240
+ upstream,
241
+ workspaceRoot: args.workspaceRoot,
242
+ workspaceFingerprint: args.workspaceFingerprint,
243
+ cache: args.cache,
244
+ forwardArgs: args.forwardArgs,
245
+ nestedProjectDirs: args.nestedDirsByProject.get(node.projectName) ?? [],
246
+ gitFilesCache: args.gitFilesCache,
247
+ hashCache: args.hashCache,
248
+ }),
249
+ )
250
+ }
234
251
  if (node.deps.some((d) => unstableById.has(d))) unstableById.add(id)
235
252
  continue
236
253
  }
@@ -432,6 +432,16 @@ export async function computeGroupKey(args: ComputeHashArgs): Promise<string> {
432
432
  return computeTaskHash(args)
433
433
  }
434
434
 
435
+ /**
436
+ * An unkeyed group's key is `computeGroupHash` of its upstream alone, so a
437
+ * live upstream with the same ids and keys in the same order derives these
438
+ * bytes (execute-task's `sameUpstream`).
439
+ */
440
+ export interface UpfrontGroupKey {
441
+ key: string
442
+ upstream: TaskOutcome[]
443
+ }
444
+
435
445
  /**
436
446
  * Derive a stable hash for a group task (no `exec`) from its upstream
437
447
  * outcomes. Lets downstream tasks that filter `inputs.tasks` to
@@ -30,13 +30,9 @@ export function keyUpstream(node: TaskNode, upstream: TaskOutcome[]): TaskOutcom
30
30
 
31
31
  /** A dependency as a key reads it: only `node` and `hash` matter. */
32
32
  export function keyedOutcome(node: TaskNode, hash: string | undefined): TaskOutcome {
33
- return {
34
- node,
35
- status: 'success',
36
- exitCode: 0,
37
- durationMs: 0,
38
- ...(hash !== undefined ? { hash } : {}),
39
- }
33
+ const o: TaskOutcome = { node, status: 'success', exitCode: 0, durationMs: 0 }
34
+ if (hash !== undefined) o.hash = hash
35
+ return o
40
36
  }
41
37
 
42
38
  /** `node.deps` less its ordering-only edges: the dependencies a key may fold. */