Skip to content

Commit f9c8d2f

Browse files
authored
Merge pull request #477 from jawkio/475-optimize-jrtcompare2object-object-boolean-hot-paths
Optimize JRT.compare2 hot paths and publish benchmark reports
2 parents 0d3fbd3 + 97cb3e5 commit f9c8d2f

12 files changed

Lines changed: 1226 additions & 19 deletions

File tree

.github/workflows/benchmarks.yml

Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
1+
name: Publish Benchmarks
2+
3+
on:
4+
push:
5+
tags:
6+
- "*"
7+
workflow_dispatch:
8+
inputs:
9+
ref:
10+
description: "Git ref to benchmark"
11+
required: false
12+
default: "main"
13+
version:
14+
description: "Version label to publish; defaults to the selected ref"
15+
required: false
16+
default: ""
17+
pattern:
18+
description: "JMH benchmark pattern"
19+
required: false
20+
default: "JRTCompare2Benchmark"
21+
jmhArgs:
22+
description: "Extra JMH arguments; when provided, CLI options override benchmark annotations"
23+
required: false
24+
default: ""
25+
publish:
26+
description: "Publish results to GitHub Pages"
27+
required: false
28+
type: boolean
29+
default: true
30+
31+
concurrency:
32+
group: benchmarks-${{ github.ref }}
33+
cancel-in-progress: false
34+
35+
env:
36+
DEFAULT_JMH_PATTERN: JRTCompare2Benchmark
37+
BENCHMARK_SITE_URL: https://jawk.io/
38+
39+
jobs:
40+
benchmarks:
41+
name: Run JMH benchmarks
42+
runs-on: ubuntu-latest
43+
permissions:
44+
contents: read
45+
outputs:
46+
publish: ${{ steps.select.outputs.publish }}
47+
48+
steps:
49+
- name: Checkout repository
50+
uses: actions/checkout@v6
51+
with:
52+
fetch-depth: 0
53+
54+
- name: Set up Temurin JDK 17
55+
uses: actions/setup-java@v5
56+
with:
57+
distribution: temurin
58+
java-version: "17"
59+
cache: maven
60+
61+
- name: Select benchmark ref
62+
id: select
63+
shell: bash
64+
env:
65+
INPUT_REF: ${{ inputs.ref }}
66+
INPUT_VERSION: ${{ inputs.version }}
67+
INPUT_PATTERN: ${{ inputs.pattern }}
68+
INPUT_JMH_ARGS: ${{ inputs.jmhArgs }}
69+
INPUT_PUBLISH: ${{ inputs.publish }}
70+
run: |
71+
if [ "${{ github.event_name }}" = "push" ]; then
72+
ref="${GITHUB_REF_NAME}"
73+
version="${GITHUB_REF_NAME}"
74+
pattern="${DEFAULT_JMH_PATTERN}"
75+
jmh_args=""
76+
publish="true"
77+
else
78+
ref="${INPUT_REF}"
79+
version="${INPUT_VERSION}"
80+
pattern="${INPUT_PATTERN}"
81+
jmh_args="${INPUT_JMH_ARGS}"
82+
publish="${INPUT_PUBLISH}"
83+
fi
84+
85+
if [ -z "${ref}" ]; then
86+
ref="main"
87+
fi
88+
if [ -z "${version}" ]; then
89+
version="${ref#refs/tags/}"
90+
fi
91+
if [ -z "${pattern}" ]; then
92+
pattern="${DEFAULT_JMH_PATTERN}"
93+
fi
94+
if [[ "${ref}" == -* ]]; then
95+
echo "Benchmark ref must not start with '-'." >&2
96+
exit 1
97+
fi
98+
99+
git fetch --tags --force origin
100+
if commit="$(git rev-parse --verify --quiet -- "${ref}^{commit}")"; then
101+
git checkout --detach "${commit}"
102+
else
103+
git fetch origin -- "${ref}"
104+
commit="$(git rev-parse --verify --quiet -- "FETCH_HEAD^{commit}")"
105+
git checkout --detach "${commit}"
106+
fi
107+
108+
safe_version="$(printf '%s' "${version}" | tr '/\\ ' '---' | tr -cd 'A-Za-z0-9._-')"
109+
if [ -z "${safe_version}" ]; then
110+
safe_version="$(git rev-parse --short HEAD)"
111+
fi
112+
113+
{
114+
echo "BENCHMARK_REF=${ref}"
115+
echo "BENCHMARK_VERSION=${version}"
116+
echo "BENCHMARK_VERSION_PATH=${safe_version}"
117+
echo "BENCHMARK_COMMIT=$(git rev-parse HEAD)"
118+
echo "JMH_PATTERN=${pattern}"
119+
echo "JMH_ARGS=${jmh_args}"
120+
echo "PUBLISH_BENCHMARKS=${publish}"
121+
} >> "${GITHUB_ENV}"
122+
echo "publish=${publish}" >> "${GITHUB_OUTPUT}"
123+
124+
- name: Display environment details
125+
shell: bash
126+
run: |
127+
java -version
128+
mvn -version
129+
echo "Benchmark ref: ${BENCHMARK_REF}"
130+
echo "Benchmark version: ${BENCHMARK_VERSION}"
131+
echo "Benchmark commit: ${BENCHMARK_COMMIT}"
132+
echo "JMH pattern: ${JMH_PATTERN}"
133+
echo "JMH arguments: ${JMH_ARGS}"
134+
135+
- name: Build benchmark jar
136+
shell: bash
137+
run: mvn -B -V -Pbenchmark -DskipTests package
138+
139+
- name: Run JMH
140+
shell: bash
141+
run: |
142+
mkdir -p target/benchmarks
143+
benchmark_jar="$(find target -maxdepth 1 -name '*-benchmarks.jar' -print -quit)"
144+
if [ -z "${benchmark_jar}" ]; then
145+
echo "Benchmark jar was not created." >&2
146+
exit 1
147+
fi
148+
java -jar "${benchmark_jar}" "${JMH_PATTERN}" ${JMH_ARGS} -rf json -rff target/benchmarks/jmh-results.json
149+
150+
- name: Capture benchmark environment
151+
shell: bash
152+
run: |
153+
node <<'NODE'
154+
const childProcess = require('child_process');
155+
const fs = require('fs');
156+
const os = require('os');
157+
158+
function commandOutput(command) {
159+
return childProcess.execSync(command, { encoding: 'utf8', shell: '/bin/bash' });
160+
}
161+
162+
const cpuModels = [...new Set(os.cpus().map(cpu => cpu.model))];
163+
const environment = {
164+
version: process.env.BENCHMARK_VERSION,
165+
versionPath: process.env.BENCHMARK_VERSION_PATH,
166+
ref: process.env.BENCHMARK_REF,
167+
commit: process.env.BENCHMARK_COMMIT,
168+
runDate: new Date().toISOString(),
169+
workflowRunUrl: `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`,
170+
benchmarkPattern: process.env.JMH_PATTERN,
171+
jmhArgs: process.env.JMH_ARGS,
172+
runner: {
173+
os: process.env.RUNNER_OS,
174+
arch: process.env.RUNNER_ARCH,
175+
name: process.env.RUNNER_NAME
176+
},
177+
system: {
178+
platform: os.platform(),
179+
release: os.release(),
180+
arch: os.arch(),
181+
cpuCount: os.cpus().length,
182+
cpus: cpuModels,
183+
totalMemory: os.totalmem()
184+
},
185+
javaVersion: commandOutput('java -version 2>&1'),
186+
mavenVersion: commandOutput('mvn -version 2>&1')
187+
};
188+
189+
fs.mkdirSync('target/benchmarks', { recursive: true });
190+
fs.writeFileSync('target/benchmarks/environment.json', `${JSON.stringify(environment, null, 2)}\n`);
191+
NODE
192+
193+
- name: Upload benchmark artifacts
194+
uses: actions/upload-artifact@v7
195+
with:
196+
name: benchmarks-${{ env.BENCHMARK_VERSION_PATH }}
197+
path: |
198+
target/benchmarks/jmh-results.json
199+
target/benchmarks/environment.json
200+
201+
- name: Build Maven site
202+
if: env.PUBLISH_BENCHMARKS == 'true'
203+
shell: bash
204+
run: mvn -B -V verify site
205+
206+
- name: Restore published benchmark history
207+
if: env.PUBLISH_BENCHMARKS == 'true'
208+
shell: bash
209+
run: |
210+
node <<'NODE'
211+
const fs = require('fs');
212+
const http = require('http');
213+
const https = require('https');
214+
const path = require('path');
215+
216+
const siteBase = new URL(process.env.BENCHMARK_SITE_URL || 'https://jawk.io/');
217+
const siteRoot = 'target/site';
218+
const resolvedSiteRoot = path.resolve(siteRoot);
219+
const indexPath = path.join(siteRoot, 'benchmarks', 'index.json');
220+
221+
function getText(url) {
222+
return new Promise(resolve => {
223+
const client = url.protocol === 'http:' ? http : https;
224+
const request = client.get(url, response => {
225+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
226+
response.resume();
227+
resolve(getText(new URL(response.headers.location, url)));
228+
return;
229+
}
230+
if (response.statusCode < 200 || response.statusCode >= 300) {
231+
response.resume();
232+
resolve(null);
233+
return;
234+
}
235+
response.setEncoding('utf8');
236+
let data = '';
237+
response.on('data', chunk => {
238+
data += chunk;
239+
});
240+
response.on('end', () => resolve(data));
241+
});
242+
request.on('error', () => resolve(null));
243+
request.setTimeout(15000, () => {
244+
request.destroy();
245+
resolve(null);
246+
});
247+
});
248+
}
249+
250+
async function restoreFile(relativePath) {
251+
if (!relativePath) {
252+
return;
253+
}
254+
const normalizedPath = relativePath.replace(/^\/+/, '');
255+
if (normalizedPath.includes('\\') || normalizedPath.split('/').includes('..')) {
256+
return;
257+
}
258+
const content = await getText(new URL(normalizedPath, siteBase));
259+
if (content === null) {
260+
return;
261+
}
262+
const outputPath = path.resolve(resolvedSiteRoot, normalizedPath);
263+
const outputRelativePath = path.relative(resolvedSiteRoot, outputPath);
264+
if (outputRelativePath.startsWith('..') || path.isAbsolute(outputRelativePath)) {
265+
return;
266+
}
267+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
268+
fs.writeFileSync(outputPath, content);
269+
}
270+
271+
(async () => {
272+
const indexText = await getText(new URL('benchmarks/index.json', siteBase));
273+
if (indexText === null) {
274+
return;
275+
}
276+
277+
fs.mkdirSync(path.dirname(indexPath), { recursive: true });
278+
fs.writeFileSync(indexPath, indexText);
279+
280+
let index;
281+
try {
282+
index = JSON.parse(indexText);
283+
} catch (error) {
284+
return;
285+
}
286+
287+
for (const release of index.releases || []) {
288+
await restoreFile(release.jmh);
289+
await restoreFile(release.environment);
290+
}
291+
})().catch(error => {
292+
console.log(`Benchmark history restore failed: ${error.message}`);
293+
});
294+
NODE
295+
296+
- name: Add benchmark JSON to Maven site
297+
if: env.PUBLISH_BENCHMARKS == 'true'
298+
shell: bash
299+
run: |
300+
release_dir="target/site/benchmarks/releases/${BENCHMARK_VERSION_PATH}"
301+
mkdir -p "${release_dir}"
302+
cp target/benchmarks/jmh-results.json "${release_dir}/jmh-results.json"
303+
cp target/benchmarks/environment.json "${release_dir}/environment.json"
304+
305+
node <<'NODE'
306+
const fs = require('fs');
307+
const path = require('path');
308+
309+
const indexPath = 'target/site/benchmarks/index.json';
310+
const version = process.env.BENCHMARK_VERSION;
311+
const versionPath = process.env.BENCHMARK_VERSION_PATH;
312+
const environment = JSON.parse(fs.readFileSync(`target/site/benchmarks/releases/${versionPath}/environment.json`, 'utf8'));
313+
let index = { latest: null, releases: [] };
314+
315+
if (fs.existsSync(indexPath)) {
316+
try {
317+
index = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
318+
} catch (error) {
319+
index = { latest: null, releases: [] };
320+
}
321+
}
322+
323+
const entry = {
324+
version,
325+
versionPath,
326+
date: environment.runDate.substring(0, 10),
327+
commit: environment.commit,
328+
workflowRunUrl: environment.workflowRunUrl,
329+
jmh: path.posix.join('benchmarks', 'releases', versionPath, 'jmh-results.json'),
330+
environment: path.posix.join('benchmarks', 'releases', versionPath, 'environment.json')
331+
};
332+
333+
index.releases = (index.releases || []).filter(release => release.versionPath !== versionPath);
334+
index.releases.unshift(entry);
335+
index.latest = version;
336+
337+
fs.mkdirSync(path.dirname(indexPath), { recursive: true });
338+
fs.writeFileSync(indexPath, `${JSON.stringify(index, null, 2)}\n`);
339+
NODE
340+
341+
- name: Configure GitHub Pages
342+
if: env.PUBLISH_BENCHMARKS == 'true'
343+
uses: actions/configure-pages@v6
344+
345+
- name: Upload GitHub Pages artifact
346+
if: env.PUBLISH_BENCHMARKS == 'true'
347+
uses: actions/upload-pages-artifact@v4
348+
with:
349+
path: target/site
350+
351+
deploy:
352+
name: Deploy benchmark site
353+
needs: benchmarks
354+
if: needs.benchmarks.outputs.publish == 'true'
355+
runs-on: ubuntu-latest
356+
permissions:
357+
pages: write
358+
id-token: write
359+
environment:
360+
name: github-pages
361+
url: ${{ steps.deployment.outputs.page_url }}
362+
363+
steps:
364+
- name: Deploy to GitHub Pages
365+
id: deployment
366+
uses: actions/deploy-pages@v5

CONTRIBUTING.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,19 @@ mvn site
2626

2727
For more information about Maven-generated documentation, visit [Maven Site plugin](https://maven.apache.org/plugins/maven-site-plugin/) and [Sentry Maven Skin](https://sentrysoftware.github.io/sentry-maven-skin/).
2828

29+
## Benchmarks
30+
31+
Microbenchmarks use [JMH](https://openjdk.org/projects/code-tools/jmh/) and are built only when the benchmark profile is enabled:
32+
33+
```bash
34+
mvn -Pbenchmark -DskipTests package
35+
java -jar target/jawk-<VERSION>-benchmarks.jar JRTCompare2Benchmark
36+
```
37+
38+
Release benchmark data is published by the *Publish Benchmarks* GitHub Action. It builds the Maven site, writes JMH
39+
JSON files under `target/site/benchmarks/releases/<VERSION>/`, updates `target/site/benchmarks/index.json`, and
40+
deploys the complete site through GitHub Pages.
41+
2942
## Development workflows
3043

3144
Please follow this workflow to contribute to this project:

0 commit comments

Comments
 (0)