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.
This commit is contained in:
cucadmuh
2026-05-29 19:18:21 +03:00
committed by GitHub
parent 9925771a86
commit 8ea0308dba
9 changed files with 96 additions and 28 deletions
+8
View File
@@ -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 ## Changed
### Linux — session GDK, WebKitGTK mitigations, and Wayland text ### Linux — session GDK, WebKitGTK mitigations, and Wayland text
@@ -11,6 +11,7 @@ use std::sync::Mutex;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use std::fs; use std::fs;
#[cfg(any(target_os = "linux", target_os = "macos"))]
const CHILD_RESCAN_EVERY: u8 = 8; const CHILD_RESCAN_EVERY: u8 = 8;
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
@@ -2,12 +2,15 @@ import {
PERF_LIVE_POLL_MS_MAX, PERF_LIVE_POLL_MS_MAX,
PERF_LIVE_POLL_MS_MIN, PERF_LIVE_POLL_MS_MIN,
PERF_LIVE_POLL_MS_STEP, PERF_LIVE_POLL_MS_STEP,
setPerfLiveIncludeThreadGroups,
setPerfLivePollIntervalMs, setPerfLivePollIntervalMs,
usePerfLiveIncludeThreadGroups,
usePerfLivePollIntervalMs, usePerfLivePollIntervalMs,
} from '../../../utils/perf/perfLivePollSettings'; } from '../../../utils/perf/perfLivePollSettings';
export default function PerfLivePollControls() { export default function PerfLivePollControls() {
const pollMs = usePerfLivePollIntervalMs(); const pollMs = usePerfLivePollIntervalMs();
const includeThreadGroups = usePerfLiveIncludeThreadGroups();
const pollSec = (pollMs / 1000).toFixed(1); const pollSec = (pollMs / 1000).toFixed(1);
return ( return (
@@ -28,6 +31,19 @@ export default function PerfLivePollControls() {
onChange={e => setPerfLivePollIntervalMs(Number(e.target.value))} onChange={e => setPerfLivePollIntervalMs(Number(e.target.value))}
/> />
</label> </label>
<label className="perf-live-poll__row perf-live-poll__row--check">
<input
type="checkbox"
checked={includeThreadGroups}
onChange={e => setPerfLiveIncludeThreadGroups(e.target.checked)}
/>
<span className="perf-live-poll__check-label">
CPU by psysonic threads
</span>
</label>
<p className="perf-live-poll__hint">
Scans in-process thread groups each poll (Linux). Off by default enable only while diagnosing.
</p>
</section> </section>
); );
} }
@@ -1,8 +1,6 @@
import { useEffect, useMemo, useRef, useState } from 'react'; import { useMemo, useRef } from 'react';
import { usePerfLiveSnapshot } from '../../../utils/perf/perfLiveStore'; import { usePerfLiveSnapshot } from '../../../utils/perf/perfLiveStore';
import { import { usePerfLiveIncludeThreadGroups } from '../../../utils/perf/perfLivePollSettings';
syncPerfLiveThreadGroupsNeed,
} from '../../../utils/perf/perfLivePollSettings';
import { import {
togglePerfLiveOverlayPin, togglePerfLiveOverlayPin,
togglePipelineOverlayPin, togglePipelineOverlayPin,
@@ -28,13 +26,9 @@ export default function SidebarPerfProbeMonitorTab() {
const coverPinned = usePipelineOverlayPinned('pipeline:cover'); const coverPinned = usePipelineOverlayPinned('pipeline:cover');
const cpu = live.cpu; const cpu = live.cpu;
const collecting = live.collecting && cpu == null; const collecting = live.collecting && cpu == null;
const includeThreadGroups = usePerfLiveIncludeThreadGroups();
const peakMemoryKbRef = useRef(1); const peakMemoryKbRef = useRef(1);
const peakThreadCpuRef = useRef(1); const peakThreadCpuRef = useRef(1);
const [threadSectionOpen, setThreadSectionOpen] = useState(false);
useEffect(() => {
syncPerfLiveThreadGroupsNeed(threadSectionOpen, livePins);
}, [threadSectionOpen, livePins]);
const maxMemoryKb = useMemo(() => { const maxMemoryKb = useMemo(() => {
const current = Math.max(1, ...(cpu?.memory.map(m => m.rss_kb) ?? [1])); const current = Math.max(1, ...(cpu?.memory.map(m => m.rss_kb) ?? [1]));
@@ -123,11 +117,10 @@ export default function SidebarPerfProbeMonitorTab() {
/> />
</PerfProbeMetricSection> </PerfProbeMetricSection>
{(cpu.threadCpu.length > 0 || threadSectionOpen) && ( {includeThreadGroups && (
<PerfProbeMetricSection <PerfProbeMetricSection
title="CPU — psysonic threads" title="CPU — psysonic threads"
defaultOpen={false} defaultOpen
onOpenChange={setThreadSectionOpen}
> >
{cpu.threadCpu.length > 0 ? cpu.threadCpu.map(row => { {cpu.threadCpu.length > 0 ? cpu.threadCpu.map(row => {
const pinId = `cpu:thread:${row.label}` as PerfLiveOverlayPinId; const pinId = `cpu:thread:${row.label}` as PerfLiveOverlayPinId;
@@ -145,7 +138,9 @@ export default function SidebarPerfProbeMonitorTab() {
/> />
); );
}) : ( }) : (
<div className="perf-monitor-empty perf-monitor-empty--inline">Collecting thread samples</div> <div className="perf-monitor-empty perf-monitor-empty--inline">
No named psysonic threads yet wait for the next poll or load audio/analysis work.
</div>
)} )}
</PerfProbeMetricSection> </PerfProbeMetricSection>
)} )}
+1
View File
@@ -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)', '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)', '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: 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)',
], ],
}, },
{ {
-6
View File
@@ -8,7 +8,6 @@ import {
subscribePerfProbeFlags, subscribePerfProbeFlags,
} from '../utils/perf/perfFlags'; } from '../utils/perf/perfFlags';
import { hasAnyLiveMetricPollNeed, usePerfLiveOverlayPins } from '../utils/perf/perfOverlayPins'; import { hasAnyLiveMetricPollNeed, usePerfLiveOverlayPins } from '../utils/perf/perfOverlayPins';
import { syncPerfLiveThreadGroupsNeed } from '../utils/perf/perfLivePollSettings';
import { useSyncExternalStore } from 'react'; import { useSyncExternalStore } from 'react';
interface Result { interface Result {
@@ -41,11 +40,6 @@ export function useSidebarPerfProbe(): Result {
useAnalysisPerfListener(needAnalysis); useAnalysisPerfListener(needAnalysis);
useEffect(() => {
if (perfProbeOpen) return;
syncPerfLiveThreadGroupsNeed(false, livePins);
}, [perfProbeOpen, livePins]);
useEffect(() => { useEffect(() => {
setPerfProbeTelemetryActive(perfProbeOpen); setPerfProbeTelemetryActive(perfProbeOpen);
return () => setPerfProbeTelemetryActive(false); return () => setPerfProbeTelemetryActive(false);
+21
View File
@@ -192,6 +192,27 @@
width: 100%; 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 { .perf-metric-section {
margin-bottom: 10px; margin-bottom: 10px;
border: 1px solid color-mix(in srgb, var(--text-muted) 18%, transparent); border: 1px solid color-mix(in srgb, var(--text-muted) 18%, transparent);
+39 -9
View File
@@ -6,8 +6,10 @@ export const PERF_LIVE_POLL_MS_MAX = 10_000;
export const PERF_LIVE_POLL_MS_STEP = 500; export const PERF_LIVE_POLL_MS_STEP = 500;
const STORAGE_KEY = 'psysonic_perf_live_poll_ms_v1'; 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 listeners = new Set<() => void>();
const threadGroupListeners = new Set<() => void>();
let pollIntervalMs = PERF_LIVE_POLL_MS_DEFAULT; let pollIntervalMs = PERF_LIVE_POLL_MS_DEFAULT;
let includeThreadGroups = false; let includeThreadGroups = false;
let scheduleBump: (() => void) | null = null; 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(); initPollInterval();
initThreadGroups();
function emitThreadGroups(): void {
threadGroupListeners.forEach(fn => fn());
}
export function getPerfLivePollIntervalMs(): number { export function getPerfLivePollIntervalMs(): number {
return pollIntervalMs; return pollIntervalMs;
@@ -78,22 +96,34 @@ export function getPerfLiveIncludeThreadGroups(): boolean {
export function setPerfLiveIncludeThreadGroups(next: boolean): void { export function setPerfLiveIncludeThreadGroups(next: boolean): void {
if (next === includeThreadGroups) return; if (next === includeThreadGroups) return;
includeThreadGroups = next; includeThreadGroups = next;
if (typeof window !== 'undefined') {
try {
window.localStorage.setItem(THREAD_GROUPS_STORAGE_KEY, next ? '1' : '0');
} catch {
/* ignore */
}
}
emitThreadGroups();
requestScheduleBump(); requestScheduleBump();
} }
/** Thread groups when the Monitor section is open or a thread metric is pinned. */ export function subscribePerfLiveIncludeThreadGroups(cb: () => void): () => void {
export function syncPerfLiveThreadGroupsNeed( threadGroupListeners.add(cb);
sectionOpen: boolean, return () => threadGroupListeners.delete(cb);
pins: ReadonlySet<string>, }
): void {
const pinnedThread = [...pins].some(pin => pin.startsWith('cpu:thread:')); export function usePerfLiveIncludeThreadGroups(): boolean {
setPerfLiveIncludeThreadGroups(sectionOpen || pinnedThread); return useSyncExternalStore(
subscribePerfLiveIncludeThreadGroups,
getPerfLiveIncludeThreadGroups,
() => false,
);
} }
export type PerfCpuSnapshotRequest = { export type PerfCpuSnapshotRequest = {
include_thread_groups: boolean; includeThreadGroups: boolean;
}; };
export function buildPerfCpuSnapshotRequest(): PerfCpuSnapshotRequest { export function buildPerfCpuSnapshotRequest(): PerfCpuSnapshotRequest {
return { include_thread_groups: includeThreadGroups }; return { includeThreadGroups };
} }
+2
View File
@@ -231,6 +231,8 @@ export function bumpPerfLivePollSchedule(): void {
window.clearTimeout(pollTimer); window.clearTimeout(pollTimer);
pollTimer = null; pollTimer = null;
} }
// Fresh baseline after interval / thread-group option changes.
prevProc = null;
schedulePoll(); schedulePoll();
} }