Skip to content

Commit a323d17

Browse files
committed
hotfix for high cpu usage while idle by making event receiver conditional
1 parent 215a21f commit a323d17

File tree

6 files changed

+57
-18
lines changed

6 files changed

+57
-18
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "cloudflare-speed-cli"
3-
version = "0.4.2"
3+
version = "0.4.3"
44
edition = "2021"
55
authors = ["kavehtehrani <codemonkey13x@gmail.com>"]
66
description = "CLI tool for Cloudflare speed testing with TUI interface"

src/cli.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ pub struct Cli {
6969
#[arg(long)]
7070
pub export_csv: Option<std::path::PathBuf>,
7171

72-
/// Use --auto-save true or --auto-save false to override (default: true)
72+
/// Use --auto-save true or --auto-save false to override
7373
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
7474
pub auto_save: bool,
7575

@@ -84,6 +84,10 @@ pub struct Cli {
8484
/// Path to a custom TLS certificate file (PEM or DER format)
8585
#[arg(long)]
8686
pub certificate: Option<std::path::PathBuf>,
87+
88+
/// Automatically start a test when the app launches
89+
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
90+
pub test_on_launch: bool,
8791
}
8892

8993
pub async fn run(args: Cli) -> Result<()> {

src/engine/mod.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,11 @@ impl TestEngine {
6060
});
6161

6262
// Control listener.
63+
// This task will exit naturally when the sender is dropped (which happens in cli.rs)
64+
// The receiver will return None and the loop will exit cleanly
6365
let paused2 = paused.clone();
6466
let cancel2 = cancel.clone();
65-
tokio::spawn(async move {
67+
let control_handle = tokio::spawn(async move {
6668
while let Some(msg) = control_rx.recv().await {
6769
match msg {
6870
EngineControl::Pause(p) => paused2.store(p, Ordering::Relaxed),
@@ -153,6 +155,13 @@ impl TestEngine {
153155
}
154156
}
155157

158+
// Abort the control listener task before returning.
159+
// In Tokio, dropping a JoinHandle does NOT cancel the task - it continues running!
160+
// This was causing high CPU usage when idle because the task was still waiting
161+
// on control_rx.recv().await even after the test completed.
162+
control_handle.abort();
163+
// Don't await the aborted task - just let it be cleaned up
164+
156165
Ok(RunResult {
157166
timestamp_utc: time::OffsetDateTime::now_utc()
158167
.format(&time::format_description::well_known::Rfc3339)

src/engine/throughput.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ pub async fn run_download_with_loaded_latency(
8989
let paused2 = paused.clone();
9090
let cancel2 = cancel.clone();
9191
let cfg2 = cfg.clone();
92-
tokio::spawn(async move {
92+
let lat_handle = tokio::spawn(async move {
9393
let res = run_latency_probes(
9494
&client2,
9595
Phase::Download,
@@ -165,6 +165,9 @@ pub async fn run_download_with_loaded_latency(
165165
.await
166166
.context("loaded latency task ended unexpectedly")?;
167167

168+
// Ensure the latency probe task has completed
169+
let _ = lat_handle.await;
170+
168171
Ok((dl, loaded_latency))
169172
}
170173

@@ -229,7 +232,7 @@ pub async fn run_upload_with_loaded_latency(
229232
let paused2 = paused.clone();
230233
let cancel2 = cancel.clone();
231234
let cfg2 = cfg.clone();
232-
tokio::spawn(async move {
235+
let lat_handle = tokio::spawn(async move {
233236
let res = run_latency_probes(
234237
&client2,
235238
Phase::Upload,
@@ -305,5 +308,8 @@ pub async fn run_upload_with_loaded_latency(
305308
.await
306309
.context("loaded latency task ended unexpectedly")?;
307310

311+
// Ensure the latency probe task has completed
312+
let _ = lat_handle.await;
313+
308314
Ok((up, loaded_latency))
309315
}

src/tui/mod.rs

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crossterm::{
99
execute,
1010
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
1111
};
12-
use futures::StreamExt;
12+
use futures::{future, StreamExt};
1313
use ratatui::{
1414
backend::CrosstermBackend,
1515
layout::{Constraint, Direction, Layout, Rect},
@@ -256,8 +256,12 @@ pub async fn run(args: Cli) -> Result<()> {
256256
let mut events = EventStream::new();
257257
let mut tick = tokio::time::interval(Duration::from_millis(100));
258258

259-
// Start first run.
260-
let mut run_ctx = start_run(&args).await?;
259+
// Start first run if test_on_launch is enabled
260+
let mut run_ctx = if args.test_on_launch {
261+
Some(start_run(&args).await?)
262+
} else {
263+
None
264+
};
261265

262266
let res = loop {
263267
tokio::select! {
@@ -272,12 +276,16 @@ pub async fn run(args: Cli) -> Result<()> {
272276
}
273277
match (k.modifiers, k.code) {
274278
(_, KeyCode::Char('q')) | (KeyModifiers::CONTROL, KeyCode::Char('c')) => {
275-
run_ctx.ctrl_tx.send(EngineControl::Cancel).await.ok();
279+
if let Some(ref ctx) = run_ctx {
280+
ctx.ctrl_tx.send(EngineControl::Cancel).await.ok();
281+
}
276282
break Ok(());
277283
}
278284
(_, KeyCode::Char('p')) => {
279-
state.paused = !state.paused;
280-
run_ctx.ctrl_tx.send(EngineControl::Pause(state.paused)).await.ok();
285+
if let Some(ref ctx) = run_ctx {
286+
state.paused = !state.paused;
287+
ctx.ctrl_tx.send(EngineControl::Pause(state.paused)).await.ok();
288+
}
281289
}
282290
(_, KeyCode::Char('r')) => {
283291
// Refresh history (only when on history tab)
@@ -318,9 +326,11 @@ pub async fn run(args: Cli) -> Result<()> {
318326
} else {
319327
// Rerun (only when NOT on history tab)
320328
state.info = "Restarting…".into();
321-
run_ctx.ctrl_tx.send(EngineControl::Cancel).await.ok();
322-
if let Some(h) = run_ctx.handle.take() {
323-
let _ = h.await;
329+
if let Some(ref mut ctx) = run_ctx {
330+
ctx.ctrl_tx.send(EngineControl::Cancel).await.ok();
331+
if let Some(h) = ctx.handle.take() {
332+
let _ = h.await;
333+
}
324334
}
325335
state.last_result = None;
326336
state.run_start = Instant::now();
@@ -353,7 +363,7 @@ pub async fn run(args: Cli) -> Result<()> {
353363
state.loaded_ul_latency_received = 0;
354364
state.phase = Phase::IdleLatency;
355365
state.paused = false;
356-
run_ctx = start_run(&args).await?;
366+
run_ctx = Some(start_run(&args).await?);
357367
}
358368
}
359369
(_, KeyCode::Char('s')) => {
@@ -531,11 +541,19 @@ pub async fn run(args: Cli) -> Result<()> {
531541
}
532542
}
533543
}
534-
maybe_engine_ev = run_ctx.event_rx.recv() => {
544+
// wrapping in conditional async to avoid spiking cpu usage when run_ctx is None
545+
maybe_engine_ev = async {
546+
if let Some(ref mut ctx) = run_ctx {
547+
ctx.event_rx.recv().await
548+
} else {
549+
future::pending().await
550+
}
551+
} => {
535552
match maybe_engine_ev {
536553
None => {
537554
// engine finished; wait for result
538-
if let Some(h) = run_ctx.handle.take() {
555+
if let Some(ctx) = &mut run_ctx {
556+
if let Some(h) = ctx.handle.take() {
539557
match h.await {
540558
Ok(Ok(r)) => {
541559
if state.auto_save {
@@ -607,6 +625,8 @@ pub async fn run(args: Cli) -> Result<()> {
607625
Ok(Err(e)) => state.info = format!("Run failed: {e:#}"),
608626
Err(e) => state.info = format!("Run join failed: {e}"),
609627
}
628+
}
629+
run_ctx = None; // Clear run_ctx after test completes
610630
}
611631
}
612632
Some(ev) => apply_event(&mut state, ev),

0 commit comments

Comments
 (0)