最近发现打开某些行数“超级多”的古老 go 代码时,自动完成 和 保存 的时候都很卡,卡到什么程度呢? 完全无法正常使用的程度。

保存文件时 CPU 100%

先说下 文件保存 的时候的卡顿问题吧。

这个问题经过排查主要是 Ale 插件引起的。

let g:ale_fixers = {
\   '*': ['remove_trailing_lines', 'trim_whitespace'],
\   'go': ['gofmt', 'goimports'],
\}

let g:ale_fix_on_save = 1

这个配置会使 ale 在 go 文件保存的时候自动执行 gofmt 和 goimports fixer

触发问题的go文件大概有1万行代码左右。

6 核心 4.0 GHz 9代 CPU, 卡顿时间 大概是 60s.

问题点可以用vim自带的 profile 找出:

:profile start ale.log
:profile file *
:profile func *
:e foo.go
:q

profile 结果:

FUNCTIONS SORTED ON TOTAL TIME
count  total (s)   self (s)  function
    6  95.284024   0.000682  <SNR>131_NeoVimCallback()
    3  95.219276   0.001431  <SNR>128_ExitCallback()
    2  95.205686   0.051308  <SNR>149_HandleExit()
    3  95.158971   0.000787  <SNR>149_RunFixer()
    1  95.148690   0.003005  ale#fix#ApplyFixes()
    1  95.145444   0.008286  ale#fix#ApplyQueuedFixes()
    1  95.124312  95.124310  ale#util#SetBufferContents()
   11   0.628545   0.003539  scrollview#HandleMouse()
   11   0.618689   0.617861  <SNR>142_ReadInputStream()
  410   0.312603   0.005017  CapsStatusline()
  410   0.307586   0.037893  vimcaps#statusline()
  410   0.269693   0.266351  <SNR>110_lockstate()
    5   0.187795   0.000137  <SNR>142_RefreshBarsAsyncCallback()
    5   0.187657   0.000680  <SNR>142_RefreshBars()
    5   0.185126   0.062890  <SNR>142_ShowScrollbar()
    5   0.120451   0.002321  <SNR>142_CalculatePosition()
   10   0.116327             <SNR>142_VirtualLineCount()
    3   0.063975   0.047837  ale#util#JoinNeovimOutput()
  205   0.061899   0.005964  AleStatus()
    4   0.032224   0.000098  <SNR>79_on_refresh_event()
    


FUNCTIONS SORTED ON SELF TIME
count  total (s)   self (s)  function
    1  95.124312  95.124310  ale#util#SetBufferContents()
   11   0.618689   0.617861  <SNR>142_ReadInputStream()
  410   0.269693   0.266351  <SNR>110_lockstate()
   10              0.116327  <SNR>142_VirtualLineCount()
    5   0.185126   0.062890  <SNR>142_ShowScrollbar()
    2  95.205686   0.051308  <SNR>149_HandleExit()
    3   0.063975   0.047837  ale#util#JoinNeovimOutput()
  410   0.307586   0.037893  vimcaps#statusline()
    4   0.032126   0.031659  hexokinase#v2#scraper#on()
    7   0.020110   0.019948  <SNR>110_toggleoff()
10428              0.016139  <SNR>128_GatherOutput()
  410              0.014748  LspStatus()
  615              0.013326  LightlineFilename()
  410   0.013600   0.012902  ClapFilterImplStatusLine()
  812   0.019331   0.011470  <SNR>147_GetCounts()
    3   0.011297   0.011285  ale#job#Start()
 1032              0.009730  ale#engine#IsCheckingBuffer()
  205              0.008788  lightline#link()
  820   0.015992   0.008526  lightline#ale#linted()
    1  95.145444   0.008286  ale#fix#ApplyQueuedFixes()

提给官方了, 作者很快回复了,真的是佩服作者这认真态度。

作者提示说,将 foldmethod=syntax 改成 foldmethod=indent 性能会好很多:

A fix for this was attempted in  #3358, but caused  #3504, so we reverted it in  #3535. I’m interested in seeing a fix for this that doesn’t cause the same cursor position jumping issue.

One thing you can try is not using syntax folding if you’re using folding, or switching to folding based on indentation, because folds tend to ruin the performance of setting lines in Vim.

后来我修改 foldmethod=syntax 发现卡顿时间 确实是少了很多,从原来 60s 左右 降低到 15s 以下了。

但是问题还是存在,暂时来说这个 issue 还没有解决。

后面我禁用了ale 自动修复功能 ( let g:ale_fix_on_save = 0 ), 然后使用neovim 自带的 lsp format 和 codeAction 来执行自动 format 和 import 修复。

autocmd BufWritePre *.go lua vim.lsp.buf.formatting_sync(nil, 1000)
  function goimports(timeoutms)
    local context = { source = { organizeImports = true } }
    vim.validate { context = { context, "t", true } }

    local params = vim.lsp.util.make_range_params()
    params.context = context

    -- See the implementation of the textDocument/codeAction callback
    -- (lua/vim/lsp/handler.lua) for how to do this properly.
    local result = vim.lsp.buf_request_sync(0, "textDocument/codeAction", params, timeout_ms)
    if not result or next(result) == nil then return end
    local actions = result[1].result
    if not actions then return end
    local action = actions[1]

    -- textDocument/codeAction can return either Command[] or CodeAction[]. If it
    -- is a CodeAction, it can have either an edit, a command or both. Edits
    -- should be executed first.
    if action.edit or type(action.command) == "table" then
      if action.edit then
        vim.lsp.util.apply_workspace_edit(action.edit)
      end
      if type(action.command) == "table" then
        vim.lsp.buf.execute_command(action.command)
      end
    else
      vim.lsp.buf.execute_command(action)
    end
  end

