抖音开放平台Logo
控制台

质量监控
收藏
我的收藏

查看小游戏性能相关指标,可区分系统和宿主端进行查看,部分指标除产品数据外有对照的“及格线”和“优秀线”,两条对照线根据全平台所有小游戏平均水平计算获得,右上角可下载当前页面所有数据。​
统计时间:可自选或选择最近 7 天、最近 30 天。​
区分不同的 app(头条、抖音、极速版)以及不同客户端(Android / iOS)。​

平均下载耗时​

小游戏整包或者主包(在使用分包的情况下,统计主包下载耗时)从下载到到下载完成 100%的时间。开发者可以通过优化包大小,或者使用小游戏分包来减少这个首包下载耗时。​

平均加载耗时​

小游戏包下载 100% 后出现三个点的 loading,直到出现首帧的时间。​
这里具体是指,游戏的代码包在下载完成后,加载完 game.js 并执行代码开始,到收到开发者首帧回调结束,这中间的时间。以下面代码为例:​
js
复制
let systemInfo = tt.getSystemInfoSync();
let canvas = tt.createCanvas(),
ctx = canvas.getContext("2d");
function draw() {
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, systemInfo.windowWidth, systemInfo.windowHeight);
ctx.fillStyle = "#000000";
ctx.font = `${parseInt(systemInfo.windowWidth / 20)}px Arial`;
ctx.fillText(
"欢迎使用字节跳动开发者工具,",
10,
(systemInfo.windowHeight * 1) / 5
);
}
setTimeout(() => {
draw();
}, 3000);
我们延迟 3 秒执行 draw 函数,而 draw 函数中就是渲染首帧的代码,在这种情况下,平均加载耗时就是 3s(实际情况会存在一定的统计误差,可能会为 3s 左右)。​

首帧

即开发者执行的第一个渲染命令,可以理解为小游戏启动后的第一个 drawcall。在 canvas 2D 环境下,就是执行的第一个绘制指令,比如上面的 fillRect,或者包括 fillText, stroke 等。在 webgl 环境下,就是第一个 gl.drawElements 或者 gl.drawArrays 指令。当小游戏引擎收到上面的调用后,会判定为小游戏发生了首帧渲染。以下面这段 webgl 渲染为例,当我们注释掉 gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);这句代码后,小游戏的首帧将永远不会被触发,那么首帧的时间无限延长,进而平均加载耗时将会无限增加。​
js
复制
function draw() {
var imgdata = tt.createImage();
imgdata.src = "./xxx.png";
imgdata.onload = () => {
drawImage(imgdata);
};
}
draw();
function drawImage(imagedata) {
var mycanves = tt.createCanvas(true);
glhandle(mycanves, {
vertex_shaders: [
`precision mediump float;
attribute vec2 aPosition;
varying vec2 vPos;
void main() {
gl_Position = vec4(aPosition.xy, 0.0, 1.0);
vPos = vec2(aPosition.x / 2. + 0.5, -aPosition.y / 2. + 0.5);
}`,
],
fragment_shaders: [
`precision mediump float;
varying vec2 vPos;
uniform sampler2D uBg;
void main () {
vec3 bgColor = texture2D(uBg,vPos).rgb;
gl_FragColor = texture2D(uBg,vPos);
// gl_FragColor = vec4(1.0,0.4,0.5,1.0);
}`,
],
init(gl, program) {
const aPosition = gl.getAttribLocation(program, "aPosition");
const vertexBuf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuf);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([-1.0, +1.0, +1.0, +1.0, -1.0, -1.0, +1.0, -1.0]),
gl.STATIC_DRAW
);
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 8, 0);
gl.useProgram(program);
gl.enableVertexAttribArray(aPosition);
const uBg = gl.getUniformLocation(program, "uBg");
gl.uniform1i(uBg, 0);
var bgtexture = gl.createTexture();
return function (time) {
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, bgtexture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGB,
gl.RGB,
gl.UNSIGNED_BYTE,
imagedata
);
// gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
};
},
});
}
function glhandle(canvas, { vertex_shaders, fragment_shaders, init }) {
console.log("opengl version", canvas);
const gl = canvas.getContext("webgl");
const shaderProgram = buildShaderProgram(
gl,
vertex_shaders[0],
fragment_shaders[0]
);
if (!shaderProgram) return;
const paint = init(gl, shaderProgram);
let last_time = 0;
requestAnimationFrame(sched);
function sched(time) {
paint(time, time - last_time);
last_time = time;
requestAnimationFrame(sched);
}
function buildShaderProgram(gl, vertex_shader, fragment_shader) {
let program = gl.createProgram();
const vs = compileShader(vertex_shader, gl.VERTEX_SHADER);
const fs = compileShader(fragment_shader, gl.FRAGMENT_SHADER);
function compileShader(source, type) {
let shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.log(`Error compiling ${type}:`);
console.log(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return;
}
gl.attachShader(program, shader);
}
gl.linkProgram(program);
gl.deleteShader(vs);
gl.deleteShader(fs);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.log("Error linking shader program:");
console.log(gl.getProgramInfoLog(program));
gl.deleteProgram(program);
return null;
}
return program;
}
}

单次平均停留时长​

用户每次在小游戏内停留的平均时长。​

重启率​

用户加载过程中点击返回或右上角重新打开。​

平均帧率​

游戏过程中的平均 FPS。​

卡死次数占比​

用户主动点击重启的总次数。​

如何判断卡死​

对于小游戏玩家而言,假如小游戏不响应任何操作,或者画面卡住,就将被理解为卡死。​
小游戏的 JS 逻辑执行和渲染都是在同一个线程中,当应用检测到开发者的逻辑代码超过一定时间,未触发帧回调事件或者未执行渲染指令,会判断为卡死,并且弹出卡死弹框引导用户手动重启。​

卡死原因​

简单而言,小游戏渲染画面,其实是 CP 方写代码在每一帧回调中写绘制代码,然后进行渲染。下面列举部分主要的卡死原因。​
注意
正常的页面停留/切后台,都不会触发任何卡死的监听。​
    1.代码脚本持续抛出大量的未被处理的 JS 异常(超过百个),并且这段时间的 drawcall 数量为 0。这个检测时间为 5s。​
    2.代码陷入死循环,导致无法响应任何事件,也无法进行绘制,这里可以理解为执行某段 JS 逻辑的时间超过或者达到限制时间,进而导致两次帧回调的时间超过 8s。就这一点而言,开发者也可以理解为,小游戏某一帧的代码执行耗费时间超过 8s。​
    3.绘制代码陷入异常,gl 操作异常,这种跟第一种情况不一致,它并没有抛出错误,只是可能操作的底层 gl 并不生效有错误产生,导致后续的绘制产生异常,一直停留在前一帧的场景。比如使用了错误的 gl 参数,导致 gl 执行报错。​
    4.代码脚本抛出异常,但是后续不抛出异常,不过后续就不进行绘制了,看起来也像卡死一样。这种属于逻辑层面导致的,一个类似的例子的是,开发者在某种特殊情况下调用了游戏全局的 pause 能力,从而使得开发者逻辑暂停,不再执行任何渲染绘制,也不影响用户操作,这种也会触发卡死检测。​

卡死表现​

当小游戏出现卡死时,将会触发下面弹框:​
用户点击重启后将会重启小游戏。​

JS 加载错误率​

当日小游戏游戏在加载过程中出现错误的概率。​
这里需要注意的是,这项指标仅针对小游戏包下载完成后,从开始执行 game.js 开始,到小游戏首帧出现之前发送的错误。​

如何识别​

小游戏在任何阶段发生的错误,都会经过小游戏平台上报的开发者后台的 【数据分析】-> 【性能分享】-> 【错误监控】 模块中,开发者可以在这里查看小游戏生命周期中发生的所有 JS 报错。 所有在加载过程中的报错,小游戏平台会自动追加 "loadScript error:" 前缀。表示该错误发生在小游戏加载过程中,脚本的初始化执行阶段。 例如:​
该报错即表示,小游戏在加载过程中发生了报错,导致加载失败。​

影响 & 解决​

小游戏在加载过程中发生的报错会导致小游戏卡在 loading 界面,用户无法正常进入小游戏。根据目前线上情况统计,发生该报错的原因大部分是由于在低版本系统或旧型号手机上,设备对于 JS 语法的支持程度较低,对于高级的 ES6,ES2020 支持程度不好,导致在执行阶段无法解析诸如 await,async 之类的高阶语法,从而发生报错。建议开发者为了保证全设备的兼容性,可以在上传小游戏代码之前,勾选 IDE 中的 ES6 转 ES5,保障代码最大的兼容性。​