Skip to content

Commit 27011a3

Browse files
committed
update microbench with different encodings
1 parent d1976c5 commit 27011a3

1 file changed

Lines changed: 125 additions & 74 deletions

File tree

benches/microbench/src/get_header.rs

Lines changed: 125 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,44 @@
22
//!
33
//! # What this measures
44
//!
5-
//! The full `get_header` pipeline end-to-end: HTTP fan-out to N in-process mock
6-
//! relays, response parsing, header validation, signature verification, and bid
7-
//! selection. This is wall-clock timing — useful for local development feedback
8-
//! and catching latency regressions across relay counts.
5+
//! The per-request `get_header` pipeline: HTTP request to a single in-process
6+
//! mock relay, response parsing, header validation, signature verification, and
7+
//! bid selection. This is wall-clock timing — useful for local development
8+
//! feedback and catching latency regressions across validation configurations.
9+
//!
10+
//! A single relay is used because relay fan-out uses `join_all` (not
11+
//! `tokio::spawn`), so all futures are polled on the same task. HTTP requests
12+
//! are truly concurrent but CPU-bound validation work (deserialization, BLS sig
13+
//! verification) is interleaved on one thread. Validation cost therefore scales
14+
//! roughly linearly with relay count — one relay is sufficient to measure the
15+
//! per-relay cost, and N relays can be estimated as ~N× that baseline.
16+
//!
17+
//! # Benchmark dimensions
18+
//!
19+
//! **Validation mode** (`HeaderValidationMode`):
20+
//! - `None` — light path: skips full deserialization and sig verification,
21+
//! extracts only fork + bid value, forwards raw bytes. Fastest option,
22+
//! requires complete trust in relays.
23+
//! - `Standard` — full deserialization, header validation (block hash, parent
24+
//! hash, timestamp, fork), BLS signature verification. Default mode.
25+
//! - `Extra` — Standard + parent block validation via RPC. NOTE: without a live
26+
//! RPC endpoint the parent block fetch returns None and `extra_validation` is
27+
//! skipped, so Extra degrades to Standard in this bench. It is included to
28+
//! catch any overhead from the mode flag itself and Accept header
29+
//! differences. A meaningful Extra benchmark would require a mock RPC server.
30+
//!
31+
//! **Encoding type** (`EncodingType`):
32+
//! - JSON only — validator requests `application/json`
33+
//! - SSZ only — validator requests `application/octet-stream`
34+
//! - Both — validator accepts either (CB picks the best available)
35+
//!
36+
//! Note: in Standard and Extra modes, `get_header` always requests both
37+
//! encodings from relays regardless of what the validator asked for, because it
38+
//! needs to unpack the body. The encoding dimension therefore only affects the
39+
//! None (light) path where the response is forwarded raw and must match what
40+
//! the validator accepts.
41+
//!
42+
//! Total: 3 modes × 3 encodings = 9 benchmark cases.
943
//!
1044
//! Criterion runs each benchmark hundreds of times, applies statistical
1145
//! analysis, and reports mean ± standard deviation. Results are saved to
@@ -17,8 +51,11 @@
1751
//! # Run all benchmarks
1852
//! cargo bench --package cb-bench-micro
1953
//!
20-
//! # Run a specific variant by filter
21-
//! cargo bench --package cb-bench-micro -- 3_relays
54+
//! # Run only the light (None) mode benchmarks
55+
//! cargo bench --package cb-bench-micro -- none
56+
//!
57+
//! # Compare modes for SSZ encoding
58+
//! cargo bench --package cb-bench-micro -- ssz
2259
//!
2360
//! # Save a named baseline to compare against later
2461
//! cargo bench --package cb-bench-micro -- --save-baseline main
@@ -31,16 +68,20 @@
3168
//!
3269
//! - PBS HTTP server overhead (we call `get_header()` directly, bypassing axum
3370
//! routing)
34-
//! - Mock relay startup time (servers are started once in setup, before timing
71+
//! - Mock relay startup time (server is started once in setup, before timing
3572
//! begins)
3673
//! - `HeaderMap` allocation (created once in setup, cloned cheaply per
3774
//! iteration)
75+
//! - Extra mode's RPC fetch (no live RPC endpoint in bench environment)
3876
3977
use std::{collections::HashSet, path::PathBuf, sync::Arc};
4078

