perf(net): near-real-time sandbox for non-host viewers
Two causes made a shared shell's PTY stream lag for non-host viewers and then arrive "all at once": 1. Nagle's algorithm — PTY echo/keystrokes/chat are tiny frames; Nagle + delayed-ACK (amplified by Tailscale RTT) coalesced them into bursts. Set TCP_NODELAY on the client websocket (Plain/--no-tls path) and on each server connection's socket (via ws.io_proto.transport). 2. Head-of-line blocking — ConnectionManager.broadcast awaited each send() serially under a global lock, so the slowest viewer gated delivery to everyone AND stalled the server's read of the broker's next frame, backing the stream up. Give each connection its own bounded outbound queue + writer task; broadcast now only enqueues and never waits on a socket. A peer that overflows its backlog (4096) is evicted (close 1013) and resyncs on reconnect rather than buffering without bound or being fed a gapped terminal stream. Init snapshot is enqueued as the guaranteed-first frame (send_state -> state_frame). Adds tests/test_manager.py (FIFO, slow-client isolation, wedged eviction, reconnect replacement). Also fmt-clean sbx.rs/theme.rs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -113,6 +113,15 @@ pub async fn connect(session: &Session) -> Result<Ws> {
|
||||
let (ws, _) = tokio_tungstenite::connect_async(&session.ws_url)
|
||||
.await
|
||||
.context("websocket connect")?;
|
||||
// Disable Nagle's algorithm. Sandbox PTY echo, keystrokes, and chat are all
|
||||
// small frames; Nagle holds a small segment waiting for more data until the
|
||||
// prior one is ACKed, and paired with delayed-ACK (~40 ms) — amplified by
|
||||
// Tailscale's RTT — that is a classic cause of "type, pause, then a burst"
|
||||
// lag for the non-host viewer. The default (and Tailscale) path is plaintext
|
||||
// ws, a `Plain` TcpStream, so set TCP_NODELAY there.
|
||||
if let MaybeTlsStream::Plain(s) = ws.get_ref() {
|
||||
let _ = s.set_nodelay(true);
|
||||
}
|
||||
Ok(ws)
|
||||
}
|
||||
|
||||
|
||||
+10
-2
@@ -553,7 +553,10 @@ pub fn vm_save_state(vm: &str, label: &str, local: bool) -> Result<String> {
|
||||
err.lines().last().unwrap_or("").trim()
|
||||
);
|
||||
}
|
||||
return Ok(format!("snapshot '{label}' + local appliance {}", path.display()));
|
||||
return Ok(format!(
|
||||
"snapshot '{label}' + local appliance {}",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
Ok(format!("snapshot '{label}' of {vm}"))
|
||||
}
|
||||
@@ -766,7 +769,12 @@ pub fn run_user_for(backend: Backend, owner: &str) -> String {
|
||||
/// `tar` inside the container/VM — uniform for files and directories, and no
|
||||
/// shell interpolation of the path. Blocking — run off the UI thread. Returns
|
||||
/// the in-sandbox destination path on success.
|
||||
pub fn push(backend: Backend, name: &str, run_user: &str, local: &std::path::Path) -> Result<String> {
|
||||
pub fn push(
|
||||
backend: Backend,
|
||||
name: &str,
|
||||
run_user: &str,
|
||||
local: &std::path::Path,
|
||||
) -> Result<String> {
|
||||
let (base, tar) = crate::ft::tar_path(local)?;
|
||||
match backend {
|
||||
Backend::Local => anyhow::bail!(
|
||||
|
||||
+9
-2
@@ -378,9 +378,16 @@ roster_width = 24
|
||||
let inks = [Color::Rgb(0xff, 0xff, 0xff), Color::Rgb(0xd0, 0xd0, 0xd0)];
|
||||
let bg = legible_bg(210.0, 0.5, &inks);
|
||||
for ink in inks {
|
||||
assert!(contrast(ink, bg) >= 4.5, "ink unreadable: {:.2}", contrast(ink, bg));
|
||||
assert!(
|
||||
contrast(ink, bg) >= 4.5,
|
||||
"ink unreadable: {:.2}",
|
||||
contrast(ink, bg)
|
||||
);
|
||||
}
|
||||
assert!(luminance(bg) > 0.0, "bright inks should permit a tinted, non-black bg");
|
||||
assert!(
|
||||
luminance(bg) > 0.0,
|
||||
"bright inks should permit a tinted, non-black bg"
|
||||
);
|
||||
|
||||
// A darker ink set forces a correspondingly deeper surface — the
|
||||
// relationship runs the right way.
|
||||
|
||||
Reference in New Issue
Block a user