mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 22:15:40 +00:00
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:
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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)',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user