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.
This commit is contained in:
Christian Duerr 2021-01-25 22:23:08 +01:00
parent 64abd7fb43
commit 9575aed681
6 changed files with 279 additions and 83 deletions

View File

@ -10,6 +10,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 ST`/`DCS = 2 s ST`
### Fixed

98
Cargo.lock generated
View File

@ -75,7 +75,7 @@ dependencies = [
"mio-anonymous-pipes",
"mio-extras",
"miow 0.3.6",
"nix",
"nix 0.19.1",
"parking_lot",
"regex-automata",
"serde",
@ -192,14 +192,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b036167e76041694579972c28cf4877b4f92da222560ddb49008937b6a6727c"
dependencies = [
"log",
"nix",
"nix 0.18.0",
]
[[package]]
name = "cc"
version = "1.0.66"
version = "1.0.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48"
checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd"
[[package]]
name = "cfg-if"
@ -394,9 +394,9 @@ dependencies = [
[[package]]
name = "core-text"
version = "19.1.0"
version = "19.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2c7f46e8b820fd5f4b28528104b28b0a91cbe9e9c5bde8017087fb44bc93a60"
checksum = "99d74ada66e07c1cefa18f8abfba765b486f250de2e4a999e5727fc0dd4b4a25"
dependencies = [
"core-foundation 0.9.1",
"core-graphics 0.22.2",
@ -428,13 +428,14 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
version = "0.8.1"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d"
checksum = "bae8f328835f8f5a6ceb6a7842a7f2d0c03692adb5c889347235d59194731fe3"
dependencies = [
"autocfg",
"cfg-if 1.0.0",
"lazy_static",
"loom",
]
[[package]]
@ -506,11 +507,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",
]
@ -568,9 +568,9 @@ dependencies = [
[[package]]
name = "embed-resource"
version = "1.5.1"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bd6a41d89e233bcd6978fe7333191a2054d518d105a1165ada1d2ebc445ce98"
checksum = "04e03c3dae04b8f252f2866d25e0ec8f2f488efba43bc6e307274b65c4321f94"
dependencies = [
"cc",
"vswhom",
@ -595,7 +595,7 @@ checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8"
dependencies = [
"cfg-if 1.0.0",
"libc",
"redox_syscall 0.2.4",
"redox_syscall 0.2.5",
"winapi 0.3.9",
]
@ -704,6 +704,19 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
[[package]]
name = "generator"
version = "0.6.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9fed24fd1e18827652b4d55652899a1e9da8e54d91624dc3437a5bc3a9f9a9c"
dependencies = [
"cc",
"libc",
"log",
"rustversion",
"winapi 0.3.9",
]
[[package]]
name = "getrandom"
version = "0.1.16"
@ -938,6 +951,17 @@ dependencies = [
"serde",
]
[[package]]
name = "loom"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d44c73b4636e497b4917eb21c33539efa3816741a2d3ff26c6316f1b529481a4"
dependencies = [
"cfg-if 1.0.0",
"generator",
"scoped-tls",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"
@ -1111,10 +1135,22 @@ dependencies = [
]
[[package]]
name = "nom"
version = "6.1.0"
name = "nix"
version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab6f70b46d6325aa300f1c7bb3d470127dfc27806d8ea6bf294ee0ce643ce2b1"
checksum = "b2ccba0cfe4fdf15982d1674c69b1fd80bad427d293849982668dfe454bd61f2"
dependencies = [
"bitflags",
"cc",
"cfg-if 1.0.0",
"libc",
]
[[package]]
name = "nom"
version = "6.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2"
dependencies = [
"memchr",
"version_check",
@ -1233,7 +1269,7 @@ dependencies = [
"cfg-if 1.0.0",
"instant",
"libc",
"redox_syscall 0.2.4",
"redox_syscall 0.2.5",
"smallvec",
"winapi 0.3.9",
]
@ -1305,9 +1341,9 @@ checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[package]]
name = "redox_syscall"
version = "0.2.4"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05ec8ca9416c5ea37062b502703cd7fcb207736bc294f6e0cf367ac6fc234570"
checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9"
dependencies = [
"bitflags",
]
@ -1361,6 +1397,12 @@ dependencies = [
"owned_ttf_parser",
]
[[package]]
name = "rustversion"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb5d2a036dc6d2d8fd16fde3498b04306e29bd193bf306a57427019b823d5acd"
[[package]]
name = "ryu"
version = "1.0.5"
@ -1507,7 +1549,7 @@ dependencies = [
"lazy_static",
"log",
"memmap2",
"nix",
"nix 0.18.0",
"wayland-client",
"wayland-cursor",
"wayland-protocols",
@ -1574,18 +1616,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.23"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76cc616c6abf8c8928e2fdcc0dbfab37175edd8fb49a4641066ad1364fdab146"
checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.23"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1"
checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0"
dependencies = [
"proc-macro2",
"quote",
@ -1726,7 +1768,7 @@ dependencies = [
"bitflags",
"downcast-rs",
"libc",
"nix",
"nix 0.18.0",
"scoped-tls",
"wayland-commons",
"wayland-scanner",
@ -1739,7 +1781,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",
@ -1751,7 +1793,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",
]

View File

@ -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"

View File

@ -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]

View File

@ -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,21 @@ 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;
/// Number of bytes in the synchronized update DCS sequence before the passthrough parameters.
const SYNC_ESCAPE_START_LEN: usize = 5;
/// Start of the DCS sequence for beginning synchronized updates.
const SYNC_START_ESCAPE_START: [u8; SYNC_ESCAPE_START_LEN] = [b'\x1b', b'P', b'=', b'1', b's'];
/// Start of the DCS sequence for terminating synchronized updates.
const SYNC_END_ESCAPE_START: [u8; SYNC_ESCAPE_START_LEN] = [b'\x1b', b'P', b'=', b'2', b's'];
/// Parse colors in XParseColor format.
fn xparse_color(color: &[u8]) -> Option<Rgb> {
if !color.is_empty() && color[0] == b'#' {
@ -81,15 +97,157 @@ fn parse_number(input: &[u8]) -> Option<u8> {
Some(num)
}
/// Internal state for VTE processor.
#[derive(Debug, Default)]
struct ProcessorState {
/// Last processed character for repetition.
preceding_char: Option<char>,
/// DCS sequence waiting for termination.
dcs: Option<Dcs>,
/// State for synchronized terminal updates.
sync_state: SyncState,
}
#[derive(Debug)]
struct SyncState {
/// Expiration time of the synchronized update.
timeout: Option<Instant>,
/// Sync DCS waiting for termination sequence.
pending_dcs: Option<Dcs>,
/// Bytes read during the synchronized update.
buffer: Vec<u8>,
}
impl Default for SyncState {
fn default() -> Self {
Self { buffer: Vec::with_capacity(SYNC_BUFFER_SIZE), pending_dcs: None, timeout: None }
}
}
/// Pending DCS sequence.
#[derive(Debug)]
enum Dcs {
/// Begin of the synchronized update.
SyncStart,
/// End of the synchronized update.
SyncEnd,
}
/// 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<char>,
impl Processor {
#[inline]
pub fn new() -> Self {
Self::default()
}
/// Process a new byte from the PTY.
#[inline]
pub fn advance<H, W>(&mut self, handler: &mut H, byte: u8, writer: &mut W)
where
H: Handler,
W: io::Write,
{
if self.state.sync_state.timeout.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<H, W>(&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_state.buffer.len() {
let byte = self.state.sync_state.buffer[i];
let mut performer = Performer::new(&mut self.state, handler, writer);
self.parser.advance(&mut performer, byte);
}
// Resetting state after processing makes sure we don't interpret buffered sync escapes.
self.state.sync_state.buffer.clear();
self.state.sync_state.timeout = None;
}
/// Synchronized update expiration time.
#[inline]
pub fn sync_timeout(&self) -> Option<&Instant> {
self.state.sync_state.timeout.as_ref()
}
/// Number of bytes in the synchronization buffer.
#[inline]
pub fn sync_bytes_count(&self) -> usize {
self.state.sync_state.buffer.len()
}
/// Process a new byte during a synchronized update.
#[cold]
fn advance_sync<H, W>(&mut self, handler: &mut H, byte: u8, writer: &mut W)
where
H: Handler,
W: io::Write,
{
self.state.sync_state.buffer.push(byte);
// Handle sync DCS escape sequences.
match self.state.sync_state.pending_dcs {
Some(_) => self.advance_sync_dcs_end(handler, byte, writer),
None => self.advance_sync_dcs_start(),
}
}
/// Find the start of sync DCS sequences.
fn advance_sync_dcs_start(&mut self) {
// Get the last few bytes for comparison.
let len = self.state.sync_state.buffer.len();
let offset = len.saturating_sub(SYNC_ESCAPE_START_LEN);
let end = &self.state.sync_state.buffer[offset..];
// Check for extension/termination of the synchronized update.
if end == SYNC_START_ESCAPE_START {
self.state.sync_state.pending_dcs = Some(Dcs::SyncStart);
} else if end == SYNC_END_ESCAPE_START || len >= SYNC_BUFFER_SIZE - 1 {
self.state.sync_state.pending_dcs = Some(Dcs::SyncEnd);
}
}
/// Parse the DCS termination sequence for synchronized updates.
fn advance_sync_dcs_end<H, W>(&mut self, handler: &mut H, byte: u8, writer: &mut W)
where
H: Handler,
W: io::Write,
{
match byte {
// Ignore DCS passthrough characters.
0x00..=0x17 | 0x19 | 0x1c..=0x7f | 0xa0..=0xff => (),
// Cancel the DCS sequence.
0x18 | 0x1a | 0x80..=0x9f => self.state.sync_state.pending_dcs = None,
// Dispatch on ESC.
0x1b => match self.state.sync_state.pending_dcs.take() {
Some(Dcs::SyncStart) => {
self.state.sync_state.timeout = Some(Instant::now() + SYNC_UPDATE_TIMEOUT);
},
Some(Dcs::SyncEnd) => self.stop_sync(handler, writer),
None => (),
},
}
}
}
/// Helper type that implements `vte::Perform`.
@ -114,28 +272,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<H, W>(&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 +308,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<W: io::Write>(&mut self, _: &mut W, _intermediate: Option<char>) {}
/// Report device status.
@ -428,8 +562,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<Mode> {
let private = match intermediate {
Some(b'?') => true,
@ -779,10 +911,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.dcs = Some(Dcs::SyncStart);
}
},
_ => debug!(
"[unhandled hook] params={:?}, ints: {:?}, ignore: {:?}, action: {:?}",
params, intermediates, ignore, action
),
}
}
#[inline]
@ -792,10 +932,15 @@ where
#[inline]
fn unhook(&mut self) {
debug!("[unhandled unhook]");
match self.state.dcs {
Some(Dcs::SyncStart) => {
self.state.sync_state.timeout = Some(Instant::now() + SYNC_UPDATE_TIMEOUT);
},
Some(Dcs::SyncEnd) => (),
_ => 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 +1317,7 @@ where
}
}
#[inline]
fn attrs_from_sgr_parameters(params: &mut ParamsIter<'_>) -> Vec<Option<Attr>> {
let mut attrs = Vec::with_capacity(params.size_hint().0);

View File

@ -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<Cow<'static, [u8]>>,
writing: Option<Writing>,
parser: ansi::Processor,
}
pub struct Notifier(pub Sender<Msg>);
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<Cow<'static, [u8]>>,
writing: Option<Writing>,
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 the event loop when a synchronized update timeout was reached.
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 => {