Xterm.js: 缓冲区性能改进

创建于 2017-07-13  ·  73评论  ·  资料来源: xtermjs/xterm.js

问题

记忆

现在我们的缓冲区占用了太多内存,特别是对于启动多个终端并设置大回滚的应用程序。 例如,使用 160x24 终端并填充 5000 回滚的演示需要大约 34mb 内存(请参阅 https://github.com/Microsoft/vscode/issues/29840#issuecomment-314539964),请记住这只是一个终端和 1080p 显示器可能使用更宽的终端。 此外,为了支持真彩色(https://github.com/sourcelair/xterm.js/issues/484),每个字符都需要存储 2 个额外的number类型,这几乎会使当前内存消耗增加一倍的缓冲区。

缓慢获取一行的文本

另一个问题是需要快速获取一行的实际文本。 这很慢的原因是由于数据的布局方式; 一行包含一个字符数组,每个字符有一个字符串。 所以我们将构造字符串,然后它会立即用于垃圾收集。 以前我们根本不需要这样做,因为文本是从行缓冲区(按顺序)拉出并渲染到 DOM 的。 然而,随着我们进一步改进 xterm.js,这变得越来越有用,选择和链接等功能都可以提取这些数据。 再次使用 160x24/5000 回滚示例,在 2014 年中的 Macbook Pro 上复制整个缓冲区需要 30-60 毫秒。

支持未来

未来的另一个潜在问题是,当我们考虑引入一些可能需要复制缓冲区中部分或全部数据的视图模型时,将需要这种东西来实现回流(https://github.com/sourcelair /xterm.js/issues/622) 正确 (https://github.com/sourcelair/xterm.js/pull/644#issuecomment-298058556) 并且可能还需要正确支持屏幕阅读器 (https://github.com) /sourcelair/xterm.js/issues/731)。 在记忆方面有一些回旋余地肯定会很好。

这个讨论开始于https://github.com/sourcelair/xterm.js/issues/484 ,这更详细,并提出了一些额外的解决方案。

如果有时间,我倾向于解决方案 3 并转向解决方案 5,并且它显示出显着的改进。 会喜欢任何反馈! /cc @jerch@mofux@rauchg@parisk

1.简单的解决方案

这基本上就是我们现在正在做的事情,只是添加了真彩色 fg 和 bg。

// [0]: charIndex
// [1]: width
// [2]: attributes
// [3]: truecolor bg
// [4]: truecolor fg
type CharData = [string, number, number, number, number];

type LineData = CharData[];

优点

  • 很简单

缺点

  • 消耗太多内存,几乎会使我们当前已经过高的内存使用量翻倍。

2.从CharData中拉出文本

这会将字符串存储在行而不是行上,这可能会在选择和链接方面看到非常大的收益,并且随着时间的推移快速访问行的整个字符串会更有用。

interface ILineData {
  // This would provide fast access to the entire line which is becoming more
  // and more important as time goes on (selection and links need to construct
  // this currently). This would need to reconstruct text whenever charData
  // changes though. We cannot lazily evaluate text due to the chars not being
  // stored in CharData
  text: string;
  charData: CharData[];
}

// [0]: charIndex
// [1]: attributes
// [2]: truecolor bg
// [3]: truecolor fg
type CharData = Int32Array;

优点

  • 无需在需要时重建线路。
  • 由于使用了Int32Array内存比现在低

缺点

  • 更新单个字符很慢,需要为单个字符更改重新生成整个字符串。

3.在范围内存储属性

拉出属性并将它们与范围相关联。 由于永远不会有重叠的属性,因此可以按顺序排列。

type LineData = CharData[]

// [0]: The character
// [1]: The width
type CharData = [string, number];

class CharAttributes {
  public readonly _start: [number, number];
  public readonly _end: [number, number];
  private _data: Int32Array;

  // Getters pull data from _data (woo encapsulation!)
  public get flags(): number;
  public get truecolorBg(): number;
  public get truecolorFg(): number;
}

class Buffer extends CircularList<LineData> {
  // Sorted list since items are almost always pushed to end
  private _attributes: CharAttributes[];

  public getAttributesForRows(start: number, end: number): CharAttributes[] {
    // Binary search _attributes and return all visible CharAttributes to be
    // applied by the renderer
  }
}

优点

  • 即使我们还存储真彩色数据,内存也比现在低
  • 可以优化属性的应用,而不是检查每个字符的属性并将其与之前的属性进行比较
  • 封装了将数据存储在数组中的复杂性( .flags而不是[0]

缺点

  • 更改另一个范围内的一系列字符的属性更为复杂

4. 将属性放入缓存

这里的想法是利用这样一个事实,即在任何一个终端会话中通常没有那么多样式,因此我们不应该创建尽可能少的样式并重复使用它们。

// [0]: charIndex
// [1]: width
type CharData = [string, number, CharAttributes];

type LineData = CharData[];

class CharAttributes {
  private _data: Int32Array;

  // Getters pull data from _data (woo encapsulation!)
  public get flags(): number;
  public get truecolorBg(): number;
  public get truecolorFg(): number;
}

interface ICharAttributeCache {
  // Never construct duplicate CharAttributes, figuring how the best way to
  // access both in the best and worst case is the tricky part here
  getAttributes(flags: number, fg: number, bg: number): CharAttributes;
}

优点

  • 即使我们也存储真彩色数据,内存使用情况与今天相似
  • 封装了将数据存储在数组中的复杂性( .flags而不是[0]

缺点

  • 比范围方法节省的内存更少

5. 3 & 4 的混合

type LineData = CharData[]

// [0]: The character
// [1]: The width
type CharData = [string, number];

class CharAttributes {
  private _data: Int32Array;

  // Getters pull data from _data (woo encapsulation!)
  public get flags(): number;
  public get truecolorBg(): number;
  public get truecolorFg(): number;
}

interface CharAttributeEntry {
  attributes: CharAttributes,
  start: [number, number],
  end: [number, number]
}

class Buffer extends CircularList<LineData> {
  // Sorted list since items are almost always pushed to end
  private _attributes: CharAttributeEntry[];
  private _attributeCache: ICharAttributeCache;

  public getAttributesForRows(start: number, end: number): CharAttributeEntry[] {
    // Binary search _attributes and return all visible CharAttributeEntry's to
    // be applied by the renderer
  }
}

interface ICharAttributeCache {
  // Never construct duplicate CharAttributes, figuring how the best way to
  // access both in the best and worst case is the tricky part here
  getAttributes(flags: number, fg: number, bg: number): CharAttributes;
}

优点

  • 可能是最快和最高效的内存
  • 当缓冲区包含许多具有样式但仅来自少数样式的块时,内存效率非常高(常见情况)
  • 封装了将数据存储在数组中的复杂性( .flags而不是[0]

缺点

  • 比其他解决方案更复杂,如果我们已经CharAttributes每个块保留一个
  • CharAttributeEntry对象中的额外开销
  • 更改另一个范围内的一系列字符的属性更为复杂

6. 2 & 3 的混合

这采用了 3 的解决方案,但还添加了一个惰性求值的文本字符串,以便快速访问行文本。 由于我们还将字符存储在CharData我们可以懒惰地评估它。

type LineData = {
  text: string,
  CharData[]
}

// [0]: The character
// [1]: The width
type CharData = [string, number];

class CharAttributes {
  public readonly _start: [number, number];
  public readonly _end: [number, number];
  private _data: Int32Array;

  // Getters pull data from _data (woo encapsulation!)
  public get flags(): number;
  public get truecolorBg(): number;
  public get truecolorFg(): number;
}

class Buffer extends CircularList<LineData> {
  // Sorted list since items are almost always pushed to end
  private _attributes: CharAttributes[];

  public getAttributesForRows(start: number, end: number): CharAttributes[] {
    // Binary search _attributes and return all visible CharAttributes to be
    // applied by the renderer
  }

  // If we construct the line, hang onto it
  public getLineText(line: number): string;
}

优点

  • 即使我们还存储真彩色数据,内存也比现在低
  • 可以优化属性的应用,而不是检查每个字符的属性并将其与之前的属性进行比较
  • 封装了将数据存储在数组中的复杂性( .flags而不是[0]
  • 更快地访问实际的行串

缺点

  • 由于挂在线串上而产生的额外内存
  • 更改另一个范围内的一系列字符的属性更为复杂

行不通的解决方案

  • 将字符串作为 int 存储在Int32Array中将不起作用,因为将 int 转换回字符需要很长时间。
areperformance typplan typproposal

最有用的评论

当前状态:

后:

所有73条评论

另一种可以混合使用的方法:使用 indexeddb、websql 或文件系统 api 将不活动的回滚条目分页到磁盘 🤔

伟大的提议。 我同意3.是目前最好的方法,因为它可以让我们在支持真彩色的同时节省内存。

如果我们到达那里并且事情继续进展顺利,那么我们可以按照5.中的建议进行优化,或者以当时我们想到的任何其他方式进行优化。

3.很棒👍。

@mofux ,虽然肯定有使用磁盘存储支持技术来减少内存占用的用例,但这会降低库在浏览器环境中的用户体验,这些环境要求用户获得使用磁盘存储的许可。

关于支持未来
我想得越多,拥有一个WebWorker的想法就越多,它可以完成解析 tty 数据、维护行缓冲区、匹配链接、匹配搜索标记等所有繁重的工作,对我很有吸引力。 基本上在单独的后台线程中完成繁重的工作而不会阻塞用户界面。 但我认为这应该是单独讨论的一部分,可能是针对 4.0 版本的 😉

+100 将来关于 WebWorker,但我认为我们需要更改我们支持的浏览器列表版本,因为并非所有浏览器都可以使用它...

当我说Int32Array ,如果环境不支持,这将是一个常规数组。

@mofux未来WebWorker好想法👍

@AndrienkoAleksandr是的,如果我们想使用WebWorker我们仍然需要通过特征检测来支持替代方案。

哇不错的清单:)

我也倾向于倾向于3.因为它承诺在超过 90% 的典型终端使用情况下大幅减少内存消耗。 Imho 内存优化应该是现阶段的主要目标。 对特定用例的进一步优化可能适用于此(我想到的是:“像应用程序一样的画布”,如 ncurses 等将使用大量的单单元更新,并且随着时间的推移会降低[start, end]列表) .

@AndrienkoAleksandr是的,我也喜欢 webworker 的想法,因为它可以减轻主线程的_some_负担。 这里的问题是(除了它可能不被所有想要的目标系统支持的事实之外)是 _some_ - JS 部分不再是所有优化的大问题,xterm.js 随着时间的推移已经看到了。 真正的性能问题是浏览器的布局/渲染......

@mofux分页到一些“外部记忆”是一个好主意,尽管它应该是一些更高抽象的一部分,而不是 xterm.js 是“给我一个交互式终端小部件的东西”。 这可以通过插件 imho 来实现。

Offtopic:对数组、typedarrays 和 asm.js 做了一些测试。 我只能说 - OMG,它就像1 : 1,5 : 10用于简单的变量加载和设置(在 FF 上甚至更多)。 如果纯 JS 速度真的开始受到影响,“使用 asm”可能会有所帮助。 但我认为这是最后的手段,因为它意味着根本性的变化。 而且 webassembly 还没有准备好发货。

Offtopic:对数组、typedarrays 和 asm.js 做了一些测试。 我只能说 - 天哪,对于简单的可变负载和集合,它就像 1 : 1,5 : 10(在 FF 上甚至更多)

@jerch澄清一下,数组与 typedarrays 是 1:1 到 1:5 吗?

用逗号很好地抓住了 - 我的意思是10:15:100速度明智。 但仅在 FF 类型数组上比普通数组稍快。 在所有浏览器上,asm 至少比 js 数组快 10 倍 - 使用 FF、webkit (Safari)、blink/V8(Chrome、Opera)进行测试。

@jerch很酷,除了更好的内存之外,typedarrays 的速度提高了 50%,现在绝对值得投资。

节省内存的想法 - 也许我们可以摆脱每个字符的width 。 将尝试实现更便宜的 wcwidth 版本。

@jerch我们需要访问它很多,我们不能延迟加载它或任何东西,因为当回流来临时我们将需要缓冲区中每个字符的宽度。 即使它很快,我们可能仍然希望保留它。

将其设为可选可能会更好,如果未指定,则假设为 1:

type CharData = [string, number?]; // not sure if this is valid syntax

[
  // 'a'
  ['a'],
  // '文'
  ['文', 2],
  // after wide
  ['', 0],
  ...
]

@Tyriar是的 - 好吧,因为我已经写好了,请在 PR #798 中查看它
对于查找表的 16k 字节成本,我的计算机上的加速是 10 到 15 倍。 如果仍然需要,也许可以将两者结合起来。

我们将来会支持更多标志: https :

另一个想法:只有终端的底部( Terminal.ybaseTerminal.ybase + Terminal.rows )是动态的。 构成大部分数据的回滚是完全静态的,也许我们可以利用这一点。 直到最近我才知道这一点,但即使是像删除行(DL、CSI Ps M)这样的东西也不会使回滚回落,而是插入另一行。 同样,向上滚动 (SU, CSI Ps S) 删除Terminal.scrollTop的项目并插入Terminal.scrollBottom

独立管理终端的底部动态部分并在行被推出时推动回滚可能会带来一些显着的收益。 例如,底部可能更冗长,以支持修改属性、更快的访问等,而回滚可以更像是上述建议的存档格式。

另一个想法:将CharAttributeEntry限制

例如:

screen shot 2017-08-07 at 8 51 52 pm

红色/绿色差异的右侧是无样式的“空白”单元格。

@泰瑞尔
有机会把这个问题重新提上议程吗? 至少对于输出密集型程序而言,保存终端数据的不同方式可能会节省大量内存和时间。 如果我们可以避免拆分和保存输入字符串的单个字符,那么 2/3/4 的一些混合将提供巨大的吞吐量提升。 此外,仅在属性更改后才保存属性将有助于节省内存。

例子:
使用新的解析器,我们可以保存一堆输入字符而不会弄乱属性,因为我们知道它们不会在该字符串的中间发生变化。 该字符串的属性可以与 wcwidths 一起保存在其他一些数据结构或属性中(是的,我们仍然需要那些来找到它们的换行符)和换行符和停止符。 当数据传入时,这基本上会放弃单元模型。
如果某些东西介入并想要对终端数据进行深入表示(例如渲染器或某些转义序列/用户想要移动光标),就会出现问题。 如果发生这种情况,我们仍然需要进行单元格计算,但仅对终端 cols 和 rows 中的内容执行此操作就足够了。 (尚不确定滚动出的内容,这可能更可缓存且重绘成本更低。)

@jerch我将在几周后的某一天在布拉格与

来自https://github.com/xtermjs/xterm.js/pull/1460#issuecomment -390500944

算法有点贵,因为每个字符都需要评估两次

@jerch如果您对从缓冲区更快地访问文本有任何想法,请告诉我们。 目前,正如您所知,其中大部分只是单个字符,但也可能是ArrayBuffer 、字符串等。我一直认为我们应该考虑更多地利用回滚以某种方式不可变的优势。

好吧,我过去对 ArrayBuffers 进行了大量实验:

  • 在典型方法的运行时间方面,它们比Array稍差(可能引擎供应商的优化程度仍然较低)
  • new UintXXArray远比用[]创建文字数组差
  • 如果您可以预分配和重用数据结构(最多 10 次),它们会得到多次回报,这是混合数组的链表性质由于幕后大量分配和 gc 而影响性能的地方
  • 对于字符串数据,来回转换吃掉了所有好处 - 遗憾的是 JS 没有为 Uint16Array 转换器提供本机字符串(尽管TextEncoder部分可行)

我对ArrayBuffer建议不要将它们用于字符串数据,因为会产生转换损失。 理论上终端可以使用ArrayBuffer从 node-pty 到终端数据(这会在到前端的路上节省几次转换),不确定渲染是否可以那样完成,我想渲染东西总是需要最后的uint16_tstring转换。 但是,即使是最后一个字符串创建也会消耗掉保存的大部分运行时——此外,还会在内部将终端变成一个丑陋的 C-ish 野兽。 因此我放弃了这种方法。

TL;DR ArrayBuffer如果您可以预分配和重用数据结构,则更好。 对于其他一切,普通数组更好。 字符串不值得被挤入 ArrayBuffers。

我提出的一个新想法试图尽可能地减少字符串的创建,尤其是。 试图避免讨厌的拆分和连接。 它有点基于您上面的第二个想法,并使用新的InputHandler.print方法、wcwidth 和 line 停止:

  • print现在可以获取最多几个终端行的整个字符串
  • 将这些字符串保存在一个简单的指针列表中而不做任何更改(没有字符串 alloc 或 gc,如果与 prealloc 结构一起使用,可以避免列表 alloc)以及当前属性
  • 将光标前进wcwidth(string) % cols
  • 特殊情况\n (硬换行):光标前进一行,将指针列表中的位置标记为硬换行
  • 使用 wrapAround 的特殊情况行溢出:将字符串中的位置标记为软换行符
  • 特殊情况\r :将最后一行内容(从当前光标位置到最后一个换行符)加载到某个行缓冲区以被覆盖
  • 数据流如上,尽管\r情况不需要单元抽象或字符串拆分
  • 属性更改没有问题,只要没有人请求真正的cols x rows表示(他们只是更改与整个字符串一起保存的 attr 标志)

顺便说一句,wcwidths 是字素算法的一个子集,因此将来可能可以互换。

现在危险的部分 1 - 有人想在cols x rows内移动光标:

  • 在换行符中向后移动cols - 当前终端内容的开始
  • 每个换行符表示一个真正的终端线
  • 将内容加载到单元模型中仅一页(尚不确定是否也可以通过巧妙的字符串定位来省略)
  • 做一些讨厌的工作:如果要求更改属性,我们有点不走运,必须回退到完整单元模型或字符串拆分和插入模型(后者可能会导致性能不佳)
  • 数据再次流动,现在该页面的缓冲区中的字符串和属性数据已降级

现在是危险的第 2 部分- 渲染器想要绘制一些东西:

  • 如果我们需要深入到单元模型,或者可以只提供带有换行符和文本属性的字符串偏移量,这有点取决于渲染器

优点:

  • 非常快的数据流
  • 针对最常见的InputHandler方法进行了优化 - print
  • 使终端调整大小时的回流线成为可能

缺点:

  • 几乎所有其他InputHandler方法在中断此流程模型和需要一些中间单元抽象的意义上都是危险的
  • 渲染器集成不清楚(至少对我来说是 atm)
  • 可能会降低应用程序之类的诅咒的性能(它们通常包含更多“危险”序列)

嗯,这是这个想法的粗略草稿,远不能用于 atm,因为许多细节尚未涵盖。 特别是 “危险”部分可能会因许多性能问题而变得令人讨厌(例如使用更糟糕的 gc 行为降低缓冲区等 pp)

@jerch

字符串不值得被挤入 ArrayBuffers。

我认为 Monaco 将其缓冲区存储在ArrayBuffer并且性能非常高。 我还没有深入研究实现。

特别是试图避免讨厌的拆分和连接

哪个?

我一直在想我们应该考虑更多地利用回滚以某种方式不可变的优势。

一个想法是将回滚与视口部分分开。 一旦一行进入回滚,它就会被推入回滚数据结构。 您可以想象 2 个CircularList对象,一个对象的线条被优化为永不改变,另一个则相反。

@Tyriar关于回滚 - 是的,因为光标永远无法访问它,因此它可能会节省一些内存以仅删除滚动行的单元格抽象。

@泰瑞尔
如果我们可以将转换限制为一个(可能是渲染输出的最后一个),那么将字符串存储在 ArrayBuffer 中是有意义的。 这比到处处理字符串要好一些。 这是可行的,因为 node-pty 也可以提供原始数据(而且 websocket 也可以为我们提供原始数据)。

特别是试图避免讨厌的拆分和连接

哪个?

整个方法是根本避免_minimize_分裂。 如果没有人请求光标跳转到缓冲数据中,字符串将永远不会被拆分并且可以直接进入渲染器(如果支持)。 根本没有细胞分裂和后来加入。

@jerch好吧,如果视口被扩展,我想我们也可以在删除一行时拉回回滚? 不是 100% 肯定,或者即使它是正确的行为。

@Tyriar啊对了。 也不确定后者,我认为原生 xterm 只允许真正的鼠标或滚动条滚动。 即使 SD/SU 也不会将滚动缓冲区内容移回“活动”终端视口。

你能指出我使用 ArrayBuffer 的摩纳哥编辑器的来源吗? 好像我自己找不到 :blush:

嗯,只需重新阅读 TextEncoder/Decoder 规范,从 node-pty 到前端的 ArrayBuffers,我们基本上都被 utf-8 困住了,除非我们在某个时候以艰难的方式翻译它。 使 xterm.js utf-8 意识到? Idk,这将涉及到更高 unicode 字符的许多中间代码点计算。 Proside - 它会为 ascii 字符节省内存。

@rebornix你能给我们一些关于摩纳哥存储缓冲区的指针吗?

以下是类型化数组和新解析器的一些数字(更容易采用):

  • UTF-8 (Uint8Array): print动作从 190 MB/s 跳到 290 MB/s
  • UTF-16 (Uint16Array): print动作从 190 MB/s 跳到 320 MB/s

总体而言 UTF-16 的性能要好得多,但这是意料之中的,因为解析器已为此进行了优化。 UTF-8 受中间代码点计算的影响。

字符串到类型数组的转换占用了我的基准ls -lR /usr/lib JS 运行时间的约 4%(始终远低于 100 毫秒,通过InputHandler.parse的循环完成)。 我没有测试反向转换(这是在单元格级别的InputHandller.print隐式完成的 atm)。 整体运行时间比字符串稍差(解析器中节省的时间不会补偿转换时间)。 当其他部分也类型化数组时,这可能会改变。

以及相应的屏幕截图(使用ls -lR /usr/lib ):

带字符串:
grafik

使用 Uint16Array:
grafik

注意EscapeSequenceParser.parse差异,它可以从类型化数组中获利(快 30%)。 InputHandler.parse进行转换,因此对于类型化数组版本来说更糟。 此外,GC Minor 对类型化数组还有更多工作要做(因为我把数组扔掉了)。

编辑:在截图中可以看到另一个方面 - GC 与大约 20% 的运行时间相关,长时间运行的帧(红色标记)都与 GC 相关。

只是另一个有点激进的想法:

  1. 创建自己的基于数组缓冲区的虚拟内存,一些大(> 5 MB)
    如果数组缓冲区的长度是从int8int16int32的 4 个透明开关的倍数,则类型是可能的。 分配器在Uint8Array上返回一个空闲索引,该指针可以通过简单的位移转换为Uint16ArrayUint32Array位置。
  2. 将传入的字符串作为 UTF-16 的uint16_t类型写入内存。
  3. 解析器在字符串指针上运行,并使用指向此内存的指针而不是字符串切片调用InputHandler方法。
  4. 在虚拟内存中创建终端数据缓冲区作为结构体类型的环形缓冲区数组,而不是原生 JS 对象,可能像这样(仍然基于单元格):
struct Cell {
    uint32_t *char_start;  // start pointer of cell content (JS with pointers hurray!)
    uint8_t length;        // length of content (8 bit here is sufficient)
    uint32_t attr;         // text attributes (might grow to hold true color someday)
    uint8_t width;         // wcwidth (maybe merge with other member, always < 4)
    .....                  // some other cell based stuff
}

优点:

  • 在可能的情况下省略 JS 对象并因此省略 GC(仅保留少数本地对象)
  • 只需将一份初始数据复制到所需的虚拟内存中
  • 几乎没有mallocfree成本(取决于分配器/解除分配器的聪明程度)
  • 将节省大量内存(避免 JS 对象内存开销)

缺点:

  • 欢迎来到 Cavascript 恐怖秀 :scream:
  • 难以实施,改变了一切
  • 在真正实施之前,速度收益尚不清楚

:微笑:

很难实现,改变了一切😉

这更接近摩纳哥的工作方式,我记得这篇博客文章讨论了存储字符元数据的策略https://code.visualstudio.com/blogs/2017/02/08/syntax-highlighting-optimizations

是的,这基本上是相同的想法。

希望我对monaco 存储缓冲区的位置的回答还为时不晚。

Alex 和我都赞成 Array Buffer 并且大多数时候它给了我们很好的性能。 我们使用ArrayBuffer的一些地方:

我们使用简单的字符串作为文本缓冲区而不是数组缓冲区,因为 V8 字符串更容易操作

  • 我们在加载文件的一开始就进行编码/解码,因此文件被转换为 JS 字符串。 V8 决定使用一个字节还是两个字节来存储一个字符。
  • 我们经常对文本缓冲区进行编辑,字符串更容易处理。
  • 我们正在使用 nodejs 本机模块,并在必要时访问 V8 内部。

以下列表只是我偶然发现的有趣概念的快速摘要,这些概念可能有助于降低内存使用和/或运行时间:

  • FlatJS (https://github.com/lars-t-hansen/flatjs) - 元语言帮助编码基于数组缓冲区的堆
  • http://2ality.com/2017/01/shared-array-buffer.html (作为 ES2017 的一部分宣布,由于 Spectre,未来可能不确定,除了具有真正并发和真正原子的非常有前途的想法)
  • webassembly/asm.js(当前状态?可用?已经有一段时间没有关注它的开发了,几年前使用 emscripten 到 asm.js 和一个用于游戏 AI 的 C 库,但结果令人印象深刻)
  • https://github.com/AssemblyScript/assemblyscript

为了让总和在这里滚动,这里有一个快速技巧,我们可以如何“合并”文本属性。

代码主要是由为缓冲区数据节省内存的想法驱动的(运行时会受到影响,尚未测试多少)。 特别是前景和背景的 RGB 文本属性(一旦支持)将使 xterm.js 由当前单元格布局占用大量内存。 该代码试图通过使用可调整大小的引用计数图集来规避此属性。 恕我直言,这是一个选项,因为单个终端几乎不会容纳超过 100 万个单元格,如果所有单元格都不同,这将使图集增长到1M * entry_size

单元格本身只需要保存属性图集的索引。 在单元格更改时,旧索引需要取消引用,而新索引需要引用。 图集索引将替换终端对象的属性属性,并将在 SGR 中自行更改。

该图集目前仅针对文本属性,但如果需要,可以扩展到所有单元格属性。 虽然当前终端缓冲区为属性数据保存了 2 个 32 位数字(在当前缓冲区设计中为 4 个 RGB),但图集会将其减少到只有一个 32 位数字。 地图集条目也可以进一步打包。

interface TextAttributes {
    flags: number;
    foreground: number;
    background: number;
}

const enum AtlasEntry {
    FLAGS = 1,
    FOREGROUND = 2,
    BACKGROUND = 3
}

class TextAttributeAtlas {
    /** data storage */
    private data: Uint32Array;
    /** flag lookup tree, not happy with that yet */
    private flagTree: any = {};
    /** holds freed slots */
    private freedSlots: number[] = [];
    /** tracks biggest idx to shortcut new slot assignment */
    private biggestIdx: number = 0;
    constructor(size: number) {
        this.data = new Uint32Array(size * 4);
    }
    private setData(idx: number, attributes: TextAttributes): void {
        this.data[idx] = 0;
        this.data[idx + AtlasEntry.FLAGS] = attributes.flags;
        this.data[idx + AtlasEntry.FOREGROUND] = attributes.foreground;
        this.data[idx + AtlasEntry.BACKGROUND] = attributes.background;
        if (!this.flagTree[attributes.flags])
            this.flagTree[attributes.flags] = [];
        if (this.flagTree[attributes.flags].indexOf(idx) === -1)
            this.flagTree[attributes.flags].push(idx);
    }

    /**
     * convenient method to inspect attributes at slot `idx`.
     * For better performance atlas idx and AtlasEntry
     * should be used directly to avoid number conversions.
     * <strong i="10">@param</strong> {number} idx
     * <strong i="11">@return</strong> {TextAttributes}
     */
    getAttributes(idx: number): TextAttributes {
        return {
            flags: this.data[idx + AtlasEntry.FLAGS],
            foreground: this.data[idx + AtlasEntry.FOREGROUND],
            background: this.data[idx + AtlasEntry.BACKGROUND]
        };
    }

    /**
     * Returns a slot index in the atlas for the given text attributes.
     * To be called upon attributes changes, e.g. by SGR.
     * NOTE: The ref counter is set to 0 for a new slot index, thus
     * values will get overwritten if not referenced in between.
     * <strong i="12">@param</strong> {TextAttributes} attributes
     * <strong i="13">@return</strong> {number}
     */
    getSlot(attributes: TextAttributes): number {
        // find matching attributes slot
        const sameFlag = this.flagTree[attributes.flags];
        if (sameFlag) {
            for (let i = 0; i < sameFlag.length; ++i) {
                let idx = sameFlag[i];
                if (this.data[idx + AtlasEntry.FOREGROUND] === attributes.foreground
                    && this.data[idx + AtlasEntry.BACKGROUND] === attributes.background) {
                    return idx;
                }
            }
        }
        // try to insert into a previously freed slot
        const freed = this.freedSlots.pop();
        if (freed) {
            this.setData(freed, attributes);
            return freed;
        }
        // else assign new slot
        for (let i = this.biggestIdx; i < this.data.length; i += 4) {
            if (!this.data[i]) {
                this.setData(i, attributes);
                if (i > this.biggestIdx)
                    this.biggestIdx = i;
                return i;
            }
        }
        // could not find a valid slot --> resize storage
        const data = new Uint32Array(this.data.length * 2);
        for (let i = 0; i < this.data.length; ++i)
            data[i] = this.data[i];
        const idx = this.data.length;
        this.data = data;
        this.setData(idx, attributes);
        return idx;
    }

    /**
     * Increment ref counter.
     * To be called for every terminal cell, that holds `idx` as text attributes.
     * <strong i="14">@param</strong> {number} idx
     */
    ref(idx: number): void {
        this.data[idx]++;
    }

    /**
     * Decrement ref counter. Once dropped to 0 the slot will be reused.
     * To be called for every cell that gets removed or reused with another value.
     * <strong i="15">@param</strong> {number} idx
     */
    unref(idx: number): void {
        this.data[idx]--;
        if (!this.data[idx]) {
            let treePart = this.flagTree[this.data[idx + AtlasEntry.FLAGS]];
            treePart.splice(treePart.indexOf(this.data[idx]), 1);
        }
    }
}

let atlas = new TextAttributeAtlas(2);
let a1 = atlas.getSlot({flags: 12, foreground: 13, background: 14});
atlas.ref(a1);
// atlas.unref(a1);
let a2 = atlas.getSlot({flags: 12, foreground: 13, background: 15});
atlas.ref(a2);
let a3 = atlas.getSlot({flags: 13, foreground: 13, background: 16});
atlas.ref(a3);
let a4 = atlas.getSlot({flags: 13, foreground: 13, background: 16});
console.log(atlas);
console.log(a1, a2, a3, a4);
console.log('a1', atlas.getAttributes(a1));
console.log('a2', atlas.getAttributes(a2));
console.log('a3', atlas.getAttributes(a3));
console.log('a4', atlas.getAttributes(a4));

编辑:
运行时间损失几乎为零,对于我使用ls -lR /usr/lib基准测试,它对 ~2.3 秒的总运行时间增加了不到 1 毫秒。 有趣的旁注 - 该命令为 5 MB 数据的输出设置了少于 64 个不同的文本属性槽,并且一旦完全实施将节省超过 20 MB。

制作了一些原型 PR 来测试对缓冲区的一些更改(有关更改背后的总体思路,请参阅 https://github.com/xtermjs/xterm.js/pull/1528#issue-196949371):

  • PR #1528:属性图集
  • PR #1529:从缓冲区中删除 wcwidth 和 charCode
  • PR #1530:用代码点/单元存储索引值替换缓冲区中的字符串

@jerch为此远离atlas这个词可能是个好主意,这样“atlas”总是意味着“纹理atlas”。 像 store 或 cache 这样的东西可能会更好?

哦,好的,“缓存”很好。

猜猜我已经完成了测试平台 PR。 还请查看 PR 评论以了解以下粗略摘要的背景。

提议:

  1. 构建一个AttributeCache来保存设计单个终端单元格所需的一切。 请参阅 #1528 以获取早期参考计数版本,该版本也可以保存真实的颜色规格。 如果需要在多个终端应用程序中节省更多内存,也可以在不同的终端实例之间共享缓存。
  2. 构建一个StringStorage来保存简短的终端内容数据字符串。 #1530 中的版本甚至通过“重载”指针含义来避免存储单个字符字符串。 wcwidth应该移到这里。
  3. 将当前的CharData[number, string, number, number]缩小到[number, number] ,其中数字是指向以下内容的指针(索引号):

    • AttributeCache条目

    • StringStorage条目

属性不太可能发生很大变化,因此单个 32 位数字将随着时间的推移节省大量内存。 的StringStorage指针为单个字符一个真正的unicode码点,因此可被用作code的条目CharData 。 实际的字符串可以通过StringStorage.getString(idx)CharData的第四个字段wcwidth可以被StringStorage.wcwidth(idx) (尚未实现)。 在CharData去除codewcwidth几乎没有运行时损失(在 #1529 中测试)。

  1. 将缩小的CharData到基于密集Int32Array的缓冲区实现中。 同样在 #1530 中使用存根类(远非功能齐全)进行了测试,最终的好处可能是:

    • 终端缓冲区的内存占用减少 80%(从 5.5 MB 到 0.75 MB)

    • 稍微快一点(尚未测试,我希望获得 20% - 30% 的速度)

    • 编辑:快得多 - ls -lR /usr/lib脚本运行时间下降到 1.3s(master 为 2.1s),而旧缓冲区仍然处于活动状态以进行游标处理,一旦删除,我希望运行时间降至 1s 以下

缺点 - 第 4 步需要大量工作,因为它需要对缓冲区接口进行一些返工。 但是,嘿 - 为了节省 80% 的 RAM 并仍然获得运行时性能,它没什么大不了的,是吗? :微笑:

我偶然发现了另一个问题 - 当前的空单元格表示。 恕我直言,一个单元格可以有3种状态:

  • :初始单元格状态,尚未写入任何内容或内容已删除。 它的宽度为 1,但没有内容。 目前在blankLineeraseChar ,但以空格作为内容。
  • null : 全宽字符后的单元格,表示它没有用于视觉表示的宽度。
  • 正常:单元格包含一些内容并具有视觉宽度(1 或 2,一旦我们支持真正的字素/比迪语,可能会更大,还不确定,哈哈)

我在这里看到的问题是,空单元格与插入空格的普通单元格无法区分,两者在缓冲区级别(相同的内容,相同的宽度)看起来相同。 我没有编写任何渲染器/输出代码,但我希望这会导致输出前端出现尴尬的情况。 特别是处理一行的右端可能会很麻烦。
一个有 15 个列的终端,首先是一些字符串输出,它被包裹起来:

1: 'H', 'e', 'l', 'l', 'o', ' ', 't', 'e', 'r', 'm', 'i', 'n', 'a', 'l', ' '
2: 'w', 'o', 'r', 'l', 'd', '!', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '

与带有ls的文件夹列表相比:

1: 'R', 'e', 'a', 'd', 'm', 'e', '.', 'm', 'd', ' ', ' ', ' ', ' ', ' ', ' '
2: 'f', 'i', 'l', 'e', 'A', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '

第一个示例在单词“终端”之后包含一个真正的空格,第二个示例从未触及“Readme.md”之后的单元格。 它在缓冲区级别表示的方式对于将内容作为终端输出打印到屏幕的标准情况非常有意义(无论如何都需要占用房间),但对于尝试处理内容字符串(如鼠标选择)的工具或回流管理器它不再清楚,空间来自哪里。

这或多或少会导致下一个问题 - 如何确定一行中的实际内容长度(包含左侧内容的单元格数量)? 一个简单的方法是从右侧计算空单元格,同样,上面的双重含义使得这很难确定。

提议:
恕我直言,这很容易通过对空单元格使用其他占位符来修复,例如控制字符或空字符串,并在需要时替换渲染过程中的那些。 也许屏幕渲染器也可以从中受益,因为它可能根本不需要处理这些单元格(取决于生成输出的方式)。

顺便说一句,对于上面的包裹字符串,这也会导致isWrapped问题,这对于回流调整大小或正确的复制和粘贴选择处理至关重要。 恕我直言,我们无法删除它,但需要比 atm 更好地集成它。

@jerch令人印象深刻的工作! :笑脸:

1 构建一个 AttributeCache 来保存设置单个终端单元样式所需的一切。 请参阅 #1528 以获取早期参考计数版本,该版本也可以保存真实的颜色规格。 如果需要在多个终端应用程序中节省更多内存,也可以在不同的终端实例之间共享缓存。

对#1528 发表了一些评论。

2 构建一个 StringStorage 来保存简短的终端内容数据字符串。 #1530 中的版本甚至通过“重载”指针含义来避免存储单个字符字符串。 wcwidth 应该移到这里。

对#1530 发表了一些评论。

4 将缩小的 CharData 移动到基于密集 Int32Array 的缓冲区实现中。 同样在 #1530 中使用存根类(远非功能齐全)进行了测试,最终的好处可能是:

还没有完全接受这个想法,我认为当我们实施回流时它会咬我们。 看起来这些步骤中的每一步都几乎可以按顺序完成,因此我们可以看到事情的进展情况,并在我们完成 3 步后看看是否有意义。

我偶然发现了另一个问题 - 当前的空单元格表示。 恕我直言,一个单元格可以有 3 个状态

这是一个来自https://github.com/xtermjs/xterm.js/issues/1286的错误示例,:+1: 区分空白单元格和“空”单元格

顺便说一句,对于上面的包裹字符串,这也会导致 isWrapped 问题,这对于回流调整大小或正确的复制和粘贴选择处理至关重要。 恕我直言,我们无法删除它,但需要比 atm 更好地集成它。

当我们处理https://github.com/xtermjs/xterm.js/issues/622时,我看到isWrapped消失了,因为 CircularList 将只包含未包装的行。

还没有完全接受这个想法,我认为当我们实施回流时它会咬我们。 看起来这些步骤中的每一步都几乎可以按顺序完成,因此我们可以看到事情的进展情况,并在我们完成 3 步后看看是否有意义。

是的,我和你在一起(玩这种完全不同的方法仍然很有趣)。 1 和 2 可以挑选,3 可以根据 1 或 2 应用。4 是可选的,我们可以坚持当前的缓冲区布局。 节省的内存是这样的:

  1. 1 + 2 + 3 in CircularList :节省 50%(~2.8MB of ~5.5 MB)
  2. 1 + 2 + 3 + 4 中途 - 只需将行数据放在类型化数组中,但坚持行索引访问:节省 82% (~0.9MB)
  3. 1 + 2 + 3 + 4 带指针算法的完全密集数组:节省 87% (~0.7MB)

1.很容易实现,具有更大 scrollBack 的内存行为仍然会显示糟糕的缩放,如下所示https://github.com/xtermjs/xterm.js/pull/1530#issuecomment -403542479 但毒性较低
2.稍微难以实现(需要在行级别上更多的间接),但可以保持Buffer的更高 API 完整。 恕我直言,选择 - 大内存保存,仍然易于集成。
3.比选项 2 多 5% 的内存节省,难以实现,将改变所有 API,从而改变整个代码库。 Imho更多的学术兴趣或无聊的下雨天要实施哈哈。

@Tyriar我用 rust 对 webassembly 的使用做了一些进一步的测试,并重写了解析器。 请注意,我的 Rust 技能有点“生疏”,因为我还没有深入研究它,因此以下可能是弱 Rust 代码的结果。 结果:

  • wasm 部分中的数据处理速度稍快(5 - 10%)。
  • 从 JS 调用 wasm 会产生一些开销并吃掉上面的所有好处。 事实上,它慢了大约 20%。
  • “二进制”将小于 JS 对应物(没有真正测量,因为我没有实现所有东西)。
  • 为了让 JS <--> wasm transition 轻松完成,需要一些 bloatcode 来处理 JS 类型(只进行字符串翻译)。
  • 我们无法避免 JS 到 wasm 的翻译,因为浏览器 DOM 和事件在那里不可访问。 它只能用于核心部分,这些部分不再那么重要(除了内存消耗)。

除非我们想用 rust(或任何其他支持 wasm 的语言)重写整个核心库,否则我们无法从迁移到 wasm lang imho 中获得任何好处。 现在 wasm langs 的一个优点是大多数支持显式内存处理(可以帮助我们解决缓冲区问题),缺点是将完全不同的语言引入主要以 TS/JS 为重点的项目(代码添加的高障碍)以及 wasm 和 JS land 之间的翻译费用。

TL; 博士
xterm.js 将广泛地融入一般的 JS 内容,如 DOM 和事件,以从 webassembly 中获得任何东西,即使是对核心部分的重写。

@jerch不错的调查 :smiley:

从 JS 调用 wasm 会产生一些开销并吃掉上面的所有好处。 事实上,它慢了大约 20%。

这也是摩纳哥本地化的主要问题,这主要表明了我的立场(尽管那是本地节点模块,而不是 wasm)。 我相信在任何可能的情况下使用ArrayBuffer应该会给我们带来性能和简单性之间的最佳平衡(易于实现,入门障碍)。

@Tyriar将尝试提出一个 AttributeStorage 来保存 RGB 数据。 还不确定 BST,对于在终端会话中只有几个颜色设置的典型用例,这在运行时会更糟,也许一旦颜色超过给定的阈值,这应该是运行时插入。 此外,内存消耗将再次增加很多,尽管它仍然会节省内存,因为属​​性仅存储一次,而不是与每个单元格一起存储(尽管每个单元格持有不同属性的最坏情况会受到影响)。
您知道为什么当前的fgbg 256 种颜色值是基于 9 位而不是 8 位吗? 附加位有什么用? 这里: https :
你能告诉我attr的当前位布局吗? 我认为类似于 StringStorage 指针的“双重含义”的类似方法可以进一步节省内存,但这需要为指针区别保留attr的 MSB,而不用于任何其他目的。 这可能会限制以后支持更多属性标志的可能性(因为FLAGS已经使用了 7 位),我们是否仍然缺少一些可能会出现的基本标志?

术语缓冲区中的 32 位attr数字可以这样打包:

# 256 indexed colors
32:       0 (no RGB color)
31..25:   flags (7 bits)
24..17:   fg (8 bits, see question above)
16..9:    bg
8..1:     unused

# RGB colors
32:       1 (RGB color)
31..25:   flags (7 bits)
24..1:    pointer to RGB data (address space is 2^24, which should be sufficient)

这样,存储只需要保存两个 32 位数字的 RGB 数据,而标志可以保留在attr数字中。

@jerch顺便给你发了一封邮件,可能又被垃圾邮件过滤器吃掉了😛

你知道为什么当前的 fg 和 bg 256 色值是基于 9 位而不是 8 位的吗? 附加位有什么用?

我认为它用于默认的 fg/bg 颜色(可以是深色或浅色),所以它实际上是 257 种颜色。

https://github.com/xtermjs/xterm.js/pull/756/files

你能给我attr的当前位布局吗?

我认为是这样的:

19+:     flags (see `FLAGS` enum)
18..18:  default fg flag
17..10:  256 fg
9..9:    default bg flag
8..1:    256 bg

你可以在旧的 PR https://github.com/xtermjs/xterm.js/pull/756/files 中看到我对真彩色的看法:

/**
 * Character data, the array's format is:
 * - string: The character.
 * - number: The width of the character.
 * - number: Flags that decorate the character.
 *
 *        truecolor fg
 *        |   inverse
 *        |   |   underline
 *        |   |   |
 *   0b 0 0 0 0 0 0 0
 *      |   |   |   |
 *      |   |   |   bold
 *      |   |   blink
 *      |   invisible
 *      truecolor bg
 *
 * - number: Foreground color. If default bit flag is set, color is the default
 *           (inherited from the DOM parent). If truecolor fg flag is true, this
 *           is a 24-bit color of the form 0xxRRGGBB, if not it's an xterm color
 *           code ranging from 0-255.
 *
 *        red
 *        |       blue
 *   0x 0 R R G G B B
 *      |     |
 *      |     green
 *      default color bit
 *
 * - number: Background color. The same as foreground color.
 */
export type CharData = [string, number, number, number, number];

所以在这里我有 2 个标志; 一种用于默认颜色(是否忽略所有颜色位)和一种用于真彩色(是否做 256 或 16 百万色)。

这可能会限制以后支持更多属性标志的可能性(因为 FLAGS 已经使用了 7 位),我们是否仍然缺少一些可能会出现的基本标志?

是的,我们想要一些额外的标志空间,例如 https://github.com/xtermjs/xterm.js/issues/580、https://github.com/xtermjs/xterm.js/issues/1145,我会说至少在可能的情况下保留 > 3 位。

不是 attr 本身内部的指针数据,可能还有另一个映射保存对 rgb 数据的引用? mapAttrIdxToRgb: { [idx: number]: RgbData

@Tyriar抱歉,有几天没上网,我担心垃圾邮件过滤器会吃掉电子邮件。 请问可以重发吗? :脸红:

为 attrs 存储使用了更聪明的查找数据结构。 最有希望的空间和搜索/插入运行时是树和作为更便宜的替代品的跳过列表。 理论上哈哈。 在实践中,两者都不能胜过我的简单数组搜索,这对我来说似乎很奇怪(某处代码中的错误?)
我在这里上传了一个测试文件https://gist.github.com/jerch/ff65f3fb4414ff8ac84a947b3a1eec58与数组与左倾红黑树,测试多达 10M 条目(这几乎是完整的寻址空间)。 与 LLRB 相比,该阵列仍然遥遥领先,但我怀疑收支平衡在 10M 左右。 在我的 7 年旧笔记本电脑上进行了测试,也许有人也可以对其进行测试,甚至更好——请指出 impl/tests 中的一些错误。

以下是一些结果(带有运行数字):

prefilled             time for inserting 1000 * 1000 (summed up, ms)
items                 array        LLRB
100-10000             3.5 - 5      ~13
100000                ~12          ~15
1000000               8            ~18
10000000              20-25        21-28

真正让我感到惊讶的是,线性数组搜索在较低区域根本没有显示任何增长,它在 ~4ms 时稳定在多达 10k 个条目(可能与缓存相关)。 10M 测试表明运行时间比预期的要差,这可能是由于内存分页造成的。 也许 JS 离带有 JIT 和所有 opts/deopts 的机器很远,但我仍然认为它们无法消除复杂性步骤(尽管 LLRB 似乎对单个 _n_ 很重,因此移动了 O( n) 与 O(logn) 向上)

顺便说一句,随机数据的差异甚至更糟。

我认为它用于默认的 fg/bg 颜色(可以是深色或浅色),所以它实际上是 257 种颜色。

所以这是从 8 种调色板颜色之一中区分SGR 39SGR 49吗?

不是 attr 本身内部的指针数据,可能还有另一个映射保存对 rgb 数据的引用? mapAttrIdxToRgb: { [idx: number]: RgbData

这将引入另一个间接使用额外的内存。 通过上面的测试,我还测试了始终在 attrs 中保存标志与将它们与 RGB 数据一起保存在存储中的区别。 由于 1M 条目的差异为 ~0.5ms,因此我不会采用这种复杂的 attrs 设置,而是在设置 RGB 后将标志复制到存储中。 我仍然会选择直接属性与指针之间的第 32 位区别,因为这将完全避免非 RGB 单元的存储。

此外,我认为 fg/bg 的默认 8 种调色板颜色目前在缓冲区中没有充分表示。 理论上终端应该支持以下颜色模式:

  1. SGR 39 + SGR 49 fg/bg 的默认颜色(可定制)
  2. SGR 30-37 + SGR 40-47 8 个用于 fg/bg 的低色调色板(可定制)
  3. SGR 90-97 + SGR 100-107 8 个用于 fg/bg 的高调色板(可定制)
  4. SGR 38;5;n + SGR 48;5;n 256 个用于 fg/bg 的索引调色板(可定制)
  5. SGR 38;2;r;g;b + SGR 48;2;r;g;b RGB 用于 fg/bg(不可定制)

选项 2.) 和 3.) 可以合并为一个字节(将它们视为单个 16 色 fg/bg 调色板),4.) 需要 2 个字节,5.) 最后将需要 6 个字节。 我们仍然需要一些位来指示颜色模式。
为了在缓冲区级别反映这一点,我们需要以下内容:

bits        for
2           fg color mode (0: default, 1: 16 palette, 2: 256, 3: RGB)
2           bg color mode (0: default, 1: 16 palette, 2: 256, 3: RGB)
8           fg color for 16 palette and 256
8           bg color for 16 palette and 256
10          flags (currently 7, 3 more reserved for future usage)
----
30

所以我们需要 32 位数字中的 30 位,留出 2 位用于其他目的。 第 32 位可以保存指针与直接属性标志,省略非 RGB 单元的存储。

此外,我建议将 attr 访问打包到一个方便的类中,以免将实现细节暴露给外部(参见上面的测试文件,有一个TextAttributes类的早期版本来实现这一点)。

抱歉,有几天没上网,我担心电子邮件被垃圾邮件过滤器吃掉了。 请问可以重发吗?

反感

哦顺便说一句,上面数组与 llrb 搜索的那些数字是废话 - 我认为优化器在 for 循环中做了一些奇怪的事情,它被破坏了。 通过稍微不同的测试设置,它清楚地显示了 O(n) 与 O(log n) 的增长更早(使用树预填充 1000 个元素已经更快)。

当前状态:

后:

一种相当简单的优化是将数组数组扁平化为单个数组。 即而不是BufferLine的 _N_ 列具有_data _N_ CharData单元格数组,其中每个CharData是 4 个数组,只需有一个_4*N_ 个元素的数组。这消除了 _N_ 个数组的对象开销。 它还改进了缓存局部性,所以它应该更快。 缺点是代码稍微复杂和丑陋,但似乎值得。

作为我之前评论的后续,似乎值得考虑在_data数组中为每个单元格使用可变数量的元素。 换句话说,一种有状态的表示。 位置的随机变化会更昂贵,但从一行开始的线性扫描可能非常快,特别是因为一个简单的数组针对缓存局部性进行了优化。 典型的顺序输出会很快,渲染也是如此。

除了减少空间之外,每个单元格元素数量可变的优点是增加了灵活性:额外的属性(例如 24 位颜色)、特定单元格或范围的注释、字形或
嵌套的 DOM 元素。

@PerBothner 谢谢你的想法! 是的,我已经使用指针算法测试了单一密集数组布局,它显示了最佳的内存利用率。 在调整大小时会出现问题,它基本上意味着重建整个内存块(复制)或快速复制到更大的块并重新对齐部分。 这是非常有经验的。 恕我直言,内存节省不合理(在上面列出的一些游乐场 PR 中进行了测试,与新的缓冲线实现相比,节省了大约 10%)。

关于您的第二条评论 - 我们已经讨论过,因为它会使处理包裹的行更容易。 现在我们决定对新的缓冲区布局采用 row X col 方法并首先完成。 我认为我们应该在执行回流调整大小后再次解决这个问题。

关于向缓冲区添加其他内容:我们目前在这里做大多数其他终端所做的事情 - 光标前进由wcwidth确定,这确保与 pty/termios 的数据布局方式保持兼容。 这基本上意味着我们在缓冲区级别只处理诸如代理对和组合字符之类的事情。 渲染器中的字符连接器可以应用任何其他“更高级别”连接规则(目前由 https://github.com/xtermjs/xterm-addon-ligatures 用于连字)。 我有一个 PR 在早期缓冲区级别也支持 unicode 字素,但我认为我们不能在那个阶段这样做,因为大多数 pty 后端没有这个概念(有没有任何概念?)并且我们最终会得到奇怪的字符集团. 真正的 BIDI 支持也是如此,我认为字素和 BIDI 最好在渲染器阶段完成,以保持光标/单元格的移动完整。

支持附加到单元格的 DOM 节点听起来很有趣,我喜欢这个想法。 目前这在直接方法中是不可能的,因为我们有不同的渲染器后端(DOM、画布 2D 和新的闪亮的 webgl 渲染器),我认为这仍然可以通过在本地不支持的地方放置一个覆盖来实现所有渲染器(只有 DOM 渲染器会可以直接做)。 我们需要某种缓冲区级别的 API 来宣布这些东西及其大小,并且渲染器可以处理这些脏活。 我认为我们应该用一个单独的问题来讨论/跟踪这个问题。

感谢您的详细回复。

_“在调整大小时出现问题,它基本上意味着重建整个内存块(复制)或快速复制到更大的块并重新对齐部分。”_

您的意思是:在调整大小时,我们必须复制 _4*N_ 个元素而不仅仅是 _N_ 个元素?

数组包含逻辑(展开)行的所有单元格可能是有意义的。 例如,假设一个 180 个字符的行和一个 80 列宽的终端。 在这种情况下,您可以有 3 个BufferLine实例共享相同的 _4*180_-element _data缓冲区,但每个BufferLine也将包含一个起始偏移量。

好吧,我把所有东西都放在一个由[cols] x [rows] x [needed single cell space]构建的大数组中。 所以它基本上仍然可以作为具有给定高度和宽度的“画布”。 这对于正常的输入流来说确实是内存高效且快速的,但是一旦调用insertCell / deleteCell (调整大小会这样做),动作发生位置后面的整个内存将不得不转移。 对于小回滚(<10k),这甚至不是问题,它确实是 >100k 行的一个展示器。
请注意,当前的类型化数组 impl 仍然需要进行这些转换,但毒性较小,因为它只需将内存内容向上移动到行尾。
我考虑过不同的布局来规避代价高昂的转变,节省无意义内存转变的主要领域实际上是将回滚与“热终端行”(最近的高达terminal.rows )分开,因为只有那些可以是通过光标跳转和插入/删除来改变。

通过多个缓冲行对象共享底层内存是解决包装问题的一个有趣想法。 还不确定这如何在不引入显式 ref 处理等的情况下可靠地工作。 在另一个版本中,我尝试使用显式内存处理来做所有事情,但引用计数器是一个真正的展示器,并且在 GC 领域感觉不对。 (有关原语,请参阅#1633)

编辑:顺便说一句,显式内存处理与当前的“每行内存”方法相当,我希望由于更好的缓存位置而获得更好的性能,猜想它被稍微多一点的 exp 吃掉了。 JS 抽象中的 mem 处理。

支持附加到单元格的 DOM 节点听起来很有趣,我喜欢这个想法。 目前这在直接方法中是不可能的,因为我们有不同的渲染器后端(DOM、画布 2D 和新的闪亮的 webgl 渲染器),我认为这仍然可以通过在本地不支持的地方放置一个覆盖来实现所有渲染器(只有 DOM 渲染器会可以直接做)。

这有点离题,但我看到我们最终将 DOM 节点与视口中的单元格关联,这些节点的作用类似于画布渲染层。 这样,消费者将能够使用 HTML 和 CSS 来“装饰”单元格,而无需进入画布 API。

数组包含逻辑(展开)行的所有单元格可能是有意义的。 例如,假设一个 180 个字符的行和一个 80 列宽的终端。 在这种情况下,您可以让 3 个 BufferLine 实例共享相同的 4*180 元素 _data 缓冲区,但每个 BufferLine 也将包含一个起始偏移量。

上面提到的回流计划在https://github.com/xtermjs/xterm.js/issues/622#issuecomment -375403572 中捕获,基本上我们想要有实际的解包缓冲区,然后在顶部管理用于快速访问任何给定行的新行(也优化了水平调整大小)。

使用密集数组方法可能是我们可以考虑的事情,但在管理此类数组中未包装的换行符以及从顶部修剪行时出现的混乱似乎不值得花费额外的开销回滚缓冲区。 无论如何,我认为在#791 完成并且我们正在查看#622 之前,我们不应该考虑此类更改。

使用 PR #1796,解析器获得类型化数组支持,这为进一步优化打开了大门,另一方面也为其他输入编码打开了大门。

现在我决定使用Uint16Array ,因为它可以轻松地与 JS 字符串来回转换。 这基本上将游戏限制为UCS2/UTF16,而当前版本的解析器也可以处理UTF32(不支持UTF8)。 基于类型化数组的终端缓冲区当前布局为 UTF32,UTF16 --> UTF32 转换在InputHandler.print 。 从这里开始,有几个可能的方向:

  • 制作所有UTF16,从而将终端缓冲区也变成UTF16
    是的,仍然没有确定在这里走哪条路,但是测试了几种缓冲区布局并得出结论,32 位数字提供了足够的空间来存储实际的字符代码 + wcwidth + 可能的组合溢出(处理方式完全不同),而 16 位不能这样做不牺牲宝贵的 charcode 位。 请注意,即使使用 UTF16 缓冲区,我们仍然必须进行 UTF32 转换,因为 wcwidth 适用于 unicode 代码点。 另请注意,基于 UTF16 的缓冲区将为较低的字符代码节省更多内存,事实上,高于 BMP 平面字符代码的情况很少发生。 这还需要一些调查。
  • 使解析器 UTF32
    那很简单,只需将所有类型化数组替换为 32 位变体。 缺点 - 必须事先完成 UTF16 到 UTF32 的转换,这意味着整个输入都会被转换,即使是永远不会由任何大于 255 的字符代码形成的转义序列。
  • 使wcwidth兼容 UTF16
    是的,如果事实证明 UTF16 更适合终端缓冲区,则应该这样做。

关于 UTF8:解析器目前无法处理原生 UTF8 序列,主要是因为中间字符与 C1 控制字符冲突。 此外,UTF8 需要适当的流处理和额外的中间状态,这很糟糕,恕我直言不应该添加到解析器中。 预先处理 UTF8 将得到更好的处理,并且可能具有到 UTF32 的转换权,以便更轻松地处理所有地方的代码点。

关于可能的 UTF8 输入编码和内部缓冲区布局,我做了一个粗略的测试。 为了排除画布渲染器对总运行时间的更大影响,我使用即将推出的 webgl 渲染器完成了这项工作。 使用我的ls -lR /usr/lib基准,我得到以下结果:

  • 当前主 + webgl 渲染器:
    grafik

  • 游乐场分支,适用 #1796、#1811 和 webgl 渲染器的部分:
    grafik

在进行解析和存储之前,操场分支会进行从 UTF8 到 UTF32 的早期转换(转换增加了大约 30 毫秒)。 加速主要通过输入流期间的 2 个热门函数获得, EscapeSequenceParser.parse (120 ms vs. 35 ms)和InputHandler.print (350 ms vs. 75 ms)。 通过节省.charCodeAt调用,两者都从类型化数组开关中受益匪浅。
我还将这些结果与 UTF16 中间类型数组进行了比较 - EscapeSequenceParser.parse稍快(约 25 毫秒)但InputHandler.print由于需要在wcwidth进行代理配对和代码点查找而落后
另请注意,我已经处于系统可以提供ls数据的极限(带有 SSD 的 i7) - 获得的加速增加了空闲时间而不是使运行速度更快。

概括:
恕我直言,我们可以得到的最快的输入处理是 UTF8 传输 + UTF32 的混合用于缓冲区表示。 虽然 UTF8 传输具有典型终端输入的最佳字节打包率,并且从 pty 到多层缓冲区(最高Terminal.write消除了无意义的转换,但基于 UTF32 的缓冲区可以非常快地存储数据。 后者的内存占用比 UTF16 略高,而 UTF16 由于更复杂的字符处理和更多的间接寻址而稍微慢一些。

结论:
我们现在应该使用基于 UTF32 的缓冲区布局。 我们还应该考虑切换到 UTF8 输入编码,但这仍然需要更多地考虑 API 的变化和对集成商的影响(似乎电子的 ipc 机制无法处理没有 BASE64 编码和 JSON 包装的二进制数据,这会抵消性能方面的努力)。

即将到来的真彩色支持的缓冲区布局:

目前基于类型化数组的缓冲区布局如下(一个单元格):

|    uint32_t    |    uint32_t    |    uint32_t    |
|      attrs     |    codepoint   |     wcwidth    |

其中attrs包含所有需要的标志 + 9 位基于 FG 和 BG 颜色。 codepoint使用 21 位(UTF32 最大为 0x10FFFF)+ 1 位表示组合字符和wcwidth 2 位(范围从 0-2)。

想法是将位重新排列为更好的打包率,为额外的 RGB 值腾出空间:

  • wcwidth放入codepoint未使用的高位
  • 将 attrs 分成 32 位的 FG 和 BG 组,将标志分配到未使用的位
|             uint32_t             |        uint32_t         |        uint32_t         |
|              content             |            FG           |            BG           |
| comb(1) wcwidth(2) codepoint(21) | flags(8) R(8) G(8) B(8) | flags(8) R(8) G(8) B(8) |

这种方法的优点是通过一个索引访问和最大值访问每个值相对便宜。 2 位操作(和/或 + 移位)。

内存占用对于当前变体来说是稳定的,但仍然很高,每个单元 12 字节。 这可以通过切换到 UTF16 和attr间接来牺牲一些运行时来进一步优化:

|        uint16_t        |              uint16_t               |
|    BMP codepoint(16)   | comb(1) wcwidth(2) attr pointer(13) |

现在我们减少到每个单元 4 个字节 + 一些空间给 attrs。 现在 attrs 也可以为其他单元格回收。 耶任务完成! - 嗯,一秒钟...

比较内存占用,第二种方法明显胜出。 对于运行时间而言并非如此,三个主要因素会大大增加运行时间:

  • attr 间接
    attr 指针需要对另一个数据容器进行一次额外的内存查找。
  • 属性匹配
    为了使用第二种方法真正节省空间,给定的属性必须与已经保存的属性相匹配。 这是一个繁琐的操作,通过简单地查看所有现有值的直接方法是在 O(n) 中保存 n 个属性,与使用 O(1) 的 32 位方法中的索引访问。 另外,对于保存的几个元素,树的运行时间更糟(使用我的 RB 树实现在大约 100 个条目左右得到回报)。
  • UTF16 代理配对
    对于 16 位类型化数组,我们必须将代码点降级为 UTF16,这也引入了运行时惩罚(如上面的评论所述)。 请注意,几乎不会出现高于 BMP 的代码点,但单独检查代码点是否会形成代理对会增加约 50 毫秒。

第二种方法的性感之处在于额外的内存节省。 因此,我使用带有修改后的BufferLine实现的操场分支(请参阅上面的评论)对其进行了测试:

grafik

是的,我们回到了在解析器中更改为 UTF8 + 类型数组之前的起点。 内存使用量从 ~ 1.5 MB 下降到 ~ 0.7 MB(具有 87 个单元格和 1000 行回滚的演示应用程序)。

从这里开始,它是节省内存与速度的问题。 由于我们已经通过从 js 数组切换到类型化数组节省了大量内存(C++ 堆从 ~ 5.6 MB 减少到 ~ 1.5 MB,切断了有毒的 JS 堆行为和 GC),我认为我们应该在这里使用更快的变体。 一旦内存使用再次成为紧迫问题,我们仍然可以切换到更紧凑的缓冲区布局,如此处的第二种方法所述。

我同意,只要不担心内存消耗,我们就可以优化速度。 我还想尽可能避免间接,因为它使代码更难阅读和维护。 我们的代码库中已经有很多概念和调整,使人们(包括我 😅)难以遵循代码流 - 引入更多这些概念和调整总是有充分理由的。 IMO 节省另一兆字节的内存并不能证明这一点。

尽管如此,我真的很喜欢阅读和从您的练习中学习,感谢您如此详细地分享!

@mofux是的,确实如此 - 代码复杂性要高得多(UTF16 代理
并且由于 32 位布局主要是平面内存(只有组合字符需要间接),有更多的优化可能(也是 #1811 的一部分,尚未针对渲染器进行测试)。

间接指向 attr 对象有一大优点:它的可扩展性要强得多。 您可以添加注释、字形或自定义绘制规则。 您可以以更简洁、更有效的方式存储链接信息。 也许定义一个ICellPainter接口,它知道如何呈现其单元格,并且您还可以将自定义属性挂在上面。

一种想法是为每个 BufferLine 使用两个数组:一个 Uint32Array 和 ICellPainter 数组,每个单元格各有一个元素。 当前的 ICellPainter 是解析器状态的一个属性,因此只要颜色/属性状态不改变,您只需重用相同的 ICellPainter。 如果需要向单元格添加特殊属性,请首先克隆 ICellPainter(如果它可能被共享)。

您可以为最常见的颜色/属性组合预先分配 ICellPainter - 至少有一个与默认颜色/属性相对应的唯一对象。

样式更改(例如更改默认前景色/背景色)可以通过更新相应的 ICellPainter 实例来实现,而无需更新每个单元格。

有可能的优化:例如,对单宽和双宽字符(或零宽字符)使用不同的 ICellPainter 实例。 (这在每个 Uint32Array 元素中节省了 2 位。) Uint32Array 中有 11 个可用的属性位(如果我们针对 BMP 字符进行优化,则更多)。 这些可用于编码最常见/有用的颜色/属性组合,可用于索引最常见的 ICellPainter 实例。 如果是这样,则可以延迟分配 ICellPainter 数组 - 即,仅当行中的某些单元格需要“不太常见”的 ICellPainter 时。

还可以删除非 BMP 字符的 _combined 数组,并将它们存储在 ICellPainter 中。 (这需要每个非 BMP 字符的唯一 ICellPainter,因此这里需要权衡。)

@PerBothner是的,间接

关于我在几个测试台中尝试过的内容的一些说明:

  • 单元格字符串内容
    我自己来自 C++,我试图像在 C++ 中一样看待这个问题,所以我从内容的指针开始。 这是一个简单的字符串指针,但大部分时间指向单个字符字符串。 多么浪费。 因此,我的第一个优化是通过直接保存代码点而不是地址来摆脱字符串抽象(在 C/C++ 中比在 JS 中容易得多)。 这几乎使读/写访问增加了一倍,同时为每个单元节省了 12 个字节(8 个字节指针 + 4 个字符串字节,64 位和 32 位 wchar_t)。 旁注 - 这里一半的速度提升与缓存相关(由于随机字符串位置导致缓存未命中)。 这在我合并单元格内容的解决方法中变得清晰 - 当codepoint设置了合并位时我索引到的一块内存(由于更好的缓存位置,这里的访问速度更快,用 valgrind 测试)。 由于需要字符串到数字的转换(尽管仍然更快),转交给 JS 的速度提升并不是那么大,但是内存节省甚至更大(猜测是由于 JS 类型的一些额外管理空间)。 问题是全局StringStorage与显式内存管理相结合,这是 JS 中的一个大反模式。 对此的快速修复是_combined对象,它将清理委托给 GC。 它仍然是一个变化的主题,顺便说一句,它旨在存储与单元格相关的任意字符串内容(考虑到字素,但我们不会很快看到它们,因为它们不受任何后端支持)。 所以这是一个单元一个单元地存储附加字符串内容的地方。
  • 属性
    使用 attrs 我开始“思考大” - 对所有终端实例中曾经使用过的所有 attrs 使用全局AttributeStorage (参见 https://github.com/jerch/xterm.js/tree/AttributeStorage)。 内存方面这很不错,主要是因为即使支持真彩色,ppl 也只使用一小组属性。 性能不太好 - 主要是由于引用计数(每个单元格必须两次查看此外部内存)和 attr 匹配。 当我试图将 ref 的东西用于 JS 时,它感觉是错误的——我按下了“停止”按钮。 事实证明,通过切换到类型化数组,我们已经节省了大量内存和 GC 调用,因此成本稍高的平面内存布局可以在这里获得速度优势。
    我在 yday 测试的(最后一条评论)是行级别的第二个类型数组,用于 attrs 与来自https://github.com/jerch/xterm.js/tree/AttributeStorage的树的匹配(非常像你的 ICellPainter 想法)。 好吧,结果并不乐观,因此我现在倾向于使用平面 32 位布局。

现在这个扁平的 32 位布局被证明是针对常见的东西进行了优化,并且不可能有不常见的附加功能。 真的。 好吧,我们仍然有标记(不习惯它们,所以我现在无法说出它们的功能),而且是的 - 缓冲区中仍然有空闲位(这对于未来的需求来说是一件好事,例如我们可以将它们用作特殊处理标志等)。

Tbh 对我来说很遗憾带有 attrs 存储的 16 位布局表现如此糟糕,将内存使用量减半仍然是一个大问题(尤其是当 ppl 开始使用滚动行 >10k 时),但运行时损失和代码复杂度超过更高的内存需要 atm imho。

你能详细说明一下 ICellPainter 的想法吗? 也许到目前为止我错过了一些关键功能。

我对 DomTerm 的目标是启用和鼓励更丰富的交互,就像传统终端模拟器所支持的那样。 使用 Web 技术可以实现许多有趣的事情,因此只专注于成为一个快速的传统终端模拟器是一种耻辱。 特别是因为 xterm.js 的许多用例(例如 IDE 的 REPL)确实可以从超越简单的文本中受益。 Xterm.js做得很好的速度侧(被人抱怨速度?),但它没有这样做很好的功能(人们都在抱怨缺少真彩和嵌入式图形,例如)。 我认为更多地关注灵活性并稍微关注性能可能是值得的。

_“你能详细说明一下 ICellPainter 的想法吗?”_

通常,ICellPainter 封装了来自 Uint32Array 的字符代码/值

interface ICellPainter {
    drawOnCanvas(ctx: CanvasRenderingContext2D, code: number, x: number, y: number);
    // transitional - to avoid allocating IGlyphIdentifier we should replace
    //  uses by pair of ICellPainter and code.  Also, a painter may do custom rendering,
    // such that there is no 'code' or IGlyphIdentifier.
    asGlyph(code: number): IGlyphIdentifier;
    width(): number; // in pixels for flexibility?
    height(): number;
    clone(): ICellPainter;
}

可以通过多种方式将单元格映射到 ICellPainter。 很明显,每个 BufferLine 都有一个 ICellPainter 数组,但这需要每个单元格有一个(至少)8 字节的指针。 一种可能性是将 _combined 数组与 ICellPainter 数组组合:如果设置了 IS_COMBINED_BIT_MASK,则 ICellPainter 还包括组合字符串。 另一种可能的优化是使用 Uint32Array 中的可用位作为数组的索引:这会增加一些额外的复杂性和间接性,但可以节省空间。

我想鼓励我们检查一下我们是否可以像 monaco-editor 那样做(我认为他们找到了一种非常聪明和高效的方式)。 它们允许您创建decorations ,而不是将此类信息存储在缓冲区中。 您为行/列范围创建一个装饰,它将坚持该范围:

// decorations are buffer-dependant (we need to know which buffer to decorate)
const decoration = buffer.createDecoration({
  type: 'link',
  data: 'https://www.google.com',
  range: { startRow: 2, startColumn: 5, endRow: 2, endColumn: 25 }
});

稍后渲染器可以拾取这些装饰并绘制它们。

请查看这个小例子,它展示了 monaco-editor api 的样子:
https://microsoft.github.io/monaco-editor/playground.html#interacting -with-the-editor-line-and-inline-decorations

对于在终端 monaco 内渲染图片之类的事情,使用可以在此处的示例中看到的视图区域的概念(以及其他概念):
https://microsoft.github.io/monaco-editor/playground.html#interacting -with-the-editor-listening-to-mouse-events

@PerBothner Thx 用于澄清和

我们最终计划在未来将输入链+缓冲区移动到一个 webworker 中。 因此缓冲区旨在在抽象级别上运行,我们不能在那里使用任何渲染/表示相关的东西,例如像素度量或任何 DOM 节点。 由于 DomTerm 是高度可定制的,我看到您对此的需求,但我认为我们应该使用增强的内部标记 API 来做到这一点,并且可以从 monaco/vscode 在这里学习(谢谢@mofux 的指针)。
我真的想让核心缓冲区没有不常见的东西,也许我们应该用一个新问题讨论可能的标记策略?

我对 16 位布局测试结果的结果仍然不满意。 由于尚未做出最终决定(我们不会在 3.11 之前看到任何此类内容),我将继续对其进行一些更改(与 32 位变体相比,它仍然是对我来说更有趣的解决方案)进行测试。

|             uint32_t             |        uint32_t         |        uint32_t         |
|              content             |            FG           |            BG           |
| comb(1) wcwidth(2) codepoint(21) | flags(8) R(8) G(8) B(8) | flags(8) R(8) G(8) B(8) |

我还认为我们应该从接近这个开始,我们可以稍后探索其他选项,但这可能是最容易启动和运行的。 属性间接性肯定对 IMO 有承诺,因为终端会话中通常没有那么多不同的属性。

我想鼓励我们检查一下我们是否可以像 monaco-editor 那样做(我认为他们找到了一种非常聪明和高效的方式)。 它们允许您创建装饰,而不是将此类信息存储在缓冲区中。 您为行/列范围创建一个装饰,它将坚持该范围:

像这样的事情是我希望看到的东西。 我在这些方面的一个想法是允许嵌入器将 DOM 元素附加到范围,以便能够绘制自定义的东西。 目前我能想到三件事,我想用这个来完成:

  • 以这种方式绘制链接下划线(将显着简化它们的绘制方式)
  • 允许在行上添加标记,例如 * 或其他东西
  • 允许行“闪烁”以指示发生了某些事情

所有这些都可以通过覆盖来实现,它是一种非常平易近人的 API(暴露 DOM 节点),并且无论渲染器类型如何都可以工作。

我不确定我们是否想进入允许嵌入器更改背景和前景色的绘制方式的业务。


@jerch我将把它放在 3.11.0 里程碑上,因为当我们删除计划的 JS 数组实现时,我认为这个问题已经完成。 https://github.com/xtermjs/xterm.js/pull/1796也计划在那时合并,但这个问题一直是为了改进缓冲区的内存布局。

此外,后面的很多讨论可能最好在https://github.com/xtermjs/xterm.js/issues/484https://github.com/xtermjs/xterm.js/issues/1852 结束(创建是因为没有装饰问题)。

@Tyriar Woot - 终于关门了 :sweat_smile:

🎉 🕺 🍾

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

parisk picture parisk  ·  3评论

circuitry2 picture circuitry2  ·  4评论

johnpoth picture johnpoth  ·  3评论

albinekb picture albinekb  ·  4评论

zhangjie2012 picture zhangjie2012  ·  3评论