Orillusion Stats 性能监控组件详解:一行代码接入 FPS 与内存实时面板

原创2026-10-14 12:23:281,939 阅读
文章标签:图形学游戏开发

Orillusion Stats 性能监控组件详解:一行代码接入 FPS 与内存实时面板

Orillusion 是基于 WebGPU 标准构建的纯 Web 3D 渲染引擎,而 @orillusion/stats 正是官方为它提供的性能监控插件。本文将围绕 packages/stats/README.md 展开,完整讲解该组件的安装、接入、生命周期与底层实现原理,并结合 packages/stats/index.ts 源码与仓库内的真实示例,带你彻底掌握在 Orillusion 场景中实时监控 FPS(帧率)与 JS 堆内存占用的方法,以及在浏览器全局构建(UMD/CDN)环境下的使用方式。

一、Stats 组件是什么

Stats 是 Orillusion 官方提供的一个性能监控组件(Component),直接继承自引擎的 ComponentBase 基类。它被设计为一行即可接入的场景组件:

scene.addComponent(Stats);

接入后,页面左上角会出现一个固定定位的半透明监控面板,实时显示两类关键指标:

  • FPS:每秒渲染帧数,用于评估渲染性能与帧耗时稳定性;
  • MB:浏览器 JS 堆内存占用(totalJSHeapSize,单位为 MB),用于排查内存泄漏与持续增长问题。

从 packages/stats/index.ts 的源码可以看出,Stats 类的注解被标记为 @group Plugin,即它属于引擎的插件体系,与核心渲染逻辑完全解耦,可以独立安装、独立使用。

从仓库结构看,Stats 插件包(packages/stats/)只包含三个核心文件:入口实现 index.ts、构建配置 vite.config.js 与包描述 package.json,整个插件体积小、依赖少,非常适合作为理解 Orillusion 组件生命周期与插件开发的入门范本。

二、安装与版本要求

按照 packages/stats/README.md 的说明,使用 npm 安装核心引擎与 Stats 插件:

npm install @orillusion/core --save
npm install @orillusion/stats --save

其中:

  • @orillusion/core 是引擎本体;
  • @orillusion/stats 是本篇要讲的性能监控插件。

从 packages/stats/package.json 可以看到版本约束关系:

"peerDependencies": {
    "@orillusion/core": ">=0.8.0"
}

也就是说,Stats 插件要求核心引擎版本不低于 0.8.0。插件包当前版本为 0.2.6,其 main 指向 UMD 构建产物 ./dist/stats.umd.js,module 指向 ES 模块产物 ./dist/stats.es.js,types 指向 ./dist/index.d.ts,因此无论使用 ES Module、CommonJS 还是 TypeScript,都能获得对应的类型提示与构建产物。

