Generator + scaffolder: emit per-root-object Criterion benchmarks from OSchema #113
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_blueprint#113
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
The scaffolder already emits a complete shape from an OSchema: typed SDK (#60), CRUD round-trip tests (
tests_emit.rs), runnable examples (examples.rs), per-root-object_admin+_webUI (#98). The missing peer is benchmarks — there's no canonical way today to track per-operation latency for a Hero service, nor a regression signal when codegen / dispatch changes (rpc2_adapter,hero_rpc2).This issue adds per-root-object Criterion benchmarks, emitted from the schema the same way the existing test/example/UI scaffolds are, with output that lives in a workspace-root
benches/directory.What this does
crates/generator/src/generate/benches_emit.rs(mirrors the existingtests_emit.rs/examples.rspattern).benches/directory at workspace root, scaffolded once +generated/subfolder ignored per #96.new,get,set,list,list_full,delete,exists..oschema.Concrete deliverables
Workspace-root layout
The
Cargo.tomldeclares each generated file as a[[bench]]entry withharness = falseso Criterion drives the loop. Standardcargo benchruns everything;cargo bench -- <entity>filters per object.Bench shape (per root object)
runner.rs(scaffolded once) wires up the in-process service + tokio runtime and calls each generatedbench_<entity>group. Contributor editsrunner.rsto customize fixtures or add custom groups; regeneratesgenerated/freely.Fixture generation
The emitter introspects each root object's fields and produces a default fixture (zero/empty values per type) so the benches compile out of the box. The contributor refines fixtures in
runner.rsfor realistic payloads.What to do
runner.rsspins up the service in-process viahero_rpc2::ServerBuilder. Wait for sign-off.crates/generator/src/generate/benches_emit.rsmirroringtests_emit.rs/examples.rs.crates/generator/src/build/scaffold.rsandcrates/generator/src/bin/scaffold.rs(default-on;--no-benchesto skip).example/recipe_server/—cargo benchruns clean against the in-process service.hero_service_scaffold.mdskill inhero_skillsto document thebenches/folder +--no-benchesflag.Acceptance
recipe_server/benches/exists, gets regenerated oncargo build, andcargo benchruns all CRUD benches against an in-process service with no manual setup.git statusclean aftercargo build(benches/generated/ignored).--no-benchesskips the entirebenches/scaffold.lab service new <name>produces a complete bench scaffold alongside the existing test/example/UI scaffolds.Out of scope
criterion --baselineis enough out of the box).runner.rs.Related
crates/generator/src/generate/tests_emit.rs,examples.rstests/) #115Design — per-root-object Criterion benches scaffold
Posting before coding per the workflow ask. Below is the proposed shape; flagging two judgement calls inline that need a thumbs-up before I implement.
1. Emitter layout
New emitter at
crates/generator/src/generate/benches_emit.rs, sibling totests_emit.rsandexamples.rs. Same module structure (impl Generator { pub(in crate::generate) fn generate_benches_file(&self, result: &mut GenerationResult) -> Result<()> }), same call site frombuild/scaffold.rs. Liketests_emit.rsit walksSchema::definitions, filters forobj.is_root_object || has_sid, and emits onebench_<entity>(c: &mut Criterion, client: &Arc<Client>, rt: &tokio::runtime::Handle)function per root object.The
benches_emit.rscodegen is invoked from the codegen path (OschemaBuilder), not from the one-shot scaffolder — same astests_emit.rs. It outputs to<workspace>/benches/generated/<entity>_crud.rs, with a<workspace>/benches/generated/mod.rsthat re-exports each module sorunner.rscanuse generated::*;.Workspace-level
benches/(Cargo.toml +.gitignore+runner.rs) is scaffolded-once frombuild/scaffold.rs::generate_benches_dir, parallel togenerate_examples_crate. Codegen never overwrites these three.2. Per-bench-group shape
Each generated
bench_<entity>function takes the sharedclient+ tokioHandle, so the runner only spins up the in-process server once. Criterion'sblock_onpattern for async viart.block_on(...):Notes:
.bench_functioneverywhere exceptdeletewhich uses.bench_with_input(viaiter_batched) to set up a fresh sid per iteration without measuring the setup. Justified becausedeletemutates global state; the other six are idempotent against the seeded sid.rt.block_on(...)— Criterion does not natively support async; this is the standardcriterion::async_executor::TokioExecutoralternative (which requirescriterion = { features = ["async_tokio"] }— happy to switch to that if preferred, slightly less verbose).<domain>_methods.rsas a sibling — same shape, oneg.bench_function(method_name, ...)per declared method. Fixture defaults per param type (see §5).3.
runner.rs— in-process spin-upMirrors
examples/recipe_server/crates/hero_recipes_examples/examples/01_walkthrough.rsstep 6 verbatim. Scaffolded-once so contributors can customize fixtures / add custom groups:Per-entity
bench_<entity>calls insidemainare themselves codegen output (ingenerated/mod.rsas apub fn run_all(c: &mut Criterion, client: &Arc<…>, rt: &Handle) { bench_recipe(…); bench_collection(…); }) sorunner.rsstays one-line per regeneration:That way
runner.rsremains truly handwritten — adding/removing root objects in the schema doesn't require editing it.4. Cargo.toml +
[[bench]]entriesThe issue body says "declares each generated file as a
[[bench]]entry". Reading that literally, each<entity>_crud.rswould be its own Cargo[[bench]]target — but each generated file is just apub fn bench_<entity>(…)library module, not a runnable harness withcriterion_main!(). To make them standalone runnable bins they'd each need their own server spin-up, which would slowcargo benchto a crawl and complicate fixture sharing.Proposed: one
[[bench]]entry pointing atrunner.rs, which is the single criterion driver and composes all the generated groups. Filtering per entity stays available viacargo bench -- recipe(Criterion's regex filter on group names). This matches howtests.rsis structured (one file, many#[test]fns).Open question: OK with the single-entry shape? If you'd rather have one
[[bench]]per entity (separate processes per entity bench run), I'll switch — it's a one-line emitter change but doubles the cold-start cost.The benches crate becomes a workspace member (added to root
Cargo.toml's[workspace] members). Gating: skipped entirely when--no-benchesis passed (mirroring--no-web).5. Fixture defaults per OSchema field type
Same table as the
_admin/_webform-input defaults (hero_rpc#98, seeui_emit::FieldKind::input_html), specialized for Rust-side defaults rather than HTML inputs:FieldKindStrString::new()SignedInt0UnsignedInt0Float0.0BoolfalseOTimeOTime::default()Enum(_)<Enum>::default()(codegen already emits#[derive(Default)]+#[default]on the first variant)PrimitiveListVec::new()JsonDefault::default()(struct types all deriveDefault)Since every generated
[rootobject]already has#[derive(Default)](verified onRecipe/Collectionin the recipe_server), the emitter can collapse the per-field fixture to a single<Type>::default()— no need to enumerate fields. For service-method benches, the emitter inspects each param type and emits the per-field defaults inline.6. Scaffolder wiring
Mirroring
with_web/without_web/--no-webexactly (PR #103 /ac70c7e):WorkspaceScaffolder::generate_benches: boolfield, defaulttruewith_benches()/without_benches()buildersbin/scaffold.rs:--no-benchesflag →scaffolder.without_benches()scaffold()callsgenerate_benches_dir(&workspace_dir, &mut result)?when the flag is on, aftergenerate_examples_cratecreate_workspace_dirsadds thebenches/directory creationgenerate_workspace_cargo_tomladds"benches"to[workspace] memberswhen the flag is ongenerate_benches_dirwrites the three preserved files:Cargo.toml,.gitignore(generated/),runner.rsTests added to
crates/generator/src/build/scaffold.rs::tests:test_scaffolder_benches_default_on_with_without_toggle(mirrorstest_scaffolder_web_default_on_with_without_toggle)test_scaffold_emits_benches_crate(mirrorstest_scaffold_emits_examples_crate)test_scaffold_no_benches_skips_dir7. Known risk — OTime serde mismatch
01_walkthrough.rscalls out a pre-existing serde shape mismatch onOTimebetween SDK and OSIS server —recipe.set+recipe.getwith a realReciperound-trip cleanly when the schema doesn't useOTime(the recipe schema doesn't), but a future schema that does will fail theset/getbench paths. If this bites during recipe_server validation I'll either (a) skipset/get/delete/existsbenches for objects whose schema containsOTimefields with a// FIXME: see hero_rpc#…comment, or (b) seed via the OSIS domain handler directly insiderunner.rs(bypassing wire ser/deser for setup only — the bench still measures the wire path).I'll start with (b) since it's strictly more robust and keeps the bench surface uniform. Acceptable?
Acceptance plan
Recipe_server validation steps:
cd examples/recipe_server && cargo bench— all 14 benches (7 ops × 2 root objects) run green.cargo bench -- recipe/list— filter works.git statusclean (benches/generated/ignored).cargo run -p hero_rpc_generator -- --name foo --workspace-dir /tmp/foo --no-benches— nobenches/dir.cargo run -p hero_rpc_generator -- --name foo --workspace-dir /tmp/foo—benches/dir present,cargo buildclean.Sign-off needed on:
[[bench]]for runner.rs vs. one per entity (§4) — proposing single.criterion = { features = ["async_tokio"] }vs. manualrt.block_on(§2) — proposing manual block_on (one fewer feature flag in a crate that already has plenty).Will hold on coding until the thumbs-up. Worktree at
/tmp/hero_rpc_113, branchissue-113-benchesalready created offorigin/development(HEAD at2aeee22).tests/) #115tests/) #115Closing as superseded.
Two issues have absorbed the original scope:
crates/osis_benches/— the Criterion harness for OSIS storage / index / wire-path benches against the template's newbenchdomain. That's the perf-regression surface this was meant to provide.lab service --start --ephemeralsubprocess as cargo tests / nu scripts. The per-service per-rootobject SDK-trait micro-bench scaffold the original design described would diverge from that single-bootstrap principle.If we later want per-rootobject SDK-trait CRUD micro-benches on top of the
osis_benchesharness, file as a new issue against the post-#124 shape. The original design comment is preserved here for context.