Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ux): cache stdin queries on startup (remove startup delay) #2173

Merged
merged 2 commits into from
Feb 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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