此外,在仓库开发环境下(如直接在本仓库中跑示例),tsconfig.json 通过 paths 映射将 @orillusion/core 指向 ../../src(即引擎源码目录),将 @orillusion/* 指向 ../*,从而在 monorepo 内实现源码级联调,无需预先构建。

三、快速上手:TypeScript / ES Module 方式接入

按 packages/stats/README.md 给出的最小示例:

import { Scene3D } from "@orillusion/core"
import { Stats } from "@orillusion/stats"

let scene = new Scene3D();
scene.addComponent(Stats);

将 Stats 添加到场景节点上,组件便会自动初始化监控面板并挂载到 document.body。把它放入一个完整可运行的 Orillusion 工程中(参考仓库示例 samples/base/Sample_Transform.ts),典型代码如下:

import { Engine3D, Scene3D, AtmosphericComponent, HoverCameraController,
         Object3D, MeshRenderer, BoxGeometry, LitMaterial, DirectLight,
         View3D, Camera3D, GridObject } from "@orillusion/core";
import { Stats } from "@orillusion/stats";

// 初始化引擎
await Engine3D.init();

// 创建场景作为根节点
let scene3D: Scene3D = new Scene3D();

// 添加性能监控组件
scene3D.addComponent(Stats);

// 添加大气天空
let sky = scene3D.addComponent(AtmosphericComponent);
sky.sunY = 0.6;

// 创建相机
let cameraObj: Object3D = new Object3D();
let camera = cameraObj.addComponent(Camera3D);
camera.perspective(45, Engine3D.aspect, 1, 5000.0);
let controller = cameraObj.addComponent(HoverCameraController);
controller.setCamera(0, 0, 30);
scene3D.addChild(cameraObj);

// 创建平行光
let light: Object3D = new Object3D();
light.rotationX = 45;
light.rotationY = 30;
let dirLight: DirectLight = light.addComponent(DirectLight);
dirLight.intensity = 3;
scene3D.addChild(light);

// 创建并配置一个盒子
const box: Object3D = new Object3D();
let mr: MeshRenderer = box.addComponent(MeshRenderer);
mr.geometry = new BoxGeometry(1, 1, 1);
mr.material = new LitMaterial();
box.y = 0.5;
scene3D.addChild(box);

// 创建视图并开始渲染
let view = new View3D();
view.scene = scene3D;
view.camera = camera;
Engine3D.startRenderView(view);

运行后,浏览器页面左上角即可看到 Stats 监控面板。这里的关键机制是:scene3D 是 Scene3D 实例,而 addComponent 由 Object3D.ts 提供——引擎会实例化传入的组件类、把实例绑定到当前对象,并调用组件内部的 __init(),随后将组件挂入 ComponentCollect 的待启动队列,等待场景开始后按生命周期自动运行。因此 scene.addComponent(Stats) 一句,就同时完成了组件的创建、初始化与注册。

四、浏览器全局构建:CDN / UMD 方式接入

不经过打包工具时,可以直接使用构建好的全局脚本(Global build)从 CDN 引入。根据 packages/stats/README.md 的说明,引入方式如下:

<script src="https://unpkg.com/@orillusion/core/dist/orillusion.umd.js"></script>
<script src="https://unpkg.com/@orillusion/stats/dist/stats.umd.js"></script>
const { Scene3D, Stats } = Orillusion

let scene = new Scene3D();
scene.addComponent(Stats);

两点使用前提值得注意:

  1. 脚本顺序不可颠倒:必须先引入 core 的 UMD 构建,再引入 stats 的 UMD 构建。原因是 stats 的构建配置(见 vite.config.js)将 @orillusion/core 声明为 external,并把全局变量名映射为 Orillusion(globals: { '@orillusion/core': 'Orillusion' })。也就是说,stats 的 UMD 产物在运行时依赖全局对象 Orillusion 上暴露的引擎 API,必须先有 core。
  2. 解构来源是全局 Orillusion:由于上面提到的全局变量映射,两个包的 UMD 构建共享同一个全局命名空间,因此 const { Scene3D, Stats } = Orillusion 才能同时取到引擎类与 Stats 组件。

同时,packages/stats/package.json 确认了产物文件命名:stats.umd.js(浏览器全局版)与 stats.es.js(ES 模块版),与上述 CDN 引用路径一致。

五、组件生命周期:面板如何创建、隐藏与销毁

Stats 作为一个标准 Orillusion 组件,其生命周期行为完全遵循 ComponentBase 的生命周期约定。对照源码 packages/stats/index.ts,我们可以梳理出它的四个关键阶段:

1. init() —— 创建 DOM 与面板

init() {
    const container = this.container = document.createElement('div')
    container.className = 'stats'
    container.setAttribute('style', 'display:flex;flex-direction:column;gap:1px;position:fixed;top:0;left:0;cursor:pointer;opacity:0.9;z-index:10000')
    this.fpsPanel = new Panel(container, 'FPS', '#0ff', '#002')
    if('memory' in performance)
        this.memPanel = new Panel(container, 'MB', '#f08', '#201')
    this.beginTime = (performance || Date).now()
    document.body.appendChild(this.container)
}

要点:

  • 容器 container 是公开属性(container: HTMLElement),class 默认为 stats,可以借助 CSS 自定义样式;
  • 默认样式为固定定位在左上角(position:fixed;top:0;left:0),纵向 flex 排列,半透明(opacity:0.9)、高 z-index(10000)保证悬浮于页面上层;
  • 始终创建 FPS 面板(前景色 #0ff 青色、背景色 #002 深蓝);
  • 仅当浏览器支持 performance.memory(即非标准 Chromium 扩展 API)时才创建 MB 内存面板(前景色 #f08、背景色 #201);
  • 源码中还保留了一份被注释掉的 drawcallPanel(DC 面板)定义,说明该组件在设计上还预留过 draw call 统计能力,只是当前版本未启用。

2. onEnable() / onDisable() —— 显示与隐藏

onDisable() {
    this.container.style.display = 'none'
}
onEnable() {
    this.container.style.display = 'flex'
}

当组件的 enable 属性被切换时,ComponentBase 内部的 __start() / __stop() 会分别触发 onEnable 与 onDisable。注意这里的开关只控制显示隐藏,不会销毁面板数据,适合临时隐藏监控而不丢失统计曲线。

3. onUpdate() —— 每秒一次的采样与刷新

onUpdate() {
    this.frames++
    const time = this.beginTime = performance.now()
    if (time >= this.prevTime + 1000) {
        this.fpsPanel.update((this.frames * 1000) / (time - this.prevTime), 100)
        this.memPanel?.update((performance as any).memory.totalJSHeapSize / 1048576, 256)
        this.prevTime = time
        this.frames = 0
    }
}

采样逻辑非常清晰:

  • onUpdate 在每一帧被引擎的组件更新管线调用(通过 ComponentBase 的 _onUpdate 注册到 ComponentCollect 的更新列表,见 ComponentCollect.ts);
  • 当累计时间满 1 秒(time >= this.prevTime + 1000)时,进行一次统计刷新:FPS 按 帧数 × 1000 ÷ 实际耗时 计算,量程上限为 100;内存值读取 performance.memory.totalJSHeapSize(字节)并除以 1048576(即 1024²)换算为 MB,量程上限为 256。

也就是说,面板上的数字每秒更新一次,而不是逐帧刷新,这样既避免了频繁重绘,也保证了数值的稳定性。

4. stop() —— 销毁与清理

stop() {
    this.fpsPanel.destroy()
    this.memPanel?.destroy()
    document.body.removeChild(this.container)
}

当组件被移除(removeComponent)或场景销毁时,stop() 会终止面板内部的 Worker、移除容器 DOM,完成资源回收。

六、面板渲染原理:OffscreenCanvas + Web Worker

这是整个插件最值得深入的部分。packages/stats/index.ts 中的 Panel 内部类没有直接在主线程绘制曲线,而是采用了 OffscreenCanvas 转交 + Web Worker 离屏渲染 的架构:

class Panel {
    canvas: HTMLCanvasElement
    private worker: Worker
    private width = 80
    private height = 48
    constructor(parent: HTMLElement, name: string, fg: string, bg: string) {
        const canvas = this.canvas = document.createElement('canvas')
        canvas.width = this.width
        canvas.height = this.height
        parent.appendChild(canvas)
        const offscreen = (canvas as any).transferControlToOffscreen()
        const blob = new Blob([`(${worker})()`], { type: 'application/javascript' })
        this.worker = new Worker(URL.createObjectURL(blob))
        this.worker.postMessage({ type: 'init', offscreen, name, fg, bg }, [offscreen])
    }
    update(value: number, maxValue: number) {
        this.worker.postMessage({ type: 'update', value, maxValue })
    }
    destroy() {
        this.worker.terminate()
    }
}

它的设计动机与收益可以总结为:

  1. 避免主线程开销:每个面板是一张 80 × 48 的小画布。通过 transferControlToOffscreen() 把画布控制权转移给 OffscreenCanvas,再通过 Blob 内嵌的 worker() 函数体创建一个独立的 Web Worker;
  2. 消息驱动:主线程只负责两件事——初始化时发送 { type: 'init', offscreen, name, fg, bg }(offscreen 作为可转移对象被移交),以及每秒通过 { type: 'update', value, maxValue } 推送采样数据;
  3. 绘制完全在 Worker 内完成,主线程的渲染管线(尤其 WebGPU 提交、绘制调用)不会因为监控面板的 2D 绘图产生额外抖动,这也保证了 Stats 面板测出的 FPS 更接近真实渲染性能。

再看 Worker 内部的绘制函数(packages/stats/index.ts),它维护了历史数据的 min / max,在 update 消息到来时执行一次滚动曲线图绘制:

  • 顶部文本区显示 当前值 + 面板名 + (历史最小值-历史最大值),例如 60 FPS (45-120);
  • 曲线区是一个 74 × 30 的滚动条带:先把已有图像整体左移 1 像素(context.drawImage(canvas, ...) 自拷贝实现横向滚动),再在最右列按 value / maxValue 的比例绘制柱条,从而形成从左向右推进、随帧率起伏的实时波形。

这种“主线程采样 → 消息传递 → Worker 绘制”的模型,使 FPS/内存监控本身的成本降到最低,是 Web 端性能监控组件的经典实现思路。

七、自定义面板样式

由于面板容器 container 是公开字段,且 class 固定为 stats,你可以直接在页面 CSS 中覆盖样式。默认的容器样式(来自 packages/stats/index.ts)为:

/* 默认内联样式,等价描述 */
.stats {
    display: flex;
    flex-direction: column;
    gap: 1px;
    position: fixed;
    top: 0;
    left: 0;
    cursor: pointer;
    opacity: 0.9;
    z-index: 10000;
}

