emacs-lsp / lsp-mode

Emacs client/library for the Language Server Protocol
https://emacs-lsp.github.io/lsp-mode
GNU General Public License v3.0
4.77k stars 883 forks source link

One keystroke lag while waiting for completion #2758

Closed blahgeek closed 3 years ago

blahgeek commented 3 years ago

So I encountered this weird problem with ts-ls server: it seems that sometimes, when emacs is waiting for the completion result, there's always one keystroke lagging. It does not happen every time, only sometimes.

For example:


At first, I'm in this state (| means the cursor): xxx.|. There's no completion popup yet (apparently sometimes it took ts-ls about 10 seconds to get the completion results, but I'm not complaining for that for now) and I think the ts-ls server is processing the request (CPU is 100%).

Then, I type some random key, like a. Nothing happens. It still displays xxx.|. Even after one or two seconds.

Then, I type another key, say b. Instantly, it displays xxx.a|, without b.

Then, I type another key, say c. Instantly, it displays xxx.ab|, without c.

And so on. There's always one keystroke lag. Any keys, including backspace, have the same behavior.

The lag would disappear as soon as the completion popup appears. After that, everything go back to normal.


My setup: emacs native-comp branch in linux. company-mode with lsp's company-capf as suggested.

I'm not sure what other information do you need, please let me know if you need any.

yyoncho commented 3 years ago

The problem does seem fundamental: either we show the newly typed character only after the (possibly long) computation is finished, or we show some old/unfinished state before that happens.

Could someone describe the approach VS Code took regarding that?

@dgutov see this: (in the video I press C-SPC two times to force completion dialog)

https://user-images.githubusercontent.com/13259670/115142735-06faf200-a04c-11eb-9477-4e2bcd85c926.mp4

dgutov commented 3 years ago

@joaotavora

That explains the how, but doesn't explain the why. That it should be possible doens't mean it should be pervasive.

I don't have a good answer ATM, except to say that if this scenario is realistic, we need to handle it. And that my personal config exhibits this problem as well. Does yours?

I don't know if you followed my suggestion, which could be to keep company unchanged, and then make the backends calling sit-for be a little more careful under these rules "if you predict you're going to take a long time waiting for completions, make sure to redisplay after a short while". I've tried that with a timer in the backend and it removed flickering for very fast backends

Without also having the pseudo-popup "unhidden" before the (redisplay) call, this approach leads to flickering all over again in my testing, because redisplay makes popup blink.

Perhaps we're testing different scenarios, though (mine are either company-clang with no-cache enabled, or lsp-mode with elixir-ls). If you can post a self-contained example which shows the improvement you're seeing, as well as specific patches, that could help (preferably without Eglot or LSP Mode: I only have one language server installed, and it's not particularly slow or popular).

Of course, the same thing can be done in company, maybe it would be cleaner. (I don't know why the extra post-command is for but that's fine)

That's the step which "unhides" the overlay-based popup showing the previous list of completions.

Also I'd like to note that your characterization of these process-based completion backends as "faux" seems to stem from a perspective based solely on company, where the authors have invented something they called async and treat company-capf with some underlying assumptions that don't 100% match reality. In those situations, you say the backend is "faux", the french word for "fake", somehow associating it with illegitimacy.

These backends are clearly informed by asynchronous functions in other environments. But any async workload is a suspended or parallelized computation which, upon its creation, returns control to the caller and allows the caller to choose what to do in the case of success or failure (explicitly, without conflating outright failure with an empty list). Neither of which happens with eglot's or lsp-mode's completion tables.

As a result, for example, Company has to make educated guesses that the seemingly straightforward code can do this complicated dance with input-pending-p under the covers, guess when that happens using side-channels (which is only possible because I read your - eglot or lsp-mode's - code, thus breaking the abstractions) and accommodate this special case to create the best behavior.

If you have a different name for these completion tables (which are irregular, so they do need a name), I could use that.

The very same SLY backend works fine with Helm, icomplete and fido-mode for example (though inside the minibuffer), where no flickering is seen and no lag is seen.

None of these deal with in-buffer completion. Or with the combination of idle and in-buffer completion anyway.

dgutov commented 3 years ago

@yyoncho

Thanks.

I can see in the video that this LS (jdt?) is slow to retrieve completions, at least at the beginning. What happens next?

I see the "Loading..." popup, is it the look of VS Code completion popup while the server has not returned any items yet? What then, it caches the response to update the popup quickly? What happens if the result set is "incomplete", which IIRC is a possibility with the LSP protocol?

What happens in the middle of the video? Did you dismiss the popup, or did it disappear because the editor had to poll the (slow) language server for completions again?

joaotavora commented 3 years ago

@dgutov

Perhaps we're testing different scenarios, though (mine are either company-clang with no-cache enabled, or lsp-mode with elixir-ls). If you can post a self-contained example which shows the improvement you're seeing, as well as specific patches, that could help (preferably without Eglot or LSP Mode: I only have one language server installed, and it's not particularly slow or popular).

Indeed ,perhaps we are talking about different things. I'm using SLY because it's easy to hack a very slow or very fast server. See the single commit of https://github.com/joaotavora/sly/tree/scratch/no-flicker-but-still-responsive-company. Installing SLY is reasonably straightforward, but you need a Common Lisp executable :neutral_face:. Not very hard, let me know if you want to try it.

But any async workload is a suspended or parallelized computation which, upon its creation, returns control to the caller and allows the caller to choose what to do in the case of success or failure

I don't understand this theorization, or how it defines a somehow more "legitimate" async. In this specific situation, we have to deal with a number of events: user input, a successful response, timeout errors, other timers. All of these things can happen in unsteady rhythms and in various orders. Some, like me, would call this asynchronous, but that's just a word. The fact is that the techniques you call "faux" handle this correctly. I'm not saying your narrow view of what "asynchronous" means can't work. It can, and it probably does, but it (1) is incompatible with other front-ends such as completing-read (2) requires users to program in ways that IMO aren't as comfortable.

As you know, if there is one thing that is fake or feigned in Emacs, it's parallelism. Emacs, like other programming models (most notably JavaScript's), is an event-based system which does one thing at a time. The techniques you call "faux" are explicitly based on this property. I don't think they would function if this property weren't there.

As a result, for example, Company has to make educated guesses that the seemingly straightforward code can do this complicated dance with input-pending-p under the covers, guess when that happens using side-channels (which is only possible because I read your - eglot or lsp-mode's - code, thus breaking the abstractions) and accommodate this special case to create the best behavior.

But look at icomplete-mode. It doesn't do any educated guesses and works fine with SLY and "every" backend. And if you sick it at Eglot's completion table, it probably works fine too, as it's the same technique.

(explicitly, without conflating outright failure with an empty list). Neither of which happens with eglot's or lsp-mode's completion tables.

Again, I can't speak for lsp-mode, but normally outright failure is signalled in the form of an Error. Also, the primitives with "drop on floor" semantics in Jsonrpc (jsonrpc-request) and SLY (sly-eval) accept an arbitrary object to return if the request is dropped because of user input. I haven't found a use for that feature yet, but it's there if that's somehow a turn-off, it could be use to signal some other condition, like a quit or some custom one.

By the way, I didn't invent these "drop on floor" semantics. while-no-input is the same thing wrapped slightly differently.

None of these deal with in-buffer completion. Or with the combination of idle and in-buffer completion anyway.

I don't see why the matter is much different. It's an in-buffer completion where the buffer happens to be the minibuffer. Even the technique based on overlays is similar. With icomplete-mode and the current SLY, there is no flickering with a fast server and neither is there unresponsive lag with slow ones. It does what could conceivably be called "the right thing" (at least in my opinion). You seem interested in how VSCode does things, that's fine. I'm just saying you could have a look inside other parts of Emacs, too.

yyoncho commented 3 years ago

@dgutov

@yyoncho

Thanks.

I can see in the video that this LS (jdt?) is slow to retrieve completions, at least at the beginning. What happens next?

I see the "Loading..." popup, is it the look of VS Code completion popup while the server has not returned any items yet?

Only if you explicitly request completions. Without this, it doesn't show any indication that it is loading the completion items.

What then, it caches the response to update the popup quickly?

At some point, it receives a complete response and it caches it. Also, the prefix is longer, thus the completion result is retrieved much faster. In this regard, there is no difference between lsp-mode and vscode.

What happens if the result set is "incomplete", which IIRC is a possibility with the LSP protocol?

The behavior of vscode, when there are no completion results, is more evident when pressing backspace.

What happens in the middle of the video? Did you dismiss the popup, or did it disappear because the editor had to poll the (slow) language server for completions again?

So I dismiss the popup and then I request the completion for a longer prefix and start to press backspace. See how VScode is showing the old data until it retrieves the data for the shorter prefix. I think that we have discussed that in the past when lsp-mode implemented the hack which returns the old data to avoid flickering of the completion dialog. IME this is a huge UE improvement.

dgutov commented 3 years ago

@joaotavora

See the single commit of https://github.com/joaotavora/sly/tree/scratch/no-flicker-but-still-responsive-company.

Replied there.

Some, like me, would call this asynchronous, but that's just a word. The fact is that the techniques you call "faux" handle this correctly.

To an extent. But if "asynchronous" is just a word, why complain about the phrase "faux async"? Especially without suggesting alternative names.

requires users to program in ways that IMO aren't as comfortable.

I fail to see how they can be less comfortable than forcing every backend to re-implement this pattern in an ad-hoc fashion. It's fragile code that is not easy to get right, as evidenced also by the present discussion.

You seem interested in how VSCode does things, that's fine.

Because I wanted to how they solved the UX problem in terms of the behavior. I've seen some people praise their responsiveness lately, in comparison to Emacs. That's orthogonal to the APIs and the implementation.

dgutov commented 3 years ago

@yyoncho

So I dismiss the popup and then I request the completion for a longer prefix and start to press backspace. See how VScode is showing the old data until it retrieves the data for the shorter prefix. I think that we have discussed that in the past when lsp-mode implemented the hack which returns the old data to avoid flickering of the completion dialog. IME this is a huge UE improvement.

All right, so VS Code indeed shows "stale" data. I suppose that makes sense, if you prefer the editor to show whatever it knows as soon as it can (both the chars right after you type, and the completions as soon as they arrive). The result can be an inconsistent view, but it seems like that's unlikely to be harmful. And it moves the blame for slow responsiveness from the editor to the language server.

If someone wants to test it and give feedback, the patch in https://github.com/emacs-lsp/lsp-mode/issues/2758#issuecomment-821903370 should be a close approximation. I'll need to rewrite it, though, after some debugging.

Also, I wonder which code should be in charge of scheduling the redisplay timer. Do you want to make the choice of the timer interval inside lsp-mode?

joaotavora commented 3 years ago

To an extent. But if "asynchronous" is just a word, why complain about the phrase "faux async"? Especially without suggesting alternative names.

As I said, it conveys the meaning of illegality or illegitimacy to one technique and purity to another, which is misleading. As to names, I don't think it needs one. It's just another technique in a toolbelt that a language with non-local returns allows.

I fail to see how they can be less comfortable than forcing every backend to re-implement this pattern in an ad-hoc fashion. It's fragile code that is not easy to get right, as evidenced also by the present discussion.

That's until you get the primitives right. Which they pretty much are, except for this thing with company, which can well be solved there (in fact you seem to have a patch for it already). Anyway, if you write JSONRPC-based backends (which are not limited to LSP/Eglot) for example, you just use the jsonrpc-request primitive, where the throw/catch/sit-for technique lives. If your protocol isn't JSONRPC-based either (1) it could be or (2) the technique is sufficiently easy to replicate (3) some intermediate primitives could be developed to make it sufficiently easy to replicate.

As to why I think it is more comfortable, it's subjective of course, but I think the reasonably small amount of code that e.g. Eglot employs to achieve this particular effect with mostly all frontends speaks for itself. Also, do you know "modern" Javascript? I've learned about it recently. In ES6 there is a keyword called await which you use with async functions and is similar to sit-for. Just mentioning it because that goes against your definition of that "async" is achieved by immediately returning control to the caller. It'd be nonsensical to call await more faux or more real than using Promise.then() on an immediately returned object, even though it's much more convenient in certain situations. A modern JS program will use both depending on the situation, and so should an Elisp one.

Given the current capf scheme, completion against potentially slow backends is simply much better served by using the techniques you call "faux" or "ad hoc". Write once: work for company and everywhere else.

I've seen some people praise their responsiveness lately, in comparison to Emacs.

Perhaps in comparison to company, which uses inhibit-redisplay carelessly (apparently partly due to my careless suggestion some years ago), but perhaps not to other frontends.

blahgeek commented 3 years ago

If someone wants to test it and give feedback, the patch in #2758 (comment) should be a close approximation. I'll need to rewrite it, though, after some debugging.

Thanks! I gave it a try and it seems working pretty well. I will continue testing it in my daily work and will let you know if I encounter any problems.

Also, I wonder which code should be in charge of scheduling the redisplay timer. Do you want to make the choice of the timer interval inside lsp-mode?

Personally I like your patch here. I think it's the right way to solve it in company.el instead of lsp-mode. I don't see any downside of doing it this way.

yyoncho commented 3 years ago

@kiennq this is fixed after switching to sit-for for lsp-request-while-no-input, right?

dgutov commented 3 years ago

@yyoncho I think it still requires a patch on my side (see the previous discussion). Just give me a few more days to work on it.

kiennq commented 3 years ago

@kiennq this is fixed after switching to sit-for for lsp-request-while-no-input, right?

No, it stills require patch from company-mode

dgutov commented 3 years ago

I've pushed the fix, check it out.

@blahgeek @kiennq @yyoncho

dgutov commented 3 years ago

Regarding "faux async", @joaotavora any anyone else who might be interested: ES6 is indeed a good reference (it is both popular and similar enough to Elisp in concurrency limitations).

await actually does return control to the caller (right away, or close enough). Because it's just syntactic sugar, and any function that uses await must be async: meaning, it returns a Promise. Check out this guide, for example.

It's unfortunate that we still don't have an official async convention for completion tables, for use with icomplete and other modes. Someday we'll get more developers with experience in asynchronous code on emacs-devel, and finally get this moving forward.

kiennq commented 3 years ago

@dgutov I believe we can implement similar await/async behavior on Emacs already with generator.el. There's even emacs-aio which provides similar await/async syntax.

joaotavora commented 3 years ago

await actually does return control to the caller (right away, or close enough). Because it's just syntactic sugar, and any function that uses await must be async: meaning, it returns a Promise.

@dgutov I criticized your assertion made here, which I quote:

"any async workload is a suspended or parallelized computation which, upon its creation, returns control to the caller and allows the caller to choose what to do in the case of success or failure"

I did it using an example of a different, non-faux, valid way to do async programming that doesn't conform to those suppositions of what "any" async workload is. My point is, summarily, that calling a primitive based on sit-for -- say jsonrpc-request in jsonrpc.el -- is very much like calling await jsonrpcRequest() in JavaScript. My point is also that this is more comfortable to the programmer.

So, either you are completely confused, or simply we are talking about totally different things. Let me tell you what I'm talking about:

My main contention is simply that the latter is more comfortable for the programmer of RPC-style systems, such as LSP and many others. In Javascript, it avoids unnecessarily verbose .then(function() {...})....catch() chains. In Emacs, it similarly avoids callback verbosity.

So I thank you for your guide, but I've been using async JS intensively the last 6 months (and async Elisp for much longer). I think I know how it works (at least according to my understanding of the concepts outlined above).

joaotavora commented 3 years ago

@kiennq

You are right that there are other, alternatives ways to what sit-for accomplishes. BTW emacs-aio is based on generator.el.

generator.el a Lisp evaluator which transforms your Lisp code (which is data) into bits that can stop and resume at certain control points. It's called a CPS ("Continuation Passing Style") transformation. See this article. Basically, it's as if all your imperative code is quietly changed into a bunch of Promise.then(). As the author of generator.el concedes, it's slower. But that may not be a problem. The big problem is that it won't work for every piece of pre-compiled code: that code won't be "magically" interruptible/continuable because generator.el has to "see" your code before applying the transformation. You cannot just stick a aio-await form in called function down the stack and expect it to work: it won't! aio-await is a macro that needs to be lexically inside an iter-lamba or iter-defun so that the CPS-transformator "sees" it.

Still, it might be a good thing if done correctly. Maybe jsonrpc-request, could, in the future, be simply implemented with iter-defun and aio-await instead of defun and sit-for. As long as the callers of jsonrpc-request wouldn't notice... it's fine.

Regardless, no completion API needs to be fundamentally changed or reworked in incompatible ways for that to occur, and that is a good thing.

kiennq commented 3 years ago

The big problem is that it won't work for every piece of pre-compiled code: that code won't be "magically" interruptible/continuable because generator.el has to "see" your code before applying the transformation. You cannot just stick a aio-await form in called function down the stack and expect it to work: it won't! aio-await is a macro that needs to be lexically inside an iter-lamba or iter-defun so that the CPS-transformator "sees" it.

I don't see this as a big problem. When you're using async/await you have to follow their syntax anyway.

Pretty much everywhere requires await to be inside of async function, C++ enforces that by requiring the co_await has to be called inside a function that returns std::future or IAsyncOperation, C# gives compiler error when await is not called inside async function, same for Javascript. emacs-aio is making the async/await syntax for Elisp very close with what's in the other languages, to the point that you don't have to worry about iter-defun anymore.

joaotavora commented 3 years ago

emacs-aio is making the async/await syntax for Elisp very close with what's in the other languages, to the point that you don't have to worry about iter-defun anymore.

@kiennq Really?? I'd be very surprised. Can I just call aio-await from inside a regular function and it will just work? I think not. Here's from the aio-await docstring:

This macro can only be used inside an async function, either
`aio-lambda' or `aio-defun'.

Do you want to write all your defun and lambda into slower aio-* versions? I think it's a problem. How big, who knows? It depends. The sit-for approach doesn't have this problem.

EDIT: aio-defun eventually expands to iter-lambda, in case the link wasn't obvious.

kiennq commented 3 years ago

@joaotavora I think you miss my part about very close with what's in the other languages.

Even javascript requires the await to be called inside async function so I don't see anything wrong with aio-await has to be called inside aio-async. For write all of defun and lambda in aio-* form, it depends on if all of your defun and lambda are async function / coroutine or not, which I think is not remotely true. Even if it's slower, it's just an implementation detail and can be improved upon. What we gain here is similar, shorter syntax to other async/await supported language. To call await, async inside a normal sync function, you can always use aio-with-async (which also similar to what other languages do).

EDIT: aio-defun eventually expands to iter-lambda, in case the link wasn't obvious.

It's using generator.el so it will eventually expands to iter-lambda, I have no objection to that. However, it's implementation details too, and when you're using emacs-aio, you don't even have to write iter-defun or care about that.

joaotavora commented 3 years ago

@joaotavora I think you miss my part about very close with what's in the other languages.

No I got it. And I agree. It's just that that fact, by itself, doesn't solve any problems magically.

don't see anything wrong with aio-await has to be called inside aio-async.

There's nothing "wrong" with CPS, of course, like there is nothing wrong with programming paradigm X. That's just my point about there not being a "right" or "wrong" or "faux" way of doing things. To each job, the most appropriate tool

CPS transformers, whatever the language, have this characteristic: you have to rewrite the containing code where you want to call its primitives, restructuring your code using aio-defun and its primitives. That's some work in itself and the end result is not compatible to legacy callers who know nothing about the CPS transformer (iter or aio in Emacs's case), like completion code. That's a drawback, a real one. In JS they rewrote wrote lots of APIs into Promise-returning API's and there are lots of adapters for old code that still works in the old style. You know have "foo" and "fooSync" duplicates for each op in fs, for example. My point is that adapting old code isn't magical.

You can hide some of this complexity behind abstraction, of course. This is why I it said jsonrpc-request could be written with aio-await (or maybe just iter-next to simplify). But right now it's working with sit-for, which also works fine.

BTW the slowness is not an "implementation detail": you have to be careful where you use it CPS, certainly not in tight loops. No idea how GCC Emacs fares with such code btw, but I do wonder.

kiennq commented 3 years ago

Also, I don't think sit-for is doing what similar to javascript async/await is doing. I've tried this

(defun async-test-a ()
  (message "1")
  (sit-for 10)
  (message "2"))
(progn
  (message "before call.")
  (async-test-a)
  (message "after call."))

;; Output:
;;     before call.
;;     1
;;     2
;;     after call.

and this for js, https://jsfiddle.net/gL02dkoh/. The output indicates that javascript async function returns immediately, not waiting like sit-for.

kiennq commented 3 years ago

But right now it's working with sit-for, which also works fine.

I do agree with that, jsonrpc-request is working well, and trying to fix non-broken maintainable code is just non-sense for me too.

joaotavora commented 3 years ago

I didn't say sit-for is doing what await does, sit-for doesn't take a function to call, for one :wink: . I said you can make a primitive p() that behaves the way await p() would. That primitive is jsonrpc-request. sit-for does what sit-for does :-)

The output indicates that javascript async function returns immediately, not waiting like sit-for.

Precisely, you'd need to await on that second function, too. But you're in "top-level" there and top-level await is not a thing (yet). You can use a IIFE for now, which is a technique used in ESM modules.

(async function () {
  console.log("before call.");
  await asyncCall();
  console.log("after call.");
})();

But this shows how in JS you have to propagate the knowledge of CPS up to the last caller.

kiennq commented 3 years ago

I didn't say sit-for is doing what await does, sit-for doesn't take a function to call, for one 😉 . I said you can make a primitive p() that behaves the way await p() would

That would be a very limited form of await since sit-for is waiting for an interrupt from user input, whereas await is waiting for p to finish and release the control back to the caller function.

I purposely omit the await in the second function to illustrate that when hitting await, the function returns control back to the caller immediately, and the next expression will be evaluated.

joaotavora commented 3 years ago

sit-for is waiting for an interrupt from user input,

No, sit-for is waiting for many more things. In particular, it's running timers, process filters and counting on a timeout. Otherwise it wouldn't work at all! This is why in jsonrpc-request you find a catch/throw there ;-) In fact you can say that sit-for is waiting for all those things + user input.

And even if you did make a jsonrpc-request with fancy new CPS, I think you'd still need one or more of: sit-for, while-no-input, accept-process-input, input-pending-p. Those are the primitives with which you'd know how and when to resolve the generator ( iter-yield or aio-resolve).

I purposely omit the await in the second function to illustrate that when hitting await, the function returns control back to the caller immediately, and the next expression will be evaluated.

Then what did you expect? An async function when called without await , returns control immediately after it's synchronous bits have run. Which is what it did. The console.log('2') is not one of those synchronous bits (it looks like it, but it's not, because of the CPS transformation).

