质量监控收藏我的收藏
收藏
我的收藏查看小游戏性能相关指标,可区分系统和宿主端进行查看,部分指标除产品数据外有对照的“及格线”和“优秀线”,两条对照线根据全平台所有小游戏平均水平计算获得,右上角可下载当前页面所有数据。
统计时间:可自选或选择最近 7 天、最近 30 天。
区分不同的 app(头条、抖音、极速版)以及不同客户端(Android / iOS)。
平均下载耗时
小游戏整包或者主包(在使用分包的情况下,统计主包下载耗时)从下载到到下载完成 100%的时间。开发者可以通过优化包大小,或者使用小游戏分包来减少这个首包下载耗时。
平均加载耗时
小游戏包下载 100% 后出现三个点的 loading,直到出现首帧的时间。
这里具体是指,游戏的代码包在下载完成后,加载完
game.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);
这句代码后,小游戏的首帧将永远不会被触发,那么首帧的时间无限延长,进而平均加载耗时将会无限增加。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,保障代码最大的兼容性。