From 3d1108240ed6e18172fac557928f8d7b596425c6 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Tue, 25 Aug 2026 21:36:23 +0200 Subject: [PATCH] =?UTF-8?q?perf(cascade):=20measure=20the=20flight=20casca?= =?UTF-8?q?de=20=E2=80=94=201.2413=20us/tick,=20estimator=20is=2091%=20(PE?= =?UTF-8?q?RF-P01)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Performance was the ONE axis on which this project could make no claim at all. `engine-throughput` has guarded the cFS engines since v1.56; the control path that actually flies had no timing baseline of any kind. The examples/*-bench binaries measure estimator ACCURACY, not time. A compositional WCET proof exists in Lean and had never been confronted with a measurement. MEASURED (cargo bench, Apple-silicon dev host, release, 2026-08-25): iekf_propagate [1.1249 1.1256 1.1263] us 91.0 % of the cascade position_tick [73.359 73.403 73.451] ns 5.9 % attitude_tick [15.456 15.465 15.476] ns 1.2 % rate_tick [10.245 10.252 10.261] ns 0.8 % mixer_mix [ 9.4437 9.4491 9.4549] ns 0.8 % full_cascade_tick [1.2406 1.2413 1.2421] us <- per-tick control loop CI spread < 0.1 % — stable measurements, not noise. TWO FINDINGS. (i) THE ESTIMATOR IS THE CASCADE. IEKF is 91 %; the other four together are ~108 ns. Any cascade performance work that is not estimator work is rounding error. Independently corroborated: jess's scry STATIC stack bounds put iekf at 4192 B against 16-112 B for the others. Dynamic timing here, static analysis there, neither derived from the other, same conclusion. (ii) THE 1 kHz BUDGET IS NOT CLOSE. 1.2413 us against a 1 ms period is 0.12 % of budget, ~806x margin on this host. Even allowing an order of magnitude for a Cortex-M7 at a fraction of the clock, the control math will not bound the loop — the HAL, scheduler and sensor I/O will. `full_cascade_tick` is the honest per-tick figure, not a synthetic sum: the cascade is single-rate, `cascade.step()` runs every stage once per tick with no divider (wasm/cm/cascade/src/lib.rs). WHAT THIS DOES NOT ESTABLISH, stated because the limits are the point: - not on-silicon (aarch64 host, warm cache, a branch predictor an M7 lacks) - not a comparison — the PX4 head-to-head on the same board is criterion (d) and is NOT done. We can say what our cascade costs, not that it costs less. - not a tight SLA — no persisted history, so ceilings are coarse ~10x catastrophic guards, same limitation FV-FALCON-PERF-001 records. PERF-P01 and FV-FALCON-PERF-002 therefore land at `implemented`, not `verified`. Criterion (d) is what would promote them, and nobody has taken that measurement. Also: the `cargo bench` step is NOT marked bench-only, because `cargo bench` is not in BENCH_PATTERNS and the marker would claim something untrue — it genuinely runs in the gate (62s), which is better evidence than a skip. gate (has-tag "performance"): 7 steps PASS, 0 FAIL rivet validate: PASS, exit 0 Refs #8. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG --- Cargo.lock | 12 ++ Cargo.toml | 1 + artifacts/swreq/SWREQ-FALCON-PERF-P01.yaml | 69 ++++++++ .../verification/FV-FALCON-PERF-002.yaml | 70 ++++++++ benches/cascade-throughput/BASELINE.md | 81 +++++++++ benches/cascade-throughput/Cargo.toml | 20 +++ .../benches/cascade_throughput.rs | 164 ++++++++++++++++++ 7 files changed, 417 insertions(+) create mode 100644 artifacts/swreq/SWREQ-FALCON-PERF-P01.yaml create mode 100644 artifacts/verification/FV-FALCON-PERF-002.yaml create mode 100644 benches/cascade-throughput/BASELINE.md create mode 100644 benches/cascade-throughput/Cargo.toml create mode 100644 benches/cascade-throughput/benches/cascade_throughput.rs diff --git a/Cargo.lock b/Cargo.lock index ceb7a081..d28b37e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,6 +56,18 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +[[package]] +name = "cascade-throughput-bench" +version = "0.1.0" +dependencies = [ + "criterion", + "relay-att", + "relay-iekf", + "relay-mix-quad", + "relay-pos", + "relay-rate", +] + [[package]] name = "cast" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index f08d7132..4cd31cff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "crates/relay-ccsds", "crates/relay-cfdp", "benches/engine-throughput", + "benches/cascade-throughput", "crates/relay-sec", "crates/relay-offboard", "crates/relay-fsafe", diff --git a/artifacts/swreq/SWREQ-FALCON-PERF-P01.yaml b/artifacts/swreq/SWREQ-FALCON-PERF-P01.yaml new file mode 100644 index 00000000..dfd23901 --- /dev/null +++ b/artifacts/swreq/SWREQ-FALCON-PERF-P01.yaml @@ -0,0 +1,69 @@ +artifacts: + - id: SWREQ-FALCON-PERF-P01 + type: sw-req + title: "PERF-P01 — the flight cascade has a measured per-cycle baseline, and a regression guard on it" + status: implemented + release: falcon-v1.135.0 + description: > + The falcon control cascade shall have a recorded per-cycle timing + baseline, per stage and for the full tick, guarded against regression. + + WHY IT EXISTS — performance was the one axis on which this project could + make NO claim at all. `engine-throughput` has guarded the cFS-lineage + engines (LC/SCH/SC/HS/CFDP) since v1.56, but the control path that + actually flies — iekf, position, attitude, rate, mixer — had no timing + baseline of any kind. The `examples/*-bench` binaries measure estimator + ACCURACY, not time. A compositional WCET proof exists in Lean; no + measurement had ever been taken against it. + + That gap mattered strategically, not just tidily. The architecture has + real structural reasons to be fast — no RTOS on the control path, no + dynamic allocation, a bounded arena, fused single address space — and + none of it was evidenced. "Proven correct AND measurably faster on the + same silicon" is a claim no open-source, verified-systems or commercial + alternative can answer; it was also entirely unsubstantiated here. + + MEASURED (2026-08-25, Apple-silicon dev host, release profile): + + iekf_propagate 1.1256 us 91.0 % of the cascade + position_tick 73.40 ns 5.9 % + attitude_tick 15.47 ns 1.2 % + rate_tick 10.25 ns 0.8 % + mixer_mix 9.45 ns 0.8 % + full_cascade_tick 1.2413 us <- the per-tick control-loop cost + + TWO FINDINGS FALL OUT. + + (i) THE ESTIMATOR IS THE CASCADE. IEKF is 91 % of per-tick cost; the other + four stages together are ~108 ns. Any cascade performance work that is not + estimator work is rounding error. This agrees with an independent + measurement — jess's scry static stack bounds put iekf at 4 192 B against + 16-112 B for the others — so two unrelated methods identify the same stage + as carrying the mass. + + (ii) THE 1 kHz BUDGET IS NOT CLOSE. 1.2413 us against a 1 ms period is + 0.12 % of budget, ~806x margin on this host. Even allowing an order of + magnitude for a Cortex-M7 at a fraction of the clock, the control math is + not what will bound the loop — the HAL, scheduler and sensor I/O will. + + `full_cascade_tick` is the honest per-tick figure rather than a synthetic + sum, because the cascade is SINGLE-RATE: `cascade.step()` executes every + stage once per tick with no divider (verified in + wasm/cm/cascade/src/lib.rs). + tags: [falcon, performance, wcet, cascade, benchmark, v1.135] + fields: + req-type: performance + priority: shall + verification-criteria: > + (a) A criterion benchmark exists covering all five cascade stages AND + the full tick, with a recorded baseline in + benches/cascade-throughput/BASELINE.md. (b) The baseline states its + host and explicitly disclaims being a cross-machine SLA or an + on-silicon figure. (c) A catastrophic-regression ceiling is recorded + per bench. (d) NOT DISCHARGEABLE BY (a)-(c): a head-to-head against + PX4's equivalent loop on the SAME board. Until that runs, this + requirement establishes what our cascade costs, NOT that it costs less + than the alternative — and the second half is the claim worth having. + links: + - type: derives-from + target: SYSREQ-FALCON-001 diff --git a/artifacts/verification/FV-FALCON-PERF-002.yaml b/artifacts/verification/FV-FALCON-PERF-002.yaml new file mode 100644 index 00000000..fed91110 --- /dev/null +++ b/artifacts/verification/FV-FALCON-PERF-002.yaml @@ -0,0 +1,70 @@ +artifacts: + - id: FV-FALCON-PERF-002 + type: sw-verification + title: "PERF-P01 — flight cascade per-cycle baseline: 1.2413 us/tick, estimator 91% (v1.135)" + status: implemented + release: falcon-v1.135.0 + description: > + Verification of SWREQ-FALCON-PERF-P01. + + (a) CONFIGURATION (CI-runnable, below). The benchmark crate exists, covers + all five stages plus the full tick, and a baseline with per-bench + regression ceilings is recorded. These steps fail if the bench is deleted, + if a stage stops being covered, or if the baseline file goes missing — + which is the regression worth catching, since nothing else in CI would + notice the flight path losing its only timing evidence. + + (b) MEASURED, by execution — `cargo bench -p cascade-throughput-bench`, + Apple-silicon dev host, release profile, 2026-08-25: + + iekf_propagate [1.1249 1.1256 1.1263] us + position_tick [73.359 73.403 73.451] ns + attitude_tick [15.456 15.465 15.476] ns + rate_tick [10.245 10.252 10.261] ns + mixer_mix [ 9.4437 9.4491 9.4549] ns + full_cascade_tick [1.2406 1.2413 1.2421] us + + Confidence intervals are tight (<0.1 % spread), so these are stable + measurements rather than noise. + + CROSS-CHECK, and it is the useful part: the timing split identifies iekf + as 91 % of the cascade, and jess's scry STATIC STACK bounds independently + put iekf at 4 192 B against 16-112 B for the other four. Two unrelated + methods — dynamic timing here, static analysis there — agree on which + stage carries the mass. Neither was derived from the other. + + SCOPE (honest, and the limits are the point): + + - NOT an on-silicon number. aarch64 dev host with a warm cache and a + branch predictor a Cortex-M7 does not have. The Lean compositional WCET + proof bounds cycle counts; this does not measure them. + - NOT a comparison. The PX4 head-to-head on the same board is criterion + (d) of the requirement and has not been run. We can currently say what + our cascade costs, not that it costs less. + - NOT a tight SLA. No persisted baseline history, so the ceilings are + coarse catastrophic-regression guards (~10x) chosen to survive hardware + variance — the same limitation FV-FALCON-PERF-001 records for the + engine benches. + + This artifact therefore stays `implemented`. Criterion (d) is what would + make it `verified`, and it is a measurement nobody has taken yet. + tags: [verification, falcon, performance, wcet, cascade, benchmark, v1.135] + fields: + method: test + steps: + # (a) the bench exists and covers every stage plus the full tick + - run: "test -f benches/cascade-throughput/benches/cascade_throughput.rs" + - run: "grep -q 'full_cascade_tick' benches/cascade-throughput/benches/cascade_throughput.rs" + - run: "test \"$(grep -c 'c.bench_function' benches/cascade-throughput/benches/cascade_throughput.rs)\" -eq 6" + # (b) a baseline with regression ceilings is recorded + - run: "grep -q 'full_cascade_tick' benches/cascade-throughput/BASELINE.md" + - run: "grep -q 'Catastrophic-regression ceiling' benches/cascade-throughput/BASELINE.md" + # (c) execution evidence — this one genuinely RUNS in the gate (~62s). + # NOT marked bench-only: `cargo bench` is not in BENCH_PATTERNS, so a + # "# bench-only" comment here would claim something untrue. It executes, + # which is better evidence than a skip, and FV-FALCON-PERF-001 already + # pays the same cost for the engine benches. + - run: "cargo bench -p cascade-throughput-bench" + links: + - type: verifies + target: SWREQ-FALCON-PERF-P01 diff --git a/benches/cascade-throughput/BASELINE.md b/benches/cascade-throughput/BASELINE.md new file mode 100644 index 00000000..e8dfc9e9 --- /dev/null +++ b/benches/cascade-throughput/BASELINE.md @@ -0,0 +1,81 @@ +# Flight cascade throughput — per-cycle baseline (PERF-P01, #8) + +Per-cycle wall-time for each stage of the falcon control cascade, and for the +whole cascade in sequence. `engine-throughput/BASELINE.md` covers the +cFS-lineage engines (LC/SCH/SC/HS/CFDP); **this file covers the control path +that actually flies**, which had no timing baseline of any kind before v1.135. + +Run: `cargo bench -p cascade-throughput-bench` +Traced by **FV-FALCON-PERF-002**. + +## Baseline (2026-08-25) + +Apple-silicon dev host, release profile. **Absolute numbers are +hardware-specific** — treat them as a relative regression baseline on +like-for-like hardware, not as a cross-machine SLA and not as an on-silicon +figure. + +| Stage | Hot path | Median | Share of cascade | +|---|---|---|---| +| `iekf_propagate` | one propagate at dt = 1 ms | **1.1256 µs** | **91 %** | +| `position_tick` | NED pos/vel → attitude setpoint | 73.40 ns | 5.9 % | +| `attitude_tick` | geometric SO(3), quat error → rate setpoint | 15.47 ns | 1.2 % | +| `rate_tick` | body-rate PID → torque | 10.25 ns | 0.8 % | +| `mixer_mix` | torque + thrust → 4 motor commands | 9.45 ns | 0.8 % | +| **`full_cascade_tick`** | **all five in sequence, one tick** | **1.2413 µs** | — | + +## What the numbers say + +**The estimator is the cascade.** IEKF is 91 % of per-tick cost; the other four +stages together are ~108 ns. Any work on cascade performance that is not work on +the estimator is rounding error. This matches the independent evidence from +jess's scry static bounds, which put iekf at 4 192 B of stack against 16–112 B +for the other four — two unrelated measurements agreeing on which stage carries +the mass. + +**The 1 kHz budget is not close.** `full_cascade_tick` is 1.2413 µs against a +1 ms rate-loop period — **0.12 % of budget, ~806× margin** on this host. Even +allowing an order of magnitude for a Cortex-M7 at a fraction of the clock, the +control math is not what will bound the loop; the HAL, the scheduler and sensor +I/O will. + +**`full_cascade_tick` is the honest per-tick figure** because the cascade is +single-rate: `cascade.step()` executes every stage once per tick, with no +divider (verified in `wasm/cm/cascade/src/lib.rs`). It is not a synthetic sum. + +## What this does NOT establish + +- **Not an on-silicon number.** This is an aarch64 dev host with a warm cache + and a branch predictor a Cortex-M7 does not have. The compositional WCET proof + in `proofs/lean/` bounds the cycle count; this bench does not measure it. +- **Not yet a comparison.** The PX4 head-to-head on the same board is the second + half of PERF-P01 and is not done. Until it is, we can say what our cascade + costs, not that it costs less than the alternative. +- **Not a tight SLA.** No persisted baseline history, so CI can only guard + against catastrophic regression, not drift. Same limitation the + engine-throughput bench records. + +## Regression guard + +A 10× regression in `full_cascade_tick` — or any stage crossing 10× its median +here — is an early warning that the control path is drifting toward the WCET +ceiling the Lean proofs bound. The ceilings are deliberately coarse so they +survive hardware variance between this host and CI runners. + +| Bench | Baseline | Catastrophic-regression ceiling | +|---|---|---| +| `full_cascade_tick` | 1.2413 µs | 15 µs | +| `iekf_propagate` | 1.1256 µs | 15 µs | +| `position_tick` | 73.40 ns | 1 µs | +| `attitude_tick` | 15.47 ns | 500 ns | +| `rate_tick` | 10.25 ns | 500 ns | +| `mixer_mix` | 9.45 ns | 500 ns | + +## Deferred + +- **PX4 head-to-head on the same board** — the claim this bench exists to make + possible. +- **On-silicon cycle counts** (Cortex-M7), which is what the WCET proof wants + confronting with reality. +- **Persisted baseline history** for a tight per-runner SLA, rather than the + coarse ceilings above. diff --git a/benches/cascade-throughput/Cargo.toml b/benches/cascade-throughput/Cargo.toml new file mode 100644 index 00000000..cfbe12bd --- /dev/null +++ b/benches/cascade-throughput/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "cascade-throughput-bench" +version = "0.1.0" +edition = "2021" +publish = false +description = "Per-cycle throughput benchmarks for the falcon FLIGHT CASCADE (iekf/position/attitude/rate/mixer). The engine-throughput bench guards the cFS engines; this one guards the control path that actually flies." + +[dependencies] +relay-iekf = { path = "../../crates/relay-iekf" } +relay-pos = { path = "../../crates/relay-pos" } +relay-att = { path = "../../crates/relay-att" } +relay-rate = { path = "../../crates/relay-rate" } +relay-mix-quad = { path = "../../crates/relay-mix-quad" } + +[dev-dependencies] +criterion = { version = "0.7", default-features = false, features = ["cargo_bench_support"] } + +[[bench]] +name = "cascade_throughput" +harness = false diff --git a/benches/cascade-throughput/benches/cascade_throughput.rs b/benches/cascade-throughput/benches/cascade_throughput.rs new file mode 100644 index 00000000..19f0dd90 --- /dev/null +++ b/benches/cascade-throughput/benches/cascade_throughput.rs @@ -0,0 +1,164 @@ +//! Per-cycle throughput benchmarks for the falcon FLIGHT CASCADE. +//! +//! `engine-throughput` guards the cFS-lineage engines (LC/SCH/SC/HS/CFDP). +//! This one guards the control path that actually flies, which had NO timing +//! baseline at all until now (#8, PERF-P01) — a gap that made "performance" +//! the one axis on which the project could make no claim whatsoever. +//! +//! These are NOT microbenchmarks of isolated functions. Each bench drives one +//! cascade stage's per-cycle hot path with realistic inputs, and `full_cascade` +//! drives all five IN SEQUENCE — which is the number that matters, because the +//! cascade is single-rate: `cascade.step()` runs every stage once per tick +//! (verified in wasm/cm/cascade/src/lib.rs; there is no rate divider). +//! +//! So `full_cascade` IS the per-tick control-loop cost, and at a 1 kHz rate +//! loop it must fit inside 1 ms with margin for the HAL and scheduler. +//! +//! Run: cargo bench -p cascade-throughput-bench +//! Baseline: benches/cascade-throughput/BASELINE.md +//! Traced by FV-FALCON-PERF-002 (PERF-P01). + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +use relay_att::{AttController, Timestamp as AttTime}; +use relay_iekf::{Iekf, Imu}; +use relay_mix_quad::QuadMixer; +use relay_pos::{Ned, PosController, PositionSetpoint, Timestamp as PosTime}; +use relay_rate::{RatePid, Timestamp as RateTime}; + +/// A gently-moving hover sample. Deliberately NOT zeros: a zeroed IMU takes +/// different branches in the estimator's normalisation paths and would make the +/// benchmark measure an unrepresentative code path. +const GYRO: [f32; 3] = [0.012, -0.008, 0.003]; +const ACCEL: [f32; 3] = [0.15, -0.09, -9.79]; +const DT: f32 = 0.001; // 1 kHz rate loop + +fn ts_rate(ms: u64) -> RateTime { + RateTime { seconds: ms / 1000, fraction: ((ms % 1000) * (1u64 << 32) / 1000) as u32 } +} +fn ts_att(ms: u64) -> AttTime { + AttTime { seconds: ms / 1000, fraction: ((ms % 1000) * (1u64 << 32) / 1000) as u32 } +} +fn ts_pos(ms: u64) -> PosTime { + PosTime { seconds: ms / 1000, fraction: ((ms % 1000) * (1u64 << 32) / 1000) as u32 } +} + +/// IEKF — one propagate step at the rate-loop dt. The estimator is the heaviest +/// stage (scry bounds it at 4192 B of stack against 16-112 B for the others), +/// so it dominates the cascade budget. +fn bench_iekf(c: &mut Criterion) { + c.bench_function("iekf_propagate", |b| { + let mut ekf = Iekf::level(); + let mut t = 0u64; + b.iter(|| { + t = t.wrapping_add(1); + ekf.propagate(black_box(Imu { gyro: GYRO, accel: ACCEL }), black_box(DT)); + black_box(ekf.state()) + }) + }); +} + +/// Position loop — outer cascade stage, NED position/velocity to attitude +/// setpoint. Hover at 5 m with a small offset so the controller has real error +/// to act on. +fn bench_position(c: &mut Criterion) { + c.bench_function("position_tick", |b| { + let mut pos = PosController::new(); + let sp = PositionSetpoint { + position_ned: [0.0, 0.0, -5.0] as Ned, + velocity_ned: [0.0, 0.0, 0.0] as Ned, + yaw_setpoint: 0.0, + }; + let mut ms = 0u64; + b.iter(|| { + ms = ms.wrapping_add(20); // 50 Hz outer loop cadence + black_box(pos.tick( + black_box(ts_pos(ms)), + black_box([0.3, -0.2, -4.8]), + black_box([0.05, -0.03, 0.01]), + black_box([1.0, 0.0, 0.0, 0.0]), + black_box(sp), + )) + }) + }); +} + +/// Attitude loop — geometric SO(3), quaternion error to body-rate setpoint. +fn bench_attitude(c: &mut Criterion) { + c.bench_function("attitude_tick", |b| { + let mut att = AttController::new(); + let mut ms = 0u64; + b.iter(|| { + ms = ms.wrapping_add(5); // 200 Hz + black_box(att.tick( + black_box(ts_att(ms)), + black_box([0.9998, 0.012, -0.008, 0.004]), + black_box([1.0, 0.0, 0.0, 0.0]), + )) + }) + }); +} + +/// Rate loop — the innermost, fastest stage. PID on body rates. +fn bench_rate(c: &mut Criterion) { + c.bench_function("rate_tick", |b| { + let mut pid = RatePid::new(); + let mut ms = 0u64; + b.iter(|| { + ms = ms.wrapping_add(1); // 1 kHz + black_box(pid.tick(black_box(ts_rate(ms)), black_box(GYRO), black_box([0.0, 0.0, 0.0]))) + }) + }); +} + +/// Mixer — control allocation, torque+thrust to four motor commands. +fn bench_mixer(c: &mut Criterion) { + c.bench_function("mixer_mix", |b| { + let mut mixer = QuadMixer::new(); + b.iter(|| black_box(mixer.mix(black_box([0.01, -0.02, 0.005]), black_box(0.55)))) + }); +} + +/// THE NUMBER THAT MATTERS — all five stages in sequence, exactly as +/// `cascade.step()` orders them. Single-rate: one execution of each per tick. +/// At 1 kHz this must fit in 1 ms with margin for the HAL and scheduler. +fn bench_full_cascade(c: &mut Criterion) { + c.bench_function("full_cascade_tick", |b| { + let mut ekf = Iekf::level(); + let mut pos = PosController::new(); + let mut att = AttController::new(); + let mut pid = RatePid::new(); + let mut mixer = QuadMixer::new(); + let sp = PositionSetpoint { + position_ned: [0.0, 0.0, -5.0] as Ned, + velocity_ned: [0.0, 0.0, 0.0] as Ned, + yaw_setpoint: 0.0, + }; + let mut ms = 0u64; + b.iter(|| { + ms = ms.wrapping_add(1); + // 1. estimator + ekf.propagate(black_box(Imu { gyro: GYRO, accel: ACCEL }), DT); + let st = ekf.state(); + // 2. position -> attitude setpoint + let att_sp = pos.tick(ts_pos(ms), [0.3, -0.2, -4.8], [0.05, -0.03, 0.01], [1.0, 0.0, 0.0, 0.0], sp); + // 3. attitude -> rate setpoint + let rate_sp = att.tick(ts_att(ms), att_sp.quaternion, [1.0, 0.0, 0.0, 0.0]); + // 4. rate -> torque + let torque = pid.tick(ts_rate(ms), GYRO, rate_sp); + // 5. mixer -> motor commands + black_box((mixer.mix(torque, att_sp.thrust), st)) + }) + }); +} + +criterion_group!( + cascade, + bench_iekf, + bench_position, + bench_attitude, + bench_rate, + bench_mixer, + bench_full_cascade +); +criterion_main!(cascade);