Files
punktfunk/clients/windows/src/app/speed.rs
T
enricobuehler ef5808254a refactor(windows): remove the legacy in-process builtin stream path
The real Windows client is the spawned punktfunk-session Vulkan binary
(pf-client-core); the in-process builtin GUI stream — reachable only via
PUNKTFUNK_BUILTIN_STREAM=1 — was dead weight kept alive by nothing and a
recurring source of wasted effort. Remove it: delete present/render/input/
audio.rs and the builtin remainder of session/video.rs, rip all the builtin
wiring (app/mod, connect, stream), and make connect always spawn.

Preserve the two shipped keepers that happened to live in those files by
relocating them to a new probe.rs: run_speed_probe (the per-host network speed
test used by the Settings speed page and --headless --speed-test) and
decodable_codecs (the codec-capability advert on the probe connect). Trim gpu.rs
to just the Settings adapter picker (adapter_names + helpers). --headless now
supports only --speed-test — the in-process decode/frame-counter went with the
pump.

Drops the now-orphaned deps opus, wasapi, crossbeam-channel, anyhow; keeps
ffmpeg-next (probe::decodable_codecs still needs it). Net 4432 deletions.
Statically verified (module wiring, imports, orphaned symbols/deps all clean);
the type-level compile runs on the windows-amd64 CI runner, which has the
toolchain this non-Windows host lacks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 01:22:09 +02:00

180 lines
6.8 KiB
Rust

//! Per-host network speed test (the GTK/Swift clients' "Test Network Speed…"): connect over the
//! real data plane, have the host burst probe filler for 2 s up to its 3 Gbps ceiling, and
//! report goodput · loss · a recommended bitrate (≈70 % of measured), applied in one tap.
use super::style::*;
use super::{Screen, Svc};
use crate::probe::run_speed_probe;
use windows_reactor::*;
/// Speed-test lifecycle. Held as ROOT state (the probe worker completes it via
/// `Svc::set_speed`, and thread-driven updates only re-render through a prop change — see the
/// app module docs). The hosts page resets it to `Running` before navigating here.
#[derive(Clone, PartialEq)]
pub(crate) enum SpeedState {
Running,
Failed(String),
Done {
mbps: f64,
loss_pct: f32,
recommended_kbps: u32,
},
}
/// Props for the speed page: the services plus the probe lifecycle that drives its re-render.
#[derive(Clone)]
pub(crate) struct SpeedProps {
pub(crate) svc: Svc,
pub(crate) state: SpeedState,
}
impl PartialEq for SpeedProps {
fn eq(&self, other: &Self) -> bool {
self.svc == other.svc && self.state == other.state
}
}
pub(crate) fn speed_page(props: &SpeedProps, cx: &mut RenderCx) -> Element {
let ctx = &props.svc.ctx;
let set_screen = &props.svc.set_screen;
let target = ctx.shared.target.lock().unwrap().clone();
// One probe run per mount (navigating here again re-mounts and re-runs).
cx.use_effect((), {
let set_speed = props.svc.set_speed.clone();
let shared = ctx.shared.clone();
let identity = ctx.identity.clone();
let target = target.clone();
move || {
use std::sync::atomic::Ordering;
// The generation the hosts page stamped for THIS run; a stale worker (user backed
// out and started another test) must not publish over the newer run.
let generation = shared.speed_gen.load(Ordering::SeqCst);
std::thread::Builder::new()
.name("pf-speedtest".into())
.spawn(move || {
let outcome = run_speed_probe(
&target.addr,
target.port,
target.fp_hex.as_deref(),
identity,
);
if shared.speed_gen.load(Ordering::SeqCst) != generation {
return; // superseded
}
set_speed.call(match outcome {
Ok(r) => {
let mbps = f64::from(r.throughput_kbps) / 1000.0;
SpeedState::Done {
mbps,
loss_pct: r.loss_pct,
// ≈70 % of measured: headroom for FEC overhead + real-world loss.
recommended_kbps: r.throughput_kbps / 10 * 7,
}
}
Err(msg) => SpeedState::Failed(msg),
});
})
.ok();
}
});
let back_btn = {
let ss = set_screen.clone();
button("Close")
.icon(Symbol::Back)
.on_click(move || ss.call(Screen::Hosts))
.horizontal_alignment(HorizontalAlignment::Center)
};
let headline = if target.name.is_empty() {
"Network speed test".to_string()
} else {
format!("Network speed test \u{00B7} {}", target.name)
};
match &props.state {
SpeedState::Running => busy_page(
&headline,
"Measuring the path over the real data plane \u{2014} a 2 s probe burst\u{2026}",
vec![back_btn.into()],
),
SpeedState::Failed(msg) => {
let content = vstack((
text_block(headline)
.font_size(18.0)
.semibold()
.horizontal_alignment(HorizontalAlignment::Center),
InfoBar::new("Speed test failed")
.message(msg.clone())
.error()
.is_closable(false),
back_btn,
))
.spacing(16.0)
.max_width(480.0)
.horizontal_alignment(HorizontalAlignment::Center)
.vertical_alignment(VerticalAlignment::Center);
content.into()
}
SpeedState::Done {
mbps,
loss_pct,
recommended_kbps,
} => {
let recommended_mbps = f64::from(*recommended_kbps) / 1000.0;
let apply_btn = {
let (ctx, ss, kbps) = (ctx.clone(), set_screen.clone(), *recommended_kbps);
button(format!("Use {recommended_mbps:.0} Mb/s"))
.accent()
.icon(Symbol::Accept)
.on_click(move || {
let mut s = ctx.settings.lock().unwrap();
s.bitrate_kbps = kbps;
s.save();
ss.call(Screen::Hosts);
})
};
let results = card(
vstack((
text_block(format!("{mbps:.0} Mbit/s"))
.font_size(34.0)
.bold()
.horizontal_alignment(HorizontalAlignment::Center),
text_block(format!("measured \u{00B7} {loss_pct:.1} % loss"))
.font_size(12.0)
.foreground(ThemeRef::SecondaryText)
.horizontal_alignment(HorizontalAlignment::Center),
text_block(format!(
"Recommended bitrate: {recommended_mbps:.0} Mb/s (\u{2248}70 % of measured, \
leaving headroom for FEC and loss)"
))
.font_size(12.0)
.foreground(ThemeRef::SecondaryText)
.horizontal_alignment(HorizontalAlignment::Center),
hstack((apply_btn, {
let ss = set_screen.clone();
button("Close")
.icon(Symbol::Cancel)
.on_click(move || ss.call(Screen::Hosts))
}))
.spacing(8.0)
.horizontal_alignment(HorizontalAlignment::Center),
))
.spacing(12.0),
);
vstack((
text_block(headline)
.font_size(18.0)
.semibold()
.horizontal_alignment(HorizontalAlignment::Center),
results,
))
.spacing(16.0)
.max_width(480.0)
.horizontal_alignment(HorizontalAlignment::Center)
.vertical_alignment(VerticalAlignment::Center)
.into()
}
}
}