Skip to content

Commit 3faeef9

Browse files
committed
fix: exclude cold connection setup from upload probes
1 parent 94e86bb commit 3faeef9

3 files changed

Lines changed: 164 additions & 34 deletions

File tree

apps/desktop/src-tauri/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,4 @@ nix = { version = "0.29.0", features = ["fs"] }
180180

181181
[dev-dependencies]
182182
tauri = { workspace = true, features = ["test"] }
183+
tokio = { workspace = true, features = ["test-util"] }

apps/desktop/src-tauri/src/upload_health.rs

Lines changed: 5 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ use crate::{
1111
web_api::{AuthedApiError, ManagerExt},
1212
};
1313

14+
mod timing;
15+
16+
use timing::{measure_warm_probe_rtt, upload_elapsed_after_rtt, upload_mbps_for_bytes};
17+
1418
const PROBE_BYTES: usize = 256 * 1024;
1519
const HEALTH_FRESH_FOR: Duration = Duration::from_secs(10 * 60);
1620
const HEALTH_REQUEST_TIMEOUT: Duration = Duration::from_secs(8);
@@ -168,25 +172,8 @@ async fn measure_probe_rtt(app: &AppHandle) -> Option<Duration> {
168172
}
169173
}
170174

171-
fn upload_elapsed_after_rtt(total_elapsed: Duration, rtt_elapsed: Option<Duration>) -> Duration {
172-
let total_elapsed = total_elapsed.max(Duration::from_millis(1));
173-
174-
let Some(rtt_elapsed) = rtt_elapsed else {
175-
return total_elapsed;
176-
};
177-
178-
match total_elapsed.checked_sub(rtt_elapsed) {
179-
Some(adjusted_elapsed) if adjusted_elapsed >= Duration::from_millis(50) => adjusted_elapsed,
180-
_ => total_elapsed,
181-
}
182-
}
183-
184-
fn upload_mbps_for_bytes(byte_count: usize, elapsed: Duration) -> f64 {
185-
(byte_count as f64 * 8.0) / elapsed.max(Duration::from_millis(1)).as_secs_f64() / 1_000_000.0
186-
}
187-
188175
async fn run_probe(app: &AppHandle) -> UploadHealthSnapshot {
189-
let rtt_elapsed = measure_probe_rtt(app).await;
176+
let rtt_elapsed = measure_warm_probe_rtt(HEALTH_RTT_TIMEOUT, || measure_probe_rtt(app)).await;
190177
let payload = probe_payload();
191178
let payload_len = payload.len();
192179

@@ -373,20 +360,4 @@ mod tests {
373360

374361
assert_eq!(cache.fresh_instant_resolution_cap().await, Some(1280));
375362
}
376-
377-
#[test]
378-
fn subtracts_rtt_from_probe_elapsed_when_safe() {
379-
assert_eq!(
380-
upload_elapsed_after_rtt(Duration::from_millis(700), Some(Duration::from_millis(500))),
381-
Duration::from_millis(200)
382-
);
383-
}
384-
385-
#[test]
386-
fn keeps_total_elapsed_when_rtt_would_overcorrect() {
387-
assert_eq!(
388-
upload_elapsed_after_rtt(Duration::from_millis(520), Some(Duration::from_millis(500))),
389-
Duration::from_millis(520)
390-
);
391-
}
392363
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
use std::{future::Future, time::Duration};
2+
3+
pub(super) async fn measure_warm_probe_rtt<Probe, ProbeFuture>(
4+
budget: Duration,
5+
mut probe: Probe,
6+
) -> Option<Duration>
7+
where
8+
Probe: FnMut() -> ProbeFuture,
9+
ProbeFuture: Future<Output = Option<Duration>>,
10+
{
11+
tokio::time::timeout(budget, async {
12+
// The first HEAD can include DNS/TCP/TLS setup that the pooled POST will not pay.
13+
probe().await?;
14+
probe().await
15+
})
16+
.await
17+
.ok()
18+
.flatten()
19+
}
20+
21+
pub(super) fn upload_elapsed_after_rtt(
22+
total_elapsed: Duration,
23+
rtt_elapsed: Option<Duration>,
24+
) -> Duration {
25+
let total_elapsed = total_elapsed.max(Duration::from_millis(1));
26+
27+
let Some(rtt_elapsed) = rtt_elapsed else {
28+
return total_elapsed;
29+
};
30+
31+
match total_elapsed.checked_sub(rtt_elapsed) {
32+
Some(adjusted_elapsed) if adjusted_elapsed >= Duration::from_millis(50) => adjusted_elapsed,
33+
_ => total_elapsed,
34+
}
35+
}
36+
37+
pub(super) fn upload_mbps_for_bytes(byte_count: usize, elapsed: Duration) -> f64 {
38+
(byte_count as f64 * 8.0) / elapsed.max(Duration::from_millis(1)).as_secs_f64() / 1_000_000.0
39+
}
40+
41+
#[cfg(test)]
42+
mod tests {
43+
use std::future::{pending, ready};
44+
45+
use super::*;
46+
47+
#[tokio::test]
48+
async fn cold_connection_time_does_not_inflate_upload_speed() {
49+
let mut samples = [
50+
Some(Duration::from_millis(500)),
51+
Some(Duration::from_millis(100)),
52+
]
53+
.into_iter();
54+
let rtt = measure_warm_probe_rtt(Duration::from_secs(2), || {
55+
ready(samples.next().expect("unexpected extra HEAD request"))
56+
})
57+
.await;
58+
59+
assert_eq!(rtt, Some(Duration::from_millis(100)));
60+
assert_eq!(samples.next(), None);
61+
let elapsed = upload_elapsed_after_rtt(Duration::from_millis(700), rtt);
62+
assert_eq!(elapsed, Duration::from_millis(600));
63+
let mbps = upload_mbps_for_bytes(256 * 1024, elapsed);
64+
assert!(mbps > 3.0 && mbps < 4.0, "unexpected upload rate: {mbps}");
65+
}
66+
67+
#[tokio::test]
68+
async fn failed_warmup_does_not_start_another_head() {
69+
let mut calls = 0;
70+
let rtt = measure_warm_probe_rtt(Duration::from_secs(2), || {
71+
calls += 1;
72+
ready(None)
73+
})
74+
.await;
75+
76+
assert_eq!(rtt, None);
77+
assert_eq!(calls, 1);
78+
}
79+
80+
#[tokio::test]
81+
async fn failed_measurement_does_not_reuse_the_cold_sample() {
82+
let mut samples = [Some(Duration::from_millis(500)), None].into_iter();
83+
let rtt = measure_warm_probe_rtt(Duration::from_secs(2), || {
84+
ready(samples.next().expect("unexpected extra HEAD request"))
85+
})
86+
.await;
87+
88+
assert_eq!(rtt, None);
89+
assert_eq!(
90+
upload_elapsed_after_rtt(Duration::from_millis(700), rtt),
91+
Duration::from_millis(700)
92+
);
93+
}
94+
95+
#[tokio::test]
96+
async fn budget_expires_while_warming_up() {
97+
let mut calls = 0;
98+
let rtt = measure_warm_probe_rtt(Duration::ZERO, || {
99+
calls += 1;
100+
pending::<Option<Duration>>()
101+
})
102+
.await;
103+
104+
assert_eq!(rtt, None);
105+
assert_eq!(calls, 1);
106+
}
107+
108+
#[tokio::test(start_paused = true)]
109+
async fn warmup_and_measurement_share_one_deadline() {
110+
let mut calls = 0;
111+
let started = tokio::time::Instant::now();
112+
let rtt = measure_warm_probe_rtt(Duration::from_secs(2), || {
113+
calls += 1;
114+
async {
115+
tokio::time::sleep(Duration::from_millis(1500)).await;
116+
Some(Duration::from_millis(1500))
117+
}
118+
})
119+
.await;
120+
121+
assert_eq!(rtt, None);
122+
assert_eq!(calls, 2);
123+
assert_eq!(started.elapsed(), Duration::from_secs(2));
124+
}
125+
126+
#[test]
127+
fn subtracts_warm_rtt_from_upload_elapsed() {
128+
assert_eq!(
129+
upload_elapsed_after_rtt(Duration::from_millis(700), Some(Duration::from_millis(100))),
130+
Duration::from_millis(600)
131+
);
132+
}
133+
134+
#[test]
135+
fn keeps_total_elapsed_when_rtt_would_overcorrect() {
136+
for rtt in [500, 520, 600] {
137+
assert_eq!(
138+
upload_elapsed_after_rtt(
139+
Duration::from_millis(520),
140+
Some(Duration::from_millis(rtt))
141+
),
142+
Duration::from_millis(520)
143+
);
144+
}
145+
}
146+
147+
#[test]
148+
fn elapsed_is_positive_without_an_rtt_sample() {
149+
assert_eq!(
150+
upload_elapsed_after_rtt(Duration::ZERO, None),
151+
Duration::from_millis(1)
152+
);
153+
assert_eq!(
154+
upload_elapsed_after_rtt(Duration::from_millis(700), None),
155+
Duration::from_millis(700)
156+
);
157+
}
158+
}

0 commit comments

Comments
 (0)