Optimising Deft for Emacs

August 1, 2023

Deft is an Emacs mode for quickly browsing, filtering, and editing directories of plain text notes, inspired by Notational Velocity. I started using it more, but as the number of notes increased it became slower and slower. Here is how I managed to make it snappy again, also for older Emacsen. (Updated with more general code).

First some background. I am on MacOS, and use Homebrew to install most of the software packages I use. Including Emacs. Homebrew offers two versions:

  • plain Emacs, which is text based and runs within the terminal you call it from, and
  • GUI Emacs, which is Emacs with proper mouse and menu support.

Plain Emacs is installed when you run brew install emacs. GUI Emacs is installed when you run brew install --cask emacs. This actually obtains the binary from Emacs For Mac OS X, maintained by David Caldwell. If you don’t use Homebrew, this is the easiest way to get a pre-compiled version of Emacs.

I prefer GUI Emacs, and have used it for years with pleasure. But, as I said, deft became slow as I started adding more notes. I had heard this had to with Emacs dealing inefficient with very long lines of text. I also heard that the latest version, 29.1, would finally fix this long standing issue. So when it was released a couple of days ago, I immediately went and tried it out. As the GUI Emacs release was not yet available, I tried the plain version first, and indeed it was snappy as hell! Awesome.

But, as I prefer the GUI version, I installed it a day later with great anticipation, only to discover that it was much much slower running deft than plain Emacs, and even slower than GUI Emacs version 28.2. So I started to investigate using the built-in profiler of Emacs, and saw that GUI Emacs was using 70 MB of RAM and 1436 ‘samples’ of CPU time to open deft with 170 notes. Plain Emacs used 26 MB (still a lot) but only 228 samples of CPU time for the same set of notes. The difference was even bigger when starting to enter the first letter of an incremental search string: 57 MB and 1100 samples for GUI Emacs versus 7 MB and 81 samples for plain Emacs.

Digging deeper, the culprit turned out to be the built in function string-width (used in deft-string-widthand truncate-string-to-width). Note that truncate-string-to-width is defined in mule-util.el, so the slowness of string-width impacts many other Emacs packages.

string-width computes the width of the string (i.e. the number of displayed characters it takes up on the screen) taking Unicode and emoticons into account. When using a plain Latin fixed width font (my notes are mostly in English), this is overkill: returning the length of the string is good enough approximation. In fact the following definition

(defun string-width (str) (length str))

would probably speed up a lot of packages, but would also break many of them if the do rely on the exact width calculation.

Luckily, in deft, string-width is always called to test whether a string is less wide than (at most) the window width, or to truncate a string to (at most) the window width. In other words, if we first cut the string to something reasonable small (we use 4 times the window width below to account for UTF and emoji roughly) before computing the actual width, we are good.

So I put the following code in my init.el to only patch deft to not use string-width directly (this is a better version of the code published yesterday:

(defun deft-truncate-string-to-window-width (str)
  (if str
      (if (> (length str) (* deft-window-width 4))
          (substring str 0 (* deft-window-width 4))
      str
      )
   ""
  )
)

(defun deft-string-width (str)
  (string-width (deft-truncate-string-to-window-width str))  
)

(defun deft-truncate-string-to-width (str width)
  (truncate-string-to-width (deft-truncate-string-to-window-width str) width)  
)

And use deft-truncate-string-to-width instead of truncate-string-to-width in deft-file-widget as follows

(defun deft-file-widget (file)
  "Add a line to the file browser for the given FILE."
  (when file
    (let* ((key (file-name-nondirectory file))
           (text (deft-file-contents file))
           (title (deft-file-title file))
           (summary (deft-file-summary file))
           (mtime (when deft-time-format
                    (format-time-string deft-time-format (deft-file-mtime file))))
           (mtime-width (deft-string-width mtime))
           (line-width (- deft-window-width mtime-width))
           (title-width (min line-width (deft-string-width title)))
           (summary-width (min (deft-string-width summary)
                               (- line-width
                                  title-width
                                  (length deft-separator)))))
      (widget-create 'link
                     :button-prefix ""
                     :button-suffix ""
                     :button-face 'deft-title-face
                     :format "%[%v%]"
                     :tag file
                     :help-echo "Edit this file"
                     :notify (lambda (widget &rest ignore)
                               (deft-open-file (widget-get widget :tag)))
                     (if title (deft-truncate-string-to-width title title-width)
                       deft-empty-file-title))
      (when (> summary-width 0)
        (widget-insert (propertize deft-separator 'face 'deft-separator-face))
        (widget-insert (propertize (deft-truncate-string-to-width summary summary-width)
                                   'face 'deft-summary-face)))
      (when mtime
        (while (< (current-column) line-width)
          (widget-insert " "))
        (widget-insert (propertize mtime 'face 'deft-time-face)))
      (widget-insert "\n"))))

With that, deft was quite responsive again. But, as I said, GUI Emacs 29.1 is even slower than GUI Emacs 28.2, so I reverted back to that version for now. I do hope a new release of GUI Emacs will fix the issue. I did file a bug report with David of course.

In case you spot any errors on this page, please notify me!
Or, leave a comment.