use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use serde_json::{json, Value};
use std::collections::HashMap;
use std::hint::black_box;
use webui_handler::plugin::webui::WebUIHydrationPlugin;
use webui_handler::{Protocol, RenderOptions, ResponseWriter, WebUIHandler};
use webui_protocol::{
ComponentData, FragmentList, InitialStateStrategy, StateProjectionMode, WebUIFragment,
WebUIFragmentRoute, WebUIProtocol,
};
fn structural_fragment(value: &str) -> WebUIFragment {
let mut token = String::with_capacity("}}}webui:".len() + value.len());
token.push_str("}}}webui:");
token.push_str(value);
WebUIFragment::signal(token, true)
}
struct BenchWriter {
output: String,
}
impl BenchWriter {
fn new(capacity: usize) -> Self {
Self {
output: String::with_capacity(capacity),
}
}
fn clear(&mut self) {
self.output.clear();
}
fn len(&self) -> usize {
self.output.len()
}
}
impl ResponseWriter for BenchWriter {
fn write(&mut self, content: &str) -> webui_handler::Result<()> {
self.output.push_str(content);
Ok(())
}
webui_handler::string_response_writer_methods!(output);
fn end(&mut self) -> webui_handler::Result<()> {
Ok(())
}
}
fn build_large_state(target_bytes: usize) -> Value {
const APPROX_RECORD_BYTES: usize = 182;
let count = target_bytes / APPROX_RECORD_BYTES + 1;
let mut items = Vec::with_capacity(count);
for idx in 0..count {
items.push(json!({
"id": idx,
"name": format!("User {idx:06}"),
"email": format!("user{idx:06}@example.com"),
"active": idx % 3 == 0,
"score": (idx % 100) as f64 + 0.5,
"roles": ["reader", "writer"],
"bio": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
}));
}
json!({
"title": "Bootstrap State Benchmark",
"generatedAt": "2024-01-01T00:00:00Z",
"count": count,
"items": items,
})
}
fn build_wide_state(keys: usize) -> Value {
let mut map = serde_json::Map::new();
map.insert("keptA".to_string(), json!("a"));
map.insert("keptB".to_string(), json!("b"));
for idx in 0..keys {
map.insert(format!("serverOnly{idx:05}"), json!(idx));
}
Value::Object(map)
}
fn build_routed_state(contact_count: usize) -> Value {
let mut contacts = Vec::with_capacity(contact_count);
for idx in 0..contact_count {
contacts.push(json!({
"id": idx,
"name": format!("Contact {idx:05}"),
"email": format!("contact{idx:05}@example.com"),
"company": "Contoso",
"phone": "+1-555-0100",
}));
}
let recent_contacts = contacts.iter().take(5).cloned().collect::<Vec<_>>();
json!({
"page": "dashboard",
"contacts": contacts,
"recentContacts": recent_contacts,
})
}
fn full_hydration_keys() -> Vec<String> {
let mut keys = vec![
"count".to_string(),
"generatedAt".to_string(),
"items".to_string(),
"title".to_string(),
];
keys.sort();
keys
}
fn server_only_hydration_keys() -> Vec<String> {
let mut keys = vec![
"count".to_string(),
"generatedAt".to_string(),
"title".to_string(),
];
keys.sort();
keys
}
fn build_partial_protocol(
hydration_keys: &[&str],
navigation_keys: &[&str],
compiler_owned: bool,
navigation_mode: Option<StateProjectionMode>,
) -> Protocol {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::route("/", "benchmark-page")],
contains_boundary: false,
},
);
fragments.insert(
"benchmark-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Benchmark</p>")],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::new(fragments);
let template_json = if compiler_owned {
r#"{"h":"<p>Benchmark</p>","th":1}"#
} else {
r#"{"h":"<p>Benchmark</p>"}"#
};
protocol.components.insert(
"benchmark-page".to_string(),
ComponentData {
template_json: template_json.to_string(),
hydration_mode: if hydration_keys.is_empty() && !compiler_owned {
StateProjectionMode::Keys as i32
} else {
keyed_mode(hydration_keys)
},
hydration_keys: hydration_keys
.iter()
.map(|key| (*key).to_string())
.collect(),
navigation_mode: Some(
navigation_mode.map_or_else(|| keyed_mode(navigation_keys), |mode| mode as i32),
),
navigation_keys: navigation_keys
.iter()
.map(|key| (*key).to_string())
.collect(),
..Default::default()
},
);
Protocol::new(protocol)
}
fn build_routed_protocol() -> WebUIProtocol {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<!DOCTYPE html><html><body>"),
WebUIFragment::route_from(WebUIFragmentRoute {
path: "/".to_string(),
fragment_id: "dashboard-page".to_string(),
exact: true,
..Default::default()
}),
WebUIFragment::route_from(WebUIFragmentRoute {
path: "/contacts".to_string(),
fragment_id: "contacts-page".to_string(),
exact: true,
..Default::default()
}),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>"),
],
contains_boundary: false,
},
);
fragments.insert(
"dashboard-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Dashboard</p>")],
contains_boundary: false,
},
);
fragments.insert(
"contacts-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Contacts</p>")],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::new(fragments);
protocol.initial_state_strategy = InitialStateStrategy::Components as i32;
protocol.components.insert(
"dashboard-page".to_string(),
ComponentData {
template_json: "{}".to_string(),
hydration_mode: StateProjectionMode::Keys as i32,
hydration_keys: vec!["page".to_string(), "recentContacts".to_string()],
..Default::default()
},
);
protocol.components.insert(
"contacts-page".to_string(),
ComponentData {
template_json: "{}".to_string(),
hydration_mode: StateProjectionMode::Keys as i32,
hydration_keys: vec!["contacts".to_string(), "page".to_string()],
..Default::default()
},
);
protocol
}
fn build_bootstrap_protocol(
hydration_keys: Vec<String>,
navigation_keys: Vec<String>,
compiler_owned: bool,
) -> WebUIProtocol {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<!DOCTYPE html><html><body>"),
WebUIFragment::component("bench-component"),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>"),
],
contains_boundary: false,
},
);
fragments.insert(
"bench-component".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>ready</p>")],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::new(fragments);
protocol.initial_state_strategy = InitialStateStrategy::Components as i32;
let template_json = if compiler_owned {
r#"{"h":"<p>ready</p>","tr":["count","generatedAt","items","title"],"th":1}"#
} else {
r#"{"h":"<p>ready</p>"}"#
};
protocol.components.insert(
"bench-component".to_string(),
ComponentData {
template_json: template_json.to_string(),
hydration_mode: if hydration_keys.is_empty() && !compiler_owned {
StateProjectionMode::Keys as i32
} else {
keyed_mode(&hydration_keys)
},
hydration_keys,
navigation_mode: Some(keyed_mode(&navigation_keys)),
navigation_keys,
..Default::default()
},
);
protocol
}
fn keyed_mode<T>(keys: &[T]) -> i32 {
if keys.is_empty() {
StateProjectionMode::None as i32
} else {
StateProjectionMode::Keys as i32
}
}
fn build_missing_metadata_bootstrap_protocol() -> WebUIProtocol {
let mut protocol = build_bootstrap_protocol(Vec::new(), Vec::new(), true);
protocol.components.clear();
protocol
}
fn serialized_len(state: &Value) -> usize {
serde_json::to_vec(state)
.map(|bytes| bytes.len())
.unwrap_or(0)
}
fn bootstrap_state_bench(c: &mut Criterion) {
let mut group = c.benchmark_group("bootstrap_state");
let options = RenderOptions::new("index.html", "/");
let full_keys = full_hydration_keys();
let metadata_keys = server_only_hydration_keys();
let full_protocol = Protocol::new(build_bootstrap_protocol(
full_keys.clone(),
full_keys.clone(),
false,
));
let mut full_fallback_protocol = build_bootstrap_protocol(Vec::new(), Vec::new(), false);
let fallback_component = full_fallback_protocol
.components
.get_mut("bench-component")
.unwrap_or_else(|| panic!("benchmark component missing"));
fallback_component.hydration_mode = StateProjectionMode::All as i32;
fallback_component.navigation_mode = Some(StateProjectionMode::All as i32);
let full_fallback_protocol = Protocol::new(full_fallback_protocol);
let server_only_protocol = Protocol::new(build_bootstrap_protocol(
metadata_keys.clone(),
metadata_keys,
false,
));
let authored_navigation_only_protocol = Protocol::new(build_bootstrap_protocol(
Vec::new(),
full_keys.clone(),
false,
));
let dormant_protocol = Protocol::new(build_bootstrap_protocol(Vec::new(), full_keys, true));
let missing_metadata_protocol = Protocol::new(build_missing_metadata_bootstrap_protocol());
for &target in &[64 * 1024usize, 256 * 1024, 1024 * 1024] {
let state = build_large_state(target);
let state_bytes = serialized_len(&state);
let label = format!("{}KB", target / 1024);
group.throughput(Throughput::Bytes(state_bytes as u64));
group.bench_with_input(
BenchmarkId::new("hydratable_collection", &label),
&state,
|b, st| {
let handler = WebUIHandler::with_plugin(|| Box::new(WebUIHydrationPlugin::new()));
let mut writer = BenchWriter::new(state_bytes + 4096);
b.iter(|| {
writer.clear();
handler
.render(
black_box(&full_protocol),
black_box(st),
&options,
&mut writer,
)
.unwrap_or_else(|error| {
panic!("hydratable_collection render failed: {error}")
});
black_box(writer.len());
});
},
);
group.bench_with_input(
BenchmarkId::new("uncertain_full_state", &label),
&state,
|b, st| {
let handler = WebUIHandler::with_plugin(|| Box::new(WebUIHydrationPlugin::new()));
let mut writer = BenchWriter::new(state_bytes + 4096);
b.iter(|| {
writer.clear();
handler
.render(
black_box(&full_fallback_protocol),
black_box(st),
&options,
&mut writer,
)
.unwrap_or_else(|error| {
panic!("uncertain_full_state render failed: {error}")
});
black_box(writer.len());
});
},
);
group.bench_with_input(
BenchmarkId::new("server_only_collection", &label),
&state,
|b, st| {
let handler = WebUIHandler::with_plugin(|| Box::new(WebUIHydrationPlugin::new()));
let mut writer = BenchWriter::new(4096);
b.iter(|| {
writer.clear();
handler
.render(
black_box(&server_only_protocol),
black_box(st),
&options,
&mut writer,
)
.unwrap_or_else(|error| {
panic!("server_only_collection render failed: {error}")
});
black_box(writer.len());
});
},
);
group.bench_with_input(
BenchmarkId::new("authored_navigation_only_component", &label),
&state,
|b, st| {
let handler = WebUIHandler::with_plugin(|| Box::new(WebUIHydrationPlugin::new()));
let mut writer = BenchWriter::new(4096);
b.iter(|| {
writer.clear();
handler
.render(
black_box(&authored_navigation_only_protocol),
black_box(st),
&options,
&mut writer,
)
.unwrap_or_else(|error| {
panic!("authored_navigation_only_component render failed: {error}")
});
black_box(writer.len());
});
},
);
group.bench_with_input(
BenchmarkId::new("dormant_scriptless_component", &label),
&state,
|b, st| {
let handler = WebUIHandler::with_plugin(|| Box::new(WebUIHydrationPlugin::new()));
let mut writer = BenchWriter::new(4096);
b.iter(|| {
writer.clear();
handler
.render(
black_box(&dormant_protocol),
black_box(st),
&options,
&mut writer,
)
.unwrap_or_else(|error| {
panic!("dormant_scriptless_component render failed: {error}")
});
black_box(writer.len());
});
},
);
group.bench_with_input(
BenchmarkId::new("missing_component_metadata", &label),
&state,
|b, st| {
let handler = WebUIHandler::with_plugin(|| Box::new(WebUIHydrationPlugin::new()));
let mut writer = BenchWriter::new(4096);
b.iter(|| {
writer.clear();
handler
.render(
black_box(&missing_metadata_protocol),
black_box(st),
&options,
&mut writer,
)
.unwrap_or_else(|error| {
panic!("missing_component_metadata render failed: {error}")
});
black_box(writer.len());
});
},
);
group.bench_with_input(
BenchmarkId::new("without_plugin", &label),
&state,
|b, st| {
let handler = WebUIHandler::new();
let mut writer = BenchWriter::new(4096);
b.iter(|| {
writer.clear();
handler
.render(
black_box(&dormant_protocol),
black_box(st),
&options,
&mut writer,
)
.unwrap_or_else(|error| panic!("without_plugin render failed: {error}"));
black_box(writer.len());
});
},
);
}
group.finish();
let wide_state = build_wide_state(30_000);
let wide_state_bytes = serialized_len(&wide_state);
let wide_keys = vec!["keptA".to_string(), "keptB".to_string()];
let wide_protocol = Protocol::new(build_bootstrap_protocol(
wide_keys.clone(),
wide_keys,
false,
));
let mut wide_group = c.benchmark_group("bootstrap_state_wide");
wide_group.throughput(Throughput::Bytes(wide_state_bytes as u64));
wide_group.bench_function("sparse_keys_30000", |b| {
let handler = WebUIHandler::with_plugin(|| Box::new(WebUIHydrationPlugin::new()));
let mut writer = BenchWriter::new(4096);
b.iter(|| {
writer.clear();
handler
.render(
black_box(&wide_protocol),
black_box(&wide_state),
&options,
&mut writer,
)
.unwrap_or_else(|error| panic!("wide-state render failed: {error}"));
black_box(writer.len());
});
});
wide_group.finish();
let routed_state = build_routed_state(1_000);
let routed_state_bytes = serialized_len(&routed_state);
let routed_protocol = build_routed_protocol();
let mut dormant_routed_protocol = build_routed_protocol();
for component in dormant_routed_protocol.components.values_mut() {
component.navigation_keys = component.hydration_keys.clone();
component.navigation_mode = Some(component.hydration_mode);
component.hydration_keys.clear();
component.hydration_mode = StateProjectionMode::None as i32;
component.template_json = r#"{"h":"<p>ready</p>","th":1}"#.to_string();
}
let mut missing_metadata_routed_protocol = dormant_routed_protocol.clone();
missing_metadata_routed_protocol.components.clear();
let routed_protocol = Protocol::new(routed_protocol);
let dormant_routed_protocol = Protocol::new(dormant_routed_protocol);
let missing_metadata_routed_protocol = Protocol::new(missing_metadata_routed_protocol);
let mut route_group = c.benchmark_group("bootstrap_state_route");
route_group.throughput(Throughput::Bytes(routed_state_bytes as u64));
for &(name, path, protocol) in &[
("dashboard_excludes_contacts", "/", &routed_protocol),
("contacts_includes_contacts", "/contacts", &routed_protocol),
(
"contacts_dormant_component",
"/contacts",
&dormant_routed_protocol,
),
(
"contacts_missing_component_metadata",
"/contacts",
&missing_metadata_routed_protocol,
),
] {
route_group.bench_function(name, |b| {
let handler = WebUIHandler::with_plugin(|| Box::new(WebUIHydrationPlugin::new()));
let render_options = RenderOptions::new("index.html", path);
let mut writer = BenchWriter::new(routed_state_bytes + 4096);
b.iter(|| {
writer.clear();
handler
.render(
black_box(protocol),
black_box(&routed_state),
&render_options,
&mut writer,
)
.unwrap_or_else(|error| panic!("route-state render failed: {error}"));
black_box(writer.len());
});
});
}
route_group.finish();
}
fn partial_state_serialization_bench(c: &mut Criterion) {
let response = json!({
"templates": {},
"templateFunctions": {},
"componentStyles": {
"version": 1,
"strategy": "link",
"resources": {},
"closures": {},
},
"cssHrefs": [],
"inventory": "",
"path": "/",
"chain": [],
});
let projected_protocol = build_partial_protocol(
&["count", "generatedAt", "title"],
&["count", "generatedAt", "title"],
false,
None,
);
let scriptless_protocol =
build_partial_protocol(&[], &["count", "generatedAt", "title"], true, None);
let static_protocol = build_partial_protocol(&[], &[], true, None);
let full_protocol = build_partial_protocol(&[], &[], false, Some(StateProjectionMode::All));
let mut group = c.benchmark_group("partial_state_serialization");
for &target in &[64 * 1024usize, 1024 * 1024] {
let state = build_large_state(target);
let state_json = serde_json::to_string(&state)
.unwrap_or_else(|error| panic!("state serialization failed: {error}"));
let label = format!("{}KB", target / 1024);
group.throughput(Throughput::Bytes(state_json.len() as u64));
group.bench_with_input(
BenchmarkId::new("legacy_parse_and_reserialize", &label),
&state_json,
|b, input| {
b.iter(|| {
let state: Value = serde_json::from_str(black_box(input))
.unwrap_or_else(|error| panic!("state parse failed: {error}"));
let mut result = response.clone();
result
.as_object_mut()
.unwrap_or_else(|| panic!("benchmark response must be an object"))
.insert("state".to_string(), state);
let output = serde_json::to_string(&result)
.unwrap_or_else(|error| panic!("response serialization failed: {error}"));
black_box(output.len());
});
},
);
group.bench_with_input(
BenchmarkId::new("explicit_full_state", &label),
&state_json,
|b, input| {
b.iter(|| {
let output = full_protocol
.render_partial_json(black_box(input), "index.html", "/", "")
.unwrap_or_else(|error| {
panic!("full partial response serialization failed: {error}")
});
black_box(output.len());
});
},
);
group.bench_with_input(
BenchmarkId::new("projected_authored_state", &label),
&state_json,
|b, input| {
b.iter(|| {
let output = projected_protocol
.render_partial_json(black_box(input), "index.html", "/", "")
.unwrap_or_else(|error| {
panic!("projected partial response serialization failed: {error}")
});
black_box(output.len());
});
},
);
group.bench_with_input(
BenchmarkId::new("projected_scriptless_state", &label),
&state_json,
|b, input| {
b.iter(|| {
let output = scriptless_protocol
.render_partial_json(black_box(input), "index.html", "/", "")
.unwrap_or_else(|error| {
panic!("scriptless partial response serialization failed: {error}")
});
black_box(output.len());
});
},
);
group.bench_with_input(
BenchmarkId::new("static_scriptless_state", &label),
&state_json,
|b, input| {
b.iter(|| {
let output = static_protocol
.render_partial_json(black_box(input), "index.html", "/", "")
.unwrap_or_else(|error| {
panic!("static partial response serialization failed: {error}")
});
black_box(output.len());
});
},
);
}
group.finish();
}
criterion_group!(
benches,
bootstrap_state_bench,
partial_state_serialization_bench
);
criterion_main!(benches);