perf(ci): pack each test tier from its own timing history (#5799)

* perf(ci): pack each test tier from its own timing history

Fast shards were still ordered with the latest full-suite timing artifact,
which stayed stale across compiled-CLI session reuse. Publish and download
test-timings-fast and test-timings-full separately, and keep the legacy
artifact as a fallback until a tier has its own history.

* perf(ci): drop the shared timing-history fallback

Shard lookup now reads only the artifact for the current tier. A missing
artifact leaves history unset instead of packing from the old test-timings
file.

* test(ci): look up fast history when only full timings exist

The empty-history case now requests the fast tier against a full-tier
artifact and checks that the fast lookup ran and returned nothing. A
missing tier stays a separate test that does not query.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
morlutoandCursor Agent authored and GitHub committed 2026-10-21 14:58:17 +08:00
1 parent 6b7d429491
commit ea24207b57
5 files changed
+300 -91

No files matched your search

+18 -4
View File
@@ -409,7 +409,9 @@ jobs:
with:
save: ${{ github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
- run: npm run test:prepare
- name: Find available trusted full-suite timing history
# Fast and full shards measure different file costs. Download only the
# matching history so one tier cannot pack the other with its weights.
- name: Find available trusted timing history
id: history
continue-on-error: true
env:
@@ -417,13 +419,14 @@ jobs:
CI_REPOSITORY: ${{ github.repository }}
CI_REPOSITORY_ID: ${{ github.repository_id }}
CI_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
CI_TIMING_TIER: ${{ needs.changes.outputs.tier }}
run: node scripts/ci/test-history.mjs
- name: Download previous timing report
if: steps.history.outputs.run-id != ''
continue-on-error: true
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: test-timings
name: ${{ steps.history.outputs.artifact-name }}
run-id: ${{ steps.history.outputs.run-id }}
github-token: ${{ github.token }}
path: .cache/ci-test-history
@@ -555,11 +558,22 @@ jobs:
include-hidden-files: true
if-no-files-found: error
retention-days: 30
# Default-branch runs only. Pull-request artifacts share this name and
# the lookup reads one page, so publishing them would hide main.
- name: Publish successful timing history
if: needs.changes.outputs.code == 'true' && needs.changes.outputs.tier == 'full' && success() && needs.test-shard.result == 'success'
if: >-
success() &&
needs.changes.outputs.code == 'true' &&
needs.test-shard.result == 'success' &&
(
needs.changes.outputs.tier == 'full' ||
(needs.changes.outputs.tier == 'fast' && needs.test-source.result == 'success')
) &&
github.ref == format('refs/heads/{0}', github.event.repository.default_branch) &&
(github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: test-timings
name: test-timings-${{ needs.changes.outputs.tier }}
path: .cache/ci-test-history/test-results.json
include-hidden-files: true
if-no-files-found: error
+25 -8
View File
@@ -521,9 +521,15 @@ reports even when a shard fails, uploads available diagnostics, and writes the
slowest files to the workflow summary. Partial reports are marked diagnostic
only. Failed, cancelled, or missing selected shards still fail the required gate,
and aggregate coverage thresholds remain enforced in full qualification.
Only successful complete full-tier merges publish the small `test-timings` artifact, with
checkout-relative paths; the build job downloads that artifact for the sequencer
instead of the coverage/JUnit bundle. Timing weights come from Vitest module
Only a successful complete merge on the default branch publishes shard history,
and only from a push, schedule, or manual run. Fast runs publish
`test-timings-fast`. Full runs publish `test-timings-full`. Pull requests do not
publish it: the lookup reads one unpaginated artifact page, and pull-request
artifacts would crowd out the default branch. The build job downloads the
artifact that matches its tier, so fast shards use durations measured without
coverage and full shards keep coverage-weighted durations. A tier with no
qualifying artifact leaves history unset. The download is that small
checkout-relative timing file, not the coverage/JUnit bundle. Timing weights come from Vitest module
diagnostics and include tests and hooks, worker preparation, environment,
imports, and setup. Import timings already include transform waits, which are
not added again. The weights describe file costs under the measured worker and
@@ -1390,11 +1396,22 @@ catalog, and managed-conformance metadata. Each shard still installs the locked
dependencies and runs its assigned tests; full qualification enables coverage. It no longer
regenerates the same metadata independently.
The build job also finds an available `test-timings` artifact from a successful
trusted default-branch scheduled or manual qualification and includes that same
report in each shard's snapshot. The bounded lookup verifies the artifact's
producer; newer schedules that reuse qualification without producing timings do
not hide the last full run's artifact.
The build job finds the newest trusted timing artifact for its tier
(`test-timings-fast` or `test-timings-full`) from a successful default-branch
push, schedule, or manual CI run, and includes that same report in each shard's
snapshot. The bounded lookup verifies the producer. Pull-request runs and other
branches are not producers. A tier with no qualifying artifact leaves history
unset, and Vitest uses its own assignment. A later schedule that reuses
qualification without uploading timings leaves the previous artifact in place.
On 2026-10-21 the newest full history was run
`39545047294`, recorded before compiled-CLI session reuse. Fast run
`39552964873` packed its eight built shards from that history, and those shard
jobs executed for 319–591 seconds. Replaying that run's file durations through
the sequencer's two-worker schedule balances every shard at about 404 seconds
of file time. The same replay with the stale history weights leaves the slowest
shard near 547 seconds and the fastest near 293. That spread is file-schedule
time, not runner queue, which is why each tier keeps its own history.
CI assigns discovered files by historical duration, with a deterministic
fallback weight for new files, rather than dividing only by file count.
Each file keeps its full duration on one worker. Extra workers overlap other
+91 -64
View File
@@ -4,21 +4,38 @@ import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { normalizeWorkflowRun } from "./workflow-run.mjs";
const ARTIFACT_NAME = "test-timings";
const TIER_EVENTS = ["push", "schedule", "workflow_dispatch"];
const TIER_PROFILES = {
fast: { artifactName: "test-timings-fast", events: TIER_EVENTS },
full: { artifactName: "test-timings-full", events: TIER_EVENTS },
};
const MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
const MAX_CANDIDATES = 5;
const EMPTY_SELECTION = { runId: "", artifactName: "" };
const positiveId = (value) =>
(typeof value === "string" || typeof value === "number") &&
/^[1-9]\d*$/.test(String(value)) &&
Number.isSafeInteger(Number(value));
function findTimingRun({ repository, branch, repositoryId, now = Date.now() }) {
function findTimingRun({
repository,
branch,
repositoryId,
tier,
now = Date.now(),
}) {
if (
!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository ?? "") ||
!branch ||
!positiveId(repositoryId)
)
return "";
return EMPTY_SELECTION;
const profile = Object.hasOwn(TIER_PROFILES, tier)
? TIER_PROFILES[tier]
: null;
// Reused and other tiers do not publish shard history. Missing history uses
// Vitest's own assignment rather than another tier's artifact.
if (!profile) return EMPTY_SELECTION;
const deadline = Date.now() + 20_000;
const api = (...arguments_) => {
const remaining = deadline - Date.now();
@@ -33,83 +50,93 @@ function findTimingRun({ repository, branch, repositoryId, now = Date.now() }) {
}),
);
};
const { artifacts } = api(
`repos/${repository}/actions/artifacts`,
"-f",
`name=${ARTIFACT_NAME}`,
"-F",
"per_page=100",
);
if (!Array.isArray(artifacts))
throw new Error("Missing timing artifact inventory");
const candidates = artifacts
.filter((artifact) => {
const lineage = artifact?.workflow_run;
const age = now - Date.parse(artifact?.created_at);
return (
artifact?.name === ARTIFACT_NAME &&
artifact?.expired === false &&
typeof artifact?.created_at === "string" &&
Number.isFinite(age) &&
age >= 0 &&
age < MAX_AGE_MS &&
positiveId(lineage?.id) &&
positiveId(lineage?.repository_id) &&
positiveId(lineage?.head_repository_id) &&
lineage.repository_id === lineage.head_repository_id &&
lineage.repository_id === Number(repositoryId) &&
lineage.head_branch === branch
);
})
.sort(
(left, right) =>
Date.parse(right.created_at) - Date.parse(left.created_at),
)
.slice(0, MAX_CANDIDATES);
for (const artifact of candidates) {
const run = normalizeWorkflowRun(
api(`repos/${repository}/actions/runs/${artifact.workflow_run.id}`),
const search = ({ artifactName, events }) => {
const { artifacts } = api(
`repos/${repository}/actions/artifacts`,
"-f",
`name=${artifactName}`,
"-F",
"per_page=100",
);
const runAge = now - Date.parse(run?.created_at);
if (
typeof run?.created_at === "string" &&
Number.isFinite(runAge) &&
runAge >= 0 &&
runAge < MAX_AGE_MS &&
run.id === artifact.workflow_run.id &&
run.status === "completed" &&
run.conclusion === "success" &&
run.workflowPath?.file === ".github/workflows/ci.yml" &&
["schedule", "workflow_dispatch"].includes(run.event) &&
run.head_branch === branch &&
run.head_repository?.full_name === repository
)
return String(run.id);
}
return "";
if (!Array.isArray(artifacts))
throw new Error("Missing timing artifact inventory");
const candidates = artifacts
.filter((artifact) => {
const lineage = artifact?.workflow_run;
const age = now - Date.parse(artifact?.created_at);
return (
artifact?.name === artifactName &&
artifact?.expired === false &&
typeof artifact?.created_at === "string" &&
Number.isFinite(age) &&
age >= 0 &&
age < MAX_AGE_MS &&
positiveId(lineage?.id) &&
positiveId(lineage?.repository_id) &&
positiveId(lineage?.head_repository_id) &&
lineage.repository_id === lineage.head_repository_id &&
lineage.repository_id === Number(repositoryId) &&
lineage.head_branch === branch
);
})
.sort(
(left, right) =>
Date.parse(right.created_at) - Date.parse(left.created_at),
)
.slice(0, MAX_CANDIDATES);
for (const artifact of candidates) {
const run = normalizeWorkflowRun(
api(`repos/${repository}/actions/runs/${artifact.workflow_run.id}`),
);
const runAge = now - Date.parse(run?.created_at);
if (
typeof run?.created_at === "string" &&
Number.isFinite(runAge) &&
runAge >= 0 &&
runAge < MAX_AGE_MS &&
run.id === artifact.workflow_run.id &&
run.status === "completed" &&
run.conclusion === "success" &&
run.workflowPath?.file === ".github/workflows/ci.yml" &&
events.includes(run.event) &&
run.head_branch === branch &&
run.head_repository?.full_name === repository
)
return String(run.id);
}
return "";
};
const runId = search(profile);
return runId
? { runId, artifactName: profile.artifactName }
: EMPTY_SELECTION;
}
function write(runId) {
function write({ runId, artifactName }) {
if (process.env.GITHUB_OUTPUT)
appendFileSync(process.env.GITHUB_OUTPUT, `run-id=${runId}\n`);
console.log(JSON.stringify({ run_id: runId }));
appendFileSync(
process.env.GITHUB_OUTPUT,
`run-id=${runId}\nartifact-name=${artifactName}\n`,
);
console.log(JSON.stringify({ run_id: runId, artifact_name: artifactName }));
}
if (
process.argv[1] &&
fileURLToPath(import.meta.url) === resolve(process.argv[1])
) {
let runId = "";
let selection = EMPTY_SELECTION;
try {
runId = findTimingRun({
selection = findTimingRun({
repository: process.env.CI_REPOSITORY,
branch: process.env.CI_DEFAULT_BRANCH,
repositoryId: process.env.CI_REPOSITORY_ID,
tier: process.env.CI_TIMING_TIER,
});
} catch {
// History improves shard ordering but must never block CI.
}
write(runId);
write(selection);
}
+149 -13
View File
@@ -17,7 +17,7 @@ const now = Date.now();
function artifact(id: number, overrides: Record<string, unknown> = {}) {
return {
id: id + 100,
name: "test-timings",
name: "test-timings-full",
expired: false,
created_at: new Date(now - 60_000).toISOString(),
workflow_run: {
@@ -51,6 +51,7 @@ async function invoke(
apiError?: boolean;
repository?: string;
repositoryId?: string;
tier?: string;
} = {},
) {
const directory = await createTestTempDirectory("rea-ci-test-history-");
@@ -92,6 +93,7 @@ async function invoke(
CI_REPOSITORY: options.repository ?? repository,
CI_REPOSITORY_ID: options.repositoryId ?? String(repositoryId),
CI_DEFAULT_BRANCH: "main",
CI_TIMING_TIER: options.tier ?? "",
GITHUB_OUTPUT: output,
},
});
@@ -120,17 +122,21 @@ it("selects the latest trusted full run that actually published timing history",
14: run(14, { path: ".github/workflows/release.yml" }),
11: run(11, { event: "workflow_dispatch" }),
},
{ tier: "full" },
);
expect(JSON.parse(result.stdout)).toEqual({ run_id: "11" });
expect(output).toBe("run-id=11\n");
expect(calls).toContain("name=test-timings");
expect(JSON.parse(result.stdout)).toEqual({
run_id: "11",
artifact_name: "test-timings-full",
});
expect(output).toBe("run-id=11\nartifact-name=test-timings-full\n");
expect(calls).toContain("name=test-timings-full");
expect(calls).toContain("per_page=100");
expect(calls.match(/actions\/runs\//gu)).toHaveLength(3);
expect(calls).not.toContain("actions/workflows");
});
it("rejects expired, foreign, old, and non-full run candidates", async () => {
it("rejects expired, foreign, old, and pull-request candidates", async () => {
const { result, output } = await invoke(
[
artifact(15, { expired: true }),
@@ -145,16 +151,30 @@ it("rejects expired, foreign, old, and non-full run candidates", async () => {
{
12: run(12, { event: "pull_request" }),
},
{ tier: "full" },
);
expect(JSON.parse(result.stdout)).toEqual({ run_id: "" });
expect(output).toBe("run-id=\n");
expect(JSON.parse(result.stdout)).toEqual({
run_id: "",
artifact_name: "",
});
expect(output).toBe("run-id=\nartifact-name=\n");
});
it("writes an empty run id when GitHub history is unavailable", async () => {
const { result, output } = await invoke([], {}, { apiError: true });
expect(JSON.parse(result.stdout)).toEqual({ run_id: "" });
expect(output).toBe("run-id=\n");
const { result, output } = await invoke(
[],
{},
{
apiError: true,
tier: "full",
},
);
expect(JSON.parse(result.stdout)).toEqual({
run_id: "",
artifact_name: "",
});
expect(output).toBe("run-id=\nartifact-name=\n");
});
it("bounds run lookups to five artifacts even when inventory has more", async () => {
@@ -167,9 +187,13 @@ it("bounds run lookups to five artifacts even when inventory has more", async ()
run(id, index < 5 ? { conclusion: "failure" } : {}),
]),
),
{ tier: "full" },
);
expect(JSON.parse(result.stdout)).toEqual({ run_id: "" });
expect(JSON.parse(result.stdout)).toEqual({
run_id: "",
artifact_name: "",
});
expect(calls.match(/actions\/runs\//gu)).toHaveLength(5);
});
@@ -182,7 +206,119 @@ it("returns empty history without querying for an invalid repository identity",
repositoryId: "not-an-id",
},
);
expect(JSON.parse(result.stdout)).toEqual({ run_id: "" });
expect(output).toBe("run-id=\n");
expect(JSON.parse(result.stdout)).toEqual({
run_id: "",
artifact_name: "",
});
expect(output).toBe("run-id=\nartifact-name=\n");
expect(calls).toBe("");
});
it("selects fast history without reading the full-tier artifact", async () => {
const { result, calls } = await invoke(
[
artifact(32, {
created_at: new Date(now - 30_000).toISOString(),
}),
artifact(31, {
name: "test-timings-fast",
created_at: new Date(now - 90_000).toISOString(),
}),
artifact(30, {
name: "test-timings-fast",
created_at: new Date(now - 20_000).toISOString(),
workflow_run: {
...artifact(30).workflow_run,
head_branch: "feature",
},
}),
],
{
32: run(32, { event: "schedule" }),
31: run(31, { event: "push" }),
},
{ tier: "fast" },
);
expect(JSON.parse(result.stdout)).toEqual({
run_id: "31",
artifact_name: "test-timings-fast",
});
expect(calls).toContain("name=test-timings-fast");
expect(calls).not.toContain("name=test-timings-full");
expect(calls.match(/actions\/runs\//gu)).toHaveLength(1);
});
it("ignores pull-request fast history instead of borrowing another artifact", async () => {
const { result, calls } = await invoke(
[artifact(41, { name: "test-timings-fast" }), artifact(40)],
{
41: run(41, { event: "pull_request" }),
40: run(40, { event: "workflow_dispatch" }),
},
{ tier: "fast" },
);
expect(JSON.parse(result.stdout)).toEqual({
run_id: "",
artifact_name: "",
});
expect(calls).toContain("name=test-timings-fast");
expect(calls).not.toContain("name=test-timings-full");
expect(calls.match(/actions\/runs\//gu)).toHaveLength(1);
});
it("leaves fast history empty when only full-tier history was published", async () => {
const { result, output, calls } = await invoke(
[artifact(70)],
{ 70: run(70, { event: "schedule" }) },
{ tier: "fast" },
);
expect(JSON.parse(result.stdout)).toEqual({
run_id: "",
artifact_name: "",
});
expect(output).toBe("run-id=\nartifact-name=\n");
expect(calls).toContain("name=test-timings-fast");
expect(calls).not.toContain("name=test-timings-full");
expect(calls).not.toMatch(/actions\/runs\//u);
});
it("does not query when the tier has no timing artifact", async () => {
const { result, output, calls } = await invoke([artifact(71)], {
71: run(71, { event: "schedule" }),
});
expect(JSON.parse(result.stdout)).toEqual({
run_id: "",
artifact_name: "",
});
expect(output).toBe("run-id=\nartifact-name=\n");
expect(calls).toBe("");
});
it("selects full-tier history without using a newer fast artifact", async () => {
const { result } = await invoke(
[
artifact(61, {
name: "test-timings-fast",
created_at: new Date(now - 10_000).toISOString(),
}),
artifact(60, {
name: "test-timings-full",
created_at: new Date(now - 80_000).toISOString(),
}),
],
{
61: run(61, { event: "push" }),
60: run(60, { event: "schedule" }),
},
{ tier: "full" },
);
expect(JSON.parse(result.stdout)).toEqual({
run_id: "60",
artifact_name: "test-timings-full",
});
});
@@ -243,10 +243,22 @@ it("keeps diagnosis available after merge failure and publishes only complete su
const diagnosis = steps.find((step) => step.with?.name === "test-results");
expect(diagnosis?.if).toContain("!cancelled()");
expect(diagnosis?.if).not.toContain("success()");
const history = steps.find((step) => step.with?.name === "test-timings");
const history = steps.find(
(step) => step.name === "Publish successful timing history",
);
expect(history?.if).toContain("success()");
expect(history?.if).toContain("needs.test-shard.result == 'success'");
expect(history?.if).toContain("needs.changes.outputs.tier == 'full'");
expect(history?.if).toContain(
"needs.changes.outputs.tier == 'fast' && needs.test-source.result == 'success'",
);
expect(history?.if).toContain("github.event_name == 'push'");
expect(history?.if).toContain(
"github.ref == format('refs/heads/{0}', github.event.repository.default_branch)",
);
expect(history?.with?.name).toBe(
"test-timings-${{ needs.changes.outputs.tier }}",
);
expect(history?.with?.path).toBe(".cache/ci-test-history/test-results.json");
const buildSteps = (await workflow()).build!.steps;
const lookup = buildSteps.find((step) =>
@@ -256,11 +268,14 @@ it("keeps diagnosis available after merge failure and publishes only complete su
CI_REPOSITORY: "${{ github.repository }}",
CI_REPOSITORY_ID: "${{ github.repository_id }}",
CI_DEFAULT_BRANCH: "${{ github.event.repository.default_branch }}",
CI_TIMING_TIER: "${{ needs.changes.outputs.tier }}",
});
const download = buildSteps.find(
(step) => step.name === "Download previous timing report",
);
expect(download?.with?.name).toBe(history?.with?.name);
expect(download?.with?.name).toBe(
"${{ steps.history.outputs.artifact-name }}",
);
expect(download?.with?.["run-id"]).toBe(
"${{ steps.history.outputs.run-id }}",
);