GNUS with threads

<2024-03-27 Wed>

Emacs users have an unreasonable longing for thread support, not the make-thread cooperative threading kind that we have, but the concurrent execution kind.

We want the Emacs experience to feel smooth. Inputs should result in stuff changing instantly on the screen, no stutters.

Is concurrent executing threads the only way to make Emacs buttery smooth?

Why does Emacs freeze up?

I would guess that most of the time that Emacs is freezing up it does so by design. It's busy waiting for some kind of IO to finish (sub process or network)

Emacs slowdown is seldom CPU bound but IO bound.

This is an very common way of doing IO with with a sub processes or network connection in elisp:

(let (response)
  (send-data process data)
  (while (not (setq response (get-response process)))
    ;; Busy wait until `get-response' returns non nil
    (accept-process-output process)))

Most of the time Emacs is not struggling to eval some elisp code, its busy waiting in these while + accept-process-output constructs. A place where all mouse and keyboard events are blocked, except the mighty C-g.

There are a couple of ways to solve this problem when doing IO in elisp.

Lets explore one of the ways together with the well know gnus loading screen (or the command that makes Emacs freeze up for 10 second).

One hacky solution

Here is an stack trace post M-x gnus; where Emacs is spending a lot of time doing nothing but it's best to block keyboard events.

> (accept-process-output)
  (map-wait-for-response)
  (nnimap-finish-retrieve-group-infos)
  ...
  (gnus-get-unread-articles nil nil)
  (gnus-setup-news nil nil nil)
  (#f(compiled-function () #<bytecode 0xac4a9c051bacf2d>))
  ...
  (gnus)

What if:

  1. The gnus command was executed on a make-thread
  2. And s/accept-process-output/thread-yield

Then we could yield to the Main thread instead of wasting flops and blocking keyboard events in (while (not response)). Good news is that accept-process-output actually does yield if there are other threads.

Let me present the non blocking gnus command variant gnus-with-thread-polling in Emacs proper with threads (the kind we have):

(defun gnus-with-thread-polling ()
  (interactive)
  (make-thread 'gnus))

with-thread-polling should be able to wrap other IO bound gnus commands.

Notes

Should I yank this into init.el?

Sure, but I can't promise that it won't break anything.

Performance

gnus-with-thread-polling will never be faster then gnus. Did some benchmarking but the response time for my email provider is nothing but stable I can't say anything conclusive.

Disclaimer

This is not how I would have used make-thread if I where to refactor gnus. But rather exploring condition-notify in the filter functions or just using callbacks from the filter functions instead.

MacOS

with-thread-polling don't work on MacOS with Emacs GUI (worked fine with emacs -nw):

  1. thread-yield will always yield to main thread but main thread won't yield back to gnus thread until it gets a keyboard/mouse event. see ns_select_1 in src/nsterm.m. (bug #70032)
  2. Emacs crashes due to gnus changing the buffer name which in turn updates the ui bar thingy in an non main thread, which is not valid in MacOS.

(2.) can be solved with the following:

;; remove osx ui bar thingy
(add-to-list 'default-frame-alist '(undecorated . t))

Profiling

But why does accept-process-output not show up when M-x profiler-start? I have no idea but slap an advice around it with benchmark-progn and see for yourself.

Thanks

Thanks to tromey in #emacs for enlightening me that accept-process-output yields.