Skip to content

[Detail Bug] SlidingThroughput::bps returns 0.0 for sub-second lookback (clamping bypassed)Β #45

@detail-app

Description

@detail-app

Detail Bug Report

https://app.detail.dev/org_89d327b3-b883-4365-b6a3-46b6701342a9/bugs/bug_e718ad42-913c-4d62-b297-211f38ae2cc6

Summary

  • Context: The SlidingThroughput::bps() method calculates throughput (bytes per second) over a sliding time window for network traffic monitoring.
  • Bug: When bps() is called with a sub-second duration (e.g., Duration::from_millis(500)), it returns 0.0 instead of computing throughput over the closest available window.
  • Actual vs. expected: The method returns 0.0 for any lookback duration less than 1 second, instead of clamping to the minimum 1-second window and returning the throughput over that period.
  • Impact: Callers requesting sub-second throughput measurements receive 0.0, which incorrectly suggests zero throughput rather than providing the best available estimate.

Code with bug

pub fn bps(&mut self, lookback: Duration) -> f64 {
    let now_tick = self.now_secs();
    self.advance_to(now_tick);

    let requested_secs = lookback.as_secs();
    if requested_secs == 0 {  // <-- BUG πŸ”΄ Early return prevents clamping
        return 0.0;
    }

    let window_secs = requested_secs.clamp(1, NUM_BUCKETS as u64) as usize;

    // Sum last `window_secs` buckets starting from head (inclusive) going backwards
    let mut sum: u64 = 0;
    let mut idx = self.head_idx;
    for _ in 0..window_secs {
        sum = sum.saturating_add(self.buckets[idx]);
        idx = (idx + self.buckets.len() - 1) % self.buckets.len();
    }

    sum as f64 / window_secs as f64
}

Evidence (Example)

Scenario:

  1. Record 1000 bytes at t=0
  2. Request throughput with 500ms lookback
  3. Request throughput with 1s lookback

Current behavior:

  • bps(Duration::from_millis(500)) returns 0.0
  • bps(Duration::from_secs(1)) returns 1000.0

Expected behavior:

  • Both calls should return 1000.0. Sub-second durations truncate to 0 seconds, but the logic should clamp to the minimum 1-second window rather than returning 0.0. The early return bypasses the clamping.

Recommended fix

Remove the early return so clamping applies to all durations:

pub fn bps(&mut self, lookback: Duration) -> f64 {
    let now_tick = self.now_secs();
    self.advance_to(now_tick);

    let requested_secs = lookback.as_secs();
    // Remove early return; let clamping handle sub-second durations  // <-- FIX 🟒

    let window_secs = requested_secs.clamp(1, NUM_BUCKETS as u64) as usize;

    // Sum last `window_secs` buckets starting from head (inclusive) going backwards
    let mut sum: u64 = 0;
    let mut idx = self.head_idx;
    for _ in 0..window_secs {
        sum = sum.saturating_add(self.buckets[idx]);
        idx = (idx + self.buckets.len() - 1) % self.buckets.len();
    }

    sum as f64 / window_secs as f64
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions