feat(performance): better tty read buffering and less allocations when rendering

This commit is contained in:
Aram Drevekenin 2020-10-19 11:55:22 +02:00
parent 049ff1a6dc
commit e50e9770fd
3 changed files with 128 additions and 429 deletions

View File

@ -21,6 +21,14 @@ use ::signal_hook::iterator::Signals;
pub type OnSigWinch = dyn Fn(Box<dyn Fn()>) + Send;
pub type SigCleanup = dyn Fn() + Send;
fn debug_log_to_file (message: String) {
use std::fs::OpenOptions;
use std::io::prelude::*;
let mut file = OpenOptions::new().append(true).create(true).open("/tmp/mosaic-log.txt").unwrap();
file.write_all(message.as_bytes()).unwrap();
file.write_all("\n".as_bytes()).unwrap();
}
pub fn sigwinch() -> (Box<OnSigWinch>, Box<SigCleanup>) {
let signals = Signals::new(&[signal_hook::SIGWINCH]).unwrap();
let on_winch = {

View File

@ -4,6 +4,7 @@ use ::async_std::task;
use ::async_std::task::*;
use ::std::pin::*;
use ::std::sync::mpsc::{channel, Sender, Receiver};
use ::std::time::{Instant, Duration};
use ::vte;
use crate::os_input_output::OsApi;
@ -23,10 +24,18 @@ impl ReadFromPid {
}
}
fn debug_log_to_file (message: String) {
use std::fs::OpenOptions;
use std::io::prelude::*;
let mut file = OpenOptions::new().append(true).create(true).open("/tmp/mosaic-log.txt").unwrap();
file.write_all(message.as_bytes()).unwrap();
file.write_all("\n".as_bytes()).unwrap();
}
impl Stream for ReadFromPid {
type Item = Vec<u8>;
fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let mut read_buffer = [0; 115200];
let mut read_buffer = [0; 65535];
let pid = self.pid;
let read_result = &self.os_input.read_from_tty_stdout(pid, &mut read_buffer);
match read_result {
@ -136,6 +145,60 @@ pub struct PtyBus {
os_input: Box<dyn OsApi>,
}
fn stream_terminal_bytes(pid: RawFd, send_screen_instructions: Sender<ScreenInstruction>, os_input: Box<dyn OsApi>) {
task::spawn({
async move {
let mut vte_parser = vte::Parser::new();
let mut vte_event_sender = VteEventSender::new(pid, send_screen_instructions.clone());
let mut terminal_bytes = ReadFromPid::new(&pid, os_input);
let mut last_byte_receive_time: Option<Instant> = None;
let mut pending_render = false;
let max_render_pause = Duration::from_millis(30);
while let Some(bytes) = terminal_bytes.next().await {
let bytes_is_empty = bytes.is_empty();
for byte in bytes {
vte_parser.advance(&mut vte_event_sender, byte);
}
if !bytes_is_empty {
// for UX reasons, if we got something on the wire, we only send the render notice if:
// 1. there aren't any more bytes on the wire afterwards
// 2. a certain period (currently 30ms) has elapsed since the last render
// (otherwise if we get a large amount of data, the display would hang
// until it's done)
// 3. the stream has ended, and so we render 1 last time
match last_byte_receive_time.as_mut() {
Some(receive_time) => {
// if receive_time.elapsed() > Duration::from_millis(30) {
if receive_time.elapsed() > max_render_pause {
pending_render = false;
send_screen_instructions.send(ScreenInstruction::Render).unwrap();
last_byte_receive_time = Some(Instant::now());
} else {
pending_render = true;
}
},
None => {
last_byte_receive_time = Some(Instant::now());
pending_render = true;
}
};
} else {
if pending_render {
pending_render = false;
send_screen_instructions.send(ScreenInstruction::Render).unwrap();
}
last_byte_receive_time = None;
task::sleep(::std::time::Duration::from_millis(10)).await;
}
}
send_screen_instructions.send(ScreenInstruction::Render).unwrap();
}
});
}
impl PtyBus {
pub fn new (send_screen_instructions: Sender<ScreenInstruction>, os_input: Box<dyn OsApi>) -> Self {
let (send_pty_instructions, receive_pty_instructions): (Sender<PtyInstruction>, Receiver<PtyInstruction>) = channel();
@ -148,50 +211,12 @@ impl PtyBus {
}
pub fn spawn_terminal_vertically(&mut self) {
let (pid_primary, _pid_secondary): (RawFd, RawFd) = self.os_input.spawn_terminal();
task::spawn({
let send_screen_instructions = self.send_screen_instructions.clone();
let os_input = self.os_input.clone();
async move {
let mut vte_parser = vte::Parser::new();
let mut vte_event_sender = VteEventSender::new(pid_primary, send_screen_instructions.clone());
let mut first_terminal_bytes = ReadFromPid::new(&pid_primary, os_input);
while let Some(bytes) = first_terminal_bytes.next().await {
let bytes_is_empty = bytes.is_empty();
for byte in bytes {
vte_parser.advance(&mut vte_event_sender, byte);
}
if !bytes_is_empty {
send_screen_instructions.send(ScreenInstruction::Render).unwrap();
} else {
task::sleep(::std::time::Duration::from_millis(10)).await;
}
}
}
});
stream_terminal_bytes(pid_primary, self.send_screen_instructions.clone(), self.os_input.clone());
self.send_screen_instructions.send(ScreenInstruction::VerticalSplit(pid_primary)).unwrap();
}
pub fn spawn_terminal_horizontally(&mut self) {
let (pid_primary, _pid_secondary): (RawFd, RawFd) = self.os_input.spawn_terminal();
task::spawn({
let send_screen_instructions = self.send_screen_instructions.clone();
let os_input = self.os_input.clone();
async move {
let mut vte_parser = vte::Parser::new();
let mut vte_event_sender = VteEventSender::new(pid_primary, send_screen_instructions.clone());
let mut first_terminal_bytes = ReadFromPid::new(&pid_primary, os_input);
while let Some(bytes) = first_terminal_bytes.next().await {
let bytes_is_empty = bytes.is_empty();
for byte in bytes {
vte_parser.advance(&mut vte_event_sender, byte);
}
if !bytes_is_empty {
send_screen_instructions.send(ScreenInstruction::Render).unwrap();
} else {
task::sleep(::std::time::Duration::from_millis(10)).await;
}
}
}
});
stream_terminal_bytes(pid_primary, self.send_screen_instructions.clone(), self.os_input.clone());
self.send_screen_instructions.send(ScreenInstruction::HorizontalSplit(pid_primary)).unwrap();
}
}

View File

@ -10,17 +10,19 @@ use crate::boundaries::Rect;
const EMPTY_TERMINAL_CHARACTER: TerminalCharacter = TerminalCharacter {
character: ' ',
foreground: Some(AnsiCode::Reset),
background: Some(AnsiCode::Reset),
strike: Some(AnsiCode::Reset),
hidden: Some(AnsiCode::Reset),
reverse: Some(AnsiCode::Reset),
slow_blink: Some(AnsiCode::Reset),
fast_blink: Some(AnsiCode::Reset),
underline: Some(AnsiCode::Reset),
bold: Some(AnsiCode::Reset),
dim: Some(AnsiCode::Reset),
italic: Some(AnsiCode::Reset),
styles: CharacterStyles {
foreground: Some(AnsiCode::Reset),
background: Some(AnsiCode::Reset),
strike: Some(AnsiCode::Reset),
hidden: Some(AnsiCode::Reset),
reverse: Some(AnsiCode::Reset),
slow_blink: Some(AnsiCode::Reset),
fast_blink: Some(AnsiCode::Reset),
underline: Some(AnsiCode::Reset),
bold: Some(AnsiCode::Reset),
dim: Some(AnsiCode::Reset),
italic: Some(AnsiCode::Reset),
}
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@ -157,7 +159,7 @@ impl CharacterStyles {
self.dim = None;
self.italic = None;
}
pub fn update_and_return_diff(&mut self, new_styles: &TerminalCharacter) -> Option<CharacterStyles> {
pub fn update_and_return_diff(&mut self, new_styles: &CharacterStyles) -> Option<CharacterStyles> {
let mut diff: Option<CharacterStyles> = None;
if self.foreground != new_styles.foreground {
if let Some(new_diff) = diff.as_mut() {
@ -535,356 +537,7 @@ impl Display for CharacterStyles {
#[derive(Clone, Copy)]
pub struct TerminalCharacter {
pub character: char,
pub foreground: Option<AnsiCode>,
pub background: Option<AnsiCode>,
pub strike: Option<AnsiCode>,
pub hidden: Option<AnsiCode>,
pub reverse: Option<AnsiCode>,
pub slow_blink: Option<AnsiCode>,
pub fast_blink: Option<AnsiCode>,
pub underline: Option<AnsiCode>,
pub bold: Option<AnsiCode>,
pub dim: Option<AnsiCode>,
pub italic: Option<AnsiCode>,
}
impl TerminalCharacter {
pub fn new (character: char) -> Self {
TerminalCharacter {
character,
foreground: None,
background: None,
strike: None,
hidden: None,
reverse: None,
slow_blink: None,
fast_blink: None,
underline: None,
bold: None,
dim: None,
italic: None,
}
}
pub fn foreground(mut self, foreground_code: Option<AnsiCode>) -> Self {
self.foreground = foreground_code;
self
}
pub fn background(mut self, background_code: Option<AnsiCode>) -> Self {
self.background = background_code;
self
}
pub fn bold(mut self, bold_code: Option<AnsiCode>) -> Self {
self.bold = bold_code;
self
}
pub fn dim(mut self, dim_code: Option<AnsiCode>) -> Self {
self.dim = dim_code;
self
}
pub fn italic(mut self, italic_code: Option<AnsiCode>) -> Self {
self.italic = italic_code;
self
}
pub fn underline(mut self, underline_code: Option<AnsiCode>) -> Self {
self.underline = underline_code;
self
}
pub fn blink_slow(mut self, slow_blink_code: Option<AnsiCode>) -> Self {
self.slow_blink = slow_blink_code;
self
}
pub fn blink_fast(mut self, fast_blink_code: Option<AnsiCode>) -> Self {
self.fast_blink = fast_blink_code;
self
}
pub fn reverse(mut self, reverse_code: Option<AnsiCode>) -> Self {
self.reverse = reverse_code;
self
}
pub fn hidden(mut self, hidden_code: Option<AnsiCode>) -> Self {
self.hidden = hidden_code;
self
}
pub fn strike(mut self, strike_code: Option<AnsiCode>) -> Self {
self.strike = strike_code;
self
}
}
impl Display for TerminalCharacter {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let mut character_ansi_codes = String::new(); // TODO: better
if let Some(ansi_code) = self.foreground {
match ansi_code {
AnsiCode::Code((param1, param2)) => {
match (param1, param2) {
(Some(param1), Some(param2)) => {
character_ansi_codes.push_str(&format!("\u{1b}[38;{};{}m", param1, param2));
},
(Some(param1), None) => {
character_ansi_codes.push_str(&format!("\u{1b}[38;{}m", param1));
},
(_, _) => {
// TODO: can this happen?
}
}
},
AnsiCode::Reset => {
character_ansi_codes.push_str(&format!("\u{1b}[39m"));
},
AnsiCode::NamedColor(named_color) => {
character_ansi_codes.push_str(&format!("\u{1b}[{}m", named_color.to_foreground_ansi_code()));
}
}
}
if let Some(ansi_code) = self.background {
match ansi_code {
AnsiCode::Code((param1, param2)) => {
match (param1, param2) {
(Some(param1), Some(param2)) => {
character_ansi_codes.push_str(&format!("\u{1b}[48;{};{}m", param1, param2));
},
(Some(param1), None) => {
character_ansi_codes.push_str(&format!("\u{1b}[48;{}m", param1));
},
(_, _) => {
// TODO: can this happen?
}
}
},
AnsiCode::Reset => {
character_ansi_codes.push_str(&format!("\u{1b}[49m"));
}
AnsiCode::NamedColor(named_color) => {
character_ansi_codes.push_str(&format!("\u{1b}[{}m", named_color.to_background_ansi_code()));
}
}
}
if let Some(ansi_code) = self.strike {
match ansi_code {
AnsiCode::Code((param1, param2)) => {
match (param1, param2) {
(Some(param1), Some(param2)) => {
character_ansi_codes.push_str(&format!("\u{1b}[9;{};{}m", param1, param2));
},
(Some(param1), None) => {
character_ansi_codes.push_str(&format!("\u{1b}[9;{}m", param1));
},
(_, _) => {
character_ansi_codes.push_str("\u{1b}[9m");
}
}
},
AnsiCode::Reset => {
character_ansi_codes.push_str(&format!("\u{1b}[29m"));
},
_ => {}
}
}
if let Some(ansi_code) = self.hidden {
match ansi_code {
AnsiCode::Code((param1, param2)) => {
match (param1, param2) {
(Some(param1), Some(param2)) => {
character_ansi_codes.push_str(&format!("\u{1b}[8;{};{}m", param1, param2));
},
(Some(param1), None) => {
character_ansi_codes.push_str(&format!("\u{1b}[8;{}m", param1));
},
(_, _) => {
character_ansi_codes.push_str("\u{1b}[8m");
}
}
},
AnsiCode::Reset => {
character_ansi_codes.push_str(&format!("\u{1b}[28m"));
},
_ => {}
}
}
if let Some(ansi_code) = self.reverse {
match ansi_code {
AnsiCode::Code((param1, param2)) => {
match (param1, param2) {
(Some(param1), Some(param2)) => {
character_ansi_codes.push_str(&format!("\u{1b}[7;{};{}m", param1, param2));
},
(Some(param1), None) => {
character_ansi_codes.push_str(&format!("\u{1b}[7;{}m", param1));
},
(_, _) => {
character_ansi_codes.push_str("\u{1b}[7m");
}
}
},
AnsiCode::Reset => {
character_ansi_codes.push_str(&format!("\u{1b}[27m"));
},
_ => {}
}
}
if let Some(ansi_code) = self.fast_blink {
match ansi_code {
AnsiCode::Code((param1, param2)) => {
match (param1, param2) {
(Some(param1), Some(param2)) => {
character_ansi_codes.push_str(&format!("\u{1b}[6;{};{}m", param1, param2));
},
(Some(param1), None) => {
character_ansi_codes.push_str(&format!("\u{1b}[6;{}m", param1));
},
(_, _) => {
character_ansi_codes.push_str("\u{1b}[6m");
}
}
},
AnsiCode::Reset => {
character_ansi_codes.push_str(&format!("\u{1b}[25m"));
},
_ => {}
}
}
if let Some(ansi_code) = self.slow_blink {
match ansi_code {
AnsiCode::Code((param1, param2)) => {
match (param1, param2) {
(Some(param1), Some(param2)) => {
character_ansi_codes.push_str(&format!("\u{1b}[5;{};{}m", param1, param2));
},
(Some(param1), None) => {
character_ansi_codes.push_str(&format!("\u{1b}[5;{}m", param1));
},
(_, _) => {
character_ansi_codes.push_str("\u{1b}[5m");
}
}
},
AnsiCode::Reset => {
character_ansi_codes.push_str(&format!("\u{1b}[25m"));
},
_ => {}
}
}
if let Some(ansi_code) = self.underline {
match ansi_code {
AnsiCode::Code((param1, param2)) => {
match (param1, param2) {
(Some(param1), Some(param2)) => {
character_ansi_codes.push_str(&format!("\u{1b}[4;{};{}m", param1, param2));
},
(Some(param1), None) => {
character_ansi_codes.push_str(&format!("\u{1b}[4;{}m", param1));
},
(_, _) => {
character_ansi_codes.push_str("\u{1b}[4m");
}
}
},
AnsiCode::Reset => {
character_ansi_codes.push_str(&format!("\u{1b}[24m"));
},
_ => {}
}
}
if let Some(ansi_code) = self.bold {
match ansi_code {
AnsiCode::Code((param1, param2)) => {
match (param1, param2) {
(Some(param1), Some(param2)) => {
character_ansi_codes.push_str(&format!("\u{1b}[1;{};{}m", param1, param2));
},
(Some(param1), None) => {
character_ansi_codes.push_str(&format!("\u{1b}[1;{}m", param1));
},
(_, _) => {
character_ansi_codes.push_str("\u{1b}[1m");
}
}
},
AnsiCode::Reset => {
character_ansi_codes.push_str(&format!("\u{1b}[22m\u{1b}[24m"));
// character_ansi_codes.push_str(&format!("\u{1b}[22m"));
// TODO: this cancels bold + underline, if this behaviour is indeed correct, we
// need to properly handle it in the struct methods etc like dim
},
_ => {}
}
}
// notice the order is important here, bold must be before underline
// because the bold reset also resets underline, and would override it
// otherwise
if let Some(ansi_code) = self.underline {
match ansi_code {
AnsiCode::Code((param1, param2)) => {
match (param1, param2) {
(Some(param1), Some(param2)) => {
character_ansi_codes.push_str(&format!("\u{1b}[4;{};{}m", param1, param2));
},
(Some(param1), None) => {
character_ansi_codes.push_str(&format!("\u{1b}[4;{}m", param1));
},
(_, _) => {
character_ansi_codes.push_str("\u{1b}[4m");
}
}
},
AnsiCode::Reset => {
character_ansi_codes.push_str(&format!("\u{1b}[24m"));
},
_ => {}
}
}
if let Some(ansi_code) = self.dim {
match ansi_code {
AnsiCode::Code((param1, param2)) => {
match (param1, param2) {
(Some(param1), Some(param2)) => {
character_ansi_codes.push_str(&format!("\u{1b}[2;{};{}m", param1, param2));
},
(Some(param1), None) => {
character_ansi_codes.push_str(&format!("\u{1b}[2;{}m", param1));
},
(_, _) => {
character_ansi_codes.push_str("\u{1b}[2m");
}
}
},
AnsiCode::Reset => {
if let Some(bold) = self.bold {
// we only reset dim if both dim and bold should be reset
match bold {
AnsiCode::Reset => character_ansi_codes.push_str(&format!("\u{1b}[22m")), // TODO: better
_ => {}
}
}
},
_ => {}
}
}
if let Some(ansi_code) = self.italic {
match ansi_code {
AnsiCode::Code((param1, param2)) => {
match (param1, param2) {
(Some(param1), Some(param2)) => {
character_ansi_codes.push_str(&format!("\u{1b}[3;{};{}m", param1, param2));
},
(Some(param1), None) => {
character_ansi_codes.push_str(&format!("\u{1b}[3;{}m", param1));
},
(_, _) => {
character_ansi_codes.push_str("\u{1b}[3m");
}
}
},
AnsiCode::Reset => {
character_ansi_codes.push_str(&format!("\u{1b}[23m"));
},
_ => {}
}
};
write!(f, "{}", character_ansi_codes)
}
pub styles: CharacterStyles,
}
impl ::std::fmt::Debug for TerminalCharacter {
@ -978,7 +631,7 @@ impl<'a> Grid <'a>{
let mut newline_indices: Vec<usize> = vec![];
for line in &self.cells {
for character in line.iter().copied() {
characters.push(character.clone());
characters.push(*character);
}
let last_newline_index = newline_indices.last().copied().unwrap_or(0);
newline_indices.push(last_newline_index + line.len());
@ -1046,9 +699,10 @@ impl Rect for &mut TerminalOutput {
impl TerminalOutput {
pub fn new (pid: RawFd, ws: Winsize, x_coords: u16, y_coords: u16) -> TerminalOutput {
let characters = Vec::with_capacity(100000);
TerminalOutput {
pid,
characters: vec![],
characters,
cursor_position: 0,
newline_indices: Vec::new(),
linebreak_indices: Vec::new(),
@ -1073,10 +727,11 @@ impl TerminalOutput {
}
}
pub fn handle_event(&mut self, event: VteEvent) {
self.should_render = true; // TODO: more accurately
// self.should_render = true; // TODO: more accurately
match event {
VteEvent::Print(c) => {
self.print(c);
self.should_render = true; // TODO: more accurately
},
VteEvent::Execute(byte) => {
self.execute(byte);
@ -1188,7 +843,7 @@ impl TerminalOutput {
// in some cases (eg. while resizing) some characters will spill over
// before they are corrected by the shell (for the prompt) or by reflowing
// lines
if let Some(new_styles) = character_styles.update_and_return_diff(&t_character) {
if let Some(new_styles) = character_styles.update_and_return_diff(&t_character.styles) {
vte_output = format!("{}{}", vte_output, new_styles);
}
vte_output.push(t_character.character);
@ -1495,36 +1150,47 @@ fn debug_log_to_file (message: String, pid: RawFd) {
impl vte::Perform for TerminalOutput {
fn print(&mut self, c: char) {
// while not ideal that we separate the reset and actual code logic here,
// combining them is a question of rendering performance and not refactoring,
// so will be addressed separately
let terminal_character = TerminalCharacter::new(c)
.foreground(self.pending_foreground_code)
.background(self.pending_background_code)
.bold(self.pending_bold_code)
.dim(self.pending_dim_code)
.italic(self.pending_italic_code)
.underline(self.pending_underline_code)
.blink_slow(self.pending_slow_blink_code)
.blink_fast(self.pending_fast_blink_code)
.reverse(self.pending_reverse_code)
.hidden(self.pending_hidden_code)
.strike(self.pending_strike_code);
// apparently, building TerminalCharacter like this without a "new" method
// is a little faster
let terminal_character = TerminalCharacter {
character: c,
styles: CharacterStyles {
foreground: self.pending_foreground_code,
background: self.pending_background_code,
bold: self.pending_bold_code,
dim: self.pending_dim_code,
italic: self.pending_italic_code,
underline: self.pending_underline_code,
slow_blink: self.pending_slow_blink_code,
fast_blink: self.pending_fast_blink_code,
reverse: self.pending_reverse_code,
hidden: self.pending_hidden_code,
strike: self.pending_strike_code,
}
};
if self.characters.len() > self.cursor_position {
self.characters.remove(self.cursor_position);
self.characters.insert(self.cursor_position, terminal_character);
let length_of_characters = self.characters.len();
let current_character_capacity = self.characters.capacity();
if current_character_capacity <= self.characters.len() {
self.characters.reserve(current_character_capacity);
}
if length_of_characters > self.cursor_position {
// this is a little hacky but significantly more performant
// than removing self.cursor_position and then inserting terminal_character
self.characters.push(terminal_character);
self.characters.swap_remove(self.cursor_position);
if !self.newline_indices.contains(&(self.cursor_position + 1)) {
// advancing the cursor beyond the borders of the line has to be done explicitly
self.cursor_position += 1;
}
} else {
for _ in self.characters.len()..self.cursor_position {
self.characters.push(EMPTY_TERMINAL_CHARACTER.clone());
for _ in length_of_characters..self.cursor_position {
self.characters.push(EMPTY_TERMINAL_CHARACTER);
};
self.characters.push(terminal_character);
let start_of_last_line = self.index_of_beginning_of_line(self.cursor_position);
let start_of_last_line = max(self.newline_indices.last(), self.linebreak_indices.last()).unwrap_or(&0);
let difference_from_last_newline = self.cursor_position - start_of_last_line;
if difference_from_last_newline == self.display_cols as usize && self.scroll_region.is_none() {
self.linebreak_indices.push(self.cursor_position);