转换流水线的性能优化
终端录制数据在多个处理阶段都存在大量重复信息。 一次屏幕绘制往往被切分成多次琐碎的 PTY(伪终端)输出,相邻帧之间绝大多数字符单元格保持不变,动画中相同的行会静止数秒,视频转换也会反复请求相同画面的栅格化。 console2svg 在调用后续渲染引擎之前,在各转换阶段对这些重复进行了彻底削减。
规范事件到达粒度的输出合并
Section titled “规范事件到达粒度的输出合并”系统绝不会将从 PTY 读取的单次字节序列直接记录为单一录制事件。 而是通过 输出合并(Coalescing:在指定时间窗口内将零散到达的 I/O 汇聚为一个批次的处理)预先压缩录制事件的数量。
默认的合并窗口为视频单帧时长的四分之一。 为了保证交互的即时性,该窗口不会低于 2 毫秒;为了避免可感知的输入延迟,其上限被限制在 20 毫秒以内。 即使是输出大量细碎转义序列来构建界面的 TUI(文本用户界面)工具,经过该预处理后,ANSI 解析器需要处理的事件边界数量也会大幅下降。
基于写时复制的屏幕快照共享
Section titled “基于写时复制的屏幕快照共享”如果在保存动画的每一帧时都对整屏进行深拷贝,内存消耗将随着“帧数 × 行数 × 列数”的乘积发生爆炸。
为了消除这一浪费,ScreenBuffer 引入了 写时复制(Copy-on-Write, CoW:写入发生前共享内存,发生修改时才复制相应区域的机制)。
生成快照时,系统仅新分配用于容纳各行引用的外层数组,而行实体的 ScreenCell[] 则与原始屏幕缓冲区共享。
随后,随着终端仿真的推进,仅在特定行发生字符修改的瞬间,系统才会将该行的单元格数组复制到新内存中。
这种行共享结构在帧间比对时同样极为高效。 如果两个快照引用了相同的行数组实例,即可直接判定为“完全一致”,无需逐个比对该行的所有单元格。 逐单元格的数据比对被严格限制在数组引用不同的行上。
基于差分更新的高速视觉签名计算
Section titled “基于差分更新的高速视觉签名计算”系统使用 视觉签名 作为判断屏幕或行是否相同的基准。 每个单元格的签名由显示字符、字符样式和全角字符标志综合计算得出。 哈希算法采用了极速的 XxHash3,并且预先计算并缓存了 ASCII 字符(0~127)的签名表。
每一行都维护一个由包含列位置信息的单元格签名集合构成的 64 位行签名。 在开启签名跟踪后,修改一个单元格时无需重新扫描整行。 只需通过异或(XOR)运算移除修改前单元格的值,再通过异或运算加入新单元格的值,即可完成行签名的增量更新。
全屏签名分为两种:
不包含光标的 GetContentSignature 用于判定动画正文是否需要保留新帧;
包含光标位置和可见状态的 GetVisualSignature 则用于视频采样时精确判定画面是否完全一致。
字符串与样式对象的内存复用
Section titled “字符串与样式对象的内存复用”终端屏幕上,相同字符和相同样式属性会在大范围内反复出现。
console2svg 预先生成了代表 ASCII 128 个字符的单字符 string 静态数组,在单元格更新文字时完全消除了调用 char.ToString() 带来的堆分配。
表示文字属性的 CellStyle 实例同样在缓冲区内共享。
在连续单元格保持相同样式的常见场景中,直接复用上一个单元格的引用;而在屏幕较远位置再次出现相同样式时,则从上限为 256 项的内部缓存中获取引用。
即便是面对连续涌入的全新 24 位 RGB 颜色,缓存达到上限时也会自动重建,以防止内存无限制增长。
SVG 元素与文字布局的紧凑合并
Section titled “SVG 元素与文字布局的紧凑合并”在输出 SVG 时,console2svg 坚决避免为每个单元格生成独立元素的低效标记。
水平方向上连续且具有相同背景色的单元格会被合并为一个矩形(<rect>)。
类似地,连续具有相同前景样式的文字会被连接为一个 <text> 元素。
行内的空格仅在连接后续文字时保留,行末的空白单元格则在输出时直接剔除。
当出现全角字符或图形字符时,文字连接会在该边界处适度截断,以防止字符排版发生错位。
<!-- 优化前(原始逐单元格绘制):每个单元格输出独立的 rect 与 text(共 6 个元素,属性冗余) --><rect x="0" y="0" width="8.4" height="18" fill="#1e1e2e"/><text x="0" y="14" fill="#89b4fa">g</text><rect x="8.4" y="0" width="8.4" height="18" fill="#1e1e2e"/><text x="8.4" y="14" fill="#89b4fa">i</text><rect x="16.8" y="0" width="8.4" height="18" fill="#1e1e2e"/><text x="16.8" y="14" fill="#89b4fa">t</text>
<!-- 优化后(console2svg 合并处理):背景与文字各自合并为单一元素 --><rect x="0" y="0" width="25.2" height="18" fill="#1e1e2e"/><text x="0" y="14" fill="#89b4fa">git</text>此外,制表符(Box-drawing 字符)在输出为 SVG 之前会被分解为水平与垂直线段几何体,将相同颜色与线宽且连续相接的线段合并后,汇集为一个 <path> 数据中。
即使是全屏布满框线的复杂 TUI 工具,数百个独立的矩形或字体字符也会被压缩为寥寥数条连续路径。
<!-- 优化前:为边框交叉点和各个线段输出大量微小碎片 --><!-- 优化后:将相同样式的外框与分割线整合为单一路径 --><path d="M 10 10 H 630 V 200 H 10 Z M 10 40 H 630" stroke="#6c7086" stroke-width="1" fill="none"/>动画行的去重与差分化
Section titled “动画行的去重与差分化”动画 SVG 绝不会输出所有保留帧的全部完整画面。
PrepareAnimatedRows 会扫描所有帧的可见行以构建 行目录,内容相同的行在 <defs> 中仅定义一次。
此外,针对仅修改行中少数字符的打字或状态更新,系统会应用 行差分,通过引用前一行的定义并仅覆盖被修改的列范围。 通过将差分范围限制在 16 列以下且不超过整行宽度的四分之一,并将引用深度控制在 4 层以内,既显著削减了输出体积,又避免了 SVG 渲染器的解析性能瓶颈。
以在 80 列 × 24 行终端中输入命令的 100 帧动画为例:
| 处理方案 | 输出的行数据量 | 预估生成文件大小 |
|---|---|---|
| 朴素全屏展开(输出各帧的全部行) | 2,400 行(完整的 80×24×100 数据) | 约 3.2 MB |
| 行目录化(唯一化行定义并引用) | 约 120 个唯一的 <g> 定义 | 约 180 KB |
| 行目录化 + 行差分(局部覆盖打字差分) | 约 35 个完整定义 + 约 40 个微小差分 | 约 85 KB |
通过这种多层级的去重策略,在丝毫不损失渲染画质的前提下,文件体积缩减了 90% 以上。
视频转换中的两级缓存与管道流式传输
Section titled “视频转换中的两级缓存与管道流式传输”在以固定帧率输出视频时,单一终端仿真器随着采样时间的推移单向向前推进,并在每一步获取画面的视觉签名。
根据该签名,系统最多缓存 128 条静态 SVG 字符串。
连续出现相同画面时,系统跳过 XML 字符串的构建,直接返回相同的 string 实例。
视频转换器进而以该 string 对象的引用作为键,最多缓存 16 项 PNG 栅格化结果。
这一 两级缓存 机制使画面静止区间的 SVG 重建与 PNG 重复栅格化被完全跳过。
生成的 PNG 图像帧无需在磁盘上写入临时文件,而是直接流式推送到正在运行的 ffmpeg 进程的标准输入(image2pipe)。
栅格化处理根据 CPU 核心数最多由 8 个线程并行执行,并通过一个具有容量限制的 FIFO 队列进行保序传输,确保图像帧按严格的时序顺序交付给编码器。