Skip to content

Commit 5968025

Browse files
authored
Integrate Bencher for overhead_bench tracking (#150)
* bencher integration * formater * remove path annotation * add comment about bench result units * make project slug a repo secret * implement e2e bench * fix * make ci run on relative branch * improve thearshold * remove temp files
1 parent 2117416 commit 5968025

5 files changed

Lines changed: 484 additions & 94 deletions

File tree

.github/workflows/benchmarks.yml

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
# Continuous benchmarking with Bencher (https://bencher.dev)
2+
#
3+
# PREREQUISITES:
4+
# 1. Create a project at https://bencher.dev and note its slug.
5+
# 2. Add the following secrets in GitHub → Settings → Secrets and variables → Actions:
6+
# - BENCHER_API_TOKEN: API token from bencher.dev
7+
# - BENCHER_PROJECT: your project slug
8+
# 3. GITHUB_TOKEN is provided automatically — no setup needed.
9+
#
10+
# overhead_bench uses a custom harness (not Criterion). Its --bmf flag
11+
# outputs Bencher Metric Format directly.
12+
13+
name: Benchmarks
14+
15+
on:
16+
push:
17+
branches:
18+
- main
19+
pull_request:
20+
workflow_dispatch:
21+
22+
permissions:
23+
checks: write
24+
pull-requests: write
25+
26+
jobs:
27+
# Runs on every push to main to build the statistical baseline.
28+
benchmark_main:
29+
name: Benchmark (main baseline)
30+
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
31+
runs-on: ubuntu-latest
32+
timeout-minutes: 30
33+
env:
34+
RUST_BACKTRACE: 1
35+
BENCHER_PROJECT: ${{ secrets.BENCHER_PROJECT }}
36+
steps:
37+
- uses: actions/checkout@v4
38+
- uses: dtolnay/rust-toolchain@stable
39+
- uses: Swatinem/rust-cache@v2
40+
41+
- name: Enable perf_event_open and kallsyms
42+
run: |
43+
sudo sysctl kernel.perf_event_paranoid=1
44+
sudo sysctl kernel.kptr_restrict=0
45+
46+
- uses: bencherdev/bencher@main
47+
48+
- name: Benchmark — poll_overhead
49+
run: |
50+
bencher run \
51+
--token '${{ secrets.BENCHER_API_TOKEN }}' \
52+
--branch '${{ github.ref_name }}' \
53+
--testbed ubuntu-latest \
54+
--adapter rust_criterion \
55+
"cargo bench --package dial9-tokio-telemetry --bench poll_overhead --features task-dump"
56+
57+
- name: Benchmark — writer_encode
58+
run: |
59+
bencher run \
60+
--token '${{ secrets.BENCHER_API_TOKEN }}' \
61+
--branch '${{ github.ref_name }}' \
62+
--testbed ubuntu-latest \
63+
--adapter rust_criterion \
64+
"cargo bench --package dial9-tokio-telemetry --bench writer_encode"
65+
66+
- name: Benchmark — codec
67+
run: |
68+
bencher run \
69+
--token '${{ secrets.BENCHER_API_TOKEN }}' \
70+
--branch '${{ github.ref_name }}' \
71+
--testbed ubuntu-latest \
72+
--adapter rust_criterion \
73+
"cargo bench --package dial9-trace-format --bench codec"
74+
75+
- name: Benchmark — overhead_bench
76+
run: |
77+
bencher run \
78+
--token '${{ secrets.BENCHER_API_TOKEN }}' \
79+
--branch '${{ github.ref_name }}' \
80+
--testbed ubuntu-latest \
81+
--adapter json \
82+
"cargo bench --bench overhead_bench -- --bmf 10"
83+
84+
- name: Benchmark — e2e_workload
85+
run: |
86+
bencher run \
87+
--token '${{ secrets.BENCHER_API_TOKEN }}' \
88+
--branch '${{ github.ref_name }}' \
89+
--testbed ubuntu-latest \
90+
--adapter json \
91+
"cargo bench --bench e2e_workload -- --bmf 10"
92+
93+
# Runs on same-repo PRs. Fork PRs are skipped — they have no access to
94+
# BENCHER_API_TOKEN, so the job would fail rather than silently skip.
95+
benchmark_pr:
96+
name: Benchmark (PR regression check)
97+
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
98+
runs-on: ubuntu-latest
99+
timeout-minutes: 30
100+
env:
101+
RUST_BACKTRACE: 1
102+
BENCHER_PROJECT: ${{ secrets.BENCHER_PROJECT }}
103+
steps:
104+
- uses: actions/checkout@v4
105+
- uses: dtolnay/rust-toolchain@stable
106+
- uses: Swatinem/rust-cache@v2
107+
108+
- name: Enable perf_event_open and kallsyms
109+
run: |
110+
sudo sysctl kernel.perf_event_paranoid=1
111+
sudo sysctl kernel.kptr_restrict=0
112+
113+
- uses: bencherdev/bencher@main
114+
115+
- name: Benchmark — poll_overhead
116+
run: |
117+
bencher run \
118+
--token '${{ secrets.BENCHER_API_TOKEN }}' \
119+
--branch '${{ github.head_ref }}' \
120+
--start-point main \
121+
--start-point-reset \
122+
--testbed ubuntu-latest \
123+
--adapter rust_criterion \
124+
--threshold-measure latency \
125+
--threshold-test percentage \
126+
--threshold-upper-boundary 0.25 \
127+
--threshold-measure throughput \
128+
--threshold-test percentage \
129+
--threshold-lower-boundary 0.25 \
130+
--error-on-alert \
131+
--github-actions '${{ secrets.GITHUB_TOKEN }}' \
132+
"cargo bench --package dial9-tokio-telemetry --bench poll_overhead --features task-dump"
133+
134+
- name: Benchmark — writer_encode
135+
run: |
136+
bencher run \
137+
--token '${{ secrets.BENCHER_API_TOKEN }}' \
138+
--branch '${{ github.head_ref }}' \
139+
--start-point main \
140+
--start-point-reset \
141+
--testbed ubuntu-latest \
142+
--adapter rust_criterion \
143+
--threshold-measure latency \
144+
--threshold-test percentage \
145+
--threshold-upper-boundary 0.25 \
146+
--threshold-measure throughput \
147+
--threshold-test percentage \
148+
--threshold-lower-boundary 0.25 \
149+
--error-on-alert \
150+
--github-actions '${{ secrets.GITHUB_TOKEN }}' \
151+
"cargo bench --package dial9-tokio-telemetry --bench writer_encode"
152+
153+
- name: Benchmark — codec
154+
run: |
155+
bencher run \
156+
--token '${{ secrets.BENCHER_API_TOKEN }}' \
157+
--branch '${{ github.head_ref }}' \
158+
--start-point main \
159+
--start-point-reset \
160+
--testbed ubuntu-latest \
161+
--adapter rust_criterion \
162+
--threshold-measure latency \
163+
--threshold-test percentage \
164+
--threshold-upper-boundary 0.25 \
165+
--threshold-measure throughput \
166+
--threshold-test percentage \
167+
--threshold-lower-boundary 0.25 \
168+
--error-on-alert \
169+
--github-actions '${{ secrets.GITHUB_TOKEN }}' \
170+
"cargo bench --package dial9-trace-format --bench codec"
171+
172+
- name: Benchmark — overhead_bench
173+
run: |
174+
bencher run \
175+
--token '${{ secrets.BENCHER_API_TOKEN }}' \
176+
--branch '${{ github.head_ref }}' \
177+
--start-point main \
178+
--start-point-reset \
179+
--testbed ubuntu-latest \
180+
--adapter json \
181+
--threshold-measure latency \
182+
--threshold-test percentage \
183+
--threshold-upper-boundary 0.25 \
184+
--threshold-measure throughput \
185+
--threshold-test percentage \
186+
--threshold-lower-boundary 0.25 \
187+
--error-on-alert \
188+
--github-actions '${{ secrets.GITHUB_TOKEN }}' \
189+
"cargo bench --bench overhead_bench -- --bmf 10"
190+
191+
- name: Benchmark — e2e_workload
192+
run: |
193+
bencher run \
194+
--token '${{ secrets.BENCHER_API_TOKEN }}' \
195+
--branch '${{ github.head_ref }}' \
196+
--start-point main \
197+
--start-point-reset \
198+
--testbed ubuntu-latest \
199+
--adapter json \
200+
--threshold-measure latency \
201+
--threshold-test percentage \
202+
--threshold-upper-boundary 0.25 \
203+
--threshold-measure throughput \
204+
--threshold-test percentage \
205+
--threshold-lower-boundary 0.25 \
206+
--error-on-alert \
207+
--github-actions '${{ secrets.GITHUB_TOKEN }}' \
208+
"cargo bench --bench e2e_workload -- --bmf 10"

dial9-tokio-telemetry/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ required-features = ["task-dump"]
7676
name = "overhead_bench"
7777
harness = false
7878

79+
[[bench]]
80+
name = "e2e_workload"
81+
harness = false
82+
7983
[[example]]
8084
name = "long_sleep"
8185
required-features = ["task-dump"]
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//! Bencher Metric Format (BMF) helpers.
2+
//! Spec: <https://bencher.dev/docs/reference/bencher-metric-format/>
3+
4+
use serde::Serialize;
5+
use std::collections::BTreeMap;
6+
7+
pub type Report = BTreeMap<String, Metric>;
8+
9+
#[derive(Serialize)]
10+
pub struct Metric {
11+
#[serde(skip_serializing_if = "Option::is_none")]
12+
pub latency: Option<Measure>,
13+
#[serde(skip_serializing_if = "Option::is_none")]
14+
pub throughput: Option<Measure>,
15+
}
16+
17+
#[derive(Serialize)]
18+
pub struct Measure {
19+
pub value: f64,
20+
}
21+
22+
impl Metric {
23+
pub fn latency(value: f64) -> Self {
24+
Self {
25+
latency: Some(Measure { value }),
26+
throughput: None,
27+
}
28+
}
29+
pub fn throughput(value: f64) -> Self {
30+
Self {
31+
latency: None,
32+
throughput: Some(Measure { value }),
33+
}
34+
}
35+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
//! Runs a fixed-size mixed CPU/IO workload — modelled on the
2+
// realistic_workload example;
3+
4+
mod bmf;
5+
6+
#[cfg(target_os = "linux")]
7+
use dial9_tokio_telemetry::telemetry::CpuProfilingConfig;
8+
use dial9_tokio_telemetry::telemetry::{RotatingWriter, TracedRuntime};
9+
use std::time::Instant;
10+
use tokio::io::{AsyncReadExt, AsyncWriteExt};
11+
use tokio::net::TcpListener;
12+
13+
const NUM_CLIENTS: usize = 4;
14+
const REQUESTS_PER_CLIENT: usize = 1_000;
15+
const NUM_CPU_TASKS: usize = 3;
16+
const CPU_TASK_ITERATIONS: usize = 20;
17+
const CPU_ITERS_PER_REQUEST: u64 = 10_000;
18+
const CPU_ITERS_PER_BURST: u64 = 50_000;
19+
const TOTAL_REQUESTS: usize = NUM_CLIENTS * REQUESTS_PER_CLIENT;
20+
21+
fn cpu_work(iterations: u64) -> u64 {
22+
let mut result = 0u64;
23+
for i in 0..iterations {
24+
result = result.wrapping_add(i.wrapping_mul(i));
25+
}
26+
result
27+
}
28+
29+
async fn workload_server(listener: TcpListener) {
30+
loop {
31+
let Ok((mut sock, _)) = listener.accept().await else {
32+
return;
33+
};
34+
tokio::spawn(async move {
35+
let mut buf = [0u8; 64];
36+
let Ok(n) = sock.read(&mut buf).await else {
37+
return;
38+
};
39+
if n == 0 {
40+
return;
41+
}
42+
let checksum = cpu_work(CPU_ITERS_PER_REQUEST);
43+
let _ = sock.write_all(&checksum.to_le_bytes()).await;
44+
});
45+
}
46+
}
47+
48+
async fn workload_client(port: u16) {
49+
for _ in 0..REQUESTS_PER_CLIENT {
50+
let mut stream = tokio::net::TcpStream::connect(("127.0.0.1", port))
51+
.await
52+
.expect("connect");
53+
stream.write_all(b"request").await.expect("write");
54+
let mut buf = [0u8; 8];
55+
stream.read_exact(&mut buf).await.expect("read");
56+
}
57+
}
58+
59+
async fn cpu_task() {
60+
for _ in 0..CPU_TASK_ITERATIONS {
61+
cpu_work(CPU_ITERS_PER_BURST);
62+
tokio::task::yield_now().await;
63+
}
64+
}
65+
66+
fn main() {
67+
let mut builder = tokio::runtime::Builder::new_multi_thread();
68+
builder.worker_threads(4).enable_all();
69+
70+
let writer = RotatingWriter::single_file("/tmp/e2e_workload_trace.bin").unwrap();
71+
let tb = TracedRuntime::builder().with_task_tracking(true);
72+
#[cfg(target_os = "linux")]
73+
let tb = tb.with_cpu_profiling(CpuProfilingConfig::default());
74+
let (runtime, _guard) = tb.build_and_start(builder, writer).unwrap();
75+
76+
let start = Instant::now();
77+
runtime.block_on(async {
78+
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
79+
let port = listener.local_addr().unwrap().port();
80+
let server = tokio::spawn(workload_server(listener));
81+
82+
let clients: Vec<_> = (0..NUM_CLIENTS)
83+
.map(|_| tokio::spawn(workload_client(port)))
84+
.collect();
85+
let cpu_tasks: Vec<_> = (0..NUM_CPU_TASKS)
86+
.map(|_| tokio::spawn(cpu_task()))
87+
.collect();
88+
89+
for c in clients {
90+
c.await.expect("client");
91+
}
92+
for t in cpu_tasks {
93+
t.await.expect("cpu task");
94+
}
95+
server.abort();
96+
});
97+
let wall = start.elapsed();
98+
99+
drop(_guard);
100+
101+
let rps = TOTAL_REQUESTS as f64 / wall.as_secs_f64();
102+
let mut report = bmf::Report::new();
103+
report.insert(
104+
"e2e::wall_time_ns".to_string(),
105+
bmf::Metric::latency(wall.as_nanos() as f64),
106+
);
107+
report.insert(
108+
"e2e::throughput_rps".to_string(),
109+
bmf::Metric::throughput(rps),
110+
);
111+
println!("{}", serde_json::to_string_pretty(&report).unwrap());
112+
}

0 commit comments

Comments
 (0)