fix(interactive): correct ConPTY terminal size + reduce lag
Sizing: the pty was spawned at the stale 80x24 default because terminal.Cols/Rows were read before any layout pass; and a resize during the spawn await was missed. Force UpdateLayout() before reading the size, subscribe Resized before spawn, resync once after, and add a LayoutUpdated-driven resync (deduped) as a safety net. Lag: the read loop awaited a UI-thread dispatch per chunk, serializing pipe reads behind rendering; switch to Dispatcher.Post (FIFO preserved, no backpressure).
This commit is contained in:
@@ -41,6 +41,8 @@ public sealed class PtyTerminalSession : IDisposable
|
||||
private CancellationTokenSource? _readCts;
|
||||
private readonly object _writeLock = new();
|
||||
private bool _disposed;
|
||||
private int _lastSyncedCols;
|
||||
private int _lastSyncedRows;
|
||||
|
||||
public bool IsRunning { get; private set; }
|
||||
public int? ExitCode { get; private set; }
|
||||
@@ -62,6 +64,19 @@ public sealed class PtyTerminalSession : IDisposable
|
||||
_terminal = terminal;
|
||||
_control = control;
|
||||
|
||||
// ApplyTemplate() only builds the control's visual tree — it does not run a layout
|
||||
// pass, so terminal.Cols/Rows would still be the TerminalOptions defaults (80x24) here,
|
||||
// not the pane's real size. Force a synchronous layout pass so TerminalView.ArrangeOverride
|
||||
// runs and sets terminal.Cols/Rows from the control's actual Bounds before we read them
|
||||
// for the child process's initial size.
|
||||
control.UpdateLayout();
|
||||
|
||||
// Subscribe before spawning (not after) so a resize that lands while SpawnAsync is still
|
||||
// being awaited (e.g. the surrounding layout settling further) isn't missed.
|
||||
terminal.DataReceived += OnTerminalDataReceived;
|
||||
terminal.Resized += OnTerminalResized;
|
||||
control.LayoutUpdated += OnControlLayoutUpdated;
|
||||
|
||||
var options = new PtyOptions
|
||||
{
|
||||
Name = "xterm-256color",
|
||||
@@ -75,11 +90,13 @@ public sealed class PtyTerminalSession : IDisposable
|
||||
|
||||
_connection = await PtyProvider.SpawnAsync(options, ct).ConfigureAwait(false);
|
||||
IsRunning = true;
|
||||
|
||||
terminal.DataReceived += OnTerminalDataReceived;
|
||||
terminal.Resized += OnTerminalResized;
|
||||
_connection.ProcessExited += OnConnectionProcessExited;
|
||||
|
||||
// The control may have been (re)arranged while the spawn above was in flight; resync once
|
||||
// now so the pty always starts at the terminal's current, real size rather than whatever
|
||||
// was captured in PtyOptions.
|
||||
SyncPtySize(terminal.Cols, terminal.Rows);
|
||||
|
||||
_readCts = new CancellationTokenSource();
|
||||
_ = ReadLoopAsync(_connection, terminal, _readCts.Token);
|
||||
|
||||
@@ -193,7 +210,16 @@ public sealed class PtyTerminalSession : IDisposable
|
||||
if (charCount <= 0) continue;
|
||||
var text = new string(chars, 0, charCount);
|
||||
|
||||
await Dispatcher.UIThread.InvokeAsync(() => terminal.Write(text));
|
||||
// Post (not InvokeAsync) so this loop doesn't block on a UI-thread round trip
|
||||
// per chunk — awaiting each dispatch here serialized pipe reads behind UI-thread
|
||||
// catch-up and was the dominant source of input/output lag. Dispatcher.Post
|
||||
// preserves FIFO order, so chunks still land in the order they were read; no
|
||||
// reordering or loss, just no backpressure from the UI thread onto the pipe read.
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
try { terminal.Write(text); }
|
||||
catch (Exception) { /* terminal may have been torn down mid-flight */ }
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
@@ -220,9 +246,27 @@ public sealed class PtyTerminalSession : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTerminalResized(object? sender, TerminalEvents.ResizeEventArgs e)
|
||||
private void OnTerminalResized(object? sender, TerminalEvents.ResizeEventArgs e) => SyncPtySize(e.Cols, e.Rows);
|
||||
|
||||
// Layoutable.LayoutUpdated fires after every layout pass in the window (not just when this
|
||||
// control's own bounds change), so it's a reliable, level-triggered safety net: whatever
|
||||
// TerminalView.ArrangeOverride last computed for terminal.Cols/Rows, make sure the pty agrees.
|
||||
// This covers races around the initial spawn where a Resized event could otherwise be missed.
|
||||
private void OnControlLayoutUpdated(object? sender, EventArgs e)
|
||||
{
|
||||
try { _connection?.Resize(e.Cols, e.Rows); }
|
||||
var terminal = _terminal;
|
||||
if (terminal is null) return;
|
||||
SyncPtySize(terminal.Cols, terminal.Rows);
|
||||
}
|
||||
|
||||
private void SyncPtySize(int cols, int rows)
|
||||
{
|
||||
var connection = _connection;
|
||||
if (connection is null) return;
|
||||
if (cols == _lastSyncedCols && rows == _lastSyncedRows) return;
|
||||
_lastSyncedCols = cols;
|
||||
_lastSyncedRows = rows;
|
||||
try { connection.Resize(cols, rows); }
|
||||
catch (Exception) { /* pty may already have exited */ }
|
||||
}
|
||||
|
||||
@@ -254,6 +298,7 @@ public sealed class PtyTerminalSession : IDisposable
|
||||
{
|
||||
_control.RemoveHandler(InputElement.KeyDownEvent, OnControlKeyDown);
|
||||
_control.RemoveHandler(InputElement.TextInputEvent, OnControlTextInput);
|
||||
_control.LayoutUpdated -= OnControlLayoutUpdated;
|
||||
}
|
||||
if (_connection is not null)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user