gregsexton / ob-ipython

org-babel integration with Jupyter for evaluation of (Python by default) code blocks
739 stars 109 forks source link

company-ob-ipython is unusably slow #151

Open tmurph opened 6 years ago

tmurph commented 6 years ago

Just want to say that I love the work that's gone into this package, and I'm excited to see if there's a way to improve the usability here. Or maybe it's only me experiencing a massive slowdown? But I kind of expect not.

I'm on Mac OSX with Emacs 25.3.1. ob-ipython version 20171209.634 installed from MELPA. org-plus-contrib version 20171218 installed via package.el from the Org package archive. python.el version 0.25.2 installed from MELPA Stable.

Issue: With company-mode enabled and company-ob-ipython added to my completion backends, whenever I edit an ipython source block, Emacs hangs on each character I type.

This persists even though I have company-minimum-prefix-length set to 3, and I'm pretty sure this is because company-ob-ipython shells out to a fresh jupyter client just to calculate the prefix for potential completion. I don't have actual timing data, it just seems obvious to me that's where the slowdown occurs.

I took a stab at rewriting the prefix calculation in pure Emacs lisp, but that failed for a bunch of uninteresting reasons. I'm not great at writing company backends.

One interesting thing I noticed, though, was that even when I was calculating a halfway decent prefix in Emacs lisp, completion was still unusably slow because the candidate list took forever to build. Again, I'm pretty sure this comes down to shelling out to a fresh jupyter client.

Sooooooo in summary, I think this is the slow code, specifically the call-process-region line, but I’m at a loss for how to speed it up.

(defun ob-ipython--complete-request (code &optional pos)
  (let ((input (json-encode `((code . ,code)
                              (pos . ,(or pos (length code))))))
        (args (list "--" ob-ipython-client-path "--conn-file"
                    (ob-ipython--get-session-from-edit-buffer (current-buffer))
                    "--complete")))
    (with-temp-buffer
      (let ((ret (apply 'call-process-region input nil
                        (ob-ipython--get-python) nil t nil
                        args)))
        (if (> ret 0)
            (ob-ipython--dump-error (buffer-string))
          (goto-char (point-min))
          (ob-ipython--collect-json))))))

Is anybody else experiencing this issue? And any ideas how to resolve it?

frdrk-jhnssn commented 6 years ago

I'm also experiencing this issue (which is my only issue with this great package). I really have no idea of the cause, but if your analysis is correct, wouldn't it be slow on any system, and the author would have noticed it?

tmurph commented 6 years ago

In private email communication, the author acknowledged the issue and walked me through some of the technical challenges currently in the way. I can summarize here:

  1. Sometimes the Jupyter kernel is just really slow to respond.
  2. Company needs to make two calls to retrieve completions, and the first call has to be done synchronously (blocking Emacs).

I've struggled to time which of these is the biggest slowdown.

Re: (1). I don't know if anything can be done from the Emacs side. I'm sure the Jupyter folks would appreciate any patches.

Re: (2) there may be some opportunities within Emacs. The first call calculates the prefix to be completed, so if we could read up on how Jupyter calculates the prefix and implement that in Emacs we could complete the first call without an expensive roundtrip (at the obvious risk of breaking completion if the Jupyter rules ever change).

The author stated that he currently sidesteps the issue by turning off on-the-fly completion. I guess for some the hiccup isn't intolerable when it's triggered manually.

tmurph commented 6 years ago

FWIW I also believe there's an obvious slowdown because of shelling out to a fresh python process for each roundtrip request to the kernel. For those keeping score at home, that's two python processes per completion request.

But, again, I have no clue if that is a big time loss or a small time loss relative to the other two points above.

dangirsh commented 6 years ago

Until this is fixed, I'm also disabling on-the-fly completions. This is done by:

(setq-local company-idle-delay nil)

Company doesn't have support for setting that per-mode, which would be nice in this situation. As a hack, disable automatic completion in the Org Src buffer with the following advice:

(defun my-org-babel-edit-prep:ipython (info)
  (setq-local company-idle-delay nil))

(advice-add 'org-babel-edit-prep:ipython :after #'my-org-babel-edit-prep:ipython)

The next step is to bind a key to company-indent-or-complete-common so you can manually ask for completions. Note that TAB is a bad choice, since commands in source blocks can call org-return-indent, which spoofs a TAB internally. This can make hitting return in a source block trigger the slow company completion.

The scimax enhancements support completion directly in the org buffer, which is very nice. For that, I'm putting this at the end of my ob-ipython buffers:

# Local Variables:
# eval: (setq-local company-idle-delay nil)
# End: