Skip to content

Commit dd2d66d

Browse files
committed
feat(settings): P3-18 内置性能诊断面板(FPS/CPU/内存实时采集+一键复制上报)
1 parent 651c2c6 commit dd2d66d

4 files changed

Lines changed: 189 additions & 0 deletions

File tree

‎client/electron/performanceMode.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,31 @@ export function registerPerformanceHandlers(): void {
7272
setPerformanceMode(mode);
7373
return { success: true, mode: getPerformanceMode() };
7474
});
75+
76+
// P3-18 性能诊断:汇总各进程 CPU/内存 + 系统内存,供内置诊断面板采集
77+
safeHandle('perf:get-metrics', async () => {
78+
const metrics = app.getAppMetrics();
79+
let totalCpu = 0;
80+
let totalMemoryKb = 0;
81+
const processes = metrics.map((m) => {
82+
totalCpu += m.cpu.percentCPUUsage;
83+
totalMemoryKb += m.memory.workingSetSize;
84+
return {
85+
type: m.type,
86+
pid: m.pid,
87+
cpu: Math.round(m.cpu.percentCPUUsage * 10) / 10,
88+
memoryMb: Math.round(m.memory.workingSetSize / 1024),
89+
};
90+
});
91+
const sysMem = process.getSystemMemoryInfo?.();
92+
return {
93+
totalCpu: Math.round(totalCpu * 10) / 10,
94+
totalMemoryMb: Math.round(totalMemoryKb / 1024),
95+
processCount: metrics.length,
96+
system: sysMem
97+
? { totalMb: Math.round(sysMem.total / 1024), freeMb: Math.round(sysMem.free / 1024) }
98+
: null,
99+
processes,
100+
};
101+
});
75102
}

‎client/electron/preload.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ const ALLOWED_CHANNELS = [
9191
'keyframe_cleanup',
9292
// 性能模式 IPC channel(三档:静谧/从容/澎湃)
9393
'performance:set-mode',
94+
// P3-18 性能诊断 IPC channel
95+
'perf:get-metrics',
9496
] as const;
9597

9698
/** 允许渲染进程监听的事件 channel 白名单(主进程 → 渲染进程推送) */

