Improve pty reading and renderer synchronization

The pty read thread now runs the parser and directly updates the
terminal in the same thread. This obviates the need for a channel which
sends every char read from the pty; this is a huge performance boon.

Synchronization between the updater and the renderer is now achieved
with a PriorityMutex. Previously, an atomic bool was (poorly) used to
request the lock on terminal. The PriorityMutex is dead simple to use,
and it _Just Works_.
This commit is contained in:
Joe Wilm 2016-07-01 10:34:08 -07:00
parent d514b38223
commit ae39d38a15
3 changed files with 160 additions and 68 deletions

View File

@ -69,7 +69,7 @@ pub enum Utf8CharsError {
/// of a byte sequence well-formed in UTF-8, but ends prematurely.
///
/// Contains number of unused bytes
IncompleteUtf8(u8),
IncompleteUtf8(usize),
/// Variant representing that an I/O error occurred.
Io(Error),

View File

@ -47,12 +47,12 @@ pub mod ansi;
mod term;
mod util;
mod io;
mod sync;
use std::io::{Write, BufWriter, BufReader};
use std::sync::atomic::{AtomicBool, Ordering};
use std::io::{Write, BufWriter, Read};
use std::sync::{mpsc, Arc};
use parking_lot::Mutex;
use sync::PriorityMutex;
use config::Config;
use font::FontDesc;
@ -62,14 +62,7 @@ use term::Term;
use tty::process_should_exit;
use util::thread;
use io::Utf8Chars;
/// Things that the render/update thread needs to respond to
#[derive(Debug)]
enum Event {
PtyChar(char),
Glutin(glutin::Event),
}
use io::{Utf8Chars, Utf8CharsError};
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
enum ShouldExit {
@ -85,44 +78,39 @@ impl<'a, W: Write> input::Notify for WriteNotifier<'a, W> {
}
/// Channel used by resize handling on mac
static mut resize_sender: Option<mpsc::Sender<Event>> = None;
static mut resize_sender: Option<mpsc::Sender<glutin::Event>> = None;
/// Resize handling for Mac
fn window_resize_handler(width: u32, height: u32) {
unsafe {
if let Some(ref tx) = resize_sender {
let _ = tx.send(Event::Glutin(glutin::Event::Resized(width, height)));
let _ = tx.send(glutin::Event::Resized(width, height));
}
}
}
fn handle_event<W>(event: Event,
fn handle_event<W>(event: glutin::Event,
writer: &mut W,
terminal: &mut Term,
pty_parser: &mut ansi::Parser,
render_tx: &mpsc::Sender<(u32, u32)>,
input_processor: &mut input::Processor) -> ShouldExit
where W: Write
{
// Handle keyboard/mouse input and other window events
match event {
// Handle char from pty
Event::PtyChar(c) => pty_parser.advance(terminal, c),
// Handle keyboard/mouse input and other window events
Event::Glutin(gevent) => match gevent {
glutin::Event::Closed => return ShouldExit::Yes,
glutin::Event::ReceivedCharacter(c) => {
let encoded = c.encode_utf8();
writer.write(encoded.as_slice()).unwrap();
},
glutin::Event::Resized(w, h) => {
terminal.resize(w as f32, h as f32);
render_tx.send((w, h)).expect("render thread active");
},
glutin::Event::KeyboardInput(state, _code, key) => {
input_processor.process(state, key, &mut WriteNotifier(writer), *terminal.mode())
},
_ => ()
}
glutin::Event::Closed => return ShouldExit::Yes,
glutin::Event::ReceivedCharacter(c) => {
let encoded = c.encode_utf8();
writer.write(encoded.as_slice()).unwrap();
},
glutin::Event::Resized(w, h) => {
terminal.resize(w as f32, h as f32);
render_tx.send((w, h)).expect("render thread active");
},
glutin::Event::KeyboardInput(state, _code, key) => {
input_processor.process(state, key, &mut WriteNotifier(writer), *terminal.mode())
},
_ => ()
}
ShouldExit::No
@ -178,47 +166,68 @@ fn main() {
let terminal = Term::new(width as f32, height as f32, cell_width as f32, cell_height as f32);
let reader = terminal.tty().reader();
let mut reader = terminal.tty().reader();
let writer = terminal.tty().writer();
let mut glyph_cache = GlyphCache::new(rasterizer, desc, font.size());
let needs_render = Arc::new(AtomicBool::new(true));
let needs_render2 = needs_render.clone();
let (tx, rx) = mpsc::channel();
let reader_tx = tx.clone();
unsafe {
resize_sender = Some(tx.clone());
}
let terminal = Arc::new(PriorityMutex::new(terminal));
let term_ref = terminal.clone();
let term_updater = terminal.clone();
// Rendering thread
//
// Holds lock only when updating terminal
let reader_thread = thread::spawn_named("TTY Reader", move || {
let chars = Utf8Chars::new(BufReader::new(reader));
for c in chars {
let c = c.unwrap();
reader_tx.send(Event::PtyChar(c)).unwrap();
let mut buf = [0u8; 4096];
let mut start = 0;
let mut pty_parser = ansi::Parser::new();
'reader: loop {
let got = reader.read(&mut buf[start..]).expect("pty fd active");
let mut remain = 0;
// if `start` is nonzero, then actual bytes in buffer is > `got` by `start` bytes.
let end = start + got;
let mut terminal = term_updater.lock_low();
for c in Utf8Chars::new(&buf[..end]) {
match c {
Ok(c) => pty_parser.advance(&mut *terminal, c),
Err(err) => match err {
Utf8CharsError::IncompleteUtf8(unused) => {
remain = unused;
break;
},
_ => panic!("{}", err),
}
}
}
// Move any leftover bytes to front of buffer
for i in 0..remain {
buf[i] = buf[end - (remain - i)];
}
start = remain;
}
});
let terminal = Arc::new(Mutex::new(terminal));
let term_ref = terminal.clone();
let mut meter = Meter::new();
let mut pty_parser = ansi::Parser::new();
let window = Arc::new(window);
let window_ref = window.clone();
let (render_tx, render_rx) = mpsc::channel::<(u32, u32)>();
let update_thread = thread::spawn_named("Update", move || {
let mut input_processor = input::Processor::new();
'main_loop: loop {
let mut writer = BufWriter::new(&writer);
let mut input_processor = input::Processor::new();
// Handle case where renderer didn't acquire lock yet
if needs_render.load(Ordering::Acquire) {
::std::thread::yield_now();
continue;
}
if process_should_exit() {
break;
@ -231,11 +240,10 @@ fn main() {
};
// Need mutable terminal for updates; lock it.
let mut terminal = terminal.lock();
let mut terminal = terminal.lock_low();
let res = handle_event(event,
&mut writer,
&mut *terminal,
&mut pty_parser,
&render_tx,
&mut input_processor);
if res == ShouldExit::Yes {
@ -249,7 +257,6 @@ fn main() {
let res = handle_event(e,
&mut writer,
&mut *terminal,
&mut pty_parser,
&render_tx,
&mut input_processor);
@ -260,11 +267,6 @@ fn main() {
Err(mpsc::TryRecvError::Disconnected) => break 'main_loop,
Err(mpsc::TryRecvError::Empty) => break,
}
// Release the lock if a render is needed
if needs_render.load(Ordering::Acquire) {
break;
}
}
}
});
@ -283,7 +285,7 @@ fn main() {
// Initialize glyph cache
{
let terminal = term_ref.lock();
let terminal = term_ref.lock_high();
renderer.with_api(terminal.size_info(), |mut api| {
glyph_cache.init(&mut api);
});
@ -306,12 +308,8 @@ fn main() {
// Need scope so lock is released when swap_buffers is called
{
// Flag that it's time for render
needs_render2.store(true, Ordering::Release);
// Acquire term lock
let terminal = term_ref.lock();
// Have the lock, ok to lower flag
needs_render2.store(false, Ordering::Relaxed);
let terminal = term_ref.lock_high();
// Draw grid + cursor
{
@ -348,7 +346,7 @@ fn main() {
'event_processing: loop {
for event in window_ref.wait_events() {
tx.send(Event::Glutin(event)).unwrap();
tx.send(event).unwrap();
if process_should_exit() {
break 'event_processing;
}

94
src/sync.rs Normal file
View File

@ -0,0 +1,94 @@
// Copyright 2016 Joe Wilm, The Alacritty Project Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Synchronization types
//!
//! Most importantly, a priority mutex is included
use std::ops::{Deref, DerefMut};
use parking_lot::{Mutex, MutexGuard};
/// A priority mutex
///
/// A triple locking strategy is used where low priority locks must go through an additional mutex
/// to access the data. The gist is
///
/// Low priority: lock low, lock next, lock data, unlock next, {do work}, unlock data, unlock low
/// High priority: lock next, lock data, unlock next, {do work}, unlock data
///
/// By keeping the low lock active while working on data, a high priority consumer has immediate
/// access to the next mutex.
pub struct PriorityMutex<T> {
/// Data
data: Mutex<T>,
/// Next-to-access
next: Mutex<()>,
/// Low-priority access
low: Mutex<()>,
}
/// Mutex guard for low priority locks
pub struct LowPriorityMutexGuard<'a, T: 'a> {
data: MutexGuard<'a, T>,
_low: MutexGuard<'a, ()>,
}
impl<'a, T> Deref for LowPriorityMutexGuard<'a, T> {
type Target = T;
#[inline]
fn deref(&self) -> &T {
self.data.deref()
}
}
impl<'a, T> DerefMut for LowPriorityMutexGuard<'a, T> {
#[inline]
fn deref_mut(&mut self) -> &mut T {
self.data.deref_mut()
}
}
impl<T> PriorityMutex<T> {
/// Create a new priority mutex
pub fn new(data: T) -> PriorityMutex<T> {
PriorityMutex {
data: Mutex::new(data),
next: Mutex::new(()),
low: Mutex::new(()),
}
}
/// Lock the mutex with high priority
pub fn lock_high(&self) -> MutexGuard<T> {
// Must bind to a temporary or the lock will be freed before going
// into data.lock()
let _next = self.next.lock();
self.data.lock()
}
/// Lock the mutex with low priority
pub fn lock_low(&self) -> LowPriorityMutexGuard<T> {
let low = self.low.lock();
// Must bind to a temporary or the lock will be freed before going
// into data.lock()
let _next = self.next.lock();
let data = self.data.lock();
LowPriorityMutexGuard {
data: data,
_low: low,
}
}
}