perf(render): pre-size generated view output

This commit is contained in:
slhx agent
2026-07-13 09:53:26 +02:00
parent 7db05356ae
commit d32f3cb893
6 changed files with 103 additions and 2 deletions
+1 -1
View File
@@ -57,7 +57,7 @@ Keep it stable. Prefer pointers to canonical sources over copied structure, file
- Use the same Workout command surface for tests, production build, and mobile release: `cargo run -p hemx-xtask -- workout test`, `cargo run -p hemx-xtask -- workout build`, `HEMX_WORKOUT_ORIGIN=https://workout.example.com cargo run -p hemx-xtask -- workout mobile-release`, and `HEMX_WORKOUT_ORIGIN=https://workout.example.com cargo run -p hemx-xtask -- workout mobile-verify`; Android/iOS SDKs, store submission targets, and signing remain external blockers, not repo-owned secrets, and do not imply a broad `hemx-mobile` framework. req: examples/006 req: examples/011 req: examples/013 - Use the same Workout command surface for tests, production build, and mobile release: `cargo run -p hemx-xtask -- workout test`, `cargo run -p hemx-xtask -- workout build`, `HEMX_WORKOUT_ORIGIN=https://workout.example.com cargo run -p hemx-xtask -- workout mobile-release`, and `HEMX_WORKOUT_ORIGIN=https://workout.example.com cargo run -p hemx-xtask -- workout mobile-verify`; Android/iOS SDKs, store submission targets, and signing remain external blockers, not repo-owned secrets, and do not imply a broad `hemx-mobile` framework. req: examples/006 req: examples/011 req: examples/013
- hemx core stays small: effects, typed ids, registries, and wire schema only; keep features in core only when they fit typed resources plus the closed EffectBatch op set, and treat DOM details as runtime lowering. Workspace crates stay separated, stable-Rust-compatible, and free of kitchen-sink boundaries; new primitives must delete special cases. Public identifiers should flow through typed wrappers over internal `ResourceId`/`ResourceRef`, not special-case opcodes. Wire output lowers symbolic authoring names to compact metadata and postcard/form-encoded envelopes, not JSON. ABI/schema versions and build fingerprints must guard runtime/server compatibility. v0 scope is the checked hypermedia core plus page/runtime/wire/diagnostic/test/axum proof, not optional sync/wasm/query/auth/router breadth. req: v0_scope/001 req: v0_scope/002 req: v0_scope/005 req: laws/001 req: invariant/001 req: invariant/005 req: typed_id/001 req: typed_id/003 req: effect_algebra/001 req: effect_algebra/006 req: wire/001 req: wire/002 req: wire/003 req: wire/004 req: wire/005 req: wire/006 req: abi/001 req: abi/002 req: abi/003 req: abi/004 req: abi/005 req: misc/001 req: misc/002 req: misc/003 req: misc/004 req: misc/005 req: misc/006 req: misc/007 req: misc/008 req: misc/009 req: misc/010 - hemx core stays small: effects, typed ids, registries, and wire schema only; keep features in core only when they fit typed resources plus the closed EffectBatch op set, and treat DOM details as runtime lowering. Workspace crates stay separated, stable-Rust-compatible, and free of kitchen-sink boundaries; new primitives must delete special cases. Public identifiers should flow through typed wrappers over internal `ResourceId`/`ResourceRef`, not special-case opcodes. Wire output lowers symbolic authoring names to compact metadata and postcard/form-encoded envelopes, not JSON. ABI/schema versions and build fingerprints must guard runtime/server compatibility. v0 scope is the checked hypermedia core plus page/runtime/wire/diagnostic/test/axum proof, not optional sync/wasm/query/auth/router breadth. req: v0_scope/001 req: v0_scope/002 req: v0_scope/005 req: laws/001 req: invariant/001 req: invariant/005 req: typed_id/001 req: typed_id/003 req: effect_algebra/001 req: effect_algebra/006 req: wire/001 req: wire/002 req: wire/003 req: wire/004 req: wire/005 req: wire/006 req: abi/001 req: abi/002 req: abi/003 req: abi/004 req: abi/005 req: misc/001 req: misc/002 req: misc/003 req: misc/004 req: misc/005 req: misc/006 req: misc/007 req: misc/008 req: misc/009 req: misc/010
- Routing, auth, sessions, transport, transitions, sync, async data helpers, multipart parsing/uploads, and storage belong in integration/user crates; hemx-axum preserves normal HTTP auth, credentials, CSRF, multipart/browser fallback, and progressive-enhancement semantics rather than defining policy in core. Sync is optional integration state reconciliation over push/transport, not core. req: auth/001 req: auth/002 req: auth/003 req: auth/004 req: auth/005 req: async_data/001 req: async_data/002 req: async_data/003 req: multipart/001 req: multipart/002 req: multipart/003 req: sync/001 req: sync/008 - Routing, auth, sessions, transport, transitions, sync, async data helpers, multipart parsing/uploads, and storage belong in integration/user crates; hemx-axum preserves normal HTTP auth, credentials, CSRF, multipart/browser fallback, and progressive-enhancement semantics rather than defining policy in core. Sync is optional integration state reconciliation over push/transport, not core. req: auth/001 req: auth/002 req: auth/003 req: auth/004 req: auth/005 req: async_data/001 req: async_data/002 req: async_data/003 req: multipart/001 req: multipart/002 req: multipart/003 req: sync/001 req: sync/008
- Public examples and beginner APIs should use templates plus Rust, generated component APIs, resources, view wrappers, render/page helpers, `#[hemx::app]`, plain `#[hemx::handler]` functions, and `IntoEffect`, not atoms, raw ids, selectors, wire formats, runtime opcodes, manual registries, `$OUT_DIR` includes, raw render/lower calls, raw HTML construction, imperative DOM mutation, or raw effect constructors; keep advanced layers out of starters. req: canonical_authoring/001 req: canonical_authoring/004 req: canonical_authoring/006 req: canonical_authoring/010 req: canonical_authoring/015 req: invariant/003 req: dx/001 req: dx/002 req: dx/010 req: component/003 req: component/004 req: view/001 req: view/002 req: html_safety/001 req: html_safety/003 req: html_safety/005 req: public_api/001 req: public_api/002 req: public_api/003 req: public_api/005 req: public_api/006 req: progressive_disclosure/001 req: progressive_disclosure/002 req: progressive_disclosure/003 req: derive_app/001 req: derive_app/002 req: derive_handler/001 req: derive_handler/002 req: derive_handler/003 req: derive_handler/004 req: derive_handler/005 - Public examples and beginner APIs should use templates plus Rust, generated component APIs, resources, view wrappers, render/page helpers, `#[hemx::app]`, plain `#[hemx::handler]` functions, and `IntoEffect`, not atoms, raw ids, selectors, wire formats, runtime opcodes, manual registries, `$OUT_DIR` includes, raw render/lower calls, raw HTML construction, imperative DOM mutation, or raw effect constructors; keep advanced layers out of starters. req: canonical_authoring/001 req: canonical_authoring/004 req: canonical_authoring/006 req: canonical_authoring/010 req: canonical_authoring/015 req: invariant/003 req: dx/001 req: dx/002 req: dx/010 req: component/003 req: component/004 req: view/001 req: view/002 req: view/003 req: html_safety/001 req: html_safety/003 req: html_safety/005 req: public_api/001 req: public_api/002 req: public_api/003 req: public_api/005 req: public_api/006 req: progressive_disclosure/001 req: progressive_disclosure/002 req: progressive_disclosure/003 req: derive_app/001 req: derive_app/002 req: derive_handler/001 req: derive_handler/002 req: derive_handler/003 req: derive_handler/004 req: derive_handler/005
- Typed partial swaps should stay expressed as generated target plus rendered partial plus swap kind, not selector-driven rerendering or response-side selector retargeting; HTTP, page navigation, push, and island behavior adapt around that loop, and docs should layer new primitives progressively. Navigation is an effect/page-swap concern, not a core router framework; enhanced links and GET forms preserve real URL/history semantics so page state stays reloadable/shareable without a client state graph. Push streams carry postcard EffectBatch over server-owned SSE/WebSocket transport and keep `data-hemx-sse` root-scoped/same-origin by default. Preserve keyed/optional scope identity for addressable loop nodes, reconcile filtered keyed collections without clearing retained rows, prefer generated keyed-slot helpers over low-level keyed calls, and route self/row-update diagnostics toward local `data-hemx-slot`/`h-key` targets. req: canonical_authoring/002 req: canonical_authoring/014 req: modes/001 req: scope/001 req: list/001 req: list/002 req: list/003 req: list/004 req: list/005 req: list/006 req: nav/001 req: nav/002 req: nav/003 req: nav/004 req: nav/005 req: push/001 req: push/002 req: push/003 req: push/004 req: push/005 req: push/006 req: push/007 req: progressive_disclosure/004 req: page_swap/001 req: page_swap/002 req: page_swap/003 req: locality/001 req: locality/002 req: target_policy/001 req: target_policy/002 - Typed partial swaps should stay expressed as generated target plus rendered partial plus swap kind, not selector-driven rerendering or response-side selector retargeting; HTTP, page navigation, push, and island behavior adapt around that loop, and docs should layer new primitives progressively. Navigation is an effect/page-swap concern, not a core router framework; enhanced links and GET forms preserve real URL/history semantics so page state stays reloadable/shareable without a client state graph. Push streams carry postcard EffectBatch over server-owned SSE/WebSocket transport and keep `data-hemx-sse` root-scoped/same-origin by default. Preserve keyed/optional scope identity for addressable loop nodes, reconcile filtered keyed collections without clearing retained rows, prefer generated keyed-slot helpers over low-level keyed calls, and route self/row-update diagnostics toward local `data-hemx-slot`/`h-key` targets. req: canonical_authoring/002 req: canonical_authoring/014 req: modes/001 req: scope/001 req: list/001 req: list/002 req: list/003 req: list/004 req: list/005 req: list/006 req: nav/001 req: nav/002 req: nav/003 req: nav/004 req: nav/005 req: push/001 req: push/002 req: push/003 req: push/004 req: push/005 req: push/006 req: push/007 req: progressive_disclosure/004 req: page_swap/001 req: page_swap/002 req: page_swap/003 req: locality/001 req: locality/002 req: target_policy/001 req: target_policy/002
- `examples/html_examples` is the copy-paste HTML pattern gallery for htmx-style examples; keep exact htmx URL slugs visible while translating behavior to boring `.heml`, generated resources, and server-owned Rust state, not HTMX syntax, selector targeting, or user-authored browser JavaScript. Shared runtime loading and declarative `data-hemx-*` are allowed. Boost containers enhance same-origin descendants only and preserve native external/download/new-tab behavior. req: htmx_equivalents/001 req: htmx_equivalents/003 req: htmx_equivalents/005 req: examples/005 req: examples/007 req: examples/012 req: page_swap/007 req: page_swap/008 - `examples/html_examples` is the copy-paste HTML pattern gallery for htmx-style examples; keep exact htmx URL slugs visible while translating behavior to boring `.heml`, generated resources, and server-owned Rust state, not HTMX syntax, selector targeting, or user-authored browser JavaScript. Shared runtime loading and declarative `data-hemx-*` are allowed. Boost containers enhance same-origin descendants only and preserve native external/download/new-tab behavior. req: htmx_equivalents/001 req: htmx_equivalents/003 req: htmx_equivalents/005 req: examples/005 req: examples/007 req: examples/012 req: page_swap/007 req: page_swap/008
- Use `cargo run -p hemx-xtask -- app new PATH` for the generic page/form/keyed-row/notice starter, and `cargo run -p hemx-xtask -- app new --mobile PATH` for the phone-first starter with host capabilities, recovery truth, and release-kit commands; do not treat it as a mobile framework or store-submission bot. req: ceremony/005 req: ceremony/006 req: ceremony/007 - Use `cargo run -p hemx-xtask -- app new PATH` for the generic page/form/keyed-row/notice starter, and `cargo run -p hemx-xtask -- app new --mobile PATH` for the phone-first starter with host capabilities, recovery truth, and release-kit commands; do not treat it as a mobile framework or store-submission bot. req: ceremony/005 req: ceremony/006 req: ceremony/007
+3
View File
@@ -794,6 +794,9 @@ what a valid business email is. [north_star]
### req: view/002 ### req: view/002
0 002 Generated slot types may target `Display`, `Hemplate`, or explicit view wrappers. Type errors suggest the expected renderable view type. [north_star] 0 002 Generated slot types may target `Display`, `Hemplate`, or explicit view wrappers. Type errors suggest the expected renderable view type. [north_star]
### req: view/003
0 003 Generated `Hemplate` views expose a template-derived size hint, and trusted hemx render helpers use it to pre-size output without changing rendered HTML. [north_star]
--- ---
## diagnostics ## diagnostics
+4
View File
@@ -6,6 +6,10 @@ edition.workspace = true
[lib] [lib]
path = "src/lib.rs" path = "src/lib.rs"
[[bench]]
name = "render"
harness = false
[dependencies] [dependencies]
hemplate = { path = "../../hemplate/hemplate" } hemplate = { path = "../../hemplate/hemplate" }
hemx-core = { path = "../hemx-core" } hemx-core = { path = "../hemx-core" }
+58
View File
@@ -0,0 +1,58 @@
//! Generated-view rendering baseline: `cargo bench -p hemx --bench render`.
use hemplate::Hemplate;
use std::hint::black_box;
use std::time::Instant;
const ITERATIONS: u32 = 100_000;
const ROUNDS: usize = 7;
#[derive(Hemplate)]
struct BenchPage {
title: String,
body: String,
items: Vec<String>,
}
fn page() -> BenchPage {
BenchPage {
title: String::from("Production readiness"),
body: "Checked hypermedia without a parallel client application. ".repeat(4),
items: (0..20).map(|index| format!("item-{index}")).collect(),
}
}
fn median_ns(mut render: impl FnMut() -> usize) -> u128 {
for _ in 0..10_000 {
black_box(render());
}
let mut rounds = [0_u128; ROUNDS];
for round in &mut rounds {
let start = Instant::now();
for _ in 0..ITERATIONS {
black_box(render());
}
*round = start.elapsed().as_nanos() / u128::from(ITERATIONS);
}
rounds.sort_unstable();
rounds[ROUNDS / 2]
}
fn main() {
let view = page();
let unhinted = median_ns(|| {
let mut html = String::new();
view.render_into(&mut html).expect("generated view renders");
black_box(html.len())
});
let hinted = median_ns(|| {
let mut html = String::with_capacity(view.size_hint());
view.render_into(&mut html).expect("generated view renders");
black_box(html.len())
});
println!(
"unhinted_median_ns={unhinted} hinted_median_ns={hinted} size_hint={} html_len={}",
view.size_hint(),
view.render().expect("generated view renders").len(),
);
}
+26 -1
View File
@@ -80,7 +80,7 @@ pub mod __private {
} }
fn render_template(view: &impl hemplate::Hemplate) -> Html { fn render_template(view: &impl hemplate::Hemplate) -> Html {
let mut html = String::new(); let mut html = String::with_capacity(view.size_hint());
view.render_into(&mut html) view.render_into(&mut html)
.expect("hemplate view renders into hemx effect payload"); .expect("hemplate view renders into hemx effect payload");
__private::html_trusted(html) __private::html_trusted(html)
@@ -186,6 +186,8 @@ pub mod prelude {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::cell::Cell;
struct InlineView; struct InlineView;
impl hemplate::Hemplate for InlineView { impl hemplate::Hemplate for InlineView {
@@ -195,6 +197,29 @@ mod tests {
} }
} }
struct HintTrackingView(Cell<bool>);
impl hemplate::Hemplate for HintTrackingView {
fn render_into(&self, out: &mut String) -> Result<(), hemplate::error::HemplateError> {
out.push_str("<strong>hinted</strong>");
Ok(())
}
fn size_hint(&self) -> usize {
self.0.set(true);
23
}
}
#[test]
fn trusted_render_path_uses_the_view_size_hint() {
// req: view/003 req: html_safety/002
let view = HintTrackingView(Cell::new(false));
assert_eq!(crate::page(&view).as_str(), "<strong>hinted</strong>");
assert!(view.0.get(), "trusted render path must consult size_hint");
}
#[test] #[test]
fn page_is_the_short_safe_html_helper() { fn page_is_the_short_safe_html_helper() {
// req: dx/006 req: html_safety/002 // req: dx/006 req: html_safety/002
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head><title>{+ self.title +}</title></head>
<body>
<h1>{+ self.title +}</h1>
<p>{+ self.body +}</p>
<ul>
<li h-for="item in &self.items">{+ item +}</li>
</ul>
</body>
</html>