需要调整位置(如移到右下角)、改变透明度或排列方向时,在组件 init() 完成后通过 stats.component.container.style.xxx 覆盖即可;也可以全局写 CSS 选择器 .stats { ... } 统一管理。若要彻底隐藏某个指标(如不需要内存面板),可以像源码那样在 init() 中根据 'memory' in performance 的探测结果来决定是否创建,或者自行替换为精简版组件。

八、在官方示例与基准测试中的典型用法

Stats 组件在仓库示例中被大量使用,尤其在渲染性能基准测试中几乎是标配。例如:

这一模式验证了 Stats 组件跨模块、跨场景的通用性——无论跑物理仿真、GUI 渲染、图形特效还是大规模 draw call 基准,都能用同一行代码挂上性能监控。

九、组件构建产物一览

对于希望深入理解该插件打包方式的读者,vite.config.js 展示了它是如何被构建成库产物的:

build: {
    target: 'esnext',
    lib: {
        entry: path.resolve('index.ts'),
        name: 'Stats',
        fileName: (format) => `stats.${format}.js`
    },
    rollupOptions: {
        external: ['@orillusion/core'],
        output: {
            globals: { '@orillusion/core': 'Orillusion' }
        }
    }
}
  • 构建目标为 esnext,产物分别为 stats.es.js(ES 模块)与 stats.umd.js(浏览器全局);
  • @orillusion/core 被标记为 external,不打进产物,由宿主应用提供,且 UMD 场景下通过 globals 映射到全局对象 Orillusion——这正是第四节 CDN 用法可行的根本原因。

总结

@orillusion/stats 以极低的接入成本(scene.addComponent(Stats) 一行代码)提供了 FPS 与 JS 堆内存两项核心指标的实时监控,并在实现上通过 OffscreenCanvas + Web Worker 把统计面板的绘制开销与 WebGPU 主渲染管线隔离。无论是日常开发调试、压测 draw call 场景,还是排查内存泄漏,它都是 Orillusion 项目里开箱即用的性能观测工具。其插件化、组件化的设计,也为开发者自行扩展更多指标(例如源码中预留的 draw call 面板)提供了清晰的参考范式。

登录后查看全文
orillusion