You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2026/05/15 10:45:20 UTC
(groovy) branch master updated: minor refactor: jmh summary graph
This is an automated email from the ASF dual-hosted git repository.
paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/master by this push:
new 704d8652b3 minor refactor: jmh summary graph
704d8652b3 is described below
commit 704d8652b3c5016f4b0a54c76028f6dfdb21c798
Author: Paul King <pa...@asert.com.au>
AuthorDate: Fri May 15 20:45:10 2026 +1000
minor refactor: jmh summary graph
---
.github/workflows/groovy-jmh-daily.yml | 31 +++
subprojects/performance/dashboard/jmh-summary.html | 277 +++++++++++++++++++++
2 files changed, 308 insertions(+)
diff --git a/.github/workflows/groovy-jmh-daily.yml b/.github/workflows/groovy-jmh-daily.yml
index f6c8107e49..ba4fc53255 100644
--- a/.github/workflows/groovy-jmh-daily.yml
+++ b/.github/workflows/groovy-jmh-daily.yml
@@ -115,3 +115,34 @@ jobs:
with:
name: reports-jmh-${{ matrix.suite }}-${{ matrix.indy }}
path: subprojects/performance/build/results/jmh/
+
+ # Single post-matrix job (avoids 10 matrix jobs racing on a gh-pages push):
+ # after every suite/mode has published its data.js, install the cross-suite
+ # summary page + its vendored JS into dev/bench/jmh/ on gh-pages.
+ publish-summary:
+ needs: benchmark
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write # only this job needs gh-pages write
+ steps:
+ - uses: actions/checkout@v6
+ - name: Install JMH summary page
+ run: |
+ set -euo pipefail
+ SRC_DIR=subprojects/performance/dashboard
+ DEST_DIR=dev/bench/jmh
+ git fetch origin gh-pages
+ git worktree add /tmp/gh-pages gh-pages
+ mkdir -p "/tmp/gh-pages/$DEST_DIR/vendor"
+ cp "$SRC_DIR/jmh-summary.html" "/tmp/gh-pages/$DEST_DIR/summary.html"
+ rsync -a --delete "$SRC_DIR/vendor/" "/tmp/gh-pages/$DEST_DIR/vendor/"
+ cd /tmp/gh-pages
+ git add "$DEST_DIR/summary.html" "$DEST_DIR/vendor"
+ if git diff --cached --quiet; then
+ echo 'JMH summary page and vendored assets already up to date.'
+ else
+ git config user.name 'github-actions[bot]'
+ git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
+ git commit -m 'Update JMH performance summary page'
+ git push origin gh-pages
+ fi
diff --git a/subprojects/performance/dashboard/jmh-summary.html b/subprojects/performance/dashboard/jmh-summary.html
new file mode 100644
index 0000000000..936549f2c3
--- /dev/null
+++ b/subprojects/performance/dashboard/jmh-summary.html
@@ -0,0 +1,277 @@
+<!DOCTYPE html>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<title>Groovy JMH Performance Summary</title>
+<style>
+ :root { color-scheme: light; }
+ body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+ margin: 2em auto;
+ max-width: 1200px;
+ padding: 0 1em;
+ color: #222;
+ background: #fff;
+ }
+ h1 { font-weight: 500; margin-bottom: 0.25em; }
+ h2 { font-weight: 500; margin: 1.5em 0 0.25em; }
+ .intro { color: #444; line-height: 1.45; margin-bottom: 1.5em; max-width: 78ch; }
+ .chart-wrap { position: relative; height: 420px; margin-bottom: 1.5em; }
+ .meta { color: #666; font-size: 0.9em; margin-top: 1em; line-height: 1.5; }
+ .meta a { color: #4A8CC2; text-decoration: none; }
+ .meta a:hover { text-decoration: underline; }
+ code { background: #f5f5f7; padding: 0.05em 0.3em; border-radius: 3px; font-size: 0.92em; }
+ .empty { color: #888; font-style: italic; }
+</style>
+</head>
+<body>
+<h1>Groovy JMH performance summary (normalised against trailing 90-day baseline)</h1>
+<p class="intro">
+ For every benchmark, the value from each run is divided by the mean of that
+ same benchmark's values over the preceding 90 days (its rolling "normal").
+ Benchmarks reported as <code>ops/ms</code> are kept as-is; benchmarks reported
+ in time-per-op units (<code>ms/op</code>, <code>us/op</code>, …) are
+ inverted, so on both charts <strong>higher is always faster</strong> and
+ <code>1.0</code> means "in line with the last 90 days". Each line is the
+ <em>geometric</em> mean of those per-benchmark ratios across a suite. The
+ <code>core</code> and <code>grails</code> suites each recombine their two
+ CI-split halves (A–G/H–Z, A–D/E–Z); <code>indy</code>
+ and <code>classic</code> are kept on separate charts.
+</p>
+
+<h2>Invokedynamic (<code>indy</code>)</h2>
+<div class="chart-wrap"><canvas id="chartIndy"></canvas></div>
+
+<h2>Classic (non-<code>indy</code>)</h2>
+<div class="chart-wrap"><canvas id="chartClassic"></canvas></div>
+
+<div class="meta">
+ <div id="lastUpdate"></div>
+ <div id="status"></div>
+ <div>
+ Per-suite raw data under <code>dev/bench/jmh/<suite>/<mode>/data.js</code> ·
+ Generated by <a href="https://github.com/benchmark-action/github-action-benchmark">github-action-benchmark</a> ·
+ <a href="../../../">Apache Groovy benchmark home</a>
+ </div>
+</div>
+
+<!-- Vendored locally (see vendor/README) instead of a CDN: avoids a third-party
+ runtime dependency / supply-chain surface for the published dashboard. -->
+<script src="vendor/chart.umd.min.js"></script>
+<script src="vendor/chartjs-adapter-date-fns.bundle.min.js"></script>
+<script>
+(function () {
+ const DAY = 864e5;
+ const WINDOW = 90 * DAY; // trailing baseline window
+
+ // Logical suite -> the CI-split data.js files that make it up. The A-G/H-Z
+ // and A-D/E-Z splits exist only to cut CI wall-clock; they are recombined here.
+ const SUITES = [
+ { key: 'bench', label: 'bench', color: '#1F77B4', parts: ['bench'] },
+ { key: 'core', label: 'core', color: '#2CA02C', parts: ['core-ag', 'core-hz'] },
+ { key: 'grails', label: 'grails', color: '#FF7F0E', parts: ['grails-ad', 'grails-ez'] },
+ ];
+ const MODES = [
+ { key: 'indy', canvas: 'chartIndy' },
+ { key: 'classic', canvas: 'chartClassic' },
+ ];
+
+ function parseDataJs(text) {
+ // Files are `window.BENCHMARK_DATA = {...};` — strip wrapper, parse as JSON
+ // (avoids clobbering a global when loading 10 of them).
+ const json = text.replace(/^\s*window\.BENCHMARK_DATA\s*=\s*/, '').replace(/;\s*$/, '');
+ return JSON.parse(json);
+ }
+
+ async function loadPart(part, mode) {
+ const resp = await fetch(part + '/' + mode + '/data.js', { cache: 'no-cache' });
+ if (!resp.ok) throw new Error(part + '/' + mode + ': HTTP ' + resp.status);
+ const data = parseDataJs(await resp.text());
+ return { data, runs: (data.entries && data.entries.Benchmark) || [] };
+ }
+
+ // Merge one or more split files into a single timeline keyed by commit id;
+ // a logical run's bench set is the union of the parts available for that commit.
+ function mergedRuns(parts) {
+ const byCommit = new Map();
+ for (const { runs } of parts) {
+ for (const r of runs) {
+ const id = r.commit && r.commit.id;
+ if (!id) continue;
+ let e = byCommit.get(id);
+ if (!e) { e = { date: r.date, commit: r.commit, benches: {} }; byCommit.set(id, e); }
+ e.date = Math.max(e.date, r.date || 0);
+ for (const b of (r.benches || [])) {
+ if (b && b.value > 0) e.benches[b.name] = { value: b.value, unit: b.unit || '' };
+ }
+ }
+ }
+ return [...byCommit.values()].sort((a, b) => a.date - b.date);
+ }
+
+ // ops/ms (and ops/s, ...) => higher is faster; anything per-op (ms/op,
+ // us/op, ns/op, s/op) => lower is faster, so invert to keep "higher = faster".
+ function normalize(value, baseline, unit) {
+ const higherBetter = /^ops\b/.test(unit) || unit.indexOf('ops/') === 0;
+ return higherBetter ? value / baseline : baseline / value;
+ }
+
+ function suiteSeries(runs) {
+ const points = [];
+ for (let i = 0; i < runs.length; i++) {
+ const cur = runs[i];
+ const logs = [];
+ for (const name in cur.benches) {
+ const b = cur.benches[name];
+ const hist = [];
+ for (let j = 0; j < i; j++) {
+ const pr = runs[j];
+ if (cur.date - pr.date > WINDOW) continue; // outside trailing window
+ const pb = pr.benches[name];
+ if (pb && pb.value > 0) hist.push(pb.value);
+ }
+ if (!hist.length || !(b.value > 0)) continue; // need history + a value
+ const base = hist.reduce((s, v) => s + v, 0) / hist.length;
+ const ratio = normalize(b.value, base, b.unit);
+ if (ratio > 0 && isFinite(ratio)) logs.push(Math.log(ratio));
+ }
+ if (!logs.length) continue;
+ const geo = Math.exp(logs.reduce((s, v) => s + v, 0) / logs.length);
+ points.push({
+ x: new Date(cur.date),
+ y: geo,
+ n: logs.length,
+ commit: cur.commit && cur.commit.id ? cur.commit.id.slice(0, 7) : '',
+ commitUrl: cur.commit && cur.commit.url,
+ });
+ }
+ return points;
+ }
+
+ const baselineLine = {
+ id: 'baselineLine',
+ afterDatasetsDraw(chart) {
+ const { ctx, chartArea: { left, right }, scales: { y } } = chart;
+ const yPx = y.getPixelForValue(1);
+ ctx.save();
+ ctx.strokeStyle = 'rgba(0,0,0,0.35)';
+ ctx.setLineDash([4, 4]);
+ ctx.lineWidth = 1;
+ ctx.beginPath();
+ ctx.moveTo(left, yPx);
+ ctx.lineTo(right, yPx);
+ ctx.stroke();
+ ctx.restore();
+ }
+ };
+
+ function renderChart(canvasId, datasets) {
+ const ctx = document.getElementById(canvasId).getContext('2d');
+ new Chart(ctx, {
+ type: 'line',
+ data: { datasets },
+ plugins: [baselineLine],
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ interaction: { mode: 'index', intersect: false },
+ scales: {
+ x: {
+ type: 'time',
+ time: { unit: 'day', tooltipFormat: 'yyyy-MM-dd' },
+ title: { display: true, text: 'Run date' },
+ },
+ y: {
+ title: { display: true, text: 'speed vs trailing 90-day baseline (higher = faster)' },
+ grid: { color: 'rgba(0,0,0,0.06)' },
+ },
+ },
+ plugins: {
+ legend: { position: 'top' },
+ tooltip: {
+ callbacks: {
+ title(items) {
+ const r = items[0] && items[0].raw;
+ const d = new Date(items[0].parsed.x).toISOString().slice(0, 10);
+ return r && r.commit ? r.commit + ' ' + d : d;
+ },
+ label(item) {
+ const r = item.raw;
+ return item.dataset.label + ': ' + item.parsed.y.toFixed(3) +
+ ' x (' + r.n + ' benchmarks)';
+ },
+ },
+ },
+ },
+ },
+ });
+ }
+
+ async function build() {
+ let lastUpdate = 0;
+ let anyData = false;
+ for (const mode of MODES) {
+ const datasets = [];
+ for (const suite of SUITES) {
+ let parts;
+ try {
+ parts = await Promise.all(suite.parts.map(p => loadPart(p, mode.key)));
+ } catch (e) {
+ // A missing/failed part just drops that suite line for this mode.
+ continue;
+ }
+ for (const p of parts) {
+ if (p.data && p.data.lastUpdate) lastUpdate = Math.max(lastUpdate, p.data.lastUpdate);
+ }
+ const points = suiteSeries(mergedRuns(parts));
+ if (!points.length) continue;
+ anyData = true;
+ datasets.push({
+ label: suite.label,
+ borderColor: suite.color,
+ backgroundColor: suite.color,
+ pointRadius: 2,
+ pointHoverRadius: 5,
+ borderWidth: 2,
+ tension: 0.15,
+ spanGaps: true,
+ data: points,
+ });
+ }
+ renderChart(mode.canvas, datasets);
+ }
+
+ if (!anyData) {
+ document.getElementById('status').textContent =
+ 'No benchmark data yet — a benchmark needs at least one run of prior ' +
+ 'history within the trailing window before it contributes a point.';
+ }
+ if (lastUpdate) {
+ document.getElementById('lastUpdate').textContent =
+ 'Last update: ' + new Date(lastUpdate).toISOString().replace('T', ' ').slice(0, 19) + ' UTC';
+ }
+ }
+
+ build().catch(err => {
+ document.getElementById('status').textContent = 'Failed to load benchmark data: ' + err.message;
+ });
+})();
+</script>
+</body>
+</html>