kiennq commented 3 years ago

In fact you can say that sit-for is waiting for all those things + user input.

I disagree with this. The catch/throw is interrupt mechanism of Elisp and sit-for is not waiting for those, else the catch/throw is not needed at all.

Then what did you expect? An async function when called without await , returns control immediately after it's synchronous bits have run. Which is what it did.

That's what make await/async much more powerful than just sit-for and also make not all primitive p() behaves the way await p() did with just sit-for. For example, I have this async function that executes command and returns the output string.

(aio-defun test--shell (command)
  "Asynchronously execute shell command COMMAND and return its output string."
  (-let (((callback . promise) (aio-make-callback))
         (buf (generate-new-buffer " *test-shell-command*")))
    (set-process-sentinel
     (start-process-shell-command "test-shell-command" buf command)
     (lambda (proc _signal)
       (when (memq (process-status proc) '(exit signal))
         (with-current-buffer buf
           (let ((data (buffer-string)))
             (funcall callback
                      (when (> (length data) 0) (substring data 0 -1)))))
         (kill-buffer buf))))
    (car (aio-chain promise))))

Now can you help to turn that function into something that can be used like this using sit-for, not blocking and not abandoning the output either?

(aio-with-async
  (let (text)
    (setq text (aio-await (test--shell "ls -R /usr/")))
    (with-current-buffer (get-buffer-create "*test*")
      (insert text))))
joaotavora commented 3 years ago

I disagree with this. The catch/throw is interrupt mechanism of Elisp and sit-for is not waiting for those, else the catch/throw is not needed at all.

Yes of course. You misunderstood me. I meant all those things are running while sit-for is. sit-for will only return locally on timeout or user input, but many more things can happen in the background, including those that make it return non-locally (a signal, or a throw). What I meant is that sit-for is not a busy loop on input only.

In your example, which is totally fine, you seem to be reinventing async-shell-command. But the (aio-await (test--shell... )) there won't return locally if the user types something. For that you would need sit-for somewhere in test--shell. If you try to insert your code into some sync legacy code, because you that code to ask an external process for output and also to be responsive to user keystrokes (which after all is what this whole issue is about), you need sit-for, while-no-input or equivalent. Because all the completing-read or read-from-minibuffer that Emacs interfaces has been built on for over 40 years are not wrapped in aio-with-async. So unless you want to tear down and rewrite all that code, it won't do what is needed.

For new code/new interfaces that don't have these requirements, it's a very defensible idea to use aio.el. I use async/await a lot in JS. I could have used aio.el when I rewrote Flymake, for example, but it wasn't (and isn't) in Emacs core and wouldn't have made such a monstrous difference anyway. Also finding lots of cps-state in my backtrace is not my idea of debugging fun :-)

