Skip to content

Commit 72e584f

Browse files
committed
refactor(3d): 性能监控迁移至 drei PerformanceMonitor 成熟方案
FPS 测量与防抖委托给 drei <PerformanceMonitor>(内置滑动均值、bounds 滞回、threshold 比例持续判定与 flip-flop 保护),替代自研单窗口测量;tier 迁移复用已单测的 tierPolicy.stepTier;窗口隐藏时卸载监控以重置采样基线(后台 rAF 节流期 FPS 不可信),并顺带重置 drei 内部 flipped/fallback 防止永久锁定。usePerformanceStore 公共 API 与 tier 三级语义保持不变,呈现质量不受影响(未触碰 multisampling 与特效开关)。
1 parent 1602c72 commit 72e584f

1 file changed

Lines changed: 46 additions & 73 deletions

File tree

Lines changed: 46 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,28 @@
11
/**
2-
* 性能监控 — FPS追踪与动态降级
2+
* 性能监控 — FPS追踪与动态降级(基于 drei 成熟方案)
33
*
4-
* 降级策略(防止“动画闪烁/长期隐藏”的三项关键设计):
5-
* 1. 滞回:低于 25 记为低、高于 50 记为高,25~50 为缓冲区不触发变更,
6-
* 避免 FPS 在单一阈值附近振荡导致 tier 来回抖动;
7-
* 2. 持续判定:同方向连续 2 个窗口(4s)才逐级调整 tier,
8-
* 防止瞬时卡顿(着色器编译、GC、场景切换)造成立即降级;
9-
* 3. 后台重置:从后台返回时重置测量基线,避免浏览器后台 rAF 节流
10-
* 被误判为性能恶化、回到前台后立即降级。
4+
* 重构说明(docs/standards/refactoring.md,外部行为不变):
5+
* - FPS 测量与防抖委托给 drei <PerformanceMonitor>(工业级实现):
6+
* 内置滑动均值(iterations×ms 采样)、bounds 滞回、threshold 比例持续判定
7+
* 与 flip-flop 保护,替代原自研单窗口测量 + 手写滞回/持续逻辑;
8+
* - tier 迁移策略抽取至纯函数模块 tierPolicy(已单测);
9+
* - 本组件仅做装配:把 drei 回调映射为 tier 逐级迁移,并在窗口隐藏时
10+
* 卸载监控以重置测量基线(后台 rAF 节流期 FPS 不可信,卸载重挂即自然
11+
* 清零,避免回到前台被误判降级)。
1112
*
1213
* @ai-context: 3D 场景核心(R3F):PerformanceMonitor。
1314
*/
14-
import { useFrame } from '@react-three/fiber';
15-
import { useEffect, useRef } from 'react';
15+
import { useEffect, useState } from 'react';
16+
import { PerformanceMonitor as DreiPerformanceMonitor } from '@react-three/drei';
1617
import { create } from 'zustand';
18+
import { stepTier, type PerformanceTier } from './tierPolicy';
1719

18-
type PerformanceTier = 'high' | 'medium' | 'low';
20+
/** FPS 下界:持续低于此值触发降级(滞回缓冲区下限) */
21+
export const FPS_LOWER_BOUND = 25;
22+
/** FPS 上界:持续高于此值触发升级(滞回缓冲区上限) */
23+
export const FPS_UPPER_BOUND = 50;
24+
/** 升降震荡(flip-flop)允许次数上限,超过则回落稳定档 */
25+
const MAX_FLIPFLOPS = 4;
1926

