name: Setup gh-pages branch

on:
  workflow_dispatch:
    inputs:
      default_commit_message:
        description: "Initial gh-pages commit message"
        required: false
        default: "chore(ci): initialize gh-pages branch for benchmark history"

permissions:
  contents: write

env:
  # Opt every JS action into Node.js 24 ahead of GitHub's June 2026 forced
  # migration; silences "Node.js 20 actions are deprecated" runner warnings.
  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

jobs:
  bootstrap:
    name: Bootstrap gh-pages
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Prepare site content
        run: |
          mkdir -p _ghpages/benchmark-data
          # Try to detect CPU MHz from the runner to enable cycles/op derivation in the viewer
          CPU_MHZ="$(awk -F: '/cpu MHz/ {print int($2+0.5); exit}' /proc/cpuinfo 2>/dev/null || true)"
          if [ -z "$CPU_MHZ" ]; then
            CPU_MHZ=""
          fi
          # Provide CPU MHz as a small JS file loaded before data/main scripts
          # (Viewer also supports BENCHMARK_DATA.meta.cpu_mhz and <meta name="cpu-mhz">)
          cat > _ghpages/cpu-mhz.js << EOF
          (function(){ try{ window.CPU_MHZ = ${CPU_MHZ:-0}; }catch(_){ } })();
          EOF
          # Lightweight viewer logic used by both the root dashboard and the benchmark-data/ index
          cat > _ghpages/main.js << 'EOF'
          (function(){
            function byId(id){return document.getElementById(id)}
            function fmt(v){
              if(typeof v==='number'){return v.toFixed(2)}
              return String(v)
            }
            function friendlyName(name){
              // Remap confusing labels for readability
              if(name === 'tick_n') return 'rate.tick_n';
              if(name === 'tick') return 'rate.tick';
              if(name === 'increment') return 'counter.inc';
              if(name === 'add') return 'counter.add';
              if(name === 'get') return 'get';
              if(name === 'set') return 'gauge.set';
              if(name === 'set_max') return 'gauge.set_max';
              if(name === 'set_min') return 'gauge.set_min';
              if(name === 'counter_access') return 'counter.access';
              if(name === 'gauge_access') return 'gauge.access';
              if(name === 'record') return 'timer.record';
              if(name === 'record_ns') return 'timer.record_ns';
              if(name === 'start_stop') return 'timer.start_stop';
              if(name === 'stats') return 'timer.stats';
              if(['1','2','4','8','16','32','64'].indexOf(String(name))>=0) return 'threads:'+name;
              return name;
            }
            function render(entries){
              if(!entries || !entries.length){ return false }
              var cont = byId('content');
              cont.innerHTML = '';
              var table = document.createElement('table');
              table.style.borderCollapse='collapse';
              table.innerHTML = '<thead><tr><th style="text-align:left;padding:6px;border-bottom:1px solid #e1e4e8">Name</th><th style="text-align:right;padding:6px;border-bottom:1px solid #e1e4e8">Value</th><th style="text-align:left;padding:6px;border-bottom:1px solid #e1e4e8">Unit</th></tr></thead>';
              var tbody = document.createElement('tbody');
              // Filter out internal helper rows
              var filtered = entries.filter(function(e){ return e.name !== 'base' && e.name !== 'change' });
              filtered.slice(-100).forEach(function(e){
                var tr = document.createElement('tr');
                tr.innerHTML = '<td style="padding:6px;border-bottom:1px solid #f0f0f0">'+friendlyName(e.name||e.benchmark||'unknown')+'</td>'+
                               '<td style="padding:6px;text-align:right;border-bottom:1px solid #f0f0f0">'+fmt(e.value||e.mean||e.score||e.result||e.time)+'</td>'+
                               '<td style="padding:6px;border-bottom:1px solid #f0f0f0">'+(e.unit||'ns/op')+'</td>';
                tbody.appendChild(tr);
              });
              table.appendChild(tbody);
              cont.appendChild(table);
              // Grouped view with base/change where available
              var section = document.createElement('div');
              section.style.marginTop = '16px';
              section.innerHTML = '<h4>Grouped View</h4>';
              var groups = { rate:[], counter:[], timer:[], gauge:[], threads:[] };
              var pending = null; // capture {base,change}
              entries.forEach(function(e){
                if(e.name==='base' || e.name==='change'){
                  pending = pending || {}; pending[e.name] = e; return;
                }
                var name = friendlyName(e.name||e.benchmark||'unknown');
                var bucket = 'other';
                if(name.indexOf('rate.')===0) bucket='rate';
                else if(name.indexOf('counter.')===0) bucket='counter';
                else if(name.indexOf('timer.')===0) bucket='timer';
                else if(name.indexOf('gauge.')===0) bucket='gauge';
                else if(name.indexOf('threads:')===0) bucket='threads';
                if(!groups[bucket]) groups[bucket]=[];
                groups[bucket].push({ item:e, name:name, meta: pending });
                pending = null;
              });
              function addGroup(title, list){
                if(!list || !list.length) return;
                var div=document.createElement('div');
                div.style.marginTop='8px';
                var html = '<div style="font-weight:600;margin:6px 0">'+title+'</div>';
                list.forEach(function(row){
                  var v = row.item;
                  html += '<div>'+row.name+' &nbsp; <span style="color:#555">'+fmt(v.value)+'</span> <span style="color:#666">'+(v.unit||'ns/op')+'</span></div>';
                  if(row.meta && (row.meta.base||row.meta.change)){
                    if(row.meta.base){ html += '<div style="margin-left:16px">— base &nbsp; <span style="color:#555">'+fmt(row.meta.base.value)+'</span> <span style="color:#666">'+(row.meta.base.unit||'ns/op')+'</span></div>'; }
                    if(row.meta.change){ html += '<div style="margin-left:16px">— change &nbsp; <span style="color:#555">'+fmt(row.meta.change.value)+'</span> <span style="color:#666">'+(row.meta.change.unit||'ns/op')+'</span></div>'; }
                  }
                });
                div.innerHTML = html; section.appendChild(div);
              }
              addGroup('Rate', groups.rate);
              addGroup('Counter', groups.counter);
              addGroup('Timer', groups.timer);
              addGroup('Gauge', groups.gauge);
              addGroup('Threads', groups.threads);
              cont.appendChild(section);
              return true;
            }
            function extractJSON(text){
              // Try pure JSON first
              try { return JSON.parse(text) } catch(_){ }
              // Try to find object {...}
              var i = text.indexOf('{'); var j = text.lastIndexOf('}');
              if(i>=0 && j>i){
                var objStr = text.slice(i, j+1);
                try { return JSON.parse(objStr) } catch(_){ }
              }
              // Try to find array [...]
              i = text.indexOf('['); j = text.lastIndexOf(']');
              if(i>=0 && j>i){
                var arrStr = text.slice(i, j+1);
                try { return { entries: JSON.parse(arrStr) } } catch(_){ }
              }
              return null;
            }
            function deriveEntries(obj){
              if(!obj) return null;
              if(Array.isArray(obj)) return obj;
              // github-action-benchmark layout: window.BENCHMARK_DATA = { entries: { SuiteName: [ { benches: [...] }, ... ] } }
              if(obj.entries){
                // If entries is an array directly
                if(Array.isArray(obj.entries)) return obj.entries;
                // If entries is an object of suites, pick Criterion or the first suite
                if(typeof obj.entries === 'object'){
                  var suite = null;
                  if(Array.isArray(obj.entries.Criterion)) suite = obj.entries.Criterion;
                  if(!suite){
                    var ks = Object.keys(obj.entries);
                    for(var i=0;i<ks.length;i++){ if(Array.isArray(obj.entries[ks[i]])){ suite = obj.entries[ks[i]]; break; } }
                  }
                  if(Array.isArray(suite)){
                    // Take the latest run (last element) and return its benches if present
                    var last = suite[suite.length-1];
                    if(last && Array.isArray(last.benches)) return last.benches;
                    return suite;
                  }
                }
              }
              if(Array.isArray(obj.bench)) return obj.bench;
              if(obj.data){
                if(Array.isArray(obj.data)) return obj.data;
                if(Array.isArray(obj.data.entries)) return obj.data.entries;
              }
              if(Array.isArray(obj.benchmarks)) return obj.benchmarks;
              return null;
            }
            function buildHistory(obj){
              // Build timeseries per benchmark name across all runs
              if(!obj || !obj.entries || typeof obj.entries!=='object') return null;
              var suite = Array.isArray(obj.entries.Criterion) ? obj.entries.Criterion : null;
              if(!suite){ var ks=Object.keys(obj.entries); for(var i=0;i<ks.length;i++){ if(Array.isArray(obj.entries[ks[i]])){ suite=obj.entries[ks[i]]; break; }}}
              if(!suite) return null;
              var series = {}; var labels = [];
              suite.forEach(function(run, runIdx){
                var ts = (run.date || (run.commit && run.commit.timestamp)) || '';
                labels.push(ts);
                // pad existing series for this run with nulls; will fill real values below
                Object.keys(series).forEach(function(k){ series[k].push(null); });
                var present = {};
                (run.benches||[]).forEach(function(b){
                  if(b.name==='base'||b.name==='change') return;
                  var name = friendlyName(b.name);
                  if(!series[name]){
                    // backfill prior runs with nulls
                    series[name] = new Array(runIdx).fill(null);
                  }
                  series[name][runIdx] = b.value;
                  present[name] = true;
                });
              });
              return { labels: labels, series: series };
            }
            function loadChartJs(cb){
              if(typeof Chart !== 'undefined'){ cb(); return; }
              var s=document.createElement('script'); s.src='https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js';
              s.onload=function(){ cb(); }; s.onerror=function(){ cb(); }; document.head.appendChild(s);
            }
            function color(idx){ var colors=['#1f77b4','#ff7f0e','#2ca02c','#d62728','#9467bd','#8c564b','#e377c2','#7f7f7f','#bcbd22','#17becf']; return colors[idx%colors.length]; }
            function renderCharts(history){
              if(!history) return;
              function mkChart(el, keys){
                var ctx = byId(el).getContext('2d');
                var datasets = keys.map(function(k, i){ return { label:k, data:history.series[k]||[], borderColor:color(i), backgroundColor:'transparent', tension:0.2, spanGaps:true, pointRadius:2 } });
                new Chart(ctx, { type:'line', data:{ labels: history.labels, datasets: datasets }, options:{ responsive:true, interaction:{ mode:'nearest', intersect:false }, scales:{ y:{ title:{ display:true, text:'ns/op' }}, x:{ ticks:{ maxRotation:45, minRotation:0 } } } } });
              }
              // Choose groups
              var counterKeys = ['counter.inc','counter.add','get'];
              var timerKeys = ['timer.record','timer.record_ns','timer.start_stop','timer.stats'];
              var rateKeys = ['rate.tick','rate.tick_n'];
              var threadKeys = Object.keys(history.series).filter(function(k){ return k.indexOf('threads:')===0; }).sort(function(a,b){ return parseInt(a.split(':')[1]) - parseInt(b.split(':')[1])});
              // Filter to only present keys
              counterKeys = counterKeys.filter(function(k){return history.series[k]});
              timerKeys = timerKeys.filter(function(k){return history.series[k]});
              rateKeys = rateKeys.filter(function(k){return history.series[k]});
              mkChart('chart-counter', counterKeys);
              mkChart('chart-timer', timerKeys);
              mkChart('chart-rate', rateKeys);
              if(threadKeys.length){ mkChart('chart-threads', threadKeys); }
            }
            function renderCards(latest){
              var el = byId('summary-cards'); if(!el) return;
              el.innerHTML='';
              var pick = ['counter.inc','counter.add','timer.record','timer.record_ns','rate.tick','rate.tick_n'];
              var map = {}; latest.forEach(function(e){ map[friendlyName(e.name)] = e; });
              pick.forEach(function(k){ if(map[k]){
                var v = map[k]; var card = document.createElement('div'); card.className='card';
                card.innerHTML = '<div class="title">'+k+'</div><div class="value">'+fmt(v.value)+'</div><div class="unit">'+(v.unit||'ns/op')+'</div>';
                el.appendChild(card);
              }});
            }
            function tryKnownGlobals(){
              var keys = Object.keys(window);
              for(var i=0;i<keys.length;i++){
                var k = keys[i];
                try{
                  var v = window[k];
                  if(v && typeof v === 'object'){
                    var entries = deriveEntries(v) || (v.data && deriveEntries(v.data));
                    if(entries && render(entries)) return true;
                  }
                }catch(_){ }
              }
              return false;
            }
            function load(){
              var onDataDir = /\/benchmark-data(\/?|\/index\.html)$/.test(location.pathname);
              var url = onDataDir ? './data.js' : './benchmark-data/data.js';
              // First try loading as a script to expose any globals the action may set
              var s = document.createElement('script');
              s.src = url + (url.indexOf('?')===-1 ? '?' : '&') + 't=' + Date.now();
              s.onload = function(){
                if(tryKnownGlobals()){
                  // If BENCHMARK_DATA exists, build charts and cards
                  try{
                    var data = window.BENCHMARK_DATA; 
                    var latest = deriveEntries(data);
                    if(latest) { renderCards(latest); }
                    var history = buildHistory(data);
                    loadChartJs(function(){ try { if(typeof Chart !== 'undefined') { renderCharts(history); } } catch(_){} });
                  }catch(_){ }
                  return;
                }
                // Fallback: fetch and attempt to parse as JSON-ish
                fetch(url, { cache: 'no-store' })
                  .then(function(r){ if(!r.ok) throw new Error(''+r.status); return r.text() })
                  .then(function(txt){
                    var parsed = extractJSON(txt);
                    var entries = deriveEntries(parsed);
                    if(entries){ renderCards(entries); }
                    if(!render(entries)) throw new Error('No entries parsed');
                    // Try to build history if full object is available
                    if(parsed && parsed.entries){ var hist = buildHistory(parsed); loadChartJs(function(){ if(typeof Chart !== 'undefined'){ renderCharts(hist); } }); }
                  })
                  .catch(function(){
                    var c = byId('content');
                    var href = onDataDir ? '.' : './benchmark-data/';
                    if(c) c.innerHTML = '<p class="note">No recognizable benchmark data found yet. Browse <a href="'+href+'">raw files</a>.</p>';
                  });
              };
              s.onerror = function(){
                var c = byId('content');
                var href = onDataDir ? '.' : './benchmark-data/';
                if(c) c.innerHTML = '<p class="note">No recognizable benchmark data found yet. Browse <a href="'+href+'">raw files</a>.</p>';
              };
              document.head.appendChild(s);
            }
            window.addEventListener('DOMContentLoaded', load);
          })();
          EOF
          cat > _ghpages/benchmark-data/index.html << 'EOF'
          <!doctype html>
          <html lang="en">
          <head>
            <meta charset="utf-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1" />
            <title>Benchmark Data Directory</title>
            <style>
              body{font:16px/1.5 -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;margin:2rem;}
              a{color:#0366d6;text-decoration:none}
              a:hover{text-decoration:underline}
              .note{color:#555}
              table{width:100%;margin-top:1rem}
            </style>
          </head>
          <body>
            <h1>Benchmark Data</h1>
            <p class="note">Auto-generated by CI (<code>github-action-benchmark</code>). If data is present, a simple table will render below.</p>
            <div id="content">
              <p class="note">No data parsed yet. If you recently pushed, please refresh in a minute.</p>
            </div>
            <script src="../cpu-mhz.js"></script>
            <script src="../main.js"></script>
            <p>Return to the <a href="../">dashboard</a>.</p>
          </body>
          </html>
          EOF
          cat > _ghpages/README.md << 'EOF'
          # Benchmark Data

          This branch stores benchmark history for github-action-benchmark.

          - Raw data: `benchmark-data/`
          - Dashboard: `index.html`
          EOF
          cat > _ghpages/index.html << 'EOF'
          <!doctype html>
          <html lang="en">
          <head>
            <meta charset="utf-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1" />
            <title>metrics-lib Benchmark Dashboard</title>
            <style>
              body{font:16px/1.5 -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;margin:2rem;}
              h1{font-size:1.6rem;margin-bottom:0.5rem; padding-bottom: 0.3rem; border-bottom: 1px solid #dedede;}
              .note{color:#555}
              ul{line-height:1.8}
              code{background:#f6f8fa;padding:2px 4px;border-radius:4px}
              a{color:#0366d6;text-decoration:none}
              a:hover{text-decoration:underline}
              .cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:16px;margin:1rem 0}
              .card{border:1px solid #e1e4e8;border-radius:8px;padding:12px}
              .title{font-weight:600;margin-bottom:4px}
              .value{font-size:1.2rem}
              .unit{font-size:0.9rem;color:#666}
              .grid{display:grid;grid-template-columns:1fr;gap:24px;margin-top:1rem}
              @media(min-width:960px){.grid{grid-template-columns:1fr 1fr}}
              .panel{border:1px solid #e1e4e8;border-radius:8px;padding:12px}
              canvas{max-width:100%;height:240px}
            </style>
            <!-- Load Chart.js (no SRI to avoid hash mismatches on CDN updates) -->
            <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
          </head>
          <body>
            <h1 align="center">
              <img width="90px" height="auto" src="https://raw.githubusercontent.com/jamesgober/rust-collection/72baabd71f00e14aa9184efcb16fa3deddda3a0a/assets/rust-logo.svg" alt="JG Rust Logo">
              <br>
              <b>metrics-lib</b>
              <br>
              <sup>Benchmark Dashboard</sup>
            </h1>
            <p class="note">This page hosts benchmark history produced by <code>github-action-benchmark</code>.</p>
            <ul>
              <li>Raw data directory: <a href="./benchmark-data/">benchmark-data/</a></li>
              <li>Project README: <a href="https://github.com/jamesgober/metrics-lib#benchmarks">Benchmarks section</a></li>
              <li>Docs: <a href="https://github.com/jamesgober/metrics-lib/blob/main/docs/benchmarks/README.md">docs/benchmarks/README.md</a></li>
            </ul>
            <!-- Trend callout appears here; viewer will populate or auto-insert if missing -->
            <div id="trend-note" class="note" style="margin: 8px 0;"></div>
            <div class="cards" id="summary-cards"></div>
            <div class="grid">
              <div class="panel"><h3>Counter ops (ns/op)</h3><canvas id="chart-counter"></canvas></div>
              <div class="panel"><h3>Timer ops (ns/op)</h3><canvas id="chart-timer"></canvas></div>
              <div class="panel"><h3>Rate ops (ns/op)</h3><canvas id="chart-rate"></canvas></div>
              <div class="panel"><h3>Threads scaling (ns/op)</h3><canvas id="chart-threads"></canvas></div>
            </div>
            <div id="content" style="margin-top:24px">
              <h3>Latest Run (table)</h3>
              <p class="note">Rendered from the latest entry in <code>benchmark-data/data.js</code>.</p>
            </div>
            <script src="./cpu-mhz.js"></script>
            <script src="./main.js"></script>
          </body>
          </html>
          EOF
          touch _ghpages/.nojekyll

      - name: Publish to gh-pages (force orphan)
        uses: peaceiris/actions-gh-pages@v4
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: _ghpages
          publish_branch: gh-pages
          force_orphan: true
          enable_jekyll: false
          commit_message: ${{ inputs.default_commit_message }}
