perf(wire): pre-size effect batch encoding
This commit is contained in:
@@ -6,6 +6,10 @@ edition.workspace = true
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bench]]
|
||||
name = "wire"
|
||||
harness = false
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = ["serde/std"]
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user