Skip to content

bug: message router polls every 33ms forever, even when idle with nothing to render #1225

Description

@BaruchWeka

Did you check docs and existing issues?

  • I have read all the noice.nvim docs
  • I have updated the plugin to the latest version before submitting this issue
  • I have searched the existing issues of noice.nvim
  • I have searched the existing issues of plugins related to this issue

Neovim version (nvim -v)

v0.11.6

Operating system/version

Ubuntu 24.03

Describe the bug

With noice loaded and Neovim completely idle, the process wakes up ~31 times/second forever. It never settles, even with no messages, no cmdline, no LSP progress and no UI activity of any kind. On a laptop this is a measurable idle-power cost — in my own config it was the second-largest source of idle wakeups.

The cause is that the router's updater interval is created with no enabled predicate:

-- lua/noice/message/router.lua:31
function M.enable()
  if not M._updater then
    M._updater = Util.interval(Config.options.throttle, Util.protect(M.update))
  end
  M._updater()
end

Util.interval only stops itself when keep() returns false:

-- lua/noice/util/init.lua:126
function T.keep()
  if M.is_exiting() then
    return false
  end
  return opts.enabled == nil or opts.enabled()
end

Since opts.enabled is nil here, keep() reduces to not is_exiting() — always true. So once Router.enable() runs at setup, the chained vim.defer_fn loop re-arms every throttle ms (default 1000/30 ≈ 33ms) for the whole session, calling M.update() on an empty queue.

Worth noting the contrast with the two intervals that do pass a predicate and correctly go quiet: hacks.fix_nohlsearch (enabled = vim.o.hlsearch and vim.v.hlsearch == 1) and health.checker (enabled = Config.is_running).

Steps To Reproduce

  1. Save the repro.lua below and start it with a known socket so the right process can be measured:

    nvim -u repro.lua --listen /tmp/repro.sock
  2. Leave it completely idle for ~45s (measuring earlier than that catches lazy.nvim still settling and reads high).

  3. Measure wakeups of the nvim --embed server process — get its pid from the instance itself rather than pgrep, so any other nvim you have open can't be picked by mistake (Linux):

    p=$(nvim --server /tmp/repro.sock --remote-expr 'getpid()')
    a=$(awk '/ctxt_switches/{s+=$2} END{print s}' /proc/$p/status); sleep 10
    b=$(awk '/ctxt_switches/{s+=$2} END{print s}' /proc/$p/status); echo "$(( (b-a)/10 )) wakeups/s"
  4. Inspect the loop's live timers from inside that same instance:

    :lua vim.uv.walk(function(h) if h:get_type()=="timer" and vim.uv.is_active(h) then
      print(("repeat=%s due_in=%s"):format(vim.uv.timer_get_repeat(h), vim.uv.timer_get_due_in(h))) end end)

Measured, running exactly the repro.lua below:

config idle wakeups/s active timers
repro.lua with spec = {} 0 none
repro.lua with noice only 31 two chained one-shots: due_in≈12 (router, 33ms) + due_in≈799 (health checker, 1000ms)

Raising opts.throttle scales it exactly as you'd expect — throttle = 200 drops it to ~5/s — which confirms the interval is the source, but it trades cmdline/message render latency for idle power, so it isn't really a fix.

Steps To Reproduce

-- repro.lua
vim.env.LAZY_STDPATH = ".repro"
load(vim.fn.system("curl -s https://raw.githubusercontent.com/folke/lazy.nvim/main/bootstrap.lua"))()

require("lazy.minit").repro({
  spec = {
    { "folke/noice.nvim", opts = {}, dependencies = { "MunifTanjim/nui.nvim" } },
  },
})

Expected Behavior

With nothing queued to render, the router shouldn't poll at all — 0 wakeups/s at idle, the way Neovim itself behaves (nvim --clean idles at exactly 0).

Passing an enabled predicate to Util.interval (roughly "queue non-empty / something is displayed") would make keep() return false and let the existing machinery stop the timer.

One wrinkle worth flagging, because it means this isn't a pure one-liner: nothing would restart it. The T.__call re-arm path is only invoked from Router.enable(), whereas the sites that react to new messages — lsp/progress.lua:57, source/notify.lua:67, ui/init.lua:88 and :147 — call Router.update() directly rather than poking the interval. Those callsites would need to call M._updater() instead, so the loop wakes back up when there's actually work.

Happy to put up a PR along those lines if that's the design you'd want, or to follow whatever shape you prefer.

Repro

vim.env.LAZY_STDPATH = ".repro"
load(vim.fn.system("curl -s https://raw.githubusercontent.com/folke/lazy.nvim/main/bootstrap.lua"))()

require("lazy.minit").repro({
  spec = {
    { "folke/noice.nvim", opts = {} },
    -- add any other plugins here
  },
})

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions