@@ -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