Skip to content
Open
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
359 changes: 359 additions & 0 deletions .github/workflows/benchmarks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,359 @@
name: Publish Benchmarks

on:
push:
tags:
- "*"
workflow_dispatch:
inputs:
ref:
description: "Git ref to benchmark"
required: false
default: "main"
version:
description: "Version label to publish; defaults to the selected ref"
required: false
default: ""
pattern:
description: "JMH benchmark pattern"
required: false
default: "JRTCompare2Benchmark"
jmhArgs:
description: "Extra JMH arguments"
required: false
default: "-wi 5 -i 10 -f 3"
publish:
description: "Publish results to GitHub Pages"
required: false
type: boolean
default: true

concurrency:
group: benchmarks-${{ github.ref }}
cancel-in-progress: false

env:
DEFAULT_JMH_PATTERN: JRTCompare2Benchmark
DEFAULT_JMH_ARGS: -wi 5 -i 10 -f 3
BENCHMARK_SITE_URL: https://jawk.io/

jobs:
benchmarks:
name: Run JMH benchmarks
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
publish: ${{ steps.select.outputs.publish }}

steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Set up Temurin JDK 17
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "17"
cache: maven

- name: Select benchmark ref
id: select
shell: bash
run: |
if [ "${{ github.event_name }}" = "push" ]; then
ref="${GITHUB_REF_NAME}"
version="${GITHUB_REF_NAME}"
pattern="${DEFAULT_JMH_PATTERN}"
jmh_args="${DEFAULT_JMH_ARGS}"
publish="true"
else
ref="${{ inputs.ref }}"
version="${{ inputs.version }}"
pattern="${{ inputs.pattern }}"
jmh_args="${{ inputs.jmhArgs }}"
publish="${{ inputs.publish }}"
fi

if [ -z "${ref}" ]; then
ref="main"
fi
if [ -z "${version}" ]; then
version="${ref#refs/tags/}"
fi
if [ -z "${pattern}" ]; then
pattern="${DEFAULT_JMH_PATTERN}"
fi
if [ -z "${jmh_args}" ]; then
jmh_args="${DEFAULT_JMH_ARGS}"
fi

git fetch --tags --force origin
if git rev-parse --verify --quiet "${ref}^{commit}" > /dev/null; then
git checkout "${ref}"
else
git fetch origin "${ref}"
git checkout FETCH_HEAD
fi

safe_version="$(printf '%s' "${version}" | tr '/\\ ' '---' | tr -cd 'A-Za-z0-9._-')"
if [ -z "${safe_version}" ]; then
safe_version="$(git rev-parse --short HEAD)"
fi

{
echo "BENCHMARK_REF=${ref}"
echo "BENCHMARK_VERSION=${version}"
echo "BENCHMARK_VERSION_PATH=${safe_version}"
echo "BENCHMARK_COMMIT=$(git rev-parse HEAD)"
echo "JMH_PATTERN=${pattern}"
echo "JMH_ARGS=${jmh_args}"
echo "PUBLISH_BENCHMARKS=${publish}"
} >> "${GITHUB_ENV}"
echo "publish=${publish}" >> "${GITHUB_OUTPUT}"

- name: Display environment details
shell: bash
run: |
java -version
mvn -version
echo "Benchmark ref: ${BENCHMARK_REF}"
echo "Benchmark version: ${BENCHMARK_VERSION}"
echo "Benchmark commit: ${BENCHMARK_COMMIT}"
echo "JMH pattern: ${JMH_PATTERN}"
echo "JMH arguments: ${JMH_ARGS}"

- name: Build benchmark jar
shell: bash
run: mvn -B -V -Pbenchmark -DskipTests package

- name: Run JMH
shell: bash
run: |
mkdir -p target/benchmarks
benchmark_jar="$(find target -maxdepth 1 -name '*-benchmarks.jar' -print -quit)"
if [ -z "${benchmark_jar}" ]; then
echo "Benchmark jar was not created." >&2
exit 1
fi
java -jar "${benchmark_jar}" "${JMH_PATTERN}" ${JMH_ARGS} -rf json -rff target/benchmarks/jmh-results.json

- name: Capture benchmark environment
shell: bash
run: |
node <<'NODE'
const childProcess = require('child_process');
const fs = require('fs');
const os = require('os');

function commandOutput(command) {
return childProcess.execSync(command, { encoding: 'utf8', shell: '/bin/bash' });
}

const cpuModels = [...new Set(os.cpus().map(cpu => cpu.model))];
const environment = {
version: process.env.BENCHMARK_VERSION,
versionPath: process.env.BENCHMARK_VERSION_PATH,
ref: process.env.BENCHMARK_REF,
commit: process.env.BENCHMARK_COMMIT,
runDate: new Date().toISOString(),
workflowRunUrl: `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`,
benchmarkPattern: process.env.JMH_PATTERN,
jmhArgs: process.env.JMH_ARGS,
runner: {
os: process.env.RUNNER_OS,
arch: process.env.RUNNER_ARCH,
name: process.env.RUNNER_NAME
},
system: {
platform: os.platform(),
release: os.release(),
arch: os.arch(),
cpuCount: os.cpus().length,
cpus: cpuModels,
totalMemory: os.totalmem()
},
javaVersion: commandOutput('java -version 2>&1'),
mavenVersion: commandOutput('mvn -version 2>&1')
};

fs.mkdirSync('target/benchmarks', { recursive: true });
fs.writeFileSync('target/benchmarks/environment.json', `${JSON.stringify(environment, null, 2)}\n`);
NODE

