|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -euo pipefail |
| 3 | + |
| 4 | +# Generate a comparison-style markdown benchmark report. |
| 5 | +# Usage: bench-report.sh <linux-json> <macos-json> |
| 6 | +# Outputs markdown to stdout. |
| 7 | + |
| 8 | +linux_json="${1:?usage: bench-report.sh <linux-json> <macos-json>}" |
| 9 | +macos_json="${2:?usage: bench-report.sh <linux-json> <macos-json>}" |
| 10 | + |
| 11 | +# Normalize workload names into (impl, pattern, element) and extract metrics. |
| 12 | +# Output: JSON array of {impl, pattern, element, ns_per_op, ...} |
| 13 | +normalize=' |
| 14 | +[.results[] | { |
| 15 | + workload, |
| 16 | + impl: ( |
| 17 | + if .workload | startswith("spsc/inline/") then "mantis/inline" |
| 18 | + elif .workload | startswith("copy/") then "mantis/copy" |
| 19 | + elif .workload | startswith("general/") then "mantis/general" |
| 20 | + elif .workload | startswith("spsc/rtrb/") then "rtrb" |
| 21 | + elif .workload | startswith("spsc/crossbeam/") then "crossbeam" |
| 22 | + else "other" |
| 23 | + end |
| 24 | + ), |
| 25 | + pattern: ( |
| 26 | + if (.workload | test("single_item/|single/")) then "single" |
| 27 | + elif (.workload | test("burst_100/|burst/100/")) then "burst_100" |
| 28 | + elif (.workload | test("burst_1000/|burst/1000/")) then "burst_1000" |
| 29 | + elif (.workload | test("batch/100/")) then "batch_100" |
| 30 | + elif (.workload | test("batch/1000/")) then "batch_1000" |
| 31 | + elif (.workload | test("full_drain/")) then "full_drain" |
| 32 | + else "other" |
| 33 | + end |
| 34 | + ), |
| 35 | + element: (.workload | split("/") | last), |
| 36 | + ns_per_op: (.ns_per_op | . * 100 | round / 100), |
| 37 | + cycles: (.cycles_per_op // null | if . == null then null elif . < 0.1 then 0 else (. * 10 | round / 10) end), |
| 38 | + insns: (.instructions_per_op // null | if . == null then null else round end), |
| 39 | + bmiss: (.branch_misses_per_op // null | if . == null then null elif . < 0.1 then 0 else (. * 10 | round / 10) end) |
| 40 | +}] |
| 41 | +' |
| 42 | + |
| 43 | +# Build a comparison table for a given pattern. |
| 44 | +# Args: $1=json_file $2=pattern $3=title |
| 45 | +render_comparison() { |
| 46 | + local json="$1" pattern="$2" title="$3" |
| 47 | + local data impls elements |
| 48 | + |
| 49 | + data=$(jq -r --arg p "$pattern" "$normalize | map(select(.pattern == \$p))" "$json") |
| 50 | + elements=$(echo "$data" | jq -r '[.[].element] | unique | .[]') |
| 51 | + impls=$(echo "$data" | jq -r '[.[].impl] | unique | .[]') |
| 52 | + |
| 53 | + # Skip if no data for this pattern |
| 54 | + if [ -z "$elements" ]; then |
| 55 | + return |
| 56 | + fi |
| 57 | + |
| 58 | + # Build header |
| 59 | + local header="| Element |" |
| 60 | + local separator="|:--------|" |
| 61 | + for impl in $impls; do |
| 62 | + header="$header $impl |" |
| 63 | + separator="$separator------:|" |
| 64 | + done |
| 65 | + |
| 66 | + echo "#### $title" |
| 67 | + echo "" |
| 68 | + echo "$header" |
| 69 | + echo "$separator" |
| 70 | + |
| 71 | + # Build rows |
| 72 | + for elem in $elements; do |
| 73 | + local row="| \`$elem\` |" |
| 74 | + # Find the best (lowest) ns/op for this element |
| 75 | + local best |
| 76 | + best=$(echo "$data" | jq -r --arg e "$elem" \ |
| 77 | + '[.[] | select(.element == $e) | .ns_per_op] | min') |
| 78 | + |
| 79 | + for impl in $impls; do |
| 80 | + local cell |
| 81 | + cell=$(echo "$data" | jq -r --arg i "$impl" --arg e "$elem" \ |
| 82 | + '.[] | select(.impl == $i and .element == $e) | .ns_per_op // empty' 2>/dev/null) |
| 83 | + if [ -z "$cell" ]; then |
| 84 | + row="$row - |" |
| 85 | + else |
| 86 | + # Bold the best value with trophy emoji |
| 87 | + if [ "$cell" = "$best" ]; then |
| 88 | + row="$row **${cell}** 🏆 |" |
| 89 | + else |
| 90 | + row="$row $cell |" |
| 91 | + fi |
| 92 | + fi |
| 93 | + done |
| 94 | + echo "$row" |
| 95 | + done |
| 96 | + echo "" |
| 97 | +} |
| 98 | + |
| 99 | +# Build an insns/op comparison table for a given pattern. |
| 100 | +render_insns_comparison() { |
| 101 | + local json="$1" pattern="$2" title="$3" |
| 102 | + local data impls elements has_any |
| 103 | + |
| 104 | + data=$(jq -r --arg p "$pattern" "$normalize | map(select(.pattern == \$p))" "$json") |
| 105 | + elements=$(echo "$data" | jq -r '[.[].element] | unique | .[]') |
| 106 | + impls=$(echo "$data" | jq -r '[.[].impl] | unique | .[]') |
| 107 | + |
| 108 | + # Check if any insns data exists |
| 109 | + has_any=$(echo "$data" | jq -r '[.[].insns | select(. != null)] | length') |
| 110 | + if [ "$has_any" = "0" ] || [ -z "$elements" ]; then |
| 111 | + return |
| 112 | + fi |
| 113 | + |
| 114 | + local header="| Element |" |
| 115 | + local separator="|:--------|" |
| 116 | + for impl in $impls; do |
| 117 | + header="$header $impl |" |
| 118 | + separator="$separator------:|" |
| 119 | + done |
| 120 | + |
| 121 | + echo "#### $title" |
| 122 | + echo "" |
| 123 | + echo "$header" |
| 124 | + echo "$separator" |
| 125 | + |
| 126 | + for elem in $elements; do |
| 127 | + local row="| \`$elem\` |" |
| 128 | + local best |
| 129 | + best=$(echo "$data" | jq -r --arg e "$elem" \ |
| 130 | + '[.[] | select(.element == $e) | .insns | select(. != null)] | min // empty') |
| 131 | + |
| 132 | + for impl in $impls; do |
| 133 | + local cell |
| 134 | + cell=$(echo "$data" | jq -r --arg i "$impl" --arg e "$elem" \ |
| 135 | + '.[] | select(.impl == $i and .element == $e) | .insns // empty' 2>/dev/null) |
| 136 | + if [ -z "$cell" ] || [ "$cell" = "null" ]; then |
| 137 | + row="$row - |" |
| 138 | + elif [ -n "$best" ] && [ "$cell" = "$best" ]; then |
| 139 | + row="$row **${cell}** 🏆 |" |
| 140 | + else |
| 141 | + row="$row $cell |" |
| 142 | + fi |
| 143 | + done |
| 144 | + echo "$row" |
| 145 | + done |
| 146 | + echo "" |
| 147 | +} |
| 148 | + |
| 149 | +# Render full detailed table in a collapsible section. |
| 150 | +render_full_table() { |
| 151 | + local json="$1" |
| 152 | + |
| 153 | + echo "| Workload | ns/op | p50 | p99 | cycles | insns | bmiss | l1d | llc |" |
| 154 | + echo "|:---------|------:|----:|----:|-------:|------:|------:|----:|----:|" |
| 155 | + |
| 156 | + jq -r '.results[] | |
| 157 | + [ |
| 158 | + .workload, |
| 159 | + (.ns_per_op | . * 100 | round / 100 | tostring), |
| 160 | + (.p50_ns | . * 10 | round / 10 | tostring), |
| 161 | + (.p99_ns | . * 10 | round / 10 | tostring), |
| 162 | + (.cycles_per_op // null | if . == null then "-" elif . < 0.1 then "<0.1" else (. * 10 | round / 10 | tostring) end), |
| 163 | + (.instructions_per_op // null | if . == null then "-" else (. | round | tostring) end), |
| 164 | + (.branch_misses_per_op // null | if . == null then "-" elif . < 0.1 then "<0.1" else (. * 10 | round / 10 | tostring) end), |
| 165 | + (.l1_misses_per_op // null | if . == null then "-" elif . == 0 then "0" else (. * 10 | round / 10 | tostring) end), |
| 166 | + (.llc_misses_per_op // null | if . == null then "-" elif . == 0 then "0" else (. * 10 | round / 10 | tostring) end) |
| 167 | + ] | "| " + join(" | ") + " |" |
| 168 | + ' "$json" |
| 169 | +} |
| 170 | + |
| 171 | +# Render one platform section. |
| 172 | +render_platform() { |
| 173 | + local json="$1" label="$2" |
| 174 | + local cpu arch compiler |
| 175 | + |
| 176 | + if [ ! -f "$json" ]; then |
| 177 | + echo "*${label} benchmark results not available.*" |
| 178 | + echo "" |
| 179 | + return |
| 180 | + fi |
| 181 | + |
| 182 | + cpu=$(jq -r '.cpu' "$json") |
| 183 | + arch=$(jq -r '.arch' "$json") |
| 184 | + compiler=$(jq -r '.compiler' "$json") |
| 185 | + |
| 186 | + echo "**CPU:** \`${cpu}\` | **Arch:** \`${arch}\` | **Compiler:** \`${compiler}\`" |
| 187 | + echo "" |
| 188 | + |
| 189 | + echo "##### Latency (ns/op, lower is better)" |
| 190 | + echo "" |
| 191 | + render_comparison "$json" "single" "Single Push+Pop" |
| 192 | + render_comparison "$json" "burst_100" "Burst 100" |
| 193 | + render_comparison "$json" "burst_1000" "Burst 1000" |
| 194 | + render_comparison "$json" "batch_100" "Batch 100" |
| 195 | + render_comparison "$json" "batch_1000" "Batch 1000" |
| 196 | + render_comparison "$json" "full_drain" "Full Drain" |
| 197 | + |
| 198 | + echo "##### Instructions per Op (lower is better)" |
| 199 | + echo "" |
| 200 | + render_insns_comparison "$json" "single" "Single Push+Pop" |
| 201 | + render_insns_comparison "$json" "burst_100" "Burst 100" |
| 202 | + |
| 203 | + echo "<details>" |
| 204 | + echo "<summary>Full results (all fields)</summary>" |
| 205 | + echo "" |
| 206 | + render_full_table "$json" |
| 207 | + echo "" |
| 208 | + echo "</details>" |
| 209 | +} |
| 210 | + |
| 211 | +commit_sha="${GITHUB_SHA:-$(git rev-parse --short HEAD)}" |
| 212 | + |
| 213 | +cat <<HEADER |
| 214 | +## Benchmark Report |
| 215 | +
|
| 216 | +<sub>Commit: \`${commit_sha}\`</sub> |
| 217 | +
|
| 218 | +HEADER |
| 219 | + |
| 220 | +echo "<details open>" |
| 221 | +echo "<summary><strong>Linux</strong></summary>" |
| 222 | +echo "" |
| 223 | +render_platform "$linux_json" "Linux" |
| 224 | +echo "</details>" |
| 225 | +echo "" |
| 226 | + |
| 227 | +echo "<details open>" |
| 228 | +echo "<summary><strong>macOS</strong></summary>" |
| 229 | +echo "" |
| 230 | +render_platform "$macos_json" "macOS" |
| 231 | +echo "</details>" |
0 commit comments