mfussenegger / nvim-jdtls

Extensions for the built-in LSP support in Neovim for eclipse.jdt.ls
GNU General Public License v3.0
1k stars 62 forks source link

Random flood of $/cancelRequest operations sent to LSP server #535

Closed mikehaertl closed 1 year ago

mikehaertl commented 1 year ago

LSP client configuration

When working on Java files from time to time it happens that nvim gets terribly slow up to the point where I can only quit and restart the editor.

Eclipse.jdt.ls version

1.24.0

Steps to Reproduce

I can not provide a clear procedure to reproduce as the effect seems to be random. And the codebase I'm working on is private.

All I can say is:

Expected Result

No slowdown.

Actual Result

The debug log is flooded with requests like this:

[DEBUG][2023-07-24 14:04:36] .../vim/lsp/rpc.lua:284    "rpc.send"  {  jsonrpc = "2.0",  method = "$/cancelRequest",  params = {    id = 2  }}
[DEBUG][2023-07-24 14:04:36] .../vim/lsp/rpc.lua:284    "rpc.send"  {  jsonrpc = "2.0",  method = "$/cancelRequest",  params = {    id = 2  }}
[DEBUG][2023-07-24 14:04:36] .../vim/lsp/rpc.lua:284    "rpc.send"  {  jsonrpc = "2.0",  method = "$/cancelRequest",  params = {    id = 2  }}

The requests come in blocks of growing size: First I have about 5-10 requests in a row. Over the course of some seconds I see about 100 requests in a row. After some minutes it ramps up into blocks of 1000s of requests with incremental id:

...
[DEBUG][2023-07-24 14:16:43] .../vim/lsp/rpc.lua:284    "rpc.send"  {  jsonrpc = "2.0",  method = "$/cancelRequest",  params = {    id = 2197  }}
[DEBUG][2023-07-24 14:16:43] .../vim/lsp/rpc.lua:284    "rpc.send"  {  jsonrpc = "2.0",  method = "$/cancelRequest",  params = {    id = 2198  }}
[DEBUG][2023-07-24 14:16:43] .../vim/lsp/rpc.lua:284    "rpc.send"  {  jsonrpc = "2.0",  method = "$/cancelRequest",  params = {    id = 2199  }}
[DEBUG][2023-07-24 14:16:43] .../vim/lsp/rpc.lua:284    "rpc.send"  {  jsonrpc = "2.0",  method = "$/cancelRequest",  params = {    id = 2200  }}

In a couple of minutes the debug log grew to 350 MB.

I could provide the debug log on a private chanel if that helps.

mikehaertl commented 1 year ago

Addition: I'm reporting this here even though it could also be a problem in the LSP implementation of neovim. I just had no idea where to start searching. If it's clearly a problem of neovim I can also report the issue there.

Any pointers or ideas how to further debug this are welcome.

mikehaertl commented 1 year ago

Forgot to add my jdtls config:

-- See `:help vim.lsp.start_client` for an overview of the supported `config` options.

-- nvim data and config path (:h stdpath)
local nvim_data_path = vim.fn.stdpath('data') .. '/'
local nvim_config_path = vim.fn.stdpath('config') .. '/'
-- location where mason installs jdts
local mason_package_path = nvim_data_path .. 'mason/packages/'
local jdtls_path = mason_package_path .. 'jdtls/'
-- root project dir of currently opened file
local project_dir = require('jdtls.setup').find_root({'.git', 'mvnw', 'gradlew'})
-- last part of the project_dir
local project_name = vim.fn.fnamemodify(project_dir, ':p:h:t')

local jdtls = require('jdtls')
local util = require('jdtls.util')
local dap = require('dap')
local dapui = require('dapui')
local widgets = require('dap.ui.widgets')
local whichkey = require('which-key')

