Skip to content

Commit 4411656

Browse files
committed
feat(performance): 三档性能模式(静谧/从容/澎湃)与渲染开销优化
设置页新增三档性能模式(默认从容/medium):tier 天花板模型(effectiveTier=min(自动tier,档位上限),自动降档仍生效但不超用户档位);模块态 3D 按档位降帧/暂停(静谧=暂停、从容=10fps、澎湃=30fps),避免 3D 被遮罩覆盖时仍全帧渲染(持续开销最大头);静谧档经 MotionConfig 全局减弱 Framer Motion 动画;主进程 performance:set-mode IPC 持久化档位并按档位缩放屏幕采集间隔。含 performanceMode 纯函数层 14 个单测。
1 parent 633e55e commit 4411656

15 files changed

Lines changed: 615 additions & 19 deletions

‎client/electron/main.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { registerDisplayMediaHandler } from './displayMediaHandler.js';
3737
import { registerDbIpcHandlers } from './db/dbIpcHandlers.js';
3838
import { registerStorageIpcHandlers } from './storageIpcHandlers.js';
3939
import { registerKeyframeScheme, registerKeyframeIpcHandlers } from './ipc/keyframeStorage.js';
40+
import { loadPerformanceMode, registerPerformanceHandlers } from './performanceMode.js';
4041

