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-reportto 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.
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.
- Profiling manual
- The Projectile package documentation also has a lot of useful debugging tips I didn’t know anything about!