‎client/src/pages/SettingsPage.tsx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { SPRING } from '@/lib/animation/springConfig';
77
import ProfileSettings from './settings/ProfileSettings';
88
import AppearanceSettings from './settings/AppearanceSettings';
99
import PerformanceSettings from './settings/PerformanceSettings';
10+
import PerformanceDiagnostics from './settings/PerformanceDiagnostics';
1011
import SoundSettings from './settings/SoundSettings';
1112
import ModeSettings from './settings/ModeSettings';
1213
import ShortcutSettings from './settings/ShortcutSettings';
@@ -118,6 +119,9 @@ export default function SettingsPage() {
118119
</SettingsSection>
119120
<SettingsSection index={2}>
120121
<PerformanceSettings />
122+
<div className="mt-3">
123+
<PerformanceDiagnostics />
124+
</div>
121125
</SettingsSection>
122126
<SettingsSection index={3}>
123127
<SoundSettings />
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/**
2+
* PerformanceDiagnostics — 内置性能诊断面板(P3-18)
3+
*
4+
* 供内测人员自助采集性能数据:实时展示 FPS(来自 3D 性能 store)、
5+
* 当前画质 tier、全进程 CPU/内存汇总与系统内存(经 perf:get-metrics IPC
6+
* 每 2s 轮询 app.getAppMetrics),并支持一键复制诊断文本上报。
7+
*
8+
* @ai-context: 设置页组件:PerformanceDiagnostics。FPS/tier 订阅
9+
* usePerformanceStore(3D 场景驱动,非 3D 页面时为其最近缓存值);
10+
* CPU/内存来自主进程 app.getAppMetrics,非 Electron 环境隐藏面板。
11+
*/
12+
import { useCallback, useEffect, useRef, useState } from 'react';
13+
import { Activity, Copy, Check } from 'lucide-react';
14+
import { Card, useToast } from '@/components/ui';
15+
import { cn } from '@/lib/utils';
16+
import { usePerformanceStore } from '@/lib/3d/core/PerformanceMonitor';
17+
import { usePerformanceModeStore } from '@/lib/performance/usePerformanceMode';
18+
19+
/** perf:get-metrics 返回结构(与主进程 performanceMode.ts 对齐) */
20+
interface PerfMetrics {
21+
totalCpu: number;
22+
totalMemoryMb: number;
23+
processCount: number;
24+
system: { totalMb: number; freeMb: number } | null;
25+
processes: Array<{ type: string; pid: number; cpu: number; memoryMb: number }>;
26+
}
27+
28+
const POLL_INTERVAL_MS = 2000;
29+
const MODE_LABEL: Record<string, string> = { low: '静谧', medium: '从容', high: '澎湃' };
30+
const TIER_LABEL: Record<string, string> = { low: '低', medium: '中', high: '高' };
31+
32+
/** 单项指标展示块 */
33+
function Metric({ label, value, unit, warn }: { label: string; value: string; unit?: string; warn?: boolean }) {
34+
return (
35+
<div className="flex flex-col gap-0.5 rounded-kb-md bg-bg-tertiary/40 px-3 py-2">
36+
<span className="text-c1 text-text-tertiary">{label}</span>
37+
<span className={cn('text-b1 font-semibold tabular-nums', warn ? 'text-semantic-error' : 'text-text-primary')}>
38+
{value}
39+
{unit && <span className="ml-0.5 text-c1 font-normal text-text-tertiary">{unit}</span>}
40+
</span>
41+
</div>
42+
);
43+
}
44+
45+
export default function PerformanceDiagnostics() {
46+
const { toast } = useToast();
47+
const [monitoring, setMonitoring] = useState(false);
48+
const [metrics, setMetrics] = useState<PerfMetrics | null>(null);
49+
const [copied, setCopied] = useState(false);
50+
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
51+
52+
const fps = usePerformanceStore((s) => s.fps);
53+
const tier = usePerformanceStore((s) => s.tier);
54+
const mode = usePerformanceModeStore((s) => s.mode);
55+
56+
const isElectron = typeof window.electronAPI?.invoke === 'function';
57+
58+
const fetchOnce = useCallback(async () => {
59+
try {
60+
const data = (await window.electronAPI?.invoke('perf:get-metrics')) as PerfMetrics | undefined;
61+
if (data) setMetrics(data);
62+
} catch {
63+
/* 采集失败静默忽略,保留上次读数 */
64+
}
65+
}, []);
66+
67+
useEffect(() => {
68+
if (!monitoring) return;
69+
fetchOnce();
70+
timerRef.current = setInterval(fetchOnce, POLL_INTERVAL_MS);
71+
return () => {
72+
if (timerRef.current) clearInterval(timerRef.current);
73+
timerRef.current = null;
74+
};
75+
}, [monitoring, fetchOnce]);
76+
77+
const handleCopy = useCallback(async () => {
78+
const lines = [
79+
`熵减性能诊断 ${new Date().toLocaleString()}`,
80+
`性能模式: ${MODE_LABEL[mode] ?? mode} | 画质 tier: ${TIER_LABEL[tier] ?? tier} | FPS: ${fps}`,
81+
];
82+
if (metrics) {
83+
lines.push(`CPU 总占用: ${metrics.totalCpu}% | 应用内存: ${metrics.totalMemoryMb} MB | 进程数: ${metrics.processCount}`);
84+
if (metrics.system) {
85+
lines.push(`系统内存: 总 ${metrics.system.totalMb} MB / 可用 ${metrics.system.freeMb} MB`);
86+
}
87+
const top = [...metrics.processes].sort((a, b) => b.memoryMb - a.memoryMb).slice(0, 5);
88+
lines.push('Top 进程(内存): ' + top.map((p) => `${p.type}#${p.pid} ${p.memoryMb}MB/${p.cpu}%`).join(', '));
89+
} else {
90+
lines.push('(未采集到进程指标,请先开始监测)');
91+
}
92+
try {
93+
await navigator.clipboard.writeText(lines.join('\n'));
94+
setCopied(true);
95+
toast({ type: 'success', message: '诊断信息已复制' });
96+
setTimeout(() => setCopied(false), 2000);
97+
} catch {
98+
toast({ type: 'error', message: '复制失败' });
99+
}
100+
}, [mode, tier, fps, metrics, toast]);
101+
102+
if (!isElectron) return null;
103+
104+
const memWarn = (metrics?.totalMemoryMb ?? 0) > 2048;
105+
const cpuWarn = (metrics?.totalCpu ?? 0) > 60;
106+
107+
return (
108+
<Card padding="md" className="flex flex-col gap-kb-md">
109+
<div className="flex items-center justify-between">
110+
<div className="flex items-center gap-2">
111+
<Activity className="w-5 h-5 text-brand-500" strokeWidth={1.5} />
112+
<div>
113+
<h2 className="text-b1 font-semibold text-text-primary">性能诊断</h2>
114+
<p className="text-c1 text-text-tertiary mt-0.5">实时资源占用,便于反馈上报</p>
115+
</div>
116+
</div>
117+
<button
118+
onClick={() => setMonitoring((v) => !v)}
119+
className={cn(
120+
'px-3 py-1.5 rounded-[var(--kb-radius-md)] text-b3 font-medium transition-all duration-200',
121+
monitoring
122+
? 'bg-semantic-error/10 text-semantic-error hover:bg-semantic-error/20'
123+
: 'bg-brand-500 text-white hover:bg-brand-600',
124+
)}
125+
>
126+
{monitoring ? '停止监测' : '开始监测'}
127+
</button>
128+
</div>
129+
130+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
131+
<Metric label="FPS" value={String(fps)} warn={fps < 30} />
132+
<Metric label="画质 tier" value={TIER_LABEL[tier] ?? tier} />
133+
<Metric label="CPU 总占用" value={metrics ? String(metrics.totalCpu) : '--'} unit="%" warn={cpuWarn} />
134+
<Metric label="应用内存" value={metrics ? String(metrics.totalMemoryMb) : '--'} unit="MB" warn={memWarn} />
135+
</div>
136+
137+
{metrics?.system && (
138+
<p className="text-c1 text-text-tertiary">
139+
系统内存:总 {metrics.system.totalMb} MB / 可用 {metrics.system.freeMb} MB · 进程数 {metrics.processCount}
140+
</p>
141+
)}
142+
143+
{!monitoring && !metrics && (
144+
<p className="text-c1 text-text-tertiary">点击「开始监测」采集实时 CPU/内存数据。</p>
145+
)}
146+
147+
<button
148+
onClick={handleCopy}
149+
className="self-start inline-flex items-center gap-1.5 px-3 py-1.5 rounded-[var(--kb-radius-md)] text-b3 font-medium text-text-secondary bg-bg-tertiary/50 hover:bg-bg-tertiary transition-all duration-200"
150+
>
151+
{copied ? <Check className="w-3.5 h-3.5 text-semantic-success" strokeWidth={2} /> : <Copy className="w-3.5 h-3.5" strokeWidth={1.5} />}
152+
复制诊断信息
153+
</button>
154+
</Card>
155+
);
156+
}

0 commit comments

Comments
 (0)