perf(wire): pre-size effect batch encoding

This commit is contained in:
slhx agent
2026-07-13 09:32:46 +02:00
parent ecbdea5cd0
commit 945122cb02
4 changed files with 189 additions and 2 deletions
+4
View File
@@ -6,6 +6,10 @@ edition.workspace = true
[lib]
path = "src/lib.rs"
[[bench]]
name = "wire"
harness = false
[features]
default = ["std"]
std = ["serde/std"]
+51
View File
@@ -0,0 +1,51 @@
//! Repeatable wire-encoding baseline: `cargo bench -p hemx-core --bench wire`.
use hemx_core::{
BuildFingerprint, Effect, EffectBatch, Payload, ResourceId, ResourceKind, ResourceRef,
EFFECT_BATCH_ABI_VERSION,
};
use std::hint::black_box;
use std::time::Instant;
const ITERATIONS: u32 = 100_000;
const ROUNDS: usize = 7;
fn representative_batch() -> EffectBatch {
let target = ResourceRef::unscoped(ResourceId::new(ResourceKind::Slot, 42));
let ops = (0..50)
.map(|index| Effect::Put {
target: target.clone(),
payload: Payload::Text(format!("item-{index}-{}", "x".repeat(64))),
})
.collect();
EffectBatch {
abi_version: EFFECT_BATCH_ABI_VERSION,
fingerprint: BuildFingerprint(7),
ops,
}
}
fn main() {
let batch = representative_batch();
for _ in 0..10_000 {
black_box(batch.to_wire());
}
let mut nanos_per_batch = [0_u128; ROUNDS];
let mut checksum = 0_usize;
for round in &mut nanos_per_batch {
let start = Instant::now();
for _ in 0..ITERATIONS {
let bytes = black_box(&batch).to_wire();
checksum = checksum.wrapping_add(black_box(bytes.len()));
}
*round = start.elapsed().as_nanos() / u128::from(ITERATIONS);
}
nanos_per_batch.sort_unstable();
println!(
"median_ns_per_batch={} wire_len={} checksum={checksum}",
nanos_per_batch[ROUNDS / 2],
batch.encoded_len(),
);
}
+76 -1
View File
@@ -459,9 +459,18 @@ pub struct EffectBatch {
}
impl EffectBatch {
/// Return the exact number of bytes produced by [`Self::to_wire`].
pub fn encoded_len(&self) -> usize {
batch_wire_len(self)
}
pub fn to_wire(&self) -> Vec<u8> {
let mut out = Vec::new();
let encoded_len = self.encoded_len();
let mut out = Vec::with_capacity(encoded_len);
let initial_capacity = out.capacity();
write_batch(self, &mut out);
debug_assert_eq!(out.len(), encoded_len);
debug_assert_eq!(out.capacity(), initial_capacity);
out
}
@@ -492,6 +501,72 @@ pub enum WireError {
}
const WIRE_MAGIC: &[u8; 4] = b"HEMX";
const WIRE_BATCH_HEADER_LEN: usize = WIRE_MAGIC.len() + 4 + 8 + 4;
fn batch_wire_len(batch: &EffectBatch) -> usize {
WIRE_BATCH_HEADER_LEN + batch.ops.iter().map(effect_wire_len).sum::<usize>()
}
fn effect_wire_len(effect: &Effect) -> usize {
1 + match effect {
Effect::Put { target, payload } => ref_wire_len(target) + payload_wire_len(payload),
Effect::Insert {
target,
key,
payload,
}
| Effect::Prepend {
target,
key,
payload,
} => ref_wire_len(target) + str_wire_len(key) + payload_wire_len(payload),
Effect::Remove { target, key } => {
ref_wire_len(target) + option_str_wire_len(key.as_deref())
}
Effect::Move {
target,
key,
before,
} => ref_wire_len(target) + str_wire_len(key) + option_str_wire_len(before.as_deref()),
Effect::Focus { target } => ref_wire_len(target),
Effect::Navigate {
url, scroll, title, ..
} => {
str_wire_len(url) + 1 + scroll_wire_len(scroll) + option_str_wire_len(title.as_deref())
}
Effect::Emit { name, payload } => str_wire_len(name) + str_wire_len(payload),
}
}
fn ref_wire_len(reference: &ResourceRef) -> usize {
1 + 4
+ 1
+ match &reference.scope {
None => 0,
Some(ScopeKey::KeyValue(value) | ScopeKey::Field(value)) => str_wire_len(value),
}
}
fn payload_wire_len(payload: &Payload) -> usize {
1 + match payload {
Payload::Text(value) | Payload::Html(value) => str_wire_len(value),
}
}
fn scroll_wire_len(scroll: &ScrollBehavior) -> usize {
1 + match scroll {
ScrollBehavior::Preserve | ScrollBehavior::Top => 0,
ScrollBehavior::Element(target) => ref_wire_len(target),
}
}
fn option_str_wire_len(value: Option<&str>) -> usize {
1 + value.map_or(0, str_wire_len)
}
const fn str_wire_len(value: &str) -> usize {
4 + value.len()
}
fn write_batch(batch: &EffectBatch, out: &mut Vec<u8>) {
out.extend_from_slice(WIRE_MAGIC);
+58 -1
View File
@@ -1,7 +1,8 @@
use hemx_core::{
event, navigate, redirect, replace, Atom, AtomSnapshot, AtomState, BuildFingerprint,
ComponentRef, CssClass, CssClasses, Effect, EffectBatch, Form, Handle, IntoEffect, KeyedSlot,
NavigateMode, ParamName, Payload, ResourceKind, SafeHtml, ScopeKey, Slot,
NavigateMode, ParamName, Payload, ResourceId, ResourceKind, ResourceRef, SafeHtml, ScopeKey,
ScrollBehavior, Slot,
};
#[test]
@@ -22,12 +23,68 @@ fn effect_batch_wire_round_trips() {
let bytes = batch.to_wire();
assert_eq!(&bytes[..4], b"HEMX");
assert_eq!(batch.encoded_len(), bytes.len()); // req: wire/007
let decoded = EffectBatch::from_wire(&bytes).unwrap();
assert_eq!(decoded, batch);
assert!(decoded.is_compatible());
}
#[test]
fn encoded_len_covers_every_effect_shape() {
let unscoped = ResourceRef::unscoped(ResourceId::new(ResourceKind::Slot, 1));
let scoped = ResourceRef::scoped(
ResourceId::new(ResourceKind::Form, 2),
ScopeKey::Field(String::from("email")),
);
let batch = EffectBatch {
abi_version: hemx_core::EFFECT_BATCH_ABI_VERSION,
fingerprint: BuildFingerprint(42),
ops: vec![
Effect::Put {
target: unscoped.clone(),
payload: Payload::Html(String::from("<p>safe</p>")),
},
Effect::Insert {
target: scoped.clone(),
key: String::from("insert"),
payload: Payload::Text(String::from("one")),
},
Effect::Prepend {
target: scoped.clone(),
key: String::from("prepend"),
payload: Payload::Text(String::from("two")),
},
Effect::Remove {
target: scoped.clone(),
key: Some(String::from("remove")),
},
Effect::Move {
target: scoped.clone(),
key: String::from("move"),
before: Some(String::from("before")),
},
Effect::Focus {
target: scoped.clone(),
},
Effect::Navigate {
url: String::from("/next"),
mode: NavigateMode::Replace,
scroll: ScrollBehavior::Element(unscoped),
title: Some(String::from("Next")),
},
Effect::Emit {
name: String::from("notice"),
payload: String::from("saved"),
},
],
};
let bytes = batch.to_wire();
assert_eq!(batch.encoded_len(), bytes.len()); // req: wire/007
assert_eq!(EffectBatch::from_wire(&bytes).unwrap(), batch);
}
#[test]
fn optional_effects_compose_into_batches() {
// req: component/005 req: public_api/003