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