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>, &hellip;) 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&ndash;G/H&ndash;Z, A&ndash;D/E&ndash;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/&lt;suite&gt;/&lt;mode&gt;/data.js</code> &middot;
+    Generated by <a href="https://github.com/benchmark-action/github-action-benchmark">github-action-benchmark</a> &middot;
+    <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>