-
-
Notifications
You must be signed in to change notification settings - Fork 648
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[completion] Cache last result of cider-complete
#3655
Conversation
f3fdbbe
to
ba0d97e
Compare
Kudos for the investigation!
Yes, serving stale completions would seem very unfortunate in environment like Clojure where interactive redefinition is encouraged. Even more so as CIDER doubles down on emphasizing runtime-gathered insights. Guaranteedly serving 'latest and greatest' makes us different from others. Therefore agressive expiration times would seem crucial. I'd suggest something like two seconds - enough to prevent same-request redundancy, but not enough to possibly get stale. An alternative to using expiration times is using a mixture of dynamic scope and memoization. The idea is that memoization lives during the timespan of a completion interaction, but not any longer. ...Very precise, GC-friendly and can have a clean fallback to the non-memoized implementation. |
This would be ideal, but we don't control the entrypoints to the completion action, and I couldn't find any sort of flag that the entrypoints would set that could help us tell apart same-session completions from newly triggered ones.
I also thought of 2 seconds. My only reservation is that on a really slow remote connection the cache could expire within the same completion event, and that sort of environment is where caching is needed the most. But I guess it is the best we can do. |
I think you can also trigger the invalidation when the |
That's a good suggestion; I also thought in the direction of
I really don't think it is necessary. The reason we want to cache completions is not because they are too expensive for us to compute, but only to save the superfluous calls (and thus, latency) because of the peculiarities of Emacs API. I think that saving the latest one would be completely enough. I also think that even relying on |
See (defun cider-repl--state-handler (response)
"Handle server state contained in RESPONSE."
(with-demoted-errors "Error in `cider-repl--state-handler': %s"
(when (member "state" (nrepl-dict-get response "status"))
(nrepl-dbind-response response (repl-type changed-namespaces session)
(when (and repl-type
cider-repl-auto-detect-type
;; tooling sessions always run on the JVM so they are not a valid criterion:
(not (equal session nrepl-tooling-session)))
(cider-set-repl-type repl-type))
(when (eq (cider-maybe-intern repl-type) 'cljs)
(setq cider-repl-cljs-upgrade-pending nil))
(unless (nrepl-dict-empty-p changed-namespaces)
(setq cider-repl-ns-cache (nrepl-dict-merge cider-repl-ns-cache changed-namespaces))
(let ((this-repl (current-buffer)))
(dolist (b (buffer-list))
(with-current-buffer b
(when (or cider-mode (derived-mode-p 'cider-repl-mode))
;; We only cider-refresh-dynamic-font-lock (and set `cider-eldoc-last-symbol')
;; for Clojure buffers directly related to this repl
;; (specifically, we omit 'friendly' sessions because a given buffer may be friendly to multiple repls,
;; so we don't want a buffer to mix up font locking rules from different repls).
;; Note that `sesman--linked-sessions' only queries for the directly linked sessions.
;; That has the additional advantage of running very/predictably fast, since it won't run our
;; `cider--sesman-friendly-session-p' logic, which can be slow for its non-cached path.
(when (member this-repl (car (sesman--linked-sessions 'CIDER)))
;; Metadata changed, so signatures may have changed too.
(setq cider-eldoc-last-symbol nil)
(when-let* ((ns-dict (or (nrepl-dict-get changed-namespaces (cider-current-ns))
(let ((ns-dict (cider-resolve--get-in (cider-current-ns))))
(when (seq-find (lambda (ns) (nrepl-dict-get changed-namespaces ns))
(nrepl-dict-get ns-dict "aliases"))
ns-dict)))))
(cider-refresh-dynamic-font-lock ns-dict)))))))))))) |
It also means that typically it won't work when it's needed the most (most times a slow network connection means connecting to a production instance, and it's not generally recommended to run cider-nrepl there)
We could observe the host of the nrepl connection - if it is localhost, make the timeout tighter |
I still think it's best to stick to track state as that's what we use for similar caches so far. It's always somewhat confusing when a project starts doing different things for similar purposes. Consistency helps with maintainability.
Well, you'll also have problems with the namespace cache and the eldoc cache as well in this instance, so at least it's all the same for the user. :-) Perhaps there should be some generic fallback in the absence of |
Robe uses It might as well cut it / be idiomatic? |
That function caches only by prefix no other conditions, so it does not suit us. |
That's a weak heuristic – the host is often still localhost because the remote REPL is exposed via SSH forwarding. But honestly, I wouldn't bother. It's better to pick a single value for the expiration (e.g. 5 seconds) and add track-state to the mix, for cases where cider-nrepl is available and user is actively developing things. If track-state is unavailable, then the cache will simply expire in 5 seconds – sounds like a sufficient trade-off. |
Regardless, it looks like a practical example of dynamic scope being useful. Is it not feasible to make a (n.b. Robe and Company come from the same author, so I'd 100% expect this to be a good idea) |
Actually, maybe I'm wrong about the scope of reuse of |
@vemv Yes, I'm stupid, the fix is coming. Thanks! |
693535a
to
ebccbb9
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
I'd suggest to try this for about a day - remember that cider master changes can reach users very quickly.
Can also patch locally myself as additional QA.
ebccbb9
to
3d8a0f0
Compare
FWIW - that's not really an issue in general as for over 10 years I've set the expectations that breakages might occur if people are tracking MELPA, but I've also promised them the adventurous users quick fixes. To quote the docs:
For me MELPA has always been another testing/feedback channel, so I rarely hesitate to release things there. |
cider-completion.el
Outdated
(lambda (prefix) | ||
(if (string-equal last-prefix prefix) | ||
last-result | ||
(prog1 (setq last-result (cider-complete prefix)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you need prog1
here? I mean I get why you used it, but I think it usually confuses people a bit. I think it's more readable to just have last-result
as the final expression in the else
, given it's wrapped in a regular prog
implicitly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's copy-pasted from completion-table-with-cache
. I'll rewrite it.
I'm aware of that, but I'd try to decouple "users can test this out" from "we have taken sufficient measures to believe this code is correct". In other words, I try to not use that MELPA intricacy as an excuse to not try our own stuff. Breaking users' workflow is never fun and not only affects them but also us (in form of support time). In practice, I asked for a small delay - during which I can apply the same patch and use it for an entire day of work. Doesn't seem a huge ask. |
Well, it's always my expectation that the people filing PRs have actually tested them out. :-) I don't really have the time to test every submission we get myself and I've rarely done it.
I'm fine if you want to personally test it today, I'm just saying that I don't really view potential breakages on The PR itself seems good to me, sans my small remark about the usage of |
Same, but normally more time and more eyes only help. Anyway. I've applied the patch and looked at the nrepl logs. Seems fine to me so far! |
3d8a0f0
to
f019ab7
Compare
f019ab7
to
f4d454f
Compare
|
f4d454f
to
9d173a7
Compare
9d173a7
to
4a30f49
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking great!
I've been spending much more time making sense of Emacs completion machinery than it is worth, but at this point it's too late for me to abandon it. Apart from other findings, I discovered that Emacs'
completion-at-point
can call itscompletion-at-point-functions
multiple times during a single invocation. I've never noticed it before because Company is smarter about that and usually issues a single call to the backend. But with the default settings (three completion styles –(basic partial-completion emacs22)
), on a prefix that yields no completion candidates,completion-at-point
can yank the "complete" op 4-6 times. As much as I tried to fight it by reducing the number of enabled styles, I could only drop it down to 2. Emacs itself recognizes that this API can call backend many times and encourages caching the response, hence the existence of this function: https://www.gnu.org/software/emacs/manual/html_node/elisp/Programmed-Completion.html#index-completion_002dtable_002dwith_002dcache.This PR proposes a simple cached
cider-complete
implementation that remembers the last result and can give it back without touching the backend. The "key" of the cache includes the prefix, the buffer where the completion was initiated, and the point in the buffer. I also thought about addingnrepl-request-counter
to the key, so that the cache is properly invalidated if user evaluated something that might change the completion results. But I noticed thateldoc
ops often like to squeeze between the calls tocomplete
, rendering the cache useless. So I need advice on this.I have a feeling that a stale incorrect cache may be quite annoying, so maybe adding an expiration time to the mix could help?
Overall, when the completion is triggered on a large list of candidates (e.g. the prefix
com.
), it feels significantly snappier with the cache. Let alone if you call it manually the second time. This would be doubly true when connecting to a remote REPL.