Skip to content

Commit

Permalink
Add support for synchronized updates
Browse files Browse the repository at this point in the history
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 alacritty#598.
  • Loading branch information
chrisduerr committed Feb 20, 2021
1 parent 202127a commit 3502fef
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 69 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`/`DCS = 2 s`

### Fixed

Expand Down
41 changes: 26 additions & 15 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion alacritty/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions alacritty_terminal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
157 changes: 123 additions & 34 deletions alacritty_terminal/src/ansi.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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<Rgb> {
if !color.is_empty() && color[0] == b'#' {
Expand Down Expand Up @@ -81,15 +94,109 @@ fn parse_number(input: &[u8]) -> Option<u8> {
Some(num)
}

/// Internal state for VTE processor.
struct ProcessorState {
/// Last processed character for repetition.
preceding_char: Option<char>,

/// Beginning of a synchronized update.
sync_start: Option<Instant>,

/// Bytes read during a synchronized update.
sync_bytes: Vec<u8>,
}

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<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_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<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_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<H, W>(&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`.
Expand All @@ -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<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
Expand Down Expand Up @@ -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<W: io::Write>(&mut self, _: &mut W, _intermediate: Option<char>) {}

/// Report device status.
Expand Down Expand Up @@ -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<Mode> {
let private = match intermediate {
Some(b'?') => true,
Expand Down Expand Up @@ -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]
Expand All @@ -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\\" };
Expand Down Expand Up @@ -1172,6 +1260,7 @@ where
}
}

#[inline]
fn attrs_from_sgr_parameters(params: &mut ParamsIter<'_>) -> Vec<Option<Attr>> {
let mut attrs = Vec::with_capacity(params.size_hint().0);

Expand Down
Loading

0 comments on commit 3502fef

Please sign in to comment.