aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim
diff options
context:
space:
mode:
authorJustin M. Keyes <justinkz@gmail.com>2026-06-30 21:33:32 +0200
committerJustin M. Keyes <justinkz@gmail.com>2026-07-01 13:26:21 +0200
commit34efdadb4e865ff28e34dcce19e8abc1dd383651 (patch)
treeca8b296f778d4a46df358ab045a0b430a99c86e3 /runtime/lua/vim
parentd253acca35205a2277af8fae5cf4a3ea3e1bb302 (diff)
downloadrneovim-34efdadb4e865ff28e34dcce19e8abc1dd383651.tar.gz
rneovim-34efdadb4e865ff28e34dcce19e8abc1dd383651.tar.bz2
rneovim-34efdadb4e865ff28e34dcce19e8abc1dd383651.zip
feat(health): report ulimit info
Problem: Nvim shows `(libuv) kqueue(): Too many open files` on macos. ref https://github.com/neovim/neovim/issues/40238 Solution: Add a healthcheck for this situation.
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r--runtime/lua/vim/_core/proc.lua20
-rw-r--r--runtime/lua/vim/_meta/options.gen.lua50
-rw-r--r--runtime/lua/vim/health/health.lua56
3 files changed, 98 insertions, 28 deletions
diff --git a/runtime/lua/vim/_core/proc.lua b/runtime/lua/vim/_core/proc.lua
new file mode 100644
index 0000000000..36bde5309a
--- /dev/null
+++ b/runtime/lua/vim/_core/proc.lua
@@ -0,0 +1,20 @@
+-- OS/process utils.
+
+local M = {}
+
+--- Counts (approximate) open file descriptors for this process. Works on Linux/macOS/BSD.
+---@return integer? # Number of open fds, or nil if `/dev/fd` failed (fds exhausted, or unavailable/Windows).
+function M.count_open_fds()
+ local n = 0
+ for _, _, err in vim.fs.dir('/dev/fd', { err = true }) do
+ -- If `/dev/fd` scan failed (e.g. EMFILE when fds exhausted), count would be misleading.
+ if err then
+ return nil
+ end
+ n = n + 1
+ end
+ -- Discount the scan's own descriptor.
+ return math.max(0, n - 1)
+end
+
+return M
diff --git a/runtime/lua/vim/_meta/options.gen.lua b/runtime/lua/vim/_meta/options.gen.lua
index a647755522..d8773efc3b 100644
--- a/runtime/lua/vim/_meta/options.gen.lua
+++ b/runtime/lua/vim/_meta/options.gen.lua
@@ -7023,24 +7023,20 @@ vim.o.sua = vim.o.suffixesadd
vim.bo.suffixesadd = vim.o.suffixesadd
vim.bo.sua = vim.bo.suffixesadd
---- Use a swapfile for the buffer. This option can be reset when a
---- swapfile is not wanted for a specific buffer. For example, with
---- confidential information that even root must not be able to access.
---- Careful: All text will be in memory:
---- - Don't use this for big files.
+--- Use a `swap-file` for the buffer (if 'updatecount' is non-zero). The
+--- 'directory' option decides where swapfiles are stored.
+---
+--- To open a new buffer without creating a swapfile, use `:noswapfile`.
+--- To disable for an existing buffer, reset its 'swapfile' option.
+--- Careful:
--- - Recovery will be impossible!
---- A swapfile will only be present when 'updatecount' is non-zero and
---- 'swapfile' is set.
---- When 'swapfile' is reset, the swap file for the current buffer is
---- immediately deleted. When 'swapfile' is set, and 'updatecount' is
---- non-zero, a swap file is immediately created.
---- Also see `swap-file`.
---- If you want to open a new buffer without creating a swap file for it,
---- use the `:noswapfile` modifier.
---- See 'directory' for where the swap file is created.
----
---- This option is used together with 'bufhidden' and 'buftype' to
---- specify special kinds of buffers. See `special-buffers`.
+--- - The entire file will be in memory.
+---
+--- When reset, the swapfile for the current buffer is immediately
+--- deleted. When re-enabled (and 'updatecount' is non-zero), a swapfile
+--- is immediately created.
+---
+--- Used with 'bufhidden' and 'buftype' to specify `special-buffers`.
---
--- @type boolean
vim.o.swapfile = true
@@ -7702,17 +7698,15 @@ vim.o.ur = vim.o.undoreload
vim.go.undoreload = vim.o.undoreload
vim.go.ur = vim.go.undoreload
---- After typing this many characters the swap file will be written to
---- disk. When zero, no swap file will be created at all (see chapter on
---- recovery `crash-recovery`). 'updatecount' is set to zero by starting
---- Vim with the "-n" option, see `startup`. When editing in readonly
---- mode this option will be initialized to 10000.
---- The swapfile can be disabled per buffer with 'swapfile'.
---- When 'updatecount' is set from zero to non-zero, swap files are
---- created for all buffers that have 'swapfile' set. When 'updatecount'
---- is set to zero, existing swap files are not deleted.
---- This option has no meaning in buffers where 'buftype' is "nofile" or
---- "nowrite".
+--- The `swap-file` will be written after typing this many characters.
+---
+--- - Ignored in buffers where 'buftype' is "nofile" or "nowrite".
+--- - Initialized to 10000 when editing in readonly `-R` mode.
+--- - To disable swapfiles per-buffer, unset the 'swapfile' option.
+--- - To disable swapfiles globally, set this option to zero (or start
+--- with `-n`). See `crash-recovery`. Existing swapfiles are not deleted.
+--- - When re-enabled (from zero to non-zero), swapfiles are created for
+--- all buffers that have 'swapfile' set.
---
--- @type integer
vim.o.updatecount = 200
diff --git a/runtime/lua/vim/health/health.lua b/runtime/lua/vim/health/health.lua
index 2b26baf3bd..09b3882d40 100644
--- a/runtime/lua/vim/health/health.lua
+++ b/runtime/lua/vim/health/health.lua
@@ -208,6 +208,61 @@ local function check_watchers()
)
end
+--- Reads a single integer from a file (e.g. a `/proc/sys` entry).
+---@param path string
+---@return integer?
+local function read_int(path)
+ local ok, lines = pcall(vim.fn.readfile, path, '', 1)
+ local v = ok and lines[1] and tonumber(lines[1])
+ return v or nil
+end
+
+-- Note: this is part of check_performance().
+local function check_limits()
+ -- 'ulimit -n' (RLIMIT_NOFILE): each Nvim buffer may hold an open swapfile. Sockets, channels, filewatchers also consume file descriptors.
+ if vim.fn.has('win32') == 0 then
+ local ok, out = system({ 'sh', '-c', 'ulimit -Sn' }) -- Must use a shell.
+ out = vim.trim(out or '')
+ local soft = tonumber(out)
+ local used = require('vim._core.proc').count_open_fds()
+ local used_str = used and (', currently open: %d'):format(used) or ''
+ local raise_advice = {
+ '\'swapfile\' buffers, sockets, channels, and file-watchers consume file descriptors. A low limit causes "(libuv) kqueue(): Too many open files" (EMFILE) errors.',
+ 'Increase the limit: add `ulimit -n 8192` to your shell rc. On macOS see also `launchctl limit maxfiles`; on Linux see `/etc/security/limits.conf` and systemd `LimitNOFILE`.',
+ }
+ if out == 'unlimited' then
+ health.ok(('ulimit -n (max open files): unlimited%s'):format(used_str))
+ elseif not ok or soft == nil then
+ health.info(('ulimit -n (max open files): unknown (%s)%s'):format(out, used_str))
+ elseif soft < 1024 then
+ health.warn(('ulimit -n (max open files) is low: %d%s'):format(soft, used_str), raise_advice)
+ elseif used and used > soft * 0.8 then
+ health.warn(
+ ('Open files near the limit: %d of %d (ulimit -n)'):format(used, soft),
+ raise_advice
+ )
+ else
+ health.ok(('ulimit -n (max open files): %d%s'):format(soft, used_str))
+ end
+ end
+
+ -- Linux inotify limits filewatchers (LSP, vim._watch). Exhausting them reports ENOSPC ("no space on device").
+ if vim.uv.os_uname().sysname == 'Linux' then
+ local watches = read_int('/proc/sys/fs/inotify/max_user_watches')
+ local instances = read_int('/proc/sys/fs/inotify/max_user_instances')
+ if watches and watches < 8192 then
+ health.warn(('fs.inotify.max_user_watches is low: %d'):format(watches), {
+ 'Filewatchers may fail with ENOSPC. To increase the limit: `sysctl fs.inotify.max_user_watches=524288` (persist in /etc/sysctl.conf).',
+ })
+ end
+ if instances and instances < 128 then
+ health.warn(('fs.inotify.max_user_instances is low: %d'):format(instances), {
+ 'To increase the limit: `sysctl fs.inotify.max_user_instances=512` (persist in /etc/sysctl.conf).',
+ })
+ end
+ end
+end
+
local function check_performance()
health.start('Performance')
@@ -240,6 +295,7 @@ local function check_performance()
end
check_watchers()
+ check_limits()
end
-- Load the remote plugin manifest file and check for unregistered plugins