Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save ivanfioravanti/11cd1e41472da5993273042b9b0566d5 to your computer and use it in GitHub Desktop.

Select an option

Save ivanfioravanti/11cd1e41472da5993273042b9b0566d5 to your computer and use it in GitHub Desktop.
parakeet.cpp Apple Metal benchmark: M5 Max vs M3 Ultra vs M4 (q4_k, speech.wav)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>parakeet.cpp — Apple Metal · M5 Max vs M3 Ultra vs M4</title>
<style>
:root{
--ink:#0a0d13;
--ink-2:#0d1118;
--panel:#10151e;
--panel-2:#131a25;
--line:#1d2531;
--line-soft:#161d28;
--text:#e9eef6;
--muted:#7d8a9c;
--faint:#566275;
/* colorblind-safe triad: azure / amber / orchid (~110deg apart, distinct across CVD types) */
--m5:#3b9eff;
--m5-deep:#1c6fd6;
--m4:#ff9e1b;
--m4-deep:#c87400;
--u3:#e07fc4;
--u3-deep:#b3559a;
--cpu:#9aa7b8;
--grid:#1a2230;
}
*{box-sizing:border-box}
html{scroll-behavior:smooth}
body{
margin:0;
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
background:var(--ink);
color:var(--text);
line-height:1.55;
font-size:15px;
-webkit-font-smoothing:antialiased;
background-image:
radial-gradient(1200px 600px at 80% -10%, rgba(59,158,255,.10), transparent 60%),
radial-gradient(900px 500px at -10% 0%, rgba(255,158,27,.06), transparent 55%),
linear-gradient(var(--line-soft) 1px, transparent 1px),
linear-gradient(90deg, var(--line-soft) 1px, transparent 1px);
background-size:auto, auto, 46px 46px, 46px 46px;
background-position:0 0, 0 0, -1px -1px, -1px -1px;
}
::selection{background:rgba(59,158,255,.3);color:#fff}
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}
.wrap{max-width:1080px;margin:0 auto;padding:3rem 1.5rem 5rem}
/* ---------- masthead ---------- */
header{position:relative;margin-bottom:2.5rem}
.kicker{
font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;
font-size:.7rem;font-weight:600;letter-spacing:.32em;
text-transform:uppercase;color:var(--m5);
display:flex;align-items:center;gap:.7rem;margin-bottom:1rem;
}
.kicker::before{content:"";width:34px;height:1px;background:var(--m5);box-shadow:0 0 8px var(--m5)}
h1{
font-weight:900;letter-spacing:-.03em;line-height:.98;margin:0;
font-size:clamp(2.1rem,5vw,3.5rem);
}
h1 .v{color:var(--faint);font-weight:800}
h1 .a{color:var(--m4)}
h1 .b{color:var(--m5)}
h1 .u{color:var(--u3)}
.lede{
color:var(--muted);font-size:1.02rem;max-width:64ch;margin:1.1rem 0 0;
}
.lede strong{color:var(--text);font-weight:600}
.lede .hl{color:var(--m5);font-weight:600}
.meta-strip{
display:flex;flex-wrap:wrap;gap:.45rem;margin-top:1.4rem;
font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.7rem;
}
.tag{
padding:.32rem .7rem;border-radius:999px;border:1px solid var(--line);
color:var(--faint);background:rgba(255,255,255,.012);letter-spacing:.02em;
}
.tag b{color:var(--text);font-weight:600}
.tag.m5{border-color:rgba(59,158,255,.45);color:var(--m5)}
.tag.m4{border-color:rgba(255,158,27,.4);color:var(--m4)}
.tag.u3{border-color:rgba(224,127,196,.45);color:var(--u3)}
a{color:var(--m5);text-decoration:none;border-bottom:1px solid rgba(59,158,255,.3)}
a:hover{border-color:var(--m5)}
/* ---------- KPI readouts ---------- */
.readouts{
display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));
gap:1px;margin:2.5rem 0;background:var(--line);
border:1px solid var(--line);border-radius:16px;overflow:hidden;
}
.readout{
background:linear-gradient(180deg,var(--panel-2),var(--panel));
padding:1.5rem 1.4rem;position:relative;overflow:hidden;
}
.readout::after{
content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--accent,var(--m5));
box-shadow:0 0 14px var(--accent,var(--m5));opacity:.9;
}
.readout .rl{
font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.66rem;letter-spacing:.16em;
text-transform:uppercase;color:var(--faint);
}
.readout .rv{
font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-weight:800;line-height:1;
font-size:2.7rem;margin:.55rem 0 .25rem;color:var(--text);letter-spacing:-.02em;
}
.readout .rv .x{font-size:1.3rem;color:var(--accent,var(--m5));margin-left:.05em}
.readout .rs{font-size:.78rem;color:var(--muted)}
/* ---------- sections ---------- */
section{margin:3.2rem 0}
.sec-head{display:flex;align-items:baseline;gap:1rem;margin-bottom:1.4rem}
.sec-head h2{font-size:1.15rem;font-weight:800;letter-spacing:-.01em;margin:0}
.sec-head .idx{
font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.72rem;color:var(--m5);
border:1px solid var(--line);border-radius:6px;padding:.15rem .5rem;
}
.sec-head .rule{flex:1;height:1px;background:var(--line)}
.panel{
background:linear-gradient(180deg,var(--panel-2),var(--panel));
border:1px solid var(--line);border-radius:16px;
padding:1.5rem 1.6rem 1.7rem;margin:1.1rem 0;position:relative;
}
.panel-grid{display:grid;grid-template-columns:1fr 1fr;gap:1.1rem}
@media(max-width:760px){.panel-grid{grid-template-columns:1fr}}
.panel h3{font-size:.95rem;font-weight:700;margin:0 0 .25rem;letter-spacing:-.01em}
.panel .ph-sub{
font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.7rem;color:var(--faint);
margin:0 0 1.1rem;letter-spacing:.02em;
}
.legend{
display:flex;flex-wrap:wrap;gap:1.2rem;margin-top:1rem;
font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.72rem;color:var(--muted);
}
.legend span{display:flex;align-items:center;gap:.5rem}
.legend i{width:14px;height:8px;border-radius:2px;display:inline-block}
.chart-svg{width:100%;height:auto;display:block;overflow:visible}
/* ---------- speedup cards ---------- */
.speed-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(112px,1fr));gap:.7rem}
.speed-card{
background:var(--ink-2);border:1px solid var(--line);border-radius:12px;
padding:.85rem .6rem;text-align:center;transition:transform .2s,border-color .2s;
}
.speed-card:hover{transform:translateY(-3px);border-color:var(--m5)}
.speed-card .sm{
font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.62rem;color:var(--muted);
line-height:1.25;min-height:2.5em;display:flex;align-items:center;justify-content:center;
}
.ring{
width:62px;height:62px;margin:.55rem auto .15rem;border-radius:50%;
display:flex;align-items:center;justify-content:center;position:relative;
background:conic-gradient(#3b9eff calc(var(--pct,0)*1%),#1d2531 0);
transition:background .9s cubic-bezier(.2,.7,.2,1);
}
.ring::before{content:"";position:absolute;inset:5px;border-radius:50%;background:var(--ink-2)}
.ring b{position:relative;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-weight:800;font-size:.82rem;color:var(--text)}
/* ---------- table ---------- */
.table-scroll{overflow-x:auto;border-radius:14px;border:1px solid var(--line)}
table{width:100%;border-collapse:collapse;font-size:.8rem;min-width:760px}
thead th{
font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-weight:600;font-size:.62rem;
letter-spacing:.08em;text-transform:uppercase;color:var(--faint);
padding:.8rem .6rem;text-align:right;background:var(--ink-2);
border-bottom:1px solid var(--line);position:sticky;top:0;
}
thead th:first-child{text-align:left}
thead th.g-m4{color:var(--m4)}
thead th.g-u3{color:var(--u3)}
thead th.g-m5{color:var(--m5)}
tbody td{
padding:.6rem .6rem;text-align:right;border-bottom:1px solid var(--line-soft);
font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-weight:500;
}
tbody td:first-child{text-align:left;color:var(--text);font-weight:600;letter-spacing:.01em}
tbody tr:last-child td{border-bottom:none}
tbody tr{transition:background .15s}
tbody tr:hover{background:rgba(59,158,255,.05)}
.c-m4{color:var(--m4)}
.c-u3{color:var(--u3)}
.c-m5{color:var(--m5);font-weight:700}
.c-mut{color:var(--faint)}
.pill{display:inline-block;padding:.1rem .42rem;border-radius:5px;font-size:.72rem;font-weight:700}
.pill.up{background:rgba(59,158,255,.13);color:var(--m5)}
.note{
font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.74rem;color:var(--muted);
border-left:2px solid var(--m4);padding:.2rem 0 .2rem 1rem;margin:1.6rem 0;line-height:1.7;
}
.note code{background:rgba(255,255,255,.06);padding:.05rem .35rem;border-radius:4px;color:var(--text)}
footer{
margin-top:4rem;padding-top:1.6rem;border-top:1px solid var(--line);
font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.72rem;color:var(--faint);
display:flex;justify-content:space-between;flex-wrap:wrap;gap:.6rem;
}
.reveal{opacity:0;transform:translateY(14px);animation:rise .7s cubic-bezier(.2,.7,.2,1) forwards}
@keyframes rise{to{opacity:1;transform:none}}
@media print{
body{background:#fff;color:#111;background-image:none}
.panel,.readout,.table-scroll{border-color:#ccc}
}
</style>
</head>
<body>
<div class="wrap">
<header class="reveal">
<div class="kicker">parakeet.cpp · apple metal benchmark</div>
<h1><span class="b">M5&nbsp;Max</span> <span class="v">vs</span> <span class="u">M3&nbsp;Ultra</span> <span class="v">vs</span> <span class="a">M4</span></h1>
<p class="lede">
Same q4_k speech models, same <span class="mono">~7.4&nbsp;s</span> clip, three Apple Silicon tiers.
The headline isn't generation — it's cores: <span class="hl">the M5 Max beats the M3 Ultra on Metal in
every model, averaging ~1.7× faster with half the GPU cores</span> (40 vs 80). Against a base M4 it's ~3×.
The encoder GEMMs are landing on the <strong>M5 GPU Neural Accelerators</strong> (Metal 4 / MPP via ggml —
not the ANE), and per-core matmul is outweighing raw core count.
</p>
<div class="meta-strip">
<span class="tag m5">M5 Max · 18C / 40G · 128GB · measured</span>
<span class="tag u3">M3 Ultra · 32C / 80G · 512GB · measured</span>
<span class="tag m4">M4 · Mac mini · 10C / 10G · 16GB · published</span>
<span class="tag">runner&nbsp;<b>bench_metal_dw.sh</b></span>
<span class="tag">quant&nbsp;<b>q4_k</b></span>
<span class="tag">metric&nbsp;<b>RTFx ↑</b></span>
</div>
</header>
<div class="readouts reveal" id="readouts" style="animation-delay:.08s"></div>
<!-- ===== chart 01 ===== -->
<section class="reveal" style="animation-delay:.12s">
<div class="sec-head">
<span class="idx mono">01</span>
<h2>Metal throughput across three tiers</h2>
<span class="rule"></span>
</div>
<div class="panel">
<h3>Metal RTFx — M4 vs M3 Ultra vs M5 Max</h3>
<p class="ph-sub">higher is faster · grouped bars per model</p>
<svg id="ch-metal" class="chart-svg" viewBox="0 0 760 560" aria-label="Metal RTFx M4 vs M3 Ultra vs M5 Max"></svg>
<div class="legend">
<span><i style="background:#ff9e1b"></i> M4 Metal</span>
<span><i style="background:#e07fc4"></i> M3 Ultra Metal</span>
<span><i style="background:#3b9eff"></i> M5 Max Metal</span>
</div>
</div>
</section>
<!-- ===== chart 02 ===== -->
<section class="reveal" style="animation-delay:.04s">
<div class="sec-head">
<span class="idx mono">02</span>
<h2>Half the cores, still ahead</h2>
<span class="rule"></span>
</div>
<div class="panel-grid">
<div class="panel">
<h3>M5 Max ÷ M3 Ultra · Metal</h3>
<p class="ph-sub">above 1.0× = M5 Max wins · per model</p>
<svg id="ch-ratio" class="chart-svg" viewBox="0 0 380 360" aria-label="M5 Max over M3 Ultra Metal"></svg>
</div>
<div class="panel">
<h3>GPU acceleration vs CPU</h3>
<p class="ph-sub">Metal ÷ CPU · all three tiers</p>
<svg id="ch-speedup" class="chart-svg" viewBox="0 0 380 360" aria-label="GPU speedup three tiers"></svg>
<div class="legend">
<span><i style="background:#ff9e1b"></i> M4</span>
<span><i style="background:#e07fc4"></i> M3 Ultra</span>
<span><i style="background:#3b9eff"></i> M5 Max</span>
</div>
</div>
</div>
</section>
<!-- ===== chart 03 ===== -->
<section class="reveal">
<div class="sec-head">
<span class="idx mono">03</span>
<h2>M5 Max · Metal ÷ CPU per model</h2>
<span class="rule"></span>
</div>
<div class="panel">
<div class="speed-grid" id="speed-grid"></div>
</div>
</section>
<!-- ===== table ===== -->
<section class="reveal">
<div class="sec-head">
<span class="idx mono">04</span>
<h2>Full results</h2>
<span class="rule"></span>
</div>
<div class="table-scroll">
<table>
<thead>
<tr>
<th>Model</th>
<th class="g-m4">M4 Metal</th><th class="g-u3">M3U Metal</th><th class="g-m5">M5 Metal</th>
<th class="g-m4">M4 CPU</th><th class="g-u3">M3U CPU</th><th class="g-m5">M5 CPU</th>
<th class="g-m4">M4 ÷CPU</th><th class="g-u3">M3U ÷CPU</th><th class="g-m5">M5 ÷CPU</th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
</div>
<p class="note">
ggml enables MPP <code>tensor_ops</code> matmul on M5 by default; the M4 and M3 Ultra baselines did not.
Requires <code>default.metallib</code> beside <code>parakeet-cli</code>.
</p>
<p class="note" style="border-left-color:var(--u3)">
Core-count vs per-core — the M3 Ultra packs <code>80</code> GPU cores against the M5 Max's <code>40</code>,
yet trails on Metal in every model. The signal points to the M5 GPU Neural Accelerators doing more matmul
per core, not the Ultra being slow. Take this as evidence for the M5 micro-architecture, not a tier ranking.
</p>
<p class="note">
Tier &amp; provenance — M4 is a base part (Mac mini, 10C/10G, 16GB) from
<a href="https://github.com/mudler/parakeet.cpp/blob/master/benchmarks/BENCHMARK.md#apple-metal-m4">BENCHMARK.md</a>;
M5 Max (128GB) and M3 Ultra (512GB) are measured here. So &ldquo;M5 ÷ M4 ≈ 3×&rdquo; blends generation
<em>and</em> tier, while &ldquo;M5 ÷ M3 Ultra&rdquo; is the cleaner cross-architecture read.
</p>
</section>
<footer>
<span><a href="https://github.com/mudler/parakeet.cpp">github.com/mudler/parakeet.cpp</a></span>
<span>Apple MPP Guide 2026 · GPU Neural Accelerators on M5</span>
</footer>
</div>
<script>
(function(){
const DATA = [
{ id:"ctc-0.6b", m4m:91.1, u3m:213.7, m5m:325.7, m4c:16.9, u3c:19.1, m5c:21.2 },
{ id:"ctc-1.1b", m4m:56.2, u3m:133.8, m5m:198.2, m4c:10.0, u3c:11.2, m5c:12.4 },
{ id:"tdt_ctc-110m", m4m:99.4, u3m:132.6, m5m:237.0, m4c:65.5, u3c:85.3, m5c:88.1 },
{ id:"rt-eou-120m", m4m:76.5, u3m:101.2, m5m:197.7, m4c:58.6, u3c:66.7, m5c:71.5 },
{ id:"rnnt-0.6b", m4m:53.0, u3m:84.3, m5m:158.1, m4c:20.3, u3c:29.8, m5c:29.1 },
{ id:"rnnt-1.1b", m4m:39.4, u3m:66.7, m5m:119.8, m4c:12.4, u3c:18.7, m5c:17.9 },
{ id:"tdt-0.6b-v2", m4m:57.1, u3m:102.0, m5m:172.1, m4c:20.1, u3c:30.2, m5c:29.2 },
{ id:"tdt-0.6b-v3", m4m:54.3, u3m:104.0, m5m:154.0, m4c:19.8, u3c:29.5, m5c:28.9 },
{ id:"tdt-1.1b", m4m:43.8, u3m:84.4, m5m:149.0, m4c:12.6, u3c:19.4, m5c:18.2 },
{ id:"tdt_ctc-1.1b", m4m:41.9, u3m:78.9, m5m:117.0, m4c:12.5, u3c:18.9, m5c:17.7 },
];
// derived
DATA.forEach(d=>{ d.m4s=d.m4m/d.m4c; d.u3s=d.u3m/d.u3c; d.m5s=d.m5m/d.m5c; });
/* Hardcoded palette — preview wrappers (htmlpreview) often break getComputedStyle + var() in SVG */
const PAL = {
text:"#e9eef6", faint:"#566275", muted:"#7d8a9c", line:"#1d2531", grid:"#1a2230",
m4:"#ff9e1b", u3:"#e07fc4", m5:"#3b9eff",
};
const avg = a => a.reduce((x,y)=>x+y,0)/a.length;
const esc = s => s.replace(/&/g,"&amp;").replace(/</g,"&lt;");
const avgVsUltra = avg(DATA.map(d=>d.m5m/d.u3m));
const avgVsM4 = avg(DATA.map(d=>d.m5m/d.m4m));
const maxM5Speed = Math.max(...DATA.map(d=>d.m5s));
const peakRTFx = Math.max(...DATA.map(d=>d.m5m));
/* ---------- KPI readouts ---------- */
const ro = [
{l:"avg M5 Max ÷ M3 Ultra · Metal", v:avgVsUltra.toFixed(1), x:"×", s:"40 GPU cores beating 80", c:"var(--m5)"},
{l:"avg M5 Max ÷ M4 · Metal", v:avgVsM4.toFixed(1), x:"×", s:"vs base M4 Mac mini", c:"var(--m5)"},
{l:"peak Metal ÷ CPU", v:maxM5Speed.toFixed(1), x:"×", s:"M5 Max · ctc-1.1b", c:"var(--m5)"},
{l:"best RTFx", v:peakRTFx.toFixed(0), x:"", s:"M5 Max · ctc-0.6b Metal", c:"var(--m5)"},
];
document.getElementById("readouts").innerHTML = ro.map(k=>`
<div class="readout" style="--accent:${k.c}">
<div class="rl">${k.l}</div>
<div class="rv">${k.v}<span class="x" style="color:${k.c}">${k.x}</span></div>
<div class="rs">${k.s}</div>
</div>`).join("");
/* ---------- grouped horizontal bars (3 tiers, Metal) ---------- */
(function(){
const el=document.getElementById("ch-metal");
const W=760,H=560, ml=104, mr=46, mt=10, mb=26;
const cw=W-ml-mr, ch=H-mt-mb;
const rowH=ch/DATA.length, maxV=340;
let s=``;
for(let t=0;t<=4;t++){
const x=ml+cw*t/4, v=maxV*t/4;
s+=`<line x1="${x}" y1="${mt}" x2="${x}" y2="${mt+ch}" stroke="${PAL.grid}" stroke-width="1"/>`;
s+=`<text x="${x}" y="${mt+ch+18}" fill="${PAL.faint}" font-size="10" font-family="ui-monospace,monospace" text-anchor="middle">${v.toFixed(0)}</text>`;
}
DATA.forEach((d,i)=>{
const bh=rowH*0.2, gap=2.5;
const groupH=bh*3+gap*2;
const y0=mt+i*rowH+(rowH-groupH)/2;
s+=`<text x="${ml-12}" y="${y0+groupH/2+3.5}" fill="${PAL.text}" font-size="11" font-weight="600" font-family="ui-monospace,monospace" text-anchor="end">${esc(d.id)}</text>`;
const series=[
{v:d.m4m, c:PAL.m4, name:"M4"},
{v:d.u3m, c:PAL.u3, name:"M3 Ultra"},
{v:d.m5m, c:PAL.m5, name:"M5 Max"},
];
series.forEach((ser,si)=>{
const w=ser.v/maxV*cw, y=y0+si*(bh+gap), dly=(i*3+si)*0.035;
s+=`<rect x="${ml}" y="${y}" width="${w}" height="${bh}" rx="2.5" fill="${ser.c}" opacity="0">
<title>${esc(d.id)} · ${ser.name} Metal: ${ser.v} RTFx</title>
<animate attributeName="width" from="0" to="${w}" dur="0.7s" begin="${dly}s" fill="freeze" calcMode="spline" keySplines="0.2 0.7 0.2 1" keyTimes="0;1" values="0;${w}"/>
<animate attributeName="opacity" from="0" to="1" dur="0.3s" begin="${dly}s" fill="freeze"/>
</rect>`;
s+=`<text x="${ml+w+5}" y="${y+bh*0.82}" fill="${ser.c}" font-size="9" font-weight="700" font-family="ui-monospace,monospace" opacity="0">
${ser.v.toFixed(0)}<animate attributeName="opacity" from="0" to="1" dur="0.3s" begin="${dly+0.5}s" fill="freeze"/></text>`;
});
});
el.innerHTML=s;
})();
/* ---------- vertical bars helper ---------- */
function vbars(id, opts){
const el=document.getElementById(id);
const W=380,H=360, ml=34, mr=10, mt=18, mb=66;
const cw=W-ml-mr, ch=H-mt-mb, slot=cw/DATA.length, maxV=opts.maxV;
let s=``;
for(let t=0;t<=4;t++){
const y=mt+ch-ch*t/4, v=maxV*t/4;
s+=`<line x1="${ml}" y1="${y}" x2="${W-mr}" y2="${y}" stroke="${PAL.grid}" stroke-width="1"/>`;
s+=`<text x="${ml-6}" y="${y+3}" fill="${PAL.faint}" font-size="9" font-family="ui-monospace,monospace" text-anchor="end">${v.toFixed(opts.dec||0)}×</text>`;
}
// optional reference line
if(opts.ref!=null){
const y=mt+ch-(opts.ref/maxV)*ch;
s+=`<line x1="${ml}" y1="${y}" x2="${W-mr}" y2="${y}" stroke="${PAL.faint}" stroke-width="1" stroke-dasharray="4 3"/>`;
s+=`<text x="${W-mr}" y="${y-4}" fill="${PAL.faint}" font-size="8.5" font-family="ui-monospace,monospace" text-anchor="end">${opts.refLabel||opts.ref+'×'}</text>`;
}
DATA.forEach((d,i)=>{
const cx=ml+(i+0.5)*slot, bars=opts.bars(d), n=bars.length, bw=slot*0.56/n;
bars.forEach((b,bi)=>{
const x=cx-(bw*n)/2+bi*bw, h=b.v/maxV*ch, y=mt+ch-h, dly=i*0.04+bi*0.02;
s+=`<rect x="${x+0.5}" y="${y}" width="${Math.max(bw-1,1)}" height="${h}" rx="2" fill="${b.c}">
<title>${esc(d.id)}: ${b.v.toFixed(1)}×</title>
<animate attributeName="height" from="0" to="${h}" dur="0.65s" begin="${dly}s" fill="freeze" calcMode="spline" keySplines="0.2 0.7 0.2 1" keyTimes="0;1" values="0;${h}"/>
<animate attributeName="y" from="${mt+ch}" to="${y}" dur="0.65s" begin="${dly}s" fill="freeze" calcMode="spline" keySplines="0.2 0.7 0.2 1" keyTimes="0;1" values="${mt+ch};${y}"/>
</rect>`;
if(n===1) s+=`<text x="${cx}" y="${y-5}" fill="${b.c}" font-size="9" font-weight="700" font-family="ui-monospace,monospace" text-anchor="middle" opacity="0">${b.v.toFixed(1)}×<animate attributeName="opacity" from="0" to="1" dur="0.3s" begin="${dly+0.45}s" fill="freeze"/></text>`;
});
s+=`<g transform="translate(${cx},${mt+ch+10}) rotate(45)"><text fill="${PAL.muted}" font-size="8.5" font-family="ui-monospace,monospace" text-anchor="start">${esc(d.id)}</text></g>`;
});
s+=`<line x1="${ml}" y1="${mt+ch}" x2="${W-mr}" y2="${mt+ch}" stroke="${PAL.line}" stroke-width="1"/>`;
el.innerHTML=s;
}
vbars("ch-ratio",{
maxV:2.2, dec:1, ref:1, refLabel:"1.0× parity",
bars:d=>[{v:d.m5m/d.u3m, c:PAL.m5}]
});
vbars("ch-speedup",{
maxV:17,
bars:d=>[{v:d.m4s,c:PAL.m4},{v:d.u3s,c:PAL.u3},{v:d.m5s,c:PAL.m5}]
});
/* ---------- speedup rings (M5 Max) ---------- */
const maxS=16;
document.getElementById("speed-grid").innerHTML = DATA.map(d=>`
<div class="speed-card">
<div class="sm">${esc(d.id)}</div>
<div class="ring" data-pct="${Math.min(100,d.m5s/maxS*100)}"><b>${d.m5s.toFixed(1)}×</b></div>
</div>`).join("");
requestAnimationFrame(()=>{
setTimeout(()=>document.querySelectorAll(".ring").forEach((r,i)=>{
setTimeout(()=>r.style.setProperty("--pct", r.dataset.pct), i*55);
}),200);
});
/* ---------- table ---------- */
document.getElementById("tbody").innerHTML = DATA.map(d=>`
<tr>
<td>${esc(d.id)}</td>
<td class="c-m4">${d.m4m.toFixed(1)}</td>
<td class="c-u3">${d.u3m.toFixed(1)}</td>
<td class="c-m5">${d.m5m.toFixed(1)}</td>
<td class="c-mut">${d.m4c.toFixed(1)}</td>
<td class="c-mut">${d.u3c.toFixed(1)}</td>
<td class="c-mut">${d.m5c.toFixed(1)}</td>
<td class="c-mut">${d.m4s.toFixed(1)}×</td>
<td class="c-u3">${d.u3s.toFixed(1)}×</td>
<td><span class="pill up">${d.m5s.toFixed(1)}×</span></td>
</tr>`).join("");
})();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment