Skip to content

Commit a605242

Browse files
Add high-precision timing library hpt with detailed README, cgo/nocgo platform-specific implementations, CI workflows, and benchmarks
1 parent f57ba76 commit a605242

23 files changed

Lines changed: 2440 additions & 0 deletions

.github/scripts/update-readme.py

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
#!/usr/bin/env python3
2+
"""Combine per-platform benchmark JSON files into a README markdown section.
3+
4+
Usage:
5+
python update-readme.py linux-cgo.json linux-nocgo.json darwin-cgo.json ...
6+
7+
Reads each JSON file (as produced by TestCIBenchmarks) and generates a single
8+
unified HTML table. Each platform+cgo combination gets its own column group,
9+
e.g. "macOS (arm64)" and "macOS (arm64) no cgo" side by side.
10+
"""
11+
12+
import json
13+
import sys
14+
import os
15+
from datetime import datetime, timezone
16+
17+
MARKER_START = "<!-- BENCHMARK_RESULTS_START -->"
18+
MARKER_END = "<!-- BENCHMARK_RESULTS_END -->"
19+
20+
PLATFORM_LABELS = {
21+
"linux": "Linux",
22+
"darwin": "macOS",
23+
"windows": "Windows",
24+
}
25+
26+
PLATFORM_ORDER = ["linux", "darwin", "windows"]
27+
28+
29+
def load_reports(paths):
30+
reports = []
31+
for path in paths:
32+
if not os.path.exists(path):
33+
continue
34+
with open(path) as f:
35+
reports.append(json.load(f))
36+
return reports
37+
38+
39+
def build_columns(reports):
40+
"""Return ordered list of (key, heading, report) tuples.
41+
42+
Ordered by platform, then cgo before nocgo within each platform.
43+
key is a unique string like 'darwin-cgo' used for cell lookups.
44+
"""
45+
# Group: {platform: {True: report, False: report}}
46+
grouped = {}
47+
for r in reports:
48+
p = r["platform"]
49+
cgo = r.get("cgo", False)
50+
grouped.setdefault(p, {})[cgo] = r
51+
52+
columns = []
53+
for p in PLATFORM_ORDER:
54+
if p not in grouped:
55+
continue
56+
variants = grouped[p]
57+
label = PLATFORM_LABELS.get(p, p)
58+
has_both = True in variants and False in variants
59+
60+
for cgo in [True, False]:
61+
if cgo not in variants:
62+
continue
63+
r = variants[cgo]
64+
arch = r.get("arch", "")
65+
heading = f"{label} ({arch})" if arch else label
66+
if has_both and not cgo:
67+
heading += " no cgo"
68+
key = f"{p}-{'cgo' if cgo else 'nocgo'}"
69+
columns.append((key, heading, r))
70+
71+
return columns
72+
73+
74+
# ---------- row builders ----------
75+
76+
def sleep_rows(columns):
77+
ref = columns[0][2] # first report as reference for durations
78+
rows = []
79+
for i, e in enumerate(ref["sleep"]):
80+
cells = {}
81+
for key, _, r in columns:
82+
s = r["sleep"][i]
83+
cells[key] = [
84+
f'<code>{s["hpt_mean"]}</code>',
85+
f'<code>{s["stdlib_mean"]}</code>',
86+
f'<b>{s["mean_improvement"]}</b>',
87+
]
88+
rows.append((e["duration"], cells))
89+
return rows
90+
91+
92+
def ticker_rows(columns):
93+
entries = [
94+
("Median jitter", "hpt_median_jitter", "stdlib_median_jitter", None),
95+
("Mean jitter", "hpt_mean_jitter", "stdlib_mean_jitter", "mean_improvement"),
96+
("p95 jitter", "hpt_p95_jitter", "stdlib_p95_jitter", None),
97+
("p99 jitter", "hpt_p99_jitter", "stdlib_p99_jitter", "p99_improvement"),
98+
("Max jitter", "hpt_max_jitter", "stdlib_max_jitter", None),
99+
("Total drift", "hpt_total_drift", "stdlib_total_drift", None),
100+
]
101+
rows = []
102+
for label, hpt_key, std_key, impr_key in entries:
103+
cells = {}
104+
for key, _, r in columns:
105+
tk = r["ticker"]
106+
hpt_val = tk.get(hpt_key, "—")
107+
std_val = tk.get(std_key, "—")
108+
impr = f'<b>{tk[impr_key]}</b>' if impr_key and impr_key in tk else "—"
109+
cells[key] = [
110+
f'<code>{hpt_val}</code>',
111+
f'<code>{std_val}</code>',
112+
impr,
113+
]
114+
rows.append((label, cells))
115+
return rows
116+
117+
118+
def timer_rows(columns):
119+
ref = columns[0][2]
120+
rows = []
121+
for i, e in enumerate(ref["timer"]):
122+
cells = {}
123+
for key, _, r in columns:
124+
t = r["timer"][i]
125+
cells[key] = [
126+
f'<code>{t["hpt_mean"]}</code>',
127+
f'<code>{t["stdlib_mean"]}</code>',
128+
f'<b>{t["mean_improvement"]}</b>',
129+
]
130+
rows.append((e["duration"], cells))
131+
return rows
132+
133+
134+
# ---------- table generator ----------
135+
136+
def generate_markdown(reports):
137+
if not reports:
138+
return "_No benchmark data available._\n"
139+
140+
columns = build_columns(reports)
141+
if not columns:
142+
return "_No benchmark data available._\n"
143+
144+
col_keys = [c[0] for c in columns]
145+
146+
sections = [
147+
("Sleep", sleep_rows(columns)),
148+
("Ticker", ticker_rows(columns)),
149+
("Timer", timer_rows(columns)),
150+
]
151+
152+
n_sub = 3 # hpt, time, impr
153+
lines = []
154+
155+
now = datetime.now(timezone.utc).strftime("%Y-%m-%d")
156+
lines.append(f"> Auto-generated on {now} by CI &mdash; "
157+
f"[view workflow](../../actions/workflows/benchmarks.yml)")
158+
lines.append("")
159+
lines.append("Lower is better for all metrics. "
160+
"Impr. = how many times more precise `hpt` is vs `time`. "
161+
"Columns without \"no cgo\" use the default cgo build "
162+
"(pthread ticker, GC-immune).")
163+
lines.append("")
164+
165+
lines.append("<table>")
166+
167+
# Header row 1: platform spans
168+
lines.append("<tr>")
169+
lines.append(' <th colspan="2" rowspan="2"></th>')
170+
for _, heading, _ in columns:
171+
lines.append(f' <th colspan="{n_sub}" align="center">{heading}</th>')
172+
lines.append("</tr>")
173+
174+
# Header row 2: sub-columns
175+
lines.append("<tr>")
176+
for _ in columns:
177+
lines.append(" <th><code>hpt</code></th>")
178+
lines.append(" <th><code>time</code></th>")
179+
lines.append(" <th>Impr.</th>")
180+
lines.append("</tr>")
181+
182+
# Data rows
183+
for section_name, rows in sections:
184+
n_rows = len(rows)
185+
for idx, (label, cells_by_key) in enumerate(rows):
186+
lines.append("<tr>")
187+
if idx == 0:
188+
lines.append(
189+
f' <th rowspan="{n_rows}" align="left">{section_name}</th>'
190+
)
191+
lines.append(f" <td><b>{label}</b></td>")
192+
for key in col_keys:
193+
for cell in cells_by_key[key]:
194+
lines.append(f" <td>{cell}</td>")
195+
lines.append("</tr>")
196+
197+
lines.append("</table>")
198+
lines.append("")
199+
200+
return "\n".join(lines)
201+
202+
203+
def update_readme(markdown):
204+
readme_path = os.path.join(os.path.dirname(__file__), "..", "..", "README.md")
205+
readme_path = os.path.normpath(readme_path)
206+
207+
with open(readme_path) as f:
208+
content = f.read()
209+
210+
start = content.find(MARKER_START)
211+
end = content.find(MARKER_END)
212+
if start == -1 or end == -1:
213+
print("ERROR: benchmark markers not found in README.md", file=sys.stderr)
214+
sys.exit(1)
215+
216+
new_content = (
217+
content[:start + len(MARKER_START)]
218+
+ "\n\n"
219+
+ markdown
220+
+ "\n"
221+
+ content[end:]
222+
)
223+
224+
with open(readme_path, "w") as f:
225+
f.write(new_content)
226+
227+
print(f"Updated {readme_path}")
228+
229+
230+
def main():
231+
if len(sys.argv) < 2:
232+
print(f"Usage: {sys.argv[0]} <results1.json> [results2.json] ...",
233+
file=sys.stderr)
234+
sys.exit(1)
235+
236+
reports = load_reports(sys.argv[1:])
237+
markdown = generate_markdown(reports)
238+
update_readme(markdown)
239+
240+
241+
if __name__ == "__main__":
242+
main()

