Skip to content

Commit

Permalink
fix(ux): cache stdin queries on startup (remove startup delay) (#2173)
Browse files Browse the repository at this point in the history
* fix(ux): cache stdin queries on startup

* style(fmt): rustfmt
  • Loading branch information
imsnif authored Feb 17, 2023
1 parent 5235407 commit 3a0e56a
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 140 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions zellij-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mio = { version = "0.7.11", features = ['os-ext'] }
serde = { version = "1.0", features = ["derive"] }
url = { version = "2.2.2", features = ["serde"] }
serde_yaml = "0.8"
serde_json = "1.0"
zellij-utils = { path = "../zellij-utils/", version = "0.34.5" }
log = "0.4.17"

Expand Down
14 changes: 2 additions & 12 deletions zellij-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,16 +265,6 @@ pub fn start_client(
os_api.send_to_server(ClientToServerMsg::TerminalResize(
os_api.get_terminal_size_using_fd(0),
));
// send a query to the terminal emulator in case the font size changed
// as well - we'll parse the response through STDIN
let terminal_emulator_query_string = stdin_ansi_parser
.lock()
.unwrap()
.window_size_change_query_string();
let _ = os_api
.get_stdout_writer()
.write(terminal_emulator_query_string.as_bytes())
.unwrap();
}
}),
Box::new({
Expand Down Expand Up @@ -348,7 +338,7 @@ pub fn start_client(

let mut stdout = os_input.get_stdout_writer();
stdout
.write_all("\u{1b}[1mLoading Zellij\u{1b}[m".as_bytes())
.write_all("\u{1b}[1mLoading Zellij\u{1b}[m\n\r".as_bytes())
.expect("cannot write to stdout");
stdout.flush().expect("could not flush");

Expand All @@ -368,7 +358,7 @@ pub fn start_client(
match client_instruction {
ClientInstruction::StartedParsingStdinQuery => {
stdout
.write_all("\n\rQuerying terminal emulator for \u{1b}[32;1mdefault colors\u{1b}[m and \u{1b}[32;1mpixel/cell\u{1b}[m ratio...".as_bytes())
.write_all("Querying terminal emulator for \u{1b}[32;1mdefault colors\u{1b}[m and \u{1b}[32;1mpixel/cell\u{1b}[m ratio...".as_bytes())
.expect("cannot write to stdout");
stdout.flush().expect("could not flush");
},
Expand Down
53 changes: 39 additions & 14 deletions zellij-client/src/stdin_ansi_parser.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
use std::time::{Duration, Instant};
use zellij_utils::consts::{VERSION, ZELLIJ_CACHE_DIR};

const STARTUP_PARSE_DEADLINE_MS: u64 = 500;
const SIGWINCH_PARSE_DEADLINE_MS: u64 = 200;
use zellij_utils::{
ipc::PixelDimensions, lazy_static::lazy_static, pane_size::SizeInPixels, regex::Regex,
};

use serde::{Deserialize, Serialize};
use std::fs::{File, OpenOptions};
use std::io::{Read, Write};
use std::path::PathBuf;
use zellij_utils::anyhow::Result;

#[derive(Debug)]
pub struct StdinAnsiParser {
raw_buffer: Vec<u8>,
Expand Down Expand Up @@ -43,18 +49,6 @@ impl StdinAnsiParser {
Some(Instant::now() + Duration::from_millis(STARTUP_PARSE_DEADLINE_MS));
query_string
}
pub fn window_size_change_query_string(&mut self) -> String {
// note that this assumes the String will be sent to the terminal emulator and so starts a
// deadline timeout (self.parse_deadline)

// <ESC>[14t => get text area size in pixels,
// <ESC>[16t => get character cell size in pixels
let query_string = String::from("\u{1b}[14t\u{1b}[16t");

self.parse_deadline =
Some(Instant::now() + Duration::from_millis(SIGWINCH_PARSE_DEADLINE_MS));
query_string
}
fn drain_pending_events(&mut self) -> Vec<AnsiStdinInstruction> {
let mut events = vec![];
events.append(&mut self.pending_events);
Expand Down Expand Up @@ -82,6 +76,34 @@ impl StdinAnsiParser {
}
self.drain_pending_events()
}
pub fn read_cache(&self) -> Option<Vec<AnsiStdinInstruction>> {
let path = self.cache_dir_path();
match OpenOptions::new().read(true).open(path.as_path()) {
Ok(mut file) => {
let mut json_cache = String::new();
file.read_to_string(&mut json_cache).ok()?;
let instructions =
serde_json::from_str::<Vec<AnsiStdinInstruction>>(&json_cache).ok()?;
if instructions.is_empty() {
None
} else {
Some(instructions)
}
},
Err(e) => {
log::error!("Failed to open STDIN cache file: {:?}", e);
None
},
}
}
pub fn write_cache(&self, events: Vec<AnsiStdinInstruction>) {
let path = self.cache_dir_path();
if let Ok(serialized_events) = serde_json::to_string(&events) {
if let Ok(mut file) = File::create(path.as_path()) {
let _ = file.write_all(serialized_events.as_bytes());
}
};
}
fn parse_byte(&mut self, byte: u8) {
if byte == b't' {
self.raw_buffer.push(byte);
Expand Down Expand Up @@ -112,9 +134,12 @@ impl StdinAnsiParser {
self.raw_buffer.push(byte);
}
}
fn cache_dir_path(&self) -> PathBuf {
ZELLIJ_CACHE_DIR.join(&format!("zellij-stdin-cache-v{}", VERSION))
}
}

#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AnsiStdinInstruction {
PixelDimensions(PixelDimensions),
BackgroundColor(String),
Expand Down
53 changes: 38 additions & 15 deletions zellij-client/src/stdin_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,37 @@ pub(crate) fn stdin_loop(
let mut holding_mouse = false;
let mut input_parser = InputParser::new();
let mut current_buffer = vec![];
// on startup we send a query to the terminal emulator for stuff like the pixel size and colors
// we get a response through STDIN, so it makes sense to do this here
send_input_instructions
.send(InputInstruction::StartedParsing)
.unwrap();
let terminal_emulator_query_string = stdin_ansi_parser
.lock()
.unwrap()
.terminal_emulator_query_string();
let _ = os_input
.get_stdout_writer()
.write(terminal_emulator_query_string.as_bytes())
.unwrap();
let query_duration = stdin_ansi_parser.lock().unwrap().startup_query_duration();
send_done_parsing_after_query_timeout(send_input_instructions.clone(), query_duration);
{
// on startup we send a query to the terminal emulator for stuff like the pixel size and colors
// we get a response through STDIN, so it makes sense to do this here
let mut stdin_ansi_parser = stdin_ansi_parser.lock().unwrap();
match stdin_ansi_parser.read_cache() {
Some(events) => {
let _ =
send_input_instructions.send(InputInstruction::AnsiStdinInstructions(events));
let _ = send_input_instructions
.send(InputInstruction::DoneParsing)
.unwrap();
},
None => {
send_input_instructions
.send(InputInstruction::StartedParsing)
.unwrap();
let terminal_emulator_query_string =
stdin_ansi_parser.terminal_emulator_query_string();
let _ = os_input
.get_stdout_writer()
.write(terminal_emulator_query_string.as_bytes())
.unwrap();
let query_duration = stdin_ansi_parser.startup_query_duration();
send_done_parsing_after_query_timeout(
send_input_instructions.clone(),
query_duration,
);
},
}
}
let mut ansi_stdin_events = vec![];
loop {
let buf = os_input.read_from_stdin();
{
Expand All @@ -54,12 +70,19 @@ pub(crate) fn stdin_loop(
if stdin_ansi_parser.should_parse() {
let events = stdin_ansi_parser.parse(buf);
if !events.is_empty() {
ansi_stdin_events.append(&mut events.clone());
let _ = send_input_instructions
.send(InputInstruction::AnsiStdinInstructions(events));
}
continue;
}
}
if !ansi_stdin_events.is_empty() {
stdin_ansi_parser
.lock()
.unwrap()
.write_cache(ansi_stdin_events.drain(..).collect());
}
current_buffer.append(&mut buf.to_vec());
let maybe_more = false; // read_from_stdin should (hopefully) always empty the STDIN buffer completely
let mut events = vec![];
Expand Down
95 changes: 0 additions & 95 deletions zellij-client/src/unit/stdin_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,98 +317,3 @@ pub fn move_focus_left_in_normal_mode() {
"All actions sent to server properly"
);
}

#[test]
pub fn terminal_info_queried_from_terminal_emulator() {
let events_sent_to_server = Arc::new(Mutex::new(vec![]));
let command_is_executing = CommandIsExecuting::new();
let client_os_api = FakeClientOsApi::new(events_sent_to_server, command_is_executing);

let client_os_api_clone = client_os_api.clone();
let (send_input_instructions, _receive_input_instructions): ChannelWithContext<
InputInstruction,
> = channels::bounded(50);
let send_input_instructions = SenderWithContext::new(send_input_instructions);
let stdin_ansi_parser = Arc::new(Mutex::new(StdinAnsiParser::new()));

let stdin_thread = thread::Builder::new()
.name("stdin_handler".to_string())
.spawn({
move || {
stdin_loop(
Box::new(client_os_api),
send_input_instructions,
stdin_ansi_parser,
)
}
});
std::thread::sleep(std::time::Duration::from_millis(500)); // wait for initial query to be sent

let extracted_stdout_buffer = client_os_api_clone.stdout_buffer();
let mut expected_query =
String::from("\u{1b}[14t\u{1b}[16t\u{1b}]11;?\u{1b}\u{5c}\u{1b}]10;?\u{1b}\u{5c}");
for i in 0..256 {
expected_query.push_str(&format!("\u{1b}]4;{};?\u{1b}\u{5c}", i));
}
assert_eq!(
String::from_utf8(extracted_stdout_buffer),
Ok(expected_query),
);
drop(stdin_thread);
}

#[test]
pub fn pixel_info_sent_to_server() {
let fake_stdin_buffer = read_fixture("terminal_emulator_startup_response");
let events_sent_to_server = Arc::new(Mutex::new(vec![]));
let command_is_executing = CommandIsExecuting::new();
let client_os_api =
FakeClientOsApi::new(events_sent_to_server.clone(), command_is_executing.clone())
.with_stdin_buffer(fake_stdin_buffer);
let config = Config::from_default_assets().unwrap();
let options = Options::default();

let (send_client_instructions, _receive_client_instructions): ChannelWithContext<
ClientInstruction,
> = channels::bounded(50);
let send_client_instructions = SenderWithContext::new(send_client_instructions);

let (send_input_instructions, receive_input_instructions): ChannelWithContext<
InputInstruction,
> = channels::bounded(50);
let send_input_instructions = SenderWithContext::new(send_input_instructions);
let stdin_ansi_parser = Arc::new(Mutex::new(StdinAnsiParser::new()));
let stdin_thread = thread::Builder::new()
.name("stdin_handler".to_string())
.spawn({
let client_os_api = client_os_api.clone();
move || {
stdin_loop(
Box::new(client_os_api),
send_input_instructions,
stdin_ansi_parser,
)
}
});

let default_mode = InputMode::Normal;
let input_thread = thread::Builder::new()
.name("input_handler".to_string())
.spawn({
move || {
input_loop(
Box::new(client_os_api),
config,
options,
command_is_executing,
send_client_instructions,
default_mode,
receive_input_instructions,
)
}
});
std::thread::sleep(std::time::Duration::from_millis(1000)); // wait for initial query to be sent
assert_snapshot!(*format!("{:?}", events_sent_to_server.lock().unwrap()));
drop(stdin_thread);
drop(input_thread);
}
11 changes: 7 additions & 4 deletions zellij-server/src/route.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::collections::VecDeque;
use std::sync::{Arc, RwLock};

use crate::{
Expand Down Expand Up @@ -670,7 +671,7 @@ macro_rules! send_to_screen_or_retry_queue {
None => {
log::warn!("Server not ready, trying to place instruction in retry queue...");
if let Some(retry_queue) = $retry_queue.as_mut() {
retry_queue.push($instruction);
retry_queue.push_back($instruction);
}
Ok(())
},
Expand All @@ -686,15 +687,17 @@ pub(crate) fn route_thread_main(
mut receiver: IpcReceiverWithContext<ClientToServerMsg>,
client_id: ClientId,
) -> Result<()> {
let mut retry_queue = vec![];
let mut retry_queue = VecDeque::new();
let err_context = || format!("failed to handle instruction for client {client_id}");
'route_loop: loop {
match receiver.recv() {
Some((instruction, err_ctx)) => {
err_ctx.update_thread_ctx();
let rlocked_sessions = session_data.read().to_anyhow().with_context(err_context)?;
let handle_instruction = |instruction: ClientToServerMsg,
mut retry_queue: Option<&mut Vec<ClientToServerMsg>>|
mut retry_queue: Option<
&mut VecDeque<ClientToServerMsg>,
>|
-> Result<bool> {
let mut should_break = false;
match instruction {
Expand Down Expand Up @@ -837,7 +840,7 @@ pub(crate) fn route_thread_main(
}
Ok(should_break)
};
for instruction_to_retry in retry_queue.drain(..) {
while let Some(instruction_to_retry) = retry_queue.pop_front() {
log::warn!("Server ready, retrying sending instruction.");
let should_break = handle_instruction(instruction_to_retry, None)?;
if should_break {
Expand Down

0 comments on commit 3a0e56a

Please sign in to comment.