集成终端性能改进
2017年10月3日 Daniel Imms, @Tyriar
集成终端的渲染引擎已完全重写,以性能为中心,面向即将发布的 Visual Studio Code 1.17 版本。在此版本中,我们从基于 DOM 的渲染系统转向使用 HTML canvas 元素。
DOM 渲染
令人惊讶的是,在为显示静态文档而设计的系统中,交互式终端的渲染是可能的。然而,随着时间的推移,我们发现 DOM 提供的某些功能需要被覆盖才能修复一些问题。
选择:为了覆盖终端用例,我们做了很多与 DOM 选择系统对抗的工作。由于我们总是只渲染 DOM 可见的内容,所以如果不重新实现选择功能,您将无法选择多页内容。滚动也会导致选择丢失。为了解决这些问题,我们添加了自定义的选择逻辑。
字符错位:由于许多等宽字体对于某些 Unicode 字符并非严格等宽,这可能导致出现下图右侧所示的情况。

解决此问题的一种方法是将所有 Unicode 字符包装在固定宽度的 span 中,但这会增加渲染一帧所需的时间。
过度的垃圾回收:由于渲染终端所需的元素数量庞大,垃圾回收器需要频繁清理内存,这常常会明显地挤占渲染时间。我们引入了一个对象池来尝试解决此问题,允许 DOM 元素被回收利用。
性能:无论我们多么努力地解决这些问题,性能始终会受到布局引擎设置的硬性上限的限制。
绕过布局引擎
在某些情况下,仅组合元素和进行布局就可能花费超过一帧(16.6 毫秒)的时间,如果我们希望在终端中保持流畅的 60 帧每秒(FPS),这是不可接受的。解决方案是采用新的基于 canvas 的渲染引擎。
HTML <canvas> 元素允许使用 JavaScript API 绘制图形和文本。
渲染层
使用多个称为“渲染层”的 canvas 元素来简化终端不同部分的渲染。
当前层按顺序排列如下:
- 文本:背景颜色和前景文本,此层是不透明的。
- 选择:鼠标选择。
- 链接:鼠标悬停在链接上时的下划线。
- 光标:终端的光标。
将这些部分分离成各自的小组件极大地简化了绘图过程。
只绘制变化的部分
新渲染器的重要部分在于它只绘制发生变化的部分。为此,我们维护了一个精简的内部模型,其中包含关于单元格绘制状态的最少量信息。然后使用该状态来快速检查单元格在执行更耗时的绘制操作之前是否需要更改。对于文本层,此模型包括对字符、文本样式、前景色和背景色的引用。
与以前的渲染引擎相比,以前的渲染引擎会移除 DOM 中的整行,重新构建并重新添加,即使没有任何变化。

上图中绿色的矩形表示重新绘制的区域。
纹理图集
纹理图集被用来进一步提高渲染速度。后台有一个 ImageBitmap,其中包含所有 ASCII 字符,以最常见的样式和默认背景色显示。
在绘制这些样式的文本时,会使用纹理图集,而不是常规调用 CanvasRenderingContext2D.fillText。由于 `ImageBitmap` 位于 GPU 上,因此绘制速度得到了显著提高。

强制跳过帧
由于 DOM 渲染速度的原因,有必要跳过额外的帧,以确保解析器有足够的时间使用 CPU。虽然这使得命令的运行速度比以前更快,但大量通过终端流式传输的数据会导致帧率下降到 10 FPS 以下。
有了新渲染器,这个限制已被解除,您现在可以在终端中享受高达 60 FPS 的体验。

结果
我们的基准测试表明,集成终端现在的渲染速度比以前快 5 到 45 倍,具体取决于情况。即使您没有注意到响应速度和帧率的提高,更快的渲染也意味着更低的电池消耗!我们希望您喜欢这些性能改进,它们将在几天内随 VS Code 1.17 版本发布,并可在 Insiders 构建版中立即进行测试。
您还可以查看 原始 xterm.js pull request,其中添加了该功能,以获取更详细的了解。
编码愉快!
Daniel Imms, VS Code 团队成员 @Tyriar