dgutov commented 3 years ago

@joaotavora

An async function when called without await , returns control immediately after it's synchronous bits have run.

That is exactly what I meant. Which puts the async/await in JS into the first category: as soon as an await statement in reached, and its argument is not immediate (e.g. a Promise), control gets returned to the caller function. No functions at all in this model are blocking.

The caller, in turn, can decide what they want to do with the returned value (which is also a Promise): they can await on it, or set up success/error callbacks manually, or combine several Promise values using e.g. Promise.all.

The Emacs counterpart would be returning some suspended computation and letting the caller decide what to do: whether it will sit-for, or sleep, or accept-process-output (thus making sure the future waits for the server response or timeouts).

The "faux async" approach in question is problematic because it takes away that choice from the caller and instead blocks, waiting for some particular condition which it has decided on its own. But it has no business making that decision.

dgutov commented 3 years ago

@kiennq

emacs-aio is a good starting point, but it seems problematic as-is on several points:

I haven't tried to use it in earnest myself, though, so I could be missing something (good or bad).

joaotavora commented 3 years ago

That is exactly what I meant. Which puts the async/await in JS into the first category: as soon as an await statement in reached, and its argument is not immediate (e.g. a Promise), control gets returned to the caller function. as soon as an await statement in reached, and its argument is not immediate (e.g. a Promise), control gets returned to the caller function.

