Skip to content
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
20 changes: 16 additions & 4 deletions cmux-tui/crates/cmux-tui/src/cli/app_focus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,23 @@ impl AppWorkspaces {
}
}

/// What the app's windows show (`snapshot.get`).
/// What the app's windows show (`snapshot.get`), from the app's last
/// published snapshot. Which workspace a window shows is the app's own
/// state, and no daemon write of this caller changes it (focus goes through
/// the app's focus actions, which answer after the window moved), so this
/// read takes no `after: "sync"` barrier: that barrier waits for a daemon
/// round trip and for the app's main actor to publish past it, and under
/// load it made each `workspace list` wait up to two 2 s app deadlines
/// (CLI-1). `after: 0` answers from any loaded snapshot, off the main actor.
pub(super) fn app_workspaces(stream: &mut UnixStream) -> Option<AppWorkspaces> {
let reply = super::app::request(stream, "snapshot.get", json!({}), super::app::READ_TIMEOUT)
.ok()?
.ok()?;
let reply = super::app::request(
stream,
"snapshot.get",
json!({ "after": 0 }),
super::app::READ_TIMEOUT,
)
.ok()?
.ok()?;
let shown = reply.pointer("/topology/focus/workspace").and_then(Value::as_str)?.to_owned();
let known = reply
.pointer("/topology/workspaces")
Expand Down
60 changes: 60 additions & 0 deletions cmux-tui/crates/cmux-tui/src/cli/app_focus/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,63 @@ fn screen_focus_runs_the_apps_screen_focus() {
assert_eq!(received[0]["params"]["action"], "screen.focus");
assert_eq!(received[0]["params"]["target"], "screen_1");
}

/// An app whose read barrier never settles: a request with `after: "sync"`
/// gets the app's own deadline error (its main actor is busy publishing,
/// TYPING UNDER LOAD L6), any other request the window's snapshot.
fn busy_app(snapshot: Value) -> (PathBuf, std::thread::JoinHandle<Vec<Value>>) {
let dir = cmux_unix_socket::short_test_dir("cmux-focus-busy");
let socket = dir.path().join("app.sock");
let listener = UnixListener::bind(&socket).unwrap();
let handle = std::thread::spawn(move || {
let (stream, _) = listener.accept().unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut writer = stream;
let mut received = Vec::new();
let mut line = String::new();
while reader.read_line(&mut line).unwrap() > 0 {
let request = serde_json::from_str::<Value>(&line).unwrap();
line.clear();
let response = if request["params"]["after"] == "sync" {
json!({"id": 1, "ok": false,
"error": {"code": "timeout", "message": "snapshot.get timed out after 2 s"}})
} else {
ok(snapshot.clone())
};
received.push(request);
writeln!(writer, "{response}").unwrap();
}
drop(dir);
received
});
(socket, handle)
}

/// CLI-1 (TYPING UNDER LOAD, STATUS 6290): `workspace list` asked the app
/// which workspace its window shows with the read barrier `after: "sync"`,
/// which waits for the app's main actor to publish a snapshot past a daemon
/// round trip. Under load each try hit the app's 2 s deadline and the CLI
/// asked twice: a 4.1 s list. Which workspace a window shows is the app's
/// own state, which no daemon write of the caller changes (focus goes
/// through the app's focus actions), so the overlay reads the published
/// snapshot and never waits for the main actor.
#[test]
fn workspace_list_reads_the_window_without_waiting_for_the_apps_read_barrier() {
let (socket, app) = busy_app(json!({
"topology": {"focus": {"workspace": "ws_home"},
"workspaces": [{"id": "ws_home"}, {"id": "ws_new"}]}
}));
let mut stream = UnixStream::connect(&socket).unwrap();
let mut reply = json!([
{"id": "ws_home", "name": "Home", "focused": false},
{"id": "ws_new", "name": "workspace-2", "focused": true}
]);
after_daemon_with(&mut stream, &follow_for(ResourceOperation::WorkspaceList), &mut reply)
.unwrap();
drop(stream);
let received = app.join().unwrap();
assert_eq!(received.len(), 1, "one snapshot read, no retry: {received:?}");
assert_ne!(received[0]["params"]["after"], "sync", "{received:?}");
assert_eq!(reply[0]["focused"], true, "{reply}");
assert_eq!(reply[1]["focused"], false, "{reply}");
}
Loading