Transitioning to ocaml-lsp-server in emacs from merlin

Thank you for the test case! Do you have a precise location and input that trigger the freeze ? I will try to reproduce.

Thanks for looking into it!

You can pretty much just start typing anything anywhere. Here’s an example where I keep tapping a at a steady interval:

Peek 2022-02-14 19-48

If I undo and try again the experience is pretty much identical every time. Even just moving around this file is quite laggy.

1 Like

The reason why it’s slow is likely the ppx. Ocamllsp indeed doesn’t suffer from this problem because the servers and clients are non blocking. It should be possible to fix this problem with merlin as well however. It’s mostly a matter of fixing making the emacs frontend non blocking as well. Does the stateless merlin client cache ppx transforms? If not that could also be problematic.

2 Likes

Don’t! The dune build in the compiler is mostly garbage.

We have it because, at first, it made the Dune people happy to have Dune as an alternate build system in the compiler (but in fact it doesn’t really work, because the compiler requires more control over the build environment than humans were able to express with Dune), and now because, as @rgrinberg points out, it’s a good way to get Merlin support without having a separate discussion about adding .merlin files or anything like that. We keep (barely) maintaining it to keep Merlin working, and I’m rather happy with this deal, it works well for us for this specific purpose.

So I guess this is the lesson from the compiler: you can have a garbage Dune file as long as it knows how to build most of your OCaml sources (it doesn’t need to be able to link, etc.), and you may get some value out of it.

3 Likes

I was able to observe the delay. The time is not spent in processing the PPX but re-typing the buffer so there is no easy fix on merlin’s side. I see two possible mitigations:

  • You appear to have a custom script that calls merlin-errors-check quite fast each time you input something. You could disable this when working on that project, or at least make it run only when you have been idle for a while.
  • The emacs lsp plugin is probably asynchronous so it might be a better choice in that case.
2 Likes

Thanks @vds!

What do you mean by “re-typing”? Essentially rendering? Is there a reason this is so expensive for this file in particular? Or is it that it happens more frequently than it should?

Hmm, I don’t have any user-config for merlin or tuareg, but I do use Doom Emacs which has its own config for it (here). I can’t see any calls to merlin-errors-check or similar-sounding functions there though. But I do see that it sets up flycheck, so it could be it’s happening through that.

FWIW it was a long time ago but I vaguely remember also suffering hangs when I was using js_of_ocaml’s style JavaScript bindings and their object phantom types.

as in running the ocaml type checker every time I believe.

There’s merlin-eldoc included in this doom config which is doing things like displaying the type of a value when you cursor is idle for some time. There’s a config value to control the idle time. If it is set to 0 it can create some performance issues.

Yes, @Khady is right: Merlin runs a modified version of the compiler’s typing algorithm on the current buffer (if it changed) to always provide up-to-date information.

I tried “bisecting” the file to identify a single culprit but I don’t think there is one: the typing time grows slightly when adding each individual test***** () value and tops at 1.5s on my machine.

Having commands called without delay on all keystrokes is probably a bit too much (since these are not just simple syntactic checks). It would be nice to have tools like flycheck have better defaults for OCaml files.

Could the editor plugin measure the time spent in time-checking, and adjust the rate of type-checking calls accordingly? For example, if type-checking took 1s, then delay the next “implicit” type-checking by at least an extra 1s. (Or maybe ask Merlin to reuse the same typedtree as before, and thus complete the request without retyping anything.)

Thanks all! I’ve now set up ocamllsp which works very smoothly, albeit with some fewer features such as no eldoc support. When looking through the Doom Emacs config for ocaml I discovered an undocumented +lsp flag which enabled ocamllsp and disabled merlin with no other action needed from me.

Ah, yeah that’s a whole different kind of typing than I was imagining :smile: That really makes a whole lot more sense with the rest of the problem description. For some reason when I read “buffer” I think of “text”, not “code”.

I could not find this config variable, but disabling merlin-eldoc did indeed remove all lag from cursor movement.

