使用 AppStats 分析性能
AppStats 通过 app.stats 暴露只读引擎测量值和可写用户计数器。无需渲染叠加面板,即可用于分析脚本、自定义仪表板和自动比较。MiniStats则提供应用统计的可视化显示,并包含自己的 CPU 计时器。
本页介绍的公开 getter 需要引擎 2.23 或更高版本,可用于 Application 和 AppBase。应用自行创建统计实例,每次访问都返回同一个对象,无需自行构造或替换。
读取统计
需要测量值时,读取相应 getter:
const stats = app.stats;
const updateMs = stats.cpuUpdateTime;
const renderMs = stats.cpuRenderTime;
const textureMiB = stats.vramTextureBytes / (1024 * 1024);
耗时以毫秒为单位,内存以字节为单位。getter 返回最新可用值,而非 MiniStats 的半秒平均值。fps 例外,它表示最近约一秒报告区间中的帧数。
读取统计不会启用 GPU 分析、创建快照对象或渲染任何内容。CPU 耗时和计数器初始为零,GPU 耗时初始为 undefined。
用户计数器
app.stats.user 返回应用持有的 Map<string, number>,可将自定义数值与引擎统计一起提供:
app.stats.user.set('activeEnemies', 12);
const activeEnemies = app.stats.user.get('activeEnemies');
app.stats.user.delete('activeEnemies');
// Clear all user counters when they are no longer needed.
app.stats.user.clear();
Map 引用只读,但内部条目可写。每个应用有自己的 Map,所有构建都支持。引擎不会重置这些值,因此累加前应先初始化,逐帧总量应在 frameupdate 中重置。单位由应用定义。若要显示在叠加面板中,可在 MiniStats 配置 user.activeEnemies 等路径,参见用户计数器。
指标与构建支持
| 测量项 | Getter | 支持的构建 |
|---|---|---|
| 帧节奏 | frameTime、fps | 全部 |
| 绘制调用 | drawCallCount | 全部 |
| CPU 更新和渲染 | cpuUpdateTime、cpuRenderTime | 全部 |
| CPU 系统阶段 | cpuSystemUpdateTime、cpuSystemPostUpdateTime | 全部 |
| 动画和物理 | cpuAnimationTime、cpuPhysicsTime | 全部 |
| GPU 帧耗时 | gpuFrameTime | 设备支持且已启用时,全部构建可用 |
| GPU 资源内存 | 显存 getter | 全部 |
| 图元 | primitiveCount | 仅调试和 profiler 构建;发布和压缩构建中为 undefined |
npm 应用需要统计图元时,使用 playcanvas/debug 或 playcanvas/profiler。应用与其他引擎类应统一从所选构建导入。CPU 耗时、绘制调用、内存和受支持的 GPU 耗时无需切换离开发布构建。
drawCallCount 统计上一帧提交的绘制命令。primitiveCount 统计全部通道提交的三角形、线和点,包括实例及 CPU 编写的多重绘制命令,发生在 GPU 裁剪和剔除之前。它根据绘制参数估算,不含 GPU 生成的间接绘制,也不检查索引条带中的图元重启索引,不执行 GPU 回读。各 getter 的确切统计范围见 API 参考。
理解耗时
frameTime 是应用两次 tick 之间的间隔,包括浏览器调度和引擎外时间,不受时间缩放或 delta time 限制影响。它并非 CPU 与 GPU 耗时之和,接近 16.67 毫秒可能仅反映 60 Hz 显示节奏。
各 CPU 指标的范围不同:
cpuUpdateTime包括组件系统、应用更新监听器和输入更新,不包括图形设备更新。cpuRenderTime包括 prerender/postrender 监听器、层级同步、批处理和渲染命令提交,不包括图形设备frameStart/frameEnd和 GPU 执行。跳过渲染时保留最近值。cpuSystemUpdateTime包括脚本update回调、物理及订阅此阶段的其他系统;cpuSystemPostUpdateTime包括脚本postUpdate。两者均属于cpuUpdateTime。cpuAnimationTime测量应用更新中专门的AnimComponentSystem阶段。旧版AnimationComponentSystem属于系统更新阶段。cpuPhysicsTime测量最近一次物理步,通常位于系统更新内,包含同步和接触处理。手动请求多个步时不会累加;尚未执行物理步,或通过物理timeScale暂停时为零。
这些阶段会重叠或相互包含,不应将所有 CPU getter 相加得到总量。MiniStats 总 CPU 计时使用更广的帧事件边界,不一定等于 cpuUpdateTime + cpuRenderTime。
帧计数器在下一次应用 tick 开始时发布,CPU 耗时在对应阶段结束时更新。frameend 中读取会得到已完成的 CPU 阶段数据,但绘制计数属于上一帧,GPU 结果更晚到达。因此统计对象是最新测量值的实时集合,而非某一帧的原子快照。
启用 GPU 计时
GPU 分析默认禁用,采样前先启用:
const profiler = app.graphicsDevice.gpuProfiler;
if (profiler) {
profiler.enabled = true;
}
// Later, once frames have rendered and query results have arrived:
const gpuMs = app.stats.gpuFrameTime;
if (gpuMs !== undefined) {
// Use gpuMs as the most recently resolved GPU duration.
}
| 后端 | 所需支持 | 测量方式 |
|---|---|---|
| WebGL 2 | EXT_disjoint_timer_query_webgl2 | 通过整帧计时查询测量经过时间。 |
| WebGPU | timestamp-query | 从首个被分析通道开始到最后一个结束的时间跨度,包含通道间间隔。 |
适配器提供对应功能时,引擎会自动请求 WebGPU 特性。即使设备不支持计时,分析器对象也可能存在,因此启用并不保证有结果。
分析禁用、不受支持、有效结果尚未到达,或上下文丢失等情况使计时失效时,gpuFrameTime 返回 undefined。不要将不可用结果当作零。有效结果可能来自数帧之前,在较新查询完成前,同一个结果可能在多个 CPU 帧中重复出现。
这表示 GPU 经过时间,而非利用率。各通道区间可能重叠,相加会高估整帧时长。WebGPU 上可用 MiniStats 查看通道细分,更深入分析请使用原生 GPU 性能工具。
MiniStats 创建 GPU 计时器时也会启用分析。不再需要 GPU 计时时,设置 profiler.enabled = false。读取统计、隐藏或销毁 MiniStats 面板,不会自动禁用设备的 GPU 分析器。
GPU 资源内存
vramTotalBytes 是以下估算值之和:
| Getter | 资源 |
|---|---|
vramTextureBytes | 纹理 |
vramVertexBufferBytes | 顶点缓冲区 |
vramIndexBufferBytes | 索引缓冲区 |
vramUniformBufferBytes | 统一缓冲区 |
vramStorageBufferBytes | 存储缓冲区 |
尚未分配被跟踪的缓冲区时,统一或存储缓冲区用量可为零。不支持存储缓冲区的后端,其存储缓冲区用量为零。
这些估算属于应用使用的图形设备,设备可能被多个应用共享。它们不表示物理 GPU 总内存用量或容量,也不包含未跟踪的驱动开销和 JavaScript 内存。除以 1024 * 1024 可换算为 MiB。
收集周期报告
为获得有用基线,应累积逐帧样本,以较低频率发布报告。下例约每 500 毫秒计算帧间隔、CPU 耗时和绘制调用的平均值,并记录最大帧间隔;GPU、图元和内存明确标记为最新可用值。
const stats = app.stats;
const report = {
windowMs: 0,
samples: 0,
frameAvgMs: 0,
framePeakMs: 0,
cpuUpdateAvgMs: 0,
cpuRenderAvgMs: 0,
drawCallsAvg: 0,
gpuLatestMs: null,
primitivesLatest: null,
vramLatestMiB: 0
};
let elapsed = 0;
let samples = 0;
let framePeak = 0;
let updateTotal = 0;
let renderTotal = 0;
let drawCallsTotal = 0;
function sampleStats() {
elapsed += stats.frameTime;
samples++;
framePeak = Math.max(framePeak, stats.frameTime);
updateTotal += stats.cpuUpdateTime;
renderTotal += stats.cpuRenderTime;
drawCallsTotal += stats.drawCallCount;
if (elapsed < 500) return;
report.windowMs = elapsed;
report.samples = samples;
report.frameAvgMs = elapsed / samples;
report.framePeakMs = framePeak;
report.cpuUpdateAvgMs = updateTotal / samples;
report.cpuRenderAvgMs = renderTotal / samples;
report.drawCallsAvg = drawCallsTotal / samples;
report.gpuLatestMs = stats.gpuFrameTime ?? null;
report.primitivesLatest = stats.primitiveCount ?? null;
report.vramLatestMiB = stats.vramTotalBytes / (1024 * 1024);
// Serialize only when publishing; console output is optional instrumentation.
console.log(JSON.stringify(report));
elapsed = 0;
samples = 0;
framePeak = 0;
updateTotal = 0;
renderTotal = 0;
drawCallsTotal = 0;
}
app.on('frameend', sampleStats);
// When profiling is finished:
// app.off('frameend', sampleStats);
采样路径复用状态,不逐帧创建数组或对象,仅发布时序列化。不可用的 GPU 或图元测量以 null 表示。GPU 不做平均,因为连续读取可能对应同一个已完成 GPU 帧。
用于仪表板或智能体集成时,可将控制台输出替换为自己的报告接收方。如果保留报告,发布时应复制或序列化,因为报告对象会复用。各次测量应采用相同检测配置;日志、叠加面板和 GPU 查询都会影响结果。
比较修改结果
- 选择可重复相机路径或交互,以及目标帧预算。两次运行使用相同设备、后端、分辨率、质量设置和引擎构建。
- 资源加载和着色器编译完成后,再采集稳定运行样本。启动性能单独通过编辑器 Profiler或浏览器性能工具测量。
- 保持应用可见,在固定时间内重复相同场景。后台节流会改变帧间隔。
- 保存基线报告,每次只改一项,再重新采集。比较平均值、峰值和内存趋势,而非只看单帧或 FPS。
- 使用浏览器性能工具分析较高 CPU 阶段耗时,通过 MiniStats 通道计时或原生工具分析 GPU 开销,并检查资源生命周期以排查持续内存增长。
向智能体提供场景、环境、测量区间和基线报告,让其定位可能瓶颈、提出单项修改、以相同方式重新采集并报告差异。使用公开 getter,而非内部统计字段,并保留不可用值,不当作零。项目配置和浏览器验证见借助 AI 开发。