This doesn't make any sense, to me. In JS you may write (if inside an async context) these 3 statements involving functions a, b and c:

a(); const retval = await b(); c(retval);

If b returns a promise, c() will only run only if and when that promise resolves. It will not run if it rejects. Lots of things may happen between that time, including things that have nothing to do with b() Similarly in Elisp. except you don't need any special CPS context:

(a) (setq retval (jsonrpc-request "b")) (c retval)

Again, lots of things can happen before c runs, but in both cases, from c() 's point of view, it can "know" what happened of the remote request: if it finished, was interrupted, what interrupted it, errored, etc. There is nothing "faux" or problematic about this. It can be said that c()'s execution was "blocked". Maybe one of us has a bizarre understanding of what "blocking" or "synchronous" means, who knows?

Anyway this is quite in contrast to what happens if you didn't put await in the first example or used jsonrpc-async-request in the second. Also nothing "faux" or problematic about that, but c() has to be written quite differently.

It turns out that most existing Elisp libs and completion-related code, c() is written in normal imperative without thinking about async, so it's quite more handy to be able to write to use jsonrpc-request. It really is, that's why the problem is solved already. In another planet, if all those libs were written differently, things would be different, but as we say around my parts, if my grandmother had wheels she would be a bus.

dgutov commented 3 years ago