Even if it’s called less frequently, when it does get called it’ll still be unresponsive for 1.5s or so. And while checking for errors could be delayed until some notion of idleness occurs, completions need to be more responsive and seem to require type-checking too. Or at least it incurs the same delay (if not worse). Even with lsp, completions takes much longer to show in the problematic file, it just doesn’t block in the meantime.

If you do M-x customize-group RET eldoc RET you can change Eldoc Idle Delay.

But you can also change merlin-eldoc’s config to remove some features. Each of them requires an additional call to merlin which indeed can be costly.

You can also reduce the type verbosity to min which will save a few calls

This is probably the most sensible thing to do. I wrote merlin-eldoc before we had ocamllsp and emacs had less async features a few years ago. Nowadays there are a lot more efforts put in the different lsp clients for emacs. Some of them do integrate with eldoc. But I don’t know which one is used by default in doom.

1 Like

Hmm, customize-group is disabled in Doom. Now I’m wondering what I’m missing out on! I usually just use describe-variable (which there’s a keybinding for) to search for them.

It uses lsp-mode by default, but also supports eglot through a flag, which I use. And I get eldoc integration with Rust’s LSP server (rust-analyzer), so I don’t think the problem is eglot either. I’ll look more into it when I get back home.

Yes, I suspect they are the culprit here.
It’d be interesting to do a bit of profiling on ocamlc processing that file. There might be some low hanging fruits in the typechecker… (or we could always ask @hhugo to change the ppx_jsoo to not generate these ghost objects :slight_smile: ).

What seems to happen is that their editor is asking merlin to check for errors whenever something changes in the buffer. In that case the typedtree cannot be reused.

Merlin does try to share the prefix of the buffer which hasn’t changed. So I’d expect the latency to be very different when editing towards the end of the file, than it would be at the beginning.
(Though perhaps this caching isn’t working because of the ppx somehow)

What seems to happen is that their editor is asking merlin to check for errors whenever something changes in the buffer. In that case the typedtree cannot be reused.

If you reuse the previous typedtree, you are not going to see new errors, but you will be fast! The idea is that it’s okay to be wrong (but perform the expected interaction: provide a completion suggestion, etc.) during a delay that follows the last type-checking work, and is proportional to its duration.

(But yes, in any case you rather want to have this done asynchronously in any case, because even if it occurs rarely a synchronous 1.5s delay is very unpleasant.)

This is my use-package based setup, works quite well:

(use-package tuareg
  :ensure t
  :custom
  (tuareg-opam-insinuate t)
  :config)

(use-package dune-format
  :ensure t)

(use-package reason-mode
  :ensure t)

(use-package lsp-mode
  :ensure t
  :after flycheck
  :commands lsp
  :bind (("C-c l n" . flycheck-next-error)
         ("C-c l d" . lsp-find-definition)
         ("C-c l r" . lsp-find-references)
         ("C-c l h" . lsp-describe-thing-at-point)
         ("C-c l i" . lsp-find-implementation)
         ("C-c l R" . lsp-rename))
  :hook ((tuareg-mode . lsp)
         (caml-mode . lsp)
         (reason-mode . lsp)
         (before-save . lsp-organize-imports))
  :custom
  (lsp-lens-enable t)
  (lsp-log-io nil)
  (lsp-headerline-breadcrumb-enable nil)
  :config
  (lsp-enable-which-key-integration t)
  (lsp-register-client
   (make-lsp-client
    :new-connection (lsp-stdio-connection
                     '("opam" "exec" "--" "ocamllsp"))
    :major-modes '(caml-mode tuareg-mode reason-mode)
    :server-id 'ocamllsp)))

(use-package apheleia
  :ensure t
  :hook
  (caml-mode . apheleia-mode)
  (tuareg-mode . apheleia-mode)
  (reason-mode . apheleia-mode)
  :config
  (setf (alist-get 'ocamlformat apheleia-formatters)
        '("opam" "exec" "--" "ocamlformat" "--impl" "-"))
  (setf (alist-get 'refmt apheleia-formatters)
        '("opam" "exec" "--" "refmt"))
  (setf (alist-get 'tuareg-mode apheleia-mode-alist)
        '(ocamlformat))
  (setf (alist-get 'reason-mode apheleia-mode-alist)
        '(refmt)))
4 Likes