@vzn/vx-history 0.0.0-stage → 0.0.648
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/LICENSE +21 -0
- package/README.md +209 -2
- package/index.ts +4 -0
- package/package.json +47 -4
- package/schemas/history.json +55 -0
- package/src/history-server.ts +354 -0
- package/src/history-view.ts +110 -0
- package/src/index.ts +887 -0
- package/src/timings-file.ts +97 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// The timings file `file` names: what vx-history learned, as one JSON
|
|
2
|
+
// file a CI cache carries between runners. `{ version, tasks: { id: ms } }`,
|
|
3
|
+
// keys sorted so an unchanged history writes the same bytes.
|
|
4
|
+
|
|
5
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
import type { TaskResources } from '@vzn/vx'
|
|
8
|
+
|
|
9
|
+
const VERSION = 1
|
|
10
|
+
|
|
11
|
+
/** The file's durations; none when it is absent, and none, with a warning, when it is not one. */
|
|
12
|
+
export function readTimings(file: string, warn: (m: string) => void): Record<string, number> {
|
|
13
|
+
let text: string
|
|
14
|
+
try {
|
|
15
|
+
text = readFileSync(file, 'utf8')
|
|
16
|
+
} catch (err) {
|
|
17
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return {}
|
|
18
|
+
warn(`[vx] vx-history: timings file ${file} not read: ${(err as Error).message}`)
|
|
19
|
+
return {}
|
|
20
|
+
}
|
|
21
|
+
let parsed: unknown
|
|
22
|
+
try {
|
|
23
|
+
parsed = JSON.parse(text)
|
|
24
|
+
} catch {
|
|
25
|
+
// quiet: not JSON is not a timings file, which the warning below says
|
|
26
|
+
parsed = undefined
|
|
27
|
+
}
|
|
28
|
+
const tasks = (parsed as { version?: unknown; tasks?: unknown } | undefined)?.tasks
|
|
29
|
+
if (
|
|
30
|
+
(parsed as { version?: unknown } | undefined)?.version !== VERSION ||
|
|
31
|
+
tasks === null ||
|
|
32
|
+
typeof tasks !== 'object' ||
|
|
33
|
+
Array.isArray(tasks)
|
|
34
|
+
) {
|
|
35
|
+
warn(
|
|
36
|
+
`[vx] vx-history: timings file ${file} ignored: not { "version": ${VERSION}, "tasks": { id: ms } }`,
|
|
37
|
+
)
|
|
38
|
+
return {}
|
|
39
|
+
}
|
|
40
|
+
const out: Record<string, number> = {}
|
|
41
|
+
for (const [id, ms] of Object.entries(tasks)) {
|
|
42
|
+
// Bounded so no chain of them sums past a number: core refuses a
|
|
43
|
+
// weight that is not finite, and the run failed on a hint.
|
|
44
|
+
if (typeof ms === 'number' && ms >= 0 && ms <= Number.MAX_SAFE_INTEGER) out[id] = ms
|
|
45
|
+
}
|
|
46
|
+
return out
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The carried durations with this machine's p50s over them, written whole:
|
|
51
|
+
* a task this run did not see keeps its carried time. Through a temp and a
|
|
52
|
+
* rename, so a reader never sees half a file. Its directory is made: a CI
|
|
53
|
+
* cache that missed restores none.
|
|
54
|
+
*/
|
|
55
|
+
export function writeTimings(
|
|
56
|
+
file: string,
|
|
57
|
+
carried: Readonly<Record<string, number>>,
|
|
58
|
+
table: ReadonlyMap<string, TaskResources>,
|
|
59
|
+
): void {
|
|
60
|
+
const merged: Record<string, number> = { ...carried }
|
|
61
|
+
for (const [id, h] of table) {
|
|
62
|
+
if (h.p50DurationMs !== undefined) merged[id] = Math.round(h.p50DurationMs)
|
|
63
|
+
}
|
|
64
|
+
const tasks: Record<string, number> = {}
|
|
65
|
+
for (const id of Object.keys(merged).sort()) tasks[id] = merged[id]!
|
|
66
|
+
const tmp = `${file}.${process.pid}.tmp`
|
|
67
|
+
mkdirSync(path.dirname(file), { recursive: true })
|
|
68
|
+
try {
|
|
69
|
+
writeFileSync(tmp, `${JSON.stringify({ version: VERSION, tasks }, null, 2)}\n`)
|
|
70
|
+
renameSync(tmp, file)
|
|
71
|
+
} catch (err) {
|
|
72
|
+
rmSync(tmp, { force: true })
|
|
73
|
+
throw err
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Says so when git does not ignore the file it rewrote: a task whose
|
|
79
|
+
* inputs cover it (a root project's `**`) keyed each run's new timings,
|
|
80
|
+
* and missed the run after every write. Asked after the write, off the run.
|
|
81
|
+
*/
|
|
82
|
+
export function warnUnignored(file: string, root: string, warn: (m: string) => void): void {
|
|
83
|
+
const rel = path.relative(root, file)
|
|
84
|
+
// `..x.json` is a name inside the root, not a step out of it.
|
|
85
|
+
if (rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) return
|
|
86
|
+
const p = Bun.spawnSync({
|
|
87
|
+
cmd: ['git', 'check-ignore', '-q', '--', rel],
|
|
88
|
+
cwd: root,
|
|
89
|
+
stdout: 'ignore',
|
|
90
|
+
stderr: 'ignore',
|
|
91
|
+
})
|
|
92
|
+
// 0: ignored; 128: no repository or git refused, which says nothing of it.
|
|
93
|
+
if (p.exitCode !== 1) return
|
|
94
|
+
warn(
|
|
95
|
+
`[vx] vx-history: timings file ${rel} is not git-ignored, so a task whose inputs cover it re-keys on every rewrite — add it to .gitignore`,
|
|
96
|
+
)
|
|
97
|
+
}
|