From be163118a37da3a0b4f9d70c1640f1cab54d8d46 Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Mon, 25 Jan 2021 22:23:08 +0100 Subject: [PATCH] Add support for synchronized updates This implements support for temporarily freezing the terminal grid to prevent rendering of incomplete frames. This can be triggered using the escapes `DCS = 1 s` (start) and `DCS = 2 s` (end). The synchronization is implemented by forwarding all received PTY bytes to a 2 MiB buffer. This should allow updating the entire grid even if it is fairly dense. Unfortunately this also means that another branch is necessary in Alacritty's parser which does have a slight performance impact. In a previous version the freezing was implemented by caching the renderable grid state whenever a synchronized update is started. While this strategy makes it possible to implement this without any performance impact without synchronized updates, a significant performance overhead is introduced whenever a synchronized update is started. Since this can happen thousands of times per frame, it is not a feasible solution. While it would be possible to render at most one synchronized update per frame, it is possible that another synchronized update comes in at any time and stays active for an extended period. As a result the state visible before the long synchronization would be the first received update per frame, not the last, which could lead to the user missing important information during the long freezing interval. Fixes #598. --- CHANGELOG.md | 1 + Cargo.lock | 111 +++++++++++-------- alacritty/Cargo.toml | 2 +- alacritty_terminal/Cargo.toml | 4 +- alacritty_terminal/src/ansi.rs | 157 +++++++++++++++++++++------ alacritty_terminal/src/event_loop.rs | 41 ++++--- 6 files changed, 217 insertions(+), 99 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80824961a6f..9a27412e008 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - IME composition preview not appearing on Windows +- Synchronized terminal updates using `DCS = 1 s`/`DCS = 2 s` ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 7a3c8b5cea7..4993927dff8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,7 +73,7 @@ dependencies = [ "mio-anonymous-pipes", "mio-extras", "miow 0.3.6", - "nix", + "nix 0.19.1", "parking_lot", "regex-automata", "serde", @@ -179,9 +179,9 @@ checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" [[package]] name = "byteorder" -version = "1.3.4" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" +checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b" [[package]] name = "calloop" @@ -190,7 +190,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b036167e76041694579972c28cf4877b4f92da222560ddb49008937b6a6727c" dependencies = [ "log", - "nix", + "nix 0.18.0", ] [[package]] @@ -493,9 +493,9 @@ dependencies = [ [[package]] name = "derivative" -version = "2.1.3" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaed5874effa6cde088c644ddcdcb4ffd1511391c5be4fdd7a5ccd02c7e4a183" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ "proc-macro2", "quote", @@ -504,11 +504,10 @@ dependencies = [ [[package]] name = "dirs" -version = "2.0.2" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" +checksum = "142995ed02755914747cc6ca76fc7e4583cd18578746716d0508ea6ed558b9ff" dependencies = [ - "cfg-if 0.1.10", "dirs-sys", ] @@ -566,10 +565,11 @@ dependencies = [ [[package]] name = "embed-resource" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3b2f39c0462f098c1ca4abc408f7482949bbe2ab19bca5a4419f91f69e5bccc" +checksum = "8f1c0b3e7316e38285d86f43cdc8b60083fa38e6e59666fad66e428498f27a44" dependencies = [ + "cc", "vswhom", "winreg", ] @@ -586,13 +586,13 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c122a393ea57648015bf06fbd3d372378992e86b9ff5a7a497b076a28c79efe" +checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall", + "redox_syscall 0.2.4", "winapi 0.3.9", ] @@ -797,9 +797,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8" +checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" dependencies = [ "libc", ] @@ -823,9 +823,9 @@ dependencies = [ [[package]] name = "inotify-sys" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4563555856585ab3180a5bf0b2f9f8d301a728462afffc8195b3f5394229c55" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" dependencies = [ "libc", ] @@ -896,15 +896,15 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.81" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb" +checksum = "1cca32fa0182e8c0989459524dc356b8f2b5c10f1b9eb521b7d182c03cf8c5ff" [[package]] name = "libloading" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9367bdfa836b7e3cf895867f7a570283444da90562980ec2263d6e1569b16bc" +checksum = "351a32417a12d5f7e82c368a66781e307834dae04c6ce0cd4456d52989229883" dependencies = [ "cfg-if 1.0.0", "winapi 0.3.9", @@ -912,9 +912,9 @@ dependencies = [ [[package]] name = "linked-hash-map" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" [[package]] name = "lock_api" @@ -927,11 +927,11 @@ dependencies = [ [[package]] name = "log" -version = "0.4.11" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ - "cfg-if 0.1.10", + "cfg-if 1.0.0", "serde", ] @@ -1107,11 +1107,23 @@ dependencies = [ "libc", ] +[[package]] +name = "nix" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ccba0cfe4fdf15982d1674c69b1fd80bad427d293849982668dfe454bd61f2" +dependencies = [ + "bitflags", + "cc", + "cfg-if 1.0.0", + "libc", +] + [[package]] name = "nom" -version = "6.0.1" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88034cfd6b4a0d54dd14f4a507eceee36c0b70e5a02236c4e4df571102be17f0" +checksum = "ab6f70b46d6325aa300f1c7bb3d470127dfc27806d8ea6bf294ee0ce643ce2b1" dependencies = [ "memchr", "version_check", @@ -1230,7 +1242,7 @@ dependencies = [ "cfg-if 1.0.0", "instant", "libc", - "redox_syscall", + "redox_syscall 0.1.57", "smallvec", "winapi 0.3.9", ] @@ -1300,6 +1312,15 @@ version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" +[[package]] +name = "redox_syscall" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ec8ca9416c5ea37062b502703cd7fcb207736bc294f6e0cf367ac6fc234570" +dependencies = [ + "bitflags", +] + [[package]] name = "redox_users" version = "0.3.5" @@ -1307,7 +1328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" dependencies = [ "getrandom", - "redox_syscall", + "redox_syscall 0.1.57", "rust-argon2", ] @@ -1323,9 +1344,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.21" +version = "0.6.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189" +checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581" [[package]] name = "rust-argon2" @@ -1378,18 +1399,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.118" +version = "1.0.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800" +checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.118" +version = "1.0.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df" +checksum = "9391c295d64fc0abb2c556bad848f33cb8296276b1ad2677d1ae1ace4f258f31" dependencies = [ "proc-macro2", "quote", @@ -1478,9 +1499,9 @@ checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" [[package]] name = "smallvec" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a55ca5f3b68e41c979bf8c46a6f1da892ca4db8f94023ce0bd32407573b1ac0" +checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" [[package]] name = "smithay-client-toolkit" @@ -1495,7 +1516,7 @@ dependencies = [ "lazy_static", "log", "memmap2", - "nix", + "nix 0.18.0", "wayland-client", "wayland-cursor", "wayland-protocols", @@ -1542,9 +1563,9 @@ checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" [[package]] name = "syn" -version = "1.0.58" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc60a3d73ea6594cd712d830cc1f0390fd71542d8c8cd24e70cc54cdfd5e05d5" +checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081" dependencies = [ "proc-macro2", "quote", @@ -1714,7 +1735,7 @@ dependencies = [ "bitflags", "downcast-rs", "libc", - "nix", + "nix 0.18.0", "scoped-tls", "wayland-commons", "wayland-scanner", @@ -1727,7 +1748,7 @@ version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "480450f76717edd64ad04a4426280d737fc3d10a236b982df7b1aee19f0e2d56" dependencies = [ - "nix", + "nix 0.18.0", "once_cell", "smallvec", "wayland-sys", @@ -1739,7 +1760,7 @@ version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6eb122c160223a7660feeaf949d0100281d1279acaaed3720eb3c9894496e5f" dependencies = [ - "nix", + "nix 0.18.0", "wayland-client", "xcursor", ] diff --git a/alacritty/Cargo.toml b/alacritty/Cargo.toml index 923659d2e9c..febfbdc78bb 100644 --- a/alacritty/Cargo.toml +++ b/alacritty/Cargo.toml @@ -34,7 +34,7 @@ copypasta = { version = "0.7.0", default-features = false } libc = "0.2" unicode-width = "0.1" bitflags = "1" -dirs = "2.0.2" +dirs = "3.0.1" [build-dependencies] gl_generator = "0.14.0" diff --git a/alacritty_terminal/Cargo.toml b/alacritty_terminal/Cargo.toml index 41b8b1f8c09..9fae15570e4 100644 --- a/alacritty_terminal/Cargo.toml +++ b/alacritty_terminal/Cargo.toml @@ -25,10 +25,10 @@ log = "0.4" unicode-width = "0.1" base64 = "0.12.0" regex-automata = "0.1.9" -dirs = "2.0.2" +dirs = "3.0.1" [target.'cfg(unix)'.dependencies] -nix = "0.18.0" +nix = "0.19.0" signal-hook = { version = "0.1", features = ["mio-support"] } [target.'cfg(windows)'.dependencies] diff --git a/alacritty_terminal/src/ansi.rs b/alacritty_terminal/src/ansi.rs index 9c8df704b2f..47eb22aeb0a 100644 --- a/alacritty_terminal/src/ansi.rs +++ b/alacritty_terminal/src/ansi.rs @@ -1,6 +1,7 @@ //! ANSI Terminal Stream Parsing. use std::convert::TryFrom; +use std::time::{Duration, Instant}; use std::{io, iter, str}; use log::{debug, trace}; @@ -12,6 +13,18 @@ use alacritty_config_derive::ConfigDeserialize; use crate::index::{Column, Line}; use crate::term::color::Rgb; +/// Maximum time before a synchronized update is aborted. +const SYNC_UPDATE_TIMEOUT: Duration = Duration::from_millis(150); + +/// Maximum number of bytes read in one synchronized update (2MiB). +const SYNC_BUFFER_SIZE: usize = 0x20_0000; + +/// Beginning sequence for synchronized updates. +const SYNC_START_ESCAPE: [u8; 7] = [b'\x1b', b'P', b'=', b'1', b's', b'\x1b', b'\\']; + +/// Termination sequence for synchronized updates. +const SYNC_END_ESCAPE: [u8; 7] = [b'\x1b', b'P', b'=', b'2', b's', b'\x1b', b'\\']; + /// Parse colors in XParseColor format. fn xparse_color(color: &[u8]) -> Option { if !color.is_empty() && color[0] == b'#' { @@ -81,15 +94,109 @@ fn parse_number(input: &[u8]) -> Option { Some(num) } +/// Internal state for VTE processor. +struct ProcessorState { + /// Last processed character for repetition. + preceding_char: Option, + + /// Beginning of a synchronized update. + sync_start: Option, + + /// Bytes read during a synchronized update. + sync_bytes: Vec, +} + +impl Default for ProcessorState { + fn default() -> Self { + Self { + sync_bytes: Vec::with_capacity(SYNC_BUFFER_SIZE), + sync_start: None, + preceding_char: None, + } + } +} + /// The processor wraps a `vte::Parser` to ultimately call methods on a Handler. +#[derive(Default)] pub struct Processor { state: ProcessorState, parser: vte::Parser, } -/// Internal state for VTE processor. -struct ProcessorState { - preceding_char: Option, +impl Processor { + #[inline] + pub fn new() -> Self { + Self::default() + } + + /// Process a new byte from the PTY. + #[inline] + pub fn advance(&mut self, handler: &mut H, byte: u8, writer: &mut W) + where + H: Handler, + W: io::Write, + { + if self.state.sync_start.is_none() { + let mut performer = Performer::new(&mut self.state, handler, writer); + self.parser.advance(&mut performer, byte); + } else { + self.advance_sync(handler, byte, writer); + } + } + + /// End a synchronized update. + pub fn stop_sync(&mut self, handler: &mut H, writer: &mut W) + where + H: Handler, + W: io::Write, + { + // Process all synchronized bytes. + for i in 0..self.state.sync_bytes.len() { + let byte = self.state.sync_bytes[i]; + let mut performer = Performer::new(&mut self.state, handler, writer); + self.parser.advance(&mut performer, byte); + } + + self.state.sync_bytes.clear(); + self.state.sync_start = None; + } + + /// Synchronized update expiration time. + #[inline] + pub fn sync_timeout(&self) -> Option<&Instant> { + self.state.sync_start.as_ref() + } + + /// Number of bytes in the synchronization buffer. + #[inline] + pub fn sync_bytes_count(&self) -> usize { + self.state.sync_bytes.len() + } + + /// Process a new byte during a synchronized update. + #[cold] + fn advance_sync(&mut self, handler: &mut H, byte: u8, writer: &mut W) + where + H: Handler, + W: io::Write, + { + self.state.sync_bytes.push(byte); + + // Get the last few bytes for comparison. + let len = self.state.sync_bytes.len(); + let end = &self.state.sync_bytes[len.saturating_sub(SYNC_END_ESCAPE.len())..]; + debug_assert_eq!(SYNC_END_ESCAPE.len(), SYNC_START_ESCAPE.len()); + + // Extend synchronization period when start is received during synchronization. + if end == SYNC_START_ESCAPE { + self.state.sync_start = Some(Instant::now() + SYNC_UPDATE_TIMEOUT); + } + + // Stop when we receive the termination sequence or the buffer length is at the limit. + if end == SYNC_END_ESCAPE || len >= SYNC_BUFFER_SIZE - 1 { + self.stop_sync(handler, writer); + } + } } /// Helper type that implements `vte::Perform`. @@ -114,28 +221,6 @@ impl<'a, H: Handler + 'a, W: io::Write> Performer<'a, H, W> { } } -impl Default for Processor { - fn default() -> Processor { - Processor { state: ProcessorState { preceding_char: None }, parser: vte::Parser::new() } - } -} - -impl Processor { - pub fn new() -> Processor { - Default::default() - } - - #[inline] - pub fn advance(&mut self, handler: &mut H, byte: u8, writer: &mut W) - where - H: Handler, - W: io::Write, - { - let mut performer = Performer::new(&mut self.state, handler, writer); - self.parser.advance(&mut performer, byte); - } -} - /// Type that handles actions from the parser. /// /// XXX Should probably not provide default impls for everything, but it makes @@ -172,8 +257,6 @@ pub trait Handler { fn move_down(&mut self, _: Line) {} /// Identify the terminal (should write back to the pty stream). - /// - /// TODO this should probably return an io::Result fn identify_terminal(&mut self, _: &mut W, _intermediate: Option) {} /// Report device status. @@ -428,8 +511,6 @@ pub enum Mode { impl Mode { /// Create mode from a primitive. - /// - /// TODO lots of unhandled values. pub fn from_primitive(intermediate: Option<&u8>, num: u16) -> Option { let private = match intermediate { Some(b'?') => true, @@ -779,10 +860,18 @@ where #[inline] fn hook(&mut self, params: &Params, intermediates: &[u8], ignore: bool, action: char) { - debug!( - "[unhandled hook] params={:?}, ints: {:?}, ignore: {:?}, action: {:?}", - params, intermediates, ignore, action - ); + match (action, intermediates) { + ('s', [b'=']) => { + // Start a synchronized update. The end is handled with a separate parser. + if params.iter().next().map_or(false, |param| param[0] == 1) { + self.state.sync_start = Some(Instant::now() + SYNC_UPDATE_TIMEOUT); + } + }, + _ => debug!( + "[unhandled hook] params={:?}, ints: {:?}, ignore: {:?}, action: {:?}", + params, intermediates, ignore, action + ), + } } #[inline] @@ -795,7 +884,6 @@ where debug!("[unhandled unhook]"); } - // TODO replace OSC parsing with parser combinators. #[inline] fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) { let terminator = if bell_terminated { "\x07" } else { "\x1b\\" }; @@ -1172,6 +1260,7 @@ where } } +#[inline] fn attrs_from_sgr_parameters(params: &mut ParamsIter<'_>) -> Vec> { let mut attrs = Vec::with_capacity(params.size_hint().0); diff --git a/alacritty_terminal/src/event_loop.rs b/alacritty_terminal/src/event_loop.rs index 2fe3f64e260..4fd0aded0ad 100644 --- a/alacritty_terminal/src/event_loop.rs +++ b/alacritty_terminal/src/event_loop.rs @@ -7,6 +7,7 @@ use std::io::{self, ErrorKind, Read, Write}; use std::marker::Send; use std::sync::Arc; use std::thread::JoinHandle; +use std::time::Instant; use log::error; #[cfg(not(windows))] @@ -58,16 +59,6 @@ struct Writing { written: usize, } -/// All of the mutable state needed to run the event loop. -/// -/// Contains list of items to write, current write state, etc. Anything that -/// would otherwise be mutated on the `EventLoop` goes here. -pub struct State { - write_list: VecDeque>, - writing: Option, - parser: ansi::Processor, -} - pub struct Notifier(pub Sender); impl event::Notify for Notifier { @@ -91,10 +82,15 @@ impl event::OnResize for Notifier { } } -impl Default for State { - fn default() -> State { - State { write_list: VecDeque::new(), parser: ansi::Processor::new(), writing: None } - } +/// All of the mutable state needed to run the event loop. +/// +/// Contains list of items to write, current write state, etc. Anything that +/// would otherwise be mutated on the `EventLoop` goes here. +#[derive(Default)] +pub struct State { + write_list: VecDeque>, + writing: Option, + parser: ansi::Processor, } impl State { @@ -261,8 +257,8 @@ where } } - if processed > 0 { - // Queue terminal redraw. + // Queue terminal redraw unless all processed bytes were synchronized. + if state.parser.sync_bytes_count() < processed && processed > 0 { self.event_proxy.send_event(Event::Wakeup); } @@ -325,13 +321,24 @@ where }; 'event_loop: loop { - if let Err(err) = self.poll.poll(&mut events, None) { + // Wakeup event loop when the maximum synchronization time was exceeded. + let sync_timeout = state.parser.sync_timeout(); + let timeout = sync_timeout.map(|st| st.saturating_duration_since(Instant::now())); + + if let Err(err) = self.poll.poll(&mut events, timeout) { match err.kind() { ErrorKind::Interrupted => continue, _ => panic!("EventLoop polling error: {:?}", err), } } + // Handle synchronized update timeout. + if events.is_empty() { + state.parser.stop_sync(&mut *self.terminal.lock(), &mut self.pty.writer()); + self.event_proxy.send_event(Event::Wakeup); + continue; + } + for event in events.iter() { match event.token() { token if token == channel_token => {