Skip to content

This change exposes BenchSession and Observer API#104

Open
adityashah1212 wants to merge 2 commits into
wfxr:mainfrom
adityashah1212:main
Open

This change exposes BenchSession and Observer API#104
adityashah1212 wants to merge 2 commits into
wfxr:mainfrom
adityashah1212:main

Conversation

@adityashah1212

@adityashah1212 adityashah1212 commented May 24, 2026

Copy link
Copy Markdown

Description

This change exposes BenchSession and Observer API to as public interfaces, so that the bench can be executed without relying on run method. This allows to customize how the bench execution occurs and also allows access to BenchReport generated after the run.

The Observer API gives even more visibility as it has access to results of each iteration and the user can define their custom Observer to observe results of each iteration if they so desire.

Breaking Change: This change removes the need for async-trait as it remove ReportCollector and hence the need for it to be dyn compatible. As for BenchSuite and StatelessBenchSuite, they had no reason to be marked async_trait as neither needed dyn compatibility to begin with. This simplifies the dependencies for the user crates.

Related Issue

#103

Checklist

  • I have tested my changes locally
  • I have updated documentation if needed

Summary by CodeRabbit

  • New Features

    • Added an Observer abstraction for custom handling of benchmark iteration results.
    • Introduced BenchSession API to configure and run benchmarks (including optional TUI).
    • New example demonstrating a custom observer and counting/reporting behavior.
  • Refactor

    • Reworked execution plumbing to simplify wiring and decouple runner, session, and reporting.
    • TUI now consumes unified result streams and presents status/error distributions.
  • Documentation

    • Updated README and examples to show the new session/observer patterns and TUI usage.

Review Change Stack

This change exposes BenchSession and Observer API to as public
interfaces, so that the bench can be executed without relying on run
method. This allows to customize how the bench execution occurs and
also allows access to BenchReport generated after the run.

The Observer API gives even more visibility as it has access to results
of each iteration and the user can define their custom Observer to
observe results of each iteration if they so desire.
@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@adityashah1212, we couldn't start this review because you've used your available PR reviews for now.

Your plan includes 2 reviews of capacity. Refill in 11 minutes and 29 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9b714e07-7ee2-418e-b262-74db99623f97

📥 Commits

Reviewing files that changed from the base of the PR and between 588df3e and 80b8366.

📒 Files selected for processing (4)
  • examples/custom_observer.rs
  • src/cli.rs
  • src/observer.rs
  • src/suite.rs
📝 Walkthrough

Walkthrough

This PR removes the async-trait dependency and refactors the crate to an Observer + BenchSession architecture: new suite and observer traits, Runner reports via Observer, Aggregator/TUI accept futures channels, and examples/docs updated to drop async-trait usage.

Changes

Core architecture refactoring

Layer / File(s) Summary
Dependencies and module structure
Cargo.toml, src/lib.rs, README.md
Removed async-trait; added futures and derive_more; reorganized public modules and updated doc examples.
Benchmark suite abstraction
src/suite.rs
Added BenchSuite and StatelessBenchSuite traits and a blanket impl adapting stateless suites to per-worker lifecycle.
Observer trait and report polish
src/observer.rs, src/report.rs
Introduced Observer, ObserverExt, Layered, and MpscObserver; BenchReport derives Debug, Clone and implements From<BenchOpts>.
Runner -> Observer integration
src/runner.rs, tests
Runner becomes Runner<BS,O> and reports via observer.notify(...); tests use futures::channel::mpsc + MpscObserver.
Result aggregation refactor
src/aggregator.rs
Replaced SilentCollector with internal Aggregator; switched to futures mpsc Result<IterReport,String> receiver and updated run API.
TUI adaptation to new flow
src/tui/mod.rs, src/tui/input.rs
Replaced TuiCollector with Tui owning state; accept Result<IterReport,String> channel; track error_dist; provide explicit Tui::run().
Session orchestration
src/session.rs
Added BenchSession builder to wire suite, observer, opts, optional TUI, spawn Runner/Aggregator/TUI, and return consolidated BenchReport.
CLI integration & baseline validation
src/cli.rs, src/baseline/mod.rs
run() now uses BenchSession; requires BenchSuite + Clone; baseline validation uses computed BenchOpts.
Type & clock polish
src/histogram.rs, src/clock.rs
LatencyHistogram derives Debug, Clone; Clock::new initializes elapsed to Duration::ZERO.

