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();
}