跳到主要内容

使用 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 2EXT_disjoint_timer_query_webgl2通过整帧计时查询测量经过时间。
WebGPUtimestamp-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 查询都会影响结果。

比较修改结果​

  1. 选择可重复相机路径或交互,以及目标帧预算。两次运行使用相同设备、后端、分辨率、质量设置和引擎构建。
  2. 资源加载和着色器编译完成后,再采集稳定运行样本。启动性能单独通过编辑器 Profiler或浏览器性能工具测量。
  3. 保持应用可见,在固定时间内重复相同场景。后台节流会改变帧间隔。
  4. 保存基线报告,每次只改一项,再重新采集。比较平均值、峰值和内存趋势,而非只看单帧或 FPS。
  5. 使用浏览器性能工具分析较高 CPU 阶段耗时,通过 MiniStats 通道计时或原生工具分析 GPU 开销,并检查资源生命周期以排查持续内存增长。
AI

向智能体提供场景、环境、测量区间和基线报告,让其定位可能瓶颈、提出单项修改、以相同方式重新采集并报告差异。使用公开 getter,而非内部统计字段,并保留不可用值,不当作零。项目配置和浏览器验证见借助 AI 开发。