From 8ea0308dba0e14e836659d24c9b743e407baf617 Mon Sep 17 00:00:00 2001 From: cucadmuh <49571317+cucadmuh@users.noreply.github.com> Date: Fri, 29 May 2026 19:18:21 +0300 Subject: [PATCH] feat(perf): explicit toggle for live thread-group CPU polling (#891) * feat(perf): explicit toggle for live thread-group CPU polling Replace implicit thread-group collection (section open / pin) with a persisted checkbox so Linux /proc scans run only when the user opts in for diagnosis. Fix IPC: pass includeThreadGroups (camelCase) so Tauri maps the flag to Rust; reset the CPU baseline when the option changes so thread % deltas are valid. * docs: CHANGELOG and credits for PR #891 * fix(perf): gate CHILD_RESCAN_EVERY to Linux/macOS only Avoid dead_code warning on Windows where perf child-PID rescan is unused. --- CHANGELOG.md | 8 ++++ src-tauri/src/lib_commands/app_api/perf.rs | 1 + .../perfProbe/PerfLivePollControls.tsx | 16 +++++++ .../perfProbe/SidebarPerfProbeMonitorTab.tsx | 21 ++++---- src/config/settingsCredits.ts | 1 + src/hooks/useSidebarPerfProbe.ts | 6 --- src/styles/components/modal.css | 21 ++++++++ src/utils/perf/perfLivePollSettings.ts | 48 +++++++++++++++---- src/utils/perf/perfLiveStore.ts | 2 + 9 files changed, 96 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44de37c7..b939f9b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -162,6 +162,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +### Performance Probe — thread-group CPU toggle + +**By [@cucadmuh](https://github.com/cucadmuh), PR [#891](https://github.com/Psychotoxical/psysonic/pull/891)** + +* Monitor live poll: explicit opt-in checkbox for Linux `/proc` thread-group CPU (off by default); fixes camelCase IPC so thread rows populate instead of staying on “Collecting…”. + + + ## Changed ### Linux — session GDK, WebKitGTK mitigations, and Wayland text diff --git a/src-tauri/src/lib_commands/app_api/perf.rs b/src-tauri/src/lib_commands/app_api/perf.rs index 115e470f..df8ba34e 100644 --- a/src-tauri/src/lib_commands/app_api/perf.rs +++ b/src-tauri/src/lib_commands/app_api/perf.rs @@ -11,6 +11,7 @@ use std::sync::Mutex; #[cfg(target_os = "linux")] use std::fs; +#[cfg(any(target_os = "linux", target_os = "macos"))] const CHILD_RESCAN_EVERY: u8 = 8; #[derive(Debug, Clone, Serialize)] diff --git a/src/components/sidebar/perfProbe/PerfLivePollControls.tsx b/src/components/sidebar/perfProbe/PerfLivePollControls.tsx index 3fbedf6f..39df88b1 100644 --- a/src/components/sidebar/perfProbe/PerfLivePollControls.tsx +++ b/src/components/sidebar/perfProbe/PerfLivePollControls.tsx @@ -2,12 +2,15 @@ import { PERF_LIVE_POLL_MS_MAX, PERF_LIVE_POLL_MS_MIN, PERF_LIVE_POLL_MS_STEP, + setPerfLiveIncludeThreadGroups, setPerfLivePollIntervalMs, + usePerfLiveIncludeThreadGroups, usePerfLivePollIntervalMs, } from '../../../utils/perf/perfLivePollSettings'; export default function PerfLivePollControls() { const pollMs = usePerfLivePollIntervalMs(); + const includeThreadGroups = usePerfLiveIncludeThreadGroups(); const pollSec = (pollMs / 1000).toFixed(1); return ( @@ -28,6 +31,19 @@ export default function PerfLivePollControls() { onChange={e => setPerfLivePollIntervalMs(Number(e.target.value))} /> + +

+ Scans in-process thread groups each poll (Linux). Off by default — enable only while diagnosing. +

); } diff --git a/src/components/sidebar/perfProbe/SidebarPerfProbeMonitorTab.tsx b/src/components/sidebar/perfProbe/SidebarPerfProbeMonitorTab.tsx index ae900572..d0153fb6 100644 --- a/src/components/sidebar/perfProbe/SidebarPerfProbeMonitorTab.tsx +++ b/src/components/sidebar/perfProbe/SidebarPerfProbeMonitorTab.tsx @@ -1,8 +1,6 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useMemo, useRef } from 'react'; import { usePerfLiveSnapshot } from '../../../utils/perf/perfLiveStore'; -import { - syncPerfLiveThreadGroupsNeed, -} from '../../../utils/perf/perfLivePollSettings'; +import { usePerfLiveIncludeThreadGroups } from '../../../utils/perf/perfLivePollSettings'; import { togglePerfLiveOverlayPin, togglePipelineOverlayPin, @@ -28,13 +26,9 @@ export default function SidebarPerfProbeMonitorTab() { const coverPinned = usePipelineOverlayPinned('pipeline:cover'); const cpu = live.cpu; const collecting = live.collecting && cpu == null; + const includeThreadGroups = usePerfLiveIncludeThreadGroups(); const peakMemoryKbRef = useRef(1); const peakThreadCpuRef = useRef(1); - const [threadSectionOpen, setThreadSectionOpen] = useState(false); - - useEffect(() => { - syncPerfLiveThreadGroupsNeed(threadSectionOpen, livePins); - }, [threadSectionOpen, livePins]); const maxMemoryKb = useMemo(() => { const current = Math.max(1, ...(cpu?.memory.map(m => m.rss_kb) ?? [1])); @@ -123,11 +117,10 @@ export default function SidebarPerfProbeMonitorTab() { /> - {(cpu.threadCpu.length > 0 || threadSectionOpen) && ( + {includeThreadGroups && ( {cpu.threadCpu.length > 0 ? cpu.threadCpu.map(row => { const pinId = `cpu:thread:${row.label}` as PerfLiveOverlayPinId; @@ -145,7 +138,9 @@ export default function SidebarPerfProbeMonitorTab() { /> ); }) : ( -
Collecting thread samples…
+
+ No named psysonic threads yet — wait for the next poll or load audio/analysis work. +
)}
)} diff --git a/src/config/settingsCredits.ts b/src/config/settingsCredits.ts index a242d6d3..2f9f3e3e 100644 --- a/src/config/settingsCredits.ts +++ b/src/config/settingsCredits.ts @@ -341,6 +341,7 @@ const CONTRIBUTOR_ENTRIES = [ 'Discord Rich Presence: configurable activity-name template — member list shows the playing track instead of "Psysonic" (PR #885)', 'Library browse: lazy 200-row local catalog chunks, unified in-page scroll, cover priority during scroll (PR #890)', 'Performance Probe: Monitor/Toggles redesign, live CPU/RSS/thread metrics, overlay pins and sparklines, macOS snapshots (PR #890)', + 'Performance Probe: opt-in thread-group CPU poll toggle and includeThreadGroups IPC fix (PR #891)', ], }, { diff --git a/src/hooks/useSidebarPerfProbe.ts b/src/hooks/useSidebarPerfProbe.ts index 185f3c7c..20fd278f 100644 --- a/src/hooks/useSidebarPerfProbe.ts +++ b/src/hooks/useSidebarPerfProbe.ts @@ -8,7 +8,6 @@ import { subscribePerfProbeFlags, } from '../utils/perf/perfFlags'; import { hasAnyLiveMetricPollNeed, usePerfLiveOverlayPins } from '../utils/perf/perfOverlayPins'; -import { syncPerfLiveThreadGroupsNeed } from '../utils/perf/perfLivePollSettings'; import { useSyncExternalStore } from 'react'; interface Result { @@ -41,11 +40,6 @@ export function useSidebarPerfProbe(): Result { useAnalysisPerfListener(needAnalysis); - useEffect(() => { - if (perfProbeOpen) return; - syncPerfLiveThreadGroupsNeed(false, livePins); - }, [perfProbeOpen, livePins]); - useEffect(() => { setPerfProbeTelemetryActive(perfProbeOpen); return () => setPerfProbeTelemetryActive(false); diff --git a/src/styles/components/modal.css b/src/styles/components/modal.css index 7c439019..3bf7d7d6 100644 --- a/src/styles/components/modal.css +++ b/src/styles/components/modal.css @@ -192,6 +192,27 @@ width: 100%; } +.perf-live-poll__row--check { + flex-direction: row; + align-items: center; + gap: 8px; + margin-top: 10px; + cursor: pointer; + user-select: none; +} + +.perf-live-poll__check-label { + font-size: 11px; + color: var(--text-primary); +} + +.perf-live-poll__hint { + margin: 6px 0 0; + font-size: 10px; + line-height: 1.35; + color: color-mix(in srgb, var(--text-muted) 88%, transparent); +} + .perf-metric-section { margin-bottom: 10px; border: 1px solid color-mix(in srgb, var(--text-muted) 18%, transparent); diff --git a/src/utils/perf/perfLivePollSettings.ts b/src/utils/perf/perfLivePollSettings.ts index c369d011..e1f519d4 100644 --- a/src/utils/perf/perfLivePollSettings.ts +++ b/src/utils/perf/perfLivePollSettings.ts @@ -6,8 +6,10 @@ export const PERF_LIVE_POLL_MS_MAX = 10_000; export const PERF_LIVE_POLL_MS_STEP = 500; const STORAGE_KEY = 'psysonic_perf_live_poll_ms_v1'; +const THREAD_GROUPS_STORAGE_KEY = 'psysonic_perf_live_thread_groups_v1'; const listeners = new Set<() => void>(); +const threadGroupListeners = new Set<() => void>(); let pollIntervalMs = PERF_LIVE_POLL_MS_DEFAULT; let includeThreadGroups = false; let scheduleBump: (() => void) | null = null; @@ -37,7 +39,23 @@ function initPollInterval(): void { } } +function initThreadGroups(): void { + if (typeof window === 'undefined') return; + try { + const raw = window.localStorage.getItem(THREAD_GROUPS_STORAGE_KEY); + if (raw == null) return; + includeThreadGroups = raw === '1' || raw === 'true'; + } catch { + /* ignore */ + } +} + initPollInterval(); +initThreadGroups(); + +function emitThreadGroups(): void { + threadGroupListeners.forEach(fn => fn()); +} export function getPerfLivePollIntervalMs(): number { return pollIntervalMs; @@ -78,22 +96,34 @@ export function getPerfLiveIncludeThreadGroups(): boolean { export function setPerfLiveIncludeThreadGroups(next: boolean): void { if (next === includeThreadGroups) return; includeThreadGroups = next; + if (typeof window !== 'undefined') { + try { + window.localStorage.setItem(THREAD_GROUPS_STORAGE_KEY, next ? '1' : '0'); + } catch { + /* ignore */ + } + } + emitThreadGroups(); requestScheduleBump(); } -/** Thread groups when the Monitor section is open or a thread metric is pinned. */ -export function syncPerfLiveThreadGroupsNeed( - sectionOpen: boolean, - pins: ReadonlySet, -): void { - const pinnedThread = [...pins].some(pin => pin.startsWith('cpu:thread:')); - setPerfLiveIncludeThreadGroups(sectionOpen || pinnedThread); +export function subscribePerfLiveIncludeThreadGroups(cb: () => void): () => void { + threadGroupListeners.add(cb); + return () => threadGroupListeners.delete(cb); +} + +export function usePerfLiveIncludeThreadGroups(): boolean { + return useSyncExternalStore( + subscribePerfLiveIncludeThreadGroups, + getPerfLiveIncludeThreadGroups, + () => false, + ); } export type PerfCpuSnapshotRequest = { - include_thread_groups: boolean; + includeThreadGroups: boolean; }; export function buildPerfCpuSnapshotRequest(): PerfCpuSnapshotRequest { - return { include_thread_groups: includeThreadGroups }; + return { includeThreadGroups }; } diff --git a/src/utils/perf/perfLiveStore.ts b/src/utils/perf/perfLiveStore.ts index 10c26244..86cc1fa6 100644 --- a/src/utils/perf/perfLiveStore.ts +++ b/src/utils/perf/perfLiveStore.ts @@ -231,6 +231,8 @@ export function bumpPerfLivePollSchedule(): void { window.clearTimeout(pollTimer); pollTimer = null; } + // Fresh baseline after interval / thread-group option changes. + prevProc = null; schedulePoll(); }