- name: Upload benchmark artifacts
uses: actions/upload-artifact@v7
with:
name: benchmarks-${{ env.BENCHMARK_VERSION_PATH }}
path: |
target/benchmarks/jmh-results.json
target/benchmarks/environment.json

- name: Build Maven site
if: env.PUBLISH_BENCHMARKS == 'true'
shell: bash
run: mvn -B -V verify site

- name: Restore published benchmark history
if: env.PUBLISH_BENCHMARKS == 'true'
shell: bash
run: |
node <<'NODE'
const fs = require('fs');
const http = require('http');
const https = require('https');
const path = require('path');

const siteBase = new URL(process.env.BENCHMARK_SITE_URL || 'https://jawk.io/');
const siteRoot = 'target/site';
const resolvedSiteRoot = path.resolve(siteRoot);
const indexPath = path.join(siteRoot, 'benchmarks', 'index.json');

function getText(url) {
return new Promise(resolve => {
const client = url.protocol === 'http:' ? http : https;
const request = client.get(url, response => {
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
response.resume();
resolve(getText(new URL(response.headers.location, url)));
return;
}
if (response.statusCode < 200 || response.statusCode >= 300) {
response.resume();
resolve(null);
return;
}
response.setEncoding('utf8');
let data = '';
response.on('data', chunk => {
data += chunk;
});
response.on('end', () => resolve(data));
});
request.on('error', () => resolve(null));
request.setTimeout(15000, () => {
request.destroy();
resolve(null);
});
});
}

async function restoreFile(relativePath) {
if (!relativePath) {
return;
}
const normalizedPath = relativePath.replace(/^\/+/, '');
if (normalizedPath.includes('\\') || normalizedPath.split('/').includes('..')) {
return;
}
const content = await getText(new URL(normalizedPath, siteBase));
if (content === null) {
return;
}
const outputPath = path.resolve(resolvedSiteRoot, normalizedPath);
const outputRelativePath = path.relative(resolvedSiteRoot, outputPath);
if (outputRelativePath.startsWith('..') || path.isAbsolute(outputRelativePath)) {
return;
}
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, content);
}

(async () => {
const indexText = await getText(new URL('benchmarks/index.json', siteBase));
if (indexText === null) {
return;
}

fs.mkdirSync(path.dirname(indexPath), { recursive: true });
fs.writeFileSync(indexPath, indexText);

let index;
try {
index = JSON.parse(indexText);
} catch (error) {
return;
}

for (const release of index.releases || []) {
await restoreFile(release.jmh);
await restoreFile(release.environment);
}
})().catch(error => {
console.log(`Benchmark history restore failed: ${error.message}`);
});
NODE

- name: Add benchmark JSON to Maven site
if: env.PUBLISH_BENCHMARKS == 'true'
shell: bash
run: |
release_dir="target/site/benchmarks/releases/${BENCHMARK_VERSION_PATH}"
mkdir -p "${release_dir}"
cp target/benchmarks/jmh-results.json "${release_dir}/jmh-results.json"
cp target/benchmarks/environment.json "${release_dir}/environment.json"

node <<'NODE'
const fs = require('fs');
const path = require('path');

const indexPath = 'target/site/benchmarks/index.json';
const version = process.env.BENCHMARK_VERSION;
const versionPath = process.env.BENCHMARK_VERSION_PATH;
const environment = JSON.parse(fs.readFileSync(`target/site/benchmarks/releases/${versionPath}/environment.json`, 'utf8'));
let index = { latest: null, releases: [] };

if (fs.existsSync(indexPath)) {
try {
index = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
} catch (error) {
index = { latest: null, releases: [] };
}
}

const entry = {
version,
versionPath,
date: environment.runDate.substring(0, 10),
commit: environment.commit,
workflowRunUrl: environment.workflowRunUrl,
jmh: path.posix.join('benchmarks', 'releases', versionPath, 'jmh-results.json'),
environment: path.posix.join('benchmarks', 'releases', versionPath, 'environment.json')
};

index.releases = (index.releases || []).filter(release => release.versionPath !== versionPath);
index.releases.unshift(entry);
index.latest = version;

fs.mkdirSync(path.dirname(indexPath), { recursive: true });
fs.writeFileSync(indexPath, `${JSON.stringify(index, null, 2)}\n`);
NODE

- name: Configure GitHub Pages
if: env.PUBLISH_BENCHMARKS == 'true'
uses: actions/configure-pages@v6

- name: Upload GitHub Pages artifact
if: env.PUBLISH_BENCHMARKS == 'true'
uses: actions/upload-pages-artifact@v4
with:
path: target/site

deploy:
name: Deploy benchmark site
needs: benchmarks
if: needs.benchmarks.outputs.publish == 'true'
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}

steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5
13 changes: 13 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ mvn site

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/).

## Benchmarks

Microbenchmarks use [JMH](https://openjdk.org/projects/code-tools/jmh/) and are built only when the benchmark profile is enabled:

```bash
mvn -Pbenchmark -DskipTests package
java -jar target/jawk-<VERSION>-benchmarks.jar JRTCompare2Benchmark
```

Release benchmark data is published by the *Publish Benchmarks* GitHub Action. It builds the Maven site, writes JMH
JSON files under `target/site/benchmarks/releases/<VERSION>/`, updates `target/site/benchmarks/index.json`, and
deploys the complete site through GitHub Pages.

## Development workflows

Please follow this workflow to contribute to this project:
Expand Down
Loading