查看原文
其他

终端还能更快吗?

包续兵(柳千) 大淘宝前端技术 2022-10-16

对于一款现代 IDE 来说,终端是非常重要的内置功能,在基于 Web 技术构建的 IDE /编辑器产品中,终端功能的实现基本上使用两个开源的库来实现

  • Xterm.js 一款使用 TypeScript 编写的前端组件,提供可以运行在浏览器中的终端模拟器
  • node-pty  forkpty(3) 的 Node.js 绑定,提供简单友好的接口来 fork 一个 shell 进程,实现可交互的终端

这两个库也是 VS Code 内置终端所使用的,在功能性、兼容性上都有非常好的表现。

基本实现

基于 xterm.js  和 node-pty 实现一个 Web 端的终端功能非常简单,但在这之前需要了解一些基本的概念,例如这两个库是如何工作的。

1、Xterm.js

Xterm.js 只是一个前端组件,它仅用于渲染出终端的界面,提供了基础的 API 来将其连接到真正的终端进程

import { Terminal } from 'xterm';
import { AttachAddon } from 'xterm-addon-attach';

const terminal = new Terminal();
const attachAddon = new AttachAddon(webSocket);

terminal.loadAddon(attachAddon);

在这个例子中,为 terminal 实例加载了一个名为 xterm-addon-attach的 addon,它主要的作用就是将 webSocket消息解析并提交到 temrinal 的缓冲区,而 terminal 会将这些消息渲染出来。xterm.js 支持 dom/canvas/webGL(实验性) 几种不同的渲染器,对于大多数场景来说,使用 canvas 性能会更好一些。

2、node-pty

与 xterm.js 不同的是,node-pty 用于连接到真正的 shell 进程,例如常见的 bash/zsh 等。它允许通过 API 来控制 shell 进程,以实现类似常见终端(iTerm、Windows Terminal 等)的一些控制功能。

import * as pty from 'node-pty';

const ptyProcess = pty.spawn(shell, [], {
  name: 'xterm-color',
  cols: 80,
  rows: 30,
  cwd: process.env.HOME,
  env: process.env
});

ptyProcess.on('data'function(data) {
 webSocket.send(data);
});

webSocket.on('data'(chunk) => {
 ptyProcess.write(chunk);
});

对于类似 OpenSumi/VS Code 这类内置终端来说,需要将这两个库连接起来,最简单的实现是使用 Websocket,前端 (xterm.js) 端将监听用户输入及键盘事件,将其通过 websocket 发送到后端(node-pty)。而后端监听来自 websocket 的消息,同时将自身的输出通过 websocket 发送到前端。这样就实现了终端的基本功能。对于链接识别、打开等等这些功能 xterm.js 都分别提供了不同的 addon 来实现,本文不再赘述。

性能问题

乍一看似乎 xterm.js 和 node-pty 已经帮我们实现了绝大多数功能,基础的使用方法也非常简单明了,然而这里面隐藏了一些性能问题,并且是大多数基于这两个库实现的终端应用都会面临的问题。

1、巨量输出

对于日常使用终端的用户来说,最明显能感受到的性能问题是在运行一些输出非常多的命令时,例如 find /,又或是 cat一个较大的文件,又或者是在较大的项目中执行 npm installnpm build等命令。这几个例子在 OpenSumi 中都会引发很明显的性能问题。

2、通信瓶颈

在 Web 端,OpenSumi 前后端之间通过  WebSocket 连接进行通信。而在 Electron 端,则使用 Node.js 的 IPC。IDE 的前端界面与后端之间除了终端之外还有其他大量的通信交互,例如文件读写、插件调用、文件监听等等服务。当然,还有本文的主角 终端。

前后端之间每一次通信的交互都是一次 RPC 调用,例如当打开一个文件时,调用后端服务的 readFile,会等待 RPC 消息回来,如果这个时间耗时较长,则会显示一个加载中的状态。JavaScript 是单线程运行的,这意味着如果 RPC 通道被其他任务占用,会导致其他调用缓慢。

做一个简单的试验可以看出问题,我们在终端里运行 find ~ 时,会列出所有 ~ 目录下的文件,根据文件数量的多少,这个命令的输出可能持续十几秒以上,而在这个例子中,很明显整个 IDE 界面的 IO 操作都被阻塞了,当运行命令时,从文件树点击打开文件、展开目录都会进入等待状态。这也是 OpenSumi 长久以来的顽疾之一。

优化

虽然这个问题在运行少数输出较多的命令时才会出现,然而一旦出现,造成的体验下降是非常严重的。可以想象在某些项目初始化安装依赖时,整个界面几乎无法操作,任何交互都会排队等待。

是否有简单而又有效的方式来针对这个场景做优化呢?

在某一天照常逛 GitHub 时,无意间看到了一个几年前关注过的项目 [Hyper](https://github.com/vercel/hyper)。很早以前的体验 hyper 时,唯一的印象就是比较卡,包括启动、执行命令等交互都不太流畅。不过好奇心驱使我打开了他们的官网,而习惯驱使我点开了观望中的 blog一栏。

Hyper.js Blog(https://hyper.is/blog)

在这篇博客中,介绍了 Hyper 3 对于各方面性能所做的优化,而其中对于 RPC 消息简单的批处理对性能提升非常明显,因为他们也面临了上述的问题

原理实际上非常简单,一句话就可以描述

合并 pty 输出的数据,16ms 后发送给客户端,如果在 16ms 内没有收到新的数据,或短时间内数据 超过限定的长度(例如 100k),则立即发送缓存的数据

能正确理解这句话就可以照葫芦画瓢实现这个优化逻辑。经过一番优化后,OpenSumi 上终端的性能同样也有了明显的提升。

这里同样在运行 find ~命令时操作文件树,没有出现加载框,打开操作也很快。

就这么简单

是的,借用 hyper 这篇博客中的原话

We're genuinely thankful to the open source community. We're not saying this only because we are building on top of an incredible set of open source libraries, but also because we find the helpful ethos of the community very touching.

也许这就是开源精神。

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存