2027
interface PerformanceState {
2128
tier: PerformanceTier;
@@ -31,73 +38,39 @@ export const usePerformanceStore = create<PerformanceState>((set) => ({
3138
setFps: (fps) => set({ fps }),
3239
}));
3340

34-
/** FPS 阈值:低于 LOWER 计低、高于 UPPER 计高,中间为缓冲区(滞回) */
35-
const LOWER_BOUND = 25;
36-
const UPPER_BOUND = 50;
37-
/** 触发 tier 变更所需的同方向连续窗口数(持续判定,每窗口 2s) */
38-
const SUSTAIN_WINDOWS = 2;
39-
/** FPS 测量窗口长度(ms) */
40-
const WINDOW_MS = 2000;
41-
4241
export function PerformanceMonitor() {
43-
const frameCount = useRef(0);
44-
const lastTime = useRef(performance.now());
45-
const lowStreak = useRef(0);
46-
const highStreak = useRef(0);
47-
48-
// 从后台返回后重置测量基线:后台期间浏览器节流 rAF,
49-
// 若不重置,返回后的第一个窗口会测出极低 FPS 被误判为降级
42+
// 窗口隐藏时卸载监控:后台期间浏览器节流 rAF,FPS 读数不可信;
43+
// 返回前台重挂即自然重置采样基线,避免误判降级。
44+
// 附带收益:同时重置 drei 内部 flipped/fallback 计数,防止 fallback 永久锁定。
45+
const [visible, setVisible] = useState(() => document.visibilityState === 'visible');
5046
useEffect(() => {
51-
const reset = () => {
52-
frameCount.current = 0;
53-
lastTime.current = performance.now();
54-
lowStreak.current = 0;
55-
highStreak.current = 0;
56-
};
57-
const onVisibility = () => {
58-
if (document.visibilityState === 'visible') reset();
59-
};
47+
const onVisibility = () => setVisible(document.visibilityState === 'visible');
6048
document.addEventListener('visibilitychange', onVisibility);
6149
return () => document.removeEventListener('visibilitychange', onVisibility);
6250
}, []);
6351

64-
useFrame(() => {
65-
frameCount.current++;
66-
const now = performance.now();
67-
const elapsed = now - lastTime.current;
68-
if (elapsed < WINDOW_MS) return;
69-
70-
const fps = Math.round(frameCount.current * (1000 / elapsed));
71-
frameCount.current = 0;
72-
lastTime.current = now;
73-
74-
// 在帧回调内取最新 tier,避免闭包捕获旧值
75-
const { tier, setTier, setFps } = usePerformanceStore.getState();
76-
setFps(fps);
77-
78-
if (fps < LOWER_BOUND) {
79-
lowStreak.current++;
80-
highStreak.current = 0;
81-
} else if (fps > UPPER_BOUND) {
82-
highStreak.current++;
83-
lowStreak.current = 0;
84-
} else {
85-
// 缓冲区:双向计数清零,tier 保持不变
86-
lowStreak.current = 0;
87-
highStreak.current = 0;
88-
}
89-
90-
// 持续同方向才逐级调整一级,避免悬崖式变更(high→low 会直接隐藏后处理)
91-
if (lowStreak.current >= SUSTAIN_WINDOWS) {
92-
lowStreak.current = 0;
93-
if (tier === 'high') setTier('medium');
94-
else if (tier === 'medium') setTier('low');
95-
} else if (highStreak.current >= SUSTAIN_WINDOWS) {
96-
highStreak.current = 0;
97-
if (tier === 'low') setTier('medium');
98-
else if (tier === 'medium') setTier('high');
99-
}
100-
});
52+
if (!visible) return null;
10153

102-
return null;
54+
return (
55+
<DreiPerformanceMonitor
56+
bounds={() => [FPS_LOWER_BOUND, FPS_UPPER_BOUND]}
57+
flipflops={MAX_FLIPFLOPS}
58+
onIncline={(api) => {
59+
// 持续高 FPS(75%+ 样本 ≥ 上界)→ 逐级升档
60+
const { tier, setTier, setFps } = usePerformanceStore.getState();
61+
setFps(Math.round(api.fps));
62+
setTier(stepTier(tier, 'up'));
63+
}}
64+
onDecline={(api) => {
65+
// 持续低 FPS(75%+ 样本 < 下界)→ 逐级降档
66+
const { tier, setTier, setFps } = usePerformanceStore.getState();
67+
setFps(Math.round(api.fps));
68+
setTier(stepTier(tier, 'down'));
69+
}}
70+
onFallback={() => {
71+
// 震荡超限 → 稳定在中档:保留后处理特效、适度降低 DPR
72+
usePerformanceStore.getState().setTier('medium');
73+
}}
74+
/>
75+
);
10376
}

0 commit comments

Comments
 (0)