4179
use alloy::primitives::B256;
4280
use axum::http::HeaderMap;
43-
use cb_common::{pbs::GetHeaderParams, signer::random_secret, types::Chain, utils::EncodingType};
81+
use cb_common::{
82+
config::HeaderValidationMode, pbs::GetHeaderParams, signer::random_secret, types::Chain,
83+
utils::EncodingType,
84+
};
4485
use cb_pbs::{PbsState, get_header};
4586
use cb_tests::{
4687
mock_relay::{MockRelayState, start_mock_relay_service_with_listener},
@@ -49,101 +90,111 @@ use cb_tests::{
4990
use criterion::{Criterion, black_box, criterion_group, criterion_main};
5091

5192
const CHAIN: Chain = Chain::Hoodi;
52-
const MAX_RELAYS: usize = 5;
53-
const RELAY_COUNTS: [usize; 3] = [1, 3, MAX_RELAYS];
5493

55-
/// Benchmarks `get_header` across three relay-count variants.
94+
const MODES: [(HeaderValidationMode, &str); 3] = [
95+
(HeaderValidationMode::None, "none"),
96+
(HeaderValidationMode::Standard, "standard"),
97+
// Extra degrades to Standard without a live RPC endpoint — included to
98+
// measure any overhead from the mode flag and Accept header differences.
99+
// See module doc comment for details.
100+
(HeaderValidationMode::Extra, "extra"),
101+
];
102+
103+
const ENCODINGS: [(&str, &[EncodingType]); 3] = [
104+
("json", &[EncodingType::Json]),
105+
("ssz", &[EncodingType::Ssz]),
106+
("both", &[EncodingType::Json, EncodingType::Ssz]),
107+
];
108+
109+
/// Build a `PbsState` for a specific validation mode with a single relay.
110+
///
111+
/// Port 0 is used because we call `get_header()` directly — no PBS server is
112+
/// started, so the port is never bound. The actual relay endpoint is carried
113+
/// inside the `RelayClient` object.
114+
fn make_pbs_state(mode: HeaderValidationMode, relay: cb_common::pbs::RelayClient) -> PbsState {
115+
let mut pbs_config = get_pbs_config(0);
116+
pbs_config.header_validation_mode = mode;
117+
let config = to_pbs_config(CHAIN, pbs_config, vec![relay]);
118+
PbsState::new(config, PathBuf::new())
119+
}
120+
121+
/// Benchmarks `get_header` across all validation modes and encoding types.
56122
///
57123
/// # Setup (runs once, not measured)
58124
///
59-
/// All MAX_RELAYS mock relays are started up-front and shared across variants.
60-
/// Each variant gets its own `PbsState` pointing to a different relay subset.
61-
/// The mock relays are in-process axum servers on localhost.
125+
/// A single mock relay is started up-front and shared across all variants.
126+
/// Each variant gets its own `PbsState` configured with the appropriate
127+
/// `HeaderValidationMode`. The mock relay is an in-process axum server on
128+
/// localhost.
62129
///
63130
/// # Per-iteration (measured)
64131
///
65132
/// Each call to `b.iter(|| ...)` runs `get_header()` once:
66-
/// - Fans out HTTP requests to N mock relays concurrently
67-
/// - Parses and validates each relay response (header data + BLS signature)
68-
/// - Selects the highest-value bid
133+
/// - Sends an HTTP request to the mock relay
134+
/// - Parses and validates the relay response (or skips in None mode)
135+
/// - Returns the bid
69136
///
70137
/// `black_box(...)` prevents the compiler from optimizing away inputs or the
71-
/// return value. Without it, the optimizer could see that the result is unused
72-
/// and eliminate the call entirely, producing a meaningless zero measurement.
138+
/// return value.
139+
///
140+
/// # Criterion grouping
141+
///
142+
/// Groups are structured as `get_header/{encoding}` with the validation mode
143+
/// as the bench function name. Each Criterion chart directly compares None vs
144+
/// Standard vs Extra for the same encoding — the comparison that matters most
145+
/// for understanding the latency cost of validation.
73146
fn bench_get_header(c: &mut Criterion) {
74147
let rt = tokio::runtime::Runtime::new().expect("tokio runtime");
75148

76-
// Start all mock relays once and build one PbsState per relay-count variant.
77-
// All relays share the same MockRelayState (and therefore the same signing
78-
// key). Each relay gets its own OS-assigned port via get_free_listener() so
79-
// there is no TOCTOU race and no hardcoded port reservations.
80-
let (states, params) = rt.block_on(async {
149+
// Start a single mock relay. It gets its own OS-assigned port via
150+
// get_free_listener() so there is no TOCTOU race.
151+
let (relay_client, params) = rt.block_on(async {
81152
let signer = random_secret();
82153
let pubkey = signer.public_key();
83154
let mock_state = Arc::new(MockRelayState::new(CHAIN, signer));
84155

85-
let mut relay_clients = Vec::with_capacity(MAX_RELAYS);
86-
for _ in 0..MAX_RELAYS {
87-
let listener = get_free_listener().await;
88-
let port = listener.local_addr().unwrap().port();
89-
tokio::spawn(start_mock_relay_service_with_listener(mock_state.clone(), listener));
90-
relay_clients.push(generate_mock_relay(port, pubkey.clone()).expect("relay client"));
91-
}
156+
let listener = get_free_listener().await;
157+
let port = listener.local_addr().unwrap().port();
158+
tokio::spawn(start_mock_relay_service_with_listener(mock_state, listener));
159+
let relay_client = generate_mock_relay(port, pubkey.clone()).expect("relay client");
92160

93-
// Give all servers time to start accepting before benchmarking begins.
161+
// Give the server time to start accepting before benchmarking begins.
94162
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
95163

96164
let params = GetHeaderParams { slot: 0, parent_hash: B256::ZERO, pubkey };
97-
98-
// Port 0 here is the port the PBS service itself would bind to for incoming
99-
// validator requests. We call get_header() as a function directly, so no
100-
// PBS server is started and this port is never used. The actual relay
101-
// endpoints are carried inside the RelayClient objects.
102-
let states: Vec<PbsState> = RELAY_COUNTS
103-
.iter()
104-
.map(|&n| {
105-
let config = to_pbs_config(CHAIN, get_pbs_config(0), relay_clients[..n].to_vec());
106-
PbsState::new(config, PathBuf::new())
107-
})
108-
.collect();
109-
110-
(states, params)
165+
(relay_client, params)
111166
});
112167

113168
// Empty HeaderMap matches what the PBS route handler receives for requests
114169
// without custom headers. Created once here to avoid measuring its
115170
// allocation per iteration.
116171
let headers = HeaderMap::new();
117172

118-
// A BenchmarkGroup groups related functions so Criterion produces a single
119-
// comparison table and chart. All variants share the name "get_header/".
120-
let mut group = c.benchmark_group("get_header");
121-
122-
for (i, relay_count) in RELAY_COUNTS.iter().enumerate() {
123-
let state = states[i].clone();
124-
let params = params.clone();
125-
let headers = headers.clone();
126-
127-
// bench_function registers one timing function. The closure receives a
128-
// `Bencher` — calling `b.iter(|| ...)` is the measured hot loop.
129-
// Everything outside `b.iter` is setup and not timed.
130-
group.bench_function(format!("{relay_count}_relays"), |b| {
131-
b.iter(|| {
132-
// block_on drives the async future to completion on the shared
133-
// runtime. get_header takes owned args, so we clone cheap types
134-
// (Arc-backed state, stack-sized params) on each iteration.
135-
rt.block_on(get_header(
136-
black_box(params.clone()),
137-
black_box(headers.clone()),
138-
black_box(state.clone()),
139-
black_box(HashSet::from([EncodingType::Json, EncodingType::Ssz])),
140-
))
141-
.expect("get_header failed")
142-
})
143-
});
144-
}
173+
for &(encoding_name, encoding_types) in &ENCODINGS {
174+
let encodings: HashSet<EncodingType> = encoding_types.iter().copied().collect();
175+
let mut group = c.benchmark_group(format!("get_header/{encoding_name}"));
176+
177+
for &(mode, mode_name) in &MODES {
178+
let state = make_pbs_state(mode, relay_client.clone());
179+
let params = params.clone();
180+
let headers = headers.clone();
181+
let encodings = encodings.clone();
182+
183+
group.bench_function(mode_name, |b| {
184+
b.iter(|| {
185+
rt.block_on(get_header(
186+
black_box(params.clone()),
187+
black_box(headers.clone()),
188+
black_box(state.clone()),
189+
black_box(encodings.clone()),
190+
))
191+
.expect("get_header failed")
192+
})
193+
});
194+
}
145195

146-
group.finish();
196+
group.finish();
197+
}
147198
}
148199

149200
// criterion_group! registers bench_get_header as a benchmark group named

0 commit comments

Comments
 (0)