任务调度器在 WebGL 引擎中的应用|WebGL地图引擎系列第三期
任务调度器在WebGL引擎中的应用|WebGL地图引擎系列第三期 作者:J
这篇文章我们来讨论有关任务调度器在 WebGL 引擎中的应用。在介绍任务调度器之前首先给大家介绍一个有关性能优化的模型:RAIL。
【RAIL 模型】
RAIL 模型是 Google 提出来的一个和性能优化相关的模型。RAIL 分别是 Response、Animation、Idle 和 Load 四个单词的首字母,代表了有关性衡量能的四个维度,它们的含义如下:
Response | 要在 100ms 内对用户的操作进行响应。比如点击按钮到界面有相应的反馈间隔不能超过 100ms,否则用户会有延迟感。 |
Animation | 动画要以 60FPS 为目标,那么动画的每一帧处理时间就限制在 16.6ms,Google 给出的建议是每一帧的处理时间不超过 10ms,这样留下 6ms 给浏览器做一些其他事情,比如GC。 |
Idle | 要充分利用程序空闲的时间,并以 50ms 为间隔来处理事情。这样一旦有用户操作也能立即响应。 |
Load | 在 1s 内要完成页面的首次加载工作,至少页面不再是空白。 |
细看每一个方向的描述会发现并没有什么新概念出现,在做性能优化的时候也基本采用同样的衡量标准。但是 Google 把所有的维度整合成一个模型,这样使得性能评估的模型更加的完整,具体内容可以到 Google 网站上了解。
在 WebGL 地图实现过程中,我们始终通过 RAIL 模型来评估性能。
【引擎绘制问题分析】
前面的文章提到,我们将地图状态变化的处理统一同步到 rAF 每一帧中进行,此外我们也将一些异步操作同步到 rAF 当中,比如纹理的创建。通常纹理是在图片 onload 事件处理函数中进行创建,而 onload 函数执行是在 rAF 循环之外的,此时访问 WebGL 的接口会增加一些额外的开销。
在地图状态变化的过程中每一帧的脚本执行时间有时会比较长,这样导致一帧的处理时间超过了 10ms,有时甚至更多。仔细分析每一帧的处理内容,可大致分为以下两个阶段:
-
准备数据阶段(update):对数据缓冲区、纹理进行更新,例如 bufferData、bufferSubData、texImage2D、texSubImage2D 等等。有时还会有清除旧缓冲区数据,旧纹理的工作。
绘制阶段(draw):buffer 的绑定,设置 shader 的 attributes 和 uniforms 以及绘制。
在 update 阶段部分的操作可能会比较耗时,例如纹理操作,大量的数据更新以及清除旧数据。一旦这些耗时的操作执行,地图操作便卡顿。
update 阶段的这些操作是可以延迟处理的,也就是在渲染每一帧的时候,不必要求所有需要 update 的数据都必须完成 update 工作。这样某些元素由于没有 update 而没有绘制出来,但最终体验没有很大的问题,最重要的是保证了地图操作的流畅性。
对于一些清除旧数据的工作,完全可以等到程序 idle 的时候处理。
【任务调度器的应用】
任务调度器的作用就在于将上面提到的这些可延迟处理的工作管理器来,在恰当的时机进行任务的执行,同时不影响地图整体渲染的帧率。
调度器机制
调度器内部将任务划分为两类,普通任务和繁重任务,这需要使用者了解自己的任务属于哪个类别。任务的划分原则如下:
-
普通任务(Normal Job):通常在几毫秒内完成且优先级比较高,比如小数据量的 bufferData、小纹理的 texImage2D 等。这些任务会在每一帧渲染的时候插空进行执行。调度器会计算出每帧留给这些任务的总执行时间,每执行完一个任务会检测是否超出,如果超出那么剩下的任务会放到下一帧执行。注意这里每一帧至少要先执行一个任务再看时间是否超出,因为某些性能较差的机器上会得出总执行时间为 0ms 的情况,如果先判断那么这些任务永远也不能执行。
繁重任务(Heavy Job):耗时较长或者没有很高的及时性,比如缓存清理。这些任务会在地图 idle 时工作,且每工作 50ms 检查是否仍旧处于 idle 状态,如果不是则停止执行。
在应用了调度器之后,执行过程如下图所示:
【小结】
使用 RAIL 模型来评估页面性能。
尽可能将操作同步到 rAF 来执行。
通过任务调度器将每个 rAF 的部分任务分配到后续的 rAF 中执行,或是等到 idle 时执行。