监控模块的职责划分:采集、计算、展示、告警
模块化架构下,一个成熟的渲染帧率监控系统,我通常会把它拆成四个独立层。这样做的好处很直接——每层可以独立迭代、独立测试,出了问题也能快速定位。
我记得刚入行时,见过一个项目把采集和计算揉在一个类里,数据来源一换,整个模块都得重写。那叫一个痛苦。所以这节课,咱们就把四层的职责和接口说清楚。
1. 采集层:只负责「听到心跳」
采集层的职责非常单一:从渲染管线中拿到每一帧的时间点。说白了,它就是一个时间戳打点器。你想想看,不管是 Unity 的 Application.onBeforeRender,还是 Chrome 的 requestAnimationFrame 回调,本质都在做同一件事——记录当前时刻。
我个人习惯让采集层只输出一个结构体:
// 采集层输出接口定义
interface FrameSample {
frameId: number; // 帧序号
timestampMs: number; // 毫秒级时间戳
source: string; // 数据来源标识,如 "vblank" / "present"
}
我在项目中遇到过一个问题:采集层如果掺入「计算帧间隔」的逻辑,当多线程同时调用时,取值会乱。所以接口一定要轻,轻到只做一件事。
List<FrameSample> 导致 GC 抖动,帧率反而更卡了。
2. 计算层:把原始数据变成指标
这层是真正的「大脑」。它接收采集层的 FrameSample 流,计算出一组指标。常见的输出接口是这样的:
// 计算层输出接口
interface FrameMetrics {
fps: number; // 当前帧率(平滑值)
minFps: number; // 窗口期内最低帧率
jankCount: number; // 窗口期内卡顿次数(掉帧 > 16ms)
avgFrameTimeMs: number; // 平均帧耗时
p99FrameTimeMs: number; // 99 分位帧耗时
windowSize: number; // 滑动窗口大小
}
为什么需要滑动窗口?因为裸的「瞬时 FPS = 1000/帧间隔」会剧烈抖动,没法看。我一般用 1 秒窗口(约 60 个样本),算移动平均。
计算层要保证自身是纯函数——输入一批样本,输出指标。不依赖外部状态,这样就能方便地做单元测试。
3. 展示层:只做渲染,不做运算
展示层拿到计算层的 FrameMetrics 后,唯一关心的是怎么画出来。你可以透过 HUD 叠加、WebSocket 推送到仪表盘,或者写日志文件。展示层不应该再做任何二次计算,比如「再求个平均值」——那是计算层的事。
展示层的接口定义很简单:
// 展示层消费接口
interface DisplayConsumer {
onMetricsUpdated(metrics: FrameMetrics): void;
onError?(error: string): void; // 可选,展示错误状态
}
我记得有一次产品经理要求在 HUD 上同时显示「实时 FPS」和「历史 FPS 折线图」。团队直接让展示层去缓存历史指标,结果内存失控。正确的做法是:让计算层输出一个包含历史窗口的 FrameMetrics[],展示层只管遍历画图。
4. 告警层:独自监听,独立触发
告警层是「守门员」。它订阅计算层的指标流,一旦指标超过阈值,就触发通知。它和展示层是兄弟关系,不是父子关系——也就是说,告警层不依赖展示层是否存在。
告警层的输入接口:
// 告警层输入接口
interface AlarmRule {
metric: keyof FrameMetrics; // 监控哪个指标,例如 "fps"
operator: 'lt' | 'gt'; // 小于 / 大于
threshold: number; // 阈值
durationMs: number; // 持续多久才告警(防抖)
action: 'dingtalk' | 'feishu' | 'console';
}
我个人建议把告警规则做成配置化,通过 JSON 文件或远程配置下发给告警层。为什么?因为不同环境阈值不同——开发版 30fps 就算流畅,线上版必须 60fps。
| 层名称 | 核心职责 | 典型接口输出 | 注意事项 |
|---|---|---|---|
| 采集层 | 记录帧时间戳 | FrameSample | 使用 Ring Buffer,避免 GC |
| 计算层 | 生成帧率指标 | FrameMetrics | 纯函数,可测试 |
| 展示层 | 可视化呈现 | HUD / WebSocket | 不做计算,只管渲染 |
| 告警层 | 阈值判断 + 通知 | 告警规则 + 回调 | 配置化,防抖降噪 |
SharedArrayBuffer 实现零拷贝传递样本,效果极佳。
嗯,到这里四层的职责就梳理清楚了。记住一个原则:每层只做自己名字对应的事。采集就是采集,别去算帧率;展示就是展示,别去改阈值。接口定义清晰后,团队里不同成员可以并行开发不同层,互不干扰。
我自己最喜欢这种分层的点在于——当你想换一个采集方案(比如从 requestAnimationFrame 换成 PerformanceObserver),你只需要改采集层,其他三层完全不需要动。这种解耦带来的维护成本降低,比任何性能优化都值。