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!