Skip to content

Commit 4d92aa8

Browse files
committed
fix(core): compact parser raw outputs over certain size
1 parent 913925d commit 4d92aa8

1 file changed

Lines changed: 55 additions & 2 deletions

File tree

packages/nx/src/native/pseudo_terminal/pseudo_terminal.rs

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,16 +91,42 @@ impl PseudoTerminal {
9191
let quiet_clone = quiet.clone();
9292
let running_clone = running.clone();
9393

94-
let parser = Arc::new(RwLock::new(Parser::new(h, w, 10000)));
94+
// Scrollback size matches PtyInstance::SCROLLBACK_SIZE (1000 rows).
95+
// Larger values make resize reparse and all_contents_formatted() more
96+
// expensive, which blocks the parser write lock and starves rendering.
97+
const SCROLLBACK_SIZE: usize = 1000;
98+
// When raw output exceeds this threshold, compact the parser to prevent
99+
// unbounded Vec growth. Without this, extend_from_slice() eventually
100+
// triggers multi-hundred-millisecond reallocs under the write lock,
101+
// causing the TUI to hang progressively worse as output accumulates.
102+
const MAX_RAW_OUTPUT_BYTES: usize = 5 * 1024 * 1024; // 5 MB
103+
104+
let parser = Arc::new(RwLock::new(Parser::new(h, w, SCROLLBACK_SIZE)));
95105
let parser_clone = parser.clone();
96106
let stdout_tx_clone = stdout_tx.clone();
97107
std::thread::spawn(move || {
98108
let mut stdout = std::io::stdout();
99109
let mut buf = [0; 8 * 1024];
110+
// Local buffer for batching parser writes when inside the TUI.
111+
// Under firehose output, reader.read() returns a full 8KB buffer
112+
// on every call. Without batching, the parser write lock is
113+
// acquired on every 8KB chunk, leaving almost no gap for the
114+
// rendering thread's try_read(). By accumulating locally and
115+
// only flushing when the read returns short (PTY caught up) or
116+
// the buffer exceeds a threshold, we reduce lock acquisitions
117+
// and give rendering predictable windows to read.
118+
let mut pending_tui_buf: Vec<u8> = Vec::new();
119+
const BATCH_THRESHOLD: usize = 64 * 1024;
100120

101121
'read_loop: loop {
102122
if let Ok(len) = reader.read(&mut buf) {
103123
if len == 0 {
124+
// EOF — flush any remaining buffered data before exiting
125+
if is_within_nx_tui && !pending_tui_buf.is_empty() {
126+
let mut parser = parser_clone.write();
127+
parser.process(&pending_tui_buf);
128+
pending_tui_buf.clear();
129+
}
104130
break;
105131
}
106132
stdout_tx_clone
@@ -111,7 +137,34 @@ impl PseudoTerminal {
111137
debug!("Read {} bytes", len);
112138
if is_within_nx_tui {
113139
trace!("Processing data via vt100 for use in tui");
114-
parser_clone.write().process(&buf[..len]);
140+
pending_tui_buf.extend_from_slice(&buf[..len]);
141+
// Batch: only take the parser write lock when the PTY
142+
// reader has caught up (short read) or we've accumulated
143+
// enough data. Under firehose output, full-buffer reads
144+
// (len == buf.len()) indicate more data is immediately
145+
// available, so we defer processing. Short reads mean
146+
// we've drained the PTY buffer and the next read will
147+
// block, giving us a natural processing window.
148+
let should_flush =
149+
len < buf.len() || pending_tui_buf.len() >= BATCH_THRESHOLD;
150+
if should_flush {
151+
let mut parser = parser_clone.write();
152+
parser.process(&pending_tui_buf);
153+
pending_tui_buf.clear();
154+
// Compact when raw output grows too large. Replays
155+
// the formatted screen state (bounded by SCROLLBACK_SIZE)
156+
// through a fresh parser, keeping raw_output small.
157+
if parser.get_raw_output().len() > MAX_RAW_OUTPUT_BYTES {
158+
let screen = parser.screen();
159+
let (rows, cols) = screen.size();
160+
let formatted = screen.all_contents_formatted();
161+
let scrollback = screen.scrollback();
162+
let mut compacted = Parser::new(rows, cols, SCROLLBACK_SIZE);
163+
compacted.process(&formatted);
164+
compacted.screen_mut().set_scrollback(scrollback);
165+
*parser = compacted;
166+
}
167+
}
115168
}
116169

117170
if !quiet {

0 commit comments

Comments
 (0)