From 735a7d0c9eb2dbda72d412b8b09866882e45b151 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Tue, 19 May 2026 00:03:36 +0200 Subject: [PATCH] feat(fswatch): report filewatchers in :checkhealth --- runtime/doc/news.txt | 1 + runtime/lua/vim/_watch.lua | 32 ++++++++++++++++++++++---- runtime/lua/vim/health/health.lua | 38 +++++++++++++++++++++++++++++++ runtime/lua/vim/lsp/health.lua | 1 + 4 files changed, 67 insertions(+), 5 deletions(-) diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 705dc93b5d..fb42c27ace 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -257,6 +257,7 @@ TUI UI +• |:checkhealth| shows filewatcher info in the Performance section. • These builtin "picker" menus delegate to |vim.ui.select()|: • :browse oldfiles • |:recover| diff --git a/runtime/lua/vim/_watch.lua b/runtime/lua/vim/_watch.lua index 3f7438dcac..29606b9685 100644 --- a/runtime/lua/vim/_watch.lua +++ b/runtime/lua/vim/_watch.lua @@ -10,6 +10,28 @@ M.FileChangeType = { Deleted = 3, } +--- Count of currently-active watchers. Each "watcher" here is one call to vim._watch.watch(), +--- vim._watch.watchdirs(), or vim._watch.inotify() that hasn't been cancelled yet. +M.active = { watch = 0, watchdirs = 0, inotify = 0 } + +--- Wraps a cancel function so the global counter is decremented exactly once, even if the caller +--- invokes the cancel function multiple times. +--- @param backend 'watch'|'watchdirs'|'inotify' +--- @param cancel fun() +--- @return fun() +local function tracked_cancel(backend, cancel) + M.active[backend] = M.active[backend] + 1 + local done = false + return function() + if done then + return + end + done = true + M.active[backend] = M.active[backend] - 1 + cancel() + end +end + --- @class vim._watch.Opts --- --- @field debounce? integer ms @@ -109,7 +131,7 @@ function M.watch(path, opts, callback) return function() end end - return function() + return tracked_cancel('watch', function() local _, stop_err = handle:stop() assert(not stop_err, stop_err) local is_closing, close_err = handle:is_closing() @@ -117,7 +139,7 @@ function M.watch(path, opts, callback) if not is_closing then handle:close() end - end + end) end --- Initializes and starts a |uv_fs_event_t| recursively watching every directory underneath the @@ -241,7 +263,7 @@ function M.watchdirs(path, opts, callback) timer:close() end - return cancel + return tracked_cancel('watchdirs', cancel) end --- @param data string @@ -328,9 +350,9 @@ function M.inotify(path, opts, callback) env = { LC_NUMERIC = 'C' }, }) - return function() + return tracked_cancel('inotify', function() obj:kill(2) - end + end) end return M diff --git a/runtime/lua/vim/health/health.lua b/runtime/lua/vim/health/health.lua index 74cad281ac..70da937751 100644 --- a/runtime/lua/vim/health/health.lua +++ b/runtime/lua/vim/health/health.lua @@ -169,6 +169,42 @@ local function check_config() end end +-- Note: this is part of check_performance(). +local function check_watchers() + health.start('Filewatchers') + local a = assert(vim._watch.active) + local total = a.watch + a.watchdirs + a.inotify + health.info( + ('filewatchers (vim._watch): %d (watch=%d, watchdirs=%d, inotify=%d)'):format( + total, + a.watch, + a.watchdirs, + a.inotify + ) + ) + + -- Walk libuv for an independent view. These counts include handles created outside `vim._watch` + -- (e.g. plugins calling `vim.uv.new_fs_event()` directly). + local libuv = { fs_event = 0, fs_poll = 0, process = 0, timer = 0 } + vim.uv.walk(function(handle) + if handle:is_closing() then + return + end + local t = handle:get_type() + if libuv[t] ~= nil then + libuv[t] = libuv[t] + 1 + end + end) + health.info( + ('libuv handles: fs_event=%d, fs_poll=%d, process=%d, timer=%d'):format( + libuv.fs_event, + libuv.fs_poll, + libuv.process, + libuv.timer + ) + ) +end + local function check_performance() health.start('Performance') @@ -199,6 +235,8 @@ local function check_performance() 'Slow shell invocation (took ' .. vim.fn.printf('%.2f', elapsed_time) .. ' seconds).' ) end + + check_watchers() end -- Load the remote plugin manifest file and check for unregistered plugins diff --git a/runtime/lua/vim/lsp/health.lua b/runtime/lua/vim/lsp/health.lua index 84e5924222..69b0ac16a1 100644 --- a/runtime/lua/vim/lsp/health.lua +++ b/runtime/lua/vim/lsp/health.lua @@ -112,6 +112,7 @@ local function check_active_clients() end end +-- See also runtime/lua/vim/health/health.lua:check_watchers() local function check_watcher() vim.health.start('vim.lsp: File Watcher')