4142
// ================================================================
4243
// 性能优化:启用 GPU 光栅化与零拷贝
@@ -151,6 +152,10 @@ if (!gotTheLock) {
151152
// 课堂关键帧图片持久化 IPC + keyframe:// 协议(详见 ipc/keyframeStorage.ts)
152153
registerKeyframeIpcHandlers();
153154

155+
// 性能模式:加载持久化档位 + 注册 IPC(详见 performanceMode.ts)
156+
loadPerformanceMode();
157+
registerPerformanceHandlers();
158+
154159
// 隐藏默认 Electron 菜单栏
155160
Menu.setApplicationMenu(null);
156161

‎client/electron/performanceMode.ts‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* performanceMode(主进程)— 性能模式状态、持久化与采集频率缩放
3+
*
4+
* 渲染进程经 performance:set-mode 设置模式并持久化到 userData,
5+
* 启动时加载,供屏幕采集间隔缩放等进程级策略使用。
6+
*
7+
* @ai-context: 主进程模块。backgroundThrottling 等需重建窗口的策略
8+
* 因与番茄钟后台计时精度冲突,暂不启用(保持 backgroundThrottling:false),
9+
* 待与番茄钟状态协同后再引入。
10+
*/
11+
import { app } from 'electron';
12+
import { readFileSync, writeFileSync } from 'fs';
13+
import { join } from 'path';
14+
import { safeHandle } from './ipcUtils.js';
15+
import { logger } from './logger.js';
16+
17+
export type PerformanceMode = 'low' | 'medium' | 'high';
18+
19+
const DEFAULT_MODE: PerformanceMode = 'medium';
20+
21+
/** 各模式采集频率倍率(作用于截屏间隔:interval / scale,scale<1 即降频) */
22+
const CAPTURE_RATE_SCALE: Record<PerformanceMode, number> = {
23+
low: 0.5,
24+
medium: 1,
25+
high: 1,
26+
};
27+
28+
let currentMode: PerformanceMode = DEFAULT_MODE;
29+
30+
function configPath(): string {
31+
return join(app.getPath('userData'), 'performance-mode.json');
32+
}
33+
34+
function isMode(v: unknown): v is PerformanceMode {
35+
return v === 'low' || v === 'medium' || v === 'high';
36+
}
37+
38+
/** 启动时加载持久化模式(文件缺失/损坏时用默认值) */
39+
export function loadPerformanceMode(): void {
40+
try {
41+
const parsed = JSON.parse(readFileSync(configPath(), 'utf-8'));
42+
if (isMode(parsed?.mode)) currentMode = parsed.mode;
43+
} catch { /* 首次运行或文件损坏:使用默认 medium */ }
44+
logger.info(`[PerformanceMode] 当前模式: ${currentMode}`);
45+
}
46+
47+
/** 设置模式并持久化(由 IPC 调用) */
48+
export function setPerformanceMode(mode: unknown): void {
49+
if (!isMode(mode)) return;
50+
currentMode = mode;
51+
try {
52+
writeFileSync(configPath(), JSON.stringify({ mode }), 'utf-8');
53+
} catch (err) {
54+
logger.error('[PerformanceMode] 持久化失败:', err);
55+
}
56+
logger.info(`[PerformanceMode] 模式已设置: ${mode}`);
57+
}
58+
59+
/** 当前模式 */
60+
export function getPerformanceMode(): PerformanceMode {
61+
return currentMode;
62+
}
63+
64+
/** 采集频率倍率(用于缩放截屏间隔) */
65+
export function getCaptureRateScale(): number {
66+
return CAPTURE_RATE_SCALE[currentMode];
67+
}
68+
69+
/** 注册性能模式 IPC handler */
70+
export function registerPerformanceHandlers(): void {
71+
safeHandle('performance:set-mode', async (_event, mode: unknown) => {
72+
setPerformanceMode(mode);
73+
return { success: true, mode: getPerformanceMode() };
74+
});
75+
}

‎client/electron/preload.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ const ALLOWED_CHANNELS = [
8989
// 课堂关键帧图片持久化 IPC channel
9090
'keyframe_save',
9191
'keyframe_cleanup',
92+
// 性能模式 IPC channel(三档:静谧/从容/澎湃)
93+
'performance:set-mode',
9294
] as const;
9395

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

‎client/electron/screenCaptureHandlers.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type { ScreenCaptureOptions, ScreenshotFrameData } from './screenCapture.
1414
import { safeHandle, getMainWindowId } from './ipcUtils.js';
1515
import { logger } from './logger.js';
1616
import { scoreAndFilterWindows } from './windowScorer.js';
17+
import { getCaptureRateScale } from './performanceMode.js';
1718

1819
// ================================================================
1920
// 模块级状态
@@ -78,6 +79,12 @@ export function registerScreenCaptureHandlers(): void {
7879
safeOptions.interval = 5000;
7980
}
8081

82+
// 按性能模式缩放采集间隔(静谧档 scale=0.5 → 间隔翻倍、频率减半,降低开销)
83+
const rateScale = getCaptureRateScale();
84+
if (typeof safeOptions.interval === 'number' && rateScale !== 1) {
85+
safeOptions.interval = Math.min(60000, Math.round(safeOptions.interval / rateScale));
86+
}
87+
8188
activeCapture = new ScreenCapture(safeOptions, (frame: ScreenshotFrameData) => {
8289
if (senderWin && !senderWin.isDestroyed()) {
8390
senderWin.webContents.send('screen_capture_frame', frame);

‎client/src/components/layout/AppLayout.tsx‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44
import { useEffect, useState } from 'react';
55
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
6-
import { AnimatePresence, motion } from 'framer-motion';
6+
import { AnimatePresence, motion, MotionConfig } from 'framer-motion';
77
import { Home } from 'lucide-react';
88
import CommandPalette from '../ui/CommandPalette';
99
import { CloseConfirmDialog } from '../ui/CloseConfirmDialog';
@@ -24,6 +24,8 @@ import { useOnboardingStore } from '@/components/onboarding/useOnboardingStore';
2424
import { useRuntimeEnv } from '@/lib/env/useRuntimeEnv';
2525
import { PWAInstallPrompt } from '@/components/PWAInstallPrompt';
2626
import { FirstDiveGate } from '@/features/onboarding/firstDive/FirstDiveGate';
27+
import { usePerformanceModeStore } from '@/lib/performance/usePerformanceMode';
28+
import { PERFORMANCE_MODE_CONFIG } from '@/lib/performance/performanceMode';
2729

2830
export default function AppLayout() {
2931
const { pathname } = useLocation();
@@ -33,6 +35,9 @@ export default function AppLayout() {
3335
const [showCloseDialog, setShowCloseDialog] = useState(false);
3436
const { sync } = useSync();
3537
const { shouldDegrade3D } = useRuntimeEnv();
38+
// 性能模式:静谧(low)档全局减弱 Framer Motion 动画(transform/layout 动画直接到位)
39+
const perfMode = usePerformanceModeStore((s) => s.mode);
40+
const reduceMotion = PERFORMANCE_MODE_CONFIG[perfMode].reduceMotion;
3641

3742
// 监听 session 过期事件
3843
useSessionExpiry();
@@ -109,6 +114,7 @@ export default function AppLayout() {
109114
}, [isInModule, enterModule, exitModule, navigate, openHelp]);
110115

111116
return (
117+
<MotionConfig reducedMotion={reduceMotion ? 'always' : 'user'}>
112118
<div className="flex flex-col h-screen overflow-hidden">
113119
{/* Layer 2: 始终最顶层 — Electron标题栏 */}
114120
<CustomTitlebar />
@@ -173,5 +179,6 @@ export default function AppLayout() {
173179
<PWAInstallPrompt />
174180
<CloseConfirmDialog open={showCloseDialog} onClose={() => setShowCloseDialog(false)} />
175181
</div>
182+
</MotionConfig>
176183
);
177184
}

‎client/src/lib/3d/core/QualityController.tsx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
*/
77
import { useThree } from '@react-three/fiber';
88
import { useEffect } from 'react';
9-
import { usePerformanceStore } from './PerformanceMonitor';
9+
import { useEffectiveTier } from '@/lib/performance/usePerformanceMode';
1010

1111
export function QualityController() {
1212
const { gl } = useThree();
13-
const tier = usePerformanceStore((s) => s.tier);
13+
// 有效 tier(自动 tier 受用户性能模式上限约束)
14+
const tier = useEffectiveTier();
1415

1516
useEffect(() => {
1617
switch (tier) {

‎client/src/lib/3d/core/SceneProvider.tsx‎

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,26 @@
44
* 但仍低于所有 UI 浮层(标题栏 z-50 / FunctionalOverlay z-10 / 引导 z-40+)。
55
* 注意:不能用负 z-index,否则会落到根 div 之后被其透明盒拦截点击。
66
*
7+
* 帧循环策略(性能优化核心):
8+
* - 概览态(!isInModule):frameloop=always 全帧渲染(可交互);
9+
* - 模块态(isInModule,3D 被遗罩覆盖):按性能模式降帧/暂停——
10+
* 静谧(low)=never 暂停、从容(medium)=demand 10fps、澎湃(high)=demand 30fps,
11+
* 避免 3D 在不可见时仍全帧渲染(持续开销最大头)。
12+
* - 模块态不渲染 PerformanceMonitor:降帧后的 FPS 是人为限制值,
13+
* 若用于测量会被误判为性能恶化而降级 tier。
14+
*
715
* @ai-context: 3D 场景核心(R3F):SceneProvider。
816
*/
9-
import { Canvas } from '@react-three/fiber';
10-
import { Suspense } from 'react';
17+
import { Canvas, useThree } from '@react-three/fiber';
18+
import { Suspense, useEffect } from 'react';
1119
import { Preload } from '@react-three/drei';
1220
import { PerformanceMonitor } from './PerformanceMonitor';
1321
import { QualityController } from './QualityController';
1422
import { MemoryManager } from './MemoryManager';
1523
import { ContextRecovery } from './ContextRecovery';
24+
import { useOrbitalStore } from '../navigation/OrbitalStore';
25+
import { usePerformanceModeStore } from '@/lib/performance/usePerformanceMode';
26+
import { PERFORMANCE_MODE_CONFIG } from '@/lib/performance/performanceMode';
1627
import * as THREE from 'three';
1728

1829
interface SceneProviderProps {
@@ -21,10 +32,33 @@ interface SceneProviderProps {
2132
interactive?: boolean;
2233
}
2334

35+
/** 模块态降帧 ticker:demand 模式下按目标帧率触发渲染(invalidate) */
36+
function ModuleFpsTicker({ fps }: { fps: number }) {
37+
const invalidate = useThree((s) => s.invalidate);
38+
useEffect(() => {
39+
if (fps <= 0) return; // 暂停档:不触发渲染
40+
const id = setInterval(() => invalidate(), 1000 / fps);
41+
return () => clearInterval(id);
42+
}, [fps, invalidate]);
43+
return null;
44+
}
45+
2446
export function SceneProvider({ children, interactive = false }: SceneProviderProps) {
47+
const isInModule = useOrbitalStore((s) => s.isInModule);
48+
const mode = usePerformanceModeStore((s) => s.mode);
49+
const moduleFps = PERFORMANCE_MODE_CONFIG[mode].moduleFps;
50+
51+
// 帧循环策略:概览态全帧;模块态按档位降帧/暂停
52+
const frameloop: 'always' | 'demand' | 'never' = !isInModule
53+
? 'always'
54+
: moduleFps === 0
55+
? 'never'
56+
: 'demand';
57+
2558
return (
2659
<div className="fixed inset-0 z-0" style={{ pointerEvents: interactive ? 'auto' : 'none' }}>
2760
<Canvas
61+
frameloop={frameloop}
2862
gl={{
2963
antialias: true,
3064
alpha: true,
@@ -49,7 +83,10 @@ export function SceneProvider({ children, interactive = false }: SceneProviderPr
4983
console.info('[3D] WebGL renderer:', rendererName);
5084
}}
5185
>
52-
<PerformanceMonitor />
86+
{/* 模块态降帧 ticker(仅 demand 模式) */}
87+
{frameloop === 'demand' && <ModuleFpsTicker fps={moduleFps} />}
88+
{/* FPS 自动降档仅在概览态测量(模块态帧率被人为限制,不可作为性能依据) */}
89+
{!isInModule && <PerformanceMonitor />}
5390
<QualityController />
5491
<MemoryManager />
5592
<ContextRecovery />

‎client/src/lib/3d/objects/ParticleSystem.tsx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import { useRef, useMemo } from 'react';
88
import { useFrame } from '@react-three/fiber';
99
import * as THREE from 'three';
10-
import { usePerformanceStore } from '../core/PerformanceMonitor';
10+
import { useEffectiveTier } from '@/lib/performance/usePerformanceMode';
1111

1212
interface ParticleSystemProps {
1313
count?: number;
@@ -25,7 +25,8 @@ export function ParticleSystem({
2525
speed = 1,
2626
}: ParticleSystemProps) {
2727
const pointsRef = useRef<THREE.Points>(null);
28-
const tier = usePerformanceStore((s) => s.tier);
28+
// 有效 tier(自动 tier 受用户性能模式上限约束)
29+
const tier = useEffectiveTier();
2930

3031
const particleCount = tier === 'low' ? 500 : tier === 'medium' ? 1200 : count;
3132

‎client/src/lib/3d/scenes/AuroraDomeWorld.tsx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import * as THREE from 'three';
1010
import { Bloom, Vignette, ChromaticAberration } from '@react-three/postprocessing';
1111
import { BlendFunction } from 'postprocessing';
1212
import { SafeEffectComposer } from '../core/SafeEffectComposer';
13-
import { usePerformanceStore } from '../core/PerformanceMonitor';
13+
import { useEffectiveTier } from '@/lib/performance/usePerformanceMode';
1414

1515
// ─── 天空穹顶着色器 ───────────────────────────────────────
1616
const domeVertexShader = /* glsl */ `
@@ -269,7 +269,8 @@ function CloudLayer() {
269269

270270
// ─── 主场景组件 ───────────────────────────────────────────
271271
export function AuroraDomeWorld() {
272-
const tier = usePerformanceStore((s) => s.tier);
272+
// 有效 tier(自动 tier 受用户性能模式上限约束)
273+
const tier = useEffectiveTier();
273274

274275
// 根据性能等级调整粒子数
275276
const particleCount = tier === 'low' ? 500 : tier === 'medium' ? 1000 : 1500;

‎client/src/lib/3d/scenes/DeepSeaWorld.tsx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
*/
77
import { Bloom, DepthOfField, Vignette } from '@react-three/postprocessing';
88
import { SafeEffectComposer } from '../core/SafeEffectComposer';
9-
import { usePerformanceStore } from '../core/PerformanceMonitor';
9+
import { useEffectiveTier } from '@/lib/performance/usePerformanceMode';
1010

1111
export function DeepSeaWorld() {
12-
const tier = usePerformanceStore((s) => s.tier);
12+
// 有效 tier(自动 tier 受用户性能模式上限约束)
13+
const tier = useEffectiveTier();
1314

1415
return (
1516
<group>

0 commit comments

Comments
 (0)