diff --git a/app/src/system/info.rs b/app/src/system/info.rs index 593db740e0c..4712c0aab5f 100644 --- a/app/src/system/info.rs +++ b/app/src/system/info.rs @@ -228,12 +228,31 @@ impl SystemInfo { .with_cpu() } + /// Returns the [`sysinfo::ProcessRefreshKind`] to use for a full + /// process-table sweep where we only need process identity (e.g. to check + /// whether a process with a given name is running). + /// + /// Unlike [`Self::refresh_kind`], this deliberately excludes CPU and memory + /// sampling. On Windows, sampling per-process CPU usage issues an + /// `NtQueryInformationProcess(ProcessCycleTime)` call for *every* process, + /// and each such call forces `KeFlushProcessWriteBuffers` — a synchronous + /// inter-processor interrupt broadcast to all logical cores. Doing that + /// across the entire process table (potentially in bursts, e.g. once per + /// shell-session bootstrap) can keep every core spinning at + /// `DISPATCH_LEVEL` long enough to trip the DPC watchdog and bugcheck the + /// machine on high-core-count systems. Enumerating names only avoids those + /// per-process syscalls. + #[cfg_attr(not(windows), allow(dead_code))] + fn all_processes_refresh_kind() -> sysinfo::ProcessRefreshKind { + sysinfo::ProcessRefreshKind::nothing() + } + #[cfg_attr(not(windows), allow(dead_code))] pub fn refresh_all_processes(&mut self) { self.system.refresh_processes_specifics( ProcessesToUpdate::All, true, /* remove_dead_processes */ - Self::refresh_kind(), + Self::all_processes_refresh_kind(), ); } diff --git a/app/src/system/info_tests.rs b/app/src/system/info_tests.rs index 7f856966700..2aa47e0a6f0 100644 --- a/app/src/system/info_tests.rs +++ b/app/src/system/info_tests.rs @@ -54,3 +54,33 @@ fn test_memory_usage_stats_construction() { assert_eq!(stats.inactive_24h_stats.num_blocks, 1); assert_eq!(stats.inactive_24h_stats.num_lines, 0); } + +/// Regression test for the Windows DPC_WATCHDOG_VIOLATION caused by +/// enumerating the full process table with per-process CPU sampling. +/// +/// The full-table sweep ([`SystemInfo::refresh_all_processes`], used only to +/// check whether a process with a given name is running) must NOT request CPU +/// or memory data: on Windows, per-process CPU sampling issues +/// `NtQueryInformationProcess(ProcessCycleTime)` for every process, each +/// forcing an all-core `KeFlushProcessWriteBuffers` IPI. The single-PID +/// self-poll ([`SystemInfo::refresh_kind`]) legitimately still samples both. +#[test] +fn all_processes_refresh_kind_does_not_sample_cpu_or_memory() { + let all = SystemInfo::all_processes_refresh_kind(); + assert!( + !all.cpu(), + "full process-table sweep must not sample per-process CPU (forces \ + NtQueryInformationProcess(ProcessCycleTime) -> all-core IPI per process)" + ); + assert!( + !all.memory(), + "full process-table sweep only needs process names, not memory" + ); + + // The single-PID self-poll should still gather CPU/memory (cheap: one PID). + let self_poll = SystemInfo::refresh_kind(); + assert!( + self_poll.cpu() && self_poll.memory(), + "current-process self-poll should still sample CPU and memory" + ); +} diff --git a/app/src/util/windows.rs b/app/src/util/windows.rs index 8780c802406..f813a40d80c 100644 --- a/app/src/util/windows.rs +++ b/app/src/util/windows.rs @@ -1,5 +1,5 @@ use std::path::{Path, PathBuf}; -use std::sync::LazyLock; +use std::sync::{LazyLock, OnceLock}; use std::{env, path}; use anyhow::{Result, anyhow}; @@ -198,14 +198,34 @@ fn microsoft_store_app_path() -> Option { Some(microsoft_store_app_path) } +/// Caches whether Kaspersky was detected, computed once per process launch. +/// +/// See [`is_kaspersky_running`] for why this is cached. +static KASPERSKY_RUNNING: OnceLock = OnceLock::new(); + /// Determines if Kaspersky is currently running by checking if there is a /// process with the name "avp" running. +/// +/// The result is cached for the lifetime of the process. Antivirus presence +/// does not meaningfully change during a Warp session, and the underlying +/// check enumerates the entire process table (see +/// [`SystemInfo::refresh_all_processes`]) — expensive on Windows. Without this +/// cache the sweep ran on *every* session bootstrap (every tab/pane/subshell), +/// which on high-core-count machines could trip the DPC watchdog. Caching +/// turns it into a single one-time check. pub fn is_kaspersky_running(ctx: &mut AppContext) -> bool { - SystemInfo::handle(ctx).update(ctx, |system_info, _| { + if let Some(cached) = KASPERSKY_RUNNING.get() { + return *cached; + } + + let running = SystemInfo::handle(ctx).update(ctx, |system_info, _| { system_info.refresh_all_processes(); system_info .processes_by_name(KASPERSKY_PROCESS_NAME) .next() .is_some() - }) + }); + + // If another caller raced us, keep the first value that was stored. + *KASPERSKY_RUNNING.get_or_init(|| running) }