//! Dev diagnostics for the debug overlay (Ctrl+Shift+D): system + process //! CPU, memory, GPU, network rates, and the loudness-normalization state of //! the active track. Polled by the frontend only while the overlay is //! visible — nothing here runs otherwise. //! //! Process figures aggregate the WHOLE process tree: the WebView2 children //! do the actual rendering, so app.exe alone would tell a comforting lie. use std::collections::HashMap; use std::sync::{Arc, Mutex}; use std::time::Instant; use serde::Serialize; use sysinfo::{Networks, Pid, ProcessRefreshKind, ProcessesToUpdate, System}; use tauri::{Manager, State}; use player::StreamProgress; pub struct DiagState(Mutex); struct Inner { sys: System, networks: Networks, last_net: Instant, gpu: Option, started: Instant, /// The ACTIVE track's transfer, straight from the player's own /// StreamProgress — the overlay must read download truth at the source, /// not the UI store's window-activity-gated mirror. current_stream: Option<(u64, Arc)>, /// On-deck pre-buffer transfers by transfer id, mirrored from the /// download event stream (those updates are otherwise swallowed — no /// player, no Downloads view). The Instant is set when the transfer /// reaches a terminal state, so finished rows linger long enough to be /// seen and then fall off. preloads: HashMap)>, } /// One on-deck transfer as the overlay sees it. #[derive(Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct PreloadDl { pub username: String, /// Full remote path — the frontend matches queue rows by this. pub path: String, pub state: String, /// Remote queue position, when the peer reported one. pub place: Option, pub received: u64, pub total: u64, pub speed_bps: u64, } /// Called from the download event stream for every preload update. pub fn note_preload(app: &tauri::AppHandle, id: u64, dl: PreloadDl, terminal: bool) { if let Some(state) = app.try_state::() { let finished = if terminal { Some(Instant::now()) } else { None }; state .0 .lock() .expect("diag lock") .preloads .insert(id, (dl, finished)); } } /// Called when a play path takes the transfer over: it is the CURRENT /// track's stream from here on (note_stream reports it), not an on-deck row. pub fn retire_preload(app: &tauri::AppHandle, id: u64) { if let Some(state) = app.try_state::() { state.0.lock().expect("diag lock").preloads.remove(&id); } } /// Called by the play paths: this track streams from a peer. pub fn note_stream(app: &tauri::AppHandle, total: u64, progress: Arc) { if let Some(state) = app.try_state::() { state.0.lock().expect("diag lock").current_stream = Some((total, progress)); } } /// Called by the play paths: this track plays from a complete local file. pub fn clear_stream(app: &tauri::AppHandle) { if let Some(state) = app.try_state::() { state.0.lock().expect("diag lock").current_stream = None; } } pub fn init(app: &tauri::AppHandle) { app.manage(DiagState(Mutex::new(Inner { sys: System::new(), networks: Networks::new_with_refreshed_list(), last_net: Instant::now(), gpu: GpuCounter::open(), started: Instant::now(), current_stream: None, preloads: HashMap::new(), }))); } #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct DebugStats { /// Whole-process-tree CPU, percent of the whole machine. pub cpu_process: f32, pub cpu_system: f32, /// GPU 3D-engine utilization (Task-Manager-style), if counters opened. /// MACHINE-WIDE — includes DWM, browsers, Discord, everything. pub gpu_percent: Option, /// Same counters, restricted to OUR process tree — the app's true GPU /// share, matching Task Manager's per-process rows. The machine-wide /// number above routinely reads 2× this on a busy desktop. pub gpu_app_percent: Option, /// Process tree working set / private commit, bytes. pub mem_working_set: u64, pub mem_private: u64, pub mem_system_used: u64, pub mem_system_total: u64, /// System-wide network rates since the previous poll, bytes/sec. pub net_rx_bps: f64, pub net_tx_bps: f64, pub process_count: usize, pub uptime_secs: u64, /// Active track's transfer, from the player's own progress handle. pub dl_received: Option, pub dl_total: Option, pub dl_done: Option, pub normalization_enabled: bool, pub target_lufs: f64, pub loudness: Option, /// Main player's live fade-gain (1.0 = full; <1 only during a preview /// open/close crossfade). Proves a fade never strands the volume. pub main_fade_gain: Option, /// The normalization gain the sink has RIGHT NOW, and the one the audio /// thread is slewing toward, both dB. Reported as a pair because the /// interesting state during a mid-track correction is the gap between /// them: equal means settled, different means a correction is in flight /// and the difference is how far it still has to travel. pub norm_now_db: Option, pub norm_target_db: Option, /// The preview voices' resting volume (mirrors the app volume; 0 = muted). pub preview_volume: Option, /// The active preview clip's loudness-normalization gain in dB. pub preview_norm_db: Option, /// On-deck pre-buffer transfers, in arrival order (finished rows linger /// ~15s so a completion is visible before it drops off). pub preload_dl: Vec, } #[tauri::command] pub fn debug_stats(app: tauri::AppHandle, state: State<'_, DiagState>) -> DebugStats { let mut inner = state.0.lock().expect("diag lock"); let inner = &mut *inner; inner.sys.refresh_cpu_usage(); inner.sys.refresh_processes_specifics( ProcessesToUpdate::All, true, ProcessRefreshKind::nothing().with_cpu().with_memory(), ); // Our process tree: app.exe + every descendant (the WebView2 family). let me = Pid::from_u32(std::process::id()); let mut tree = vec![me]; let mut grew = true; while grew { grew = false; for (pid, proc_) in inner.sys.processes() { if let Some(parent) = proc_.parent() { if tree.contains(&parent) && !tree.contains(pid) { tree.push(*pid); grew = true; } } } } let cores = inner.sys.cpus().len().max(1) as f32; let mut cpu_process = 0.0f32; let mut mem_working_set = 0u64; let mut mem_private = 0u64; for pid in &tree { if let Some(p) = inner.sys.process(*pid) { cpu_process += p.cpu_usage(); // % of one core mem_working_set += p.memory(); // sysinfo's virtual_memory() is the raw address space on Windows // (WebView reserves terabytes of it) — useless. The meaningful // "reserved" figure is the private commit, straight from Win32. mem_private += private_commit(pid.as_u32()); } } cpu_process /= cores; inner.sys.refresh_memory(); // Network: sysinfo reports bytes since the LAST refresh per interface. let elapsed = inner.last_net.elapsed().as_secs_f64().max(0.05); inner.last_net = Instant::now(); inner.networks.refresh(true); let (mut rx, mut tx) = (0u64, 0u64); for (_, data) in inner.networks.iter() { rx += data.received(); tx += data.transmitted(); } let (normalization_enabled, target_lufs, loudness) = crate::loudness::snapshot(&app); let (dl_received, dl_total, dl_done) = match &inner.current_stream { Some((total, progress)) => { let (received, done) = progress.received_done(); (Some(received), Some(*total), Some(done)) } None => (None, None, None), }; let tree_u32: Vec = tree.iter().map(|p| p.as_u32()).collect(); let gpu_sample = inner.gpu.as_mut().and_then(|g| g.sample(&tree_u32)); // On-deck transfers: drop terminal rows after they've been visible a // while, then report the rest in arrival (transfer-id) order. inner .preloads .retain(|_, v| v.1.is_none_or(|t| t.elapsed().as_secs() < 15)); let mut preload_ids: Vec = inner.preloads.keys().copied().collect(); preload_ids.sort_unstable(); let preload_dl: Vec = preload_ids .iter() .filter_map(|id| inner.preloads.get(id).map(|(dl, _)| dl.clone())) .collect(); DebugStats { cpu_process, cpu_system: inner.sys.global_cpu_usage(), gpu_percent: gpu_sample.map(|(sys, _)| sys), gpu_app_percent: gpu_sample.map(|(_, app)| app), mem_working_set, mem_private, mem_system_used: inner.sys.used_memory(), mem_system_total: inner.sys.total_memory(), net_rx_bps: rx as f64 / elapsed, net_tx_bps: tx as f64 / elapsed, process_count: tree.len(), uptime_secs: inner.started.elapsed().as_secs(), dl_received, dl_total, dl_done, normalization_enabled, target_lufs, loudness, norm_now_db: app .try_state::() .map(|s| s.player.norm_gain_now_target_db().0), norm_target_db: app .try_state::() .map(|s| s.player.norm_gain_now_target_db().1), preview_volume: app .try_state::() .map(|s| crate::preview::target_volume(&s)), preview_norm_db: app .try_state::() .map(|s| crate::preview::active_norm_db(&s)), main_fade_gain: app .try_state::() .map(|p| p.player.fade_gain()), preload_dl, } } /// Private commit ("reserved") of one process, bytes. 0 if unreadable. fn private_commit(pid: u32) -> u64 { use windows::Win32::Foundation::CloseHandle; use windows::Win32::System::ProcessStatus::{ K32GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS, PROCESS_MEMORY_COUNTERS_EX, }; use windows::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION}; unsafe { let Ok(handle) = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) else { return 0; }; let mut counters = PROCESS_MEMORY_COUNTERS_EX::default(); let ok = K32GetProcessMemoryInfo( handle, (&mut counters as *mut PROCESS_MEMORY_COUNTERS_EX).cast::(), std::mem::size_of::() as u32, ) .as_bool(); let _ = CloseHandle(handle); if ok { counters.PrivateUsage as u64 } else { 0 } } } // ------------------------------------------------------------ GPU (PDH) --- /// Total 3D-engine utilization via the PDH "GPU Engine" counter set — the /// number Task Manager labels "GPU". Instances are per-process-per-engine; /// summing the 3D ones gives machine-wide utilization. First sample after /// opening reads as None (PDH rate counters need two collections). struct GpuCounter { query: windows::Win32::System::Performance::PDH_HQUERY, counter: windows::Win32::System::Performance::PDH_HCOUNTER, } // PDH handles are plain registry-ish handles; the query is only touched // under the DiagState mutex. unsafe impl Send for GpuCounter {} impl GpuCounter { fn open() -> Option { use windows::core::w; use windows::Win32::System::Performance::{ PdhAddEnglishCounterW, PdhOpenQueryW, PDH_HCOUNTER, PDH_HQUERY, }; unsafe { let mut query = PDH_HQUERY::default(); if PdhOpenQueryW(None, 0, &mut query) != 0 { return None; } let mut counter = PDH_HCOUNTER::default(); let path = w!("\\GPU Engine(*engtype_3D)\\Utilization Percentage"); if PdhAddEnglishCounterW(query, path, 0, &mut counter) != 0 { return None; } Some(GpuCounter { query, counter }) } } /// Returns (machine_total, app_tree_total). Instance names carry the /// owning pid — "pid_1234_luid_..._engtype_3D" — so the app's share is /// the sum over instances whose pid is in our process tree. fn sample(&mut self, app_pids: &[u32]) -> Option<(f32, f32)> { use windows::Win32::System::Performance::{ PdhCollectQueryData, PdhGetFormattedCounterArrayW, PDH_FMT_COUNTERVALUE_ITEM_W, PDH_FMT_DOUBLE, }; unsafe { if PdhCollectQueryData(self.query) != 0 { return None; } let mut buf_len: u32 = 0; let mut count: u32 = 0; // Size probe returns PDH_MORE_DATA; then fetch. let _ = PdhGetFormattedCounterArrayW( self.counter, PDH_FMT_DOUBLE, &mut buf_len, &mut count, None, ); if buf_len == 0 { return None; } let mut buf = vec![0u8; buf_len as usize]; let items = buf.as_mut_ptr() as *mut PDH_FMT_COUNTERVALUE_ITEM_W; if PdhGetFormattedCounterArrayW( self.counter, PDH_FMT_DOUBLE, &mut buf_len, &mut count, Some(items), ) != 0 { return None; } let mut total = 0.0f64; let mut app = 0.0f64; for i in 0..count as usize { let item = &*items.add(i); let v = item.FmtValue.Anonymous.doubleValue; total += v; if !item.szName.is_null() { if let Some(pid) = instance_pid(item.szName) { if app_pids.contains(&pid) { app += v; } } } } Some((total.min(100.0) as f32, app.min(100.0) as f32)) } } } /// Parse the pid out of a "pid_1234_luid_…" counter-instance name. unsafe fn instance_pid(name: windows::core::PWSTR) -> Option { let s = name.to_string().ok()?; let rest = s.strip_prefix("pid_")?; let digits: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect(); digits.parse().ok() } impl Drop for GpuCounter { fn drop(&mut self) { use windows::Win32::System::Performance::PdhCloseQuery; unsafe { let _ = PdhCloseQuery(self.query); } } }