Optimize Rust code until nothing left to improve. Loops automatically. Use when the user says "optimize" or "/optimize".
Recursos
1Instal·la
npx skillscat add katopz/katgpt-rs/rust-optimize Instal·la des del registre de SkillsCat.
Rust Optimization
Optimize *.rs files in the target scope using the checklist below.
This is a LOOP. You keep optimizing until you cannot find anything to optimize.
Termination
After each turn, end your response with exactly one of:
Continue optimizing remaining files.— you made code changes this turnNo optimizations this pass.— you read files but changed nothing
If you only summarized what you read without changing code, say "no optimizations this pass".
Checklist
- Complexity: O(n) scan → O(1) lookup, merge HashMap lookups, merge loops, boundary-vs-volume (Stokes/divergence theorem: integrate boundary
O(n^{(d-1)/d})instead of interiorO(n)for low-dim d≤3 region mass queries — curse-of-dim caps it at d≤3) - Allocations:
String→&'static str, pre-allocate, eliminate.to_string() - Layout: field reorder (u64→u32→u8),
#[repr(u8)]enums, remove#[repr(C)] - Arithmetic:
f32counters →u32/u64 - Iterators: fix double
chunks_exact(), use index arithmetic - Concurrency:
Arc<RwLock<HashMap>>→papaya,Mutex<u32>→AtomicU32 - SIMD: chunked loops, branch-free inner loops
- Caching: pre-compute lookup tables, compute once not N×M
- Errors:
unwrap()→?,let _ =→.log_err() - Bevy / Real-time loop (see § Bevy / Real-time Loop for details):
- Event loop spin:
WinitSettings::default()(Continuous) pegs CPU at 100% even idle — switch toUpdateMode::reactive_low_power(max_wait)matching tick rate - Vsync ≠ event loop:
PresentMode::Fifocontrols GPU only, independent of CPU loop spin - Static geometry: bake to
LineList/TriangleListmesh once, NEVER redraw viagizmos.line()per-frame (each call is CPU + per-frame buffer grow) - Asset lifecycle: despawning a Bevy
Entitydoes NOT release itsAssets<Mesh>/Assets<StandardMaterial>handles — callmeshes.remove(handle.0.id())+materials.remove(...)on despawn or leak indefinitely (regrow-on-new-EntityId patterns leak ~N assets per cycle) - GUI repaint storms:
bevy_eguisetsset_request_repaint_callbackthat firesbevy_winit::WakeUpondelay.is_zero()— bypasses reactivemax_waitand spins the loop. Diagnose via in-process perf overlay before assuming the engine itself is the hog. - Frame-rate cap fallback: a
Last-schedulestd::thread::sleepsystem capped at e.g. 60 FPS is a belt-and-suspenders defense when vsync + reactive mode somehow don't throttle
- Event loop spin:
- Perf diagnosis: ship an in-process perf overlay (
sysinfofor CPU/RAM + Bevy'sTime::delta()for FPS) inside the GUI itself — externalpssampling misses focused-window swings and requires keeping a terminal attached
Rules
- Do NOT create plans or issues.
- Do NOT create new files unless necessary.
- Commit when done (
perf:orrefactor:prefix). - Use sigmoid, not softmax.
Optimization Skill
Hot-path Rust optimization patterns. Apply to any microsecond-sensitive code.
When to Optimize
- Profile first — never optimize without numbers
- Identify the top 3 bottlenecks (80% of time is in 20% of code)
- Measure after each change — some "optimizations" make things worse
- Run both debug (reveals algorithmic cost) and release (reveals compiler optimizations)
Do
Profiling
- Break down complex functions into per-component micro-benchmarks
- Use
std::hint::black_box()to prevent dead-code elimination - Warm up before measuring (100+ iterations) to prime CPU caches
- Run 10,000+ iterations for stable results
- Print component-level breakdowns with
--nocapturetest harness - Compare same-commit, back-to-back runs to isolate feature impact from system noise
Data Structures
- Use fixed-size arrays
[T; N]when domain is bounded - Pre-compute lookup tables once, store in config/context — O(1) reads beat O(n) scans
- Track per-slot aggregates during insert/evict instead of scanning on read
- Cache allocations:
Vec::with_capacity()once,clear()+ reuse across calls - Pass pre-allocated scratch buffers as
&mut [T]parameters instead of allocating inside hot loops
Manifold / Cell-Complex Geometry
For code operating on cell complexes / meshes / grid manifolds (DEC, FEM, game maps):
- Boundary-vs-volume (Stokes / divergence theorem): to compute a region's total mass / energy / activation magnitude, integrate over the boundary
∂M(surface area,O(n^{(d-1)/d})cells) instead of the interiorM(volume,O(n)cells). Valid when the field is curl-free / exact; reconstruction error is bounded by the harmonic component (compute via Hodge decomposition). Win shrinks fast as dimension d grows — practical only for d ≤ 3 (2D game maps, 3D belief regions, KG embeddings). For d ≥ 8 (HLA state) or d ≥ 64 (style weights) the boundary is larger than the interior, so boundary-only is a loss. - Conservation-by-construction: identity
curl(grad)=0/div(curl)=0enforced by DEC operator construction (not a soft penalty) gives mass-conservation invariants for free. Use as a modelless validator: ifdiv(flow) > τ, mass leaked/created = anomaly. - Pre-compute incidence / Hodge on topology change only: DEC operators (
exterior_derivative,codifferential,hodge_decompose) depend only on the cell complex topology, not on the field values. Compute once on map/complex load, cache, invalidate only when topology changes — zero per-tick DEC op cost on a stable map. - Cache the Hodge spectrum / Betti numbers alongside the operators — they are topology invariants reused across every field query.
SIMD / Auto-vectorization
Auto-vectorization (let LLVM do the work):
- Write chunked loops (4 or 8 elements at a time) to help LLVM auto-vectorize
- Cast
usize→u64slices (same layout on 64-bit) for wider SIMD lanes - Use
u64equality comparison — compiler maps to_mm256_cmpeq_epi64on AVX2 - Keep inner loops branch-free (use
bool as usizeinstead ofif) - Verify with release build — SIMD benefits only appear with optimizations enabled
Portable std::simd recipes (when auto-vec isn't enough):
Reach for std::simd only after profiling shows auto-vectorization failing. Patterns below are distilled from mcyoung's vb64 writeup (https://mcyoung.xyz/2023/11/27/simd-base64/). Full skeleton in recipes/swizzle_lookup.rs.
- Branchless range dispatch: replace
matchon byte ranges withsimd_ge/simd_lemasks +mask.select(splat_a, splat_b). 1 select beats N branches. - Perfect-hash lookup via
swizzle_dyn: if(byte >> 4) - (byte == c)distinguishes all ranges, build an 8-entry offset table and do 1 shuffle instead of N compares. Index vector must be same width as lookup table. - Widening cast for sub-byte packing:
sextets.cast::<u16>() << Simd::from([2,4,6,8]), then split intolo = v.cast::<u8>()andhi = (v >> 8).cast::<u8>(), OR them after rotating hi by 1 lane. Lets bits cross byte boundaries without per-bit ops. - Lane-deletion swizzle: when every k-th lane is garbage, use a const swizzle
|i| i + i/(k-1)to skip those lanes. Compile-time indices = singlevpshufb. - Slop-buffer commit:
out.reserve(final_len + N/4), write full SIMD vectors viaptr.cast::<Simd<u8,N>>().write_unaligned(), only callset_len()after success. On error, never commit — garbage writes vanish. - Delayed failure: accumulate
error |= !okfrom each iteration, returnErronce after the loop. Errors are rare; don't pay branch cost per chunk. - Unroll-and-jam with overlapping loads: use
chunks_exact(N)for the hot path +Simd::from_slice(). For the remainder, loadu64frompandp+len-8(overlap by 1 byte), OR them — 2 loads cover any 8–15 byte tail.
Decision rules:
- SIMD wins: lane count ≥ 8, branch-heavy parse/codec, no allocator in loop, input ≥ 16 bytes
- Scalar wins: input < 16 bytes, branch predicts well (profile says so), cold path, or auto-vec already covers it
swizzle_dynrequires index vector length == lookup table length — pad the table if needed- Bound all generic SIMD fns with
LaneCount<N>: SupportedLaneCount - Tune
Nby benchmark; on x86-64 with AVX2,N = 32(one YMM) is usually optimal
Parallelism / Rayon
- Only parallelize when per-task work exceeds thread-pool overhead (~5μs for rayon)
- Benchmark serial vs parallel at actual workload size before committing
- Rule of thumb: parallelism wins only when per-iteration work > 10μs or count > 1000
- Use
rayon::join(|| left(), || right())for recursive divide-and-conquer — the primitive that powers Rayon; work-stealing ensures threads don't sit idle - Use
.par_sort()and.par_extend()instead of manual.par_iter().collect()— Rayon provides optimized parallel versions of stdlib algorithms - Use custom
ThreadPoolto isolate core usage (e.g., reserve CPU for a web server):
let pool = rayon::ThreadPoolBuilder::new().num_threads(4).build().unwrap();
pool.install(|| { /* parallel code here */ });- Prefer contiguous data (
Vec, slices) — Rayon splits chunks efficiently;LinkedListforces traversal before splitting - Profile before and after with
criterion— warm-up cost and orchestration overhead may negate gains
Parallelism: When to Use What
| Feature | std::iter |
rayon::par_iter |
tokio::spawn |
|---|---|---|---|
| Best For | Small data / Simple logic | Big data / CPU-heavy | I/O / Networking |
| Overhead | Zero | Medium (task splitting) | High (runtime / context switch) |
| Execution | Sequential | Multi-threaded (parallel) | Concurrent (event loop) |
Allocation
- Pre-build lookup tables and cached data in config structs via builder pattern
- Reuse scratch buffers across loop iterations instead of allocating per-iteration
- Pre-allocate output arrays upfront, write in-place instead of collecting per-iteration
- Reorder struct fields to eliminate padding (group by alignment: u64 → u32 → u8)
- Use
#[repr(u8)]on field-less enums to guarantee 1-byte size
WASM FFI / Sandboxed Execution
- Batch API: Serialize shared state once, validate all N×M combinations in one FFI call (amortize ~250ns FFI floor across 24 pairs → 5–6× speedup)
- Zero-copy serialization: Use fixed-size stack buffers (
[u8; 1024]) instead ofVec::with_capacity()— eliminates allocator overhead in tight loops (~3.6× faster) - Fuel budgeting: Set WASM fuel proportional to worst-case algorithmic complexity, not average case. BFS on bounded grids with N entities can spike 4–5× above typical. Fuzz-test with max inputs to find the ceiling.
- Lock-free instance pools: Use
papaya::HashMap<ThreadId, T>for per-thread WASM stores — lock-free reads on existing entries, uncontendedMutexper thread. Better than a single globalMutexfor multi-threaded servers. - Batch state layout: Omit per-item data from shared state; pass player/entity arrays separately. Grid(169) + bombs(N×4) shared once, then per-entity
(id, x, y)array alongside action indices and output results buffer. - TypedFunc clone:
wasmtime::TypedFuncis cheap to clone (handle index). Clone it to release&selfborrow before calling mutableStoremethods — avoids borrow-checker conflicts with zero cost.
Caching
- Compute once per position/context, not per-sample-per-position (N×M → M calls)
- Pre-compute values that don't change across samples (entropy, base path, threshold)
Bevy / Real-time Loop
Real-time render loops fail in ways the standard CPU profile doesn't catch. The hot CPU path can be 0% algorithmic work — it can be the event loop itself spinning between frames. Patterns below are distilled from the bevy_egui_orchard_demo 100%-CPU saga (commits d6fb415 → 565524f → 01b4687 in riir-game-sdk).
Event loop spin (bevyengine/bevy#10261)
Bevy's default WinitSettings uses UpdateMode::Continuous, which spins the winit event loop as fast as possible. Even an empty Bevy app sits at ~50% CPU on macOS (Apple Silicon). This is a known Bevy issue — not user code.
Fix: switch to reactive mode so the loop parks between events:
use bevy::winit::{UpdateMode, WinitSettings};
use std::time::Duration;
app.insert_resource(WinitSettings {
// max_wait = sim tick rate. Scene can't visibly change faster than the
// sim produces state, so waiting match-tick-rate between idle redraws is free.
focused_mode: UpdateMode::reactive_low_power(Duration::from_millis(50)), // 20 Hz sim
unfocused_mode: UpdateMode::reactive_low_power(Duration::from_secs(1)),
});Vsync is independent. PresentMode::Fifo controls GPU-side blocking on the swapchain; it does NOT prevent the CPU event loop from spinning. You need BOTH PresentMode::Fifo (GPU vsync) AND WinitSettings::reactive_low_power (CPU loop).
Static geometry → bake once, draw never
NEVER redraw static line/mesh data via per-frame APIs:
// BAD: ~4k gizmos.line() calls per frame, each pushing to a per-frame CPU buffer
fn draw_overlays_system(mut gizmos: Gizmos, overlays: Res<Overlays>) {
for &[a, b] in &overlays.grid_lines { gizmos.line(a, b, COLOR); } // 728 calls
for &[a, b] in &overlays.isolines { gizmos.line(a, b, COLOR); } // 3450 calls
}
// GOOD: bake into a LineList mesh once at Startup, render cost is GPU-only
fn setup_scene(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>, overlays: Res<Overlays>) {
let mesh = build_line_list_mesh(&overlays.grid_lines, &overlays.isolines);
commands.spawn((
Mesh3d(meshes.add(mesh)),
MeshMaterial3d(materials.add(StandardMaterial { unlit: true, ..default() })),
));
}Asset lifecycle (despawn ≠ release)
Despawning a Bevy Entity does NOT release its asset handles. The Assets<T> cache is keyed by AssetId and only grows.
// BAD: leaks mesh + material forever. Regrow-on-new-EntityId patterns
// (apples harvested + respawned) compound: each cycle adds N leaked assets.
for (be, _mirror) in &mirrors {
if !present.contains(&mirror.snap_id) {
commands.entity(be).despawn(); // entity gone, mesh + material still in cache
}
}
// GOOD: release assets alongside despawn
for (be, _mirror, mesh_h, mat_h) in &mirrors {
if !present.contains(&mirror.snap_id) {
meshes.remove(mesh_h.0.id());
materials.remove(mat_h.0.id());
commands.entity(be).despawn();
}
}Symptom: RSS grows linearly over a long session (e.g. 200MB → 900MB over an hour). The bevy_egui_orchard_demo saga hit exactly this.
GUI repaint-request storms
bevy_egui (and similar immediate-mode GUI integrations) install a set_request_repaint_callback that fires bevy_winit::WakeUp events on delay.is_zero(). These bypass the reactive max_wait — the loop wakes immediately, processes the egui frame, egui repaints again, fires another WakeUp, infinite 60+ Hz loop.
Important nuance (verified in egui 0.33 source): plain ui.label(format!(...)) does NOT call Context::request_repaint(). Repaints originate only from: hover/click animations (only while in-progress), tooltips, scroll areas, CollapsingHeader, SidePanel/TopBottomPanel resize handles, explicit ctx.request_repaint() calls. A format!("frames {}", n) label whose value changes every frame does NOT itself drive repaints — but it IS wasteful (the displayed number flickers). Throttle per-frame-changing displayed values to 1 Hz for readability, not for perf.
Diagnose via in-process perf overlay before assuming the engine itself is the hog. If ORCHARD_NO_EGUI=1 drops CPU dramatically, the culprit is egui-side. Use finer-grained bisection (ORCHARD_NO_PERF_OVERLAY, ORCHARD_NO_MINIMAP, ORCHARD_NO_DEBUG_PANEL) to identify WHICH panel — typically it's a resize handle the cursor is parked over, or a hover-state animation that hasn't settled.
Mitigations:
- Override
ctx.set_request_repaint_callbackto enforce a min delay - Use egui's
RunningMode::Reactiveif exposed - Accept the cost — UI responsiveness requires repaints on hover/click
- Gate individual panels behind env vars so the user can bisect
Frame-rate cap fallback
A Last-schedule sleep is a brute-force defense when vsync + reactive mode somehow don't throttle (driver quirks, repaint storms, etc.):
fn frame_rate_cap_system(mut last_frame_end: Local<Option<Instant>>) {
const TARGET_FPS: u64 = 60;
let target = Duration::from_secs_f32(1.0 / TARGET_FPS as f32);
let now = Instant::now();
if let Some(last) = *last_frame_end {
let elapsed = now.duration_since(last);
if elapsed < target { std::thread::sleep(target - elapsed); }
}
*last_frame_end = Some(Instant::now());
}
app.add_systems(Last, frame_rate_cap_system);In-process perf overlay
External ps -p $PID -o %cpu,rss sampling misses per-frame swings and requires a terminal attached. Ship a perf overlay inside the GUI itself so the user can see what's burning CPU while they interact:
use sysinfo::{Pid, ProcessRefreshKind, ProcessesToUpdate, System};
#[derive(Resource)]
struct PerfMonitor {
sys: System,
pid: Pid,
last_refresh: Instant,
cpu_pct: f32,
rss_bytes: u64,
fps_history: Vec<f32>,
cpu_history: Vec<f32>,
}
impl PerfMonitor {
fn refresh(&mut self) {
let now = Instant::now();
if now.duration_since(self.last_refresh) < Duration::from_millis(250) { return; }
self.sys.refresh_processes_specifics(
ProcessesToUpdate::Some(&[self.pid]), false, ProcessRefreshKind::everything(),
);
if let Some(p) = self.sys.process(self.pid) {
self.cpu_pct = p.cpu_usage(); // 100.0 = 1 core fully used
self.rss_bytes = p.memory(); // bytes on macOS, bytes on Linux
}
self.last_refresh = now;
}
}Note sysinfo MSRV: 0.32 supports Rust 1.93; 0.39+ requires 1.95. The sysinfo API changed between versions (refresh_process_specifics in 0.39 → refresh_processes_specifics(ProcessesToUpdate::Some(&[pid]), ...) in 0.32).
Bisection via env vars
When a perf issue only manifests under specific conditions (focused window, mouse hover, etc.), ship env-var-driven kill switches so the user can isolate the culprit without code changes:
struct RunFlags {
no_egui: bool, // ORCHARD_NO_EGUI=1 — kills entire EguiPlugin
no_perf_overlay: bool, // ORCHARD_NO_PERF_OVERLAY=1 — hides only the 📊 perf window
no_minimap: bool, // ORCHARD_NO_MINIMAP=1 — hides only the left-panel minimap
no_debug_panel: bool, // ORCHARD_NO_DEBUG_PANEL=1 — hides only the bottom debug panel
no_shadows: bool, // ORCHARD_NO_SHADOWS=1 — disables shadow-map rendering
no_sim: bool, // ORCHARD_NO_SIM=1 — skips the per-tick sim step
no_overlays: bool, // ORCHARD_NO_OVERLAYS=1 — skips the static LineList overlay mesh
bench_secs: Option<f32>, // ORCHARD_BENCH=<n> auto-exits after n seconds
}The user runs each variant for ~10s, notes CPU%, and the env var that drops CPU is the culprit subsystem. Start coarse (NO_EGUI) then go fine (NO_PERF_OVERLAY / NO_MINIMAP / NO_DEBUG_PANEL) — this distinguishes "egui as a whole" from "a specific panel" and avoids mis-attributing the cause to the wrong surface.
Don't
Don't: Rayon for tiny workloads
// BAD: rayon overhead (~5μs) >> computation (~0.1μs per row)
let counts: Vec<_> = (0..10).into_par_iter()
.map(|i| compute_row(variants, i))
.collect();
// GOOD: serial for small m
let counts: Vec<_> = (0..10)
.map(|i| compute_row(variants, i))
.collect();Threshold: rayon wins only at m ≥ 64 with μs/row work, or m ≥ 1000 with ns/row work.
Don't: GPU for microsecond workloads
GPU kernel launch overhead is ~50μs. If your computation is 2-5μs, GPU is a net negative.
GPU wins only for: batched matmul, large tensor ops, or when you can amortize launch across many ops.
Don't: Allocate inside hot loops
// BAD: allocates every call, every sample, every position
for sample in 0..10 {
let support = rule.support(vocab_size); // Vec allocation!
}
// GOOD: pre-compute once, reuse
let config = Config::default().with_cached_data(size);
for sample in 0..10 {
let support = config.data_for(rule); // &[T] — zero alloc
}Don't: Linear scan for hot-path queries
// BAD: O(n) scan per query
fn query(&self, key: usize) -> f32 {
for item in &self.items {
if item.key == key { ... }
}
}
// GOOD: O(1) precomputed index
struct Store {
stats: [SlotStats; MAX_SLOTS], // updated on insert/evict
}
fn query(&self, key: usize) -> f32 {
self.stats[key].rate() // O(1)
}Don't: Recompute unchanged values
// BAD: same value recomputed N×M times
for sample in 0..N {
for &pos in &positions {
let h = expensive_calc(data[pos]); // SAME value every sample!
}
}
// GOOD: compute once per position
let cache: Vec<f32> = positions.iter()
.map(|&pos| expensive_calc(data[pos]))
.collect(); // M calls instead of N×MDon't: Parallelize without measuring
Always benchmark before AND after adding parallelism. If the serial version is faster, keep serial.
Parallel overhead: thread wake (~2μs) + work stealing (~3μs) + synchronization.
If your total work is < 10μs, parallelism will make it slower.
Don't: Use Mutex in Rayon closures
Mutex introduces contention — 16 threads fighting for one lock effectively run sequentially (or slower).
Prefer atomic types or reduce/fold patterns:
// BAD: shared Mutex — threads serialize on lock
let results = Mutex::new(Vec::new());
(0..1000).into_par_iter().for_each(|i| {
results.lock().unwrap().push(compute(i)); // contention!
});
// GOOD: map + collect — threads work independently, merge at end
let results: Vec<_> = (0..1000).into_par_iter()
.map(|i| compute(i))
.collect();Don't: Ignore Rayon panic propagation
If a closure inside a Rayon thread panics, Rayon propagates that panic to the calling thread.
This can crash your entire application if not handled at the top level. Wrap parallel closures
in catch_unwind or ensure invariants are validated before entering Rayon.
Don't: Ignore cache locality in parallel splits
Splitting work too finely loses CPU cache benefits. Processing contiguous chunks is faster than
jumping across memory addresses in parallel. Prefer chunk-based splitting over per-element parallelism
when data is large but per-element work is small.
Don't: Ignore binary bloat from feature flags
Adding code behind a feature flag still affects the entire binary when enabled:
- Larger binary → more icache misses → slower hot loops in unrelated code
- Feature-gated code in the same crate affects code layout and branch prediction
Mitigation:
- Isolate feature-gated benchmarks into separate binaries (
[[bin]]) or test files - Compare no-feature vs with-feature on the same commit, back-to-back
- If regressions appear only with feature enabled and code is properly gated, it's binary bloat, not a bug
Don't: Under-budget WASM fuel for complex algorithms
WASM fuel limits prevent infinite loops but can silently trap legitimate computation.
Complex BFS/graph algorithms with N entities on bounded domains can spike well above average:
// BAD: fuel based on average case, traps on worst case
const FUEL_PER_CALL: u64 = 10_000; // sufficient for 1–2 bombs
// BFS with 4+ bombs × 4 directions × range × 169 cells = ~40K ops → SILENT TRAP
// GOOD: fuel based on worst-case analysis + headroom
const FUEL_PER_CALL: u64 = 50_000; // 16 bombs × 4 dirs × range 3 × 169 cells ≈ 40K + marginSymptom: WASM returns false for valid inputs that should return true. Only manifests with complex inputs. Batch APIs may mask this if they use higher fuel multipliers. Fuzz-test with maximum entity counts to catch fuel traps.
Don't: Serialize per-item when state is shared across a batch
When validating N items against the same state (e.g., N players on one game grid), serializing the state N times wastes both allocation and FFI overhead:
// BAD: 24 × (serialize + FFI + compute) = ~12µs/tick
for player in 0..4 {
for action in 0..6 {
let state = serialize(grid, player, action); // 24 serializations!
wasm.is_valid(state); // 24 FFI calls!
}
}
// GOOD: 1 × (serialize + FFI + batch compute) = ~1.7µs/tick
let state = serialize_grid(grid, bombs); // 1 serialization
wasm.batch_validate(state, players, actions, results); // 1 FFI callThe batch API turns N×M individual calls into 1 call. The WASM module internally loops over all combinations, reusing the parsed state. For 4 players × 6 actions, this gives ~5.8× speedup.
Don't: Compare benchmarks across different CPU thermal states
Laptop CPUs throttle aggressively. A 30% "regression" may just be heat.
Always compare same-commit, back-to-back runs to isolate feature impact from system noise.
Profiling Template
// tests/prof_bench.rs — run with: cargo test --features X prof_bench -- --nocapture
#[cfg(feature = "X")]
#[test]
fn prof_components() {
let warmup = 100;
let iters = 10000;
for _ in 0..warmup { black_box(component_a()); }
let start = Instant::now();
for _ in 0..iters { black_box(component_a()); }
let t_a = start.elapsed();
// ... same pattern for component_b, component_c ...
println!(" Component A: {:.2} μs", t_a.as_micros() as f64 / iters as f64);
println!(" Total Δ: {:.2} μs", total.as_micros() as f64 / iters as f64);
}WASM FFI Batch Template
// Pattern: batch validate N items × M actions in one FFI call
//
// Memory layout written to WASM:
// [0..state_end) shared state (grid + bombs, no per-entity data)
// [players_off..+N×12) entity array: N × (id, x, y) as u32 LE
// [actions_off..+M×4) action indices as u32 LE
// [results_off..+N×M×4) output: u32 LE results (0/1 or Q16.16)
//
// WASM export signature:
// batch_is_valid(state_ptr, state_len, players_ptr, player_count,
// actions_ptr, action_count, results_ptr) -> u32
const MAX_ENTITIES: usize = 4;
const ACTION_COUNT: usize = 6;
const ACTIONS_BYTES: [u8; ACTION_COUNT * 4] = [0,0,0,0, 1,0,0,0, 2,0,0,0, 3,0,0,0, 4,0,0,0, 5,0,0,0];
fn batch_validate(&self, grid: &Grid, players: &[(u8,i32,i32)], bombs: &[Bomb]) -> BatchResult {
self.with_inner(|inner| {
// 1. Serialize shared state once (zero-copy stack buffer)
let (state_bytes, state_tokens) = inner.state_buf.serialize_grid(grid, bombs);
let mut tmp = [0u8; 1024];
tmp[..state_bytes].copy_from_slice(inner.state_buf.as_bytes(state_bytes));
// 2. Compute aligned offsets
let players_off = (state_bytes + 7) & !7; // align8
let actions_off = players_off + players.len() * 12;
let results_off = actions_off + ACTION_COUNT * 4;
// 3. Write to WASM memory
inner.write_memory(0, &tmp[..state_bytes])?;
inner.write_memory(players_off, &players_to_bytes(players))?;
inner.write_memory(actions_off, &ACTIONS_BYTES)?;
// 4. Call batch export
let batch_fn = inner.batch_fn.as_ref()?.clone();
batch_fn.call(&mut inner.store, (0, state_tokens, players_off as u32,
players.len() as u32, actions_off as u32, ACTION_COUNT as u32,
results_off as u32))?;
// 5. Read results
Some(BatchResult::from_memory(inner, results_off, players.len(), ACTION_COUNT))
})
}