docs(crates): give each published crate its own crates.io README and examples
Each published crate (irtt-rs, irtt-client, irtt-server, irtt-stats, irtt-proto) now points crates.io at its own README instead of inheriting the root project README via the workspace-wide readme default, which is removed. Each crate README is a short landing page: what the crate is, who should use it, which sibling crate to use instead if this isn't the right abstraction level, and links to docs.rs and the main repository. Add real Cargo example targets teaching irtt-client's blocking/Tokio/ managed API tiers, a basic and a multi-listener irtt-server example, and an irtt-stats example contrasting finite vs continuous retention. irtt-proto intentionally has none. Update each crate's top-level rustdoc to explain its API-tier choices and point at the examples, and enable docs.rs rustdoc example scraping for irtt-client and irtt-server (via doc-scrape-examples on each example target, needed because the crate has dev-dependencies). The client examples are caller-paced against next_send_deadline() rather than sending on every loop iteration, bound their receive wait by the next send/timeout deadline instead of awaiting indefinitely, poll timeouts on a short fixed cadence, and surface send/receive/close errors instead of discarding them via unwrap_or_default()/if-let-Ok -- so they demonstrate correct pacing and terminate/report cleanly rather than racing the server, hanging on a lost reply, or silently dropping a failed final close.
This commit is contained in:
20 files changed
+751
-12
No files matched your search
@@ -7,12 +7,15 @@ license.workspace = true
|
||||
rust-version.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
readme.workspace = true
|
||||
readme = "README.md"
|
||||
keywords = ["irtt", "networking", "latency", "statistics"]
|
||||
categories = ["network-programming"]
|
||||
|
||||
[dependencies]
|
||||
irtt-client.workspace = true
|
||||
|
||||
[[example]]
|
||||
name = "finite_vs_continuous"
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = "1.11.0"
|
||||
@@ -0,0 +1,38 @@
|
||||
# irtt-stats
|
||||
|
||||
Statistics aggregation over [`irtt-client`](https://crates.io/crates/irtt-client)
|
||||
events: loss/duplicate/late accounting, RTT and one-way-delay timing, and
|
||||
inter-packet delay variation (IPDV), as cumulative and rolling snapshots.
|
||||
|
||||
## Relationship to irtt-client
|
||||
|
||||
This crate does not open sessions or drive sockets. Feed it the `ClientEvent`
|
||||
stream produced by an `irtt-client` `Client`, `AsyncClient`, or managed
|
||||
session via `StatsCollector::process`, and read back a `Snapshot`.
|
||||
|
||||
A `StatsCollector` has one sequence/IPDV namespace and one packet count, both
|
||||
scoped to a single target. A managed session covering multiple targets needs
|
||||
one collector per target — as the `irtt-rs` CLI does — since a target's
|
||||
sequence numbers start over at zero and feeding two targets into one
|
||||
collector would pair up unrelated probes.
|
||||
|
||||
## Retention modes
|
||||
|
||||
`StatsConfig::finite()` retains exact timing samples for its cumulative
|
||||
snapshot, so every metric with samples reports an exact median there —
|
||||
retention grows with the probe count. `StatsConfig::continuous()` keeps
|
||||
bounded running statistics instead (no exact median) plus a bounded
|
||||
4096-entry adjacent-sequence IPDV store per target, for long-running or
|
||||
unbounded sessions. Rolling-window snapshots are always running-only and
|
||||
report no medians, whichever `StatsConfig` produced them.
|
||||
|
||||
See `examples/` in the repository for a runnable comparison of both modes.
|
||||
|
||||
## Documentation
|
||||
|
||||
Full API documentation is on [docs.rs/irtt-stats](https://docs.rs/irtt-stats).
|
||||
|
||||
## Project
|
||||
|
||||
Part of [irtt-rs](https://github.com/Lochnair/irtt-rs), an independent Rust
|
||||
implementation of an IRTT-compatible protocol stack.
|
||||
@@ -0,0 +1,102 @@
|
||||
//! Contrasts finite and continuous retention modes.
|
||||
//!
|
||||
//! Feeds the same synthetic stream of `irtt-client` events into two
|
||||
//! collectors — one configured with [`StatsConfig::finite`], one with
|
||||
//! [`StatsConfig::continuous`] — and prints how their cumulative snapshots
|
||||
//! differ. This uses synthetic events rather than a live session so the
|
||||
//! example runs deterministically with no network or server involved.
|
||||
//!
|
||||
//! Run with:
|
||||
//!
|
||||
//! ```text
|
||||
//! cargo run -p irtt-stats --example finite_vs_continuous
|
||||
//! ```
|
||||
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::time::{Duration, Instant, SystemTime};
|
||||
|
||||
use irtt_client::{ClientEvent, ClientTimestamp, PacketMeta, RttSample, SignedDuration};
|
||||
use irtt_stats::{StatsCollector, StatsConfig};
|
||||
|
||||
fn main() {
|
||||
let mut finite = StatsCollector::new(StatsConfig::finite());
|
||||
let mut continuous = StatsCollector::new(StatsConfig::continuous());
|
||||
|
||||
for event in synthetic_probe_stream(10) {
|
||||
finite.process(&event);
|
||||
continuous.process(&event);
|
||||
}
|
||||
|
||||
let finite_snapshot = finite.snapshot();
|
||||
let continuous_snapshot = continuous.snapshot();
|
||||
|
||||
println!(
|
||||
"finite: replies={}, rtt median_ns={:?}",
|
||||
finite_snapshot.events.echo_replies, finite_snapshot.rtt.primary.median_ns
|
||||
);
|
||||
println!(
|
||||
"continuous: replies={}, rtt median_ns={:?}",
|
||||
continuous_snapshot.events.echo_replies, continuous_snapshot.rtt.primary.median_ns
|
||||
);
|
||||
println!(
|
||||
"continuous mode keeps no exact samples, so its median is always None, \
|
||||
while finite mode's median is available once it has samples."
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds a short synthetic stream of `EchoSent`/`EchoReply` pairs, plus one
|
||||
/// loss, standing in for what a real `irtt-client` session would emit.
|
||||
fn synthetic_probe_stream(count: u32) -> Vec<ClientEvent> {
|
||||
let remote = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2112);
|
||||
let mut events = Vec::new();
|
||||
|
||||
for seq in 0..count {
|
||||
let sent_at = ClientTimestamp {
|
||||
wall: SystemTime::now(),
|
||||
mono: Instant::now(),
|
||||
};
|
||||
events.push(ClientEvent::EchoSent {
|
||||
seq,
|
||||
remote,
|
||||
scheduled_at: sent_at.mono,
|
||||
sent_at,
|
||||
bytes: 64,
|
||||
send_call: Duration::from_micros(50),
|
||||
timer_error: Duration::from_micros(20),
|
||||
});
|
||||
|
||||
if seq == count - 1 {
|
||||
// The last probe times out instead of receiving a reply.
|
||||
events.push(ClientEvent::EchoLoss {
|
||||
seq,
|
||||
sent_at,
|
||||
timeout_at: sent_at.mono + Duration::from_secs(1),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
let rtt = Duration::from_millis(10 + u64::from(seq));
|
||||
let received_at = ClientTimestamp {
|
||||
wall: sent_at.wall + rtt,
|
||||
mono: sent_at.mono + rtt,
|
||||
};
|
||||
events.push(ClientEvent::EchoReply {
|
||||
seq,
|
||||
remote,
|
||||
sent_at,
|
||||
received_at,
|
||||
rtt: RttSample {
|
||||
raw: rtt,
|
||||
adjusted: None,
|
||||
effective: SignedDuration::from_duration(rtt),
|
||||
},
|
||||
server_timing: None,
|
||||
one_way: None,
|
||||
received_stats: None,
|
||||
bytes: 64,
|
||||
packet_meta: PacketMeta::default(),
|
||||
});
|
||||
}
|
||||
|
||||
events
|
||||
}
|
||||
@@ -1,7 +1,24 @@
|
||||
//! Statistics aggregation for `irtt-client` events.
|
||||
//!
|
||||
//! The crate consumes `irtt-client` events and produces cumulative or rolling
|
||||
//! snapshots for reporting and integration code.
|
||||
//! [`StatsCollector`] consumes [`irtt_client::ClientEvent`] values through
|
||||
//! [`StatsCollector::process`] and produces a cumulative [`Snapshot`], plus
|
||||
//! optional count- and time-based rolling snapshots, for reporting and
|
||||
//! integration code.
|
||||
//!
|
||||
//! # Retention modes
|
||||
//!
|
||||
//! [`StatsConfig`] selects between two retention modes via [`SampleMode`].
|
||||
//! [`StatsConfig::finite`] ([`SampleMode::Exact`]) retains an exact sample per
|
||||
//! timing metric, so every metric reports an exact median once it has
|
||||
//! samples; retention grows with the number of probes processed. This is the
|
||||
//! default and suits a run with a known, bounded duration.
|
||||
//! [`StatsConfig::continuous`] ([`SampleMode::RunningOnly`]) keeps only
|
||||
//! running statistics — no exact median — and bounds its adjacent-sequence
|
||||
//! IPDV tracking, so memory stays bounded for a long-running or unbounded
|
||||
//! session. Rolling snapshots always use running statistics regardless of
|
||||
//! this setting.
|
||||
//!
|
||||
//! See `examples/` in the repository for a runnable comparison of both modes.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
Reference in new issue
Block a user