From 55e4301036bb938474fc9768c41e28df867d9286 Mon Sep 17 00:00:00 2001 From: Andreas Schneider Date: Sat, 6 Jul 2024 11:44:19 +0200 Subject: [PATCH] feat(lsp): drop fswatch, use inotifywait (#29374) This patch replaces fswatch with inotifywait from inotify-toools: https://github.com/inotify-tools/inotify-tools fswatch takes ~1min to set up recursively for the Samba source code directory. inotifywait needs less than a second to do the same thing. https://github.com/emcrisostomo/fswatch/issues/321 Also it fswatch seems to be unmaintained in the meantime. Signed-off-by: Andreas Schneider --- .github/scripts/install_deps.sh | 2 +- runtime/doc/lua.txt | 17 ++++---- runtime/lua/vim/_watch.lua | 61 +++++++++++++---------------- runtime/lua/vim/lsp/_watchfiles.lua | 4 +- runtime/lua/vim/lsp/health.lua | 6 +-- test/functional/lua/watch_spec.lua | 9 +++-- test/functional/plugin/lsp_spec.lua | 8 ++-- 7 files changed, 54 insertions(+), 53 deletions(-) diff --git a/.github/scripts/install_deps.sh b/.github/scripts/install_deps.sh index 66f418eb10..b90a84fc24 100755 --- a/.github/scripts/install_deps.sh +++ b/.github/scripts/install_deps.sh @@ -30,7 +30,7 @@ if [[ $os == Linux ]]; then fi if [[ -n $TEST ]]; then - sudo apt-get install -y locales-all cpanminus attr libattr1-dev gdb fswatch + sudo apt-get install -y locales-all cpanminus attr libattr1-dev gdb inotify-tools # Use default CC to avoid compilation problems when installing Python modules CC=cc python3 -m pip -q install --user --upgrade pynvim diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 39a047cbab..dfaf666874 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -543,16 +543,19 @@ Example: File-change detection *watch-file* vim.api.nvim_command( "command! -nargs=1 Watch call luaeval('watch_file(_A)', expand(''))") < - *fswatch-limitations* -When on Linux and using fswatch, you may need to increase the maximum number -of `inotify` watches and queued events as the default limit can be too low. To -increase the limit, run: >sh - sysctl fs.inotify.max_user_watches=100000 - sysctl fs.inotify.max_queued_events=100000 + *inotify-limitations* +When on Linux you may need to increase the maximum number of `inotify` watches +and queued events as the default limit can be too low. To increase the limit, +run: >sh + sysctl fs.inotify.max_user_watches=494462 < -This will increase the limit to 100000 watches and queued events. These lines +This will increase the limit to 494462 watches and queued events. These lines can be added to `/etc/sysctl.conf` to make the changes persistent. +Note that each watch is a structure in the Kernel, thus available memory is +also a bottleneck for using inotify. In fact, a watch can take up to 1KB of +space. This means a million watches could result in 1GB of extra RAM usage. + Example: TCP echo-server *tcp-server* 1. Save this code to a file. 2. Execute it with ":luafile %". diff --git a/runtime/lua/vim/_watch.lua b/runtime/lua/vim/_watch.lua index 02b3f536c2..40f18ce5b0 100644 --- a/runtime/lua/vim/_watch.lua +++ b/runtime/lua/vim/_watch.lua @@ -227,11 +227,12 @@ end --- @param data string --- @param opts vim._watch.Opts? --- @param callback vim._watch.Callback -local function fswatch_output_handler(data, opts, callback) +local function on_inotifywait_output(data, opts, callback) local d = vim.split(data, '%s+') -- only consider the last reported event - local fullpath, event = d[1], d[#d] + local path, event, file = d[1], d[2], d[#d] + local fullpath = vim.fs.joinpath(path, file) if skip(fullpath, opts) then return @@ -240,20 +241,16 @@ local function fswatch_output_handler(data, opts, callback) --- @type integer local change_type - if event == 'Created' then + if event == 'CREATE' then change_type = M.FileChangeType.Created - elseif event == 'Removed' then + elseif event == 'DELETE' then change_type = M.FileChangeType.Deleted - elseif event == 'Updated' then + elseif event == 'MODIFY' then change_type = M.FileChangeType.Changed - elseif event == 'Renamed' then - local _, staterr, staterrname = uv.fs_stat(fullpath) - if staterrname == 'ENOENT' then - change_type = M.FileChangeType.Deleted - else - assert(not staterr, staterr) - change_type = M.FileChangeType.Created - end + elseif event == 'MOVED_FROM' then + change_type = M.FileChangeType.Deleted + elseif event == 'MOVED_TO' then + change_type = M.FileChangeType.Created end if change_type then @@ -265,24 +262,22 @@ end --- @param opts vim._watch.Opts? --- @param callback vim._watch.Callback Callback for new events --- @return fun() cancel Stops the watcher -function M.fswatch(path, opts, callback) - -- debounce isn't the same as latency but close enough - local latency = 0.5 -- seconds - if opts and opts.debounce then - latency = opts.debounce / 1000 - end - +function M.inotify(path, opts, callback) local obj = vim.system({ - 'fswatch', - '--event=Created', - '--event=Removed', - '--event=Updated', - '--event=Renamed', - '--event-flags', + 'inotifywait', + '--quiet', -- suppress startup messages + '--no-dereference', -- don't follow symlinks + '--monitor', -- keep listening for events forever '--recursive', - '--latency=' .. tostring(latency), - '--exclude', - '/.git/', + '--event', + 'create', + '--event', + 'delete', + '--event', + 'modify', + '--event', + 'move', + '@.git', -- ignore git directory path, }, { stderr = function(err, data) @@ -292,11 +287,11 @@ function M.fswatch(path, opts, callback) if data and #vim.trim(data) > 0 then vim.schedule(function() - if vim.fn.has('linux') == 1 and vim.startswith(data, 'Event queue overflow') then - data = 'inotify(7) limit reached, see :h fswatch-limitations for more info.' + if vim.fn.has('linux') == 1 and vim.startswith(data, 'Failed to watch') then + data = 'inotify(7) limit reached, see :h inotify-limitations for more info.' end - vim.notify('fswatch: ' .. data, vim.log.levels.ERROR) + vim.notify('inotify: ' .. data, vim.log.levels.ERROR) end) end end, @@ -306,7 +301,7 @@ function M.fswatch(path, opts, callback) end for line in vim.gsplit(data or '', '\n', { plain = true, trimempty = true }) do - fswatch_output_handler(line, opts, callback) + on_inotifywait_output(line, opts, callback) end end, -- --latency is locale dependent but tostring() isn't and will always have '.' as decimal point. diff --git a/runtime/lua/vim/lsp/_watchfiles.lua b/runtime/lua/vim/lsp/_watchfiles.lua index 49328fbe9b..98e9818bcd 100644 --- a/runtime/lua/vim/lsp/_watchfiles.lua +++ b/runtime/lua/vim/lsp/_watchfiles.lua @@ -9,8 +9,8 @@ local M = {} if vim.fn.has('win32') == 1 or vim.fn.has('mac') == 1 then M._watchfunc = watch.watch -elseif vim.fn.executable('fswatch') == 1 then - M._watchfunc = watch.fswatch +elseif vim.fn.executable('inotifywait') == 1 then + M._watchfunc = watch.inotify else M._watchfunc = watch.watchdirs end diff --git a/runtime/lua/vim/lsp/health.lua b/runtime/lua/vim/lsp/health.lua index ffe595ab37..18066a84db 100644 --- a/runtime/lua/vim/lsp/health.lua +++ b/runtime/lua/vim/lsp/health.lua @@ -90,8 +90,8 @@ local function check_watcher() watchfunc_name = 'libuv-watch' elseif watchfunc == vim._watch.watchdirs then watchfunc_name = 'libuv-watchdirs' - elseif watchfunc == vim._watch.fswatch then - watchfunc_name = 'fswatch' + elseif watchfunc == vim._watch.inotifywait then + watchfunc_name = 'inotifywait' else local nm = debug.getinfo(watchfunc, 'S').source watchfunc_name = string.format('Custom (%s)', nm) @@ -99,7 +99,7 @@ local function check_watcher() report_info('File watch backend: ' .. watchfunc_name) if watchfunc_name == 'libuv-watchdirs' then - report_warn('libuv-watchdirs has known performance issues. Consider installing fswatch.') + report_warn('libuv-watchdirs has known performance issues. Consider installing inotify-tools.') end end diff --git a/test/functional/lua/watch_spec.lua b/test/functional/lua/watch_spec.lua index bd8faadf5b..3d2dda716e 100644 --- a/test/functional/lua/watch_spec.lua +++ b/test/functional/lua/watch_spec.lua @@ -23,10 +23,13 @@ describe('vim._watch', function() local function run(watchfunc) it('detects file changes (watchfunc=' .. watchfunc .. '())', function() - if watchfunc == 'fswatch' then + if watchfunc == 'inotify' then skip(is_os('win'), 'not supported on windows') skip(is_os('mac'), 'flaky test on mac') - skip(not is_ci() and n.fn.executable('fswatch') == 0, 'fswatch not installed and not on CI') + skip( + not is_ci() and n.fn.executable('inotifywait') == 0, + 'inotify-tools not installed and not on CI' + ) end if watchfunc == 'watch' then @@ -123,5 +126,5 @@ describe('vim._watch', function() run('watch') run('watchdirs') - run('fswatch') + run('inotify') end) diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index 0630df65d5..2b8a7aed9e 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -5128,12 +5128,12 @@ describe('LSP', function() it( string.format('sends notifications when files change (watchfunc=%s)', watchfunc), function() - if watchfunc == 'fswatch' then + if watchfunc == 'inotify' then skip(is_os('win'), 'not supported on windows') skip(is_os('mac'), 'flaky test on mac') skip( - not is_ci() and fn.executable('fswatch') == 0, - 'fswatch not installed and not on CI' + not is_ci() and fn.executable('inotifywait') == 0, + 'inotify-tools not installed and not on CI' ) end @@ -5265,7 +5265,7 @@ describe('LSP', function() test_filechanges('watch') test_filechanges('watchdirs') - test_filechanges('fswatch') + test_filechanges('inotify') it('correctly registers and unregisters', function() local root_dir = '/some_dir'