Where parallels cross

Interesting bits of life

How I inspected my Emacs configuration and discovered once again Org-Mode people are smart

Too long; didn't read

Use org-babel-load-file on your org-mode based configuration directly to exploit its caching mechanism: tangling code blocks is slow!

The problem

In my Emacs workflow I liked to have everything in a single org-mode file. Even my configuration! Now, although it is nice to have everything at a search away, this became significantly slow with time.

One thing that really bored me was the emacs-init-time: it was longer than 20 seconds.

It is a problem indeed

Add to this that I like to start Emacs anew often, this was quite troublesome. You do not want to wait on your editor. And more importantly you do not want to waste your life.

And there is a solution

Well, I started from debugging the initiation time of my packages. Some time ago, I found myself spending quite some time fixing misconfigurations in my packages, so I decided to label the start and the end of a package run time. It would look like this:

(message "--- expand-region begin")
(use-package expand-region
  :bind (("C-=" . er/expand-region)))
(message "--- expand-region end")

So at any problem I would find in my logging something like:

...
expand-region begin

And I would not find an end. That would give away the culprit of my problem.

Since all my packages are setup in this way, the problem of knowing how long they take is simple: just swap message for a function that takes time.

Since I did not have patience to search for something already made by someone else, I just hacked my way through:

(defun my/init-audit-message (string)
  "Print out STRING and calculate length of init."
  (message string)
  (if (not (string= "end" (substring string -3))) 
      (setq my/init-audit-message-begin (current-time))
    (message
     "It took %s seconds in total."
     (time-to-seconds
      (time-subtract
       (current-time)
       my/init-audit-message-begin))))
  nil)

This prints a message after the end saying how long it took to load the package.

Now I would have:

(my/init-audit-message "--- expand-region begin")
(use-package expand-region
  :bind (("C-=" . er/expand-region)))
(my/init-audit-message "--- expand-region end")

And the logging would look like:

...
expand-region begin
expand-region end
It took 0.0001 seconds in total.
...

I found out that some packages needed to be load more lazily (use-package offers keywords like :defer, :commands, :mode just for that), and my initialization time went from 20 seconds to approximately 12 seconds.

This was an improvement, but still disappointing! Where did those seconds come from?

After some head scratching moments I realized my problem was in my init.el. My configuration is a literate program written in org-mode. So at startup Emacs tangles the Elisp snippets in a configuration.el and then loads it. The tangling takes quite some time because it is IO bound (writing files to the file system is not that fast and tangling is even slower).

The smart org-mode maintainers are clearly aware of the issue because if you peek into org-babel-load-file you will see:

...
    (unless (org-file-newer-than-p
             tangled-file
             (file-attribute-modification-time
              (file-attributes (file-truename file))))
...

That code avoids tangling unless the configuration file is newer than the tangled file. The issue is that I was too many layers of indirection away: I was tangling my configuration on Emacs exit which resulted always in a new file, so always slow tangling.

Now I keep the agenda in its own org-mode file and my starting time is of at most a couple of seconds (unless I update my configuration). Finally happy!!

Conclusion

So please inspect your configuration performance if you start your Emacs multiple times a day and please save yourself some time everyday: your time is the most important thing you have!

Happy hacking!

Comments