作者 | Aram Drevekenin

译者 | 马超

出品 | CSDN(ID:CSDNnews)

Zellij是一款非常优秀的终端工作区和多路复用器(类似于tmux和screen),由于使用Rust语言开发,因此与Zellij与WebAssembly原生兼容。笔者注意到在过去的几个月中,Zellij的开发者一直在对Zellij进行优化与排坑,他们发布了一些很多意义的技术博客来记录整个优化过程。博客中展示了一些非常值得总结和重视的问题,通过他们的分享我们可以看到,Zellij的开发者们提出了很多创造性的解决方案。通过两个主要的技术提升点,他们大幅调优了Zellij在大量显示刷新场景下的性能。下面我把相关技术博客为大家进行解读。

由于Zellij 是一个非常庞大的应用程序,其实际代码非常复杂,细抠所有技术细节,可能会把读者完全绕晕。因此本文使用的代码示例都是简化后的版本,仅用于讨论问题的示例。

问题一的描述

Zellij 是一个终端多路复用器,它允许用户创建多个“选项卡”和“窗口”,Zellij 会为每个终端窗口进行状态保持,其中状态信息包括文本、样式以及窗口内光标位置等要素。这种设计可以方便用户每次连接到现有会话时都保证用户体验的一致性,并可以支持用户在内部选项卡之间自由切换。不过状态在之前版本中 Zellij 窗口中显示大量数据时,性能问题会非常明显。例如,cat输入一个非常大的文件,这时Zellij会比裸终端仿真器慢得多,甚至比与其他终端多路复用器也慢。下面笔者将带着大家共同深入研究这个问题。

问题一巨大流量的冲击

Zellij使用多线程架构,PTY线程和Screen渲染线程执行特定任务并通过MPSC 通道互相通信。其中PTY线程查询PTY,也就是用户屏幕上的输入、输出,并将原始数据发送到Screen线程。该线程解析数据并建立终端窗口的内部状态。PTY线程会将终端的状态呈现到用户屏幕上,并向Screen线程发送渲染请求。

PTY 线程不断轮询 PTY,以查看它在异步数据接收的while循环中是否有新数据。如果没有接收到数据,则休眠一段固定的时间。简单的讲PTY线程会在以下任一情况下发生时发送数据:

1.PTY 读取缓冲区中没有更多数据

2.最后一条屏幕刷新指令已经被执行了30毫秒或更长时间。

第二种设计是出于用户体验的原因。这样,如果 PTY 有大量数据流,用户将在屏幕上实时看到这些数据的更新。

让我们看一下代码:

task::spawn({async move {// TerminalBytes是异步数据流     let mut terminal_bytes = TerminalBytes::new(pid);let mut last_render = Instant::now();let mut pending_render = false;let max_render_pause = Duration::from_millis(30);while let Some(bytes) = terminal_bytes.next().await {let receiving_data = !bytes.is_emPTY();if receiving_data {send_data_to_screen(bytes);pending_render = true;}if pending_render && last_render.elapsed() > max_render_pause {send_render_to_screen();last_render = Instant::now();pending_render = false;}if !receiving_data {task::sleep(max_render_pause).await;}}}
})

解决问题

为了测试这个大规模显示流程的性能,我们cat了一个 2,000,000 行的文件,并使用hyperfine基准测试工具,并使用--show-output参数来测试标准输出场景,并使用tmux进行对比。

hyperfine --show-output "cat /tmp/bigfile"在 tmux 内运行的结果:(窗口大小:59 行,104 列)

Time (mean ± σ):      5.593 s ±  0.055 s    [User: 1.3 ms, System: 2260.6 ms]
Range (min … max):    5.526 s …  5.678 s    10 runs

hyperfine --show-output "cat /tmp/bigfile"在 Zellij 内部运行的结果:(窗口大小:59 行,104 列)

Time (mean ± σ):     19.175 s ±  0.347 s    [User: 4.5 ms, System: 2754.7 ms]
Range (min … max):   18.647 s … 19.803 s    10 runs

可以看到优化前tmux的性能几乎是Zellij的8倍多。

第一个问题点:MPSC通道溢出

第一个性能问题是MPSC 通道的溢出,由于 PTY 线程和屏幕线程之间没有同步控制,PTY进程发送数据的速度要远比Screen线程处理数据的速度要快很多。PTY和SCREEN之间的不平衡将在以下几个方面影响性能:

1.通道缓冲区空间不断增长,占用越来越多的内存

2.屏幕线程渲染的次数远比合理值要高,因为屏幕线程需要越来越多的时间来处理队列中的消息。

问题一的解决之道,将MPSC转换为有界通道

这个紧迫问题的解决方案是限制通道的缓冲区大小,并由此在两个线程之间创建同步关系。为此开发者们放弃了MPSC而选择了有界同步通道crossbeam,crossbeam提供了一个非常有用的宏select!。此外,开发者们还删除了自定义的后台轮询的异步流实现,转而使用 async_stdFile以获得“异步 i/o”效果。

我们来看看代码中的变化:

task::spawn({async move {let render_pause = Duration::from_millis(30);let mut render_deadline = None;let mut buf = [0u8; 65536];let mut async_reader = AsyncFileReader::new(pid);    // 用async_std实现异步IO//以下是异步实现在deadline时进行特殊处理loop {match deadline_read(&mut async_reader, render_deadline, &mut buf).await {ReadResult::Ok(0) | ReadResult::Err(_) => break, // EOF or error                ReadResult::Timeout => {async_send_render_to_screen(bytes).await;render_deadline = None;}ReadResult::Ok(n_bytes) => {let bytes = &buf[..n_bytes];async_send_data_to_screen(bytes).await;render_deadline.get_or_insert(Instant::now() + render_pause);}}}}
})

所以这或多或少是事后的样子:

性能改进

让我们回到最初的性能测试。

以下是运行时的数字hyperfine --show-output "cat /tmp/bigfile"(窗格大小:59 行,104 列):

# Zellij before this fix
Time (mean ± σ):     19.175 s ±  0.347 s    [User: 4.5 ms, System: 2754.7 ms]
Range (min … max):   18.647 s … 19.803 s    10 runs
# Zellij after this fix
Time (mean ± σ):      9.658 s ±  0.095 s    [User: 2.2 ms, System: 2426.2 ms]
Range (min … max):    9.433 s …  9.761 s    10 runs
# Tmux
Time (mean ± σ):      5.593 s ±  0.055 s    [User: 1.3 ms, System: 2260.6 ms]
Range (min … max):    5.526 s …  5.678 s    10 runs

虽然有了近一倍的性能提升,但从 Tmux 的数据来看,Zellij仍然可以做得更好。

第二个问题,提高渲染和数据解析的性能

接下来开发者们又将管道绑定到屏幕线程,如果提高屏幕线程中两个相关作业的性能,能够使整个过程运行得更快:解析数据并将其渲染到用户终端。屏幕线程的数据解析部分的作用是将ANSI/VT等控制指令(如\r\n这样的回车或者换行符)转化为Zellij可以控制的数据结构。

以下是这些数据结构的相关部分:

struct Grid {viewport: Vec,cursor: Cursor,width: usize,height: usize,
}struct Row {columns: Vec,
}struct Cursor {x: usize,y: usize
}#[derive(Clone, Copy)]struct TerminalCharacter {character: char,styles: CharacterStyles
}

预分配内存

解析器执行最频繁的操作就是给一行文字内添加显示的字符。特别是在行尾添加字符。这个动作主要涉及将那些TerminalCharacters推入到列向量中。每个推送都涉及一个从堆上分配一段内存空间,这个内存分配的操作是非常耗时的,这点笔者在之前的博客《一行无用的枚举代码,却让Rust性能提升10%》中有过介绍。因此可以通过在每次创建行或调整终端窗口大小时预分配内存,来获得性能上的提升。所以开发者们从改变 Row(行)类的构造函数开始:

impl Row {pub fn new() -> Self {Row {columns: Vec::new(),}}}
}

对此:

impl Row {pub fn new(width: usize) -> Self {Row {columns: Vec::with_capacity(width),//通过指定capacity来预分配一段内存}}}
}

缓存字符宽度

我们知道一些特殊的字符比如中文全角字符会比普通的英文字符占用更多的空间。这方面Zellij 又引入了unicode-width crate 来计算每个字符的宽度。

在Zellij给一行内容中添加字符时,终端仿真器需要知道该行的当前宽度,以便决定是否应该将字符换行到下一行。所以它需要不断地查看和累加行中前一个字符的宽度。因为需要找到一个计算字符宽度的方法。

代码如下:

#[derive(Clone, Copy)]struct TerminalCharacter {character: char,styles: CharacterStyles
}impl Row {pub fn width(&self) -> usize {let mut width = 0;for terminal_character in self.columns.iter() {width += terminal_character.character.width();}width}
}

加入缓存之后速度变得更快:

#[derive(Clone, Copy)]struct TerminalCharacter {character: char,styles: CharacterStyles,width: usize,
}impl Row {pub fn width(&self) -> usize {let mut width = 0;for terminal_character in self.columns.iter() {width += terminal_character.width;}width}
}

渲染速度提升

Screen 线程的渲染部分本质上执行与数据解析部分反向操作。它获取由上述数据结构表示的每个窗口状态,并将其转换为 ANSI/VT 的控制指令,以发送到操作系统自身的终端仿真器并对其解释执行。也就是说对于普通字符就进行显示渲染,如果是控制符则发给系统shell执行。

fn render(&mut self) -> String {let mut vte_output = String::new();let mut character_styles = CharacterStyles::new();let x = self.get_x();let y = self.get_y();for (line_index, line) in grid.viewport.iter().enumerate() {vte_output.push_str(// goto row/col and reset styles            &format!("\u{1b}[{};{}H\u{1b}[m", y + line_index + 1, x + 1));for (col, t_character) in line.iter().enumerate() {let styles_diff = character_styles.update_and_return_diff(&t_character.styles);if let Some(new_styles) = styles_diff {vte_output.push_str(&new_styles);                // 如果不是一类字符,则在此替换处理}vte_output.push(t_character.character);}character_styles.clear();}vte_output
}

我们知道STDOUT写入是一种非常耗费性能的操作,为此开发者们再次寄出缓冲区这个神器。该缓冲区主要跟踪最新与次新渲染请求的差异,最终只将缓冲区内这些不同的差异部分进行渲染。

代码如下:

#[derive(Debug)]pub struct CharacterChunk {pub terminal_characters: Vec,pub x: usize,pub y: usize,
}#[derive(Clone, Debug)]pub struct OutputBuffer {changed_lines: Vec, // line index    should_update_all_lines: bool,
}impl OutputBuffer {pub fn update_line(&mut self, line_index: usize) {self.changed_lines.push(line_index);}pub fn clear(&mut self) {self.changed_lines.clear();}pub fn changed_chunks_in_viewport(&self,viewport: &[Row],) -> Vec{let mut line_changes = self.changed_lines.to_vec();line_changes.sort_unstable();line_changes.dedup();let mut changed_chunks = Vec::with_capacity(line_changes.len());for line_index in line_changes {let mut terminal_characters: Vec= viewport.get(line_index).unwrap().columns.iter().copied().collect();changed_chunks.push(CharacterChunk {x: 0,y: line_index,terminal_characters,});}changed_chunks}
}}

我们看到这个实现最小修改单位是行,还有进一步优化为仅修改行内部分变动字符的方案,这种方案大幅虽然增加了复杂性,不过也带来了非常显着的性能提升。

以下为对比测试结果:

hyperfine --show-output "cat /tmp/bigfile"修复后运行结果:(窗格大小:59 行,104 列)

# Zellij before all fixes
Time (mean ± σ):     19.175 s ±  0.347 s    [User: 4.5 ms, System: 2754.7 ms]
Range (min … max):   18.647 s … 19.803 s    10 runs
# Zellij after the first fix
Time (mean ± σ):      9.658 s ±  0.095 s    [User: 2.2 ms, System: 2426.2 ms]
Range (min … max):    9.433 s …  9.761 s    10 runs
# Zellij after the second fix (includes both fixes)
Time (mean ± σ):      5.270 s ±  0.027 s    [User: 2.6 ms, System: 2388.7 ms]
Range (min … max):    5.220 s …  5.299 s    10 runs
# Tmux
Time (mean ± σ):      5.593 s ±  0.055 s    [User: 1.3 ms, System: 2260.6 ms]
Range (min … max):    5.526 s …  5.678 s    10 runs

通过这一系列的改进之后,Zellij在cat一个大文件时的性能已经可以和Tmux比肩了。

结论

总结一下Zellij通过优化通道双方数据处理的不平衡关系,加入缓冲并优化渲染粒度等精彩的方式大幅提升了Zellij多路终端复用器的性能,很多优化的思路非常值得开发者们借鉴。

原文链接:https://www.poor.dev/blog/performance/

声明:本文由CSDN翻译,转载请注明来源。

如何提高 Rust 程序的性能?相关推荐

  1. Linux中使用异步 I/O 大大提高应用程序的性能

    Linux中使用异步 I/O 大大提高应用程序的性能 原文: http://www.ibm.com/developerworks/cn/linux/l-async/ AIO 简介 Linux 异步 I ...

  2. (转)使用异步 I/O 大大提高应用程序的性能

    AIO 简介 Linux 异步 I/O 是 Linux 内核中提供的一个相当新的增强.它是 2.6 版本内核的一个标准特性,但是我们在 2.4 版本内核的补丁中也可以找到它.AIO 背后的基本思想是允 ...

  3. 使用异步 I/O 大大提高应用程序的性能

    Linux® 中最常用的输入/输出(I/O)模型是同步 I/O.在这个模型中,当请求发出之后,应用程序就会阻塞,直到请求满足为止.这是很好的一种解决方案,因为调用应用程序在等待 I/O 请求完成时不需 ...

  4. [转载] 使用异步 I/O 大大提高应用程序的性能

    原文: http://www.ibm.com/developerworks/cn/linux/l-async/ Linux® 中最常用的输入/输出(I/O)模型是同步 I/O.在这个模型中,当请求发出 ...

  5. 使用异步 I/O 大大提高应用程序的性能(来自IBM)

    AIO简介 Linux 异步 I/O 是 Linux 内核中提供的一个相当新的增强.它是 2.6 版本内核的一个标准特性,但是我们在 2.4 版本内核的补丁中也可以找到它.AIO 背后的基本思想是允许 ...

  6. 使用内存映射文件来提高你程序的性能

    本人在学习<WINDOWS核心编程>的时候对JEFFREY大师提到的一个小程序写了两个版本来比较性能,该程序的原始需求是这样的:对一个大文件进行倒序,也就是将一个文件头变成尾,尾变成头. ...

  7. 使用Memcached提高.NET应用程序的性能

    在应用程序运行的过程中总会有一些经常需要访问并且变化不频繁的数据,如果每次获取这些数据都需要从数据库或者外部文件系统中去读取,性能肯定会受到影响,所以通常的做法就是将这部分数据缓存起来,只要数据没有发 ...

  8. rust go java 性能_Java,Go和Rust之间的比较 - Dexter

    这是Java,Go和Rust之间的比较.这不是基准测试,而是关于:可执行文件大小.内存使用.CPU使用率.运行时要求之间的比较,当然还有一个小的基准测试,可以每秒获取一些请求. 测试三个Web服务的存 ...

  9. DBUtils开源JDBC类库,对JDBC简单封装(作用是:简化编码工作量,同时不会影响程序的性能)...

    DBUtils:提高了程序的性能,编程更加简便 架包 mysql-connector-java-jar commons-dbcp-1.4jar commons-pool-1.5.5jar common ...

最新文章

  1. 小程序二次贝塞尔曲线,购物车商品曲线飞入效果
  2. k8s 使用 Init Container 确保依赖的服务已经启动
  3. 关于通过反汇编查看dll的方法【转】(
  4. 17校招真题题集(2)6-10
  5. 删除a标签下面的横线
  6. 有关语音识别技术的一些信息点
  7. 2019: 属于BERT预训练语言模型之年
  8. mapreduce对日志数据上下行流量汇总
  9. Python Window10 环境安装流程
  10. 在springcacheinvokecontext中没找到field_CNN中的感受野
  11. 声卡调试精调效果都用那些宿主(DAW)机架和效果器插件
  12. WPS插件开发流程(1)
  13. unicode 表情对照表
  14. 从猎豹到山狮-苹果操作系统热衷于猫科动物代号
  15. hibernate: 用Disjunction和Conjunction构造复杂的查询条件
  16. Rust Reqwest 学习
  17. mysql分组之后再求和
  18. intel parallel studio xe 2020 Update 4
  19. 二十一世纪大学英语读写基础教程学习笔记(原文)——10 - The Future(未来)
  20. 海康服务器系统装完重启转圈蓝屏,win10 1903系统重启后一直在转圈无法正常启动的解决方法...

热门文章

  1. 实现Fragment在ViewPager中滑动
  2. ConcurrentHashMap 底层原理,你真的理解了吗?
  3. 算法不会,尚能饭否之集合(Set)
  4. 彼得定律与员工职业生涯规划(该提拔谁,职业规划,知人善用)
  5. 【jenkins】jenkins+maven+gitlab+testng,jenkins配置
  6. Xor Sum 2(位运算)
  7. innobackupex做MySQL增量备份及恢复
  8. Android 下拉刷新上拉载入 多种应用场景 超级大放送(上)
  9. AngularJS track by $index引起的思考
  10. IE8 新增的Javascript,css功能