Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions crates/tinyclient/src/gui/call/health.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,42 @@ const WORSE_AFTER: Duration = Duration::from_secs(3);
/// And better: a call that went bad has to prove itself for longer.
const BETTER_AFTER: Duration = Duration::from_secs(10);

/// Seconds of ping and receive bitrate kept for Call health's time graph.
pub(in crate::gui) const CHART_SECONDS: usize = 60;

/// One poll on the Call health chart: when it was read, round trip, and what we were receiving.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(in crate::gui) struct ChartSample {
pub at: Instant,
pub rtt_ms: u32,
pub recv_kbps: u32,
}

/// Append one poll to the chart, dropping the oldest past [`CHART_SECONDS`].
pub(in crate::gui) fn push_chart(samples: &mut VecDeque<ChartSample>, stats: &CallStats, at: Instant) {
if samples.len() >= CHART_SECONDS {
samples.pop_front();
}
samples.push_back(ChartSample { at, rtt_ms: stats.rtt_ms, recv_kbps: stats.receive_kbps });
}

/// A full minute of made-up samples around `stats`, for `--shot` so the graph is not empty.
pub(in crate::gui) fn seed_chart(stats: &CallStats, now: Instant) -> VecDeque<ChartSample> {
let base_rtt = stats.rtt_ms.max(1);
let base_recv = stats.receive_kbps.max(1);
(0..CHART_SECONDS)
.map(|i| {
let rtt_wobble = ((i * 37) % 11) as i32 - 5;
let recv_wobble = ((i * 53) % 21) as i32 - 10;
ChartSample {
at: now - Duration::from_secs((CHART_SECONDS - 1 - i) as u64),
rtt_ms: (base_rtt as i32 + rtt_wobble).max(1) as u32,
recv_kbps: (base_recv as i32 + recv_wobble * 2).max(1) as u32,
}
})
.collect()
}

/// The link over the last several seconds, fed each poll, and the grade it has earned. While
/// nothing flows nothing is known about loss or jitter, so they and the grade stay as they were.
#[derive(Debug, Default)]
Expand Down Expand Up @@ -463,6 +499,27 @@ mod tests {
CallStats { rtt_ms, loss_permille, jitter_ms, link_measured: true, ..CallStats::default() }
}

#[test]
fn the_chart_keeps_a_minute_and_shot_seed_fills_it() {
let start = Instant::now();
let mut samples = VecDeque::new();
for n in 0..70 {
push_chart(
&mut samples,
&CallStats { rtt_ms: n, receive_kbps: n * 2, ..CallStats::default() },
start + Duration::from_secs(n as u64),
);
}
assert_eq!(samples.len(), CHART_SECONDS);
assert_eq!(samples.front().map(|sample| sample.rtt_ms), Some(10));
assert_eq!(samples.back().map(|sample| sample.rtt_ms), Some(69));
assert!(samples.back().unwrap().at > samples.front().unwrap().at);
let seeded = seed_chart(&CallStats { rtt_ms: 38, receive_kbps: 182, ..CallStats::default() }, start);
assert_eq!(seeded.len(), CHART_SECONDS);
assert!(seeded.iter().all(|sample| sample.rtt_ms > 0 && sample.recv_kbps > 0));
assert_eq!(seeded.back().map(|sample| sample.at), Some(start));
}

#[test]
fn a_packet_lost_now_and_then_does_not_move_the_bars() {
// One packet of fifty is 20 per mille, and three in one bad second is 60: the old grade
Expand Down
9 changes: 8 additions & 1 deletion crates/tinyclient/src/gui/tiny/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ use crate::gui::call::health;
use gpui::{prelude::*, Bounds, ClipboardItem, Context, Entity, Focusable, PathPromptOptions, Pixels, Point, Task, Window};
use protocol::{AccountKey, CallStats, Change, Key, Link, Network, Presence, Request, Response, TinyCommunity};
use std::cell::Cell;
use std::collections::{HashMap, HashSet};
use std::collections::{HashMap, HashSet, VecDeque};
use std::io;
use std::rc::Rc;
use std::time::{Duration, Instant};
Expand Down Expand Up @@ -196,6 +196,8 @@ pub struct State {
/// The poll before `stats`, so the audio counters (counts over the whole call) can be read
/// as what grew in the last second.
stats_before: Option<CallStats>,
/// The last minute of ping and receive bitrate for Call health's time graph.
stats_chart: VecDeque<health::ChartSample>,
/// The polls over the last several seconds: what the bars and the numbers show.
steady: health::Steady,
/// Why there are no numbers, when mediad said.
Expand Down Expand Up @@ -277,6 +279,7 @@ impl State {
sliding: None,
stats: None,
stats_before: None,
stats_chart: VecDeque::new(),
steady: health::Steady::default(),
stats_note: None,
stats_task: None,
Expand Down Expand Up @@ -881,6 +884,7 @@ impl Shell {
if self.tiny_state.stats_task.take().is_some() {
self.tiny_state.stats = None;
self.tiny_state.stats_before = None;
self.tiny_state.stats_chart.clear();
self.tiny_state.steady = health::Steady::default();
self.tiny_state.stats_note = None;
}
Expand All @@ -892,6 +896,7 @@ impl Shell {
self.tiny_state.applied = call;
#[cfg(feature = "gui-shot")]
if let Some(stats) = crate::gui::call::shot_stats() {
self.tiny_state.stats_chart = health::seed_chart(&stats, Instant::now());
self.tiny_state.stats = Some(stats);
return;
}
Expand All @@ -905,13 +910,15 @@ impl Shell {
match answer {
Ok(Response::CallStats { stats }) => {
this.tiny_state.steady.feed(&stats, Instant::now());
health::push_chart(&mut this.tiny_state.stats_chart, &stats, Instant::now());
this.tiny_state.stats_before = this.tiny_state.stats.replace(stats);
this.tiny_state.stats_note = None;
}
// Before media flows mediad says why there is nothing to measure yet.
Ok(Response::Error { detail, .. }) => {
this.tiny_state.stats = None;
this.tiny_state.stats_before = None;
this.tiny_state.stats_chart.clear();
this.tiny_state.steady = health::Steady::default();
this.tiny_state.stats_note = Some(detail);
}
Expand Down
Loading
Loading