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
100 changes: 100 additions & 0 deletions .github/workflows/ci-benchmarks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
name: JMH Benchmarks
on:
workflow_dispatch:
schedule:
# Weekly, Sunday 04:00 UTC - distinct from ci-container.yml (daily 06:00)
# and prethink.yml (Monday 06:00) so runners aren't all queued together.
- cron: '0 4 * * 0'

permissions:
contents: read # default; the benchmarks job declares write explicitly

# Only one publish to gh-pages at a time; a manual dispatch overlapping the
# scheduled run would otherwise race pushing the benchmark-data commit.
concurrency:
group: jmh-benchmarks-gh-pages
cancel-in-progress: false

env:
DEV_JDK: '21'
# Kept small enough that both benchmark suites fit in the job timeout on a
# shared GitHub-hosted runner; not intended to produce publishable numbers,
# only a consistent trend line. See exist-core-jmh/README.md and
# exist-indexes-jmh/README.md for slower, more precise local invocations.
JMH_ARGS: '-wi 2 -i 5 -f 1'

jobs:
benchmarks:
name: Run JMH benchmarks
runs-on: ubuntu-latest
timeout-minutes: 90
permissions:
contents: write # github-action-benchmark pushes the data series to gh-pages
steps:
- uses: actions/checkout@v7

- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: ${{ env.DEV_JDK }}

- uses: ./.github/actions/maven-cache

- name: Build exist-core-jmh
run: >
mvn -B install -pl exist-core-jmh -am -DskipTests
-Ddependency-check.skip=true -Ddocker=false

- name: Run exist-core-jmh benchmarks
run: >
java -jar exist-core-jmh/target/exist-core-jmh-*-benchmarks.jar
-rf json -rff exist-core-jmh/target/jmh-result.json
${{ env.JMH_ARGS }}

- name: Track exist-core-jmh results on gh-pages
if: github.repository == 'eXist-db/exist'
uses: benchmark-action/github-action-benchmark@v1
with:
name: 'exist-core-jmh'
tool: 'jmh'
output-file-path: exist-core-jmh/target/jmh-result.json
benchmark-data-dir-path: dev/bench/core
github-token: ${{ secrets.GITHUB_TOKEN }}
auto-push: true
# Report-only: flag regressions, never fail the run.
alert-threshold: '150%'
comment-on-alert: true
fail-on-alert: false
summary-always: true

- name: Build exist-indexes-jmh
run: >
mvn -B install -pl exist-indexes-jmh -am -DskipTests
-Ddependency-check.skip=true -Ddocker=false

- name: Run exist-indexes-jmh benchmarks
run: >
mvn -B exec:exec -pl exist-indexes-jmh
-Dbenchmark.args="-rf json -rff target/jmh-result.json ${{ env.JMH_ARGS }}"

- name: Track exist-indexes-jmh results on gh-pages
if: github.repository == 'eXist-db/exist'
uses: benchmark-action/github-action-benchmark@v1
with:
name: 'exist-indexes-jmh'
tool: 'jmh'
output-file-path: exist-indexes-jmh/target/jmh-result.json
benchmark-data-dir-path: dev/bench/indexes
github-token: ${{ secrets.GITHUB_TOKEN }}
auto-push: true
alert-threshold: '150%'
comment-on-alert: true
fail-on-alert: false
summary-always: true

- name: Save Maven cache
if: github.event_name == 'schedule'
uses: actions/cache/save@v6
with:
path: ~/.m2
key: maven-${{ github.ref_name }}-${{ hashFiles('**/pom.xml') }}
48 changes: 48 additions & 0 deletions exist-core-jmh/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# exist-core-jmh

JMH micro-benchmarks for `exist-core` (and the Lucene index extension, which several benchmarks here also exercise). Each class exists to make a specific optimization or regression claim defensible with numbers instead of prose — see the class-level Javadoc for the issue/PR each one is a companion to.

## What's benchmarked

| Class | Package | Measures |
|---|---|---|
| `AxisBenchmark` | `org.exist.dom.persistent` | Sibling/non-sibling axis evaluation over three corpus shapes ([#2697](https://github.com/eXist-db/exist/issues/2697)) |
| `PrecedingAxisBenchmark` | `org.exist.xquery` | `preceding::*` position-dependence on a flat 50,000-element document ([#2129](https://github.com/eXist-db/exist/issues/2129)) |
| `ArrowOperatorBenchmark` | `org.exist.xquery` | Overhead of `=>` vs. the equivalent direct call |
| `TypeSubTypeOfBenchmark` | `org.exist.xquery.value` | `Type#subTypeOf(int, int)`, the hot path behind every atomic comparison ([#6322](https://github.com/eXist-db/exist/issues/6322)) |
| `StringJoinBenchmark` | `org.exist.xquery.utils` | Java `String` join strategies |
| `URIUtilsBenchmark` | `org.exist.xquery.utils` | URI utility helpers |
| `LockTableBenchmark` | `org.exist.storage.lock` | Lock table contention |
| `ReindexBenchmark` | `org.exist.storage` | End-to-end `xmldb:reindex()` fast path ([#572](https://github.com/eXist-db/exist/issues/572)) |
| `LucenePhraseQueryBenchmark` | `org.exist.indexing.lucene` | Query-time Lucene phrase lookup over many small documents ([#2812](https://github.com/eXist-db/exist/issues/2812)) |
| `ReindexDeleteStrategyBenchmark` | `org.exist.indexing.lucene` | Reindex-time Lucene delete strategies for mixed document shapes |
| `UtilExpandHighlightingBenchmark` | `org.exist.indexing.lucene` | `util:expand` match-highlighting cost vs. highlighting off, single-hit and batch ([#5738](https://github.com/eXist-db/exist/issues/5738), [#6387](https://github.com/eXist-db/exist/issues/6387)) |

## Running

Build the module first — this also installs a fresh `exist-core` into the local Maven repo so the benchmark picks up your branch's code:

```bash
JAVA_HOME=/path/to/java-21 \
mvn install -pl exist-core-jmh -am -DskipTests \
-Ddependency-check.skip=true -Ddocker=false
```

The `package` phase shades an uber-jar with `org.openjdk.jmh.Main` as its entry point (`target/exist-core-jmh-<version>-benchmarks.jar`). Run it directly:

```bash
java -jar exist-core-jmh/target/exist-core-jmh-*-benchmarks.jar AxisBenchmark -wi 3 -i 5 -f 1
```

Useful variants:

- `... -rf json -rff target/jmh-result.json` — machine-readable output (this is what CI feeds to the gh-pages dashboard)
- `... -prof gc` — GC profile
- `... TypeSubTypeOfBenchmark.identical` — filter by regex
- No class filter runs every `@Benchmark` in the jar

`ArrowOperatorBenchmark` needs the *unshaded* classes plus runtime classpath instead of the shaded jar — the shade transformer trips a log4j2 caller-class assertion when booting a `BrokerPool`. See that class's Javadoc for the exact invocation.

## Continuous tracking

[`.github/workflows/ci-benchmarks.yml`](../.github/workflows/ci-benchmarks.yml) runs the full suite weekly (and on manual dispatch) with reduced iteration counts (`-wi 2 -i 5 -f 1` — a trend signal, not a publishable number) and publishes JSON results via [`benchmark-action/github-action-benchmark`](https://github.com/benchmark-action/github-action-benchmark) to the `gh-pages` branch, under `dev/bench/core`. Once GitHub Pages is enabled for this repository, the dashboard is served at `https://exist-db.github.io/exist/dev/bench/core/`.
Loading
Loading