.github/workflows/benchmarks.yml

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: Benchmarks
2+
3+
on:
4+
push:
5+
branches: [master, main]
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
benchmark:
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
include:
17+
- os: ubuntu-latest
18+
name: linux-cgo
19+
cgo: "1"
20+
- os: ubuntu-latest
21+
name: linux-nocgo
22+
cgo: "0"
23+
- os: macos-latest
24+
name: darwin-cgo
25+
cgo: "1"
26+
- os: macos-latest
27+
name: darwin-nocgo
28+
cgo: "0"
29+
- os: windows-latest
30+
name: windows
31+
cgo: "0"
32+
runs-on: ${{ matrix.os }}
33+
steps:
34+
- uses: actions/checkout@v4
35+
36+
- uses: actions/setup-go@v5
37+
with:
38+
go-version-file: go.mod
39+
40+
- name: Run benchmarks
41+
env:
42+
BENCH_OUTPUT: ${{ matrix.name }}.json
43+
CGO_ENABLED: ${{ matrix.cgo }}
44+
run: go test -run TestCIBenchmarks -v -count=1 -timeout=5m ./...
45+
46+
- uses: actions/upload-artifact@v4
47+
with:
48+
name: benchmark-${{ matrix.name }}
49+
path: ${{ matrix.name }}.json
50+
51+
update-readme:
52+
needs: benchmark
53+
runs-on: ubuntu-latest
54+
steps:
55+
- uses: actions/checkout@v4
56+
57+
- uses: actions/download-artifact@v4
58+
with:
59+
path: benchmark-results
60+
merge-multiple: true
61+
62+
- name: List collected results
63+
run: ls -la benchmark-results/
64+
65+
- name: Generate benchmark section
66+
run: |
67+
python3 .github/scripts/update-readme.py \
68+
benchmark-results/linux-cgo.json \
69+
benchmark-results/linux-nocgo.json \
70+
benchmark-results/darwin-cgo.json \
71+
benchmark-results/darwin-nocgo.json \
72+
benchmark-results/windows.json
73+
74+
- name: Commit updated README
75+
run: |
76+
git config user.name "github-actions[bot]"
77+
git config user.email "github-actions[bot]@users.noreply.github.com"
78+
git add README.md
79+
git diff --cached --quiet && echo "No changes to commit" && exit 0
80+
git commit -m "docs: update benchmark results [skip ci]"
81+
git push

0 commit comments

Comments
 (0)