SampleMode::Exact now retains exact samples for send call, timer error, and server processing, which previously always built their TimeMetric with retention disabled.
All eleven public timing metrics now follow the same Exact/RunningOnly retention contract, derived from one retain_exact decision in CoreStats::new.
No new capability API was needed. TimeStats::median_ns: Option<f64> already distinguishes a retained median from an absent one; the missing behavior was retention, not representation.
Behavior
Finite summaries gain real medians for the send call, timer error, and server processing rows when samples exist, instead of -.
Continuous mode remains bounded and medians remain unavailable for every timing metric; the existing continuous-mode note stays correct.
Absent optional server-processing measurements remain absent — a reply without a processing measurement adds no sample, and no zero/default value is synthesized.
Event semantics are unchanged: send call and timer error still come only from ClientEvent::EchoSent, and server processing only from replies that carry one.
LateReplyMode is untouched. A CountOnly matched late reply still contributes no timing measurements, so it does not enter server-processing exact retention.
Retention estimate
The estimator stays in irtt-stats. EXACT_SAMPLE_VECS_PER_REPLY = 8 becomes EXACT_SAMPLES_PER_PROBE = 11, since exact storage now includes send-side metrics associated with probes rather than replies.
Finite estimate on this target rises from 668 to 764 bytes per probe, reflecting real additional retention. The running-only ceiling is unchanged at ~1.6 MiB.
The estimator keeps PR #97's philosophy: approximate, conservative, size_of-grounded, deterministic, saturating, optional measurements assumed present for an upper bound, running-only bounded, time-based rolling excluded as documented.
exact_mode_retains_one_sample_per_counted_metric counts the public timing metrics a fully measured probe actually retains and asserts it equals EXACT_SAMPLES_PER_PROBE, so changing retention without revisiting the estimate fails in irtt-stats.
The CLI still calls StatsConfig::estimated_retained_bytes(...) and consumed the new figure automatically — no CLI test needed a hardcoded adjustment, and no frontend memory heuristic was added.
TUI layout. The TUI timing panel does not render a median column, so it is unaffected. PR #93's timing-panel wrapping observation is deliberately not addressed here.
Tests
irtt-stats: exact medians for send call (odd count), timer error (even count), and server processing; absent server processing retains no samples; running-only still counts those three but reports no median; a regression test asserting every timing metric with samples reports a median in exact mode; a CountOnly late-reply assertion that server processing gains no retained median.
irtt-stats retention: the estimate covers every retained timing sample, documented as an implementation test rather than a stable public constant. Existing linearity, saturation, running-only bound, rolling, and time-based-exclusion tests are unchanged and still pass.
irtt-cli: a finite summary test proving real median cells for the three rows using spread values whose median differs from min, mean, and max, plus a continuous counterpart asserting those cells stay -.
The client and tui feature-only builds emit the pre-existing cmd::format dead-code warnings (unused format_percent_ratio, format_count, format_optional_count, format_optional_ns_f64, format_signed_duration). They are unrelated to this change, are not errors under any required command, and were left alone.
## Summary
- `SampleMode::Exact` now retains exact samples for send call, timer error, and server processing, which previously always built their `TimeMetric` with retention disabled.
- All eleven public timing metrics now follow the same Exact/RunningOnly retention contract, derived from one `retain_exact` decision in `CoreStats::new`.
- No new capability API was needed. `TimeStats::median_ns: Option<f64>` already distinguishes a retained median from an absent one; the missing behavior was retention, not representation.
## Behavior
- Finite summaries gain real medians for the `send call`, `timer error`, and `server processing` rows when samples exist, instead of `-`.
- Continuous mode remains bounded and medians remain unavailable for every timing metric; the existing continuous-mode note stays correct.
- Absent optional server-processing measurements remain absent — a reply without a processing measurement adds no sample, and no zero/default value is synthesized.
- Event semantics are unchanged: send call and timer error still come only from `ClientEvent::EchoSent`, and server processing only from replies that carry one.
- `LateReplyMode` is untouched. A `CountOnly` matched late reply still contributes no timing measurements, so it does not enter server-processing exact retention.
## Retention estimate
- The estimator stays in `irtt-stats`. `EXACT_SAMPLE_VECS_PER_REPLY = 8` becomes `EXACT_SAMPLES_PER_PROBE = 11`, since exact storage now includes send-side metrics associated with probes rather than replies.
- Finite estimate on this target rises from 668 to 764 bytes per probe, reflecting real additional retention. The running-only ceiling is unchanged at ~1.6 MiB.
- The estimator keeps PR #97's philosophy: approximate, conservative, `size_of`-grounded, deterministic, saturating, optional measurements assumed present for an upper bound, running-only bounded, time-based rolling excluded as documented.
- `exact_mode_retains_one_sample_per_counted_metric` counts the public timing metrics a fully measured probe actually retains and asserts it equals `EXACT_SAMPLES_PER_PROBE`, so changing retention without revisiting the estimate fails in `irtt-stats`.
- The CLI still calls `StatsConfig::estimated_retained_bytes(...)` and consumed the new figure automatically — no CLI test needed a hardcoded adjustment, and no frontend memory heuristic was added.
## Scope
No changes to:
- the median algorithm or sample variance;
- the rolling API or rolling retention semantics;
- `LateReplyMode`;
- IPDV tracking or continuous memory bounds;
- `irtt-client`, `irtt-server`, or `irtt-proto` behavior;
- CLI summary columns, row ordering, units, formatting vocabulary, or verbosity rules;
- TUI layout. The TUI timing panel does not render a median column, so it is unaffected. PR #93's timing-panel wrapping observation is deliberately not addressed here.
## Tests
- `irtt-stats`: exact medians for send call (odd count), timer error (even count), and server processing; absent server processing retains no samples; running-only still counts those three but reports no median; a regression test asserting every timing metric with samples reports a median in exact mode; a `CountOnly` late-reply assertion that server processing gains no retained median.
- `irtt-stats` retention: the estimate covers every retained timing sample, documented as an implementation test rather than a stable public constant. Existing linearity, saturation, running-only bound, rolling, and time-based-exclusion tests are unchanged and still pass.
- `irtt-cli`: a finite summary test proving real median cells for the three rows using spread values whose median differs from min, mean, and max, plus a continuous counterpart asserting those cells stay `-`.
## Verification
All run on this branch, all passing:
- `cargo test -p irtt-stats --all-features --locked`
- `cargo test -p irtt-cli --all-features --locked`
- `cargo fmt --all -- --check`
- `cargo check --workspace --all-targets --all-features --locked`
- `cargo test --workspace --all-targets --all-features --locked`
- `cargo clippy --workspace --all-targets --all-features --locked -- -D warnings`
- `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps --locked`
- `cargo check -p irtt-cli --no-default-features --features client|tui|server --all-targets --locked`
- `git diff --check`
The `client` and `tui` feature-only builds emit the pre-existing `cmd::format` dead-code warnings (unused `format_percent_ratio`, `format_count`, `format_optional_count`, `format_optional_ns_f64`, `format_signed_duration`). They are unrelated to this change, are not errors under any required command, and were left alone.
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
### 💡 Codex Review
Here are some automated review suggestions for this pull request.
**Reviewed commit:** `db9bd515bb`
<details> <summary>ℹ️ About Codex in GitHub</summary>
<br/>
[Your team has set up Codex to review pull requests in this repo](https://chatgpt.com/codex/cloud/settings/general). Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
</details>
Qualify the exact-mode promise for rolling snapshots
When a caller enables rolling_count or rolling_time on an exact configuration, rolling_count() and rolling_time() still recompute their snapshots with CoreStats::new(SampleMode::RunningOnly, ...) in rolling.rs:62, so populated timing metrics return median_ns: None. The new promise that Exact makes every timing metric report a median is therefore false for the crate's public rolling snapshots; either preserve the configured sample mode during rolling recomputation or explicitly limit this contract to the cumulative snapshot.
**<sub><sub></sub></sub> Qualify the exact-mode promise for rolling snapshots**
When a caller enables `rolling_count` or `rolling_time` on an exact configuration, `rolling_count()` and `rolling_time()` still recompute their snapshots with `CoreStats::new(SampleMode::RunningOnly, ...)` in `rolling.rs:62`, so populated timing metrics return `median_ns: None`. The new promise that `Exact` makes every timing metric report a median is therefore false for the crate's public rolling snapshots; either preserve the configured sample mode during rolling recomputation or explicitly limit this contract to the cumulative snapshot.
AGENTS.md reference: [AGENTS.md:L53-L53](https://github.com/Lochnair/irtt-rs/blob/db9bd515bbbf2aae9da274abf85359f3fa2d54cf/AGENTS.md#L53-L53)
Useful? React with 👍 / 👎.
Correct — rolling snapshots recompute with RunningOnly in rolling.rs:62 regardless of config, so the new wording over-promised. Rolling semantics are out of scope for this PR, so I scoped the contract instead: SampleMode/finite()/samples now say the exact-median promise applies to the cumulative snapshot, rolling_count()/rolling_time() state plainly that they use running statistics only and report no median, and rolling_snapshots_report_no_medians_even_in_exact_mode pins both halves so the wording cannot go stale (71b5819).
Correct — rolling snapshots recompute with `RunningOnly` in `rolling.rs:62` regardless of config, so the new wording over-promised. Rolling semantics are out of scope for this PR, so I scoped the contract instead: `SampleMode`/`finite()`/`samples` now say the exact-median promise applies to the cumulative snapshot, `rolling_count()`/`rolling_time()` state plainly that they use running statistics only and report no median, and `rolling_snapshots_report_no_medians_even_in_exact_mode` pins both halves so the wording cannot go stale (71b5819).
_🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
SampleMode::Exactnow retains exact samples for send call, timer error, and server processing, which previously always built theirTimeMetricwith retention disabled.retain_exactdecision inCoreStats::new.TimeStats::median_ns: Option<f64>already distinguishes a retained median from an absent one; the missing behavior was retention, not representation.Behavior
send call,timer error, andserver processingrows when samples exist, instead of-.ClientEvent::EchoSent, and server processing only from replies that carry one.LateReplyModeis untouched. ACountOnlymatched late reply still contributes no timing measurements, so it does not enter server-processing exact retention.Retention estimate
irtt-stats.EXACT_SAMPLE_VECS_PER_REPLY = 8becomesEXACT_SAMPLES_PER_PROBE = 11, since exact storage now includes send-side metrics associated with probes rather than replies.size_of-grounded, deterministic, saturating, optional measurements assumed present for an upper bound, running-only bounded, time-based rolling excluded as documented.exact_mode_retains_one_sample_per_counted_metriccounts the public timing metrics a fully measured probe actually retains and asserts it equalsEXACT_SAMPLES_PER_PROBE, so changing retention without revisiting the estimate fails inirtt-stats.StatsConfig::estimated_retained_bytes(...)and consumed the new figure automatically — no CLI test needed a hardcoded adjustment, and no frontend memory heuristic was added.Scope
No changes to:
LateReplyMode;irtt-client,irtt-server, orirtt-protobehavior;Tests
irtt-stats: exact medians for send call (odd count), timer error (even count), and server processing; absent server processing retains no samples; running-only still counts those three but reports no median; a regression test asserting every timing metric with samples reports a median in exact mode; aCountOnlylate-reply assertion that server processing gains no retained median.irtt-statsretention: the estimate covers every retained timing sample, documented as an implementation test rather than a stable public constant. Existing linearity, saturation, running-only bound, rolling, and time-based-exclusion tests are unchanged and still pass.irtt-cli: a finite summary test proving real median cells for the three rows using spread values whose median differs from min, mean, and max, plus a continuous counterpart asserting those cells stay-.Verification
All run on this branch, all passing:
cargo test -p irtt-stats --all-features --lockedcargo test -p irtt-cli --all-features --lockedcargo fmt --all -- --checkcargo check --workspace --all-targets --all-features --lockedcargo test --workspace --all-targets --all-features --lockedcargo clippy --workspace --all-targets --all-features --locked -- -D warningsRUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps --lockedcargo check -p irtt-cli --no-default-features --features client|tui|server --all-targets --lockedgit diff --checkThe
clientandtuifeature-only builds emit the pre-existingcmd::formatdead-code warnings (unusedformat_percent_ratio,format_count,format_optional_count,format_optional_ns_f64,format_signed_duration). They are unrelated to this change, are not errors under any required command, and were left alone.💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:
db9bd515bbℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
When a caller enables
rolling_countorrolling_timeon an exact configuration,rolling_count()androlling_time()still recompute their snapshots withCoreStats::new(SampleMode::RunningOnly, ...)inrolling.rs:62, so populated timing metrics returnmedian_ns: None. The new promise thatExactmakes every timing metric report a median is therefore false for the crate's public rolling snapshots; either preserve the configured sample mode during rolling recomputation or explicitly limit this contract to the cumulative snapshot.AGENTS.md reference: AGENTS.md:L53-L53
Useful? React with 👍 / 👎.
Correct — rolling snapshots recompute with
RunningOnlyinrolling.rs:62regardless of config, so the new wording over-promised. Rolling semantics are out of scope for this PR, so I scoped the contract instead:SampleMode/finite()/samplesnow say the exact-median promise applies to the cumulative snapshot,rolling_count()/rolling_time()state plainly that they use running statistics only and report no median, androlling_snapshots_report_no_medians_even_in_exact_modepins both halves so the wording cannot go stale (71b5819).🤖 Addressed by Claude Code