Subject: bug#77588: Catastrophic slowdown in eglot via `flymake-diag-region'

From: JD Smith <[email-masked]>
Date: Tue, 8 Apr 2025 19:45:05 -0400

> On Apr 8, 2025, at 5:00 PM, João Távora <[email-masked]> wrote:
> 
>> 
>> Maybe I am missing something, but if the push diagnostics and
>> didChange cross paths, I could see how the latter could be out of
>> date.
> 
> Indeed, but they don't cross paths, at least not on virtually all
> servers that I know of.  Servers on a "push diagnostics" model operate
> by sending at most 1 publishDiagnostics notification _after_ receiiving
> a didChange.  Which makes sense.  You're notified of some changes V1,
> you do some calculations, and you publish your results.  If you operate
> asynchronously and during that time you get another notification of
> changes V2, you probably have throw most work out (certainly don't
> publish it).
> 
> If you supply the Eglot events log to your server session we can check
> what's happening.  I have used basedpyright+eglot in the past though,
> and I never noticed any problems (doesn't mean they can't or weren't
> happening under the hood, but I certainly didn't experience any
> catastrophic slowdown).

This is an interesting point.  Here's an event log: [1].  I modified the old notifier to simply alert when versions didn't match.  It reports:

Diagnostics version mismatch 28 received, 29 current

In the log, you can see the following pattern:

[jsonrpc] e[19:15:32.870] --> textDocument/inlayHint[107] 
[jsonrpc] e[19:15:32.905] <-- textDocument/inlayHint[107] 
[jsonrpc] e[19:15:33.079] --> textDocument/didChange             ; Version 29 change sent
[jsonrpc] e[19:15:33.079] --> textDocument/completion[108] 
[jsonrpc] e[19:15:33.383]   --> textDocument/inlayHint[109] 
[jsonrpc] e[19:15:33.446]   <-- textDocument/publishDiagnostics  ; Version 28 diagnostics received
[jsonrpc] e[19:15:33.800]   --> textDocument/signatureHelp[110] 
[jsonrpc] e[19:15:33.801]   --> textDocument/hover[111] 
[jsonrpc] e[19:15:33.801]   --> textDocument/documentHighlight[112] 
[jsonrpc] e[19:15:33.807]   <-- textDocument/completion[108] 
[jsonrpc] e[19:15:33.809] <-- textDocument/inlayHint[109] 
[jsonrpc] e[19:15:34.095] <-- textDocument/publishDiagnostics    ; Version 29 diagnostics received

I.e. v28 diagnostics are received *after* v29 didChange is sent.  Up to 50% of the updates have this version mismatch.  Is it significant that the publishDiagnostics is "nested" inside a completion?  Note that it also happens (though less frequently) even when typing a multiple line comment, always shortly after a newline is entered.  I agree this is strange: basepyright should drop the v28 update like a rock.

Also during these pauses, I found that end-of-thing often (but not always) errors out:

1 -> (end-of-thing symbol)
1 <- end-of-thing: !non-local\ exit!
======================================================================
1 -> (end-of-thing sexp)
1 <- end-of-thing: !non-local\ exit!
======================================================================
1 -> (end-of-thing symbol)
1 <- end-of-thing: 306546
======================================================================
1 -> (end-of-thing sexp)
1 <- end-of-thing: !non-local\ exit!
======================================================================
1 -> (end-of-thing symbol)
1 <- end-of-thing: 306546
======================================================================
1 -> (end-of-thing sexp)
1 <- end-of-thing: !non-local\ exit!
======================================================================

I think this explains why my slow-down isn't as bad here: end-of-thing is now *crashing* (inside ignore-errors).  Not the most useful speedup to be sure, and not sure what changed, probably a parsing difference in the file. 

Repeating that command by hand shows the real error:

Debugger entered--Lisp error: (error "No sexp here")
  signal(error ("No sexp here"))
  error("No %s here" sexp)
  #<subr end-of-thing>(sexp)
  apply(#<subr end-of-thing> sexp)
  #f(compiled-function (body &rest args) #<bytecode 0x1dcd258a89e09bb5>)(#<subr end-of-thing> sexp)
  apply(#f(compiled-function (body &rest args) #<bytecode 0x1dcd258a89e09bb5>) #<subr end-of-thing> sexp)
  end-of-thing(sexp)
  eval((end-of-thing 'sexp) t)
  #f(compiled-function () #<bytecode 0x12f7d3ddabb81c2a>)()
  #f(compiled-function () #<bytecode -0x5db3e1955cb81d1>)()
  handler-bind-1(#f(compiled-function () #<bytecode -0x5db3e1955cb81d1>) (error) eval-expression--debug)
  eval-expression((end-of-thing 'sexp) nil nil 127)
  funcall-interactively(eval-expression (end-of-thing 'sexp) nil nil 127)
  call-interactively(eval-expression nil nil)
  command-execute(eval-expression)

[1] https://gist.githubusercontent.com/jdtsmith/a604c1e99f98f48da4d76f0abf07aedc/raw/0eb71ea878c70dc4d44afe56e44731d7dd135e19/event-buffer.txt