1use std::{
4 collections::{HashMap, HashSet, VecDeque},
5 sync::{
6 Arc, Mutex,
7 atomic::{AtomicU64, Ordering},
8 },
9 time::Duration,
10};
11
12use tokio::{
13 sync::mpsc,
14 task::{AbortHandle, JoinHandle, JoinSet},
15 time::{Instant, MissedTickBehavior},
16};
17
18use crate::{
19 Error, FlowInfo, FlowStage, LatencySample, LoadedLatencyReport, LossReport, LossSample,
20 MeasurementPathReport, NetBenchConfig, NetBenchEvent, NetBenchFlow, NetBenchProbeConfig,
21 NetBenchProbeReport, NetBenchReceiveStream, NetBenchReport, NetBenchSendStream,
22 NetBenchSession, NetBenchTelemetry, PROTOCOL_VERSION, PathKind, Result, SCHEMA_VERSION,
23 ThroughputDirection, ThroughputReport, ThroughputSample, TransportReport,
24 config::{LOADED_LATENCY_INTERVAL, MAX_PROBE_RATE_PER_SECOND, THROUGHPUT_SAMPLE_INTERVAL},
25 statistics::latency_report,
26 wire::{
27 Capabilities, ControlMessage, PROBE_MAGIC, Probe, ProbeKind, read_control, write_control,
28 },
29};
30
31const THROUGHPUT_CLEANUP_TIMEOUT: Duration = Duration::from_secs(5);
32type SendStream = Box<dyn NetBenchSendStream>;
33type RecvStream = Box<dyn NetBenchReceiveStream>;
34
35#[derive(Clone)]
36struct Connection(Arc<dyn NetBenchSession>);
37
38impl Connection {
39 fn remote_id(&self) -> String {
40 self.0.remote_peer_id()
41 }
42
43 fn telemetry(&self) -> NetBenchTelemetry {
44 self.0.telemetry()
45 }
46
47 fn max_datagram_size(&self) -> Option<usize> {
48 self.0.max_datagram_size()
49 }
50
51 async fn open_uni(&self) -> Result<SendStream> {
52 Ok(self.0.open_bi().await?.into_split().0)
53 }
54
55 async fn accept_uni(&self) -> Result<RecvStream> {
56 Ok(self.0.accept_bi().await?.into_split().1)
57 }
58
59 async fn send_datagram_wait(&self, bytes: Vec<u8>) -> Result<()> {
60 self.0.send_datagram(bytes).await
61 }
62
63 async fn read_datagram(&self) -> Result<Vec<u8>> {
64 self.0.read_datagram().await
65 }
66}
67
68#[derive(Clone)]
70pub struct NetBenchInitiator;
71
72impl std::fmt::Debug for NetBenchInitiator {
73 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 formatter.debug_struct("NetBenchInitiator").finish()
75 }
76}
77
78impl Default for NetBenchInitiator {
79 fn default() -> Self {
80 Self::new()
81 }
82}
83
84impl NetBenchInitiator {
85 #[must_use]
87 pub fn new() -> Self {
88 Self
89 }
90
91 pub async fn run(&self, flow: NetBenchFlow, config: NetBenchConfig) -> Result<NetBenchReport> {
97 self.start(flow, config).await?.result().await
98 }
99
100 pub async fn run_probes(
109 &self,
110 flow: NetBenchFlow,
111 config: NetBenchProbeConfig,
112 ) -> Result<NetBenchProbeReport> {
113 let (event_tx, _event_rx) = mpsc::channel(128);
114 let overall_timeout = config.overall_timeout;
115 tokio::time::timeout(
116 overall_timeout,
117 run_probe_benchmark(flow, config, &EventSink(event_tx)),
118 )
119 .await
120 .map_err(|_| Error::Timeout {
121 stage: "probe benchmark",
122 })?
123 }
124
125 #[allow(
131 clippy::unused_async,
132 reason = "keeps the documented start(...).await API"
133 )]
134 pub async fn start(&self, flow: NetBenchFlow, config: NetBenchConfig) -> Result<NetBenchTest> {
135 let (event_tx, event_rx) = mpsc::channel(128);
136 let events = EventSink(event_tx);
137 let overall_timeout = config.overall_timeout;
138 let stage = BenchmarkStage::default();
139 let task_stage = stage.clone();
140 let task = tokio::spawn(async move {
141 let result = if let Ok(result) = tokio::time::timeout(
142 overall_timeout,
143 run_benchmark(flow, config, &events, &task_stage),
144 )
145 .await
146 {
147 result
148 } else {
149 Err(Error::Timeout {
150 stage: task_stage.current(),
151 })
152 };
153 if let Ok(report) = &result {
154 events.send(NetBenchEvent::Finished(report.clone()));
155 }
156 result
157 });
158 let abort = task.abort_handle();
159
160 Ok(NetBenchTest {
161 events: event_rx,
162 task: Some(task),
163 abort,
164 })
165 }
166}
167
168#[derive(Debug)]
170pub struct NetBenchTest {
171 events: mpsc::Receiver<NetBenchEvent>,
172 task: Option<JoinHandle<Result<NetBenchReport>>>,
173 abort: AbortHandle,
174}
175
176impl NetBenchTest {
177 pub async fn next(&mut self) -> Option<NetBenchEvent> {
179 self.events.recv().await
180 }
181
182 pub fn cancel(&self) {
186 self.abort.abort();
187 }
188
189 #[must_use]
191 pub fn is_finished(&self) -> bool {
192 self.task.as_ref().is_none_or(JoinHandle::is_finished)
193 }
194
195 pub async fn abort_and_wait(mut self) -> Result<()> {
201 self.cancel();
202 let Some(task) = self.task.take() else {
203 return Ok(());
204 };
205 match task.await {
206 Ok(_) => Ok(()),
207 Err(error) if error.is_cancelled() => Ok(()),
208 Err(error) => Err(Error::Protocol(format!(
209 "benchmark task failed while being cancelled: {error}"
210 ))),
211 }
212 }
213
214 pub async fn result(mut self) -> Result<NetBenchReport> {
220 let task = self
221 .task
222 .take()
223 .ok_or_else(|| Error::Protocol("benchmark result was already consumed".to_owned()))?;
224 match task.await {
225 Ok(result) => result,
226 Err(error) if error.is_cancelled() => Err(Error::Cancelled),
227 Err(error) => Err(Error::Protocol(format!(
228 "benchmark task ended without a result: {error}"
229 ))),
230 }
231 }
232}
233
234impl Drop for NetBenchTest {
235 fn drop(&mut self) {
236 self.cancel();
237 }
238}
239
240#[derive(Clone, Debug)]
241struct BenchmarkStage(Arc<Mutex<&'static str>>);
242
243impl Default for BenchmarkStage {
244 fn default() -> Self {
245 Self(Arc::new(Mutex::new("flow setup")))
246 }
247}
248
249impl BenchmarkStage {
250 fn set(&self, stage: &'static str) {
251 *lock_unpoisoned(&self.0) = stage;
252 }
253
254 fn current(&self) -> &'static str {
255 *lock_unpoisoned(&self.0)
256 }
257}
258
259fn lock_unpoisoned<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
260 mutex
261 .lock()
262 .unwrap_or_else(std::sync::PoisonError::into_inner)
263}
264
265#[derive(Clone)]
266struct EventSink(mpsc::Sender<NetBenchEvent>);
267
268impl EventSink {
269 fn send(&self, event: NetBenchEvent) {
270 let _ = self.0.try_send(event);
271 }
272}
273
274#[allow(
275 clippy::too_many_lines,
276 reason = "linear ordering documents the benchmark state machine"
277)]
278async fn run_benchmark(
279 flow: NetBenchFlow,
280 config: NetBenchConfig,
281 events: &EventSink,
282 stage: &BenchmarkStage,
283) -> Result<NetBenchReport> {
284 let (session, mut control_send, mut control_recv) = flow.into_parts();
285 let connection = Connection(session);
286 let total_started = Instant::now();
287 let connect_started = Instant::now();
288 let peer_id = connection.remote_id();
289
290 stage.set("protocol negotiation");
291 events.send(NetBenchEvent::FlowStage(FlowStage::ControlStreamReady));
292 write_control(
293 &mut control_send,
294 &ControlMessage::ClientHello {
295 protocol_versions: vec![PROTOCOL_VERSION],
296 capabilities: Capabilities {
297 datagram_probes: true,
298 loaded_latency: true,
299 path_stats: true,
300 },
301 },
302 )
303 .await?;
304 events.send(NetBenchEvent::FlowStage(FlowStage::ClientHelloSent));
305
306 let hello = read_control(&mut control_recv).await?;
307 let first_control_message_time = connect_started.elapsed();
308 let ControlMessage::ServerHello {
309 selected_version,
310 limits,
311 capabilities,
312 } = hello
313 else {
314 return Err(peer_or_protocol_error(hello));
315 };
316 if selected_version != PROTOCOL_VERSION {
317 return Err(Error::UnsupportedProtocolVersion);
318 }
319 events.send(NetBenchEvent::FlowStage(FlowStage::ServerHelloReceived {
320 version: selected_version,
321 }));
322 if !capabilities.datagram_probes {
323 return finish_with_error(
324 &mut control_send,
325 &mut control_recv,
326 Error::Protocol("peer does not support Datagram probes".to_owned()),
327 )
328 .await;
329 }
330 if let Err(error) = validate_against_server_limits(&config, limits) {
331 return finish_with_error(&mut control_send, &mut control_recv, error).await;
332 }
333
334 stage.set("path stabilization");
335 let path_at_flow_start = selected_path_kind(&connection);
336 let (initial_path, stabilization_time_to_direct) = stabilize_path(
337 &connection,
338 config.path_stabilization_timeout,
339 connect_started,
340 events,
341 )
342 .await;
343 events.send(NetBenchEvent::Ready(FlowInfo {
344 negotiation_time: first_control_message_time,
345 path: initial_path,
346 }));
347 let (path_monitor, path_monitor_state) =
348 spawn_path_monitor(connection.clone(), events.clone(), connect_started);
349
350 let before = TransportSnapshot::capture(&connection);
351 let mut test_id = 1_u64;
352
353 stage.set("idle latency");
354 events.send(NetBenchEvent::LatencyStarted);
355 write_control(
356 &mut control_send,
357 &ControlMessage::StartLatency {
358 test_id,
359 duration_ms: duration_ms(config.latency_duration),
360 interval_ms: duration_ms(config.latency_interval),
361 },
362 )
363 .await?;
364 let idle = measure_probes(
365 connection.clone(),
366 test_id,
367 config.latency_duration,
368 config.latency_interval,
369 config.probe_timeout,
370 ProbeEventMode::Latency(events.clone()),
371 )
372 .await?;
373 write_control(&mut control_send, &ControlMessage::StopTest { test_id }).await?;
374 let idle_latency = latency_report(&idle.rtts);
375 test_id += 1;
376
377 stage.set("loss probes");
378 events.send(NetBenchEvent::LossStarted);
379 write_control(
380 &mut control_send,
381 &ControlMessage::StartLoss {
382 test_id,
383 duration_ms: duration_ms(config.loss_duration),
384 rate_per_second: config.loss_rate_per_second,
385 timeout_ms: duration_ms(config.probe_timeout),
386 },
387 )
388 .await?;
389 let loss_interval =
390 Duration::from_secs_f64(1.0 / f64::from(config.loss_rate_per_second.max(1)));
391 let loss_samples = measure_probes(
392 connection.clone(),
393 test_id,
394 config.loss_duration,
395 loss_interval,
396 config.probe_timeout,
397 ProbeEventMode::Loss(events.clone()),
398 )
399 .await?;
400 write_control(&mut control_send, &ControlMessage::StopTest { test_id }).await?;
401 let loss = loss_samples.loss_report();
402 test_id += 1;
403
404 if !config.download_warmup.is_zero() {
405 stage.set("download warm-up");
406 events.send(NetBenchEvent::DownloadWarmupStarted);
407 let _ = download_phase(
408 &connection,
409 &mut control_send,
410 &mut control_recv,
411 test_id,
412 config.download_warmup,
413 config.parallel_streams,
414 config.chunk_size,
415 None,
416 )
417 .await?;
418 test_id += 1;
419 }
420
421 stage.set("download throughput");
422 events.send(NetBenchEvent::DownloadStarted);
423 let download_future = download_phase(
424 &connection,
425 &mut control_send,
426 &mut control_recv,
427 test_id,
428 config.download_duration,
429 config.parallel_streams,
430 config.chunk_size,
431 Some(events.clone()),
432 );
433 let download_probe_future = measure_probes(
434 connection.clone(),
435 test_id,
436 config.download_duration,
437 LOADED_LATENCY_INTERVAL,
438 config.probe_timeout,
439 ProbeEventMode::Latency(events.clone()),
440 );
441 let (download, download_loaded) = tokio::try_join!(download_future, download_probe_future)?;
442 test_id += 1;
443
444 if !config.upload_warmup.is_zero() {
445 stage.set("upload warm-up");
446 events.send(NetBenchEvent::UploadWarmupStarted);
447 if !matches!(
448 selected_path_kind(&connection),
449 PathKind::Direct | PathKind::DirectIpv4 | PathKind::DirectIpv6
450 ) {
451 let _ = stabilize_path(
452 &connection,
453 config.path_stabilization_timeout,
454 connect_started,
455 events,
456 )
457 .await;
458 }
459 let _ = upload_phase(
460 &connection,
461 &mut control_send,
462 &mut control_recv,
463 test_id,
464 config.upload_warmup,
465 config.parallel_streams,
466 config.chunk_size,
467 None,
468 )
469 .await?;
470 test_id += 1;
471 }
472
473 stage.set("upload throughput");
474 events.send(NetBenchEvent::UploadStarted);
475 let upload_future = upload_phase(
476 &connection,
477 &mut control_send,
478 &mut control_recv,
479 test_id,
480 config.upload_duration,
481 config.parallel_streams,
482 config.chunk_size,
483 Some(events.clone()),
484 );
485 let upload_probe_future = measure_probes(
486 connection.clone(),
487 test_id,
488 config.upload_duration,
489 LOADED_LATENCY_INTERVAL,
490 config.probe_timeout,
491 ProbeEventMode::Latency(events.clone()),
492 );
493 let (upload, upload_loaded) = tokio::try_join!(upload_future, upload_probe_future)?;
494
495 stage.set("finalization");
496 let after = TransportSnapshot::capture(&connection);
497 path_monitor.abort_and_wait().await;
498 let final_path = selected_path_kind(&connection);
499 let path_monitor_state = lock_unpoisoned(&path_monitor_state).clone();
500 let time_to_direct = if path_at_flow_start == PathKind::Relay {
501 stabilization_time_to_direct.or(path_monitor_state.time_to_direct)
502 } else {
503 None
504 };
505 let download_p50 = latency_report(&download_loaded.rtts).p50;
506 let upload_p50 = latency_report(&upload_loaded.rtts).p50;
507 let loaded_latency = LoadedLatencyReport {
508 idle_p50: idle_latency.p50,
509 download_p50,
510 download_increase: download_p50.saturating_sub(idle_latency.p50),
511 upload_p50,
512 upload_increase: upload_p50.saturating_sub(idle_latency.p50),
513 };
514
515 finish_flow(&mut control_send, &mut control_recv).await?;
516
517 Ok(NetBenchReport {
518 schema_version: SCHEMA_VERSION,
519 protocol_version: selected_version,
520 peer_id,
521 total_duration: total_started.elapsed(),
522 path: MeasurementPathReport {
523 negotiation_time: first_control_message_time,
524 initial_path,
525 final_path,
526 path_changed: path_at_flow_start != initial_path
527 || initial_path != final_path
528 || path_monitor_state.path_changed,
529 became_direct: time_to_direct.is_some(),
530 time_to_direct,
531 },
532 idle_latency,
533 loss,
534 download,
535 upload,
536 loaded_latency,
537 transport: after.delta(before),
538 })
539}
540
541#[allow(
542 clippy::too_many_lines,
543 reason = "mirrors the full benchmark negotiation while omitting all throughput phases"
544)]
545async fn run_probe_benchmark(
546 flow: NetBenchFlow,
547 config: NetBenchProbeConfig,
548 events: &EventSink,
549) -> Result<NetBenchProbeReport> {
550 let (session, mut control_send, mut control_recv) = flow.into_parts();
551 let connection = Connection(session);
552 let total_started = Instant::now();
553 let connect_started = Instant::now();
554 let peer_id = connection.remote_id();
555
556 events.send(NetBenchEvent::FlowStage(FlowStage::ControlStreamReady));
557 write_control(
558 &mut control_send,
559 &ControlMessage::ClientHello {
560 protocol_versions: vec![PROTOCOL_VERSION],
561 capabilities: Capabilities {
562 datagram_probes: true,
563 loaded_latency: false,
564 path_stats: true,
565 },
566 },
567 )
568 .await?;
569 events.send(NetBenchEvent::FlowStage(FlowStage::ClientHelloSent));
570
571 let hello = read_control(&mut control_recv).await?;
572 let first_control_message_time = connect_started.elapsed();
573 let ControlMessage::ServerHello {
574 selected_version,
575 limits,
576 capabilities,
577 } = hello
578 else {
579 return Err(peer_or_protocol_error(hello));
580 };
581 if selected_version != PROTOCOL_VERSION {
582 return Err(Error::UnsupportedProtocolVersion);
583 }
584 events.send(NetBenchEvent::FlowStage(FlowStage::ServerHelloReceived {
585 version: selected_version,
586 }));
587 if !capabilities.datagram_probes {
588 return finish_with_error(
589 &mut control_send,
590 &mut control_recv,
591 Error::Protocol("peer does not support Datagram probes".to_owned()),
592 )
593 .await;
594 }
595 if let Err(error) = validate_probes_against_server_limits(&config, limits) {
596 return finish_with_error(&mut control_send, &mut control_recv, error).await;
597 }
598
599 let path_at_flow_start = selected_path_kind(&connection);
600 let (initial_path, stabilization_time_to_direct) = stabilize_path(
601 &connection,
602 config.path_stabilization_timeout,
603 connect_started,
604 events,
605 )
606 .await;
607 events.send(NetBenchEvent::Ready(FlowInfo {
608 negotiation_time: first_control_message_time,
609 path: initial_path,
610 }));
611 let (path_monitor, path_monitor_state) =
612 spawn_path_monitor(connection.clone(), events.clone(), connect_started);
613 let before = TransportSnapshot::capture(&connection);
614
615 let mut test_id = 1_u64;
616 events.send(NetBenchEvent::LatencyStarted);
617 write_control(
618 &mut control_send,
619 &ControlMessage::StartLatency {
620 test_id,
621 duration_ms: duration_ms(config.latency_duration),
622 interval_ms: duration_ms(config.latency_interval),
623 },
624 )
625 .await?;
626 let idle = measure_probes(
627 connection.clone(),
628 test_id,
629 config.latency_duration,
630 config.latency_interval,
631 config.probe_timeout,
632 ProbeEventMode::Latency(events.clone()),
633 )
634 .await?;
635 write_control(&mut control_send, &ControlMessage::StopTest { test_id }).await?;
636 let idle_latency = latency_report(&idle.rtts);
637 test_id += 1;
638
639 events.send(NetBenchEvent::LossStarted);
640 write_control(
641 &mut control_send,
642 &ControlMessage::StartLoss {
643 test_id,
644 duration_ms: duration_ms(config.loss_duration),
645 rate_per_second: config.loss_rate_per_second,
646 timeout_ms: duration_ms(config.probe_timeout),
647 },
648 )
649 .await?;
650 let loss_interval =
651 Duration::from_secs_f64(1.0 / f64::from(config.loss_rate_per_second.max(1)));
652 let loss_samples = measure_probes(
653 connection.clone(),
654 test_id,
655 config.loss_duration,
656 loss_interval,
657 config.probe_timeout,
658 ProbeEventMode::Loss(events.clone()),
659 )
660 .await?;
661 write_control(&mut control_send, &ControlMessage::StopTest { test_id }).await?;
662 let loss = loss_samples.loss_report();
663
664 let after = TransportSnapshot::capture(&connection);
665 path_monitor.abort_and_wait().await;
666 let final_path = selected_path_kind(&connection);
667 let path_monitor_state = lock_unpoisoned(&path_monitor_state).clone();
668 let time_to_direct = if path_at_flow_start == PathKind::Relay {
669 stabilization_time_to_direct.or(path_monitor_state.time_to_direct)
670 } else {
671 None
672 };
673 finish_flow(&mut control_send, &mut control_recv).await?;
674
675 Ok(NetBenchProbeReport {
676 schema_version: SCHEMA_VERSION,
677 protocol_version: selected_version,
678 peer_id,
679 total_duration: total_started.elapsed(),
680 path: MeasurementPathReport {
681 negotiation_time: first_control_message_time,
682 initial_path,
683 final_path,
684 path_changed: path_at_flow_start != initial_path
685 || initial_path != final_path
686 || path_monitor_state.path_changed,
687 became_direct: time_to_direct.is_some(),
688 time_to_direct,
689 },
690 idle_latency,
691 loss,
692 transport: after.delta(before),
693 })
694}
695
696fn validate_against_server_limits(
697 config: &NetBenchConfig,
698 limits: crate::wire::ServerLimits,
699) -> Result<()> {
700 validate_probe_parameters(
701 config.latency_duration,
702 config.latency_interval,
703 config.loss_duration,
704 config.loss_rate_per_second,
705 )?;
706 if limits.parallel_streams == 0 {
707 return Err(Error::ThroughputDeniedByPeer);
708 }
709 if config.parallel_streams == 0 {
710 return Err(Error::Protocol(
711 "parallel stream count must be greater than zero".to_owned(),
712 ));
713 }
714 if config.chunk_size == 0 {
715 return Err(Error::Protocol(
716 "throughput chunk size must be greater than zero".to_owned(),
717 ));
718 }
719 if duration_ms(config.download_duration) == 0 || duration_ms(config.upload_duration) == 0 {
720 return Err(Error::Protocol(
721 "throughput durations must be at least one millisecond".to_owned(),
722 ));
723 }
724 let maximum = Duration::from_millis(u64::from(limits.test_duration_ms));
725 for duration in [
726 config.latency_duration,
727 config.loss_duration,
728 config.download_duration,
729 config.upload_duration,
730 config.download_warmup,
731 config.upload_warmup,
732 ] {
733 if !duration.is_zero() && duration_ms(duration) == 0 {
734 return Err(Error::Protocol(
735 "phase durations must be zero or at least one millisecond".to_owned(),
736 ));
737 }
738 if duration > maximum {
739 return Err(Error::DurationLimitExceeded {
740 requested: duration,
741 maximum,
742 });
743 }
744 }
745 if config.parallel_streams > limits.parallel_streams {
746 return Err(Error::Protocol(format!(
747 "requested {} streams but server allows {}",
748 config.parallel_streams, limits.parallel_streams
749 )));
750 }
751 if config.chunk_size > limits.chunk_size {
752 return Err(Error::Protocol(format!(
753 "requested {} byte chunks but server allows {}",
754 config.chunk_size, limits.chunk_size
755 )));
756 }
757 Ok(())
758}
759
760fn validate_probes_against_server_limits(
761 config: &NetBenchProbeConfig,
762 limits: crate::wire::ServerLimits,
763) -> Result<()> {
764 validate_probe_parameters(
765 config.latency_duration,
766 config.latency_interval,
767 config.loss_duration,
768 config.loss_rate_per_second,
769 )?;
770 let maximum = Duration::from_millis(u64::from(limits.test_duration_ms));
771 for duration in [config.latency_duration, config.loss_duration] {
772 if duration > maximum {
773 return Err(Error::DurationLimitExceeded {
774 requested: duration,
775 maximum,
776 });
777 }
778 }
779 Ok(())
780}
781
782fn validate_probe_parameters(
783 latency_duration: Duration,
784 latency_interval: Duration,
785 loss_duration: Duration,
786 loss_rate_per_second: u32,
787) -> Result<()> {
788 if duration_ms(latency_duration) == 0 || duration_ms(loss_duration) == 0 {
789 return Err(Error::Protocol(
790 "probe durations must be at least one millisecond".to_owned(),
791 ));
792 }
793 if duration_ms(latency_interval) == 0 {
794 return Err(Error::Protocol(
795 "latency probe interval must be at least one millisecond".to_owned(),
796 ));
797 }
798 if !(1..=MAX_PROBE_RATE_PER_SECOND).contains(&loss_rate_per_second) {
799 return Err(Error::Protocol(format!(
800 "loss probe rate must be within 1..={MAX_PROBE_RATE_PER_SECOND} per second"
801 )));
802 }
803 Ok(())
804}
805
806async fn finish_with_error<T>(
807 send: &mut SendStream,
808 recv: &mut RecvStream,
809 error: Error,
810) -> Result<T> {
811 finish_flow(send, recv).await?;
812 Err(error)
813}
814
815fn peer_or_protocol_error(message: ControlMessage) -> Error {
816 match message {
817 ControlMessage::Error {
818 code,
819 message: peer_message,
820 } => Error::Peer {
821 code: code as u16,
822 message: peer_message,
823 },
824 other => Error::Protocol(format!("expected ServerHello, received {other:?}")),
825 }
826}
827
828async fn stabilize_path(
829 connection: &Connection,
830 timeout: Duration,
831 connect_started: Instant,
832 events: &EventSink,
833) -> (PathKind, Option<Duration>) {
834 let deadline = Instant::now() + timeout;
835 let mut previous = None;
836 loop {
837 let kind = selected_path_kind(connection);
838 if previous != Some(kind) {
839 events.send(NetBenchEvent::FlowStage(FlowStage::PathObserved {
840 path: kind,
841 }));
842 previous = Some(kind);
843 }
844 if matches!(
845 kind,
846 PathKind::Direct | PathKind::DirectIpv4 | PathKind::DirectIpv6
847 ) {
848 let elapsed = connect_started.elapsed();
849 events.send(NetBenchEvent::FlowStage(FlowStage::DirectPathSelected {
850 elapsed,
851 }));
852 return (kind, Some(elapsed));
853 }
854 if Instant::now() >= deadline {
855 if kind == PathKind::Relay {
856 events.send(NetBenchEvent::FlowStage(FlowStage::RelayRetained {
857 waited: timeout,
858 }));
859 }
860 return (kind, None);
861 }
862 tokio::time::sleep(Duration::from_millis(100)).await;
863 }
864}
865
866#[derive(Clone)]
867enum ProbeEventMode {
868 Latency(EventSink),
869 Loss(EventSink),
870}
871
872struct ProbeResults {
873 sent: u64,
874 received: u64,
875 duplicated: u64,
876 reordered: u64,
877 rtts: Vec<Duration>,
878}
879
880impl ProbeResults {
881 #[allow(
882 clippy::cast_precision_loss,
883 reason = "ratios are intentionally reported as f64"
884 )]
885 fn loss_report(&self) -> LossReport {
886 let timed_out = self.sent.saturating_sub(self.received);
887 LossReport {
888 sent: self.sent,
889 received: self.received,
890 timed_out,
891 duplicated: self.duplicated,
892 reordered: self.reordered,
893 timeout_ratio: if self.sent == 0 {
894 0.0
895 } else {
896 timed_out as f64 / self.sent as f64
897 },
898 }
899 }
900}
901
902async fn measure_probes(
903 connection: Connection,
904 test_id: u64,
905 duration: Duration,
906 interval: Duration,
907 timeout: Duration,
908 mode: ProbeEventMode,
909) -> Result<ProbeResults> {
910 if connection.max_datagram_size().is_none() {
911 return Err(Error::Protocol(
912 "QUIC Datagram is unavailable on this connection".to_owned(),
913 ));
914 }
915
916 let started = Instant::now();
917 let send_deadline = started + duration;
918 let final_deadline = send_deadline + timeout;
919 let mut ticker = tokio::time::interval(interval.max(Duration::from_millis(1)));
920 ticker.set_missed_tick_behavior(MissedTickBehavior::Delay);
921 let mut sent_at = HashMap::<u64, Instant>::new();
922 let mut received = HashSet::<u64>::new();
923 let mut sequence = 0_u64;
924 let mut duplicated = 0_u64;
925 let mut reordered = 0_u64;
926 let mut highest_received = None::<u64>;
927 let mut rtts = Vec::new();
928
929 loop {
930 if Instant::now() >= final_deadline {
931 break;
932 }
933 tokio::select! {
934 _ = ticker.tick(), if Instant::now() < send_deadline => {
935 let probe = Probe {
936 magic: PROBE_MAGIC,
937 test_id,
938 sequence,
939 kind: ProbeKind::Request,
940 };
941 let payload = postcard::to_allocvec(&probe)?;
942 match tokio::time::timeout_at(
943 send_deadline,
944 connection.send_datagram_wait(payload),
945 )
946 .await
947 {
948 Ok(result) => result.map_err(Error::network)?,
949 Err(_) => break,
950 }
951 sent_at.insert(sequence, Instant::now());
952 sequence += 1;
953 }
954 datagram = connection.read_datagram() => {
955 let bytes = datagram.map_err(Error::network)?;
956 let Ok(probe) = postcard::from_bytes::<Probe>(&bytes) else {
957 continue;
958 };
959 if probe.magic != PROBE_MAGIC || probe.test_id != test_id || probe.kind != ProbeKind::Response {
960 continue;
961 }
962 let Some(sent) = sent_at.get(&probe.sequence) else {
963 continue;
964 };
965 if !received.insert(probe.sequence) {
966 duplicated += 1;
967 continue;
968 }
969 if highest_received.is_some_and(|highest| probe.sequence < highest) {
970 reordered += 1;
971 }
972 highest_received = Some(highest_received.map_or(probe.sequence, |value| value.max(probe.sequence)));
973 let rtt = sent.elapsed();
974 rtts.push(rtt);
975 match &mode {
976 ProbeEventMode::Latency(events) => events.send(NetBenchEvent::LatencySample(
977 LatencySample { sequence: probe.sequence, rtt }
978 )),
979 ProbeEventMode::Loss(events) => events.send(NetBenchEvent::LossSample(
980 LossSample {
981 sent: sequence,
982 received: received.len() as u64,
983 outstanding: sequence.saturating_sub(received.len() as u64),
984 }
985 )),
986 }
987 }
988 () = tokio::time::sleep_until(final_deadline) => break,
989 }
990
991 if Instant::now() >= send_deadline && received.len() == sent_at.len() {
992 break;
993 }
994 }
995
996 Ok(ProbeResults {
997 sent: sequence,
998 received: received.len() as u64,
999 duplicated,
1000 reordered,
1001 rtts,
1002 })
1003}
1004
1005#[allow(clippy::too_many_arguments)]
1006async fn download_phase(
1007 connection: &Connection,
1008 control_send: &mut SendStream,
1009 control_recv: &mut RecvStream,
1010 test_id: u64,
1011 duration: Duration,
1012 streams: u16,
1013 chunk_size: u32,
1014 events: Option<EventSink>,
1015) -> Result<ThroughputReport> {
1016 write_control(
1017 control_send,
1018 &ControlMessage::StartDownload {
1019 test_id,
1020 duration_ms: duration_ms(duration),
1021 streams,
1022 chunk_size,
1023 },
1024 )
1025 .await?;
1026
1027 let total = Arc::new(AtomicU64::new(0));
1028 let mut download_streams = Vec::with_capacity(usize::from(streams));
1029 for _ in 0..streams {
1030 let mut stream = connection.accept_uni().await?;
1031 let mut magic = [0_u8; 1];
1032 stream.read_exact(&mut magic).await?;
1033 if magic[0] != 0x44 {
1034 return Err(Error::Protocol(
1035 "download stream has an invalid pre-measurement header".to_owned(),
1036 ));
1037 }
1038 download_streams.push(stream);
1039 }
1040 expect_ready(control_recv, test_id).await?;
1041 write_control(control_send, &ControlMessage::TestReady { test_id }).await?;
1042
1043 let started = Instant::now();
1044 let deadline = started + duration;
1045 let sample_task = spawn_sampler(
1046 Arc::clone(&total),
1047 started,
1048 ThroughputDirection::Download,
1049 events,
1050 );
1051 let mut tasks = JoinSet::new();
1052 for mut stream in download_streams {
1053 let total = Arc::clone(&total);
1054 tasks.spawn(async move {
1055 let mut buffer = vec![0_u8; 64 * 1024];
1056 while Instant::now() < deadline {
1057 match tokio::time::timeout_at(deadline, stream.read(&mut buffer)).await {
1058 Ok(Ok(read)) if read > 0 && Instant::now() <= deadline => {
1059 total.fetch_add(read as u64, Ordering::Relaxed);
1060 }
1061 Ok(Ok(_)) | Err(_) => break,
1062 Ok(Err(error)) => return Err(error),
1063 }
1064 }
1065 stream.cancel();
1066 Result::<()>::Ok(())
1067 });
1068 }
1069 while let Some(result) = tasks.join_next().await {
1070 result.map_err(Error::network)??;
1071 }
1072 tokio::time::sleep_until(deadline).await;
1073 sample_task.abort_and_wait().await;
1074 let bytes = total.load(Ordering::Relaxed);
1075 tracing::debug!(
1076 test_id,
1077 direction = "download",
1078 cleanup_timeout = ?THROUGHPUT_CLEANUP_TIMEOUT,
1079 "waiting for throughput completion on the prioritized control stream"
1080 );
1081 tokio::time::timeout(
1082 THROUGHPUT_CLEANUP_TIMEOUT,
1083 expect_finished(control_recv, test_id),
1084 )
1085 .await
1086 .map_err(|_| Error::Timeout {
1087 stage: "download cleanup",
1088 })??;
1089 Ok(throughput_report(bytes, duration, streams))
1090}
1091
1092#[allow(
1093 clippy::cast_precision_loss,
1094 clippy::too_many_arguments,
1095 clippy::too_many_lines,
1096 reason = "receiver-confirmed live samples keep the upload state machine explicit"
1097)]
1098async fn upload_phase(
1099 connection: &Connection,
1100 control_send: &mut SendStream,
1101 control_recv: &mut RecvStream,
1102 test_id: u64,
1103 duration: Duration,
1104 streams: u16,
1105 chunk_size: u32,
1106 events: Option<EventSink>,
1107) -> Result<ThroughputReport> {
1108 write_control(
1109 control_send,
1110 &ControlMessage::StartUpload {
1111 test_id,
1112 duration_ms: duration_ms(duration),
1113 streams,
1114 chunk_size,
1115 },
1116 )
1117 .await?;
1118
1119 let mut upload_streams = Vec::with_capacity(usize::from(streams));
1120 for _ in 0..streams {
1121 let mut stream = connection.open_uni().await?;
1122 stream.write_all(&[0x55]).await?;
1123 upload_streams.push(stream);
1124 }
1125 expect_ready(control_recv, test_id).await?;
1126
1127 let started = Instant::now();
1128 let deadline = started + duration;
1129 let chunk = Arc::new(vec![
1130 0x5A;
1131 usize::try_from(chunk_size).map_err(Error::network)?
1132 ]);
1133 let mut tasks = JoinSet::new();
1134 for mut stream in upload_streams {
1135 let chunk = Arc::clone(&chunk);
1136 tasks.spawn(async move {
1137 while Instant::now() < deadline {
1138 match tokio::time::timeout_at(deadline, stream.write(&chunk)).await {
1139 Ok(Ok(_)) => {}
1140 Ok(Err(Error::FlowStopped)) | Err(_) => break,
1141 Ok(Err(error)) => return Err(error),
1142 }
1143 }
1144 stream.cancel();
1145 Result::<()>::Ok(())
1146 });
1147 }
1148 let mut progress = VecDeque::<(Duration, u64)>::from([(Duration::ZERO, 0)]);
1149 let cleanup_deadline = deadline + THROUGHPUT_CLEANUP_TIMEOUT;
1150 tracing::debug!(
1151 test_id,
1152 direction = "upload",
1153 cleanup_timeout = ?THROUGHPUT_CLEANUP_TIMEOUT,
1154 "waiting for throughput completion on the prioritized control stream"
1155 );
1156 let received_bytes = loop {
1157 let message = tokio::time::timeout_at(cleanup_deadline, read_control(control_recv))
1158 .await
1159 .map_err(|_| Error::Timeout {
1160 stage: "upload cleanup",
1161 })??;
1162 match message {
1163 ControlMessage::ThroughputProgress {
1164 test_id: received_test_id,
1165 received_bytes,
1166 duration_ns,
1167 } if received_test_id == test_id => {
1168 let receiver_elapsed = Duration::from_nanos(duration_ns).min(duration);
1169 progress.push_back((receiver_elapsed, received_bytes));
1170 while progress.len() > 5 {
1171 progress.pop_front();
1172 }
1173 let (old_elapsed, old_bytes) = progress.front().copied().unwrap_or_default();
1174 let sample_duration = receiver_elapsed.saturating_sub(old_elapsed);
1175 let interval_bps = if sample_duration.is_zero() {
1176 0.0
1177 } else {
1178 received_bytes.saturating_sub(old_bytes) as f64 * 8.0
1179 / sample_duration.as_secs_f64()
1180 };
1181 if let Some(events) = &events {
1182 events.send(NetBenchEvent::UploadSample(ThroughputSample {
1183 direction: ThroughputDirection::Upload,
1184 elapsed: receiver_elapsed,
1185 received_bytes,
1186 interval_bps,
1187 }));
1188 }
1189 }
1190 ControlMessage::TestFinished {
1191 test_id: received_test_id,
1192 received_bytes,
1193 ..
1194 } if received_test_id == test_id => break received_bytes,
1195 ControlMessage::Error { code, message } => {
1196 return Err(Error::Peer {
1197 code: code as u16,
1198 message,
1199 });
1200 }
1201 message => {
1202 return Err(Error::Protocol(format!(
1203 "expected upload progress or completion, received {message:?}"
1204 )));
1205 }
1206 }
1207 };
1208 tokio::time::timeout_at(cleanup_deadline, async {
1209 while let Some(result) = tasks.join_next().await {
1210 result.map_err(Error::network)??;
1211 }
1212 Result::<()>::Ok(())
1213 })
1214 .await
1215 .map_err(|_| Error::Timeout {
1216 stage: "upload stream cleanup",
1217 })??;
1218 Ok(throughput_report(received_bytes, duration, streams))
1219}
1220
1221async fn expect_ready(control_recv: &mut RecvStream, expected_test_id: u64) -> Result<()> {
1222 match read_control(control_recv).await? {
1223 ControlMessage::TestReady { test_id } if test_id == expected_test_id => Ok(()),
1224 ControlMessage::TestReady { test_id } => Err(Error::Protocol(format!(
1225 "received readiness for test {test_id}, expected {expected_test_id}"
1226 ))),
1227 ControlMessage::Error { code, message } => Err(Error::Peer {
1228 code: code as u16,
1229 message,
1230 }),
1231 message => Err(Error::Protocol(format!(
1232 "expected TestReady, received {message:?}"
1233 ))),
1234 }
1235}
1236
1237#[allow(
1238 clippy::cast_precision_loss,
1239 reason = "throughput samples are intentionally reported as f64"
1240)]
1241fn spawn_sampler(
1242 total: Arc<AtomicU64>,
1243 started: Instant,
1244 direction: ThroughputDirection,
1245 events: Option<EventSink>,
1246) -> AbortOnDropTask<()> {
1247 AbortOnDropTask::new(tokio::spawn(async move {
1248 let Some(events) = events else {
1249 return;
1250 };
1251 let mut samples = VecDeque::<(Instant, u64)>::from([(started, 0)]);
1252 let mut ticker = tokio::time::interval(THROUGHPUT_SAMPLE_INTERVAL);
1253 ticker.set_missed_tick_behavior(MissedTickBehavior::Delay);
1254 ticker.tick().await;
1255 loop {
1256 ticker.tick().await;
1257 let now = Instant::now();
1258 let bytes = total.load(Ordering::Relaxed);
1259 samples.push_back((now, bytes));
1260 while samples.len() > 5 {
1261 samples.pop_front();
1262 }
1263 let (previous_time, previous_bytes) = samples.front().copied().unwrap_or((now, bytes));
1264 let delta_bytes = bytes.saturating_sub(previous_bytes);
1265 let delta_seconds = now.duration_since(previous_time).as_secs_f64();
1266 let interval_bps = if delta_seconds > 0.0 {
1267 delta_bytes as f64 * 8.0 / delta_seconds
1268 } else {
1269 0.0
1270 };
1271 events.send(match direction {
1272 ThroughputDirection::Download => NetBenchEvent::DownloadSample(ThroughputSample {
1273 direction,
1274 elapsed: started.elapsed(),
1275 received_bytes: bytes,
1276 interval_bps,
1277 }),
1278 ThroughputDirection::Upload => NetBenchEvent::UploadSample(ThroughputSample {
1279 direction,
1280 elapsed: started.elapsed(),
1281 received_bytes: bytes,
1282 interval_bps,
1283 }),
1284 });
1285 }
1286 }))
1287}
1288
1289#[derive(Clone, Debug, Default)]
1290struct PathMonitorState {
1291 path_changed: bool,
1292 time_to_direct: Option<Duration>,
1293}
1294
1295fn spawn_path_monitor(
1296 connection: Connection,
1297 events: EventSink,
1298 flow_started: Instant,
1299) -> (AbortOnDropTask<()>, Arc<Mutex<PathMonitorState>>) {
1300 let state = Arc::new(Mutex::new(PathMonitorState::default()));
1301 let task_state = Arc::clone(&state);
1302 let task = AbortOnDropTask::new(tokio::spawn(async move {
1303 let mut previous = selected_path_kind(&connection);
1304 let mut ticker = tokio::time::interval(Duration::from_millis(250));
1305 loop {
1306 ticker.tick().await;
1307 let current = selected_path_kind(&connection);
1308 if current != previous {
1309 let mut state = lock_unpoisoned(&task_state);
1310 state.path_changed = true;
1311 if previous == PathKind::Relay
1312 && matches!(
1313 current,
1314 PathKind::Direct | PathKind::DirectIpv4 | PathKind::DirectIpv6
1315 )
1316 && state.time_to_direct.is_none()
1317 {
1318 state.time_to_direct = Some(flow_started.elapsed());
1319 }
1320 drop(state);
1321 events.send(NetBenchEvent::FlowStage(FlowStage::PathObserved {
1322 path: current,
1323 }));
1324 previous = current;
1325 }
1326 }
1327 }));
1328 (task, state)
1329}
1330
1331#[derive(Debug)]
1332struct AbortOnDropTask<T>(Option<JoinHandle<T>>);
1333
1334impl<T> AbortOnDropTask<T> {
1335 fn new(task: JoinHandle<T>) -> Self {
1336 Self(Some(task))
1337 }
1338
1339 async fn abort_and_wait(mut self) {
1340 if let Some(task) = self.0.take() {
1341 task.abort();
1342 let _ = task.await;
1343 }
1344 }
1345}
1346
1347impl<T> Drop for AbortOnDropTask<T> {
1348 fn drop(&mut self) {
1349 if let Some(task) = &self.0 {
1350 task.abort();
1351 }
1352 }
1353}
1354
1355async fn expect_finished(recv: &mut RecvStream, expected_test_id: u64) -> Result<(u64, Duration)> {
1356 match read_control(recv).await? {
1357 ControlMessage::TestFinished {
1358 test_id,
1359 received_bytes,
1360 duration_ns,
1361 } if test_id == expected_test_id => Ok((received_bytes, Duration::from_nanos(duration_ns))),
1362 ControlMessage::Error { code, message } => Err(Error::Peer {
1363 code: code as u16,
1364 message,
1365 }),
1366 other => Err(Error::Protocol(format!(
1367 "expected TestFinished({expected_test_id}), received {other:?}"
1368 ))),
1369 }
1370}
1371
1372async fn finish_flow(send: &mut SendStream, recv: &mut RecvStream) -> Result<()> {
1373 write_control(send, &ControlMessage::FlowFinished).await?;
1374 match read_control(recv).await? {
1375 ControlMessage::FlowFinishedAck => {
1376 send.finish().map_err(Error::network)?;
1377 Ok(())
1378 }
1379 ControlMessage::Error { code, message } => Err(Error::Peer {
1380 code: code as u16,
1381 message,
1382 }),
1383 other => Err(Error::Protocol(format!(
1384 "expected FlowFinishedAck, received {other:?}"
1385 ))),
1386 }
1387}
1388
1389#[allow(
1390 clippy::cast_precision_loss,
1391 reason = "throughput is intentionally reported as f64"
1392)]
1393fn throughput_report(bytes: u64, duration: Duration, streams: u16) -> ThroughputReport {
1394 ThroughputReport {
1395 received_bytes: bytes,
1396 measurement_duration: duration,
1397 bits_per_second: if duration.is_zero() {
1398 0.0
1399 } else {
1400 bytes as f64 * 8.0 / duration.as_secs_f64()
1401 },
1402 streams,
1403 }
1404}
1405
1406fn selected_path_kind(connection: &Connection) -> PathKind {
1407 connection.telemetry().path
1408}
1409
1410#[derive(Clone, Copy, Default)]
1411struct TransportSnapshot {
1412 connection_lost_packets: u64,
1413 connection_lost_bytes: u64,
1414 udp_rx_datagrams: u64,
1415 udp_tx_datagrams: u64,
1416 congestion_events: u64,
1417 black_holes_detected: u64,
1418 current_mtu: u16,
1419 rtt: Duration,
1420}
1421
1422impl TransportSnapshot {
1423 fn capture(connection: &Connection) -> Self {
1424 let telemetry = connection.telemetry();
1425 Self {
1426 connection_lost_packets: telemetry.lost_packets,
1427 connection_lost_bytes: telemetry.lost_bytes,
1428 udp_rx_datagrams: telemetry.rx_datagrams,
1429 udp_tx_datagrams: telemetry.tx_datagrams,
1430 congestion_events: telemetry.congestion_events,
1431 black_holes_detected: telemetry.black_holes_detected,
1432 current_mtu: telemetry.current_mtu,
1433 rtt: telemetry.rtt,
1434 }
1435 }
1436
1437 fn delta(self, before: Self) -> TransportReport {
1438 TransportReport {
1439 lost_packets: self
1440 .connection_lost_packets
1441 .saturating_sub(before.connection_lost_packets),
1442 lost_bytes: self
1443 .connection_lost_bytes
1444 .saturating_sub(before.connection_lost_bytes),
1445 congestion_events: self
1446 .congestion_events
1447 .saturating_sub(before.congestion_events),
1448 udp_rx_datagrams: self
1449 .udp_rx_datagrams
1450 .saturating_sub(before.udp_rx_datagrams),
1451 udp_tx_datagrams: self
1452 .udp_tx_datagrams
1453 .saturating_sub(before.udp_tx_datagrams),
1454 current_mtu: self.current_mtu,
1455 black_holes_detected: self
1456 .black_holes_detected
1457 .saturating_sub(before.black_holes_detected),
1458 final_rtt: self.rtt,
1459 }
1460 }
1461}
1462
1463fn duration_ms(duration: Duration) -> u32 {
1464 u32::try_from(duration.as_millis()).unwrap_or(u32::MAX)
1465}