If b returns a promise, c() will only run only if and when that promise resolves. It will not run if it rejects

Because await is syntactic sugar, and it makes the function be effectively rewritten in CPS under the hood to something like:

a(); b().then(function (ret) {const retval = ret; c(retval);})

(a) (setq retval (jsonrpc-request "b")) (c retval)

Even if it looks similar that doesn't necessarily mean it does a similar thing (it doesn't).

There is nothing "faux" or problematic about this.

Except the things I have described in the previous message, and you snipped.

This is going nowhere again, so forgive me if I stop responding.

joaotavora commented 3 years ago

Even if it looks similar that doesn't necessarily mean it does a similar thing (it doesn't).

It has the same effect, as I perfectly showed.I don't care about the implementation, having it hidden is the point! I just originally mentioned await so I could explain how I think it's better to use jsonrpc-request. I'd probably be using aio-await if I could of course. Clearly superious if you have the conditions to use it. But we don't here, simply.

Except the things I have described in the previous message, and you snipped.

What you said is nonsensical, so what could i say? First, the caller doesn't see sit-for, or a-p-o at all: it sees jsonrpc-request. So how can I respond to that? And the caller of jsonrpc-request does get to say what things to wait on.

This is going nowhere again, so forgive me if I stop responding.

Don't sweat it, no prob at all, I'll sign off too. Thanks for fixing Company :+1:

