@vzn/vx 0.0.621 → 0.0.623
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 +5 -5
- package/src/cli/why.ts +60 -2
- package/src/orchestrator/logger.ts +4 -0
- package/src/orchestrator/summary.ts +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.623",
|
|
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.
|
|
25
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
26
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
27
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
24
|
+
"@vzn/vx-linux-x64": "0.0.623",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.623",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.623",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.623"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cli/why.ts
CHANGED
|
@@ -9,7 +9,9 @@ import path from 'node:path'
|
|
|
9
9
|
import { Cache } from '../cache/index.js'
|
|
10
10
|
import { flagHint, formatValue, seeHelp } from './help.js'
|
|
11
11
|
import { splitTaskId } from '../graph/index.js'
|
|
12
|
+
import type { Database } from 'bun:sqlite'
|
|
12
13
|
import {
|
|
14
|
+
type CacheKeyDiff,
|
|
13
15
|
cacheKeyDiff,
|
|
14
16
|
explainCacheKeyQuery as explainCacheKey,
|
|
15
17
|
latestRunId,
|
|
@@ -232,9 +234,13 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
232
234
|
)
|
|
233
235
|
}
|
|
234
236
|
const diff = cacheKeyDiff(db, runId, taskId)
|
|
237
|
+
// An upstream change is never the cause, only its carrier: follow
|
|
238
|
+
// each moved dependency in this run down to the tasks whose own
|
|
239
|
+
// inputs moved, so one `vx why` names the file, not the next task.
|
|
240
|
+
const roots = rootCauses(db, runId, taskId, diff.entries)
|
|
235
241
|
|
|
236
242
|
if (parsed.format === 'json') {
|
|
237
|
-
process.stdout.write(`${JSON.stringify({ taskId, runId, why, diff })}\n`)
|
|
243
|
+
process.stdout.write(`${JSON.stringify({ taskId, runId, why, diff, roots })}\n`)
|
|
238
244
|
return 0
|
|
239
245
|
}
|
|
240
246
|
|
|
@@ -293,7 +299,22 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
293
299
|
` ${e.change.padEnd(7)} ${e.kind.padEnd(kindW)} ${printable(e.name)} ${beforeAfter}`.trimEnd(),
|
|
294
300
|
)
|
|
295
301
|
}
|
|
296
|
-
|
|
302
|
+
if (roots.length > 0) {
|
|
303
|
+
lines.push('', ' root cause:')
|
|
304
|
+
for (const r of roots.slice(0, ROOTS_SHOWN)) {
|
|
305
|
+
const what = r.entries.map((e) =>
|
|
306
|
+
e.name === e.kind ? e.kind : `${e.kind} ${printable(e.name)}`,
|
|
307
|
+
)
|
|
308
|
+
lines.push(
|
|
309
|
+
` ${r.chain.join(' \u2190 ')} \u2190 ${what.slice(0, 3).join(', ')}${what.length > 3 ? ` and ${what.length - 3} more` : ''}`,
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
if (roots.length > ROOTS_SHOWN) lines.push(` and ${roots.length - ROOTS_SHOWN} more`)
|
|
313
|
+
}
|
|
314
|
+
const rootKinds = roots.flatMap((r) => r.entries.map((e) => e.kind))
|
|
315
|
+
const kinds = [...new Set([...diff.entries.map((e) => e.kind), ...rootKinds])].filter(
|
|
316
|
+
(k) => k in WHAT_TO_DO && !(k === 'upstream' && roots.length > 0),
|
|
317
|
+
)
|
|
297
318
|
if (kinds.length > 0) {
|
|
298
319
|
lines.push('', ' what to do:')
|
|
299
320
|
// An upstream row names the task to ask next: the command, not a
|
|
@@ -321,3 +342,40 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
321
342
|
cache.close()
|
|
322
343
|
}
|
|
323
344
|
}
|
|
345
|
+
|
|
346
|
+
const ROOTS_SHOWN = 5
|
|
347
|
+
|
|
348
|
+
interface RootCause {
|
|
349
|
+
/** From the asked task down to the one whose own inputs moved. */
|
|
350
|
+
chain: string[]
|
|
351
|
+
entries: CacheKeyDiff['entries']
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* The tasks under `taskId`'s moved upstreams whose OWN key components
|
|
356
|
+
* moved in the same run, each with the chain that carried it up. A task
|
|
357
|
+
* visited once is not walked again (a diamond names its root once).
|
|
358
|
+
*/
|
|
359
|
+
function rootCauses(
|
|
360
|
+
db: Database,
|
|
361
|
+
runId: string,
|
|
362
|
+
taskId: string,
|
|
363
|
+
entries: CacheKeyDiff['entries'],
|
|
364
|
+
): RootCause[] {
|
|
365
|
+
const roots: RootCause[] = []
|
|
366
|
+
const seen = new Set([taskId])
|
|
367
|
+
const walk = (chain: string[], moved: CacheKeyDiff['entries']): void => {
|
|
368
|
+
for (const e of moved) {
|
|
369
|
+
if (e.kind !== 'upstream' || seen.has(e.name)) continue
|
|
370
|
+
seen.add(e.name)
|
|
371
|
+
const next = [...chain, e.name]
|
|
372
|
+
const d = cacheKeyDiff(db, runId, e.name)
|
|
373
|
+
if (!d.found) continue
|
|
374
|
+
const own = d.entries.filter((x) => x.kind !== 'upstream')
|
|
375
|
+
if (own.length > 0) roots.push({ chain: next, entries: own })
|
|
376
|
+
walk(next, d.entries)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
walk([taskId], entries)
|
|
380
|
+
return roots
|
|
381
|
+
}
|
|
@@ -283,6 +283,7 @@ export function defaultLogger(
|
|
|
283
283
|
let restoredLocal = 0
|
|
284
284
|
let restoredRemote = 0
|
|
285
285
|
let skippedCount = 0
|
|
286
|
+
let savedMs = 0
|
|
286
287
|
// Cache-miss duration spread, accumulated incrementally (the same
|
|
287
288
|
// numbers the final summary computes from the outcome list).
|
|
288
289
|
let spreadMax = 0
|
|
@@ -369,6 +370,7 @@ export function defaultLogger(
|
|
|
369
370
|
restoredRemote,
|
|
370
371
|
miss: succeeded + failed - noCache,
|
|
371
372
|
noCache,
|
|
373
|
+
savedMs,
|
|
372
374
|
left: total - done,
|
|
373
375
|
spread:
|
|
374
376
|
spreadCount > 0
|
|
@@ -698,10 +700,12 @@ export function defaultLogger(
|
|
|
698
700
|
case 'cache-hit':
|
|
699
701
|
if (outcome.restored === false) upToDate++
|
|
700
702
|
else restoredLocal++
|
|
703
|
+
savedMs += outcome.storedDurationMs ?? 0
|
|
701
704
|
break
|
|
702
705
|
case 'cache-hit-remote':
|
|
703
706
|
if (outcome.restored === false) upToDate++
|
|
704
707
|
else restoredRemote++
|
|
708
|
+
savedMs += outcome.storedDurationMs ?? 0
|
|
705
709
|
break
|
|
706
710
|
case 'skipped':
|
|
707
711
|
skippedCount++
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import type { TaskOutcome } from '../graph/index.js'
|
|
7
7
|
import { paint, type ColorSupport } from './colors.js'
|
|
8
8
|
import { tallyOutcomes } from './tally.js'
|
|
9
|
+
import { isCacheHit } from './telemetry.js'
|
|
9
10
|
import { outcomeLabel, projectOutcome, ranNoCache } from './events.js'
|
|
10
11
|
import { isGroupTask } from '../graph/index.js'
|
|
11
12
|
import { formatElapsed } from '../util/index.js'
|
|
@@ -119,6 +120,8 @@ export interface SummaryStats {
|
|
|
119
120
|
spread: { maxMs: number; minMs: number; sumMs: number; count: number } | null
|
|
120
121
|
/** Tasks an `admit` policy held with a worker free, and the waits summed. Absent with no hold. */
|
|
121
122
|
held?: { count: number; sumMs: number }
|
|
123
|
+
/** The hits' stored exec times summed: the work the cache skipped. */
|
|
124
|
+
savedMs?: number
|
|
122
125
|
/** A run of groups only: their names, so `0 tasks` says why. */
|
|
123
126
|
emptyGroups?: readonly string[]
|
|
124
127
|
}
|
|
@@ -320,6 +323,10 @@ function resultLine(
|
|
|
320
323
|
: `${hits} cached (${Math.floor((hits / cacheable) * 100)}%)`,
|
|
321
324
|
)
|
|
322
325
|
if (noCache > 0) parts.push(paint('', `${noCache} no-cache`, colors, { dim: true }))
|
|
326
|
+
// What the cache paid back, beside what the run cost: the stored exec
|
|
327
|
+
// time of every hit, never the restore this run spent on it.
|
|
328
|
+
if ((stats.savedMs ?? 0) >= 1)
|
|
329
|
+
parts.push(paint(SUCCESS, `${formatElapsed(stats.savedMs!)} saved`, colors))
|
|
323
330
|
parts.push(formatElapsed(totalMs))
|
|
324
331
|
return parts.join(` ${paint('', '\u00b7', colors, { dim: true })} `)
|
|
325
332
|
}
|
|
@@ -373,6 +380,9 @@ export function formatRunSummary(
|
|
|
373
380
|
const durations = outcomes
|
|
374
381
|
.filter((o) => (o.status === 'success' || o.status === 'failed') && !isGroupTask(o.node))
|
|
375
382
|
.map((o) => o.durationMs)
|
|
383
|
+
let savedMs = 0
|
|
384
|
+
for (const o of outcomes)
|
|
385
|
+
if (isCacheHit(o.status) && !isGroupTask(o.node)) savedMs += o.storedDurationMs ?? 0
|
|
376
386
|
const heldOutcomes = outcomes.filter((o) => o.admissionHeldMs !== undefined)
|
|
377
387
|
const notRun = outcomes.filter((o) => neverStarted(o) && !isGroupTask(o.node)).length
|
|
378
388
|
const emptyGroups = outcomes.every((o) => isGroupTask(o.node))
|
|
@@ -391,6 +401,7 @@ export function formatRunSummary(
|
|
|
391
401
|
restoredRemote: t.restoredRemote,
|
|
392
402
|
miss: t.total - t.skipped - (t.cachedLocal + t.cachedRemote) - noCache,
|
|
393
403
|
noCache,
|
|
404
|
+
savedMs,
|
|
394
405
|
spread:
|
|
395
406
|
durations.length > 0
|
|
396
407
|
? {
|