vim.uv event loop: bounding RPC latency when concurrent clients queue behind plugin callbacks
0 reputation · 03 Sept 2022, 00:06 UTC
0 reputation · 03 Sept 2022, 00:06 UTC
Neovim services RPC requests from external clients on its single-threaded libuv event loop, so concurrent requests are effectively serialized. Documented behavior is that long-running Lua work in a callback — a timer-driven buffer scan, for instance — delays everything else pending on the main loop, including queued RPC responses.
The goal here is a configuration decision for a plugin that performs periodic Lua-heavy work while remote clients issue requests concurrently. Assume Neovim 0.10 or later, where vim.uv is the preferred alias for the libuv bindings and vim.loop remains available; confirm the alias and deprecation status for the pinned release before relying on either name.
uv.spawn-style child processes does not map directly onto it.Specific questions:
vim.schedule deferrals or vim.uv timer callbacks provide any fairness or ordering guarantee relative to queued RPC requests?33775 reputation · 03 Sept 2022, 11:39 UTC
Slice the Lua work into small chunks and yield control back to the libuv event loop after each chunk using a zero-timeout vim.uv timer (or coroutine.yield inside a timer callback). This lets queued RPC callbacks, which run in the poll phase, execute before the next Lua chunk resumes, keeping RPC latency bounded and predictable.
vim.uv is the preferred alias for the libuv bindings in Neovim 0.10+; vim.loop remains available. Confirm the alias and deprecation status against your pinned release before relying on either name.When a plugin performs CPU-intensive work, the event loop stays occupied until the Lua function returns. By inserting a yield, the loop can advance to the poll phase where RPC callbacks are processed, then return to the next chunk.
vim.uv.hrtime).local function process_chunk(state)
-- do one slice of work here
if not state.done then
local t = vim.uv.new_timer()
t:start(0, 0, function()
t:close()
process_chunk(state) -- resume with next chunk
end)
end
end
vim.schedule places a function at the end of the current loop turn; it does not yield. If the scheduled function is CPU-bound it still blocks the loop and offers no fairness relative to queued RPC callbacks.vim.uv timer with a 0 ms timeout schedules its callback for the next loop turn, allowing the current poll phase (where RPC callbacks run) to finish first. This provides the needed fairness, though it is cooperative scheduling rather than a hard ordering guarantee.Measure two intervals:
vim.uv.hrtime before and after the chunk.If RPC RTT grows in step with the measured chunk duration while per-chunk work time stays flat, the delay is event-loop starvation. If RTT increases without a corresponding rise in chunk work time, the slowdown is genuine request handling. As a control, run the same workload as one unchunked block: latency spikes should match the total work duration, confirming the starvation effect.
The starvation test requires instrumenting the plugin with vim.uv.hrtime around each chunk. If you cannot add such timestamps, the distinction cannot be made reliably; confirm whether you can instrument the plugin before tuning chunk sizes.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.