@decohere/ci-tools 0.0.1-dev.112 → 0.0.1-dev.113
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 +2 -2
- package/src/audit/upsert-issue.mjs +5 -39
- package/src/bench/bench.mjs +5 -5
- package/src/bench/file-regressions.mjs +15 -14
- package/src/bench/history.mjs +64 -41
- package/src/bench/nightly.mjs +118 -0
- package/src/bench/regression-issue.mjs +15 -22
- package/src/github/credential-nudge.mjs +22 -97
- package/src/github/oidc-bootstrap-nudge.mjs +24 -182
- package/src/lib/github-issue.mjs +73 -0
- package/src/pages/bench-page.mjs +13 -3
- package/tests/bench/history.test.mjs +58 -30
- package/tests/bench/regression-issue.test.mjs +12 -20
- package/tests/github/credential-nudge.test.mjs +53 -2
- package/tests/github/oidc-bootstrap-nudge.test.mjs +14 -29
- package/tests/lib/github-issue.test.mjs +74 -0
- package/tests/pages/bench-page.test.mjs +17 -0
- package/src/audit/upsert-clickup.mjs +0 -117
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decohere/ci-tools",
|
|
3
|
-
"version": "0.0.1-dev.
|
|
3
|
+
"version": "0.0.1-dev.113",
|
|
4
4
|
"description": "CI utility scripts for badge generation, coverage reporting, pages artifact assembly, and GitHub API tooling",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"exports": {
|
|
17
17
|
"./lib/svg": "./src/lib/svg.mjs",
|
|
18
18
|
"./lib/lcov": "./src/lib/lcov.mjs",
|
|
19
|
+
"./lib/github-issue": "./src/lib/github-issue.mjs",
|
|
19
20
|
"./coverage/badge": "./src/coverage/badge.mjs",
|
|
20
21
|
"./coverage/report": "./src/coverage/report.mjs",
|
|
21
22
|
"./badges/ci-status": "./src/badges/ci-status.mjs",
|
|
@@ -25,7 +26,6 @@
|
|
|
25
26
|
"./audit/summarize": "./src/audit/summarize.mjs",
|
|
26
27
|
"./audit/enforce": "./src/audit/enforce.mjs",
|
|
27
28
|
"./audit/upsert-issue": "./src/audit/upsert-issue.mjs",
|
|
28
|
-
"./audit/upsert-clickup": "./src/audit/upsert-clickup.mjs",
|
|
29
29
|
"./bench/bench": "./src/bench/bench.mjs",
|
|
30
30
|
"./bench/trace": "./src/bench/trace.mjs",
|
|
31
31
|
"./changeset/aggregate-changelog": "./src/changeset/aggregate-changelog.mjs",
|
|
@@ -1,20 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { upsertIssue } from "../lib/github-issue.mjs";
|
|
5
3
|
import { formatMarkdown, parseAudit } from "./format.mjs";
|
|
6
4
|
|
|
7
5
|
const auditFilePath = process.argv[2] ?? "npm-audit.json";
|
|
8
6
|
const ISSUE_TITLE = "security(ci): npm audit vulnerabilities detected";
|
|
9
7
|
|
|
10
|
-
function runGh(args) {
|
|
11
|
-
const result = spawnSync("gh", args, { encoding: "utf8", env: process.env });
|
|
12
|
-
if (result.status !== 0) {
|
|
13
|
-
throw new Error(result.stderr || result.stdout || `gh command failed: ${args.join(" ")}`);
|
|
14
|
-
}
|
|
15
|
-
return result.stdout;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
8
|
function runUrl() {
|
|
19
9
|
if (
|
|
20
10
|
process.env.GITHUB_SERVER_URL &&
|
|
@@ -26,21 +16,6 @@ function runUrl() {
|
|
|
26
16
|
return "(run URL unavailable)";
|
|
27
17
|
}
|
|
28
18
|
|
|
29
|
-
function findExistingIssueNumber() {
|
|
30
|
-
const output = runGh([
|
|
31
|
-
"issue",
|
|
32
|
-
"list",
|
|
33
|
-
"--state",
|
|
34
|
-
"open",
|
|
35
|
-
"--limit",
|
|
36
|
-
"200",
|
|
37
|
-
"--json",
|
|
38
|
-
"number,title",
|
|
39
|
-
]);
|
|
40
|
-
const issues = JSON.parse(output);
|
|
41
|
-
return issues.find((issue) => issue.title === ISSUE_TITLE)?.number ?? null;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
19
|
if (!existsSync(auditFilePath)) {
|
|
45
20
|
console.log(`No audit file found at ${auditFilePath}; skipping issue update.`);
|
|
46
21
|
process.exit(0);
|
|
@@ -59,17 +34,8 @@ const body = [
|
|
|
59
34
|
`- Commit: \`${sha}\``,
|
|
60
35
|
`- Workflow run: ${runUrl()}`,
|
|
61
36
|
"",
|
|
62
|
-
"This issue is managed by CI and
|
|
37
|
+
"This issue is managed by CI: main-branch runs and the nightly audit update it in place while vulnerabilities remain.",
|
|
63
38
|
].join("\n");
|
|
64
39
|
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const existingIssueNumber = findExistingIssueNumber();
|
|
69
|
-
if (existingIssueNumber) {
|
|
70
|
-
runGh(["issue", "comment", String(existingIssueNumber), "--body-file", tempFile]);
|
|
71
|
-
console.log(`Updated existing issue #${existingIssueNumber}.`);
|
|
72
|
-
} else {
|
|
73
|
-
runGh(["issue", "create", "--title", ISSUE_TITLE, "--body-file", tempFile]);
|
|
74
|
-
console.log("Created new npm audit tracking issue.");
|
|
75
|
-
}
|
|
40
|
+
const result = upsertIssue({ title: ISSUE_TITLE, body });
|
|
41
|
+
console.log(`${result.created ? "Opened" : "Updated"} issue #${result.number} ${result.url}`);
|
package/src/bench/bench.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* (see history.mjs). The history lives in projects/ci-tools/dist/ci/bench/bench-history.json;
|
|
13
13
|
* the nightly bench workflow restores it from Pages, saves, and publishes the charts.
|
|
14
14
|
*
|
|
15
|
-
* Environment: BENCH_ENV names the machine class (CI sets gh-actions); BENCH_SAMPLES (default
|
|
15
|
+
* Environment: BENCH_HISTORY_FILE overrides where `save` writes; BENCH_ENV names the machine class (CI sets gh-actions); BENCH_SAMPLES (default
|
|
16
16
|
* 10) is the sample count for every benchmark; BENCH_LIBS and BENCH_FILES size the flux workspace.
|
|
17
17
|
*/
|
|
18
18
|
|
|
@@ -41,10 +41,10 @@ import {
|
|
|
41
41
|
} from "./history.mjs";
|
|
42
42
|
|
|
43
43
|
const WORKSPACE_ROOT = process.cwd();
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
);
|
|
44
|
+
// BENCH_HISTORY_FILE lets the nightly job save each comparison round to its own file (#667).
|
|
45
|
+
const HISTORY_FILE =
|
|
46
|
+
process.env.BENCH_HISTORY_FILE ??
|
|
47
|
+
join(WORKSPACE_ROOT, "projects/ci-tools/dist/ci/bench/bench-history.json");
|
|
48
48
|
|
|
49
49
|
/** Runs a TypeScript benchmark from source, the way the repo runs its CLIs. */
|
|
50
50
|
function fluxSuite(file) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* file-regressions.mjs — after
|
|
5
|
-
*
|
|
4
|
+
* file-regressions.mjs — after nightly.mjs, open an issue for each benchmark that got slower
|
|
5
|
+
* (regressions.json: head slower than base in every round on one runner), or update the open
|
|
6
6
|
* one. Needs `gh` and GH_TOKEN with issues: write.
|
|
7
7
|
*
|
|
8
8
|
* node projects/ci-tools/src/bench/file-regressions.mjs # file issues
|
|
@@ -11,11 +11,10 @@
|
|
|
11
11
|
|
|
12
12
|
import { spawnSync } from "node:child_process";
|
|
13
13
|
import { readFileSync } from "node:fs";
|
|
14
|
-
import
|
|
15
|
-
import { contextId, findRegressions, getContext, normalize, REGRESSION } from "./history.mjs";
|
|
14
|
+
import { REGRESSION } from "./history.mjs";
|
|
16
15
|
import { issueBody, issueTitle } from "./regression-issue.mjs";
|
|
17
16
|
|
|
18
|
-
const
|
|
17
|
+
const REGRESSIONS_FILE = "projects/ci-tools/dist/ci/bench/regressions.json";
|
|
19
18
|
const dryRun = process.argv.includes("--dry-run");
|
|
20
19
|
|
|
21
20
|
function gh(args, input) {
|
|
@@ -33,19 +32,19 @@ const runUrl = process.env.GITHUB_RUN_ID
|
|
|
33
32
|
? `${repoUrl}/actions/runs/${process.env.GITHUB_RUN_ID}`
|
|
34
33
|
: "(local run)";
|
|
35
34
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
const { base, head, context, regressions } = JSON.parse(
|
|
36
|
+
readFileSync(REGRESSIONS_FILE, "utf8"),
|
|
37
|
+
);
|
|
38
|
+
if (base == null) {
|
|
39
|
+
console.log("Nothing was compared tonight (no earlier nightly, or no new commit).");
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
42
|
if (regressions.length === 0) {
|
|
43
|
-
console.log(
|
|
44
|
-
`No benchmark regressions on this machine (${runs.length} runs in its history).`,
|
|
45
|
-
);
|
|
43
|
+
console.log(`No benchmark slower on ${head.slice(0, 7)} than on ${base.slice(0, 7)}.`);
|
|
46
44
|
process.exit(0);
|
|
47
45
|
}
|
|
48
46
|
|
|
47
|
+
const machine = `${context.benchEnv}, ${context.cpu.model} × ${context.cpu.count}`;
|
|
49
48
|
const open = dryRun
|
|
50
49
|
? []
|
|
51
50
|
: JSON.parse(
|
|
@@ -59,6 +58,8 @@ for (const regression of regressions) {
|
|
|
59
58
|
runUrl,
|
|
60
59
|
machine,
|
|
61
60
|
threshold: REGRESSION.threshold,
|
|
61
|
+
base,
|
|
62
|
+
head,
|
|
62
63
|
});
|
|
63
64
|
if (dryRun) {
|
|
64
65
|
console.log(`# ${title}\n\n${body}\n`);
|
package/src/bench/history.mjs
CHANGED
|
@@ -118,14 +118,42 @@ export function previousRun(history, id) {
|
|
|
118
118
|
return history.contexts[id]?.runs[0];
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
/**
|
|
121
|
+
/** Machine types with no run for this long are dropped: CI runner types come and go (#667). */
|
|
122
|
+
export const STALE_AFTER_DAYS = 30;
|
|
123
|
+
|
|
124
|
+
/** The history without machine types whose newest run is older than `days` before `now`. */
|
|
125
|
+
export function pruneStale(history, now, days = STALE_AFTER_DAYS) {
|
|
126
|
+
const cutoff = now.getTime() - days * 24 * 60 * 60 * 1000;
|
|
127
|
+
const contexts = Object.fromEntries(
|
|
128
|
+
Object.entries(history.contexts).filter(
|
|
129
|
+
([, entry]) => entry.runs[0] != null && new Date(entry.runs[0].date).getTime() >= cutoff,
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
return { version: HISTORY_VERSION, contexts };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A new history with `run` first in its context, trimmed to `max` runs, and machine types
|
|
137
|
+
* without a recent run dropped. Does not mutate.
|
|
138
|
+
*/
|
|
122
139
|
export function appendRun(history, ctx, run, max = MAX_RUNS) {
|
|
123
140
|
const id = contextId(ctx);
|
|
124
141
|
const runs = [run, ...(history.contexts[id]?.runs ?? [])].slice(0, max);
|
|
125
|
-
return
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
142
|
+
return pruneStale(
|
|
143
|
+
{
|
|
144
|
+
version: HISTORY_VERSION,
|
|
145
|
+
contexts: { ...history.contexts, [id]: { context: ctx, runs } },
|
|
146
|
+
},
|
|
147
|
+
new Date(run.date),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The newest run on any machine: the commit the previous nightly measured. */
|
|
152
|
+
export function latestRun(history) {
|
|
153
|
+
return Object.values(history.contexts)
|
|
154
|
+
.map((entry) => entry.runs[0])
|
|
155
|
+
.filter((run) => run != null)
|
|
156
|
+
.sort((a, b) => new Date(b.date) - new Date(a.date))[0];
|
|
129
157
|
}
|
|
130
158
|
|
|
131
159
|
/** One row per current result, with the change in median against the previous run. */
|
|
@@ -177,48 +205,43 @@ export function formatComparison(rows) {
|
|
|
177
205
|
}
|
|
178
206
|
|
|
179
207
|
/**
|
|
180
|
-
* When a benchmark counts as slower (#
|
|
181
|
-
*
|
|
182
|
-
*
|
|
208
|
+
* When a benchmark counts as slower (#667): the nightly job benchmarks tonight's commit (head)
|
|
209
|
+
* and the one the previous nightly measured (base) on the same runner, alternating head, base,
|
|
210
|
+
* head, base. A benchmark regressed when head's median beats base's by more than `threshold`
|
|
211
|
+
* in every round, so one noisy round files nothing.
|
|
183
212
|
*/
|
|
184
|
-
export const REGRESSION = { threshold: 0.2,
|
|
213
|
+
export const REGRESSION = { threshold: 0.2, rounds: 2 };
|
|
185
214
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const recent = runs.slice(0, options.confirmRuns);
|
|
195
|
-
const before = runs.slice(options.confirmRuns, options.confirmRuns + options.baselineRuns);
|
|
196
|
-
if (recent.length < options.confirmRuns) return [];
|
|
197
|
-
const medianIn = (run, id) => run.results.find((result) => result.id === id)?.median;
|
|
215
|
+
/**
|
|
216
|
+
* The benchmarks slower on head than on base. `headRounds` and `baseRounds` are lists of result
|
|
217
|
+
* lists, one per round, run alternately on one machine. Benchmarks either side lacks are skipped.
|
|
218
|
+
*/
|
|
219
|
+
export function compareRounds(headRounds, baseRounds, options = REGRESSION) {
|
|
220
|
+
const rounds = Math.min(headRounds.length, baseRounds.length);
|
|
221
|
+
if (rounds < options.rounds) return [];
|
|
222
|
+
const medianIn = (results, id) => results.find((result) => result.id === id)?.median;
|
|
198
223
|
const regressions = [];
|
|
199
|
-
for (const
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
const slower = recentValues.every(
|
|
207
|
-
(value) => Number.isFinite(value) && value > baseline * (1 + options.threshold),
|
|
224
|
+
for (const head of headRounds[0]) {
|
|
225
|
+
const pairs = Array.from({ length: rounds }, (_, round) => ({
|
|
226
|
+
head: medianIn(headRounds[round], head.id),
|
|
227
|
+
base: medianIn(baseRounds[round], head.id),
|
|
228
|
+
}));
|
|
229
|
+
const comparable = pairs.every(
|
|
230
|
+
(pair) => Number.isFinite(pair.head) && Number.isFinite(pair.base) && pair.base > 0,
|
|
208
231
|
);
|
|
232
|
+
if (!comparable) continue;
|
|
233
|
+
const slower = pairs.every((pair) => pair.head > pair.base * (1 + options.threshold));
|
|
209
234
|
if (!slower) continue;
|
|
235
|
+
const mean = (values) => values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
236
|
+
const headMs = mean(pairs.map((pair) => pair.head));
|
|
237
|
+
const baseMs = mean(pairs.map((pair) => pair.base));
|
|
210
238
|
regressions.push({
|
|
211
|
-
id:
|
|
212
|
-
label:
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
sha: run.sha,
|
|
218
|
-
median: recentValues[index],
|
|
219
|
-
})),
|
|
220
|
-
// The change landed between the newest baseline run and the oldest slow one.
|
|
221
|
-
lastGood: before[0] == null ? null : { date: before[0].date, sha: before[0].sha },
|
|
239
|
+
id: head.id,
|
|
240
|
+
label: head.label,
|
|
241
|
+
base: baseMs,
|
|
242
|
+
head: headMs,
|
|
243
|
+
percent: (headMs / baseMs - 1) * 100,
|
|
244
|
+
rounds: pairs,
|
|
222
245
|
});
|
|
223
246
|
}
|
|
224
247
|
return regressions;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* nightly.mjs — the nightly benchmark run (#667). Benchmarks tonight's commit (head) and the
|
|
5
|
+
* commit the previous nightly measured (base) on this runner, alternating head, base, head,
|
|
6
|
+
* base, so the comparison never mixes CPU models. Then:
|
|
7
|
+
*
|
|
8
|
+
* - appends head's first round to the history (projects/ci-tools/dist/ci/bench/), for the charts;
|
|
9
|
+
* - writes regressions.json next to it: the benchmarks slower on head in every round
|
|
10
|
+
* (history.mjs `compareRounds`), which file-regressions.mjs turns into issues.
|
|
11
|
+
*
|
|
12
|
+
* Base runs from its own checkout (a git worktree) with its own dependencies and its own
|
|
13
|
+
* bench.mjs, so each commit is measured with its own code. With no earlier nightly, or no new
|
|
14
|
+
* commit since, it benchmarks head once and compares nothing.
|
|
15
|
+
*
|
|
16
|
+
* node projects/ci-tools/src/bench/nightly.mjs (from the repo root; needs the full git history)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { execFileSync } from "node:child_process";
|
|
20
|
+
import {
|
|
21
|
+
existsSync,
|
|
22
|
+
mkdirSync,
|
|
23
|
+
mkdtempSync,
|
|
24
|
+
readFileSync,
|
|
25
|
+
rmSync,
|
|
26
|
+
writeFileSync,
|
|
27
|
+
} from "node:fs";
|
|
28
|
+
import os from "node:os";
|
|
29
|
+
import { dirname, join } from "node:path";
|
|
30
|
+
import {
|
|
31
|
+
appendRun,
|
|
32
|
+
compareRounds,
|
|
33
|
+
getContext,
|
|
34
|
+
latestRun,
|
|
35
|
+
normalize,
|
|
36
|
+
REGRESSION,
|
|
37
|
+
} from "./history.mjs";
|
|
38
|
+
|
|
39
|
+
const ROOT = process.cwd();
|
|
40
|
+
const DIST = join(ROOT, "projects/ci-tools/dist/ci/bench");
|
|
41
|
+
const HISTORY_FILE = join(DIST, "bench-history.json");
|
|
42
|
+
const REGRESSIONS_FILE = join(DIST, "regressions.json");
|
|
43
|
+
const BENCH = "projects/ci-tools/src/bench/bench.mjs";
|
|
44
|
+
|
|
45
|
+
function run(command, args, cwd, env = {}) {
|
|
46
|
+
execFileSync(command, args, {
|
|
47
|
+
cwd,
|
|
48
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
49
|
+
env: { ...process.env, ...env },
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function git(...args) {
|
|
54
|
+
return execFileSync("git", args, { cwd: ROOT, encoding: "utf8" }).trim();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Runs one round of every suite in `cwd` and returns its results, saved to its own history. */
|
|
58
|
+
function benchRound(cwd, label, round, workDir) {
|
|
59
|
+
process.stderr.write(`\n══ ${label}, round ${round + 1}\n`);
|
|
60
|
+
const historyFile = join(workDir, `${label}-${round}.json`);
|
|
61
|
+
run("node", [BENCH, "save"], cwd, { BENCH_HISTORY_FILE: historyFile });
|
|
62
|
+
// Base's bench.mjs may predate BENCH_HISTORY_FILE and save to its checkout's default path.
|
|
63
|
+
const file = existsSync(historyFile)
|
|
64
|
+
? historyFile
|
|
65
|
+
: join(cwd, "projects/ci-tools/dist/ci/bench/bench-history.json");
|
|
66
|
+
const saved = normalize(JSON.parse(readFileSync(file, "utf8")));
|
|
67
|
+
const newest = latestRun(saved);
|
|
68
|
+
if (newest == null) throw new Error(`${label} round ${round + 1} saved no run`);
|
|
69
|
+
return newest.results;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const history = normalize(
|
|
73
|
+
existsSync(HISTORY_FILE) ? JSON.parse(readFileSync(HISTORY_FILE, "utf8")) : null,
|
|
74
|
+
);
|
|
75
|
+
const head = git("rev-parse", "HEAD");
|
|
76
|
+
const base = latestRun(history)?.sha ?? null;
|
|
77
|
+
const workDir = mkdtempSync(join(os.tmpdir(), "decohere-nightly-"));
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const headRounds = [];
|
|
81
|
+
const baseRounds = [];
|
|
82
|
+
const compareWith = base != null && base !== head ? base : null;
|
|
83
|
+
if (compareWith == null) {
|
|
84
|
+
process.stderr.write(
|
|
85
|
+
base == null
|
|
86
|
+
? "No earlier nightly run: benchmarking head only.\n"
|
|
87
|
+
: `No new commit since the last nightly (${head.slice(0, 7)}): benchmarking head only.\n`,
|
|
88
|
+
);
|
|
89
|
+
headRounds.push(benchRound(ROOT, "head", 0, workDir));
|
|
90
|
+
} else {
|
|
91
|
+
const baseDir = join(workDir, "base");
|
|
92
|
+
run("git", ["worktree", "add", "--detach", baseDir, compareWith], ROOT);
|
|
93
|
+
run("pnpm", ["install", "--frozen-lockfile", "--prefer-offline"], baseDir);
|
|
94
|
+
for (let round = 0; round < REGRESSION.rounds; round++) {
|
|
95
|
+
headRounds.push(benchRound(ROOT, "head", round, workDir));
|
|
96
|
+
baseRounds.push(benchRound(baseDir, "base", round, workDir));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const ctx = getContext(os, process.env, process.version);
|
|
101
|
+
const next = appendRun(history, ctx, { date: ctx.date, sha: head, results: headRounds[0] });
|
|
102
|
+
mkdirSync(dirname(HISTORY_FILE), { recursive: true });
|
|
103
|
+
writeFileSync(HISTORY_FILE, `${JSON.stringify(next, null, 2)}\n`);
|
|
104
|
+
|
|
105
|
+
const regressions = compareWith == null ? [] : compareRounds(headRounds, baseRounds);
|
|
106
|
+
writeFileSync(
|
|
107
|
+
REGRESSIONS_FILE,
|
|
108
|
+
`${JSON.stringify({ base: compareWith, head, context: ctx, regressions }, null, 2)}\n`,
|
|
109
|
+
);
|
|
110
|
+
process.stderr.write(
|
|
111
|
+
`\nSaved head's run. ${compareWith == null ? "Nothing compared." : `Compared with ${compareWith.slice(0, 7)}: ${regressions.length} benchmark(s) slower in every round.`}\n`,
|
|
112
|
+
);
|
|
113
|
+
} finally {
|
|
114
|
+
if (existsSync(join(workDir, "base"))) {
|
|
115
|
+
run("git", ["worktree", "remove", "--force", join(workDir, "base")], ROOT);
|
|
116
|
+
}
|
|
117
|
+
rmSync(workDir, { recursive: true, force: true });
|
|
118
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The GitHub issue for a benchmark that got slower (#660): one open issue per benchmark,
|
|
2
|
+
* The GitHub issue for a benchmark that got slower (#660, #667): one open issue per benchmark,
|
|
3
3
|
* found again by its exact title and updated in place while the regression lasts.
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -13,38 +13,31 @@ function ms(value) {
|
|
|
13
13
|
return `${value.toFixed(1)} ms`;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
function
|
|
17
|
-
return sha
|
|
16
|
+
function short(sha) {
|
|
17
|
+
return sha.slice(0, 7);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export function issueBody(regression, { repoUrl, runUrl, machine, threshold }) {
|
|
21
|
-
const
|
|
22
|
-
const oldestSlow = regression.recent.at(-1);
|
|
23
|
-
const range =
|
|
24
|
-
regression.lastGood?.sha != null && newest.sha != null
|
|
25
|
-
? `[${regression.lastGood.sha.slice(0, 7)}...${newest.sha.slice(0, 7)}](${repoUrl}/compare/${regression.lastGood.sha}...${newest.sha})`
|
|
26
|
-
: "unknown (no baseline commit recorded)";
|
|
20
|
+
export function issueBody(regression, { repoUrl, runUrl, machine, threshold, base, head }) {
|
|
21
|
+
const range = `[${short(base)}...${short(head)}](${repoUrl}/compare/${base}...${head})`;
|
|
27
22
|
return [
|
|
28
23
|
"## Problem",
|
|
29
24
|
"",
|
|
30
|
-
`The nightly benchmark **${regression.label}** (\`${regression.id}\`)
|
|
25
|
+
`The nightly benchmark **${regression.label}** (\`${regression.id}\`) is ${regression.percent.toFixed(0)}% slower on [\`${short(head)}\`](${repoUrl}/commit/${head}) than on [\`${short(base)}\`](${repoUrl}/commit/${base}), the commit the previous nightly measured. Both ran on the same runner, alternately, and head was more than ${Math.round(threshold * 100)}% slower in every round.`,
|
|
31
26
|
"",
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
"| Round | Base | Head |",
|
|
28
|
+
"|---|---|---|",
|
|
29
|
+
...regression.rounds.map(
|
|
30
|
+
(round, index) => `| ${index + 1} | ${ms(round.base)} | ${ms(round.head)} |`,
|
|
36
31
|
),
|
|
37
|
-
`- Machine: ${machine}`,
|
|
38
32
|
"",
|
|
39
|
-
|
|
33
|
+
`- Machine: ${machine}`,
|
|
34
|
+
`- Commits: ${range}`,
|
|
40
35
|
"",
|
|
41
36
|
"## What to do",
|
|
42
37
|
"",
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"2. Reproduce locally: check out the last normal commit, `pnpm bench:save`, then the newest commit, `pnpm bench`, and read the comparison.",
|
|
47
|
-
"3. Fix the cause, or if the slowdown is intended, say why here and close the issue.",
|
|
38
|
+
`1. Look through the commits in ${range} for the change.`,
|
|
39
|
+
`2. Reproduce locally: check out \`${short(base)}\`, run \`pnpm bench:save\`, then check out \`${short(head)}\` and run \`pnpm bench\` to compare.`,
|
|
40
|
+
`3. Fix the cause, or if the slowdown is intended, say why here and close the issue. Trend: [benchmark page](${PAGES_URL}).`,
|
|
48
41
|
"",
|
|
49
42
|
`Updated by the nightly bench workflow: ${runUrl}`,
|
|
50
43
|
].join("\n");
|