Skip to content

Commit df8246e

Browse files
authored
regression test performance (#73)
* regression test * round
1 parent 2bf0fb1 commit df8246e

10 files changed

Lines changed: 249 additions & 0 deletions

File tree

.github/workflows/regression.yml

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
name: regression
2+
3+
# Performance regression test: runs perf/benchmarks/ through the transpiled
4+
# interpreter built from the PR branch and from the base branch, then posts
5+
# a before/after comparison as a PR comment.
6+
# Inspired by https://github.com/abaplint/abaplint/blob/main/.github/workflows/regression.yml
7+
8+
on:
9+
pull_request:
10+
11+
permissions:
12+
contents: read
13+
pull-requests: write
14+
15+
concurrency:
16+
group: regression-${{ github.event.pull_request.number }}
17+
cancel-in-progress: true
18+
19+
jobs:
20+
perf:
21+
runs-on: ubuntu-latest
22+
timeout-minutes: 20
23+
steps:
24+
- uses: actions/checkout@v6
25+
with:
26+
path: after
27+
- uses: actions/checkout@v6
28+
with:
29+
ref: ${{ github.event.pull_request.base.sha }}
30+
path: before
31+
- uses: actions/setup-node@v6
32+
33+
- name: build PR branch
34+
working-directory: after
35+
run: |
36+
npm ci
37+
npx abap_transpile
38+
39+
- name: build base branch
40+
working-directory: before
41+
run: |
42+
npm ci
43+
npx abap_transpile
44+
45+
# alternate base/PR passes so both sides sample the same machine conditions
46+
- name: run benchmarks
47+
run: |
48+
for i in 1 2 3 4 5 6 7; do
49+
node after/perf/run.mjs before/output before.json
50+
node after/perf/run.mjs after/output after.json
51+
done
52+
53+
- name: compare
54+
run: |
55+
node after/perf/compare.mjs before.json after.json | tee comment-body.md
56+
cat comment-body.md >> "$GITHUB_STEP_SUMMARY"
57+
58+
- name: find existing comment
59+
if: github.event.pull_request.head.repo.full_name == github.repository
60+
uses: peter-evans/find-comment@v3
61+
id: fc
62+
with:
63+
issue-number: ${{ github.event.pull_request.number }}
64+
comment-author: 'github-actions[bot]'
65+
body-includes: Performance regression results
66+
67+
- name: create or update comment
68+
if: github.event.pull_request.head.repo.full_name == github.repository
69+
uses: peter-evans/create-or-update-comment@v4
70+
with:
71+
comment-id: ${{ steps.fc.outputs.comment-id }}
72+
issue-number: ${{ github.event.pull_request.number }}
73+
body-path: comment-body.md
74+
edit-mode: replace

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ output
1111
# current score: PASS=230 FAIL=36 TOTAL=266
1212
# (add your own .js files here freely)
1313
_tmp/
14+
15+
# perf regression result files (perf/run.mjs)
16+
before.json
17+
after.json

perf/benchmarks/array.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const arr = [];
2+
for (let i = 0; i < 2000; i++) {
3+
arr.push(i);
4+
}
5+
let total = 0;
6+
for (let i = 0; i < arr.length; i++) {
7+
total = total + arr[i];
8+
}
9+
console.log(total);

perf/benchmarks/class.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
class Counter {
2+
constructor() {
3+
this.n = 0;
4+
}
5+
inc() {
6+
this.n = this.n + 1;
7+
}
8+
}
9+
const c = new Counter();
10+
for (let i = 0; i < 1000; i++) {
11+
c.inc();
12+
}
13+
console.log(c.n);

perf/benchmarks/fib.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
function fib(n) {
2+
if (n < 2) return n;
3+
return fib(n - 1) + fib(n - 2);
4+
}
5+
console.log(fib(15));

perf/benchmarks/loop.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
let sum = 0;
2+
for (let i = 0; i < 3000; i++) {
3+
sum = sum + i;
4+
}
5+
console.log(sum);

perf/benchmarks/object.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const obj = { count: 0, name: "bench" };
2+
for (let i = 0; i < 2000; i++) {
3+
obj.count = obj.count + 1;
4+
}
5+
console.log(obj.count);

perf/benchmarks/string.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
let s = "";
2+
for (let i = 0; i < 1000; i++) {
3+
s = s + "ab";
4+
}
5+
console.log(s.length);

perf/compare.mjs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Compares two result files produced by perf/run.mjs (one or more passes
2+
// each) and prints a markdown summary with the median time per benchmark.
3+
//
4+
// usage: node perf/compare.mjs <before.json> <after.json>
5+
6+
import * as fs from "node:fs";
7+
8+
const [beforeFile, afterFile] = process.argv.slice(2);
9+
if (beforeFile === undefined || afterFile === undefined) {
10+
console.error("usage: node perf/compare.mjs <before.json> <after.json>");
11+
process.exit(1);
12+
}
13+
14+
// [{name, ms, value} | {name, error}] per pass -> {name: {median, value, error}}
15+
function aggregate(file) {
16+
const passes = JSON.parse(fs.readFileSync(file, "utf-8"));
17+
const byName = new Map();
18+
for (const pass of passes) {
19+
for (const r of pass) {
20+
const agg = byName.get(r.name) ?? {times: []};
21+
if (r.error !== undefined) {
22+
agg.error = r.error;
23+
} else {
24+
agg.times.push(r.ms);
25+
agg.value = r.value;
26+
}
27+
byName.set(r.name, agg);
28+
}
29+
}
30+
for (const agg of byName.values()) {
31+
agg.times.sort((x, y) => x - y);
32+
agg.median = agg.times[Math.floor(agg.times.length / 2)];
33+
}
34+
return byName;
35+
}
36+
37+
const before = aggregate(beforeFile);
38+
const after = aggregate(afterFile);
39+
const passCount = JSON.parse(fs.readFileSync(afterFile, "utf-8")).length;
40+
41+
const lines = [];
42+
lines.push("## Performance regression results");
43+
lines.push("");
44+
lines.push("| Benchmark | Base | PR | Change |");
45+
lines.push("|---|---:|---:|---:|");
46+
47+
for (const [name, a] of after) {
48+
const b = before.get(name);
49+
const col = agg => agg === undefined ? "n/a" : agg.error !== undefined ? "ERROR" : `${Math.round(agg.median)}ms`;
50+
let changeCol = "";
51+
if (b !== undefined && b.error === undefined && a.error === undefined) {
52+
const delta = ((a.median - b.median) / b.median) * 100;
53+
const sign = delta >= 0 ? "+" : "";
54+
let marker = "";
55+
if (delta > 15) {
56+
marker = " 🔺";
57+
} else if (delta < -15) {
58+
marker = " 🟢";
59+
}
60+
changeCol = `${sign}${delta.toFixed(1)}%${marker}`;
61+
if (a.value !== b.value) {
62+
const fmt = v => String(v).replace(/\r?\n/g, "\\n").replace(/\|/g, "\\|");
63+
changeCol += ` ⚠️ result changed: \`${fmt(b.value)}\` → \`${fmt(a.value)}\``;
64+
}
65+
} else if (a.error !== undefined || b?.error !== undefined) {
66+
changeCol = `⚠️ ${a.error ?? b.error}`;
67+
}
68+
lines.push(`| ${name} | ${col(b)} | ${col(a)} | ${changeCol} |`);
69+
}
70+
71+
lines.push("");
72+
lines.push(`Median of ${passCount} interleaved passes of the transpiled interpreter — small deltas are noise.`);
73+
74+
console.log(lines.join("\n"));

perf/run.mjs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Runs one timing pass of every perf/benchmarks/*.js file through the
2+
// transpiled ZCL_MJS interpreter and appends the pass to the result file.
3+
//
4+
// Invoke multiple times, alternating between the two builds under comparison,
5+
// so both sides sample the same machine conditions (see regression.yml).
6+
// perf/compare.mjs computes the median per benchmark across all passes.
7+
//
8+
// usage: node perf/run.mjs <transpiled-output-dir> [result.json]
9+
10+
import * as fs from "node:fs";
11+
import * as path from "node:path";
12+
import {fileURLToPath, pathToFileURL} from "node:url";
13+
import {performance} from "node:perf_hooks";
14+
15+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
16+
17+
const outputDir = process.argv[2];
18+
const resultFile = process.argv[3];
19+
if (outputDir === undefined) {
20+
console.error("usage: node perf/run.mjs <transpiled-output-dir> [result.json]");
21+
process.exit(1);
22+
}
23+
24+
const {initializeABAP} = await import(pathToFileURL(path.resolve(outputDir, "init.mjs")));
25+
await initializeABAP();
26+
27+
const benchmarkDir = path.join(__dirname, "benchmarks");
28+
const files = fs.readdirSync(benchmarkDir).filter(f => f.endsWith(".js")).sort();
29+
const pass = [];
30+
31+
for (const file of files) {
32+
const name = path.basename(file, ".js");
33+
const source = fs.readFileSync(path.join(benchmarkDir, file), "utf-8");
34+
try {
35+
const start = performance.now();
36+
const res = await abap.Classes["ZCL_MJS"].eval({iv_source: new abap.types.String().set(source)});
37+
const ms = Math.round((performance.now() - start) * 10) / 10;
38+
const value = res.get().trim();
39+
console.log(`${name}: ${ms}ms = ${value}`);
40+
pass.push({name, ms, value});
41+
} catch (e) {
42+
const error = "" + (e?.message ?? e);
43+
console.log(`${name}: ERROR ${error}`);
44+
pass.push({name, error});
45+
}
46+
}
47+
48+
if (resultFile) {
49+
let passes = [];
50+
if (fs.existsSync(resultFile)) {
51+
passes = JSON.parse(fs.readFileSync(resultFile, "utf-8"));
52+
}
53+
passes.push(pass);
54+
fs.writeFileSync(resultFile, JSON.stringify(passes, null, 2));
55+
}

0 commit comments

Comments
 (0)