vim.api.nvim_command('autocmd BufWritePre *.go lua goimports(1000)')

另外提一句: gopls 已经内置了 gofumpt 支持了, 配置 gofumpt = true 即可,老灯的配置如下:

[email protected] client: (required, vim.lsp.client)
local mix_attach = function(client)
  require('lsp-status').on_attach(client)
  require('completion').on_attach(client)
  require('illuminate').on_attach(client)
end

-- lsp.gopls.setup{}
-- https://github.com/neovim/nvim-lspconfig/blob/master/lua/lspconfig/gopls.lua
-- https://github.com/golang/tools/blob/master/gopls/doc/vim.md#neovim
-- https://github.com/neovim/nvim-lspconfig/blob/master/CONFIG.md#gopls
lsp.gopls.setup({
  on_attach = mix_attach,
  settings = {
    gopls = {
      usePlaceholders = true,
      completeUnimported = true,
      allowModfileModifications = true,
      allowImplicitNetworkAccess = true,
      usePlaceholders = true,
      -- https://github.com/golang/tools/blob/master/gopls/doc/settings.md#gofumpt-bool
      -- https://github.com/mvdan/gofumpt/commit/38fc491470bae6f44e2d38b06277dd95cf1bdf97
      -- https://go-review.googlesource.com/c/tools/+/241985/7/gopls/internal/hooks/hooks.go#22
      gofumpt = true,
    },
  },
  capabilities = lsp_status.capabilities,
})

自动完成或保存时 CPU 100%

这个问题排查了我很久,排查了很久的原因是,问题代码在 lua 调用 treesitter 的相关代码里。

直接用 vim 自带的 profile 没法找到影响性能的问题点。

另一个原因是,当时的 nvim lsp 也有 CPU 性能问题,大文件会导致 CPU 100%, 但是二者的情况不同。

nvim lsp 导致 CPU 100% 时 CPU 会很快恢复正常 Load, 而 p00f/nvim-ts-rainbow 引起的则不会。

当然,导致我排查了这很久,主要原因还是自己想当然,压根没有考虑到是 treesitter 相关的插件引起的。

因为这个 rainbow 配置成启用也就一行 rainbow = { enable = false, }, 而已。

用最小配置来测试,然后把怀疑有问题的插件加进来。当然,最后还是定位到原因了,那就是由 p00f/nvim-ts-rainbow 引起的。当然,我不是第一个发现这个问题的。别人早就提了:https://github.com/p00f/nvim-ts-rainbow/issues/5

当然,我也在这个 issue 下面回复了一下。但是这个作者并不鸟我, 倒是同样遇到这个问题的另一个用户 还 专门 跑到我的另一个 issue 回复我了。

当初切到这个 ts 版的 raibow 是因为 luochen1990/rainbow 与 nvim ts 有兼容性问题需要 hack 处理。

当 nvim CPU 100% 时,很久都不能恢复(几十秒以上)。此时如果强行 kill 掉 nvim 会在终端看到一大堆类似如下错误:

Error executing vim.schedule lua callback: /usr/local/share/nvim/runtime/lua/vim/treesitter/query.lua:135: bad argument #1 to 'get_node_text' (string expected, got nil)

切回 vimL 版的 luochen1990/rainbow:

Plug 'luochen1990/rainbow'
let g:rainbow_active = 1


lua <<EOF


-- fixup nvim-treesitter cause luochen1990/rainbow not working problem
-- see https://github.com/nvim-treesitter/nvim-treesitter/issues/123#issuecomment-651162962
-- https://github.com/nvim-treesitter/nvim-treesitter/issues/654#issuecomment-727562988
-- https://github.com/luochen1990/rainbow/issues/151#issuecomment-677644891
require "nvim-treesitter.highlight"
-- vim.TSHighlighter is removed, please use vim.treesitter.highlighter
-- see https://github.com/neovim/neovim/pull/14145/commits/b5401418768af496ef23b790f700a44b61ad784d
-- deactivate highlight of TSPunctBracket
local hlmap =vim.treesitter.highlighter.hl_map
hlmap.error = nil
hlmap["punctuation.delimiter"] = "Delimiter"
hlmap["punctuation.bracket"] = nil

EOF

refs

https://github.com/golang/tools/blob/master/gopls/doc/vim.md#neovim-imports

https://github.com/golang/tools/blob/master/gopls/doc/settings.md#gofumpt-bool

https://github.com/mvdan/gofumpt/commit/38fc491470bae6f44e2d38b06277dd95cf1bdf97

https://go-review.googlesource.com/c/tools/+/241985/7/gopls/internal/hooks/hooks.go#22

https://github.com/p00f/nvim-ts-rainbow/issues/5

https://github.com/dense-analysis/ale/issues/3634