How to Profile Slow Scrolling (And Other Performance Bottlenecks) in Emacs

I played around with Nicolas Rougier’s NANO Emacs configuration because it looks so hot and I wanted to try some of his tasteful settings myself.

One thing that let me down for a couple days since I started eyeballing the package was performance. In my huge org files to organize app development tasks, scrolling was so-so. Pixel scrolling, which I discovered through Nicolas’s configuration ((pixel-scroll-mode +1)), didn’t work at all on my machine. I have a MacBook Pro 13" from 2020. This is a text editor. Something’s not right with my config.

I discovered Emacs’s built-in profiling package. That. Is. Amazing.

  • M-x profiler-start, select CPU usage
  • scroll in the big file for a while
  • M-x profiler-stop
  • M-x profiler-report to show the measurements

The output surprisingly reminds me of Xcode Instrument’s profiler. It’s good. You get a nested, interactive list like this:

- command-execute                                                6597  96%
 - call-interactively                                            6597  96%
  - funcall-interactively                                        6597  96%
   - eval-expression                                             6597  96%
    - eval                                                       6597  96%
     - pixel-scroll-mode                                         6597  96%
      - debug                                                    6597  96%
       - recursive-edit                                          6566  96%
        - command-execute                                        6437  94%
         - call-interactively                                    6437  94%
          - funcall-interactively                                5570  81%
           - mwheel-scroll                                       5527  81%
            - pixel-scroll-up                                    5389  79%
             - pixel-line-height                                 4442  65%
              - pixel-visual-line-height                         4442  65%
               - pixel-visible-pos-in-window                     4438  65%
                - pos-visible-in-window-p                        4430  65%
                 - eval                                          4430  65%
                  - concat                                       4311  63%
                   - projectile-project-name                     4311  63%
                    + projectile-project-root                    4311  63%
                  + doom-modeline-format--main                    115   1%
             + pixel-scroll-pixel-up                              560   8%
             + pixel-point-at-top-p                               385   5%
            + pixel-scroll-down                                   138   2%
            ...

From this I learned that projectile-project-name is responsible for 63% of CPU load during the scroll operation. That didn’t make much sense to me, because I wasn’t switching buffers, so no need to update the project name, right?

Turns out my configuration was stupid:

(use-package projectile
  :demand
  ;; Remove the mode name for projectile-mode, but show the project name.
  :delight '(:eval (concat " p[" (projectile-project-name) "]"))
  :config
  ;; ...
  )

This shortens the minor mode “lighter” (that’s the info text about which minor mode is currently active) to show p[PROJECTNAME]. Used to be Projectile, which I didn’t find very helpful.

Since the next entry in the scrolling callbacks at that point is doom-modeline-format--main, I assume that during scrolling, the modeline (aka status bar) updates continuously, e.g. to display the current line and column of the cursor position, and the % I have scrolled, and such things. And that modeline update apparently also requests the project name over and over and over and that’s kinda slow, it seems?

I would’ve expected a stack trace/call tree of modeline-update-sth-sth first, but the educated guess here still paid off: I removed the lighter completely (just :delight, no arguments) and am now enjoying butter-smooth scrolling for the most part.

Nice!

Also, very cool that Emacs is in fact a Lisp engine, and that everything that happens inside the editor is part of the program, so you can inspect and customize and profile it. That was super helpful!

My first attempt, by the way, was to load 50% of my configuration file and then see how performance was. Essentially like git bisect, aka “I don’t know what’s going on and am mechanically narrowing down the source of the problem step by step”.

With profiling, you leave the guessing game behind completely and get really useful, focused data. I recommend adding this to your toolset.

See also:

Receive new .