@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
package/src/history/history.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// (zero-config); the interface is the seam a plugin fills for any other.
|
|
13
13
|
|
|
14
14
|
import type { Database } from 'bun:sqlite'
|
|
15
|
-
import { branchSource, EXECUTED_RUNS_SQL } from '../cache/index.js'
|
|
15
|
+
import { branchSource, type Cache, EXECUTED_RUNS_SQL, indexDb } from '../cache/index.js'
|
|
16
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'
|
|
@@ -117,8 +117,40 @@ export class EmptyHistoryProvider implements HistoryProvider {
|
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/** One local cache's run history, read and written by core (`localHistory`): plain rows out, no handle. */
|
|
121
|
+
export interface LocalHistory extends HistoryProvider {
|
|
122
|
+
/** Each task's measured need (`p50DurationMs`, peak cores and memory). */
|
|
123
|
+
resourcesFor(taskIds: readonly string[]): Promise<ReadonlyMap<string, TaskResources>>
|
|
124
|
+
/** Whether timings of commit `sha` were imported. */
|
|
125
|
+
hasImported(sha: string): boolean
|
|
126
|
+
/** Keep commit `sha`'s timings (task id → ms) and needs as forecast rows. */
|
|
127
|
+
importTimings(
|
|
128
|
+
sha: string,
|
|
129
|
+
timings: ReadonlyMap<string, number>,
|
|
130
|
+
needs?: ReadonlyMap<string, TaskResources>,
|
|
131
|
+
defs?: ReadonlyMap<string, string>,
|
|
132
|
+
): void
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface LocalHistoryOptions {
|
|
136
|
+
/** The newest invocations a read sees (default 50). */
|
|
137
|
+
readonly recent?: number | undefined
|
|
138
|
+
/** The run's branch (`currentBranch`); undefined: a read sees every branch's timings. */
|
|
139
|
+
readonly branch?: string | null | undefined
|
|
140
|
+
/** Each task's definition (`taskDefinitions`): given, a task reads only rows of it as it is now. */
|
|
141
|
+
readonly defs?: ReadonlyMap<string, string> | undefined
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The run history in `cache`'s index. A plugin reads history only through
|
|
146
|
+
* this: core owns every read and write of cache.db (2026-10-29).
|
|
147
|
+
*/
|
|
148
|
+
export function localHistory(cache: Cache, options: LocalHistoryOptions = {}): LocalHistory {
|
|
149
|
+
return new LocalHistoryProvider(indexDb(cache), options.recent, options.branch, options.defs)
|
|
150
|
+
}
|
|
151
|
+
|
|
120
152
|
/** Reads from the orchestrator's local SQLite cache.db. */
|
|
121
|
-
export class LocalHistoryProvider implements
|
|
153
|
+
export class LocalHistoryProvider implements LocalHistory {
|
|
122
154
|
constructor(
|
|
123
155
|
private readonly db: Database,
|
|
124
156
|
readonly recent: number = DEFAULT_RECENT,
|
package/src/history/index.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -66,8 +66,15 @@ export { run, planRun, prepareRun } from './orchestrator/index.js'
|
|
|
66
66
|
// (`@vzn/vx-history` does; a plugin package reaches core only
|
|
67
67
|
// through this façade, which is what put these here).
|
|
68
68
|
export { currentBranch } from './orchestrator/index.js'
|
|
69
|
-
export {
|
|
70
|
-
export type {
|
|
69
|
+
export { localHistory } from './history/index.js'
|
|
70
|
+
export type {
|
|
71
|
+
HistoryProvider,
|
|
72
|
+
HistoryTable,
|
|
73
|
+
LocalHistory,
|
|
74
|
+
LocalHistoryOptions,
|
|
75
|
+
TaskHistory,
|
|
76
|
+
TaskResources,
|
|
77
|
+
} from './history/index.js'
|
|
71
78
|
export type { PreparedRun } from './orchestrator/index.js'
|
|
72
79
|
// `RunOptions.affected`: what `--affected`'s diff changed, per project.
|
|
73
80
|
export type { AffectedChanges } from './workspace/index.js'
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import os from 'node:os'
|
|
9
9
|
import path from 'node:path'
|
|
10
|
-
import { Cache, CACHE_VERSION, SCHEMA_VERSION } from '../cache/index.js'
|
|
10
|
+
import { indexDb, Cache, CACHE_VERSION, SCHEMA_VERSION } from '../cache/index.js'
|
|
11
11
|
import { PLUGIN_HOOKS, type WorkspaceConfig } from '../config.js'
|
|
12
12
|
import {
|
|
13
13
|
cgroupCpuQuota,
|
|
@@ -245,7 +245,7 @@ async function collectWorkspaceInfo(
|
|
|
245
245
|
indexStore = cache.storeDir
|
|
246
246
|
stats = cache.stats()
|
|
247
247
|
orphans = await cache.orphanStats()
|
|
248
|
-
flaky = flakyTasks(cache
|
|
248
|
+
flaky = flakyTasks(indexDb(cache))
|
|
249
249
|
} finally {
|
|
250
250
|
cache.close()
|
|
251
251
|
}
|
|
@@ -458,6 +458,9 @@ function hitOutcome(restore: RestoreHitArgs, restored: boolean): TaskOutcome {
|
|
|
458
458
|
...(hit.cpuMs !== undefined ? { storedCpuMs: hit.cpuMs } : {}),
|
|
459
459
|
...(hit.peakRssBytes !== undefined ? { storedPeakRssBytes: hit.peakRssBytes } : {}),
|
|
460
460
|
...(hit.sizeBytes !== undefined ? { artifactBytes: hit.sizeBytes } : {}),
|
|
461
|
+
...(hit.outputRows !== undefined
|
|
462
|
+
? { outputBytes: hit.outputRows.reduce((n, r) => n + r.size, 0) }
|
|
463
|
+
: {}),
|
|
461
464
|
...(hit.fetchMs !== undefined ? { fetchMs: hit.fetchMs } : {}),
|
|
462
465
|
hash,
|
|
463
466
|
restored: restored || hit.source === 'remote',
|
|
@@ -146,7 +146,6 @@ export {
|
|
|
146
146
|
type TaskLog,
|
|
147
147
|
taskLog,
|
|
148
148
|
} from './run-failures.js'
|
|
149
|
-
export { type RunStep, runSteps, runWarnings } from './run-steps.js'
|
|
150
149
|
export { resolveRunId, shortRunId } from './run-id.js'
|
|
151
150
|
export { collectInfo, type CollectInfoOptions, type InfoFacts } from './doctor.js'
|
|
152
151
|
export { localFloor, type LocalFloor } from './local-floor.js'
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// The live list's `cpu` and `mem`: what each running task's process tree
|
|
2
|
+
// uses now. Looked at on a timer only once the list asks for a row, so a
|
|
3
|
+
// run with no live list (a pipe, CI) registers pids and reads nothing.
|
|
4
|
+
// The look is async and off the dispatch path: a slow one skips a tick.
|
|
5
|
+
|
|
6
|
+
import { sampleTrees, type TreeUsage } from '../exec/index.js'
|
|
7
|
+
import type { LiveUsage } from '../report/index.js'
|
|
8
|
+
|
|
9
|
+
/** How often a tracked tree is looked at while the live list shows. */
|
|
10
|
+
const LOOK_MS = 500
|
|
11
|
+
|
|
12
|
+
type Every = (ms: number, fn: () => Promise<void>) => () => void
|
|
13
|
+
|
|
14
|
+
const everyInterval: Every = (ms, fn) => {
|
|
15
|
+
const timer = setInterval(() => void fn(), ms)
|
|
16
|
+
timer.unref()
|
|
17
|
+
return () => clearInterval(timer)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Tracked {
|
|
21
|
+
pid: number
|
|
22
|
+
last?: { ts: number; cpuMs: number }
|
|
23
|
+
now?: LiveUsage
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createLiveUsage(
|
|
27
|
+
opts: {
|
|
28
|
+
every?: Every
|
|
29
|
+
now?: () => number
|
|
30
|
+
sample?: (pids: number[]) => Promise<Map<number, TreeUsage>>
|
|
31
|
+
} = {},
|
|
32
|
+
): {
|
|
33
|
+
track(taskId: string, pid: number): () => void
|
|
34
|
+
usage(taskId: string): LiveUsage | undefined
|
|
35
|
+
} {
|
|
36
|
+
const every = opts.every ?? everyInterval
|
|
37
|
+
const now = opts.now ?? Date.now
|
|
38
|
+
const sample = opts.sample ?? sampleTrees
|
|
39
|
+
const tracked = new Map<string, Tracked>()
|
|
40
|
+
let asked = false
|
|
41
|
+
let looking = false
|
|
42
|
+
let stop: (() => void) | undefined
|
|
43
|
+
|
|
44
|
+
const look = async (): Promise<void> => {
|
|
45
|
+
if (looking) return
|
|
46
|
+
looking = true
|
|
47
|
+
try {
|
|
48
|
+
const entries = [...tracked]
|
|
49
|
+
const usage = await sample(entries.map(([, t]) => t.pid))
|
|
50
|
+
const ts = now()
|
|
51
|
+
for (const [id, t] of entries) {
|
|
52
|
+
const u = usage.get(t.pid)
|
|
53
|
+
if (u === undefined || tracked.get(id) !== t) continue
|
|
54
|
+
// A descendant that exited takes its CPU out of the live sum, so
|
|
55
|
+
// an interval can read negative: none is shown below zero.
|
|
56
|
+
if (t.last !== undefined && ts > t.last.ts) {
|
|
57
|
+
const cores = Math.max(0, (u.cpuMs - t.last.cpuMs) / (ts - t.last.ts))
|
|
58
|
+
t.now = { cores: Math.round(cores * 100) / 100, rssBytes: u.rssBytes }
|
|
59
|
+
}
|
|
60
|
+
t.last = { ts, cpuMs: u.cpuMs }
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
// A failed look is a missing point, never a broken run.
|
|
64
|
+
} finally {
|
|
65
|
+
looking = false
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const start = (): void => {
|
|
69
|
+
if (asked && stop === undefined && tracked.size > 0) stop = every(LOOK_MS, look)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
track(taskId, pid) {
|
|
74
|
+
const t: Tracked = { pid }
|
|
75
|
+
tracked.set(taskId, t)
|
|
76
|
+
start()
|
|
77
|
+
return () => {
|
|
78
|
+
if (tracked.get(taskId) === t) tracked.delete(taskId)
|
|
79
|
+
if (tracked.size === 0 && stop !== undefined) {
|
|
80
|
+
stop()
|
|
81
|
+
stop = undefined
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
usage(taskId) {
|
|
86
|
+
if (!asked) {
|
|
87
|
+
asked = true
|
|
88
|
+
start()
|
|
89
|
+
}
|
|
90
|
+
return tracked.get(taskId)?.now
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -7,14 +7,15 @@
|
|
|
7
7
|
// fallback for a project the file does not list and the `--affected`
|
|
8
8
|
// diff are one implementation here, so a third lockfile is a parser.
|
|
9
9
|
//
|
|
10
|
-
// The digests are memoised
|
|
11
|
-
// xxh3, so a warm run pays one read +
|
|
12
|
-
// parse; within one process (a run, a watch cycle) the file's size,
|
|
10
|
+
// The digests are memoised in cache.db (`lockfile_memos`, one row per
|
|
11
|
+
// lockfile and claimant) by the file's xxh3, so a warm run pays one read +
|
|
12
|
+
// hash + one row read, never a parse; within one process (a run, a watch cycle) the file's size,
|
|
13
13
|
// mtime, ctime and inode gate even the read; and per run the read happens once (a WeakMap
|
|
14
14
|
// on the context core hands every `key` call of a run).
|
|
15
15
|
|
|
16
16
|
import path from 'node:path'
|
|
17
|
-
import {
|
|
17
|
+
import { stat } from 'node:fs/promises'
|
|
18
|
+
import { type LockfileMemo, readLockfileMemo, writeLockfileMemo } from '../cache/index.js'
|
|
18
19
|
import { relPosix, UserError, xxh3, xxh3hex } from '../util/index.js'
|
|
19
20
|
import type { TaskNode } from '../graph/index.js'
|
|
20
21
|
import type {
|
|
@@ -62,6 +63,12 @@ export interface LockfileClaimOptions {
|
|
|
62
63
|
* parse. Called once per distinct content, under either scope.
|
|
63
64
|
*/
|
|
64
65
|
readonly importers?: (text: string) => ReadonlyMap<string, LockfileImporter>
|
|
66
|
+
/**
|
|
67
|
+
* Each importer's recorded dependency names, from the same text, kept in
|
|
68
|
+
* the memo so a staleness check reads them without a parse. Called once
|
|
69
|
+
* per distinct content.
|
|
70
|
+
*/
|
|
71
|
+
readonly recorded?: (text: string) => ReadonlyMap<string, readonly string[]>
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
/**
|
|
@@ -89,6 +96,11 @@ export interface LockfileClaimHooks {
|
|
|
89
96
|
readonly workspaceRoot: string
|
|
90
97
|
readonly cacheDir: string
|
|
91
98
|
}): Promise<ReadonlyMap<string, LockfileImporter> | undefined>
|
|
99
|
+
/** `recorded`'s answer for the file as it is now, as `importers` reads it; undefined when the file is absent or `recorded` is unset. */
|
|
100
|
+
recorded(ctx: {
|
|
101
|
+
readonly workspaceRoot: string
|
|
102
|
+
readonly cacheDir: string
|
|
103
|
+
}): Promise<ReadonlyMap<string, ReadonlySet<string>> | undefined>
|
|
92
104
|
}
|
|
93
105
|
|
|
94
106
|
interface Digests {
|
|
@@ -96,16 +108,7 @@ interface Digests {
|
|
|
96
108
|
readonly lock: string
|
|
97
109
|
readonly importers: ReadonlyMap<string, string>
|
|
98
110
|
readonly edges: ReadonlyMap<string, LockfileImporter>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
interface Memo {
|
|
102
|
-
version: number
|
|
103
|
-
claimant: string
|
|
104
|
-
lock: string
|
|
105
|
-
/** `extraFiles` → content hash when the digests were computed. */
|
|
106
|
-
extras?: Record<string, string>
|
|
107
|
-
importers: Record<string, string>
|
|
108
|
-
edges?: Record<string, LockfileImporter>
|
|
111
|
+
readonly recorded: ReadonlyMap<string, ReadonlySet<string>> | undefined
|
|
109
112
|
}
|
|
110
113
|
|
|
111
114
|
/**
|
|
@@ -202,6 +205,7 @@ export function lockfileClaim(options: LockfileClaimOptions): LockfileClaimHooks
|
|
|
202
205
|
String(digest),
|
|
203
206
|
String(options.extraFiles ?? ''),
|
|
204
207
|
String(options.importers ?? ''),
|
|
208
|
+
String(options.recorded ?? ''),
|
|
205
209
|
].join('\0'),
|
|
206
210
|
)
|
|
207
211
|
if (scope !== 'project' && scope !== 'workspace') {
|
|
@@ -216,8 +220,18 @@ export function lockfileClaim(options: LockfileClaimOptions): LockfileClaimHooks
|
|
|
216
220
|
extras: readonly string[]
|
|
217
221
|
extraStats: Readonly<Record<string, string>>
|
|
218
222
|
digests: Digests
|
|
223
|
+
/** The memo a run found no index to keep in (a discover before the first run's open). */
|
|
224
|
+
unsaved?: LockfileMemo
|
|
219
225
|
}
|
|
220
226
|
>()
|
|
227
|
+
// Workspace scope with nothing else to keep has nothing a memo saves.
|
|
228
|
+
const memoises =
|
|
229
|
+
scope === 'project' || options.importers !== undefined || options.recorded !== undefined
|
|
230
|
+
const readMemo = (cacheDir: string, lock: string) => {
|
|
231
|
+
const memo = readLockfileMemo(cacheDir, file, claimant)
|
|
232
|
+
if (memo === undefined || memo.version !== version || memo.lock !== lock) return undefined
|
|
233
|
+
return memo
|
|
234
|
+
}
|
|
221
235
|
// Per run: every `key` call of one run gets the same context object.
|
|
222
236
|
const perRun = new WeakMap<object, Promise<Digests>>()
|
|
223
237
|
|
|
@@ -227,7 +241,12 @@ export function lockfileClaim(options: LockfileClaimOptions): LockfileClaimHooks
|
|
|
227
241
|
try {
|
|
228
242
|
identity = identityOf(await stat(full))
|
|
229
243
|
} catch {
|
|
230
|
-
const digests: Digests = {
|
|
244
|
+
const digests: Digests = {
|
|
245
|
+
lock: '',
|
|
246
|
+
importers: new Map(),
|
|
247
|
+
edges: new Map(),
|
|
248
|
+
recorded: undefined,
|
|
249
|
+
}
|
|
231
250
|
seen.set(workspaceRoot, { identity: 'gone', extras: [], extraStats: {}, digests })
|
|
232
251
|
return digests
|
|
233
252
|
}
|
|
@@ -235,26 +254,30 @@ export function lockfileClaim(options: LockfileClaimOptions): LockfileClaimHooks
|
|
|
235
254
|
if (last !== undefined && last.identity === identity) {
|
|
236
255
|
const now = await statsOf(workspaceRoot, last.extras)
|
|
237
256
|
if (now.every((s, i) => s === last.extraStats[last.extras[i]!])) {
|
|
257
|
+
if (last.unsaved !== undefined && writeLockfileMemo(cacheDir, file, claimant, last.unsaved))
|
|
258
|
+
delete last.unsaved
|
|
238
259
|
return last.digests
|
|
239
260
|
}
|
|
240
261
|
}
|
|
241
262
|
const bytes = await Bun.file(full).bytes()
|
|
242
263
|
const lockHash = xxh3hex(bytes)
|
|
243
|
-
const memoFile = path.join(cacheDir, 'lockfile-claims', `${file}.json`)
|
|
244
264
|
// The extra files the last digest read, and their hashes now: a warm run
|
|
245
265
|
// re-hashes the files the memo names and never parses to learn them.
|
|
246
|
-
const memo =
|
|
266
|
+
const memo = memoises ? readMemo(cacheDir, lockHash) : undefined
|
|
247
267
|
let extras: Record<string, string> = {}
|
|
248
268
|
let extraStats: Record<string, string> = {}
|
|
249
269
|
let importers: ReadonlyMap<string, string> | undefined
|
|
250
270
|
let edges: ReadonlyMap<string, LockfileImporter> = new Map()
|
|
271
|
+
let recorded: ReadonlyMap<string, readonly string[]> | undefined
|
|
272
|
+
let unsaved: LockfileMemo | undefined
|
|
251
273
|
if (memo !== undefined) {
|
|
252
274
|
const now = await hashExtras(workspaceRoot, Object.keys(memo.extras))
|
|
253
275
|
if (Object.entries(memo.extras).every(([rel, h]) => now.hashes[rel] === h)) {
|
|
254
276
|
extras = memo.extras
|
|
255
277
|
extraStats = now.stats
|
|
256
|
-
importers = memo.importers
|
|
257
|
-
edges = memo.edges
|
|
278
|
+
importers = new Map(Object.entries(memo.importers))
|
|
279
|
+
edges = new Map(Object.entries(memo.edges as Record<string, LockfileImporter>))
|
|
280
|
+
recorded = memo.recorded === undefined ? undefined : new Map(Object.entries(memo.recorded))
|
|
258
281
|
}
|
|
259
282
|
}
|
|
260
283
|
if (importers === undefined) {
|
|
@@ -268,9 +291,17 @@ export function lockfileClaim(options: LockfileClaimOptions): LockfileClaimHooks
|
|
|
268
291
|
? new Map<string, string>()
|
|
269
292
|
: digest(text, new Map(Object.entries(extras)))
|
|
270
293
|
edges = options.importers?.(text) ?? new Map()
|
|
271
|
-
|
|
272
|
-
if (
|
|
273
|
-
|
|
294
|
+
recorded = options.recorded?.(text)
|
|
295
|
+
if (memoises) {
|
|
296
|
+
const kept: LockfileMemo = {
|
|
297
|
+
version,
|
|
298
|
+
lock: lockHash,
|
|
299
|
+
extras,
|
|
300
|
+
importers: Object.fromEntries(importers),
|
|
301
|
+
edges: Object.fromEntries(edges),
|
|
302
|
+
...(recorded !== undefined ? { recorded: Object.fromEntries(recorded) } : {}),
|
|
303
|
+
}
|
|
304
|
+
if (!writeLockfileMemo(cacheDir, file, claimant, kept)) unsaved = kept
|
|
274
305
|
}
|
|
275
306
|
}
|
|
276
307
|
const rels = Object.keys(extras)
|
|
@@ -278,8 +309,22 @@ export function lockfileClaim(options: LockfileClaimOptions): LockfileClaimHooks
|
|
|
278
309
|
rels.length === 0
|
|
279
310
|
? lockHash
|
|
280
311
|
: xxh3hex([lockHash, ...rels.map((r) => `${r}\0${extras[r]}`)].join('\0'))
|
|
281
|
-
const digests: Digests = {
|
|
282
|
-
|
|
312
|
+
const digests: Digests = {
|
|
313
|
+
lock,
|
|
314
|
+
importers,
|
|
315
|
+
edges,
|
|
316
|
+
recorded:
|
|
317
|
+
recorded === undefined
|
|
318
|
+
? undefined
|
|
319
|
+
: new Map([...recorded].map(([i, names]) => [i, new Set(names)])),
|
|
320
|
+
}
|
|
321
|
+
seen.set(workspaceRoot, {
|
|
322
|
+
identity,
|
|
323
|
+
extras: rels,
|
|
324
|
+
extraStats,
|
|
325
|
+
digests,
|
|
326
|
+
...(unsaved !== undefined ? { unsaved } : {}),
|
|
327
|
+
})
|
|
283
328
|
return digests
|
|
284
329
|
}
|
|
285
330
|
const loadOnce = (ctx: KeyHookContext): Promise<Digests> => {
|
|
@@ -339,8 +384,7 @@ export function lockfileClaim(options: LockfileClaimOptions): LockfileClaimHooks
|
|
|
339
384
|
// parser: `--affected` asks on every diff, and a cold parse of
|
|
340
385
|
// a large lockfile was most of its selection's cost.
|
|
341
386
|
async extraFiles(_file: string, bytes: Uint8Array, ctx: FingerprintContext) {
|
|
342
|
-
const
|
|
343
|
-
const memo = await readMemo(memoFile, version, claimant, xxh3hex(bytes))
|
|
387
|
+
const memo = memoises ? readMemo(ctx.cacheDir, xxh3hex(bytes)) : undefined
|
|
344
388
|
return memo !== undefined
|
|
345
389
|
? Object.keys(memo.extras)
|
|
346
390
|
: options.extraFiles!(decode(bytes))
|
|
@@ -360,6 +404,10 @@ export function lockfileClaim(options: LockfileClaimOptions): LockfileClaimHooks
|
|
|
360
404
|
const digests = await load(ctx.workspaceRoot, ctx.cacheDir)
|
|
361
405
|
return digests.lock === '' ? undefined : digests.edges
|
|
362
406
|
},
|
|
407
|
+
async recorded(ctx) {
|
|
408
|
+
const digests = await load(ctx.workspaceRoot, ctx.cacheDir)
|
|
409
|
+
return digests.lock === '' ? undefined : digests.recorded
|
|
410
|
+
},
|
|
363
411
|
}
|
|
364
412
|
}
|
|
365
413
|
|
|
@@ -394,59 +442,6 @@ function importerOf(workspaceRoot: string, projectDir: string): string {
|
|
|
394
442
|
return relPosix(workspaceRoot, projectDir) || '.'
|
|
395
443
|
}
|
|
396
444
|
|
|
397
|
-
async function readMemo(
|
|
398
|
-
memoFile: string,
|
|
399
|
-
version: number,
|
|
400
|
-
claimant: string,
|
|
401
|
-
lock: string,
|
|
402
|
-
): Promise<
|
|
403
|
-
| {
|
|
404
|
-
extras: Record<string, string>
|
|
405
|
-
importers: Map<string, string>
|
|
406
|
-
edges: Map<string, LockfileImporter>
|
|
407
|
-
}
|
|
408
|
-
| undefined
|
|
409
|
-
> {
|
|
410
|
-
try {
|
|
411
|
-
const memo = (await Bun.file(memoFile).json()) as Memo
|
|
412
|
-
if (memo.version !== version || memo.claimant !== claimant || memo.lock !== lock) {
|
|
413
|
-
return undefined
|
|
414
|
-
}
|
|
415
|
-
return {
|
|
416
|
-
extras: memo.extras ?? {},
|
|
417
|
-
importers: new Map(Object.entries(memo.importers)),
|
|
418
|
-
edges: new Map(Object.entries(memo.edges ?? {})),
|
|
419
|
-
}
|
|
420
|
-
} catch {
|
|
421
|
-
return undefined
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
async function writeMemo(
|
|
426
|
-
memoFile: string,
|
|
427
|
-
identity: Pick<Memo, 'version' | 'claimant' | 'lock'>,
|
|
428
|
-
extras: Record<string, string>,
|
|
429
|
-
importers: ReadonlyMap<string, string>,
|
|
430
|
-
edges: ReadonlyMap<string, LockfileImporter>,
|
|
431
|
-
): Promise<void> {
|
|
432
|
-
const memo: Memo = {
|
|
433
|
-
...identity,
|
|
434
|
-
extras,
|
|
435
|
-
importers: Object.fromEntries(importers),
|
|
436
|
-
...(edges.size > 0 ? { edges: Object.fromEntries(edges) } : {}),
|
|
437
|
-
}
|
|
438
|
-
// Write-then-rename: a reader never sees a half-written memo, and two
|
|
439
|
-
// concurrent runs each land a whole one.
|
|
440
|
-
const tmp = `${memoFile}.tmp-${process.pid}-${Date.now()}`
|
|
441
|
-
try {
|
|
442
|
-
await Bun.write(tmp, JSON.stringify(memo))
|
|
443
|
-
await rename(tmp, memoFile)
|
|
444
|
-
} catch {
|
|
445
|
-
// The memo is a speed-up, never a requirement: the digests are
|
|
446
|
-
// computed either way, and the next run computes them again.
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
445
|
function decode(bytes: Uint8Array): string {
|
|
451
446
|
return new TextDecoder().decode(bytes)
|
|
452
447
|
}
|
|
@@ -527,9 +527,9 @@ export interface AdmitContext {
|
|
|
527
527
|
}
|
|
528
528
|
|
|
529
529
|
export interface ScheduleHookContext extends BaseContext {
|
|
530
|
-
/** The run's local cache
|
|
530
|
+
/** The run's local cache: `localHistory(localCache)` reads the run history a policy learns from. */
|
|
531
531
|
readonly localCache: Cache
|
|
532
|
-
/** The run's branch (`currentBranch`), the timings a forecast reads first: pass it to `
|
|
532
|
+
/** The run's branch (`currentBranch`), the timings a forecast reads first: pass it to `localHistory`. */
|
|
533
533
|
readonly branch: string | null
|
|
534
534
|
/** The refs the run reads, in order (`CacheContext.refs`): a remote's timings are read by them too. */
|
|
535
535
|
readonly refs: readonly string[]
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
UserError,
|
|
21
21
|
} from '../util/index.js'
|
|
22
22
|
import {
|
|
23
|
+
indexDb,
|
|
23
24
|
Cache,
|
|
24
25
|
type CacheLayer,
|
|
25
26
|
applyCacheFlags,
|
|
@@ -834,7 +835,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
834
835
|
let own = pluginCaches.get(plugin)
|
|
835
836
|
if (own === undefined) {
|
|
836
837
|
own = pluginCache(plugin, {
|
|
837
|
-
db: localCache
|
|
838
|
+
db: indexDb(localCache),
|
|
838
839
|
policy,
|
|
839
840
|
remotes,
|
|
840
841
|
scope: cacheScope,
|