Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/jtreg.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ jobs:
read -ra filters <<< "$FILTERS"
node scripts/jtreg/run.ts build/ref/jdk25u --only-passing --jobs 4 --timeout 600 --vm dist/node.js \
--shard ${{ matrix.shard }}/${{ strategy.job-total }} "${filters[@]}"
# each test's run time, for refreshing test/jtreg/times.tsv
- uses: actions/upload-artifact@v7
if: always()
with:
name: times-${{ matrix.shard }}
path: build/jtreg/times.tsv
if-no-files-found: ignore
retention-days: 30

# one check for branch protection, whatever the number of shards
result:
Expand Down
14 changes: 14 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ node scripts/jtreg/run.ts <jdk-source> java/lang
without the `+build` part. A sparse checkout of `test/lib` and the test
directories is enough.

`test/jtreg/passing.txt` lists the tests CI runs, with `--only-passing`.
`--shard k/n` splits them into n shards of about equal run time, using each
test's time on java-web from `test/jtreg/times.tsv` (the median for a test
not in it). Each CI shard uploads its `build/jtreg/times.tsv` as an artifact.
To refresh the file from a run, download those artifacts and merge them in:
a test's new time replaces its old one, and tests the run left out keep theirs.

```sh
gh run download <run-id> -p 'times-*' -D build/times
cat test/jtreg/times.tsv build/times/*/times.tsv |
awk -F'\t' '{ t[$2] = $1 } END { for (id in t) print t[id] "\t" id }' |
sort -rn > build/times.tsv && mv build/times.tsv test/jtreg/times.tsv
```

`pnpm build` also builds `dist/selftest/`, a browser check of library mode
(objects, fields, arrays, exceptions), page natives and javac.
`node scripts/serve.ts 8080 --like-github-pages` serves `dist/` the way GitHub
Expand Down
34 changes: 31 additions & 3 deletions scripts/jtreg/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
// tests that pass there count), then on java-web in a child process.
// Results go to build/jtreg/results.jsonl; --only-passing reruns the tests
// listed in test/jtreg/passing.txt (those under the filters, if any) on
// java-web alone, and fails if any of them regress. --shard k/n runs every
// nth of the selected tests, for parallel CI jobs.
// java-web alone, and fails if any of them regress. --shard k/n runs the kth
// of n shards with about equal run time, for parallel CI jobs.

import { createHash } from "node:crypto";
import { spawn, spawnSync } from "node:child_process";
Expand All @@ -21,6 +21,8 @@ const root = join(import.meta.dirname, "..", "..");
const out = join(root, "build", "jtreg");
const resultsFile = join(out, "results.jsonl");
const passingFile = join(root, "test", "jtreg", "passing.txt");
// how long each listed test takes on java-web, for splitting them into shards
const timesFile = join(root, "test", "jtreg", "times.tsv");

const argv = process.argv.slice(2);
const flag = (name: string, def: string) => {
Expand Down Expand Up @@ -412,6 +414,32 @@ function findTests(paths: string[]): string[] {
return files;
}

// This shard's tests: the slowest first, each to the shard with the least
// time so far, by the times in test/jtreg/times.tsv (the median when unknown).
// Every shard computes the same split.
function shard(tests: TestCase[]): TestCase[] {
if (shardN === 1) return tests;
const known = new Map<string, number>();
if (existsSync(timesFile)) {
for (const l of readFileSync(timesFile, "utf8").split("\n")) {
const [ms, id] = l.split("\t");
if (id && Number.isFinite(Number(ms))) known.set(id, Number(ms));
}
}
const sorted = [...known.values()].sort((a, b) => a - b);
const median = sorted.length ? sorted[sorted.length >> 1] : 1;
const load = new Array(shardN).fill(0);
const mine = new Set<TestCase>();
const order = tests.map((tc) => ({ tc, ms: known.get(tc.id) ?? median }))
.sort((a, b) => b.ms - a.ms || (a.tc.id < b.tc.id ? -1 : a.tc.id > b.tc.id ? 1 : 0));
for (const { tc, ms } of order) {
const k = load.indexOf(Math.min(...load));
load[k] += ms;
if (k === shardK - 1) mine.add(tc);
}
return tests.filter((tc) => mine.has(tc));
}

// test ids in passing.txt
function listed(): string[] {
if (!existsSync(passingFile)) return [];
Expand All @@ -436,7 +464,7 @@ if (onlyPassing) {
return ids.has(tc.id) || whole.has(file) || !narrowed.has(file);
});
}
selected = selected.filter((_, i) => i % shardN === shardK - 1);
selected = shard(selected);
const done = new Set<string>();
if (!rerun && !onlyPassing && existsSync(resultsFile)) {
for (const l of readFileSync(resultsFile, "utf8").split("\n")) if (l) done.add(JSON.parse(l).id);
Expand Down
2 changes: 1 addition & 1 deletion scripts/jtreg/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function loaded(): Set<string> {
}
const notLoaded = /\.md$|^docs\/|^LICENSE|^renovate\.json$|^\.coderabbit|^types\/|^test\/(?!jtreg\/)/;

const shardsFor = (n: number) => Array.from({ length: Math.min(4, Math.ceil(n / 400)) }, (_, i) => i + 1);
const shardsFor = (n: number) => Array.from({ length: Math.min(8, Math.ceil(n / 400)) }, (_, i) => i + 1);
const listed = readFileSync(join(root, passingFile), "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
const under = (id: string, f: string) => id === f || id.startsWith(f + "/") || id.startsWith(f + "#");

Expand Down
Loading
Loading