Examples and documentation update

Layer / File(s) Summary
Async-trait removal from examples
examples/*, examples/simple_stateless.rs
Removed async_trait imports and #[async_trait] attributes across examples; simple_stateless.rs uses std::time and makes SimpleBench public.
Custom observer example
examples/custom_observer.rs
New example showing StatelessBenchSuite + Observer (CountingObserver) wired via BenchSession with assertions.
Documentation updates
README.md, src/lib.rs
Updated examples and top-level docs to reflect removal of async_trait and new suite/observer/session patterns.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 Async-trait hops away so light,
Observers gather every sight,
Sessions hum and runners cheer,
Futures flow and dashboards clear,
Cleaner benchmarks take flight!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: exposing BenchSession and Observer API as public interfaces, which aligns with the primary objective of the changeset.
Description check ✅ Passed The description covers all required template sections: a detailed explanation of what the PR does, the related issue reference, and a completed checklist. It also explains the breaking change and benefits.
Docstring Coverage ✅ Passed Docstring coverage is 90.48% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (4)
examples/custom_observer.rs (1)

44-48: ⚡ Quick win

Assert observer notifications explicitly in the example.

The example wires a custom observer but never verifies it was called. Add a post-run counter assertion to validate observer behavior end-to-end.

💡 Suggested fix
     let opts = BenchOpts::builder().iterations(1000).build()?;
     let observer = CountingObserver::default();
+    let observed = observer.clone();
 
     let session = BenchSession::new(SimpleBench).opts(opts).observer(observer);
     let report = session.run().await?;
+    assert_eq!(observed.0.load(Ordering::Relaxed), 1000);
     assert_eq!(report.stats.overall.iters, 1000);
     assert_eq!(report.status_dist[&Status::server_error(500)], 200);
     assert_eq!(report.status_dist[&Status::client_error(400)], 200);

Also applies to: 52-52

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/custom_observer.rs` around lines 44 - 48, The example registers a
CountingObserver but doesn't assert it was invoked; after running the session
(the call to BenchSession::new(SimpleBench).opts(opts).observer(observer) and
session.run().await), add an explicit assertion on the observer's counter (the
CountingObserver instance) to verify notifications were received (e.g., check
its count increased or equals expected iterations) so the example validates
observer behavior end-to-end.
src/aggregator.rs (1)

1-11: 💤 Low value

Documentation references removed types.

The module-level documentation still references SilentCollector, but this type has been renamed to Aggregator and made pub(crate). Update the doc comments to reflect the current implementation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aggregator.rs` around lines 1 - 11, Update the module doc comment to
reflect the renamed and visibility-changed type: replace all references to
`SilentCollector` with `Aggregator` and note that `Aggregator` is `pub(crate)`
(non-public) where relevant; also ensure references to `JsonReporter` remain
accurate (e.g., `crate::reporter::JsonReporter`) and adjust any example/use-case
wording that implied a publicly exported `SilentCollector` to indicate the
internal `Aggregator` instead.
src/tui/mod.rs (1)

67-71: ⚡ Quick win

Use nonzero! macro instead of unsafe block.

The nonzero macro is already imported (line 38) and provides a safe, compile-time checked alternative to unsafe { NonZeroU8::new_unchecked(32) }.

Suggested fix
 impl Default for TuiSettings {
     fn default() -> Self {
-        Self { fps: unsafe { NonZeroU8::new_unchecked(32) }, auto_quit: true }
+        Self { fps: nonzero!(32u8), auto_quit: true }
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tui/mod.rs` around lines 67 - 71, Replace the unsafe NonZeroU8
construction in the Default impl for TuiSettings: inside fn default() of impl
Default for TuiSettings swap unsafe { NonZeroU8::new_unchecked(32) } for the
safe compile-time nonzero! macro (e.g., nonzero!(32u8) or nonzero!(32)) so the
fps field is created without unsafe.
src/session.rs (1)

18-18: 💤 Low value

Typo: "conslidated" → "consolidated".

Fix
-/// Define a bench session that can be run and get a conslidated report
+/// Define a bench session that can be run and get a consolidated report
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/session.rs` at line 18, Update the top-of-file/doc-comment in
src/session.rs to fix the typo "conslidated" → "consolidated"; locate the doc
string that currently reads "Define a bench session that can be run and get a
conslidated report" (near the BenchSession/module comment) and replace the
misspelled word so the comment reads "consolidated".
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/custom_observer.rs`:
- Around line 22-26: The status selection uses per-worker ordering
(info.worker_seq) which makes aggregate counts non-deterministic under parallel
workers; change the selector to use the global iteration sequence
(info.runner_seq) in the status assignment (the match that sets `status`) and
the other occurrence mentioned (lines ~49-51) so status generation is
iteration-global and deterministic, or alternatively force a single worker if
you intentionally want worker-local behavior; update references of
`info.worker_seq` to `info.runner_seq` in the `status` logic and the other
identical spot.

In `@src/cli.rs`:
- Around line 274-279: The TUI is being enabled based only on cli.quiet which
ignores non-TTY stdout; update the logic that decides between session.run() and
session.with_tui(...) to use the existing cli.collector() / Collector::Silent
behavior (or explicitly check stdout().is_tty()) so the TUI is not enabled when
stdout is not a TTY or when cli.quiet is true; locate the code around
BenchSession::new(suite).opts(opts) and replace the current cli.quiet-only
branch (which calls session.with_tui(TuiSettings { fps: cli.fps, auto_quit:
!cli.quit_manually })) with a decision that calls session.with_tui only when
collector indicates interactive (i.e., not Collector::Silent / stdout().is_tty()
&& !cli.quiet), otherwise call session.run().

In `@src/observer.rs`:
- Around line 11-12: The documentation in observer.rs references removed types
crate::collector::TuiCollector and crate::collector::SilentCollector; update
those stale links to the current public API by either removing the specific type
links or replacing them with the appropriate exposed symbols (e.g.,
crate::collector::Collector trait or the current concrete collector types) so
the docs compile and reflect the new architecture—search for the TuiCollector
and SilentCollector mentions in observer.rs and change them to the current
collector interface or a generic crate::collector module reference.
- Line 15: The file imports and calls tracing::warn / warn! unconditionally even
though tracing is optional; gate the import and any warn! invocations behind
#[cfg(feature = "tracing")] (or define a small cfg-based shim: when feature
"tracing" is set re-export tracing::warn, otherwise provide a no-op warn macro)
so the crate builds with default-features = false, and update the rustdoc
references that point to non-existent crate::collector::TuiCollector and
crate::collector::SilentCollector by removing or replacing them with the
correct, current types/modules (or plain text) so docs no longer link to a
missing collector module.

In `@src/suite.rs`:
- Around line 46-49: The blanket impl for BenchSuite is overly restrictive
because it requires Sync; update the impl<T> BenchSuite for T where clause to
remove the Sync bound so it reads something like T: StatelessBenchSuite + Send +
'static; specifically edit the impl that references BenchSuite and
StatelessBenchSuite to drop the Sync constraint so stateless suites that are
Send but not Sync can implement BenchSuite.

---

Nitpick comments:
In `@examples/custom_observer.rs`:
- Around line 44-48: The example registers a CountingObserver but doesn't assert
it was invoked; after running the session (the call to
BenchSession::new(SimpleBench).opts(opts).observer(observer) and
session.run().await), add an explicit assertion on the observer's counter (the
CountingObserver instance) to verify notifications were received (e.g., check
its count increased or equals expected iterations) so the example validates
observer behavior end-to-end.

In `@src/aggregator.rs`:
- Around line 1-11: Update the module doc comment to reflect the renamed and
visibility-changed type: replace all references to `SilentCollector` with
`Aggregator` and note that `Aggregator` is `pub(crate)` (non-public) where
relevant; also ensure references to `JsonReporter` remain accurate (e.g.,
`crate::reporter::JsonReporter`) and adjust any example/use-case wording that
implied a publicly exported `SilentCollector` to indicate the internal
`Aggregator` instead.

In `@src/session.rs`:
- Line 18: Update the top-of-file/doc-comment in src/session.rs to fix the typo
"conslidated" → "consolidated"; locate the doc string that currently reads
"Define a bench session that can be run and get a conslidated report" (near the
BenchSession/module comment) and replace the misspelled word so the comment
reads "consolidated".

In `@src/tui/mod.rs`:
- Around line 67-71: Replace the unsafe NonZeroU8 construction in the Default
impl for TuiSettings: inside fn default() of impl Default for TuiSettings swap
unsafe { NonZeroU8::new_unchecked(32) } for the safe compile-time nonzero! macro
(e.g., nonzero!(32u8) or nonzero!(32)) so the fps field is created without
unsafe.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4204df50-d9a1-4be1-a06e-01b463b590fd

📥 Commits

Reviewing files that changed from the base of the PR and between 1835f6b and 4a137df.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (28)
  • Cargo.toml
  • README.md
  • examples/baseline.rs
  • examples/custom_observer.rs
  • examples/http_hyper.rs
  • examples/http_reqwest.rs
  • examples/logging.rs
  • examples/postgres.rs
  • examples/simple_stateless.rs
  • examples/warmup.rs
  • src/aggregator.rs
  • src/baseline/mod.rs
  • src/cli.rs
  • src/clock.rs
  • src/collector/mod.rs
  • src/histogram.rs
  • src/lib.rs
  • src/observer.rs
  • src/report.rs
  • src/runner.rs
  • src/session.rs
  • src/suite.rs
  • src/tui/input.rs
  • src/tui/mod.rs
  • src/tui/render.rs
  • src/tui/state.rs
  • src/tui/terminal.rs
  • src/tui/tui_log.rs
💤 Files with no reviewable changes (8)
  • examples/postgres.rs
  • examples/logging.rs
  • examples/http_hyper.rs
  • src/collector/mod.rs
  • examples/baseline.rs
  • examples/http_reqwest.rs
  • README.md
  • examples/warmup.rs

Comment thread examples/custom_observer.rs Outdated
Comment thread src/cli.rs
Comment thread src/observer.rs Outdated
Comment thread src/observer.rs Outdated
Comment thread src/suite.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/cli.rs (1)

275-279: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use resolved collector when choosing run mode.

Line 275 still branches only on cli.quiet, so non-TTY output and --collector silent can incorrectly enter the TUI path. Branch using cli.collector() (or include !stdout().is_tty()) to keep behavior consistent with CLI semantics.

Suggested fix
 let session = BenchSession::new(suite).opts(opts);
- let report = if cli.quiet {
+ let report = if matches!(cli.collector(), Collector::Silent) {
     session.run().await
 } else {
     session.with_tui(TuiSettings { fps: cli.fps, auto_quit: !cli.quit_manually || !stdout().is_tty() }).run().await
 }?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli.rs` around lines 275 - 279, The current branch uses cli.quiet to
decide whether to call session.run() or session.with_tui(...).run(), but this
ignores the resolved collector setting and TTY status; update the branching to
use the resolved collector (call cli.collector()) and/or include
stdout().is_tty() so that when the collector is "silent" or stdout is not a TTY
we call session.run(), otherwise use session.with_tui(TuiSettings { fps:
cli.fps, auto_quit: !cli.quit_manually || !stdout().is_tty() }).run().await;
ensure you reference cli.collector(), cli.quiet, session.with_tui, TuiSettings
and stdout().is_tty() when making the change so the CLI semantics are preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@src/cli.rs`:
- Around line 275-279: The current branch uses cli.quiet to decide whether to
call session.run() or session.with_tui(...).run(), but this ignores the resolved
collector setting and TTY status; update the branching to use the resolved
collector (call cli.collector()) and/or include stdout().is_tty() so that when
the collector is "silent" or stdout is not a TTY we call session.run(),
otherwise use session.with_tui(TuiSettings { fps: cli.fps, auto_quit:
!cli.quit_manually || !stdout().is_tty() }).run().await; ensure you reference
cli.collector(), cli.quiet, session.with_tui, TuiSettings and stdout().is_tty()
when making the change so the CLI semantics are preserved.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f1a129fe-f482-4cb1-8f4b-80de61843755

📥 Commits

Reviewing files that changed from the base of the PR and between 4a137df and 588df3e.

📒 Files selected for processing (4)
  • examples/custom_observer.rs
  • src/cli.rs
  • src/observer.rs
  • src/suite.rs

@adityashah1212

Copy link
Copy Markdown
Author

@wfxr, can you please review this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant