Merge pull request #10 from levineuwirth/lsp-frameloop-order
T M4.5 frame-loop reorder: tick_async last (1-frame LSP await latency)
This commit is contained in:
commit
83db560f48
|
|
@ -1092,9 +1092,13 @@ fn dispatcher_loop(
|
|||
Err(mpsc::RecvTimeoutError::Disconnected) => break,
|
||||
}
|
||||
|
||||
editor.tick_async();
|
||||
// `tick_async` last: the M4.5 async bridge settles awaiters
|
||||
// inside `tick_lsp` (via the message bus); draining + resuming
|
||||
// in the same frame keeps LSP `:await()` latency at one frame
|
||||
// instead of two. Mirrors the in-process loop in `editor::run`.
|
||||
editor.tick_processes();
|
||||
editor.tick_lsp();
|
||||
editor.tick_async();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -938,10 +938,20 @@ pub fn run(file: Option<PathBuf>) -> io::Result<()> {
|
|||
}
|
||||
}
|
||||
}
|
||||
state.tick_async();
|
||||
// `tick_async` runs *last*, after the supervisor/LSP/MCP
|
||||
// ticks have absorbed this frame's inbound I/O. The async
|
||||
// bridge (T M4.5) settles an awaiter inside `tick_lsp`/
|
||||
// `tick_mcp` by posting to the message bus; `tick_async`
|
||||
// drains that bus and resumes the parked coroutine. With
|
||||
// `tick_async` last, settle→resume happens in the *same*
|
||||
// frame; running it first would defer every LSP/MCP await
|
||||
// resumption by a full frame. The documented invariant is
|
||||
// only `tick_processes → tick_lsp → tick_mcp` (same-batch
|
||||
// supervisor I/O ordering), which is preserved.
|
||||
state.tick_processes();
|
||||
state.tick_lsp();
|
||||
state.tick_mcp();
|
||||
state.tick_async();
|
||||
}
|
||||
let _ = frontend.poll_event(Duration::from_millis(0));
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -3646,3 +3646,81 @@ fn m4_5_await_superseded_request_is_cancelled() {
|
|||
);
|
||||
let _ = state.lua_host.lua().load("pmacs.lsp.stop(_G._lsp)").exec();
|
||||
}
|
||||
|
||||
/// Regression for the T M4.5 frame-loop reorder. Drives the *exact*
|
||||
/// production tick order (`processes → lsp → mcp → async`) and asserts
|
||||
/// an awaited request resolves in the SAME frame its response was
|
||||
/// absorbed by `tick_lsp` — not the next one. Under the pre-reorder
|
||||
/// order (`async` first) this gap is 2 frames; here it must be 0.
|
||||
/// No other test drives production ordering (the suite open-codes
|
||||
/// per-test orders), so this is the only guard against a regression
|
||||
/// to `tick_async`-first.
|
||||
#[test]
|
||||
fn m4_5_await_resolves_same_frame_as_response_absorbed() {
|
||||
use pmacs::editor::EditorState;
|
||||
let mut state = EditorState::new();
|
||||
spawn_lsp_and_init(&mut state, None);
|
||||
state
|
||||
.lua_host
|
||||
.lua()
|
||||
.load(
|
||||
"_G._done=false
|
||||
pmacs.async(function()
|
||||
pmacs.lsp.request_completion(_G._lsp,'file:///x.rs',0,0):await()
|
||||
_G._done=true
|
||||
end)",
|
||||
)
|
||||
.exec()
|
||||
.expect("dispatch await coroutine");
|
||||
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
let mut absorbed_cycle: Option<u32> = None;
|
||||
let mut done_cycle: Option<u32> = None;
|
||||
let mut cycle: u32 = 0;
|
||||
while done_cycle.is_none() {
|
||||
assert!(Instant::now() < deadline, "await never resolved");
|
||||
cycle += 1;
|
||||
// Production order: processes → lsp → mcp → async.
|
||||
state.tick_processes();
|
||||
state.tick_lsp();
|
||||
// `tick_lsp`'s `handle_response` both absorbs into the store
|
||||
// and settles the awaiter (same call), so store-population is
|
||||
// a faithful proxy for "response absorbed this frame".
|
||||
if absorbed_cycle.is_none() {
|
||||
let n: i64 = state
|
||||
.lua_host
|
||||
.lua()
|
||||
.load(
|
||||
"local it = pmacs.completion.items(_G._lsp,'file:///x.rs') \
|
||||
return (it and #it) or 0",
|
||||
)
|
||||
.eval()
|
||||
.unwrap_or(0);
|
||||
if n > 0 {
|
||||
absorbed_cycle = Some(cycle);
|
||||
}
|
||||
}
|
||||
state.tick_mcp();
|
||||
state.tick_async();
|
||||
if done_cycle.is_none() {
|
||||
let done: bool = state
|
||||
.lua_host
|
||||
.lua()
|
||||
.load("return _G._done == true")
|
||||
.eval()
|
||||
.unwrap_or(false);
|
||||
if done {
|
||||
done_cycle = Some(cycle);
|
||||
}
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
let absorbed = absorbed_cycle.expect("completion store must populate");
|
||||
let done = done_cycle.expect("coroutine must finish");
|
||||
assert_eq!(
|
||||
absorbed, done,
|
||||
"await must resolve in the same frame the response is absorbed \
|
||||
(absorbed @cycle {absorbed}, done @cycle {done}); a positive gap \
|
||||
means tick_async ran before tick_lsp — the reorder regressed"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue