chore(release): 0.4.1
Bump workspace version 0.4.0 → 0.4.1. Patch release: dispatch hot-path performance only. No public API changes; every chart that compiled against 0.4.0 compiles unchanged and behaves identically. CHANGELOG.md captures the per-pass writeup under the 0.4.1 heading. Headlines (deepest dispatch in the bench suite): h8_cross_tree -82% instructions / -72% cycles vs pre_lca_fix bubble_top -22% instructions / -6% cycles vs handler walk terminate_event -22% instructions / -23% cycles vs PARENT walk The runtime LCA helper, is_up_transition, the per-call heapless::Vec ancestry build, the runtime PARENT bubble walk, and (on timer-free charts) the entire TimerTable machinery are all gone. Callgrind reports the entire HSM machinery as a single inlined `step` symbol. Bench suite expanded from 7 to 13 benches: bubble_leaf/mid/top quantify handler resolution at varying ancestor depths, timer_fire_dispatch covers the only path that exercises pop_expired's non-empty branch, terminate_event covers do_terminate, and bubble_cross_tree covers the realistic HSM worst case (deep bubble + deep cross-tree transition). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
6 files changed
+60
-50
No files matched your search
+1
-1
@@ -4,7 +4,7 @@ All notable changes to this workspace are documented here. The format
|
||||
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
|
||||
this project adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## Unreleased
|
||||
## 0.4.1 — 2026-04-28
|
||||
|
||||
### Performance — dispatch hot path
|
||||
|
||||
|
||||
Generated
+3
-3
@@ -495,7 +495,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hsmc"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
dependencies = [
|
||||
"crossterm",
|
||||
"defmt 0.3.100",
|
||||
@@ -515,7 +515,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hsmc-macros"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
dependencies = [
|
||||
"hsmc",
|
||||
"proc-macro2",
|
||||
@@ -526,7 +526,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hsmc-verification"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
dependencies = [
|
||||
"creusot-std",
|
||||
"hsmc",
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ resolver = "2"
|
||||
members = ["hsmc", "hsmc-macros", "verification"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
authors = ["Stephen Waits <steve@waits.net>"]
|
||||
|
||||
@@ -329,13 +329,15 @@ fn compute_terminate_paths(states: &[StateIr]) -> (Vec<u16>, Vec<(u16, u16)>) {
|
||||
fn compute_event_dispatch(ir: &Ir) -> Vec<(u16, Vec<u16>, Option<u16>)> {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// `event_id → (action_ids, transition_target)` aggregation for one state.
|
||||
type StateHandlers = BTreeMap<u16, (Vec<u16>, Option<u16>)>;
|
||||
|
||||
let n_states = ir.states.len();
|
||||
let n_events = ir.event_variants.len();
|
||||
|
||||
// Per-state, per-event handler resolution. Mirrors the aggregation in
|
||||
// `state_handler_arms` (sort by decl_index, group by trigger).
|
||||
let mut per_state: Vec<BTreeMap<u16, (Vec<u16>, Option<u16>)>> =
|
||||
vec![BTreeMap::new(); n_states];
|
||||
let mut per_state: Vec<StateHandlers> = vec![StateHandlers::new(); n_states];
|
||||
for (sid, state) in ir.states.iter().enumerate() {
|
||||
let mut handlers = state.handlers.iter().collect::<Vec<_>>();
|
||||
handlers.sort_by_key(|h| h.decl_index);
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ trace-tracing = ["dep:tracing"]
|
||||
journal = []
|
||||
|
||||
[dependencies]
|
||||
hsmc-macros = { path = "../hsmc-macros", version = "0.4.0" }
|
||||
hsmc-macros = { path = "../hsmc-macros", version = "0.4.1" }
|
||||
heapless = "0.8"
|
||||
tokio = { version = "1", features = ["sync", "time", "rt", "macros"], optional = true }
|
||||
embassy-sync = { version = "0.8", optional = true }
|
||||
|
||||
+50
-42
@@ -16,22 +16,30 @@
|
||||
//! Each bench's setup function primes the chart to a known state outside the
|
||||
//! measured region. The body sends one event and runs `step()` once.
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
#[cfg(not(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy")))))]
|
||||
fn main() {
|
||||
eprintln!("hsmc dispatch benches require Linux (iai-callgrind + valgrind).");
|
||||
// The benches use sync action impls; under `--features tokio` /
|
||||
// `--features embassy` the codegen emits async fns and the impls
|
||||
// would need an `async` keyword. Rather than maintain two parallel
|
||||
// bench files, we gate the whole suite to the default-features /
|
||||
// Linux configuration, which is what we measure latency in anyway.
|
||||
eprintln!(
|
||||
"hsmc dispatch benches require Linux + default features \
|
||||
(iai-callgrind + valgrind, sync action impls)."
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
use hsmc::{statechart, Duration};
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
use iai_callgrind::{library_benchmark, library_benchmark_group, main};
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
use std::hint::black_box;
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Chart 1: H=2 sibling. LCA(A, B) = P. Two states deep.
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
mod h2 {
|
||||
use super::*;
|
||||
|
||||
@@ -66,7 +74,7 @@ mod h2 {
|
||||
// Chart 2: H=4 cross-tree. LCA(L3, R3) = root. Stresses LCA over 8 ancestors.
|
||||
// Also has up- and self-transition events for additional benches.
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
mod h4 {
|
||||
use super::*;
|
||||
|
||||
@@ -116,7 +124,7 @@ mod h4 {
|
||||
// Chart 3: H=8 cross-tree. LCA(L7, R7) = root. Worst-case for the
|
||||
// pre-fix O(H²) LCA — old code does ~64 compares; new code does ~16.
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
mod h8 {
|
||||
use super::*;
|
||||
|
||||
@@ -172,7 +180,7 @@ mod h8 {
|
||||
// Chart 4: emit-chain. S0 →(Go)→ S1 →(emits Next)→ S2 →(emits Next)→ S3 →(emits Next)→ S4.
|
||||
// One Send + one Step ⇒ chart drains 4 events through 3 self-emits.
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
mod chain {
|
||||
use super::*;
|
||||
|
||||
@@ -220,7 +228,7 @@ mod chain {
|
||||
// dispatch shape; the only thing that varies between benches is the
|
||||
// bubble distance to the handler.
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
mod bubble {
|
||||
use super::*;
|
||||
|
||||
@@ -274,7 +282,7 @@ mod bubble {
|
||||
// suite declares no `Duration` triggers and (post timer-elide pass) has
|
||||
// the timer-table machinery removed at codegen.
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
mod timer_fire {
|
||||
use super::*;
|
||||
|
||||
@@ -305,7 +313,7 @@ mod timer_fire {
|
||||
// Only bench that exercises `__check_terminate`'s true arm and the
|
||||
// terminate-on-event path.
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
mod term {
|
||||
use super::*;
|
||||
|
||||
@@ -347,7 +355,7 @@ mod term {
|
||||
// realistic worst case for HSM dispatch — a parent's "back to default"
|
||||
// handler that descends deep on the other side.
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
mod bubble_cross {
|
||||
use super::*;
|
||||
|
||||
@@ -406,61 +414,61 @@ mod bubble_cross {
|
||||
// the bench fn body.
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
fn setup_h2_warm() -> h2::H2<8> {
|
||||
let mut m = h2::H2::new(h2::Ctx::default());
|
||||
let mut m = h2::H2::new(h2::Ctx);
|
||||
let _ = m.step(Duration::ZERO); // priming step → enters P → A
|
||||
m
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
fn setup_h2_cold() -> h2::H2<8> {
|
||||
h2::H2::new(h2::Ctx::default())
|
||||
h2::H2::new(h2::Ctx)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
fn setup_h4_warm() -> h4::H4<8> {
|
||||
let mut m = h4::H4::new(h4::Ctx::default());
|
||||
let mut m = h4::H4::new(h4::Ctx);
|
||||
let _ = m.step(Duration::ZERO); // priming step → L1 → L2 → L3
|
||||
m
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
fn setup_h8_warm() -> h8::H8<8> {
|
||||
let mut m = h8::H8::new(h8::Ctx::default());
|
||||
let mut m = h8::H8::new(h8::Ctx);
|
||||
let _ = m.step(Duration::ZERO); // priming step → L1..L7
|
||||
m
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
fn setup_chain_warm() -> chain::Chain<8> {
|
||||
let mut m = chain::Chain::new(chain::Ctx::default());
|
||||
let mut m = chain::Chain::new(chain::Ctx);
|
||||
let _ = m.step(Duration::ZERO); // priming step → S0
|
||||
m
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
fn setup_bubble_warm() -> bubble::Bubble<8> {
|
||||
let mut m = bubble::Bubble::new(bubble::Ctx);
|
||||
let _ = m.step(Duration::ZERO); // priming step → Outer → L1 → L2 → L3 → L4
|
||||
m
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
fn setup_timer_fire_armed() -> timer_fire::TimerFire<8> {
|
||||
let mut m = timer_fire::TimerFire::new(timer_fire::Ctx);
|
||||
let _ = m.step(Duration::ZERO); // priming step → enters Armed, arms 1ns timer
|
||||
m
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
fn setup_term_warm() -> term::Term<8> {
|
||||
let mut m = term::Term::new(term::Ctx);
|
||||
let _ = m.step(Duration::ZERO); // priming step → L1 → L2 → L3
|
||||
m
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
fn setup_bubble_cross_warm() -> bubble_cross::BubbleCross<8> {
|
||||
let mut m = bubble_cross::BubbleCross::new(bubble_cross::Ctx);
|
||||
let _ = m.step(Duration::ZERO); // priming step → L1 → L2 → L3 → L4 → L5
|
||||
@@ -472,7 +480,7 @@ fn setup_bubble_cross_warm() -> bubble_cross::BubbleCross<8> {
|
||||
// chart by value, sends one event, and runs one step.
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
#[library_benchmark]
|
||||
#[bench::cold(setup = setup_h2_cold)]
|
||||
fn h2_cold_start(mut m: h2::H2<8>) -> h2::H2<8> {
|
||||
@@ -481,7 +489,7 @@ fn h2_cold_start(mut m: h2::H2<8>) -> h2::H2<8> {
|
||||
m
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
#[library_benchmark]
|
||||
#[bench::warm(setup = setup_h2_warm)]
|
||||
fn h2_warm_lateral(mut m: h2::H2<8>) -> h2::H2<8> {
|
||||
@@ -490,7 +498,7 @@ fn h2_warm_lateral(mut m: h2::H2<8>) -> h2::H2<8> {
|
||||
m
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
#[library_benchmark]
|
||||
#[bench::cross(setup = setup_h4_warm)]
|
||||
fn h4_cross_tree(mut m: h4::H4<8>) -> h4::H4<8> {
|
||||
@@ -499,7 +507,7 @@ fn h4_cross_tree(mut m: h4::H4<8>) -> h4::H4<8> {
|
||||
m
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
#[library_benchmark]
|
||||
#[bench::up(setup = setup_h4_warm)]
|
||||
fn h4_up(mut m: h4::H4<8>) -> h4::H4<8> {
|
||||
@@ -508,7 +516,7 @@ fn h4_up(mut m: h4::H4<8>) -> h4::H4<8> {
|
||||
m
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
#[library_benchmark]
|
||||
#[bench::self_(setup = setup_h4_warm)]
|
||||
fn h4_self(mut m: h4::H4<8>) -> h4::H4<8> {
|
||||
@@ -517,7 +525,7 @@ fn h4_self(mut m: h4::H4<8>) -> h4::H4<8> {
|
||||
m
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
#[library_benchmark]
|
||||
#[bench::cross(setup = setup_h8_warm)]
|
||||
fn h8_cross_tree(mut m: h8::H8<8>) -> h8::H8<8> {
|
||||
@@ -526,7 +534,7 @@ fn h8_cross_tree(mut m: h8::H8<8>) -> h8::H8<8> {
|
||||
m
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
#[library_benchmark]
|
||||
#[bench::leaf(setup = setup_bubble_warm)]
|
||||
fn bubble_leaf(mut m: bubble::Bubble<8>) -> bubble::Bubble<8> {
|
||||
@@ -536,7 +544,7 @@ fn bubble_leaf(mut m: bubble::Bubble<8>) -> bubble::Bubble<8> {
|
||||
m
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
#[library_benchmark]
|
||||
#[bench::mid(setup = setup_bubble_warm)]
|
||||
fn bubble_mid(mut m: bubble::Bubble<8>) -> bubble::Bubble<8> {
|
||||
@@ -546,7 +554,7 @@ fn bubble_mid(mut m: bubble::Bubble<8>) -> bubble::Bubble<8> {
|
||||
m
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
#[library_benchmark]
|
||||
#[bench::top(setup = setup_bubble_warm)]
|
||||
fn bubble_top(mut m: bubble::Bubble<8>) -> bubble::Bubble<8> {
|
||||
@@ -556,7 +564,7 @@ fn bubble_top(mut m: bubble::Bubble<8>) -> bubble::Bubble<8> {
|
||||
m
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
#[library_benchmark]
|
||||
#[bench::fire(setup = setup_timer_fire_armed)]
|
||||
fn timer_fire_dispatch(mut m: timer_fire::TimerFire<8>) -> timer_fire::TimerFire<8> {
|
||||
@@ -566,7 +574,7 @@ fn timer_fire_dispatch(mut m: timer_fire::TimerFire<8>) -> timer_fire::TimerFire
|
||||
m
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
#[library_benchmark]
|
||||
#[bench::quit(setup = setup_term_warm)]
|
||||
fn terminate_event(mut m: term::Term<8>) -> term::Term<8> {
|
||||
@@ -577,7 +585,7 @@ fn terminate_event(mut m: term::Term<8>) -> term::Term<8> {
|
||||
m
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
#[library_benchmark]
|
||||
#[bench::switch(setup = setup_bubble_cross_warm)]
|
||||
fn bubble_cross_tree(mut m: bubble_cross::BubbleCross<8>) -> bubble_cross::BubbleCross<8> {
|
||||
@@ -589,7 +597,7 @@ fn bubble_cross_tree(mut m: bubble_cross::BubbleCross<8>) -> bubble_cross::Bubbl
|
||||
m
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
#[library_benchmark]
|
||||
#[bench::chain(setup = setup_chain_warm)]
|
||||
fn chain_self_emit(mut m: chain::Chain<8>) -> chain::Chain<8> {
|
||||
@@ -602,7 +610,7 @@ fn chain_self_emit(mut m: chain::Chain<8>) -> chain::Chain<8> {
|
||||
m
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
library_benchmark_group!(
|
||||
name = dispatch;
|
||||
benchmarks =
|
||||
@@ -621,5 +629,5 @@ library_benchmark_group!(
|
||||
chain_self_emit,
|
||||
);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", not(any(feature = "tokio", feature = "embassy"))))]
|
||||
main!(library_benchmark_groups = dispatch);
|
||||
Reference in new issue
Block a user