dgutov commented 3 years ago

it sees jsonrpc-request

The caller of eglot-completion-at-point does not even see that. Thus whatever modest freedoms jsonrpc-request does provide to its caller are irrelevant.

yyoncho commented 3 years ago

@dgutov I tested the latest company and it seems like the issue is gone. Marking the issue as closed.

joaotavora commented 3 years ago

The caller of eglot-completion-at-point does not even see that.

A great thing! Welcome to abstraction land!

Thus whatever modest freedoms jsonrpc-request does provide to its caller are irrelevant.

Even better!

wyuenho commented 3 years ago

@yyoncho I'm still encountering this issue when using lsp-jedi, I'm using the latest lsp-mode and company.

dgutov commented 3 years ago

@wyuenho "This issue" = one keystroke lag? With the latest company from MELPA?

wyuenho commented 3 years ago

@dgutov Yes, one keystroke lag. Latest company from melpa.

yyoncho commented 3 years ago

@wyuenho can you confirm that it does happen with lsp-start-plain.el? If it happens with lsp-start-plain.el, can you provide detailed steps on how to reproduce, including a project to test with and what are the steps that you perform?

yyoncho commented 3 years ago

(and maybe start another issue because most likely you are hitting something else).

wyuenho commented 3 years ago

So preliminary testing is, if I set company-idle-delay to 0 or something really small like 0.02, the one keystroke lag occurs very regularly. The missing character won't appear until I press another key or the server has responded to a completion request. If I leave it to the default, then it's pretty hard to trigger this bug.

A separate but perhaps related issue is, sometimes when I press 2 self-insert keys together due to typing mistakes, (say io together) when waiting is done, those 2 characters will be repeated 5-20 times. This is happening regardless of what I set company-idle-delay to.

joaotavora commented 3 years ago

If it helps, after switching to the latest company.el I don't notice any lag anymore, in any of my two extensions, Eglot and SLY, that use Company. Previously, they suffered from a lag-related problem similar (but not quite as serious) to the one described in this issue's title. Emacs 28 and 27

yyoncho commented 3 years ago

@wyuenho are you sure that you are not hitting GC?

wyuenho commented 3 years ago

@yyoncho If I don't type an extra character, the missing character will never show up, so I don't think it's related to GC.

wyuenho commented 3 years ago

Also I have garbage collection message turned on, I don't see the GC message when the key lag happens.

dgutov commented 3 years ago

You can also experiment with different values of company-async-redisplay-delay. E.g. setting it to 0 or 0.001.