perf(wire): pre-size effect batch encoding
This commit is contained in:
@@ -6,6 +6,10 @@ edition.workspace = true
|
|||||||
[lib]
|
[lib]
|
||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "wire"
|
||||||
|
harness = false
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["std"]
|
default = ["std"]
|
||||||
std = ["serde/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 {
|
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> {
|
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);
|
write_batch(self, &mut out);
|
||||||
|
debug_assert_eq!(out.len(), encoded_len);
|
||||||
|
debug_assert_eq!(out.capacity(), initial_capacity);
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,6 +501,72 @@ pub enum WireError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const WIRE_MAGIC: &[u8; 4] = b"HEMX";
|
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>) {
|
fn write_batch(batch: &EffectBatch, out: &mut Vec<u8>) {
|
||||||
out.extend_from_slice(WIRE_MAGIC);
|
out.extend_from_slice(WIRE_MAGIC);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use hemx_core::{
|
use hemx_core::{
|
||||||
event, navigate, redirect, replace, Atom, AtomSnapshot, AtomState, BuildFingerprint,
|
event, navigate, redirect, replace, Atom, AtomSnapshot, AtomState, BuildFingerprint,
|
||||||
ComponentRef, CssClass, CssClasses, Effect, EffectBatch, Form, Handle, IntoEffect, KeyedSlot,
|
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]
|
#[test]
|
||||||
@@ -22,12 +23,68 @@ fn effect_batch_wire_round_trips() {
|
|||||||
|
|
||||||
let bytes = batch.to_wire();
|
let bytes = batch.to_wire();
|
||||||
assert_eq!(&bytes[..4], b"HEMX");
|
assert_eq!(&bytes[..4], b"HEMX");
|
||||||
|
assert_eq!(batch.encoded_len(), bytes.len()); // req: wire/007
|
||||||
let decoded = EffectBatch::from_wire(&bytes).unwrap();
|
let decoded = EffectBatch::from_wire(&bytes).unwrap();
|
||||||
|
|
||||||
assert_eq!(decoded, batch);
|
assert_eq!(decoded, batch);
|
||||||
assert!(decoded.is_compatible());
|
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]
|
#[test]
|
||||||
fn optional_effects_compose_into_batches() {
|
fn optional_effects_compose_into_batches() {
|
||||||
// req: component/005 req: public_api/003
|
// req: component/005 req: public_api/003
|
||||||
|
|||||||
Reference in New Issue
Block a user