@vzn/vx 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/README.md +3 -4
- package/launcher.sh +2 -1
- package/package.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/show.json +15 -20
- package/skills/vx/SKILL.md +3 -3
- package/src/bin.ts +37 -3
- package/src/cache/archive.ts +197 -32
- package/src/cache/cache.ts +882 -230
- package/src/cache/checkpointer.ts +102 -0
- package/src/cache/file-hashes.ts +2 -1
- package/src/cache/fold-memo.ts +24 -4
- package/src/cache/fs-clock.ts +26 -0
- package/src/cache/git-inputs.ts +188 -22
- package/src/cache/history-stream.ts +151 -0
- package/src/cache/history-writer.ts +76 -0
- package/src/cache/index.ts +12 -1
- package/src/cache/inputs.ts +616 -135
- package/src/cache/key-fold.ts +1 -1
- package/src/cache/layer.ts +29 -16
- package/src/cache/layered-cache.ts +28 -7
- package/src/cache/lockfile-memo.ts +2 -1
- package/src/cache/output-index.ts +106 -44
- package/src/cache/plugin-cache.ts +2 -2
- package/src/cache/policy.ts +6 -2
- package/src/cache/remote-lane.ts +3 -1
- package/src/cache/run-history.ts +94 -25
- package/src/cache/schema.ts +46 -10
- package/src/cli/cache.ts +149 -36
- package/src/cli/completions.ts +2 -2
- package/src/cli/core-alias.ts +9 -2
- package/src/cli/docs.ts +4 -2
- package/src/cli/help.ts +27 -4
- package/src/cli/index.ts +62 -7
- package/src/cli/info.ts +5 -4
- package/src/cli/init.ts +55 -20
- package/src/cli/last.ts +218 -43
- package/src/cli/lock.ts +187 -24
- package/src/cli/plan-format.ts +24 -21
- package/src/cli/plugin-commands.ts +6 -1
- package/src/cli/plugin-templates.ts +36 -45
- package/src/cli/run.ts +68 -34
- package/src/cli/select.ts +160 -64
- package/src/cli/show.ts +70 -28
- package/src/cli/task-verb.ts +12 -7
- package/src/cli/upgrade.ts +210 -76
- package/src/cli/watch-filter.ts +88 -15
- package/src/cli/watch-fs.ts +34 -47
- package/src/cli/watch-judge.ts +73 -9
- package/src/cli/watch-set.ts +10 -8
- package/src/cli/watch.ts +190 -33
- package/src/cli/why.ts +107 -33
- package/src/cli/workspace-config.ts +11 -1
- package/src/config.ts +56 -30
- package/src/exec/env.ts +3 -1
- package/src/exec/executor.ts +116 -16
- package/src/exec/index.ts +1 -0
- package/src/exec/kill-tree.ts +7 -7
- package/src/exec/runner.ts +270 -108
- package/src/exec/sandbox-binds.ts +109 -18
- package/src/exec/sandbox-paths.ts +3 -1
- package/src/exec/sandbox-runtime.ts +446 -64
- package/src/exec/sandbox-violations.ts +250 -46
- package/src/exec/spawn-off.ts +10 -5
- package/src/exec/task-proxy.ts +93 -0
- package/src/exec/warm-shell.ts +29 -7
- package/src/graph/definition.ts +16 -3
- package/src/graph/folds.ts +89 -16
- package/src/graph/index.ts +8 -0
- package/src/graph/priorities.ts +8 -5
- package/src/graph/scheduler.ts +176 -39
- package/src/graph/stand-ins.ts +36 -0
- package/src/graph/task-graph.ts +347 -160
- package/src/graph/task-status.ts +5 -1
- package/src/handoff.ts +45 -10
- package/src/history/failure-mode.ts +39 -6
- package/src/history/history.ts +103 -30
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +193 -73
- package/src/index.ts +6 -3
- package/src/orchestrator/admission.ts +14 -10
- package/src/orchestrator/affected-tasks.ts +35 -13
- package/src/orchestrator/command-windows.ts +8 -5
- package/src/orchestrator/deferred-outputs.ts +42 -13
- package/src/orchestrator/doctor.ts +21 -9
- package/src/orchestrator/download-policy.ts +8 -10
- package/src/orchestrator/excluded-keys.ts +15 -3
- package/src/orchestrator/execute-args.ts +9 -2
- package/src/orchestrator/execute-task.ts +612 -138
- package/src/orchestrator/graph-memo.ts +26 -4
- package/src/orchestrator/graph-stage.ts +27 -4
- package/src/orchestrator/held-servers.ts +36 -14
- package/src/orchestrator/hit-restore.ts +141 -93
- package/src/orchestrator/index.ts +4 -1
- package/src/orchestrator/keyed-projects.ts +53 -0
- package/src/orchestrator/local-floor.ts +21 -3
- package/src/orchestrator/local-shortcircuit.ts +2 -2
- package/src/orchestrator/lockfile-claim.ts +30 -4
- package/src/orchestrator/miss-save.ts +60 -61
- package/src/orchestrator/options.ts +13 -0
- package/src/orchestrator/output-host.ts +16 -4
- package/src/orchestrator/output-log.ts +1 -54
- package/src/orchestrator/persistent-task.ts +32 -16
- package/src/orchestrator/persistent.ts +152 -15
- package/src/orchestrator/placement.ts +82 -16
- package/src/orchestrator/plan.ts +39 -17
- package/src/orchestrator/plugin-host.ts +238 -110
- package/src/orchestrator/plugin.ts +59 -27
- package/src/orchestrator/prepare.ts +194 -38
- package/src/orchestrator/projects.ts +89 -10
- package/src/orchestrator/read-only.ts +115 -0
- package/src/orchestrator/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-context.ts +6 -2
- package/src/orchestrator/run-failures.ts +101 -23
- package/src/orchestrator/run-id.ts +13 -2
- package/src/orchestrator/run-keys.ts +87 -28
- package/src/orchestrator/run-live.ts +16 -13
- package/src/orchestrator/run-lock.ts +27 -4
- package/src/orchestrator/run-records.ts +95 -74
- package/src/orchestrator/run-report.ts +3 -2
- package/src/orchestrator/run-stop.ts +17 -9
- package/src/orchestrator/run.ts +531 -122
- package/src/orchestrator/sandbox-fix.ts +40 -12
- package/src/orchestrator/sandbox-request.ts +314 -44
- package/src/orchestrator/save-lane.ts +24 -13
- package/src/orchestrator/shell-verdict.ts +29 -1
- package/src/orchestrator/signals.ts +103 -8
- package/src/orchestrator/stable-keys.ts +34 -27
- package/src/orchestrator/task-artifacts.ts +16 -3
- package/src/orchestrator/task-hash.ts +191 -34
- package/src/orchestrator/telemetry-host.ts +25 -6
- package/src/orchestrator/telemetry.ts +85 -13
- package/src/orchestrator/upstream.ts +3 -1
- package/src/report/events.ts +23 -6
- package/src/report/forecast.ts +103 -37
- package/src/report/framed-output.ts +179 -162
- package/src/report/index.ts +2 -1
- package/src/report/logger.ts +108 -31
- package/src/report/plain-output.ts +15 -9
- package/src/report/status-line.ts +28 -55
- package/src/report/summary.ts +61 -18
- package/src/settings.ts +50 -14
- package/src/ui/components.ts +36 -42
- package/src/ui/index.ts +3 -1
- package/src/ui/live-region.ts +67 -8
- package/src/ui/terminal.ts +12 -0
- package/src/ui/theme.ts +16 -1
- package/src/util/config-frame.ts +142 -6
- package/src/util/edit-distance.ts +17 -1
- package/src/util/errors.ts +23 -7
- package/src/util/foreign-cache-dir.ts +60 -0
- package/src/util/git-ignored.ts +59 -5
- package/src/util/index.ts +15 -1
- package/src/util/paths.ts +51 -7
- package/src/util/plain-text.ts +19 -0
- package/src/util/secret-mask.ts +57 -1
- package/src/util/settle.ts +18 -1
- package/src/util/size.ts +12 -2
- package/src/util/tail.ts +4 -1
- package/src/util/write-file.ts +86 -9
- package/src/workspace/affected.ts +225 -27
- package/src/workspace/bracket-literal.ts +2 -2
- package/src/workspace/config-bytecode.ts +23 -4
- package/src/workspace/config-eval.ts +90 -16
- package/src/workspace/config-imports.ts +96 -41
- package/src/workspace/config-schema.ts +192 -86
- package/src/workspace/dependencies-lock.ts +7 -14
- package/src/workspace/filter.ts +18 -2
- package/src/workspace/index.ts +3 -1
- package/src/workspace/json-data.ts +34 -8
- package/src/workspace/lockfile.ts +86 -37
- package/src/workspace/migration.ts +58 -31
- package/src/workspace/module-state.ts +62 -0
- package/src/workspace/nested-dirs.ts +8 -0
- package/src/workspace/project-loader.ts +276 -32
- package/src/workspace/repo-id.ts +29 -6
- package/src/workspace/workspace.ts +190 -30
package/src/graph/task-status.ts
CHANGED
|
@@ -16,7 +16,11 @@ export type TaskStatus =
|
|
|
16
16
|
// are partial.
|
|
17
17
|
| 'aborted'
|
|
18
18
|
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Where a task's result came from, derived ONCE in core from the status.
|
|
21
|
+
* `none`: it never engaged the cache, or its hit's restore failed (a
|
|
22
|
+
* failed task, in no cache column).
|
|
23
|
+
*/
|
|
20
24
|
export type CacheSource = 'miss' | 'local' | 'remote' | 'none'
|
|
21
25
|
|
|
22
26
|
/**
|
package/src/handoff.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// from the workspace root, the one lookup every verb shares. Here for
|
|
3
3
|
// the compiled binary installed by hand; an npm install's launcher
|
|
4
4
|
// (npm-launcher.sh) walks the same way and `exec`s, which Bun cannot. The
|
|
5
|
-
// walk runs before anything else loads, through `Bun.
|
|
6
|
-
// importing `node:fs` alone was ~1.1 ms of a 4 ms
|
|
5
|
+
// walk runs before anything else loads, through `Bun.Glob` and
|
|
6
|
+
// `Bun.file(…).exists()`: importing `node:fs` alone was ~1.1 ms of a 4 ms
|
|
7
|
+
// start (2026-10-13).
|
|
7
8
|
|
|
8
9
|
/** `vx upgrade -g`: moves only the global vx, so it never hands off (2026-10-16). */
|
|
9
10
|
export function upgradesOnlyItself(argv: readonly string[]): boolean {
|
|
@@ -85,21 +86,55 @@ export function workspaceFile(dir: string): Promise<string | null> {
|
|
|
85
86
|
/** vx wrote a workspace config (`vx init`): what was learned before it is stale. */
|
|
86
87
|
export function workspaceFileWritten(): void {
|
|
87
88
|
files.clear()
|
|
89
|
+
broken.clear()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const workspaceNames = new Bun.Glob('workspace.*')
|
|
93
|
+
const broken = new Set<string>()
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The workspace config in `d`'s `.vx`: the first of the names that is a
|
|
97
|
+
* file, else the first entry by one of them (a broken link, a directory),
|
|
98
|
+
* which marks the root all the same, as `vx lock`'s discovery counts it,
|
|
99
|
+
* and is refused there ({@link workspaceFileBroken}). A level without
|
|
100
|
+
* one costs a stat and a failed open, where a stat per name was six:
|
|
101
|
+
* 9 us against 15 us from three levels down, 1 us at a root holding
|
|
102
|
+
* `.vx/workspace.ts` either way (min of 2000, 2026-11-07).
|
|
103
|
+
*/
|
|
104
|
+
async function workspaceEntryIn(d: string): Promise<string | null> {
|
|
105
|
+
// The name `vx init` writes, first: at the root, the stat it always was.
|
|
106
|
+
const first = `${d}/${WORKSPACE_CONFIG_FILENAMES[0]}`
|
|
107
|
+
if (await Bun.file(first).exists()) return first
|
|
108
|
+
let names: string[]
|
|
109
|
+
try {
|
|
110
|
+
names = [...workspaceNames.scanSync({ cwd: `${d}/.vx`, onlyFiles: false })]
|
|
111
|
+
} catch {
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
const ranked = WORKSPACE_CONFIG_FILENAMES.filter((n) => names.includes(n.slice(4)))
|
|
115
|
+
if (ranked.length === 0) return null
|
|
116
|
+
// `exists()` is false for a directory and a broken link.
|
|
117
|
+
for (const name of ranked)
|
|
118
|
+
if (name !== WORKSPACE_CONFIG_FILENAMES[0] && (await Bun.file(`${d}/${name}`).exists()))
|
|
119
|
+
return `${d}/${name}`
|
|
120
|
+
const file = `${d}/${ranked[0]}`
|
|
121
|
+
broken.add(file)
|
|
122
|
+
return file
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The workspace config {@link workspaceFile} found is no file: a broken link, a directory. */
|
|
126
|
+
export function workspaceFileBroken(file: string): boolean {
|
|
127
|
+
return broken.has(file)
|
|
88
128
|
}
|
|
89
129
|
|
|
90
130
|
async function findWorkspaceFile(start: string): Promise<string | null> {
|
|
91
131
|
let top = await gitTop(start)
|
|
92
132
|
const passed: string[] = []
|
|
93
133
|
let found: string | null = null
|
|
94
|
-
|
|
134
|
+
for (let d = start; ; d = d.slice(0, d.lastIndexOf('/'))) {
|
|
95
135
|
passed.push(d || '/')
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (await Bun.file(file).exists()) {
|
|
99
|
-
found = file
|
|
100
|
-
break walk
|
|
101
|
-
}
|
|
102
|
-
}
|
|
136
|
+
found = await workspaceEntryIn(d)
|
|
137
|
+
if (found !== null) break
|
|
103
138
|
if (d === '') break
|
|
104
139
|
if (d === top) {
|
|
105
140
|
// A submodule whose `.git` is a directory (added from a clone, not
|
|
@@ -30,7 +30,7 @@ export type FailureMode = 'stable' | 'flaky-recoverable' | 'flaky-fatal'
|
|
|
30
30
|
* `status`, `cache_hit`, `cached` columns) with how often it failed, how often it
|
|
31
31
|
* passed — an executed success or a cache hit, which replays a success —
|
|
32
32
|
* and how often it failed AFTER its first pass (`relapses`). A task with
|
|
33
|
-
* no cache
|
|
33
|
+
* no `cache: true` keys on its config alone and runs every time, so its key
|
|
34
34
|
* says nothing about its inputs: no reader counts it, as no run's row
|
|
35
35
|
* calls it flaky. Nor is a row that names a `blocked_by`: it ran behind a
|
|
36
36
|
* failure (`--continue=always`), on bytes its key never named, so its
|
|
@@ -75,7 +75,7 @@ export function mixedOutcomeKeysSql(source: string, where = ''): string {
|
|
|
75
75
|
export function mixedOutcomeKeyCount(db: Database, project: string, task: string): number {
|
|
76
76
|
const row = db
|
|
77
77
|
.query(
|
|
78
|
-
`SELECT COUNT(*) AS n FROM (${mixedOutcomeKeysSql('runs', ' AND project = ? AND task = ?')})`,
|
|
78
|
+
`SELECT COUNT(*) AS n FROM (${mixedOutcomeKeysSql('main.runs', ' AND project = ? AND task = ?')})`,
|
|
79
79
|
)
|
|
80
80
|
.get(project, task) as { n: number }
|
|
81
81
|
return row.n
|
|
@@ -198,6 +198,29 @@ export function detectFlaky(db: Database, candidates: readonly FlakyCandidate[])
|
|
|
198
198
|
return out
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Whether a task that just failed on `hash` is known to be flaky: this key
|
|
203
|
+
* passed before (the failure itself is the flake), or the retained history
|
|
204
|
+
* holds a key of the task that failed after it passed, or a pass that took
|
|
205
|
+
* a retry. Asked only after a failure, so a green run never reads it.
|
|
206
|
+
*/
|
|
207
|
+
export function knownFlaky(db: Database, project: string, task: string, hash: string): boolean {
|
|
208
|
+
const passed = db
|
|
209
|
+
.query(
|
|
210
|
+
`SELECT 1 FROM ${OUTCOMES} WHERE hash = ? AND project = ? AND task = ?
|
|
211
|
+
AND (status = 'success' OR status IN ${HIT_STATUSES}) LIMIT 1`,
|
|
212
|
+
)
|
|
213
|
+
.get(hash, project, task)
|
|
214
|
+
if (passed !== null) return true
|
|
215
|
+
const flaked = db
|
|
216
|
+
.query(
|
|
217
|
+
`SELECT 1 FROM (${keyOutcomesSql(OUTCOMES, ' AND project = ? AND task = ?')})
|
|
218
|
+
WHERE relapses > 0 OR retried_failures > 0 LIMIT 1`,
|
|
219
|
+
)
|
|
220
|
+
.get(project, task)
|
|
221
|
+
return flaked !== null
|
|
222
|
+
}
|
|
223
|
+
|
|
201
224
|
/** One task the recorded history shows failing on a key that had passed. */
|
|
202
225
|
export interface FlakyTask {
|
|
203
226
|
/** `project#task`. */
|
|
@@ -213,18 +236,28 @@ export interface FlakyTask {
|
|
|
213
236
|
|
|
214
237
|
/**
|
|
215
238
|
* Every task with a key that failed after it had passed anywhere in the retained history
|
|
216
|
-
* (
|
|
239
|
+
* (until a bounded clean's `RunHistory.pruneOlderThan`), most failures first: the
|
|
217
240
|
* doctor's list. A pass that took a retry mixes its key on its own: it is
|
|
218
241
|
* one `success` row whose failed attempts count as failures (`vx info`
|
|
219
|
-
* said `none` over one).
|
|
220
|
-
*
|
|
242
|
+
* said `none` over one). Only keys with a failure or a retry can be on it,
|
|
243
|
+
* so one plain pass over `task_outcomes` finds those and the projection
|
|
244
|
+
* reads their rows alone, by `task_outcomes_hash`: projecting every key
|
|
245
|
+
* sorted the whole table, 4.2 s at 2M rows against 0.15. An index on the
|
|
246
|
+
* candidates would make the pass a probe; it is a schema change, which
|
|
247
|
+
* resets every cache.
|
|
221
248
|
*/
|
|
222
249
|
export function flakyTasks(db: Database): FlakyTask[] {
|
|
223
250
|
const rows = db
|
|
224
251
|
.query(
|
|
225
252
|
`SELECT project, task, COUNT(*) AS keys, SUM(passes) AS passes, SUM(failures) AS failures
|
|
226
253
|
FROM (SELECT project, task, passes, failures + retried_failures AS failures
|
|
227
|
-
FROM (${keyOutcomesSql(
|
|
254
|
+
FROM (${keyOutcomesSql(
|
|
255
|
+
OUTCOMES,
|
|
256
|
+
// NOT INDEXED: by `task_outcomes_hash` the pass looked each
|
|
257
|
+
// row up from the index, 1.1 s against 0.15.
|
|
258
|
+
` AND hash IN (SELECT hash FROM ${OUTCOMES} NOT INDEXED
|
|
259
|
+
WHERE attempts > 1 OR status = 'failed')`,
|
|
260
|
+
)})
|
|
228
261
|
WHERE relapses > 0 OR retried_failures > 0)
|
|
229
262
|
GROUP BY project, task
|
|
230
263
|
ORDER BY failures DESC, passes DESC, project, task`,
|
package/src/history/history.ts
CHANGED
|
@@ -121,6 +121,8 @@ export class EmptyHistoryProvider implements HistoryProvider {
|
|
|
121
121
|
export interface LocalHistory extends HistoryProvider {
|
|
122
122
|
/** Each task's measured need (`p50DurationMs`, peak cores and memory). */
|
|
123
123
|
resourcesFor(taskIds: readonly string[]): Promise<ReadonlyMap<string, TaskResources>>
|
|
124
|
+
/** Only each task's `p50DurationMs` as `resourcesFor` answers it, read without the rest. */
|
|
125
|
+
durationsFor(taskIds: readonly string[]): Promise<ReadonlyMap<string, number>>
|
|
124
126
|
/** Whether timings of commit `sha` were imported. */
|
|
125
127
|
hasImported(sha: string): boolean
|
|
126
128
|
/** Keep commit `sha`'s timings (task id → ms) and needs as forecast rows. */
|
|
@@ -243,7 +245,7 @@ export class LocalHistoryProvider implements LocalHistory {
|
|
|
243
245
|
THEN ${NEED_SQL} END),
|
|
244
246
|
MAX(CASE WHEN r.cache_hit = 1 AND e.cpu_ms IS NOT NULL AND e.duration_ms > 0
|
|
245
247
|
THEN e.cpu_ms * 1.0 / e.duration_ms END)) AS cpu
|
|
246
|
-
FROM runs r
|
|
248
|
+
FROM main.runs r
|
|
247
249
|
-- A hit executed nothing here, but its entry carries what the
|
|
248
250
|
-- PRODUCING execution used (the artifact's sidecar, indexed at save
|
|
249
251
|
-- and ingest alike), so a task this machine has only ever restored
|
|
@@ -400,6 +402,13 @@ export class LocalHistoryProvider implements LocalHistory {
|
|
|
400
402
|
const out = new Map<string, number>()
|
|
401
403
|
this.lackedBranchHistory = false
|
|
402
404
|
if (wanted.size === 0) return out
|
|
405
|
+
// A store that has timed nothing (a cold cache, a first run) answers no
|
|
406
|
+
// tier: one row's probe, where asking every task of 8,002 was 20 ms of
|
|
407
|
+
// a cold run's schedule stage (2026-11-07).
|
|
408
|
+
if (this.db.query('SELECT 1 FROM task_durations LIMIT 1').get() === null) {
|
|
409
|
+
this.lackedBranchHistory = true
|
|
410
|
+
return out
|
|
411
|
+
}
|
|
403
412
|
// Tiers, first answer wins: a WHERE over `task_durations`, its params,
|
|
404
413
|
// and how many of the newest rows it reads.
|
|
405
414
|
type Tier = { where: string; params: string[]; limit: number }
|
|
@@ -480,7 +489,7 @@ export class LocalHistoryProvider implements LocalHistory {
|
|
|
480
489
|
`SELECT r.project, r.task${this.byDef}, MAX(r.peak_rss_bytes) AS rss,
|
|
481
490
|
AVG(CASE WHEN r.cpu_ms IS NOT NULL AND r.duration_ms > 0
|
|
482
491
|
THEN ${NEED_SQL} END) AS cpu
|
|
483
|
-
FROM runs r
|
|
492
|
+
FROM main.runs r
|
|
484
493
|
WHERE r.id >= ? AND r.status = 'success' AND (r.cache_hit IS NULL OR r.cache_hit = 0)
|
|
485
494
|
GROUP BY r.project, r.task${this.byDef}`,
|
|
486
495
|
)
|
|
@@ -495,25 +504,7 @@ export class LocalHistoryProvider implements LocalHistory {
|
|
|
495
504
|
...(row.cpu !== null ? { maxCpuParallelism: row.cpu } : {}),
|
|
496
505
|
})
|
|
497
506
|
}
|
|
498
|
-
|
|
499
|
-
project: string
|
|
500
|
-
task: string
|
|
501
|
-
def?: string | null
|
|
502
|
-
rss: number | null
|
|
503
|
-
cpu: number | null
|
|
504
|
-
ms: number | null
|
|
505
|
-
}
|
|
506
|
-
const hits = this.db
|
|
507
|
-
.query(
|
|
508
|
-
`SELECT r.project, r.task${this.byDef}, MAX(e.peak_rss_bytes) AS rss,
|
|
509
|
-
MAX(CASE WHEN e.cpu_ms IS NOT NULL AND e.duration_ms > 0
|
|
510
|
-
THEN e.cpu_ms * 1.0 / e.duration_ms END) AS cpu,
|
|
511
|
-
MAX(CASE WHEN e.duration_ms > 0 THEN e.duration_ms END) AS ms
|
|
512
|
-
FROM (SELECT DISTINCT project, task, hash, def FROM runs WHERE id >= ? AND cache_hit = 1) r
|
|
513
|
-
JOIN entries e ON e.hash = r.hash
|
|
514
|
-
GROUP BY r.project, r.task${this.byDef}`,
|
|
515
|
-
)
|
|
516
|
-
.all(floor) as Hit[]
|
|
507
|
+
const hits = this.producerRows(floor)
|
|
517
508
|
// A hit's CPU is its producer's use, not need: the entry keeps no
|
|
518
509
|
// share, so a crowded producer's reads low. Only a task with no need
|
|
519
510
|
// from a run here or the default branch's import takes it: main's
|
|
@@ -522,12 +513,7 @@ export class LocalHistoryProvider implements LocalHistory {
|
|
|
522
513
|
const hitCpu = new Map<string, number>()
|
|
523
514
|
for (const row of hits) {
|
|
524
515
|
const key = `${row.project}#${row.task}`
|
|
525
|
-
if (
|
|
526
|
-
!wanted.has(key) ||
|
|
527
|
-
!this.current(key, row.def) ||
|
|
528
|
-
(row.rss === null && row.cpu === null && row.ms === null)
|
|
529
|
-
)
|
|
530
|
-
continue
|
|
516
|
+
if (!wanted.has(key) || !this.produced(key, row)) continue
|
|
531
517
|
if (row.cpu !== null) hitCpu.set(key, row.cpu)
|
|
532
518
|
const had = out.get(key)
|
|
533
519
|
const rss = Math.max(had?.maxPeakRssBytes ?? -Infinity, row.rss ?? -Infinity)
|
|
@@ -558,7 +544,7 @@ export class LocalHistoryProvider implements LocalHistory {
|
|
|
558
544
|
AVG(${NEED_SQL} / r.placement_cores) AS fill,
|
|
559
545
|
MAX(CASE WHEN r.peak_rss_bytes >= ${SATURATED} * r.placement_memory
|
|
560
546
|
THEN r.placement_memory END) AS memory
|
|
561
|
-
FROM runs r
|
|
547
|
+
FROM main.runs r
|
|
562
548
|
WHERE r.id >= ? AND r.placement_cores IS NOT NULL AND r.duration_ms > 0
|
|
563
549
|
AND r.status IN ('success', 'failed') AND (r.cache_hit IS NULL OR r.cache_hit = 0)
|
|
564
550
|
GROUP BY r.project, r.task${this.byDef}, r.placement_cores) r
|
|
@@ -589,6 +575,51 @@ export class LocalHistoryProvider implements LocalHistory {
|
|
|
589
575
|
return out
|
|
590
576
|
}
|
|
591
577
|
|
|
578
|
+
/**
|
|
579
|
+
* Only the `p50DurationMs` of each task `resourcesFor` answers: its
|
|
580
|
+
* forecast, else a restored task's producer time. For a reader that
|
|
581
|
+
* needs nothing else (`@vzn/vx-history`'s timings file): the window's
|
|
582
|
+
* grouped reads are skipped, and the hits are read only when some task
|
|
583
|
+
* has no forecast. 23-27 ms against 40 at 8,002 tasks.
|
|
584
|
+
*/
|
|
585
|
+
async durationsFor(taskIds: readonly string[]): Promise<ReadonlyMap<string, number>> {
|
|
586
|
+
if (taskIds.length === 0) return new Map()
|
|
587
|
+
const wanted = new Set(taskIds)
|
|
588
|
+
const out = this.forecasts(wanted)
|
|
589
|
+
if (out.size === wanted.size) return out
|
|
590
|
+
// As `resourcesFor` sets it: the last row of a task wins, a row of no time unsetting it.
|
|
591
|
+
const produced = new Map<string, number | null>()
|
|
592
|
+
for (const row of this.producerRows(this.floor())) {
|
|
593
|
+
const key = `${row.project}#${row.task}`
|
|
594
|
+
if (wanted.has(key) && !out.has(key) && this.produced(key, row)) produced.set(key, row.ms)
|
|
595
|
+
}
|
|
596
|
+
for (const [key, ms] of produced) if (ms !== null) out.set(key, ms)
|
|
597
|
+
return out
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* The window's restored tasks, by what their entries' producers used:
|
|
602
|
+
* each distinct hit key joined to its entry once.
|
|
603
|
+
*/
|
|
604
|
+
private producerRows(floor: number): ProducerRow[] {
|
|
605
|
+
return this.db
|
|
606
|
+
.query(
|
|
607
|
+
`SELECT r.project, r.task${this.byDef}, MAX(e.peak_rss_bytes) AS rss,
|
|
608
|
+
MAX(CASE WHEN e.cpu_ms IS NOT NULL AND e.duration_ms > 0
|
|
609
|
+
THEN e.cpu_ms * 1.0 / e.duration_ms END) AS cpu,
|
|
610
|
+
MAX(CASE WHEN e.duration_ms > 0 THEN e.duration_ms END) AS ms
|
|
611
|
+
FROM (SELECT DISTINCT project, task, hash, def FROM main.runs WHERE id >= ? AND cache_hit = 1) r
|
|
612
|
+
JOIN entries e ON e.hash = r.hash
|
|
613
|
+
GROUP BY r.project, r.task${this.byDef}`,
|
|
614
|
+
)
|
|
615
|
+
.all(floor) as ProducerRow[]
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/** Whether a producer row is of the task as it is now and measured anything. */
|
|
619
|
+
private produced(key: string, row: ProducerRow): boolean {
|
|
620
|
+
return this.current(key, row.def) && (row.rss !== null || row.cpu !== null || row.ms !== null)
|
|
621
|
+
}
|
|
622
|
+
|
|
592
623
|
/**
|
|
593
624
|
* A task never measured here takes its need from the newest imported
|
|
594
625
|
* row that has one (`importTimings`): a fresh CI machine packs by what
|
|
@@ -641,6 +672,38 @@ export class LocalHistoryProvider implements LocalHistory {
|
|
|
641
672
|
}
|
|
642
673
|
}
|
|
643
674
|
|
|
675
|
+
/**
|
|
676
|
+
* Of `taskIds`, each whose one timing (of its definition, given `defs`)
|
|
677
|
+
* was recorded at or after `since`: a run's own, with no usual to compare
|
|
678
|
+
* it with. At most two rows per task off `task_durations_task`, where
|
|
679
|
+
* grouping every task's rows cost `vx last` 1.5 s at 2M rows.
|
|
680
|
+
*/
|
|
681
|
+
timedOnlySince(taskIds: readonly string[], since: number): string[] {
|
|
682
|
+
if (taskIds.length === 0) return []
|
|
683
|
+
const rows = this.db
|
|
684
|
+
.query(
|
|
685
|
+
`SELECT j.key AS i FROM json_each(?) AS j
|
|
686
|
+
WHERE (SELECT MIN(recorded_at) FROM (
|
|
687
|
+
SELECT recorded_at FROM task_durations
|
|
688
|
+
WHERE project = j.value ->> 0 AND task = j.value ->> 1 ${this.durationDef}
|
|
689
|
+
LIMIT 2) HAVING COUNT(*) = 1) >= ?`,
|
|
690
|
+
)
|
|
691
|
+
.all(JSON.stringify(taskIds.map((id) => this.keyOf(id))), since) as { i: number }[]
|
|
692
|
+
return rows.map((r) => taskIds[r.i]!)
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Each task id the window's rows name, sorted: what a mistyped id is
|
|
697
|
+
* matched against. Every pair of the whole table was a scan of it, 270 ms
|
|
698
|
+
* at 2M rows, on an error's path.
|
|
699
|
+
*/
|
|
700
|
+
recentTaskIds(): string[] {
|
|
701
|
+
const rows = this.db
|
|
702
|
+
.query('SELECT DISTINCT project, task FROM main.runs WHERE id >= ? ORDER BY project, task')
|
|
703
|
+
.all(this.floor()) as { project: string; task: string }[]
|
|
704
|
+
return rows.map((r) => `${r.project}#${r.task}`)
|
|
705
|
+
}
|
|
706
|
+
|
|
644
707
|
/** The first `runs` id of the newest `recent` invocations (see `loadFor`). */
|
|
645
708
|
private floor(): number {
|
|
646
709
|
// The first row of each invocation in the window, an index probe each:
|
|
@@ -649,7 +712,7 @@ export class LocalHistoryProvider implements LocalHistory {
|
|
|
649
712
|
const row = this.db
|
|
650
713
|
.query(
|
|
651
714
|
`SELECT COUNT(*) AS n, MIN(first) AS id,
|
|
652
|
-
(SELECT COALESCE(MAX(id), 0) + 1 FROM runs) AS past
|
|
715
|
+
(SELECT COALESCE(MAX(id), 0) + 1 FROM main.runs) AS past
|
|
653
716
|
FROM (SELECT (SELECT MIN(id) FROM runs WHERE run_id = i.run_id) AS first
|
|
654
717
|
FROM (SELECT run_id FROM invocations ORDER BY rowid DESC LIMIT ?) i)`,
|
|
655
718
|
)
|
|
@@ -671,7 +734,7 @@ export class LocalHistoryProvider implements LocalHistory {
|
|
|
671
734
|
const rows = this.db
|
|
672
735
|
.query(
|
|
673
736
|
`SELECT project, task, COUNT(*) AS n FROM (${mixedOutcomeKeysSql(
|
|
674
|
-
'runs',
|
|
737
|
+
'main.runs',
|
|
675
738
|
` AND id >= ? AND (project, task) IN (VALUES ${placeholders})`,
|
|
676
739
|
)}) GROUP BY project, task`,
|
|
677
740
|
)
|
|
@@ -685,6 +748,16 @@ export class LocalHistoryProvider implements LocalHistory {
|
|
|
685
748
|
}
|
|
686
749
|
}
|
|
687
750
|
|
|
751
|
+
/** A restored task's producer use, from the window's hits (`producerRows`). */
|
|
752
|
+
interface ProducerRow {
|
|
753
|
+
project: string
|
|
754
|
+
task: string
|
|
755
|
+
def?: string | null
|
|
756
|
+
rss: number | null
|
|
757
|
+
cpu: number | null
|
|
758
|
+
ms: number | null
|
|
759
|
+
}
|
|
760
|
+
|
|
688
761
|
// A group holds at least one row, so every SUM is a number and `sorted` is
|
|
689
762
|
// never empty; q < 1 keeps the index inside it.
|
|
690
763
|
function pickPercentile(sorted: number[], q: number): number {
|
package/src/history/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ export {
|
|
|
16
16
|
} from './history.js'
|
|
17
17
|
export {
|
|
18
18
|
detectFlaky,
|
|
19
|
+
knownFlaky,
|
|
19
20
|
type FailureMode,
|
|
20
21
|
type FlakyCandidate,
|
|
21
22
|
type FlakyFinding,
|
|
@@ -32,6 +33,7 @@ export {
|
|
|
32
33
|
latestRunId,
|
|
33
34
|
listInvocations,
|
|
34
35
|
listRuns,
|
|
36
|
+
recordedTasks,
|
|
35
37
|
rootCauses,
|
|
36
38
|
upstreamTask,
|
|
37
39
|
whyDidThisRerun,
|