use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use std::collections::HashMap;
use std::hint::black_box;
use webui_protocol::{
ComparisonOperator, ConditionExpr, FragmentList, LogicalOperator, WebUIFragment, WebUIProtocol,
};
#[allow(dead_code)]
fn create_test_protocol() -> WebUIProtocol {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("Hello, WebUI!\n"),
WebUIFragment::for_loop("person", "people", "for-1"),
WebUIFragment::signal("description", true),
WebUIFragment::if_cond(ConditionExpr::identifier("contact"), "if-1"),
],
},
);
fragments.insert(
"for-1".to_string(),
FragmentList {
fragments: vec![WebUIFragment::signal("person.name", false)],
},
);
fragments.insert(
"if-1".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("contact-card")],
},
);
fragments.insert(
"contact-card".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("Hello, "),
WebUIFragment::signal("name", false),
],
},
);
WebUIProtocol::new(fragments)
}
fn create_simple_protocol() -> WebUIProtocol {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("Hello, WebUI!\n"),
WebUIFragment::for_loop("person", "people", "for-1"),
],
},
);
fragments.insert(
"for-1".to_string(),
FragmentList {
fragments: vec![WebUIFragment::signal("person.name", false)],
},
);
WebUIProtocol::new(fragments)
}
fn serialize_protobuf_benchmark(c: &mut Criterion) {
let protocol = create_simple_protocol();
c.bench_function("serialize_protobuf", |b| {
b.iter(|| black_box(&protocol).to_protobuf())
});
}
fn deserialize_protobuf_benchmark(c: &mut Criterion) {
let protocol = create_simple_protocol();
let bytes = protocol.to_protobuf().expect("encode failed");
c.bench_function("deserialize_protobuf", |b| {
b.iter(|| WebUIProtocol::from_protobuf(black_box(&bytes)))
});
}
fn complex_condition_benchmark(c: &mut Criterion) {
let nested = ConditionExpr::compound(
ConditionExpr::predicate("user.role", ComparisonOperator::Equal, "admin"),
LogicalOperator::And,
ConditionExpr::negated(ConditionExpr::predicate(
"user.disabled",
ComparisonOperator::Equal,
"true",
)),
);
let mut fragments = HashMap::new();
fragments.insert(
"main".to_string(),
FragmentList {
fragments: vec![WebUIFragment::if_cond(nested, "then")],
},
);
fragments.insert(
"then".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("ok")],
},
);
let protocol = WebUIProtocol::new(fragments);
let bytes = protocol.to_protobuf().expect("encode failed");
c.bench_function("deserialize_complex_condition", |b| {
b.iter(|| WebUIProtocol::from_protobuf(black_box(&bytes)))
});
}
fn create_medium_protocol() -> WebUIProtocol {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><title>"),
WebUIFragment::signal("title", false),
WebUIFragment::raw("</title></head><body>"),
WebUIFragment::component("app"),
WebUIFragment::raw("<script src=\"/app.js\"></script></body></html>"),
],
},
);
fragments.insert(
"app".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div class=\"app\"><header><h1>"),
WebUIFragment::signal("title", false),
WebUIFragment::raw("</h1><span class=\"count\">"),
WebUIFragment::signal("remainingCount", false),
WebUIFragment::raw(" remaining</span></header><div class=\"input-row\"><input type=\"text\" placeholder=\"Add item...\"/><button>Add</button></div><ul class=\"list\">"),
WebUIFragment::for_loop("item", "items", "item-frag"),
WebUIFragment::raw("</ul>"),
WebUIFragment::if_cond(ConditionExpr::identifier("showFooter"), "footer-frag"),
WebUIFragment::raw("</div>"),
],
},
);
fragments.insert(
"item-frag".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<li"),
WebUIFragment::attribute("data-id", "item.id"),
WebUIFragment::attribute_template("class", "item-class-tmpl"),
WebUIFragment::raw(">"),
WebUIFragment::signal("item.title", false),
WebUIFragment::if_cond(
ConditionExpr::predicate("item.state", ComparisonOperator::Equal, "'done'"),
"done-badge",
),
WebUIFragment::raw(
"<button class=\"toggle\">✓</button><button class=\"delete\">✕</button></li>",
),
],
},
);
fragments.insert(
"item-class-tmpl".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("todo-item "),
WebUIFragment::signal("item.state", false),
],
},
);
fragments.insert(
"done-badge".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<span class=\"badge done\">✓</span>")],
},
);
fragments.insert(
"footer-frag".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<footer><p>"),
WebUIFragment::signal("footerText", false),
WebUIFragment::raw("</p><a"),
WebUIFragment::attribute("href", "helpUrl"),
WebUIFragment::raw(">Help</a></footer>"),
],
},
);
WebUIProtocol::new(fragments)
}
fn create_large_protocol(component_count: usize) -> WebUIProtocol {
let mut fragments = HashMap::new();
let mut root_frags = Vec::with_capacity(component_count * 2 + 4);
root_frags.push(WebUIFragment::raw("<html><body><nav>"));
root_frags.push(WebUIFragment::for_loop("link", "navLinks", "nav-link-frag"));
root_frags.push(WebUIFragment::raw("</nav><main>"));
for idx in 0..component_count {
let frag_id = format!("panel-{idx}");
root_frags.push(WebUIFragment::component(&frag_id));
}
root_frags.push(WebUIFragment::raw("</main></body></html>"));
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: root_frags,
},
);
fragments.insert(
"nav-link-frag".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<a"),
WebUIFragment::attribute("href", "link.url"),
WebUIFragment::attribute_boolean(
"disabled",
ConditionExpr::identifier("link.disabled"),
),
WebUIFragment::raw(">"),
WebUIFragment::signal("link.label", false),
WebUIFragment::raw("</a>"),
],
},
);
for idx in 0..component_count {
let panel_id = format!("panel-{idx}");
let body_id = format!("panel-body-{idx}");
let cond_id = format!("panel-detail-{idx}");
fragments.insert(
panel_id,
FragmentList {
fragments: vec![
WebUIFragment::raw(format!("<section class=\"panel\" data-idx=\"{idx}\">")),
WebUIFragment::raw("<h3>"),
WebUIFragment::signal("title", false),
WebUIFragment::raw("</h3>"),
WebUIFragment::component(&body_id),
WebUIFragment::if_cond(ConditionExpr::identifier("showDetails"), &cond_id),
WebUIFragment::raw("</section>"),
],
},
);
fragments.insert(
body_id,
FragmentList {
fragments: vec![
WebUIFragment::raw("<div class=\"panel-body\"><p>"),
WebUIFragment::signal("description", false),
WebUIFragment::raw("</p><span class=\"metric\">"),
WebUIFragment::signal("metric", false),
WebUIFragment::raw("</span></div>"),
],
},
);
fragments.insert(
cond_id,
FragmentList {
fragments: vec![
WebUIFragment::raw("<details><summary>More</summary><p>"),
WebUIFragment::signal("details", false),
WebUIFragment::raw("</p></details>"),
],
},
);
}
WebUIProtocol::new(fragments)
}
fn serialize_medium_benchmark(c: &mut Criterion) {
let protocol = create_medium_protocol();
c.bench_function("serialize_medium_protobuf", |b| {
b.iter(|| black_box(&protocol).to_protobuf())
});
}
fn deserialize_medium_benchmark(c: &mut Criterion) {
let protocol = create_medium_protocol();
let bytes = protocol.to_protobuf().expect("encode failed");
c.bench_function("deserialize_medium_protobuf", |b| {
b.iter(|| WebUIProtocol::from_protobuf(black_box(&bytes)))
});
}
fn protocol_size_sweep_bench(c: &mut Criterion) {
let mut group = c.benchmark_group("protocol_size_sweep");
for &count in &[5usize, 15, 30, 50] {
let protocol = create_large_protocol(count);
let bytes = protocol.to_protobuf().expect("encode failed");
group.throughput(Throughput::Bytes(bytes.len() as u64));
group.bench_with_input(
BenchmarkId::new("serialize", count),
&protocol,
|b, proto| {
b.iter(|| black_box(proto).to_protobuf());
},
);
group.bench_with_input(BenchmarkId::new("deserialize", count), &bytes, |b, data| {
b.iter(|| WebUIProtocol::from_protobuf(black_box(data)));
});
}
group.finish();
}
criterion_group!(
benches,
serialize_protobuf_benchmark,
deserialize_protobuf_benchmark,
complex_condition_benchmark,
serialize_medium_benchmark,
deserialize_medium_benchmark,
protocol_size_sweep_bench
);
criterion_main!(benches);