|
200 | 200 |
|
201 | 201 | /* Across Rubies: one card per benchmark, a chart with the Rubies on the x axis. */ |
202 | 202 | .rcard { border-bottom: 1px solid var(--line); padding-block: 18px; display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; } |
203 | | -.rcard h3 { margin: 0; font-size: 15.5px; line-height: 1.35; color: var(--ink); text-wrap: balance; } |
204 | | -.rcard h3 code { font-weight: 400; } |
| 203 | +.rcard .what { margin: 0; font-size: 13px; color: var(--muted); } |
| 204 | +.rcard h3 { margin: 0; font-size: 18px; line-height: 1.35; color: var(--ink); display: grid; grid-template-columns: 26px minmax(0, 1fr); gap: 10px; align-items: start; } |
| 205 | +.rcard h3 > span:last-child { text-wrap: pretty; overflow-wrap: anywhere; } |
| 206 | +.rcard h3 code { font-weight: 700; font-size: .85em; } |
| 207 | +.rcard h3 .ico { margin-top: 1px; } |
| 208 | +.ico.mix { background: #fff; color: var(--red); box-shadow: inset 0 0 0 3px var(--red); } |
| 209 | +.rcard .say { padding-left: 36px; } |
| 210 | +.rcard td .lose { color: var(--red); } |
205 | 211 | .rcard .say { margin: 0; font-size: 14px; color: var(--text); max-width: 75ch; } |
206 | 212 | .rcard .say b { color: var(--ink); } |
207 | 213 | .rlegend { display: flex; flex-wrap: wrap; gap: 4px 16px; font-size: 13px; color: var(--text); } |
@@ -669,34 +675,88 @@ <h2 class="bh">FastRuby<span>.io</span> keeps fast-ruby running</h2> |
669 | 675 | const day = M.shards.length ? M.shards[0].from.slice(0, 10) : ""; |
670 | 676 | $("rrun").innerHTML = `Results from ${runLink}${at}, ${esc(day)}: ${M.labels.length} builds on ${M.shards.length} machine${M.shards.length === 1 ? "" : "s"}, built ${esc(M.built_at)}.`; |
671 | 677 |
|
672 | | - function summary(b) { |
673 | | - const plain = M.labels.filter((l) => !variant(l)); |
674 | | - let wins = 0, ties = 0, ran = 0; |
675 | | - const ratios = []; |
676 | | - plain.forEach((l) => { |
677 | | - const r = b.runs[l]; |
678 | | - if (!r || !ok(r.entries[0])) return; |
679 | | - const c = r.entries[0]; |
680 | | - const others = r.entries.slice(1).filter(ok); |
681 | | - if (!others.length) return; |
682 | | - ran++; |
683 | | - const top = others.reduce((a, o) => (o[0] > a[0] ? o : a)); |
684 | | - if (same(c, top)) ties++; |
685 | | - else if (c[0] > top[0]) { wins++; ratios.push([c[0] / top[0], l]); } |
| 678 | + // The claimed report against the fastest other report of one build: { s: "ok" | "eq" | "no", ratio, winner }, or null when they cannot be compared. |
| 679 | + function versus(r) { |
| 680 | + const c = r && r.entries[0]; |
| 681 | + if (!ok(c)) return null; |
| 682 | + let w = -1; |
| 683 | + r.entries.forEach((e, i) => { if (i && ok(e) && (w < 0 || e[0] > r.entries[w][0])) w = i; }); |
| 684 | + if (w < 0) return null; |
| 685 | + const o = r.entries[w]; |
| 686 | + if (same(c, o)) return { s: "eq", winner: w }; |
| 687 | + return c[0] > o[0] ? { s: "ok", ratio: c[0] / o[0], winner: w } : { s: "no", ratio: o[0] / c[0], winner: w }; |
| 688 | + } |
| 689 | + // Card icons, as in "Does it hold?"; "~" when the claim wins on some Rubies and loses on others. |
| 690 | + const RICON = { ok: "✓", eq: "=", no: "✗", na: "–", mix: "~" }; |
| 691 | + // Ratios as in "Does it hold?": 1.04×, 2.3×, 35×, 12k×, 27M×. |
| 692 | + const x2 = (r) => r >= 1e6 ? (r / 1e6).toFixed(1) + "M×" : r >= 1e4 ? Math.round(r / 1e3) + "k×" : r >= 10 ? Math.round(r) + "×" : r < 1.1 ? r.toFixed(2) + "×" : r.toFixed(1) + "×"; |
| 693 | + // The Ruby without its build: under YJIT the build is already in the heading. |
| 694 | + const rb = (l) => nice(base(l)); |
| 695 | + // Rubies next to each other on the axis, as "Ruby 2.1 to 2.6"; a run never spans two engines. |
| 696 | + function runsOf(labels, cols) { |
| 697 | + const out = []; |
| 698 | + let run = []; |
| 699 | + const flush = () => { if (run.length) out.push(run.length === 1 ? rb(run[0]) : `${rb(run[0])} to ${base(run[run.length - 1]).replace(/^[a-z]+_/, "")}`); run = []; }; |
| 700 | + cols.forEach((l) => { |
| 701 | + if (!labels.includes(l)) return flush(); |
| 702 | + if (run.length && base(run[0]).split("_")[0] !== base(l).split("_")[0]) flush(); |
| 703 | + run.push(l); |
686 | 704 | }); |
687 | | - let s = `<code>${esc(b.names[0])}</code> is fastest on <b>${wins} of ${ran}</b> builds without a JIT`; |
688 | | - if (ties) s += ` (${ties} tie${ties > 1 ? "s" : ""})`; |
689 | | - if (ratios.length) { |
690 | | - ratios.sort((a, c) => a[0] - c[0]); |
691 | | - const lo = ratios[0], hi = ratios[ratios.length - 1]; |
692 | | - s += lo === hi ? `, by ${lo[0].toFixed(1)}×` : `, by ${lo[0].toFixed(1)}× (${esc(nice(lo[1]))}) to ${hi[0].toFixed(1)}× (${esc(nice(hi[1]))})`; |
| 705 | + flush(); |
| 706 | + return out.join(", "); |
| 707 | + } |
| 708 | + |
| 709 | + // What happened, for the selected build: a headline (icon, sentence) and one line of numbers under it. |
| 710 | + function verdict(b) { |
| 711 | + const cols = columns().all; |
| 712 | + const claim = `<code>${esc(b.names[0])}</code>`, nm = (i) => `<code>${esc(b.names[i])}</code>`; |
| 713 | + const where = mode ? ` with ${esc(mode.toUpperCase())}` : ""; |
| 714 | + const res = cols.map((l) => [l, versus(b.runs[l])]).filter(([, v]) => v); |
| 715 | + const n = res.length, of = (s) => res.filter(([, v]) => v.s === s); |
| 716 | + const wins = of("ok"), ties = of("eq"), losses = of("no"); |
| 717 | + const all = `all ${n} Rubies${where}`; |
| 718 | + let icon = "ok", head; |
| 719 | + if (!n) { icon = "na"; head = `Did not run on any Ruby${where}`; } |
| 720 | + else if (!losses.length && !ties.length) head = b.names.length === 2 ? `${claim} beats ${nm(1)} on <b>${all}</b>` : `${claim} is the fastest on <b>${all}</b>`; |
| 721 | + else if (!losses.length && !wins.length) { icon = "eq"; head = b.names.length === 2 ? `${claim} and ${nm(1)} are about the same on <b>${all}</b>` : `${claim} ties with the fastest on <b>${all}</b>`; } |
| 722 | + else if (!losses.length) head = `${claim} wins or ties on <b>${all}</b> (${ties.length} tie${ties.length > 1 ? "s" : ""})`; |
| 723 | + else { |
| 724 | + // Who beat the claim, and on which Rubies: the most frequent winner first, at most two named. |
| 725 | + const by = {}; |
| 726 | + losses.forEach(([l, v]) => { (by[v.winner] = by[v.winner] || []).push(l); }); |
| 727 | + const top = Object.keys(by).sort((a, c) => by[c].length - by[a].length); |
| 728 | + const named = top.slice(0, 2).map((w) => `${nm(+w)} wins on ${esc(runsOf(by[w], cols))}`); |
| 729 | + const rest = top.slice(2).reduce((s, w) => s + by[w].length, 0); |
| 730 | + if (rest) named.push(`others on ${rest} more`); |
| 731 | + if (losses.length === n && top.length === 1) { icon = "no"; head = `${nm(+top[0])} beats ${claim} on <b>${all}</b>`; } |
| 732 | + else if (losses.length === n) { icon = "no"; head = `${claim} loses on <b>${all}</b>: ${named.join("; ")}`; } |
| 733 | + else { icon = "mix"; head = `${claim} wins on <b>${wins.length} of ${n} Rubies${where}</b>${ties.length ? ` (${ties.length} tie${ties.length > 1 ? "s" : ""})` : ""}; ${named.join("; ")}`; } |
693 | 734 | } |
694 | | - const line = plain.filter((l) => mri(l) && l !== "ruby_head" && b.runs[l] && ok(b.runs[l].entries[0])); |
| 735 | + |
| 736 | + const facts = []; |
| 737 | + const span = (list, verb) => { |
| 738 | + if (!list.length) return; |
| 739 | + const s = list.slice().sort((a, c) => a[1].ratio - c[1].ratio), lo = s[0], hi = s[s.length - 1]; |
| 740 | + facts.push(lo === hi || x2(lo[1].ratio) === x2(hi[1].ratio) ? `${verb} by ${x2(hi[1].ratio)}` : `${verb} by ${x2(lo[1].ratio)} (${esc(rb(lo[0]))}) to ${x2(hi[1].ratio)} (${esc(rb(hi[0]))})`); |
| 741 | + }; |
| 742 | + span(wins, "Wins"); |
| 743 | + span(losses, "Loses"); |
| 744 | + // Its own speed over the released Rubies of this build; head changes every week, so it is left out. |
| 745 | + const line = columns().line.filter((l) => base(l) !== "ruby_head" && b.runs[l] && ok(b.runs[l].entries[0])); |
695 | 746 | if (line.length > 1) { |
696 | | - const first = line[0], last = line[line.length - 1]; |
697 | | - s += `. Its own speed from ${esc(nice(first))} to ${esc(nice(last))}: <b>${pct(b.runs[last].entries[0][0], b.runs[first].entries[0][0])}</b>`; |
| 747 | + const f = b.runs[line[0]].entries[0], t = b.runs[line[line.length - 1]].entries[0]; |
| 748 | + facts.push(`Own speed from ${esc(rb(line[0]))} to ${esc(base(line[line.length - 1]).replace(/^ruby_/, ""))}: ${sameRuby(t, f, b.variation) ? "about the same" : `<b>${pct(t[0], f[0])}</b>`}`); |
| 749 | + } |
| 750 | + if (mode && line.length) { |
| 751 | + const l = line[line.length - 1], j = b.runs[l].entries[0], g = b.runs[base(l)] && b.runs[base(l)].entries[0]; |
| 752 | + if (ok(g)) facts.push(`${esc(mode.toUpperCase())} vs no JIT on ${esc(rb(l))}: ${sameRuby(j, g, b.variation) ? "about the same" : `<b>${pct(j[0], g[0])}</b>`}`); |
698 | 753 | } |
699 | | - return s + "."; |
| 754 | + // Rubies on the chart that are not in the count. |
| 755 | + const left = cols.filter((l) => !res.some(([k]) => k === l)); |
| 756 | + const susp = left.filter((l) => b.runs[l] && b.runs[l].entries.some(sus)), gone = left.filter((l) => !susp.includes(l)); |
| 757 | + if (susp.length) facts.push(`Not counted: ${esc(runsOf(susp, cols))} (suspect)`); |
| 758 | + if (gone.length) facts.push(`Did not run: ${esc(runsOf(gone, cols))}`); |
| 759 | + return { icon, head, sub: facts.join(" · ") }; |
700 | 760 | } |
701 | 761 |
|
702 | 762 | // The y axis is logarithmic, so a 100× gap between two reports and a 10% change between two Rubies are both readable on one chart. |
@@ -793,8 +853,8 @@ <h2 class="bh">FastRuby<span>.io</span> keeps fast-ruby running</h2> |
793 | 853 | const e = r.entries[i]; |
794 | 854 | t += `<div class="r"><i style="background:${color(i)}"></i>${esc(nm)}: ${!e ? "did not run" : fmt(e[0]) + " i/s" + (sus(e) ? " (suspect)" : "")}</div>`; |
795 | 855 | }); |
796 | | - const c = r.entries[0], o = r.entries[1]; |
797 | | - if (ok(c) && ok(o)) t += `<div class="n">${same(c, o) ? "Same-ish: the gap is within the error." : `${esc(b.names[0])} is ${(c[0] / o[0]).toFixed(2)}× ${esc(b.names[1])}.`}</div>`; |
| 856 | + const c = r.entries[0], v = versus(r); |
| 857 | + if (v) t += `<div class="n">${v.s === "eq" ? `Same-ish as ${esc(b.names[v.winner])}: the gap is within the error.` : v.s === "ok" ? `${esc(b.names[0])} is ${v.ratio.toFixed(2)}× faster than ${esc(b.names[v.winner])}.` : `${esc(b.names[v.winner])} is ${v.ratio.toFixed(2)}× faster than ${esc(b.names[0])}.`}</div>`; |
798 | 858 | const prev = k > 0 && k < cols.line.length && b.runs[cols.line[k - 1]]; |
799 | 859 | const pc = prev && prev.entries[0]; |
800 | 860 | if (ok(pc) && ok(c)) t += `<div class="n">vs ${esc(nice(cols.line[k - 1]))}: ${sameRuby(c, pc, b.variation) ? "same-ish" : pct(c[0], pc[0])}</div>`; |
@@ -815,7 +875,7 @@ <h2 class="bh">FastRuby<span>.io</span> keeps fast-ruby running</h2> |
815 | 875 |
|
816 | 876 | function table(b) { |
817 | 877 | const cols = columns(); |
818 | | - let h = `<tr><th>Build</th>${b.names.map((nm, i) => `<th>${esc(nm)}${i ? "" : " (claimed)"}</th>`).join("")}<th>Gap</th></tr>`; |
| 878 | + let h = `<tr><th>Build</th>${b.names.map((nm, i) => `<th>${esc(nm)}${i ? "" : " (claimed)"}</th>`).join("")}<th>Claimed vs fastest other</th></tr>`; |
819 | 879 | cols.all.forEach((l, k) => { |
820 | 880 | const r = b.runs[l], prev = k > 0 && k < cols.line.length ? b.runs[cols.line[k - 1]] : null; |
821 | 881 | h += `<tr><td>${esc(nice(l))}${l === REF ? " (reference)" : ""}</td>`; |
@@ -828,8 +888,8 @@ <h2 class="bh">FastRuby<span>.io</span> keeps fast-ruby running</h2> |
828 | 888 | const d = ok(pe) ? (sameRuby(e, pe, b.variation) ? ' <small class="sm">same-ish</small>' : ` <small>${pct(e[0], pe[0])}</small>`) : ""; |
829 | 889 | h += `<td>${fmt(e[0])} <small>±${(e[1] / e[0] * 100).toFixed(1)}%</small>${d}</td>`; |
830 | 890 | }); |
831 | | - const c = r && r.entries[0], o = r && r.entries[1]; |
832 | | - h += `<td>${ok(c) && ok(o) ? (same(c, o) ? '<span class="sm">tie</span>' : (c[0] / o[0]).toFixed(2) + "×") : ""}</td></tr>`; |
| 891 | + const v = versus(r); |
| 892 | + h += `<td>${!v ? "" : v.s === "eq" ? '<span class="sm">tie</span>' : v.s === "ok" ? `${v.ratio.toFixed(2)}× faster` : `<span class="lose">${v.ratio.toFixed(2)}× slower</span>`}</td></tr>`; |
833 | 893 | }); |
834 | 894 | return `<div class="tb"><table>${h}</table></div>`; |
835 | 895 | } |
@@ -854,7 +914,9 @@ <h2 class="bh">FastRuby<span>.io</span> keeps fast-ruby running</h2> |
854 | 914 | B.forEach((b, n) => { |
855 | 915 | if (b.section !== sec) { sec = b.section; h += `<h2>${esc(sec)}<span class="on">${esc(MODES.find(([m]) => m === mode)[1])}</span></h2>`; } |
856 | 916 | const legend = b.names.map((nm, i) => `<span><i style="background:${color(i)}"></i><code>${esc(nm)}</code>${i ? "" : " (claimed)"}</span>`).join("") + (mode ? '<span class="ghost"><i></i>without the JIT</span>' : ""); |
857 | | - h += `<article class="rcard"><h3>${md(b.title)}</h3><p class="say">${summary(b)}</p><div class="rlegend">${legend}</div>` + |
| 917 | + const v = verdict(b); |
| 918 | + h += `<article class="rcard"><p class="what">${md(b.title)}</p><h3><span class="ico ${v.icon}" aria-hidden="true">${RICON[v.icon]}</span><span>${v.head}</span></h3>` + |
| 919 | + (v.sub ? `<p class="say">${v.sub}</p>` : "") + `<div class="rlegend">${legend}</div>` + |
858 | 920 | `<div class="chart" data-n="${n}"></div>${foot(b)}` + |
859 | 921 | `<details data-n="${n}"${open.has(n) ? " open" : ""}><summary>Numbers per build</summary>${table(b)}</details>` + |
860 | 922 | `<div class="links"><a href="${M.repo}/blob/main/${encodeURI(b.file)}">View the code</a></div></article>`; |
|