mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 00:04:54 +00:00
Moves both AEAD layers (ChaCha20-Poly1305, one round per layer per packet) plus the sendmsg syscall off the rx_loop task onto a per-shard worker pool, adds per-peer connect(2)-ed UDP with SO_REUSEPORT, and uses Linux UDP GSO (sendmsg+UDP_SEGMENT — kernel splits one super-skb into N on-the-wire datagrams in a single TX-stack walk) when packets in a batch are uniform-size. Same kernel primitive WireGuard's in-kernel module and BoringTun use to hit 2.5–3.2 Gbps single-stream. Single TCP stream on a 5-node docker-bridge mesh, 5 x 15 s x P=1: A→D: 1379 → 2708 Mbps (1.96x, RTT +0.12 ms) A→E: 1394 → 2663 Mbps (1.91x, RTT +0.11 ms) E→A: 1406 → 2624 Mbps (1.87x, RTT +0.19 ms) Static-peer pairs only — every CoV under 3%, 0 outliers, 0% ICMP loss. The ~+100 µs RTT is the worker queue handoff cost; AEAD + sendmmsg now run on a separate core in exchange. What lands: - src/node/encrypt_worker.rs: std::thread + crossbeam_channel workers; hash-by-destination dispatch pins a TCP flow to one worker so wire ordering is preserved; per-worker sendmmsg(2) batching up to 32; Linux uses sendmsg(2)+UDP_SEGMENT when packets in a group are uniform-size. - src/node/decrypt_worker.rs: receive-side mirror. Each shard owns its session's recv cipher + replay window in a thread-local HashMap (no shared RwLock/Mutex). Sessions are handed off at promote_connection and re-registered on K-bit flip / rekey cutover. - src/node/handlers/session.rs try_send_session_data_pipelined: FSP+FMP both seal in-place in the worker on one wire-buffer alloc; no intermediate inner_plaintext / fsp_payload Vecs. - src/transport/udp/connected_peer.rs + peer_drain.rs: per-peer connect(2)-ed UDP socket with SO_REUSEPORT (set on the listen socket too — without that, EADDRINUSE on activation and every packet falls back to the wildcard path); the worker sends with msg_name=NULL and the kernel uses its cached 5-tuple. Tick- driven activation in handlers/connected_udp.rs, idempotent. - src/transport/udp/mod.rs: mem::replace the recvmmsg backing buffer instead of buf.to_vec() per packet — single pointer swap, no MTU-sized memcpy. - src/protocol/link.rs SessionDatagramRef: zero-copy borrowed view used by handle_session_datagram for the bulk local-delivery path; handle_session_payload takes the borrowed payload directly (no payload[35..].to_vec()). - src/transport/mod.rs TransportAddr::from_socket_addr: collapses the two-alloc from_string(addr.to_string()) pattern to one. - src/node/handlers/rx_loop.rs: decrypt-fallback drain promoted ahead of packet_rx in the select! (TCP ACK starvation fix); interleaved fallback drain every 32 packets inside the rx burst loop. - noise::Session: send_cipher_clone / recv_cipher_clone / recv_replay_snapshot_owned / take_send_counter / accept_replay so off-task workers can hold a cloned cipher + reserved counter while the dispatcher keeps replay/counter sequencing serial. CipherState::cipher_clone returns a refcount-bumped LessSafeKey. AsyncUdpSocket: AsRawFd so workers issue raw sendmmsg / sendmsg without going through the tokio reactor. - Worker pool sizing: both default to num_cpus, overridable via FIPS_ENCRYPT_WORKERS=N / FIPS_DECRYPT_WORKERS=N. Per-peer connected UDP can be disabled via FIPS_CONNECTED_UDP=0. - src/perf_profile.rs: optional per-stage timing reporter under FIPS_PERF=1 (or FIPS_PIPELINE_TRACE=1). Off by default; zero overhead when disabled. - All cfg(unix)-gated. Windows continues on the existing tokio- based send/recv. Decrypt worker session lifecycle: - Node::unregister_decrypt_worker_session mirrors the existing register helper. Wired at the two natural sites that already iterate peers_by_index: the rekey drain-completion block in handlers/rekey.rs (drops the worker entry for the old our_index once the drain window has expired and the cache_key is unreachable to any in-flight OLD-K packet), and remove_active_peer in handlers/dispatch.rs (drops the worker entry for each of the four index slots: current, rekey, pending, previous). Only our_index is normally registered; unregister_session is fire- and-forget for missing entries, so calling unconditionally on all four slots is correct and bounds the cleanup without per- slot accounting. Without these callers the per-worker sessions HashMap and the Node's decrypt_registered_sessions set would grow monotonically per rekey on long-lived peers. Testing: - testing/static/scripts/bench-multirun.sh: multi-run iperf3 + ping bench. N reruns (default 5), median / min / max / CoV % / per-run outlier flag, avg ping RTT, ICMP loss %, TCP retransmit total. Plain client→dest labels + topology header. Pre-bench peer-convergence check (FIPS_BENCH_CONVERGE_SECS, default 15); per-path route verification via stats.bytes_sent deltas — fails fast if traffic exits via a non-static-peer link. - testing/static/docker-compose.yml: passes FIPS_ENCRYPT_WORKERS / FIPS_DECRYPT_WORKERS / FIPS_PERF through to containers for A/B benchmarking without rebuilds. - testing/static/scripts/iperf-test.sh: same plain client→dest labels + topology header (was multihop/direct/N hop, which conflated topology distance with on-wire path). - .config/nextest.toml: synthetic UDP node tests serialized through a max-threads=1 test group. Localhost handshakes drop on shared CI runners under parallel load; one-at-a-time keeps assertions reliable. - src/node/tests/spanning_tree.rs: repair_missing_edge_handshakes — retries up to 5 times for synthetic edges whose msg1 was dropped, with a drain after each edge retry instead of after each attempt's full burst. - src/node/decrypt_worker.rs::tests: two unit tests asserting WorkerMsg::UnregisterSession removes the worker-thread session HashMap entry (handle_msg_unregister_session_removes_entry) and is a no-op for never-seen cache_keys (handle_msg_unregister_session_idempotent_on_unknown_key), which is the safety invariant the unconditional unregister calls at the four index slots in remove_active_peer rely on. - src/node/encrypt_worker.rs::unix_tests pipelined_send_wire_layout_roundtrips_canonical_decoders: mirrors the encoder geometry of try_send_session_data_pipelined (no coords, the common established-session path), runs the worker's real seal + send via flush_direct_batch_sync, and decodes the resulting wire packet using only canonical receive-side decoders (EncryptedHeader::parse, SessionDatagramRef::decode, FSP header parse, noise::open). Any divergence between the hand-rolled encoder offsets (fsp_aad_offset, fsp_plaintext_offset) and the decoders fails at one of the parse / open / decode steps before the inner-plaintext assertion fires. Complements the existing fsp_preseal_runs_before_outer_fmp_seal test which covers the seal-ordering invariant with synthetic headers but does not exercise the wire-layout invariant. CHANGELOG.md [Unreleased] # Changed entry added describing the worker-pool threading model, hash-by-destination dispatch, sendmmsg/UDP_GSO, per-peer connected UDP, the operator-facing env vars, and the bench numbers above. Cherry-picks from mmalmi/master (paths translated from crates/fips-core/src/ to src/): 9b7c723, 0deb5cb, 13f7339, e036c0e, 3740a68, 3792f83, 8510193, 4910b07, e53f545, e4e2896, 5fe4af5, 1d01ada, 8c37008, e12469e, 6eb2860. Co-authored-by: Johnathan Corgan <johnathan@corganlabs.com>
403 lines
14 KiB
Rust
403 lines
14 KiB
Rust
// Some entry points (e.g. `stamp`, `record_since`) are only called from
|
|
// paths that aren't yet wired up in this PR (FSP-pipelined dispatch,
|
|
// per-stage worker telemetry). Keep them in tree so the follow-up wiring
|
|
// PR is a pure call-site change.
|
|
#![allow(dead_code)]
|
|
|
|
//! Runtime perf profiler for the FMP/FSP hot path and queue handoffs.
|
|
//!
|
|
//! Avoids external dependencies (`perf`, samply, etc.) by instrumenting
|
|
//! the key stages directly with `AtomicU64` ns counters, histograms,
|
|
//! and packet counts. A background task prints a per-stage breakdown
|
|
//! every `FIPS_PERF_INTERVAL_SECS` seconds when `FIPS_PERF=1` or
|
|
//! `FIPS_PIPELINE_TRACE=1` is set at runtime.
|
|
//!
|
|
//! Enabling adds `Instant::now()` plus a few relaxed atomics per
|
|
//! measured stage, so the measured numbers are slightly pessimistic vs
|
|
//! production. The relative picture is the point: it shows whether a
|
|
//! run is spending time in crypto, syscalls, or scheduler/channel
|
|
//! waits.
|
|
//!
|
|
//! Stages tracked, inbound:
|
|
//! * `UDP_RECV` — recvmmsg syscall + per-message buffer copy
|
|
//! * `FMP_DECRYPT` — outer AEAD open + replay window
|
|
//! * `LINK_DISPATCH` — `dispatch_link_message` excluding FSP work
|
|
//! * `FSP_DECRYPT` — inner AEAD open + replay window
|
|
//! * `TUN_WRITE` — IPv6 shim decompress + tun_tx.send
|
|
//!
|
|
//! Stages tracked, outbound:
|
|
//! * `FSP_ENCRYPT` — inner AEAD seal (`send_session_data`)
|
|
//! * `FMP_ENCRYPT` — outer AEAD seal (`send_encrypted_link_message`)
|
|
//! * `UDP_SEND` — sendmmsg/sendmsg/sendto flush
|
|
//!
|
|
//! Handoff waits tracked:
|
|
//! * `TRANSPORT_QUEUE_WAIT` — UDP/transport receive loop → rx_loop
|
|
//! * `ENDPOINT_COMMAND_WAIT` — FipsEndpoint send → node command loop
|
|
//! * `FMP_WORKER_QUEUE_WAIT` — rx_loop FMP job dispatch → worker
|
|
//! * `ENDPOINT_EVENT_WAIT` — rx_loop endpoint delivery → endpoint recv
|
|
|
|
use std::sync::OnceLock;
|
|
use std::sync::atomic::{AtomicU64, Ordering::Relaxed};
|
|
use std::time::Instant;
|
|
|
|
/// Number of measurement buckets. Indices match `Stage`.
|
|
const N_STAGES: usize = 16;
|
|
const N_EVENTS: usize = 6;
|
|
const HIST_BUCKETS: usize = 48;
|
|
|
|
/// Stage identifier. `as usize` indexes into the counter arrays.
|
|
#[derive(Copy, Clone, Debug)]
|
|
#[repr(usize)]
|
|
pub enum Stage {
|
|
UdpRecv = 0,
|
|
FmpDecrypt = 1,
|
|
LinkDispatch = 2,
|
|
FspDecrypt = 3,
|
|
TunWrite = 4,
|
|
FspEncrypt = 5,
|
|
FmpEncrypt = 6,
|
|
UdpSend = 7,
|
|
/// Whole `Node::process_packet` body. Anchor for "what fraction of
|
|
/// the receive hot path is in the non-AEAD parts of the pipeline".
|
|
ProcessPacket = 8,
|
|
/// Just the `endpoint_event_tx.send()` for inbound application
|
|
/// payloads — wakes the embedded-endpoint consumer task.
|
|
EndpointDeliver = 9,
|
|
/// Whole `handle_encrypted_session_msg` (FSP receive path) minus
|
|
/// the `FspDecrypt` sub-span. Surfaces dispatch + ipv6_shim +
|
|
/// `Vec::drain` cost on the inner session layer.
|
|
FspHandle = 10,
|
|
/// Whole `handle_endpoint_data_command` body — the SENDER's
|
|
/// per-packet "do everything to push one outbound packet"
|
|
/// dispatch. Compare against the sum of `FspEncrypt`,
|
|
/// `FmpEncrypt`, and `UdpSend` to see how much of the sender
|
|
/// hot path is in state-touching dispatch (sessions/peers
|
|
/// lookups, MMP/stats updates, Vec allocs) vs the AEAD/syscall
|
|
/// work that's a natural fit for an off-task worker.
|
|
EndpointSend = 11,
|
|
/// Time spent waiting after `FipsEndpoint::send`/`blocking_send`
|
|
/// creates a node command until `rx_loop` starts handling it.
|
|
EndpointCommandWait = 12,
|
|
/// Time spent waiting after `rx_loop` creates an FMP encrypt/send
|
|
/// worker job until the worker thread starts encrypting it.
|
|
FmpWorkerQueueWait = 13,
|
|
/// Time spent waiting after a transport receives a packet until
|
|
/// `rx_loop` starts processing it.
|
|
TransportQueueWait = 14,
|
|
/// Time spent waiting after `rx_loop` delivers endpoint data until
|
|
/// the embedded endpoint consumer receives it.
|
|
EndpointEventWait = 15,
|
|
}
|
|
|
|
impl Stage {
|
|
const fn name(self) -> &'static str {
|
|
match self {
|
|
Stage::UdpRecv => "udp_recv",
|
|
Stage::FmpDecrypt => "fmp_decrypt",
|
|
Stage::LinkDispatch => "link_dispatch",
|
|
Stage::FspDecrypt => "fsp_decrypt",
|
|
Stage::TunWrite => "tun_write",
|
|
Stage::FspEncrypt => "fsp_encrypt",
|
|
Stage::FmpEncrypt => "fmp_encrypt",
|
|
Stage::UdpSend => "udp_send",
|
|
Stage::ProcessPacket => "process_packet",
|
|
Stage::EndpointDeliver => "endpoint_deliver",
|
|
Stage::FspHandle => "fsp_handle",
|
|
Stage::EndpointSend => "endpoint_send",
|
|
Stage::EndpointCommandWait => "endpoint_command_wait",
|
|
Stage::FmpWorkerQueueWait => "fmp_worker_queue_wait",
|
|
Stage::TransportQueueWait => "transport_queue_wait",
|
|
Stage::EndpointEventWait => "endpoint_event_wait",
|
|
}
|
|
}
|
|
}
|
|
|
|
fn stage_from_index(idx: usize) -> Stage {
|
|
match idx {
|
|
0 => Stage::UdpRecv,
|
|
1 => Stage::FmpDecrypt,
|
|
2 => Stage::LinkDispatch,
|
|
3 => Stage::FspDecrypt,
|
|
4 => Stage::TunWrite,
|
|
5 => Stage::FspEncrypt,
|
|
6 => Stage::FmpEncrypt,
|
|
7 => Stage::UdpSend,
|
|
8 => Stage::ProcessPacket,
|
|
9 => Stage::EndpointDeliver,
|
|
10 => Stage::FspHandle,
|
|
11 => Stage::EndpointSend,
|
|
12 => Stage::EndpointCommandWait,
|
|
13 => Stage::FmpWorkerQueueWait,
|
|
14 => Stage::TransportQueueWait,
|
|
15 => Stage::EndpointEventWait,
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
/// Count-only events that clarify which hot-path variant is active.
|
|
#[derive(Copy, Clone, Debug)]
|
|
#[repr(usize)]
|
|
pub enum Event {
|
|
UdpSendConnected = 0,
|
|
UdpSendWildcard = 1,
|
|
UdpSendBackpressure = 2,
|
|
ConnectedUdpInstalled = 3,
|
|
ConnectedUdpActivationFailed = 4,
|
|
UdpSendBackpressureSleep = 5,
|
|
}
|
|
|
|
impl Event {
|
|
const fn name(self) -> &'static str {
|
|
match self {
|
|
Event::UdpSendConnected => "udp_send_connected",
|
|
Event::UdpSendWildcard => "udp_send_wildcard",
|
|
Event::UdpSendBackpressure => "udp_send_backpressure",
|
|
Event::ConnectedUdpInstalled => "connected_udp_installed",
|
|
Event::ConnectedUdpActivationFailed => "connected_udp_activation_failed",
|
|
Event::UdpSendBackpressureSleep => "udp_send_backpressure_sleep",
|
|
}
|
|
}
|
|
}
|
|
|
|
fn event_from_index(idx: usize) -> Event {
|
|
match idx {
|
|
0 => Event::UdpSendConnected,
|
|
1 => Event::UdpSendWildcard,
|
|
2 => Event::UdpSendBackpressure,
|
|
3 => Event::ConnectedUdpInstalled,
|
|
4 => Event::ConnectedUdpActivationFailed,
|
|
5 => Event::UdpSendBackpressureSleep,
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
static TOTAL_NS: [AtomicU64; N_STAGES] = [const { AtomicU64::new(0) }; N_STAGES];
|
|
static COUNT: [AtomicU64; N_STAGES] = [const { AtomicU64::new(0) }; N_STAGES];
|
|
static MAX_NS: [AtomicU64; N_STAGES] = [const { AtomicU64::new(0) }; N_STAGES];
|
|
static HIST: [AtomicU64; N_STAGES * HIST_BUCKETS] =
|
|
[const { AtomicU64::new(0) }; N_STAGES * HIST_BUCKETS];
|
|
static EVENTS: [AtomicU64; N_EVENTS] = [const { AtomicU64::new(0) }; N_EVENTS];
|
|
|
|
/// True iff perf/pipeline tracing is enabled. Read once at startup so
|
|
/// the per-packet check is a single cached load.
|
|
pub(crate) fn enabled() -> bool {
|
|
static ENABLED: OnceLock<bool> = OnceLock::new();
|
|
*ENABLED.get_or_init(|| {
|
|
["FIPS_PERF", "FIPS_PIPELINE_TRACE"].into_iter().any(|key| {
|
|
std::env::var(key)
|
|
.map(|s| s == "1" || s.eq_ignore_ascii_case("true"))
|
|
.unwrap_or(false)
|
|
})
|
|
})
|
|
}
|
|
|
|
/// Capture a timestamp for a future queue-wait measurement. Returns
|
|
/// `None` when tracing is disabled so callers can store it cheaply in
|
|
/// packet/job structs without paying `Instant::now()` in production.
|
|
#[inline]
|
|
pub(crate) fn stamp() -> Option<Instant> {
|
|
if enabled() {
|
|
Some(Instant::now())
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Record time elapsed since a previously captured stamp.
|
|
#[inline]
|
|
pub(crate) fn record_since(stage: Stage, start: Option<Instant>) {
|
|
if let Some(start) = start {
|
|
record(stage, start.elapsed().as_nanos() as u64);
|
|
}
|
|
}
|
|
|
|
/// Record `elapsed_ns` for the given stage. No-op when disabled.
|
|
pub fn record(stage: Stage, elapsed_ns: u64) {
|
|
if !enabled() {
|
|
return;
|
|
}
|
|
let idx = stage as usize;
|
|
let elapsed_ns = elapsed_ns.max(1);
|
|
TOTAL_NS[idx].fetch_add(elapsed_ns, Relaxed);
|
|
COUNT[idx].fetch_add(1, Relaxed);
|
|
MAX_NS[idx].fetch_max(elapsed_ns, Relaxed);
|
|
HIST[(idx * HIST_BUCKETS) + bucket_for_ns(elapsed_ns)].fetch_add(1, Relaxed);
|
|
}
|
|
|
|
#[inline]
|
|
pub fn record_event(event: Event) {
|
|
record_event_count(event, 1);
|
|
}
|
|
|
|
pub fn record_event_count(event: Event, count: u64) {
|
|
if !enabled() || count == 0 {
|
|
return;
|
|
}
|
|
EVENTS[event as usize].fetch_add(count, Relaxed);
|
|
}
|
|
|
|
/// RAII timer — `drop` records the elapsed time into the stage.
|
|
/// Use:
|
|
/// ```ignore
|
|
/// let _t = profile::Timer::start(Stage::FmpDecrypt);
|
|
/// // ... AEAD work ...
|
|
/// ```
|
|
pub struct Timer {
|
|
stage: Stage,
|
|
start: Option<Instant>,
|
|
}
|
|
|
|
impl Timer {
|
|
#[inline]
|
|
pub fn start(stage: Stage) -> Self {
|
|
let start = if enabled() {
|
|
Some(Instant::now())
|
|
} else {
|
|
None
|
|
};
|
|
Self { stage, start }
|
|
}
|
|
}
|
|
|
|
impl Drop for Timer {
|
|
fn drop(&mut self) {
|
|
if let Some(t0) = self.start {
|
|
let ns = t0.elapsed().as_nanos() as u64;
|
|
record(self.stage, ns);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Spawn a background task that prints a per-stage breakdown every
|
|
/// `FIPS_PERF_INTERVAL_SECS` seconds (default 5). Idempotent — only
|
|
/// the first call spawns. No-op when profiling isn't enabled.
|
|
pub fn maybe_spawn_reporter() {
|
|
if !enabled() {
|
|
return;
|
|
}
|
|
static STARTED: OnceLock<()> = OnceLock::new();
|
|
if STARTED.set(()).is_err() {
|
|
return;
|
|
}
|
|
let interval = std::env::var("FIPS_PERF_INTERVAL_SECS")
|
|
.ok()
|
|
.and_then(|s| s.parse::<u64>().ok())
|
|
.unwrap_or(5)
|
|
.max(1);
|
|
tokio::spawn(async move {
|
|
let mut prev_total = [0u64; N_STAGES];
|
|
let mut prev_count = [0u64; N_STAGES];
|
|
let mut prev_hist = [0u64; N_STAGES * HIST_BUCKETS];
|
|
let mut prev_events = [0u64; N_EVENTS];
|
|
loop {
|
|
tokio::time::sleep(std::time::Duration::from_secs(interval)).await;
|
|
let mut line = format!("[pipe {}s]", interval);
|
|
for i in 0..N_STAGES {
|
|
let t = TOTAL_NS[i].load(Relaxed);
|
|
let c = COUNT[i].load(Relaxed);
|
|
let dt = t.saturating_sub(prev_total[i]);
|
|
let dc = c.saturating_sub(prev_count[i]);
|
|
prev_total[i] = t;
|
|
prev_count[i] = c;
|
|
|
|
let base = i * HIST_BUCKETS;
|
|
let mut hist_delta = [0u64; HIST_BUCKETS];
|
|
for (bucket, delta) in hist_delta.iter_mut().enumerate().take(HIST_BUCKETS) {
|
|
let idx = base + bucket;
|
|
let current = HIST[idx].load(Relaxed);
|
|
*delta = current.saturating_sub(prev_hist[idx]);
|
|
prev_hist[idx] = current;
|
|
}
|
|
if dc == 0 {
|
|
continue;
|
|
}
|
|
let stage = stage_from_index(i);
|
|
let avg_ns = if dc > 0 { dt / dc } else { 0 };
|
|
let pps = if interval > 0 { dc / interval } else { 0 };
|
|
let p50 = percentile_ns(&hist_delta, dc, 50);
|
|
let p95 = percentile_ns(&hist_delta, dc, 95);
|
|
let p99 = percentile_ns(&hist_delta, dc, 99);
|
|
let approx_max = interval_max_ns(&hist_delta);
|
|
let lifetime_max = MAX_NS[i].load(Relaxed);
|
|
line.push_str(&format!(
|
|
" {}={}/s avg={} p50<={} p95<={} p99<={} max<={} allmax={}",
|
|
stage.name(),
|
|
pps,
|
|
fmt_ns(avg_ns),
|
|
fmt_ns(p50),
|
|
fmt_ns(p95),
|
|
fmt_ns(p99),
|
|
fmt_ns(approx_max),
|
|
fmt_ns(lifetime_max),
|
|
));
|
|
}
|
|
for i in 0..N_EVENTS {
|
|
let current = EVENTS[i].load(Relaxed);
|
|
let delta = current.saturating_sub(prev_events[i]);
|
|
prev_events[i] = current;
|
|
if delta == 0 {
|
|
continue;
|
|
}
|
|
let event = event_from_index(i);
|
|
let per_sec = delta / interval;
|
|
line.push_str(&format!(" {}={}/s", event.name(), per_sec));
|
|
}
|
|
// eprintln so it always lands regardless of RUST_LOG.
|
|
eprintln!("{}", line);
|
|
}
|
|
});
|
|
}
|
|
|
|
fn bucket_for_ns(ns: u64) -> usize {
|
|
if ns <= 1 {
|
|
return 0;
|
|
}
|
|
((u64::BITS - (ns - 1).leading_zeros()) as usize).min(HIST_BUCKETS - 1)
|
|
}
|
|
|
|
fn bucket_upper_ns(bucket: usize) -> u64 {
|
|
if bucket == 0 {
|
|
1
|
|
} else if bucket >= 63 {
|
|
u64::MAX
|
|
} else {
|
|
1u64 << bucket
|
|
}
|
|
}
|
|
|
|
fn percentile_ns(hist_delta: &[u64; HIST_BUCKETS], total: u64, pct: u64) -> u64 {
|
|
if total == 0 {
|
|
return 0;
|
|
}
|
|
let target = total.saturating_mul(pct).saturating_add(99) / 100;
|
|
let mut seen = 0u64;
|
|
for (idx, count) in hist_delta.iter().enumerate() {
|
|
seen = seen.saturating_add(*count);
|
|
if seen >= target {
|
|
return bucket_upper_ns(idx);
|
|
}
|
|
}
|
|
bucket_upper_ns(HIST_BUCKETS - 1)
|
|
}
|
|
|
|
fn interval_max_ns(hist_delta: &[u64; HIST_BUCKETS]) -> u64 {
|
|
for idx in (0..HIST_BUCKETS).rev() {
|
|
if hist_delta[idx] != 0 {
|
|
return bucket_upper_ns(idx);
|
|
}
|
|
}
|
|
0
|
|
}
|
|
|
|
fn fmt_ns(ns: u64) -> String {
|
|
if ns >= 1_000_000_000 {
|
|
format!("{:.1}s", ns as f64 / 1_000_000_000.0)
|
|
} else if ns >= 1_000_000 {
|
|
format!("{:.1}ms", ns as f64 / 1_000_000.0)
|
|
} else if ns >= 1_000 {
|
|
format!("{:.1}us", ns as f64 / 1_000.0)
|
|
} else {
|
|
format!("{ns}ns")
|
|
}
|
|
}
|