@vzn/vx 0.0.646 → 0.0.649
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 +29 -22
- package/launcher.cjs +9 -2
- package/launcher.sh +2 -2
- package/package.json +5 -5
- package/schemas/cache.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/info.json +16 -2
- package/schemas/last.json +112 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +18 -2
- package/src/bin.ts +53 -12
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +268 -53
- package/src/cache/git-inputs.ts +219 -87
- package/src/cache/index.ts +3 -2
- package/src/cache/inputs.ts +460 -78
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +49 -9
- package/src/cache/layered-cache.ts +123 -58
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +19 -3
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +90 -14
- package/src/cache/run-history.ts +70 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +25 -6
- package/src/cache/tar-stream.ts +5 -0
- package/src/cache/umask.ts +1 -2
- package/src/cli/cache.ts +58 -27
- package/src/cli/completions.ts +12 -5
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +58 -19
- package/src/cli/index.ts +79 -33
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +186 -31
- package/src/cli/last.ts +439 -45
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +70 -14
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +109 -34
- package/src/cli/select.ts +143 -46
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +151 -49
- package/src/cli/watch-cycle.ts +10 -5
- package/src/cli/watch-filter.ts +71 -3
- package/src/cli/watch-fs.ts +22 -6
- package/src/cli/watch-judge.ts +48 -6
- package/src/cli/watch.ts +163 -42
- package/src/cli/why.ts +211 -73
- package/src/cli/workspace-config.ts +54 -15
- package/src/config.ts +13 -1
- package/src/exec/env.ts +7 -2
- package/src/exec/executor.ts +154 -10
- package/src/exec/index.ts +6 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +3 -1
- package/src/exec/runner.ts +253 -54
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +211 -24
- package/src/exec/sandbox-violations.ts +52 -20
- package/src/graph/critical-path.ts +79 -0
- package/src/graph/default-build.ts +19 -0
- package/src/graph/definition.ts +38 -0
- package/src/graph/folds.ts +38 -5
- package/src/graph/index.ts +4 -1
- package/src/graph/priorities.ts +74 -8
- package/src/graph/scheduler.ts +270 -60
- package/src/graph/task-graph.ts +228 -35
- package/src/handoff.ts +97 -9
- package/src/history/failure-mode.ts +6 -3
- package/src/history/history.ts +167 -61
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +90 -30
- package/src/index.ts +30 -13
- package/src/orchestrator/admission.ts +10 -6
- package/src/orchestrator/affected-tasks.ts +11 -6
- package/src/orchestrator/deferred-outputs.ts +2 -0
- package/src/orchestrator/doctor.ts +134 -42
- package/src/orchestrator/execute-args.ts +4 -1
- package/src/orchestrator/execute-task.ts +130 -38
- package/src/orchestrator/fingerprint-watch.ts +1 -1
- package/src/orchestrator/graph-memo.ts +140 -0
- package/src/orchestrator/graph-stage.ts +18 -1
- package/src/orchestrator/held-servers.ts +1 -1
- package/src/orchestrator/hit-restore.ts +217 -119
- package/src/orchestrator/index.ts +4 -3
- package/src/orchestrator/local-floor.ts +119 -25
- package/src/orchestrator/local-shortcircuit.ts +28 -4
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +42 -8
- package/src/orchestrator/options.ts +8 -6
- package/src/orchestrator/output-host.ts +43 -14
- package/src/orchestrator/output-log.ts +90 -10
- package/src/orchestrator/persistent-task.ts +84 -29
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +185 -17
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +214 -42
- package/src/orchestrator/plugin.ts +113 -37
- package/src/orchestrator/prepare.ts +168 -30
- package/src/orchestrator/projects.ts +65 -40
- package/src/orchestrator/remote-prefetch.ts +119 -25
- package/src/orchestrator/run-artifacts.ts +95 -32
- package/src/orchestrator/run-context.ts +39 -41
- package/src/orchestrator/run-failures.ts +65 -23
- package/src/orchestrator/run-id.ts +6 -3
- package/src/orchestrator/run-keys.ts +149 -13
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -29
- package/src/orchestrator/run-report.ts +3 -3
- package/src/orchestrator/run-steps.ts +58 -22
- package/src/orchestrator/run-stop.ts +22 -5
- package/src/orchestrator/run.ts +331 -88
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +70 -20
- package/src/orchestrator/shell-verdict.ts +5 -5
- package/src/orchestrator/signals.ts +26 -1
- package/src/orchestrator/stable-keys.ts +92 -8
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-artifacts.ts +90 -26
- package/src/orchestrator/task-hash.ts +78 -13
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +7 -1
- package/src/orchestrator/telemetry-host.ts +22 -2
- package/src/orchestrator/telemetry.ts +53 -21
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +31 -9
- package/src/report/framed-output.ts +141 -50
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +170 -179
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +81 -53
- package/src/report/summary.ts +114 -32
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +7 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +4 -2
- package/src/util/config-frame.ts +41 -4
- package/src/{graph → util}/dependency-spec.ts +22 -13
- package/src/util/edit-distance.ts +13 -1
- package/src/util/env-flag.ts +9 -0
- package/src/util/errors.ts +59 -3
- package/src/util/essential-env.ts +57 -0
- package/src/util/git-ignored.ts +1 -1
- package/src/util/hash.ts +35 -0
- package/src/util/hide-env.ts +45 -5
- package/src/util/index.ts +21 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +91 -21
- package/src/util/report.ts +6 -1
- package/src/util/secret-mask.ts +62 -1
- package/src/util/secrets.ts +7 -2
- package/src/util/size.ts +11 -3
- package/src/util/tail.ts +6 -1
- package/src/util/timing.ts +13 -1
- package/src/util/ui.ts +61 -3
- package/src/util/which.ts +1 -1
- package/src/util/write-file.ts +67 -0
- package/src/workspace/affected.ts +710 -107
- package/src/workspace/bracket-literal.ts +84 -0
- package/src/workspace/canonical-dir.ts +37 -0
- package/src/workspace/config-bytecode.ts +17 -11
- package/src/workspace/config-eval.ts +29 -8
- package/src/workspace/config-imports.ts +199 -58
- package/src/workspace/config-schema.ts +242 -39
- package/src/workspace/filter.ts +37 -8
- package/src/workspace/index.ts +11 -1
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +69 -18
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +226 -37
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
|
@@ -141,7 +141,7 @@ const keyOf = (c: { project: string; task: string; hash: string }): string =>
|
|
|
141
141
|
/**
|
|
142
142
|
* Which of this run's executed tasks are flaky, judged BEFORE the run's own
|
|
143
143
|
* rows are recorded: a failure on a key that has passed before, a pass on a
|
|
144
|
-
* key that already did so, or a within-run retry. A first pass after only
|
|
144
|
+
* key that already did so, or a pass that took a within-run retry. A first pass after only
|
|
145
145
|
* failures is a recovery, not a flake. The counts fold this run in.
|
|
146
146
|
*
|
|
147
147
|
* The cost follows the run's colour. No candidate (every task a hit or a
|
|
@@ -187,7 +187,10 @@ export function detectFlaky(db: Database, candidates: readonly FlakyCandidate[])
|
|
|
187
187
|
for (const c of suspects) {
|
|
188
188
|
const before = counts.get(keyOf(c)) ?? { passes: 0, failures: 0, relapses: 0 }
|
|
189
189
|
const relapsed = c.status === 'failed' ? before.passes > 0 : before.relapses > 0
|
|
190
|
-
|
|
190
|
+
// A retry proves nothing until one attempt passes: every attempt
|
|
191
|
+
// failing on a key that never passed is a break, as `flakyTasks` reads it.
|
|
192
|
+
const retriedPass = c.status === 'success' && c.attempts > 1
|
|
193
|
+
if (!relapsed && !retriedPass) continue
|
|
191
194
|
const passes = before.passes + (c.status === 'success' ? 1 : 0)
|
|
192
195
|
const failures = before.failures + (c.status === 'failed' ? 1 : 0)
|
|
193
196
|
out.push({ ...c, taskId: `${c.project}#${c.task}`, passes, failures })
|
|
@@ -213,7 +216,7 @@ export interface FlakyTask {
|
|
|
213
216
|
* (30 days, `RunHistory.pruneOlderThan`), most failures first: the
|
|
214
217
|
* doctor's list. A pass that took a retry mixes its key on its own: it is
|
|
215
218
|
* one `success` row whose failed attempts count as failures (`vx info`
|
|
216
|
-
* said `none` over one). A full scan of `
|
|
219
|
+
* said `none` over one). A full scan of `task_outcomes`, which an inspection verb
|
|
217
220
|
* affords.
|
|
218
221
|
*/
|
|
219
222
|
export function flakyTasks(db: Database): FlakyTask[] {
|
package/src/history/history.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// HistoryTable + HistoryProvider — historical run data the `schedule`
|
|
2
|
-
// plugin (`@vzn/vx-
|
|
2
|
+
// plugin (`@vzn/vx-history`, and its `vx history` verb) and
|
|
3
3
|
// `--dry`'s time prediction read.
|
|
4
4
|
//
|
|
5
5
|
// The data has been in cache.db.runs since schema v11; what's new is
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import type { Database } from 'bun:sqlite'
|
|
15
15
|
import { branchSource, EXECUTED_RUNS_SQL } from '../cache/index.js'
|
|
16
|
-
import { splitTaskId } from '../util/index.js'
|
|
16
|
+
import { isMeasure, splitTaskId } from '../util/index.js'
|
|
17
17
|
import { failureModeOf, mixedOutcomeKeysSql } from './failure-mode.js'
|
|
18
18
|
import type { FailureMode } from './failure-mode.js'
|
|
19
19
|
import { isPassStatus, TASK_STATUSES } from '../graph/index.js'
|
|
@@ -33,11 +33,21 @@ export const PASS_STATUSES = `(${TASK_STATUSES.filter(isPassStatus)
|
|
|
33
33
|
/** The share of a machine a task must use to have filled it. */
|
|
34
34
|
const SATURATED = 0.9
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* The cores a run of `runs r` kept busy, as need: its CPU over its time,
|
|
38
|
+
* divided by the share of a core its threads got (`cpu_share`, v49). A run
|
|
39
|
+
* on a crowded worker used its fair share only: nine test shards on four
|
|
40
|
+
* cores each used 0.36 of the 1.1 they take alone, and learning 0.36 packed
|
|
41
|
+
* the next run as tight (main c27c238, 2026-10-23). Never past the machine.
|
|
42
|
+
*/
|
|
43
|
+
const NEED_SQL = `MIN(r.cpu_ms * 1.0 / (r.duration_ms * COALESCE(r.cpu_share, 1)),
|
|
44
|
+
COALESCE(r.placement_cores, r.cpu_ms * 1.0 / (r.duration_ms * COALESCE(r.cpu_share, 1))))`
|
|
45
|
+
|
|
36
46
|
/** Per (project#task) — last RECENT runs collapsed into a summary. */
|
|
37
47
|
export interface TaskHistory {
|
|
38
48
|
/** Total runs in the recent window. */
|
|
39
49
|
runs: number
|
|
40
|
-
/** Wall-clock p50 (ms). Cache-hit rows excluded so this reflects work actually done. */
|
|
50
|
+
/** Wall-clock p50 (ms), a crowded run's as if alone. Cache-hit rows excluded so this reflects work actually done. */
|
|
41
51
|
p50DurationMs: number | undefined
|
|
42
52
|
/** Wall-clock p99 (ms). Same exclusion. */
|
|
43
53
|
p99DurationMs: number | undefined
|
|
@@ -56,17 +66,21 @@ export interface TaskHistory {
|
|
|
56
66
|
*/
|
|
57
67
|
maxPeakRssBytes?: number
|
|
58
68
|
/**
|
|
59
|
-
* The
|
|
60
|
-
*
|
|
61
|
-
*
|
|
69
|
+
* The CPU parallelism the window's successful executions needed on
|
|
70
|
+
* average: cpu time over wall time over the share of a core it got, so
|
|
71
|
+
* 2.0 means two cores busy for the whole task. An average, not the
|
|
72
|
+
* most: one crowded run's need read 2.84 cores for a build that uses
|
|
73
|
+
* 1.3 to 1.9, booked it a whole 3-core Mac, and the tasks beside it
|
|
74
|
+
* waited 45 s (main b5b83bb). Undefined when no row carried both numbers.
|
|
62
75
|
*/
|
|
63
76
|
maxCpuParallelism?: number
|
|
64
77
|
/**
|
|
65
78
|
* The most cores, and the most memory, of a machine an executor ran it
|
|
66
79
|
* on (`ExecuteResult.machine`) that it used all of: 90% of the cores
|
|
67
|
-
* over its whole run,
|
|
80
|
+
* over its whole run, averaged over the window's runs on machines that
|
|
81
|
+
* size, or 90% of the memory in any one. A burst to every core is
|
|
68
82
|
* not one: tasks beside it slow for a moment, and booking a machine for
|
|
69
|
-
* it left CI agents half idle (
|
|
83
|
+
* it left CI agents half idle (2026-10-22). A
|
|
70
84
|
* task that filled a machine wanted a bigger one. Set by `resourcesFor`.
|
|
71
85
|
*/
|
|
72
86
|
saturatedCores?: number
|
|
@@ -110,8 +124,37 @@ export class LocalHistoryProvider implements HistoryProvider {
|
|
|
110
124
|
readonly recent: number = DEFAULT_RECENT,
|
|
111
125
|
/** The run's branch (`currentBranch`); undefined: a forecast reads every branch's timings. */
|
|
112
126
|
private readonly branch?: string | null,
|
|
127
|
+
/**
|
|
128
|
+
* Each task's definition (`taskDefinitions`): given, a task reads only
|
|
129
|
+
* the rows of it as it is now, and a task not in it reads none. A row
|
|
130
|
+
* of no definition (timings shared before definitions were kept) is
|
|
131
|
+
* read: dropping them left a PR's run with no task measured (#4150).
|
|
132
|
+
*/
|
|
133
|
+
private readonly defs?: ReadonlyMap<string, string>,
|
|
113
134
|
) {}
|
|
114
135
|
|
|
136
|
+
/** `r.def` in a SELECT and GROUP BY when rows are matched to definitions. */
|
|
137
|
+
private get byDef(): string {
|
|
138
|
+
return this.defs === undefined ? '' : ', r.def'
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Whether a row of `id` under `def` is the task as it is now. */
|
|
142
|
+
private current(id: string, def: string | null | undefined): boolean {
|
|
143
|
+
return this.defs === undefined || def == null || this.defs.get(id) === def
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** A task id's `[project, task, def?]`, the JSON a statement matches rows by. */
|
|
147
|
+
private keyOf(id: string): string[] {
|
|
148
|
+
const [project, task] = splitTaskId(id)
|
|
149
|
+
if (this.defs === undefined) return [project, task]
|
|
150
|
+
return [project, task, this.defs.get(id) ?? '']
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The condition on a `task_durations` row's `def`, against `j.value ->> 2`. */
|
|
154
|
+
private get durationDef(): string {
|
|
155
|
+
return this.defs === undefined ? '' : 'AND (def IS NULL OR def = j.value ->> 2)'
|
|
156
|
+
}
|
|
157
|
+
|
|
115
158
|
/**
|
|
116
159
|
* The window is the last `recent` INVOCATIONS, read as one rowid slice.
|
|
117
160
|
*
|
|
@@ -121,8 +164,8 @@ export class LocalHistoryProvider implements HistoryProvider {
|
|
|
121
164
|
* ranking every pair's rows newest-first (`ROW_NUMBER` over a partition)
|
|
122
165
|
* sorted the whole table either way, 230 ms at 116k rows. Rows are
|
|
123
166
|
* appended in time order, one contiguous block per invocation, so the
|
|
124
|
-
* rows of the newest `recent` invocations are exactly `id
|
|
125
|
-
* the `recent
|
|
167
|
+
* rows of the newest `recent` invocations are exactly `id >=` the first
|
|
168
|
+
* row of the newest `recent` headers — a primary-key range, no sort, plain
|
|
126
169
|
* aggregates, and at most one row per pair per invocation, so "the last
|
|
127
170
|
* `recent` invocations" IS the last `recent` runs for a pair that runs
|
|
128
171
|
* every time and whatever fewer it has for one that runs less often. A
|
|
@@ -150,22 +193,24 @@ export class LocalHistoryProvider implements HistoryProvider {
|
|
|
150
193
|
const sql = `
|
|
151
194
|
SELECT
|
|
152
195
|
r.project,
|
|
153
|
-
r.task,
|
|
196
|
+
r.task${this.defs === undefined ? '' : ', r.def AS def'},
|
|
154
197
|
COUNT(*) AS total,
|
|
155
198
|
SUM(CASE WHEN r.status IN ${PASS_STATUSES} THEN 1 ELSE 0 END) AS successes,
|
|
156
199
|
SUM(CASE WHEN r.cache_hit = 1 THEN 1 ELSE 0 END) AS hits,
|
|
157
200
|
SUM(CASE WHEN r.status = 'failed' THEN 1 ELSE 0 END) AS failures,
|
|
158
|
-
SUM(CASE WHEN r.attempts > 1 THEN 1 ELSE 0 END) AS retried,
|
|
201
|
+
SUM(CASE WHEN r.status = 'success' AND r.attempts > 1 THEN 1 ELSE 0 END) AS retried,
|
|
159
202
|
GROUP_CONCAT(CASE WHEN (r.cache_hit IS NULL OR r.cache_hit = 0) AND r.status = 'success'
|
|
160
203
|
THEN r.duration_ms END) AS ds,
|
|
161
204
|
MAX(CASE WHEN (r.cache_hit IS NULL OR r.cache_hit = 0) AND r.status = 'success'
|
|
162
205
|
THEN r.peak_rss_bytes
|
|
163
206
|
WHEN r.cache_hit = 1 THEN e.peak_rss_bytes END) AS rss,
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
207
|
+
-- A hit's producer use only where no run here measured it (resourcesFor).
|
|
208
|
+
COALESCE(
|
|
209
|
+
AVG(CASE WHEN (r.cache_hit IS NULL OR r.cache_hit = 0) AND r.status = 'success'
|
|
210
|
+
AND r.cpu_ms IS NOT NULL AND r.duration_ms > 0
|
|
211
|
+
THEN ${NEED_SQL} END),
|
|
212
|
+
MAX(CASE WHEN r.cache_hit = 1 AND e.cpu_ms IS NOT NULL AND e.duration_ms > 0
|
|
213
|
+
THEN e.cpu_ms * 1.0 / e.duration_ms END)) AS cpu
|
|
169
214
|
FROM runs r
|
|
170
215
|
-- A hit executed nothing here, but its entry carries what the
|
|
171
216
|
-- PRODUCING execution used (the artifact's sidecar, indexed at save
|
|
@@ -175,11 +220,12 @@ export class LocalHistoryProvider implements HistoryProvider {
|
|
|
175
220
|
-- since contributes nothing.
|
|
176
221
|
LEFT JOIN entries e ON r.cache_hit = 1 AND e.hash = r.hash
|
|
177
222
|
WHERE r.id >= ? AND ${EXECUTED_RUNS_SQL}
|
|
178
|
-
GROUP BY r.project, r.task
|
|
223
|
+
GROUP BY r.project, r.task${this.byDef}
|
|
179
224
|
`
|
|
180
225
|
type Row = {
|
|
181
226
|
project: string
|
|
182
227
|
task: string
|
|
228
|
+
def?: string | null
|
|
183
229
|
total: number
|
|
184
230
|
successes: number
|
|
185
231
|
hits: number
|
|
@@ -192,9 +238,10 @@ export class LocalHistoryProvider implements HistoryProvider {
|
|
|
192
238
|
// Every pair in the slice is grouped (the slice is bounded, so this is
|
|
193
239
|
// cheap) and the asked-for ones are picked out here — a per-row IN-list
|
|
194
240
|
// probe in SQL cost more than the grouping it would have saved.
|
|
195
|
-
const rows = (this.db.query(sql).all(floor) as Row[]).filter(
|
|
196
|
-
|
|
197
|
-
|
|
241
|
+
const rows = (this.db.query(sql).all(floor) as Row[]).filter((r) => {
|
|
242
|
+
const id = `${r.project}#${r.task}`
|
|
243
|
+
return (wanted === null || wanted.has(id)) && this.current(id, r.def)
|
|
244
|
+
})
|
|
198
245
|
|
|
199
246
|
// The flakiness signal needs per-key outcomes, which the aggregate above
|
|
200
247
|
// folded away — one more pass over the slice, only for the pairs whose
|
|
@@ -264,18 +311,27 @@ export class LocalHistoryProvider implements HistoryProvider {
|
|
|
264
311
|
sha: string,
|
|
265
312
|
timings: ReadonlyMap<string, number>,
|
|
266
313
|
needs: ReadonlyMap<string, TaskResources> = new Map(),
|
|
314
|
+
/** Each task's definition when it was timed (`taskDefinition`). */
|
|
315
|
+
defs: ReadonlyMap<string, string> = new Map(),
|
|
267
316
|
): void {
|
|
268
317
|
const insert = this.db.query(
|
|
269
318
|
`INSERT OR IGNORE INTO task_durations(project, task, source, sha, duration_ms, recorded_at,
|
|
270
|
-
cpu_parallelism, peak_rss_bytes, saturated_cores, saturated_memory_bytes)
|
|
271
|
-
VALUES (?, ?, 'remote', ?, ?, ?, ?, ?, ?, ?)`,
|
|
319
|
+
cpu_parallelism, peak_rss_bytes, saturated_cores, saturated_memory_bytes, def)
|
|
320
|
+
VALUES (?, ?, 'remote', ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
272
321
|
)
|
|
273
322
|
const now = Date.now()
|
|
274
323
|
this.db
|
|
275
324
|
.transaction(() => {
|
|
276
325
|
for (const [id, ms] of timings) {
|
|
277
|
-
const [project, task] = splitTaskId(id)
|
|
278
326
|
const n = needs.get(id)
|
|
327
|
+
// A plugin's Infinity or negative, kept, made every later forecast
|
|
328
|
+
// NaN or below zero: the row is never written again to correct it.
|
|
329
|
+
if (
|
|
330
|
+
!isMeasure(ms) ||
|
|
331
|
+
(n !== undefined && !Object.values(n).every((v) => v === undefined || isMeasure(v)))
|
|
332
|
+
)
|
|
333
|
+
continue
|
|
334
|
+
const [project, task] = splitTaskId(id)
|
|
279
335
|
insert.run(
|
|
280
336
|
project,
|
|
281
337
|
task,
|
|
@@ -286,6 +342,7 @@ export class LocalHistoryProvider implements HistoryProvider {
|
|
|
286
342
|
n?.maxPeakRssBytes ?? null,
|
|
287
343
|
n?.saturatedCores ?? null,
|
|
288
344
|
n?.saturatedMemoryBytes ?? null,
|
|
345
|
+
defs.get(id) ?? null,
|
|
289
346
|
)
|
|
290
347
|
}
|
|
291
348
|
})
|
|
@@ -293,7 +350,7 @@ export class LocalHistoryProvider implements HistoryProvider {
|
|
|
293
350
|
}
|
|
294
351
|
|
|
295
352
|
/**
|
|
296
|
-
* Each task's duration forecast (
|
|
353
|
+
* Each task's duration forecast (2026-10-14): the median of its
|
|
297
354
|
* last `FORECAST_RUNS` timings on this branch, else the default branch's
|
|
298
355
|
* newest imported timing (`importTimings`), else the median on any
|
|
299
356
|
* branch, else none. The timings are the shared store's `task_durations`, so every
|
|
@@ -333,11 +390,11 @@ export class LocalHistoryProvider implements HistoryProvider {
|
|
|
333
390
|
.query(
|
|
334
391
|
`SELECT j.key AS i, (SELECT group_concat(ms) FROM (
|
|
335
392
|
SELECT duration_ms AS ms FROM task_durations
|
|
336
|
-
WHERE project = j.value ->> 0 AND task = j.value ->> 1 AND ${tier.where}
|
|
393
|
+
WHERE project = j.value ->> 0 AND task = j.value ->> 1 AND ${tier.where} ${this.durationDef}
|
|
337
394
|
ORDER BY id DESC LIMIT ${tier.limit})) AS ds
|
|
338
395
|
FROM json_each(?) AS j`,
|
|
339
396
|
)
|
|
340
|
-
.all(...tier.params, JSON.stringify(pending.map(
|
|
397
|
+
.all(...tier.params, JSON.stringify(pending.map((id) => this.keyOf(id)))) as {
|
|
341
398
|
i: number
|
|
342
399
|
ds: string | null
|
|
343
400
|
}[]
|
|
@@ -377,70 +434,108 @@ export class LocalHistoryProvider implements HistoryProvider {
|
|
|
377
434
|
type Executed = {
|
|
378
435
|
project: string
|
|
379
436
|
task: string
|
|
437
|
+
def?: string | null
|
|
380
438
|
rss: number | null
|
|
381
439
|
cpu: number | null
|
|
382
440
|
}
|
|
383
441
|
const executed = this.db
|
|
384
442
|
.query(
|
|
385
|
-
`SELECT project, task, MAX(peak_rss_bytes) AS rss,
|
|
386
|
-
|
|
387
|
-
THEN
|
|
388
|
-
FROM runs
|
|
389
|
-
WHERE id >= ? AND status = 'success' AND (cache_hit IS NULL OR cache_hit = 0)
|
|
390
|
-
GROUP BY project, task`,
|
|
443
|
+
`SELECT r.project, r.task${this.byDef}, MAX(r.peak_rss_bytes) AS rss,
|
|
444
|
+
AVG(CASE WHEN r.cpu_ms IS NOT NULL AND r.duration_ms > 0
|
|
445
|
+
THEN ${NEED_SQL} END) AS cpu
|
|
446
|
+
FROM runs r
|
|
447
|
+
WHERE r.id >= ? AND r.status = 'success' AND (r.cache_hit IS NULL OR r.cache_hit = 0)
|
|
448
|
+
GROUP BY r.project, r.task${this.byDef}`,
|
|
391
449
|
)
|
|
392
450
|
.all(floor) as Executed[]
|
|
393
451
|
const p50s = this.forecasts(wanted)
|
|
394
452
|
for (const row of executed) {
|
|
395
453
|
const key = `${row.project}#${row.task}`
|
|
396
|
-
if (!wanted.has(key)) continue
|
|
454
|
+
if (!wanted.has(key) || !this.current(key, row.def)) continue
|
|
397
455
|
out.set(key, {
|
|
398
456
|
p50DurationMs: p50s.get(key),
|
|
399
457
|
...(row.rss !== null ? { maxPeakRssBytes: row.rss } : {}),
|
|
400
458
|
...(row.cpu !== null ? { maxCpuParallelism: row.cpu } : {}),
|
|
401
459
|
})
|
|
402
460
|
}
|
|
403
|
-
type Hit = {
|
|
461
|
+
type Hit = {
|
|
462
|
+
project: string
|
|
463
|
+
task: string
|
|
464
|
+
def?: string | null
|
|
465
|
+
rss: number | null
|
|
466
|
+
cpu: number | null
|
|
467
|
+
ms: number | null
|
|
468
|
+
}
|
|
404
469
|
const hits = this.db
|
|
405
470
|
.query(
|
|
406
|
-
`SELECT
|
|
471
|
+
`SELECT r.project, r.task${this.byDef}, MAX(e.peak_rss_bytes) AS rss,
|
|
407
472
|
MAX(CASE WHEN e.cpu_ms IS NOT NULL AND e.duration_ms > 0
|
|
408
|
-
THEN e.cpu_ms * 1.0 / e.duration_ms END) AS cpu
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
473
|
+
THEN e.cpu_ms * 1.0 / e.duration_ms END) AS cpu,
|
|
474
|
+
MAX(CASE WHEN e.duration_ms > 0 THEN e.duration_ms END) AS ms
|
|
475
|
+
FROM (SELECT DISTINCT project, task, hash, def FROM runs WHERE id >= ? AND cache_hit = 1) r
|
|
476
|
+
JOIN entries e ON e.hash = r.hash
|
|
477
|
+
GROUP BY r.project, r.task${this.byDef}`,
|
|
412
478
|
)
|
|
413
479
|
.all(floor) as Hit[]
|
|
480
|
+
// A hit's CPU is its producer's use, not need: the entry keeps no
|
|
481
|
+
// share, so a crowded producer's reads low. Only a task with no need
|
|
482
|
+
// from a run here or the default branch's import takes it: main's
|
|
483
|
+
// blob carried each hit shard's crowded use forward, and the next run
|
|
484
|
+
// packed them as tight (main 357d3aa, 2026-10-23).
|
|
485
|
+
const hitCpu = new Map<string, number>()
|
|
414
486
|
for (const row of hits) {
|
|
415
487
|
const key = `${row.project}#${row.task}`
|
|
416
|
-
if (
|
|
488
|
+
if (
|
|
489
|
+
!wanted.has(key) ||
|
|
490
|
+
!this.current(key, row.def) ||
|
|
491
|
+
(row.rss === null && row.cpu === null && row.ms === null)
|
|
492
|
+
)
|
|
493
|
+
continue
|
|
494
|
+
if (row.cpu !== null) hitCpu.set(key, row.cpu)
|
|
417
495
|
const had = out.get(key)
|
|
418
496
|
const rss = Math.max(had?.maxPeakRssBytes ?? -Infinity, row.rss ?? -Infinity)
|
|
419
|
-
const cpu = Math.max(had?.maxCpuParallelism ?? -Infinity, row.cpu ?? -Infinity)
|
|
420
497
|
out.set(key, {
|
|
421
|
-
|
|
498
|
+
...had,
|
|
499
|
+
// A task this store only restored is timed by its producer: one a
|
|
500
|
+
// branch's run made, main restored and never timed, and every
|
|
501
|
+
// later run planned it at a guess (PR #4339's docs chain: 90 s
|
|
502
|
+
// guessed, 45 s run, 2026-10-24).
|
|
503
|
+
p50DurationMs: p50s.get(key) ?? row.ms ?? undefined,
|
|
422
504
|
...(rss !== -Infinity ? { maxPeakRssBytes: rss } : {}),
|
|
423
|
-
...(cpu !== -Infinity ? { maxCpuParallelism: cpu } : {}),
|
|
424
505
|
})
|
|
425
506
|
}
|
|
426
507
|
// A failure counts: a task killed for memory filled its machine too.
|
|
427
|
-
type Saturated = {
|
|
508
|
+
type Saturated = {
|
|
509
|
+
project: string
|
|
510
|
+
task: string
|
|
511
|
+
def?: string | null
|
|
512
|
+
cores: number | null
|
|
513
|
+
memory: number | null
|
|
514
|
+
}
|
|
428
515
|
const saturated = this.db
|
|
429
516
|
.query(
|
|
430
|
-
`SELECT project, task,
|
|
431
|
-
MAX(CASE WHEN
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
517
|
+
`SELECT r.project, r.task${this.byDef},
|
|
518
|
+
MAX(CASE WHEN r.fill >= ${SATURATED} THEN r.cores END) AS cores,
|
|
519
|
+
MAX(r.memory) AS memory
|
|
520
|
+
FROM (SELECT r.project, r.task${this.byDef}, r.placement_cores AS cores,
|
|
521
|
+
AVG(${NEED_SQL} / r.placement_cores) AS fill,
|
|
522
|
+
MAX(CASE WHEN r.peak_rss_bytes >= ${SATURATED} * r.placement_memory
|
|
523
|
+
THEN r.placement_memory END) AS memory
|
|
524
|
+
FROM runs r
|
|
525
|
+
WHERE r.id >= ? AND r.placement_cores IS NOT NULL AND r.duration_ms > 0
|
|
526
|
+
AND r.status IN ('success', 'failed') AND (r.cache_hit IS NULL OR r.cache_hit = 0)
|
|
527
|
+
GROUP BY r.project, r.task${this.byDef}, r.placement_cores) r
|
|
528
|
+
GROUP BY r.project, r.task${this.byDef}`,
|
|
439
529
|
)
|
|
440
530
|
.all(floor) as Saturated[]
|
|
441
531
|
for (const row of saturated) {
|
|
442
532
|
const key = `${row.project}#${row.task}`
|
|
443
|
-
if (
|
|
533
|
+
if (
|
|
534
|
+
!wanted.has(key) ||
|
|
535
|
+
!this.current(key, row.def) ||
|
|
536
|
+
(row.cores === null && row.memory === null)
|
|
537
|
+
)
|
|
538
|
+
continue
|
|
444
539
|
out.set(key, {
|
|
445
540
|
...(out.get(key) ?? { p50DurationMs: p50s.get(key) }),
|
|
446
541
|
...(row.cores !== null ? { saturatedCores: row.cores } : {}),
|
|
@@ -450,6 +545,10 @@ export class LocalHistoryProvider implements HistoryProvider {
|
|
|
450
545
|
// A forecast reads past the window: a task with none in it still has one.
|
|
451
546
|
for (const [key, p50] of p50s) if (!out.has(key)) out.set(key, { p50DurationMs: p50 })
|
|
452
547
|
this.remoteNeeds(wanted, out)
|
|
548
|
+
for (const [key, cpu] of hitCpu) {
|
|
549
|
+
const had = out.get(key)!
|
|
550
|
+
if (had.maxCpuParallelism === undefined) out.set(key, { ...had, maxCpuParallelism: cpu })
|
|
551
|
+
}
|
|
453
552
|
return out
|
|
454
553
|
}
|
|
455
554
|
|
|
@@ -477,10 +576,10 @@ export class LocalHistoryProvider implements HistoryProvider {
|
|
|
477
576
|
JOIN task_durations d ON d.id = (
|
|
478
577
|
SELECT id FROM task_durations
|
|
479
578
|
WHERE project = j.value ->> 0 AND task = j.value ->> 1 AND source = 'remote'
|
|
480
|
-
AND cpu_parallelism IS NOT NULL
|
|
579
|
+
AND cpu_parallelism IS NOT NULL ${this.durationDef}
|
|
481
580
|
ORDER BY id DESC LIMIT 1)`,
|
|
482
581
|
)
|
|
483
|
-
.all(JSON.stringify(pending.map(
|
|
582
|
+
.all(JSON.stringify(pending.map((id) => this.keyOf(id)))) as {
|
|
484
583
|
i: number
|
|
485
584
|
cpu: number
|
|
486
585
|
rss: number | null
|
|
@@ -507,14 +606,21 @@ export class LocalHistoryProvider implements HistoryProvider {
|
|
|
507
606
|
|
|
508
607
|
/** The first `runs` id of the newest `recent` invocations (see `loadFor`). */
|
|
509
608
|
private floor(): number {
|
|
609
|
+
// The first row of each invocation in the window, an index probe each:
|
|
610
|
+
// a run of only a group records a header and no row (X-50), and at the
|
|
611
|
+
// oldest slot it gave no first row, so the window read the whole table.
|
|
510
612
|
const row = this.db
|
|
511
613
|
.query(
|
|
512
|
-
`SELECT
|
|
513
|
-
(SELECT
|
|
614
|
+
`SELECT COUNT(*) AS n, MIN(first) AS id,
|
|
615
|
+
(SELECT COALESCE(MAX(id), 0) + 1 FROM runs) AS past
|
|
616
|
+
FROM (SELECT (SELECT MIN(id) FROM runs WHERE run_id = i.run_id) AS first
|
|
617
|
+
FROM (SELECT run_id FROM invocations ORDER BY rowid DESC LIMIT ?) i)`,
|
|
514
618
|
)
|
|
515
|
-
.get(this.recent
|
|
619
|
+
.get(this.recent) as { n: number; id: number | null; past: number }
|
|
516
620
|
// Fewer invocations than the window (or none): the whole table is the window.
|
|
517
|
-
|
|
621
|
+
if (row.n < this.recent) return 0
|
|
622
|
+
// A window of only empty invocations holds no row.
|
|
623
|
+
return row.id ?? row.past
|
|
518
624
|
}
|
|
519
625
|
|
|
520
626
|
/** Mixed-outcome key count per `project#task`, over the slice, for `pairs` only. */
|
package/src/history/index.ts
CHANGED