local config = {
  -- The command that starts the language server
  -- See: https://github.com/eclipse/eclipse.jdt.ls#running-from-the-command-line
  cmd = {

    '/home/mike/.sdkman/candidates/java/17.0.6-tem/bin/java',

    '-Declipse.application=org.eclipse.jdt.ls.core.id1',
    '-Dosgi.bundles.defaultStartLevel=4',
    '-Declipse.product=org.eclipse.jdt.ls.core.product',
    '-Dlog.protocol=true',
    '-Dlog.level=ALL',
    '-Xmx1g',
    '--add-modules=ALL-SYSTEM',
    '--add-opens', 'java.base/java.util=ALL-UNNAMED',
    '--add-opens', 'java.base/java.lang=ALL-UNNAMED',

    '-javaagent:' .. jdtls_path .. 'lombok.jar',
    '-jar',  vim.fn.glob(jdtls_path .. 'plugins/org.eclipse.equinox.launcher_*.jar', 1),
    '-configuration', (jdtls_path .. 'config_linux'),

    -- directory where jdtls stores project related data
    '-data', (nvim_data_path .. 'jdtls_data/' .. project_name),
  },

  -- One dedicated LSP server & client will be started per unique root_dir
  root_dir = project_dir,

  -- Inject nvim-cmp capabilities, e.g. to support snippets provided by jdtls
  capabilities = require('cmp_nvim_lsp').default_capabilities(),

  -- Here you can configure eclipse.jdt.ls specific settings
  -- See https://github.com/eclipse/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line#initialize-request
  -- for a list of options
  settings = {
    java = {
      autobuild = { enabled = false },
      codeGeneration = {
        hashCodeEquals = {
          useJava7Objects = true,
        },
        useBlocks = true,
        toString = {
          template = "${object.className}{${member.name()}=${member.value}, ${otherMembers}}"
        },
      },
      completion = {
        -- Static members or types with static members. Content assist will
        -- propose those static members even if the import is missing.
        favoriteStaticMembers = {
          "java.lang.String.*",
          "org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.*",
          "org.springframework.security.test.web.servlet.response.SecurityMockMvcResultHandlers.*",
          "org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.*",
          "org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*",
          "org.springframework.test.web.client.match.MockRestRequestMatchers.*",
          "org.springframework.test.web.client.response.MockRestResponseCreators.*",
          "org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*",
          "org.springframework.test.web.servlet.result.MockMvcResultMatchers.*",
          "org.assertj.core.api.Assertions.*",
          "org.awaitility.Awaitility.*",
          "org.hamcrest.Matchers.*",
          "org.mockito.BDDMockito.*",
          "org.mockito.Mockito.*",
          "org.mockito.ArgumentMatchers.*",
          -- Defaults:
          "org.junit.Assert.*",
          "org.junit.Assume.*",
          "org.junit.jupiter.api.Assertions.*",
          "org.junit.jupiter.api.Assumptions.*",
          "org.junit.jupiter.api.DynamicContainer.*",
          "org.junit.jupiter.api.DynamicTest.*",
        },

        -- Members or types that should be excluded from completion.
        -- Useful for nasty suggestions that are never used and only add noise
        -- to suggestions e.g  "java.awt.*"
        filteredTypes = {
        },
      },
      configuration = {
        -- See https://github.com/eclipse/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line#initialize-request
        -- And search for `interface RuntimeOption`
        -- The `name` is NOT arbitrary, but must match one of the elements from `enum ExecutionEnvironment` in the link above
        runtimes = {
          {
            name = "JavaSE-11",
            path = "/usr/lib/jvm/java-11-openjdk-amd64/",
          },
          {
            name = "JavaSE-17",
            path = "/home/mike/.sdkman/candidates/java/17.0.6-tem/",
          },
          {
            name = "JavaSE-20",
            path = "/home/mike/.sdkman/candidates/java/20.0.1-tem/",
          },
        },
      },
      contentProvider = {
        preferred = 'fernflower'
      },
      format = {
        settings = {
          -- Formatter specifications are in an XML format as used by Eclipse [1].
          -- Our file is a copy from vscode-java [2] with local modifications.
          --
          --  [1] https://github.com/redhat-developer/vscode-java/wiki/Formatter-settings
          --  [2] https://github.com/redhat-developer/vscode-java/blob/master/formatters/eclipse-formatter.xml
          url = nvim_config_path .. 'eclipse-formatter.xml',
          profile = 'Eclipse'
        },
      },
      -- Should make gradle use java 11 instead of system default, but has no effect:
      -- import = {
      --   gradle = {
      --     java = {
      --       home = '/usr/lib/jvm/java-11-openjdk-amd64/',
      --     }
      --   }
      -- },
      signatureHelp = {
        description = { enabled = true },
      },
      sources = {
        organizeImports = {
          starThreshold = 9999;
          staticStarThreshold = 9999;
        },
      },
    },
  },

  -- Language server `initializationOptions`
  -- You need to extend the `bundles` with paths to jar files
  -- if you want to use additional eclipse.jdt.ls plugins.
  --
  -- See https://github.com/mfussenegger/nvim-jdtls#java-debug-installation
  init_options = {
    bundles = vim.list_extend(
      {
        -- Required for DAP
        vim.fn.glob(mason_package_path .. 'java-debug-adapter/extension/server/com.microsoft.java.debug.plugin-*.jar', 1),
      },

      -- Required for test_class / test_nearest_method
      vim.split(vim.fn.glob(mason_package_path .. 'java-test/extension/server/{junit,org,com.microsoft.java.test.plugin}*.jar', 1), "\n")
    )
  },

  on_attach = function(client, bufnr)
    -- With `hotcodereplace = 'auto' the debug adapter will try to apply code changes
    -- you make during a debug session immediately.
    -- You can use the `JdtHotcodeReplace` command to trigger it manually
    jdtls.setup_dap({ hotcodereplace = 'auto' })

    jdtls.setup.add_commands()

    -- Opens the DAP UI as soon as debugger runs/continues
    dap.listeners.after.event_initialized["dapui_config"] = function()
      dapui.open()
    end

    whichkey.register({
      ['<A-o>'] = { jdtls.organize_imports, 'Java: Organize imports'},
      ['<F9>'] = { dap.step_over, 'Debugger: step over'},
      ['<F10>'] = { dap.step_over, 'Debugger: step into'},
      ['<F12>'] = { dap.step_over, 'Debugger: step out'},
      ['<space>'] = {
        d = {
          name = 'debug (DAP)',
          a = { '<Cmd>:vsplit | terminal w3m https://docs.oracle.com/javase/8/docs/api/compact3-summary.html<CR>', 'Browse Java 8 API'},
          b = { dap.toggle_breakpoint, 'Toggle breakpoint' },
          B = { function() dap.set_breakpoint(vim.fn.input('Breakpoint condition: ')) end, 'Set conditional breakpoint' },
          c = { dap.continue, 'Continue' },
          d = { dap.clear_breakpoints, 'Clear breakpoints' },
          e = { jdtls.test_class, 'Debug test class' },
          E = { jdtls.test_nearest_method, 'Debug test method' },
          f = { function() widgets.centered_float(widgets.frames) end, 'Show frames' },
          h = { widgets.hover, 'Show hover', mode = {'n', 'v'} },
          i = { function()
            require('jdtls.dap').setup_dap_main_class_configs({
              verbose = true,
            })
          end, 'Init autostart main class' },

          I = { function()

            -- Configures, how to launch a debug adapter for java.
            -- We use a callback where we ask the LSP server to start a debug session.
            -- The response contains the port that the adapter can connect to,
            -- to debug the running application.
            dap.adapters.java = function(callback)
              util.execute_command(
                -- Ask the LSP server to start a debug session
                {command = 'vscode.java.startDebugSession'},
                -- The response provides the port that the adapter will listen on
                function(err0, port)
                  assert(not err0, vim.inspect(err0))
                  print("Attaching to debug adapter on port ", port)
                  callback({
                    type = 'server';
                    host = '127.0.0.1';
                    port = port;
                  })
                end
              )
            end

            -- Configuration for the debug adapter
            dap.configurations.java = {
              {
                type = 'java',
                -- attach to a running Java program that was started with --debug-jvm
                -- and that is waiting on 5005 for a debug adapter to attach
                request = 'attach',
                name = "Java attach",
                hostName = "127.0.0.1",
                port = 5005,
                projectName = project_name,
              },
            }
          end, 'Init external (req. --debug-jvm)' },
          r = { dap.repl.open, 'Open REPL' },
          s = { function() widgets.centered_float(widgets.scopes) end, 'Show scopes' },
          t = { dapui.toggle, 'Toggle UI' },
        },
      },
      ['<leader>se']= { vim.lsp.buf.format, 'Format code'},
    })
  end
}
-- This starts a new client & server,
-- or attaches to an existing client & server depending on the `root_dir`.
require('jdtls').start_or_attach(config)
mfussenegger commented 1 year ago

Both neovim core and nvim-jdtls don't cancel any requests automatically.

I suspect it could be the completion plugin. There are some cases where computing the completions can take a while - especially if re-builds are ongoing.

Maybe check if it has some throttle settings.

You could also try setting:

java = {
    autobuild = { enabled = false },
   ...

In the lsp settings. It helps with completion speed but also means that you have to force a build via :JdtCompile before you debug. Otherwise you'd debug an outdated version.


nvim-jdtls btw. takes care of the dap.adapters.java definition. You should be able to remove that from your config

mikehaertl commented 1 year ago

Thanks for the pointers. I did a quick grep for cancelRequest in my plugin directory and there were no matches in any of the completion related plugins (cmp-nvim-lsp and cmp-nvim-lsp-signature-help are the ones I use).

The only match is in nvim-dap-ui:

nvim-dap-ui/lua/dapui/async/lsp.lua
79:                  async_request("$/cancelRequest", { requestId = req_id }, bufnr)

But I hardly ever use the debugger. Not sure if it still does something in the background, though. More ideas on what to look for are very welcome.


It helps with completion speed but also means that you have to force a build via :JdtCompile before you debug.

Hmm, I found that even without your suggested setting I already have to :JdtCompile to not get an error when I run jdtls.test_nearest_method.

Anyway, as for now it seems like completion is not the problem I leave the autobuild setting away.

mfussenegger commented 1 year ago

They'd be calling client.cancel_request instead of sending a $/cancelRequest request directly.

mikehaertl commented 1 year ago

Ah, ok. Now it gets more interesting:

cmp-nvim-lsp/lua/cmp_nvim_lsp/source.lua
130:    self.client.cancel_request(self.request_ids[method])

lspsaga.nvim/lua/lspsaga/symbol/init.lua
35:      client.cancel_request(id)

I also checked to which request the id of the cancelRequest relates to. It always seems to be textDocument/documentSymbol requests, which again matches lspsaga:

lspsaga.nvim/lua/lspsaga/symbol/init.lua
99:  _, request_id = client.request('textDocument/documentSymbol', params, function(err, result, ctx)

Soo ... I think we can close this here and I have to open another issue with lspsaga.

Thanks for your help.