@vzn/vx 0.0.649 → 0.0.650
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/cache/cache.ts +24 -27
- package/src/cache/fold-memo.ts +8 -1
- package/src/cache/index.ts +11 -0
- package/src/cache/lockfile-memo.ts +119 -0
- package/src/cache/remote-hits.ts +9 -1
- package/src/cache/run-notes.ts +123 -0
- package/src/cache/schema.ts +43 -0
- package/src/cli/last.ts +12 -21
- package/src/cli/plugin-commands.ts +2 -1
- package/src/cli/why.ts +3 -3
- package/src/graph/scheduler.ts +11 -5
- package/src/history/history.ts +34 -2
- package/src/history/index.ts +3 -0
- package/src/index.ts +9 -2
- package/src/orchestrator/doctor.ts +2 -2
- package/src/orchestrator/hit-restore.ts +3 -0
- package/src/orchestrator/index.ts +0 -1
- package/src/orchestrator/live-usage.ts +93 -0
- package/src/orchestrator/lockfile-claim.ts +74 -79
- package/src/orchestrator/plugin.ts +2 -2
- package/src/orchestrator/prepare.ts +2 -1
- package/src/orchestrator/run-failures.ts +34 -113
- package/src/orchestrator/run.ts +47 -15
- package/src/orchestrator/steps.ts +2 -2
- package/src/report/events.ts +11 -3
- package/src/report/forecast.ts +24 -23
- package/src/report/framed-output.ts +156 -28
- package/src/report/index.ts +2 -1
- package/src/report/logger.ts +44 -15
- package/src/report/plain-output.ts +1 -1
- package/src/report/status-line.ts +38 -14
- package/src/report/summary.ts +4 -0
- package/src/util/colors.ts +3 -0
- package/src/util/hidden-prompt.ts +8 -2
- package/src/util/secrets.ts +3 -0
- package/src/orchestrator/run-steps.ts +0 -93
|
@@ -292,9 +292,14 @@ function paintTaskId(node: TaskNode, colors: ColorSupport, opts: { bold?: boolea
|
|
|
292
292
|
export const TIME_COL = 7 // "0ms" … "999.99s", then "16m 40s" … "99m 59s"
|
|
293
293
|
const STATUS_COL = 7 // "success" / "running" / "skipped" / "aborted" / "not run"
|
|
294
294
|
const CACHE_COL = 8 // "no-cache" / "remote" / "local" / "fresh" / "miss"
|
|
295
|
-
const BLOCKED_COL = 7 //
|
|
295
|
+
const BLOCKED_COL = 7 // `blocks`: how many tasks are downstream of the row's
|
|
296
|
+
const USUAL_COL = 7 // "~18s" live, "+6.0s" / "−1m 05s" after
|
|
297
|
+
const WAITED_COL = 7 // the time cell's width: "1.20s"
|
|
298
|
+
const CPU_COL = 5 // "180%", "1200%"
|
|
299
|
+
const MEM_COL = 6 // "412MB", "1.1GB", "1023MB"
|
|
300
|
+
const OUT_COL = 7 // `outputs`: "12MB", "900B"
|
|
301
|
+
const WORKER_COL = 12 // `local`, or a plugin's name for the machine, cut at 12
|
|
296
302
|
/** The list header's colour: violet-400, the summary rule's first stop. */
|
|
297
|
-
const HEADER = '#a78bfa'
|
|
298
303
|
|
|
299
304
|
/** Right-align the duration in a TIME_COL cell (pad on the left). */
|
|
300
305
|
function timeCell(ms: number | null, colors: ColorSupport): string {
|
|
@@ -375,10 +380,11 @@ function taskGlyph(o: TaskOutcome, colors: ColorSupport): string {
|
|
|
375
380
|
}
|
|
376
381
|
|
|
377
382
|
/**
|
|
378
|
-
* One reported task row: `<glyph> <time> <
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
383
|
+
* One reported task row on the list grid: `<glyph> <time> <usual> <waited>
|
|
384
|
+
* <status> <cache> <cpu> <mem> <blocks> <worker> <name>`. `glyph` is
|
|
385
|
+
* pre-painted; status/cache are raw text + color, padded to their
|
|
386
|
+
* columns; `paintedId` carries identity coloring. `ms = null` blanks the
|
|
387
|
+
* time (a skip never ran); `cache = ''` and an absent cell blank theirs.
|
|
382
388
|
*/
|
|
383
389
|
export function formatTaskRow(
|
|
384
390
|
glyph: string,
|
|
@@ -389,32 +395,118 @@ export function formatTaskRow(
|
|
|
389
395
|
cacheColor: string,
|
|
390
396
|
paintedId: string,
|
|
391
397
|
colors: ColorSupport = NO_COLOR,
|
|
392
|
-
|
|
393
|
-
blocked?: string,
|
|
398
|
+
cells: RowCells = {},
|
|
394
399
|
): string {
|
|
395
400
|
const st = cell(status, statusColor, STATUS_COL, colors)
|
|
396
401
|
// `aborting` runs one into the cache cell, which is blank beside it.
|
|
397
402
|
const ca = cell(cache, cacheColor, CACHE_COL - Math.max(0, status.length - STATUS_COL), colors)
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
+
const usual = cells.usual ?? { text: '', painted: '' }
|
|
404
|
+
const us = `${' '.repeat(Math.max(0, USUAL_COL - usual.text.length))}${usual.painted}`
|
|
405
|
+
const waited = cells.waitedMs === undefined ? '' : formatElapsed(cells.waitedMs)
|
|
406
|
+
const cpu = cells.cpu === undefined ? '' : `${Math.round(cells.cpu * 100)}%`
|
|
407
|
+
const mem = cells.memBytes === undefined ? '' : formatSize(cells.memBytes)
|
|
408
|
+
const out = cells.outputBytes === undefined ? '' : formatSize(cells.outputBytes)
|
|
409
|
+
const blocks = cells.blocks ?? ''
|
|
410
|
+
const worker = workerName(cells.worker ?? '')
|
|
411
|
+
return (
|
|
412
|
+
` ${glyph} ${timeCell(ms, colors)} ${us} ${right(waited, WAITED_COL, colors)} ${st} ${ca} ` +
|
|
413
|
+
`${right(cpu, CPU_COL, colors)} ${right(mem, MEM_COL, colors)} ${right(out, OUT_COL, colors)} ` +
|
|
414
|
+
`${' '.repeat(Math.max(0, BLOCKED_COL - blocks.length))}${paint(WARN, blocks, colors)} ` +
|
|
415
|
+
`${worker === '' ? '' : ui.inline(ui.place(worker), colors)}${' '.repeat(WORKER_COL - worker.length)} ${paintedId}`
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** A row's cells past time, status and cache; each blank when absent. */
|
|
420
|
+
export interface RowCells {
|
|
421
|
+
/** After the time, pre-painted: `~18s` live, `+6.0s` / `−1.4s` after. */
|
|
422
|
+
usual?: { text: string; painted: string }
|
|
423
|
+
/** How long the task waited ready for a slot. */
|
|
424
|
+
waitedMs?: number
|
|
425
|
+
/** Cores in use (live) or on average (after): 1.8 prints `180%`. */
|
|
426
|
+
cpu?: number
|
|
427
|
+
/** Resident memory now (live) or at its peak (after). */
|
|
428
|
+
memBytes?: number
|
|
429
|
+
/** The bytes of the task's outputs: what its save stored, or its hit restored. */
|
|
430
|
+
outputBytes?: number
|
|
431
|
+
/** How many tasks are downstream. */
|
|
432
|
+
blocks?: string
|
|
433
|
+
/** The machine it runs or ran on: `local`, or the executor's name for it. */
|
|
434
|
+
worker?: string
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function right(text: string, width: number, colors: ColorSupport): string {
|
|
438
|
+
return ' '.repeat(Math.max(0, width - text.length)) + paint('', text, colors, { dim: true })
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** `900B`, `412MB`, `1.1GB`: one width for a size, whatever it is. */
|
|
442
|
+
export function formatSize(bytes: number): string {
|
|
443
|
+
if (bytes < 1024) return `${bytes}B`
|
|
444
|
+
const mb = bytes / 1024 / 1024
|
|
445
|
+
if (mb < 1) return `${Math.round(bytes / 1024)}KB`
|
|
446
|
+
if (mb < 9.95) return `${mb.toFixed(1)}MB`
|
|
447
|
+
if (mb < 1000) return `${Math.round(mb)}MB`
|
|
448
|
+
const gb = mb / 1024
|
|
449
|
+
return gb < 10 ? `${gb.toFixed(1)}GB` : `${Math.round(gb)}GB`
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** A worker's name in its 12 columns: a longer one ends in `…`. */
|
|
453
|
+
function workerName(name: string): string {
|
|
454
|
+
return name.length > WORKER_COL ? `${name.slice(0, WORKER_COL - 1)}\u2026` : name
|
|
403
455
|
}
|
|
404
456
|
|
|
405
457
|
/**
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
* executor places tasks: the name after the id's dot. `blocked` on the live
|
|
409
|
-
* list, whose rows count the tasks each running one holds back.
|
|
458
|
+
* A wait worth a cell: under 50 ms the task started at once, the
|
|
459
|
+
* scheduler's own tick and a sibling's spawn, not a full pool.
|
|
410
460
|
*/
|
|
411
|
-
export function
|
|
412
|
-
|
|
461
|
+
export function waited(ms: number | undefined): boolean {
|
|
462
|
+
return ms !== undefined && ms >= 50
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** A task's average cores over its run: CPU time over wall time. */
|
|
466
|
+
function coresOf(o: TaskOutcome): number | undefined {
|
|
467
|
+
return o.cpuMs === undefined || o.durationMs <= 0 ? undefined : o.cpuMs / o.durationMs
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* The results list's `±usual` cell: how far an executed task ran from its
|
|
472
|
+
* usual time, `+6.0s` red or `−1.4s` green, only past a fifth of the usual
|
|
473
|
+
* either way; blank with no usual time to compare.
|
|
474
|
+
*/
|
|
475
|
+
function usualDelta(
|
|
476
|
+
ms: number,
|
|
477
|
+
usualMs: number | undefined,
|
|
413
478
|
colors: ColorSupport = NO_COLOR,
|
|
414
|
-
|
|
415
|
-
)
|
|
416
|
-
|
|
417
|
-
|
|
479
|
+
): { text: string; painted: string } {
|
|
480
|
+
if (usualMs === undefined || usualMs <= 0 || Math.abs(ms - usualMs) <= usualMs * 0.2)
|
|
481
|
+
return { text: '', painted: '' }
|
|
482
|
+
const d = ms - usualMs
|
|
483
|
+
const text = `${d > 0 ? '+' : '\u2212'}${formatElapsed(Math.abs(d), true)}`
|
|
484
|
+
return { text, painted: paint(d > 0 ? ERROR : SUCCESS, text, colors) }
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* The header over a task list, on the row grid: dim and underlined across
|
|
489
|
+
* the whole line, gaps included, straight over its rows (bold in a hue, it
|
|
490
|
+
* blended into rows of the same hues).
|
|
491
|
+
*/
|
|
492
|
+
export function formatListHeader(colors: ColorSupport = NO_COLOR): string {
|
|
493
|
+
return header('\u00b1usual', colors)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* The live list's header: the usual duration where the results list has
|
|
498
|
+
* how far a task ran from it; the other columns are the results'.
|
|
499
|
+
*/
|
|
500
|
+
export function formatRunningHeader(colors: ColorSupport = NO_COLOR): string {
|
|
501
|
+
return header('usual', colors)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function header(usual: string, colors: ColorSupport): string {
|
|
505
|
+
const head =
|
|
506
|
+
` ${'time'.padStart(TIME_COL)} ${usual.padStart(USUAL_COL)} ${'waited'.padStart(WAITED_COL)} ` +
|
|
507
|
+
`${'status'.padEnd(STATUS_COL)} ${'cache'.padEnd(CACHE_COL)} ${'cpu'.padStart(CPU_COL)} ` +
|
|
508
|
+
`${'mem'.padStart(MEM_COL)} ${'outputs'.padStart(OUT_COL)} ${'blocks'.padStart(BLOCKED_COL)} ${'worker'.padEnd(WORKER_COL)} task`
|
|
509
|
+
return paint('', head, colors, { dim: true, underline: true })
|
|
418
510
|
}
|
|
419
511
|
|
|
420
512
|
/** Compact one-liner for a cache hit with nothing to replay. */
|
|
@@ -423,8 +515,11 @@ export function formatTaskHitLine(
|
|
|
423
515
|
o: TaskOutcome,
|
|
424
516
|
colors: ColorSupport = NO_COLOR,
|
|
425
517
|
onPath = false,
|
|
518
|
+
blocks?: number,
|
|
426
519
|
): string {
|
|
427
|
-
return formatOutcomeRow(o, paintTaskId(node, colors), o.durationMs, colors, onPath
|
|
520
|
+
return formatOutcomeRow(o, paintTaskId(node, colors), o.durationMs, colors, onPath, {
|
|
521
|
+
...ranCells(o, blocks),
|
|
522
|
+
})
|
|
428
523
|
}
|
|
429
524
|
|
|
430
525
|
/**
|
|
@@ -448,8 +543,34 @@ export function formatTaskExecutedLine(
|
|
|
448
543
|
o: TaskOutcome,
|
|
449
544
|
colors: ColorSupport = NO_COLOR,
|
|
450
545
|
onPath = false,
|
|
546
|
+
blocks?: number,
|
|
547
|
+
usualMs?: number,
|
|
451
548
|
): string {
|
|
452
|
-
return formatOutcomeRow(o, paintTaskId(node, colors), o.durationMs, colors, onPath
|
|
549
|
+
return formatOutcomeRow(o, paintTaskId(node, colors), o.durationMs, colors, onPath, {
|
|
550
|
+
...executedCells(o, blocks),
|
|
551
|
+
usual: usualDelta(o.durationMs, usualMs, colors),
|
|
552
|
+
})
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** An executed task's cells: where and how long it waited, its average cores and peak memory. */
|
|
556
|
+
export function executedCells(o: TaskOutcome, blocks: number | undefined): RowCells {
|
|
557
|
+
const cores = coresOf(o)
|
|
558
|
+
return {
|
|
559
|
+
...ranCells(o, blocks),
|
|
560
|
+
...(cores !== undefined ? { cpu: cores } : {}),
|
|
561
|
+
...(o.peakRssBytes !== undefined ? { memBytes: o.peakRssBytes } : {}),
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** The cells every row of a task that reached a machine carries. */
|
|
566
|
+
function ranCells(o: TaskOutcome, blocks: number | undefined): RowCells {
|
|
567
|
+
return {
|
|
568
|
+
// A skip reached no machine.
|
|
569
|
+
...(o.status === 'skipped' ? {} : { worker: o.where ?? 'local' }),
|
|
570
|
+
...(blocks !== undefined ? { blocks: String(blocks) } : {}),
|
|
571
|
+
...(waited(o.queuedMs) ? { waitedMs: o.queuedMs! } : {}),
|
|
572
|
+
...(o.outputBytes !== undefined ? { outputBytes: o.outputBytes } : {}),
|
|
573
|
+
}
|
|
453
574
|
}
|
|
454
575
|
|
|
455
576
|
/** Shared: render any outcome on the grid. */
|
|
@@ -459,6 +580,7 @@ function formatOutcomeRow(
|
|
|
459
580
|
ms: number | null,
|
|
460
581
|
colors: ColorSupport,
|
|
461
582
|
onPath: boolean,
|
|
583
|
+
cells: RowCells,
|
|
462
584
|
): string {
|
|
463
585
|
const st = statusOf(o)
|
|
464
586
|
const ca = cacheOf(o)
|
|
@@ -472,9 +594,9 @@ function formatOutcomeRow(
|
|
|
472
594
|
ca.color,
|
|
473
595
|
paintedId,
|
|
474
596
|
colors,
|
|
597
|
+
cells,
|
|
475
598
|
) +
|
|
476
599
|
(onPath ? pathNote(colors) : '') +
|
|
477
|
-
whereNote(o, colors) +
|
|
478
600
|
flakyNote(o, colors)
|
|
479
601
|
)
|
|
480
602
|
}
|
|
@@ -485,7 +607,7 @@ export function pathNote(colors: ColorSupport = NO_COLOR): string {
|
|
|
485
607
|
}
|
|
486
608
|
|
|
487
609
|
/** Where an executor ran the task, when not here, right after its id: `@small#2` (2026-10-21). */
|
|
488
|
-
|
|
610
|
+
function whereNote(o: TaskOutcome, colors: ColorSupport = NO_COLOR): string {
|
|
489
611
|
return o.where === undefined ? '' : placeNote(o.where, colors)
|
|
490
612
|
}
|
|
491
613
|
|
|
@@ -493,7 +615,7 @@ export function whereNote(o: TaskOutcome, colors: ColorSupport = NO_COLOR): stri
|
|
|
493
615
|
* ` · <where>` after the id: the dot dim, the place in its own hue. Last
|
|
494
616
|
* on the row, as a worker's name has no width to pad to (2026-10-21).
|
|
495
617
|
*/
|
|
496
|
-
|
|
618
|
+
function placeNote(where: string, colors: ColorSupport = NO_COLOR): string {
|
|
497
619
|
return `${paint('', ' \u00b7', colors, { dim: true })} ${ui.inline(ui.place(where), colors)}`
|
|
498
620
|
}
|
|
499
621
|
|
|
@@ -524,6 +646,7 @@ export function formatTaskSkippedLine(
|
|
|
524
646
|
node: TaskNode,
|
|
525
647
|
colors: ColorSupport = NO_COLOR,
|
|
526
648
|
skip?: Parameters<typeof skippedReason>[0],
|
|
649
|
+
blocks?: number,
|
|
527
650
|
): string {
|
|
528
651
|
const why = skippedReason(skip)
|
|
529
652
|
const suffix = why === '' ? '' : ` ${paint('', `• ${why}`, colors, { dim: true })}`
|
|
@@ -537,6 +660,7 @@ export function formatTaskSkippedLine(
|
|
|
537
660
|
'',
|
|
538
661
|
paintTaskId(node, colors),
|
|
539
662
|
colors,
|
|
663
|
+
blocks === undefined ? {} : { blocks: String(blocks) },
|
|
540
664
|
) + suffix
|
|
541
665
|
)
|
|
542
666
|
}
|
|
@@ -550,8 +674,10 @@ export function formatTaskAbortedLine(
|
|
|
550
674
|
node: TaskNode,
|
|
551
675
|
o: TaskOutcome,
|
|
552
676
|
colors: ColorSupport = NO_COLOR,
|
|
677
|
+
blocks?: number,
|
|
553
678
|
): string {
|
|
554
679
|
const id = paintTaskId(node, colors)
|
|
680
|
+
const bl: RowCells = blocks === undefined ? {} : { blocks: String(blocks) }
|
|
555
681
|
if (neverStarted(o)) {
|
|
556
682
|
return formatTaskRow(
|
|
557
683
|
paint('', '\u25cc', colors, { dim: true }),
|
|
@@ -562,6 +688,7 @@ export function formatTaskAbortedLine(
|
|
|
562
688
|
'',
|
|
563
689
|
id,
|
|
564
690
|
colors,
|
|
691
|
+
bl,
|
|
565
692
|
)
|
|
566
693
|
}
|
|
567
694
|
return formatTaskRow(
|
|
@@ -573,6 +700,7 @@ export function formatTaskAbortedLine(
|
|
|
573
700
|
'',
|
|
574
701
|
id,
|
|
575
702
|
colors,
|
|
703
|
+
ranCells(o, blocks),
|
|
576
704
|
)
|
|
577
705
|
}
|
|
578
706
|
|
package/src/report/index.ts
CHANGED
|
@@ -31,7 +31,8 @@ export {
|
|
|
31
31
|
warnOf,
|
|
32
32
|
} from './logger.js'
|
|
33
33
|
export { formatPersistentList } from './framed-output.js'
|
|
34
|
-
export { createForecast } from './forecast.js'
|
|
34
|
+
export { blocksCounts, createForecast } from './forecast.js'
|
|
35
35
|
export { fileLocations, fileMemo, type OutputLocation } from './path-links.js'
|
|
36
36
|
export { plainOutput } from './plain-output.js'
|
|
37
|
+
export type { LiveUsage } from './status-line.js'
|
|
37
38
|
export { formatOutcomeTable, formatRunSummary } from './summary.js'
|
package/src/report/logger.ts
CHANGED
|
@@ -8,10 +8,11 @@ import {
|
|
|
8
8
|
formatListHeader,
|
|
9
9
|
formatTaskAbortedLine,
|
|
10
10
|
formatTaskBlock,
|
|
11
|
+
executedCells,
|
|
12
|
+
waited,
|
|
11
13
|
formatTaskExecutedLine,
|
|
12
14
|
flakyNote,
|
|
13
15
|
pathNote,
|
|
14
|
-
whereNote,
|
|
15
16
|
formatTaskHashLine,
|
|
16
17
|
formatTaskHitLine,
|
|
17
18
|
formatTaskSkippedLine,
|
|
@@ -91,7 +92,8 @@ export interface Logger {
|
|
|
91
92
|
/** The run's canonical start (epoch ms) — equals the final summary's `startedAt`. */
|
|
92
93
|
startedAtMs?: number
|
|
93
94
|
}): void
|
|
94
|
-
|
|
95
|
+
/** `waitedMs`: how long it waited ready for a slot, when not started at once. */
|
|
96
|
+
taskStart?(node: TaskNode, info?: { waitedMs?: number }): void
|
|
95
97
|
/** The task graph finished (any outcome); vx's own closing steps follow. Idempotent. */
|
|
96
98
|
runEnd?(): void
|
|
97
99
|
/**
|
|
@@ -354,6 +356,8 @@ export function defaultLogger(
|
|
|
354
356
|
const onPath = new Set<string>()
|
|
355
357
|
let blockedCount = 0
|
|
356
358
|
let expected: ReadonlyMap<string, number> | undefined
|
|
359
|
+
// Each task's usual time once history is read: the results' `±usual`.
|
|
360
|
+
let usualOf: ReadonlyMap<string, number> | undefined
|
|
357
361
|
let blocked: readonly BlockedTask[] | undefined
|
|
358
362
|
let blockedBy: ReadonlyMap<string, number> | undefined
|
|
359
363
|
let etaAtMs = 0
|
|
@@ -521,6 +525,7 @@ export function defaultLogger(
|
|
|
521
525
|
expected = f.expected
|
|
522
526
|
blocked = f.blocked
|
|
523
527
|
blockedBy = f.blockedBy
|
|
528
|
+
usualOf = f.usual
|
|
524
529
|
}
|
|
525
530
|
const etaMs = deadlineMs === undefined ? undefined : deadlineMs - now
|
|
526
531
|
if (signals.progress && total > 0) {
|
|
@@ -604,6 +609,7 @@ export function defaultLogger(
|
|
|
604
609
|
nowMs: Date.now(),
|
|
605
610
|
summaryLines,
|
|
606
611
|
...(runContext?.whereRunning !== undefined ? { where: runContext.whereRunning } : {}),
|
|
612
|
+
...(runContext?.usage !== undefined ? { usage: runContext.usage } : {}),
|
|
607
613
|
...(expected !== undefined ? { expected } : {}),
|
|
608
614
|
...(critical !== undefined ? { critical } : {}),
|
|
609
615
|
},
|
|
@@ -688,7 +694,7 @@ export function defaultLogger(
|
|
|
688
694
|
const emitRow = (line: string): void => {
|
|
689
695
|
if (!headed) {
|
|
690
696
|
headed = true
|
|
691
|
-
emitLine(formatListHeader(
|
|
697
|
+
emitLine(formatListHeader(colors))
|
|
692
698
|
}
|
|
693
699
|
emitLine(line)
|
|
694
700
|
}
|
|
@@ -731,10 +737,17 @@ export function defaultLogger(
|
|
|
731
737
|
(framesTerminal && holdsTerminal(node)) ||
|
|
732
738
|
(view.mode === 'focused' && isPrimary(node) && !isGroupTask(node) && requestedCount <= 1)
|
|
733
739
|
|
|
740
|
+
// How many tasks are downstream of each: every results row's `blocks`.
|
|
741
|
+
const blocksOf = (id: string): number | undefined => runContext?.blocks?.().get(id)
|
|
734
742
|
const failureRow = (node: TaskNode, outcome: TaskOutcome): string =>
|
|
735
|
-
formatFailureLine(
|
|
743
|
+
formatFailureLine(
|
|
744
|
+
node.id,
|
|
745
|
+
outcome.durationMs,
|
|
746
|
+
colors,
|
|
747
|
+
cacheWordOf(outcome),
|
|
748
|
+
executedCells(outcome, blocksOf(node.id)),
|
|
749
|
+
) +
|
|
736
750
|
(onPath.has(node.id) ? pathNote(colors) : '') +
|
|
737
|
-
whereNote(outcome, colors) +
|
|
738
751
|
flakyNote(outcome, colors)
|
|
739
752
|
|
|
740
753
|
return {
|
|
@@ -798,7 +811,7 @@ export function defaultLogger(
|
|
|
798
811
|
tail = false
|
|
799
812
|
killStatus()
|
|
800
813
|
},
|
|
801
|
-
taskStart(node) {
|
|
814
|
+
taskStart(node, info) {
|
|
802
815
|
if (isGroupTask(node)) return
|
|
803
816
|
if (holdsTerminal(node)) {
|
|
804
817
|
killStatus()
|
|
@@ -820,7 +833,12 @@ export function defaultLogger(
|
|
|
820
833
|
}
|
|
821
834
|
// The rows are the status region's; with none, nothing reads them.
|
|
822
835
|
if (statusDead) return
|
|
823
|
-
const slot: WorkerSlot = {
|
|
836
|
+
const slot: WorkerSlot = {
|
|
837
|
+
id: node.id,
|
|
838
|
+
startedMs: Date.now(),
|
|
839
|
+
cache: ranNoCache({ node }) ? 'no-cache' : 'miss',
|
|
840
|
+
...(waited(info?.waitedMs) ? { waitedMs: info!.waitedMs! } : {}),
|
|
841
|
+
}
|
|
824
842
|
const free = slots.indexOf(null)
|
|
825
843
|
if (free >= 0) slots[free] = slot
|
|
826
844
|
else slotQueue.push(slot)
|
|
@@ -1016,7 +1034,7 @@ export function defaultLogger(
|
|
|
1016
1034
|
// Otherwise a row in the task list, in every view that lists a
|
|
1017
1035
|
// failure: the run exits red, and the row names why.
|
|
1018
1036
|
if (view.mode !== 'none' && view.mode !== 'hash-only') {
|
|
1019
|
-
emitRow(formatTaskAbortedLine(node, outcome, colors))
|
|
1037
|
+
emitRow(formatTaskAbortedLine(node, outcome, colors, blocksOf(node.id)))
|
|
1020
1038
|
}
|
|
1021
1039
|
return
|
|
1022
1040
|
}
|
|
@@ -1099,7 +1117,7 @@ export function defaultLogger(
|
|
|
1099
1117
|
}
|
|
1100
1118
|
case 'errors-only':
|
|
1101
1119
|
if (outcome.status === 'skipped') {
|
|
1102
|
-
emitRow(formatTaskSkippedLine(node, colors, outcome))
|
|
1120
|
+
emitRow(formatTaskSkippedLine(node, colors, outcome, blocksOf(node.id)))
|
|
1103
1121
|
return
|
|
1104
1122
|
}
|
|
1105
1123
|
if (outcome.status !== 'failed') return
|
|
@@ -1136,9 +1154,18 @@ export function defaultLogger(
|
|
|
1136
1154
|
),
|
|
1137
1155
|
)
|
|
1138
1156
|
} else if (outcome.status === 'success') {
|
|
1139
|
-
emitRow(
|
|
1157
|
+
emitRow(
|
|
1158
|
+
formatTaskExecutedLine(
|
|
1159
|
+
node,
|
|
1160
|
+
outcome,
|
|
1161
|
+
colors,
|
|
1162
|
+
onPath.has(node.id),
|
|
1163
|
+
blocksOf(node.id),
|
|
1164
|
+
usualOf?.get(node.id),
|
|
1165
|
+
),
|
|
1166
|
+
)
|
|
1140
1167
|
} else if (outcome.status === 'skipped') {
|
|
1141
|
-
emitRow(formatTaskSkippedLine(node, colors, outcome))
|
|
1168
|
+
emitRow(formatTaskSkippedLine(node, colors, outcome, blocksOf(node.id)))
|
|
1142
1169
|
}
|
|
1143
1170
|
return
|
|
1144
1171
|
case 'focused':
|
|
@@ -1147,7 +1174,7 @@ export function defaultLogger(
|
|
|
1147
1174
|
// stopped), so no frame-open fired — and a skip has no output,
|
|
1148
1175
|
// so a one-liner carries everything a frame would.
|
|
1149
1176
|
if (outcome.status === 'skipped') {
|
|
1150
|
-
emitRow(formatTaskSkippedLine(node, colors, outcome))
|
|
1177
|
+
emitRow(formatTaskSkippedLine(node, colors, outcome, blocksOf(node.id)))
|
|
1151
1178
|
return
|
|
1152
1179
|
}
|
|
1153
1180
|
if (streamsLive(node)) {
|
|
@@ -1198,7 +1225,7 @@ export function defaultLogger(
|
|
|
1198
1225
|
// Dependency-pulled nodes: silent on success; a skip gets its
|
|
1199
1226
|
// row, failures the ◼ row now and their frame replayed at runEnd.
|
|
1200
1227
|
if (outcome.status === 'skipped') {
|
|
1201
|
-
emitRow(formatTaskSkippedLine(node, colors, outcome))
|
|
1228
|
+
emitRow(formatTaskSkippedLine(node, colors, outcome, blocksOf(node.id)))
|
|
1202
1229
|
} else if (outcome.status === 'failed') {
|
|
1203
1230
|
emitRow(failureRow(node, outcome))
|
|
1204
1231
|
deferredFailures.push(
|
|
@@ -1222,11 +1249,13 @@ export function defaultLogger(
|
|
|
1222
1249
|
// point); misses/failures are always framed. One-liners
|
|
1223
1250
|
// stay outside ::group:: — there's nothing to collapse.
|
|
1224
1251
|
if (isHit && output.every((c) => c.text.trim().length === 0)) {
|
|
1225
|
-
emitRow(
|
|
1252
|
+
emitRow(
|
|
1253
|
+
formatTaskHitLine(node, outcome, colors, onPath.has(node.id), blocksOf(node.id)),
|
|
1254
|
+
)
|
|
1226
1255
|
return
|
|
1227
1256
|
}
|
|
1228
1257
|
if (outcome.status === 'skipped') {
|
|
1229
|
-
emitRow(formatTaskSkippedLine(node, colors, outcome))
|
|
1258
|
+
emitRow(formatTaskSkippedLine(node, colors, outcome, blocksOf(node.id)))
|
|
1230
1259
|
return
|
|
1231
1260
|
}
|
|
1232
1261
|
const block = formatTaskBlock(
|
|
@@ -42,7 +42,7 @@ export function plainOutput(sink: Logger): Logger {
|
|
|
42
42
|
status: (line) => sink.status(line),
|
|
43
43
|
...(sink.warn && { warn: (line) => sink.warn!(line) }),
|
|
44
44
|
...(sink.runStart && { runStart: (info) => sink.runStart!(info) }),
|
|
45
|
-
...(sink.taskStart && { taskStart: (node) => sink.taskStart!(node) }),
|
|
45
|
+
...(sink.taskStart && { taskStart: (node, info) => sink.taskStart!(node, info) }),
|
|
46
46
|
...(sink.runEnd && { runEnd: () => sink.runEnd!() }),
|
|
47
47
|
...(sink.stepStart && { stepStart: (id) => sink.stepStart!(id) }),
|
|
48
48
|
...(sink.stepEnd && { stepEnd: (id, info) => sink.stepEnd!(id, info) }),
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
|
|
8
8
|
import { dumbTerminal, paint, type ColorSupport } from '../util/index.js'
|
|
9
9
|
import {
|
|
10
|
-
|
|
10
|
+
formatRunningHeader,
|
|
11
11
|
formatTaskRow,
|
|
12
12
|
paintIdParts,
|
|
13
13
|
pathNote,
|
|
14
|
-
|
|
14
|
+
type RowCells,
|
|
15
15
|
} from './framed-output.js'
|
|
16
16
|
import type { BlockedTask } from './forecast.js'
|
|
17
17
|
|
|
@@ -327,6 +327,16 @@ export interface WorkerSlot {
|
|
|
327
327
|
startedMs: number
|
|
328
328
|
/** Its executor holds it for a machine to begin it (`TaskExecutor.running` is `null`). */
|
|
329
329
|
waiting?: boolean
|
|
330
|
+
/** How long it waited ready for a slot; absent when it started at once. */
|
|
331
|
+
waitedMs?: number
|
|
332
|
+
/** Its cache cell: `no-cache` for a task that caches nothing, else `miss` (the default). */
|
|
333
|
+
cache?: 'miss' | 'no-cache'
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** What a running task's process tree uses now. */
|
|
337
|
+
export interface LiveUsage {
|
|
338
|
+
cores: number
|
|
339
|
+
rssBytes: number
|
|
330
340
|
}
|
|
331
341
|
|
|
332
342
|
export interface StatusRegionState {
|
|
@@ -336,6 +346,8 @@ export interface StatusRegionState {
|
|
|
336
346
|
where?: (taskId: string) => string | null | undefined
|
|
337
347
|
/** A running task's usual duration from history; its row shows it. */
|
|
338
348
|
expected?: ReadonlyMap<string, number>
|
|
349
|
+
/** A running task's live CPU and memory; its row's `cpu` and `mem`. */
|
|
350
|
+
usage?: (taskId: string) => LiveUsage | undefined
|
|
339
351
|
/**
|
|
340
352
|
* Ready persistent tasks (dev servers …) whose children keep
|
|
341
353
|
* running after their outcome lands. Pinned until runEnd — the
|
|
@@ -395,6 +407,7 @@ export function formatFailureLine(
|
|
|
395
407
|
durationMs: number | null,
|
|
396
408
|
colors: ColorSupport = NO_COLOR,
|
|
397
409
|
cacheWord: 'miss' | 'no-cache' = 'miss',
|
|
410
|
+
cells: RowCells = {},
|
|
398
411
|
): string {
|
|
399
412
|
return formatTaskRow(
|
|
400
413
|
paint(ERROR, '◼\uFE0E', colors),
|
|
@@ -405,6 +418,7 @@ export function formatFailureLine(
|
|
|
405
418
|
'',
|
|
406
419
|
paintPinnedId(id, colors),
|
|
407
420
|
colors,
|
|
421
|
+
cells,
|
|
408
422
|
)
|
|
409
423
|
}
|
|
410
424
|
|
|
@@ -431,6 +445,7 @@ export function formatStepLine(
|
|
|
431
445
|
'',
|
|
432
446
|
paintPinnedId(id, colors),
|
|
433
447
|
colors,
|
|
448
|
+
{ worker: 'local' },
|
|
434
449
|
)
|
|
435
450
|
}
|
|
436
451
|
|
|
@@ -447,6 +462,12 @@ export function formatStatusRegion(
|
|
|
447
462
|
colors: ColorSupport = NO_COLOR,
|
|
448
463
|
): string[] {
|
|
449
464
|
const dim = (t: string) => paint('', t, colors, { dim: true })
|
|
465
|
+
// A task's usual duration, the live list's `usual` cell: `~18s`.
|
|
466
|
+
const usualOf = (id: string): { text: string; painted: string } => {
|
|
467
|
+
const ms = s.expected?.get(id)
|
|
468
|
+
const text = ms === undefined ? '' : formatExpected(ms)
|
|
469
|
+
return { text, painted: dim(text) }
|
|
470
|
+
}
|
|
450
471
|
// Leading blank separates the live running region from the completed
|
|
451
472
|
// task list scrolling above it (owner: "separate running from list").
|
|
452
473
|
const lines: string[] = ['']
|
|
@@ -463,8 +484,7 @@ export function formatStatusRegion(
|
|
|
463
484
|
'',
|
|
464
485
|
paintPinnedId(id, colors),
|
|
465
486
|
colors,
|
|
466
|
-
|
|
467
|
-
'',
|
|
487
|
+
{ worker: 'local' },
|
|
468
488
|
),
|
|
469
489
|
)
|
|
470
490
|
}
|
|
@@ -507,27 +527,31 @@ export function formatStatusRegion(
|
|
|
507
527
|
'',
|
|
508
528
|
paintPinnedId(slot.id, colors),
|
|
509
529
|
colors,
|
|
510
|
-
'',
|
|
530
|
+
{ worker: s.where?.(slot.id) ?? 'local' },
|
|
511
531
|
),
|
|
512
532
|
)
|
|
513
533
|
continue
|
|
514
534
|
}
|
|
515
535
|
const at = s.where?.(slot.id)
|
|
516
|
-
const
|
|
536
|
+
const use = s.usage?.(slot.id)
|
|
517
537
|
lines.push(
|
|
518
538
|
formatTaskRow(
|
|
519
539
|
' ',
|
|
520
540
|
Math.max(0, s.nowMs - slot.startedMs),
|
|
521
541
|
'running',
|
|
522
542
|
ACCENT,
|
|
523
|
-
|
|
543
|
+
slot.cache ?? 'miss',
|
|
524
544
|
'',
|
|
525
545
|
paintPinnedId(slot.id, colors),
|
|
526
546
|
colors,
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
547
|
+
{
|
|
548
|
+
usual: usualOf(slot.id),
|
|
549
|
+
...(slot.waitedMs !== undefined ? { waitedMs: slot.waitedMs } : {}),
|
|
550
|
+
...(use !== undefined ? { cpu: use.cores, memBytes: use.rssBytes } : {}),
|
|
551
|
+
blocks: String(s.blockedBy?.get(slot.id) ?? 0),
|
|
552
|
+
worker: at ?? 'local',
|
|
553
|
+
},
|
|
554
|
+
) + (s.critical === slot.id ? pathNote(colors) : ''),
|
|
531
555
|
)
|
|
532
556
|
for (const b of nested.get(slot.id) ?? []) {
|
|
533
557
|
rows++
|
|
@@ -541,7 +565,7 @@ export function formatStatusRegion(
|
|
|
541
565
|
'',
|
|
542
566
|
`${dim('\u2514')} ${paintPinnedId(b.id, colors)}`,
|
|
543
567
|
colors,
|
|
544
|
-
|
|
568
|
+
{ usual: usualOf(b.id) },
|
|
545
569
|
),
|
|
546
570
|
)
|
|
547
571
|
}
|
|
@@ -558,11 +582,11 @@ export function formatStatusRegion(
|
|
|
558
582
|
'',
|
|
559
583
|
paintPinnedId(step.id, colors),
|
|
560
584
|
colors,
|
|
561
|
-
'',
|
|
585
|
+
{ worker: 'local' },
|
|
562
586
|
),
|
|
563
587
|
)
|
|
564
588
|
}
|
|
565
|
-
if (lines.length > 1) lines.splice(1, 0,
|
|
589
|
+
if (lines.length > 1) lines.splice(1, 0, formatRunningHeader(colors))
|
|
566
590
|
// The live summary section — identical shape to the final printout,
|
|
567
591
|
// so the region visually BECOMES the summary when the run ends.
|
|
568
592
|
lines.push(...s.summaryLines)
|
package/src/report/summary.ts
CHANGED
|
@@ -223,6 +223,10 @@ export interface RunContext {
|
|
|
223
223
|
executorRows?: () => readonly (readonly [string, ui.Inline])[]
|
|
224
224
|
/** Where a running task runs, `null` while it waits for a machine (`TaskExecutor.running`): its live row. */
|
|
225
225
|
whereRunning?: (taskId: string) => string | null | undefined
|
|
226
|
+
/** A running task's live CPU and memory: its live row's `cpu` and `mem`. */
|
|
227
|
+
usage?: (taskId: string) => { cores: number; rssBytes: number } | undefined
|
|
228
|
+
/** Each task's count of the tasks downstream of it in the run, built on first ask: the rows' `blocks`. */
|
|
229
|
+
blocks?: () => ReadonlyMap<string, number>
|
|
226
230
|
/** Worker-pool size for this run; shown on the `info` row. */
|
|
227
231
|
concurrency?: number
|
|
228
232
|
remoteCacheEnabled: boolean
|
package/src/util/colors.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
const RESET = '\x1b[0m'
|
|
13
13
|
const BOLD = '\x1b[1m'
|
|
14
14
|
const DIM = '\x1b[2m'
|
|
15
|
+
const UNDERLINE = '\x1b[4m'
|
|
15
16
|
|
|
16
17
|
// State hues every report shares with the run summary.
|
|
17
18
|
export const SUCCESS = '#22c55e'
|
|
@@ -27,6 +28,7 @@ export interface ColorSupport {
|
|
|
27
28
|
export interface PaintOptions {
|
|
28
29
|
bold?: boolean
|
|
29
30
|
dim?: boolean
|
|
31
|
+
underline?: boolean
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
/**
|
|
@@ -102,6 +104,7 @@ export function paint(
|
|
|
102
104
|
let prefix = ''
|
|
103
105
|
if (opts.bold) prefix += BOLD
|
|
104
106
|
if (opts.dim) prefix += DIM
|
|
107
|
+
if (opts.underline) prefix += UNDERLINE
|
|
105
108
|
if (color) prefix += ansiFor(color)
|
|
106
109
|
if (prefix.length === 0) return text
|
|
107
110
|
return `${prefix}${text}${RESET}`
|