refactor: startup optimizations

I revisit all our startup optimizations to see how they fair in Emacs
29.x and 30.x. Most of them still hold up. I've revised and updated most
of the accompanying comments to better explain them, given what I know
now compared to when I first wrote them.
This commit is contained in:
Henrik Lissner 2024-04-03 20:15:15 -04:00
parent 2debe85a8a
commit 869ad10f34
No known key found for this signature in database
GPG Key ID: B60957CA074D39A3

View File

@ -352,12 +352,12 @@ users).")
;;; Startup optimizations ;;; Startup optimizations
;; Here are Doom's hackiest (and least offensive) startup optimizations. They ;; Here are Doom's hackiest (and least offensive) startup optimizations. They
;; exploit implementation details and unintended side-effects, and will change ;; exploit implementation details and unintended side-effects in Emacs' startup
;; often between major Emacs releases. However, I disable them if this is a ;; process, and will change often between major Emacs releases. However, I
;; daemon session (where startup time matters less). ;; disable them if this is a daemon session (where startup time matters less).
;; ;;
;; Most of these have been tested on Linux and on fairly fast machines (with ;; Most of these have been tested on Linux and on fairly fast machines (with
;; SSDs), so your mileage may vary depending on your hardware. ;; SSDs), so your mileage may vary depending on hardware and `window-system'.
(unless (daemonp) (unless (daemonp)
;; PERF: `file-name-handler-alist' is consulted on each call to `require', ;; PERF: `file-name-handler-alist' is consulted on each call to `require',
;; `load', or various file/io functions (like `expand-file-name' or ;; `load', or various file/io functions (like `expand-file-name' or
@ -366,14 +366,13 @@ users).")
(let ((old-value (default-toplevel-value 'file-name-handler-alist))) (let ((old-value (default-toplevel-value 'file-name-handler-alist)))
(set-default-toplevel-value (set-default-toplevel-value
'file-name-handler-alist 'file-name-handler-alist
;; HACK: If the bundled elisp for this Emacs install isn't byte-compiled ;; HACK: The libraries bundled with Emacs can either be compiled,
;; (but is compressed), then leave the gzip file handler there so Emacs ;; compressed, or neither. We use calc-loaddefs.el as a heuristic to
;; won't forget how to read read them. ;; guess what state all these libraries are in. If they're compressed, we
;; ;; need to leave the gzip file handler in `file-name-handler-alist' so
;; calc-loaddefs.el is our heuristic for this because it is built-in to ;; Emacs knows how to load them. If they're compiled or neither, we can
;; all supported versions of Emacs, and calc.el explicitly loads it ;; omit the gzip handler altogether (at least during startup) for a boost
;; uncompiled. This ensures that the only other, possible fallback would ;; in startup and package load time.
;; be calc-loaddefs.el.gz.
(if (eval-when-compile (if (eval-when-compile
(locate-file-internal "calc-loaddefs.el" load-path)) (locate-file-internal "calc-loaddefs.el" load-path))
nil nil
@ -395,55 +394,46 @@ users).")
(unless noninteractive (unless noninteractive
;; PERF: Resizing the Emacs frame (to accommodate fonts that are smaller or ;; PERF: Resizing the Emacs frame (to accommodate fonts that are smaller or
;; larger than the system font) appears to impact startup time ;; larger than the system font) appears to impact startup time
;; dramatically. The larger the delta in font size, the greater the delay. ;; dramatically. The larger the delta, the greater the delay. Even trivial
;; Even trivial deltas can yield a ~1000ms loss, though it varies wildly ;; deltas can yield up to a ~1000ms loss, depending on font size and
;; depending on font size. ;; `window-system'. PGTK seems least affected and NS/MAC the most.
(setq frame-inhibit-implied-resize t) (setq frame-inhibit-implied-resize t)
;; PERF,UX: Reduce *Message* noise at startup. An empty scratch buffer (or ;; PERF: A fair bit of startup time goes into initializing the splash and
;; the dashboard) is more than enough, and faster to display. ;; scratch buffers in the typical Emacs session (b/c they activate a
;; non-trivial major mode, generate the splash buffer, and trigger
;; premature frame redraws by writing to *Messages*). These hacks prevent
;; most of this work from happening for some decent savings in startup
;; time. Our dashboard and `doom/open-scratch-buffer' provide a faster
;; (and more useful) alternative anyway.
(setq inhibit-startup-screen t (setq inhibit-startup-screen t
inhibit-startup-echo-area-message user-login-name) inhibit-startup-echo-area-message user-login-name
;; PERF,UX: Remove "For information about GNU Emacs..." message at startup. initial-major-mode 'fundamental-mode
;; It's redundant with our dashboard and incurs a premature redraw. initial-scratch-message nil)
;; PERF,UX: Prevent "For information about GNU Emacs..." line in *Messages*.
(advice-add #'display-startup-echo-area-message :override #'ignore) (advice-add #'display-startup-echo-area-message :override #'ignore)
;; PERF: Suppress the vanilla startup screen completely. We've disabled it ;; PERF: Suppress the vanilla startup screen completely. We've disabled it
;; with `inhibit-startup-screen', but it would still initialize anyway. ;; with `inhibit-startup-screen', but it would still initialize anyway.
;; This involves some file IO and/or bitmap work (depending on the frame ;; This involves file IO and/or bitmap work (depending on the frame type)
;; type) that we can no-op for a free 50-100ms boost in startup time. ;; that we can no-op for a free 50-100ms saving in startup time.
(advice-add #'display-startup-screen :override #'ignore) (advice-add #'display-startup-screen :override #'ignore)
;; PERF: Shave seconds off startup time by starting the scratch buffer in
;; `fundamental-mode', rather than, say, `org-mode' or `text-mode', which
;; pull in a ton of packages. This buffer is created whether or not we're
;; in an interactive session. Plus, `doom/open-scratch-buffer' provides a
;; better scratch buffer, so keep the initial one blank.
(setq initial-major-mode 'fundamental-mode
initial-scratch-message nil)
(unless initial-window-system (unless initial-window-system
;; PERF: Inexplicably, `tty-run-terminal-initialization' can sometimes ;; PERF: `tty-run-terminal-initialization' can take 2-3s when starting up
;; take 2-3s when starting up Emacs in the terminal. Whatever slows it ;; TTY Emacs (non-daemon sessions), depending on your TERM, TERMINFO,
;; down at startup doesn't appear to affect it if it's called a little ;; and TERMCAP, but this work isn't very useful on modern systems (the
;; later in the startup process, so that's what I do. ;; type I expect Doom's users to be using). The function seems less
;; REVIEW: This optimization is not well understood. Investigate it! ;; expensive if run later in the startup process, so I defer it.
;; REVIEW: This may no longer be needed in 29+. Needs testing!
(define-advice tty-run-terminal-initialization (:override (&rest _) defer) (define-advice tty-run-terminal-initialization (:override (&rest _) defer)
(advice-remove #'tty-run-terminal-initialization #'tty-run-terminal-initialization@defer) (advice-remove #'tty-run-terminal-initialization #'tty-run-terminal-initialization@defer)
(add-hook 'window-setup-hook (add-hook 'window-setup-hook
(doom-partial #'tty-run-terminal-initialization (doom-partial #'tty-run-terminal-initialization
(selected-frame) nil t)))) (selected-frame) nil t))))
;; PERF,UX: Site files tend to use `load-file', which emits "Loading X..." ;; PERF: `load-suffixes' and `load-file-rep-suffixes' are consulted on each
;; messages in the echo area. Writing to the echo-area triggers a ;; `require' and `load'. Doom won't load any modules this early, so omit
;; redisplay, which can be expensive during startup. This may also cause ;; .so for a tiny startup boost. Is later restored in doom-start.
;; an flash of white when creating the first frame. Needs to be undo
;; later, though.
(define-advice load-file (:override (file) silence)
(load file nil 'nomessage))
;; PERF: `load-suffixes' and `load-file-rep-suffixes' are consulted on
;; each `require' and `load'. Doom won't load any modules this early, so
;; omit .so for a tiny startup boost. Is later restored in doom-start.
(put 'load-suffixes 'initial-value (default-toplevel-value 'load-suffixes)) (put 'load-suffixes 'initial-value (default-toplevel-value 'load-suffixes))
(put 'load-file-rep-suffixes 'initial-value (default-toplevel-value 'load-file-rep-suffixes)) (put 'load-file-rep-suffixes 'initial-value (default-toplevel-value 'load-file-rep-suffixes))
(set-default-toplevel-value 'load-suffixes '(".elc" ".el")) (set-default-toplevel-value 'load-suffixes '(".elc" ".el"))
@ -455,35 +445,34 @@ users).")
(setq load-suffixes (get 'load-suffixes 'initial-value) (setq load-suffixes (get 'load-suffixes 'initial-value)
load-file-rep-suffixes (get 'load-file-rep-suffixes 'initial-value)))) load-file-rep-suffixes (get 'load-file-rep-suffixes 'initial-value))))
;; PERF: Doom uses `defcustom' to indicate variables that users are ;; PERF: Doom uses `defcustom' merely to announce variables that users may
;; expected to reconfigure. Trouble is it fires off initializers meant ;; reconfigure. Trouble is it fires off initializers meant to accommodate
;; to accommodate any user attempts to configure them before they were ;; any user attempts to configure them *before* they are defined, which
;; defined. This is unnecessary work before $DOOMDIR/init.el is loaded, ;; isn't possible since the user's first opportunity to modify them comes
;; so I disable them until it is. ;; long after they're defined (in $DOOMDIR/init.el), so this is
;; unnecessary work. To spare Emacs the startup time, I disable this
;; behavior until $DOOMDIR is loaded.
(setq custom-dont-initialize t) (setq custom-dont-initialize t)
(add-hook! 'doom-before-init-hook (add-hook! 'doom-before-init-hook
(defun doom--reset-custom-dont-initialize-h () (defun doom--reset-custom-dont-initialize-h ()
(setq custom-dont-initialize nil))) (setq custom-dont-initialize nil)))
;; PERF: Doom disables the UI elements by default, so that there's less ;; PERF: The mode-line procs a couple dozen times during startup, before the
;; for the frame to initialize. However, the toolbar is still populated ;; user can even see the first mode-line. This is normally fast, but we
;; regardless, so I lazy load it until tool-bar-mode is actually used. ;; can't predict what the user (or packages) will put into the mode-line.
(advice-add #'tool-bar-setup :override #'ignore) ;; Also, mode-line packages have a bad habit of throwing performance to
;; the wind, so best we just disable the mode-line until we can see one.
;; PERF: The mode-line procs a couple dozen times during startup. This is
;; normally quite fast, but disabling the default mode-line and reducing
;; the update delay timer seems to stave off ~30-50ms.
(put 'mode-line-format 'initial-value (default-toplevel-value 'mode-line-format)) (put 'mode-line-format 'initial-value (default-toplevel-value 'mode-line-format))
(setq-default mode-line-format nil) (setq-default mode-line-format nil)
(dolist (buf (buffer-list)) (dolist (buf (buffer-list))
(with-current-buffer buf (setq mode-line-format nil))) (with-current-buffer buf (setq mode-line-format nil)))
;; PERF,UX: Premature redisplays can substantially affect startup times ;; PERF,UX: Premature redisplays/redraws can substantially affect startup
;; and/or produce ugly flashes of unstyled Emacs. ;; times and/or flash a white/unstyled Emacs frame during startup, so I
;; try real hard to suppress them until we're sure the session is ready.
(setq-default inhibit-redisplay t (setq-default inhibit-redisplay t
inhibit-message t) inhibit-message t)
;; COMPAT: Then reset with advice, because `startup--load-user-init-file' ;; COMPAT: If the above vars aren't reset, Emacs could appear frozen or
;; will never be interrupted by errors. And if these settings are left ;; garbled after startup (or in case of an startup error).
;; set, Emacs could appear frozen or garbled.
(defun doom--reset-inhibited-vars-h () (defun doom--reset-inhibited-vars-h ()
(setq-default inhibit-redisplay nil (setq-default inhibit-redisplay nil
;; Inhibiting `message' only prevents redraws and ;; Inhibiting `message' only prevents redraws and
@ -491,35 +480,43 @@ users).")
(redraw-frame)) (redraw-frame))
(add-hook 'after-init-hook #'doom--reset-inhibited-vars-h) (add-hook 'after-init-hook #'doom--reset-inhibited-vars-h)
;; PERF,UX: An annoying aspect of site-lisp files is that they're often ;; PERF: Doom disables the UI elements by default, so that there's less for
;; noisy (they emit load messages or other output to stdout). These ;; the frame to initialize. However, `tool-bar-setup' is still called and
;; queue unnecessary redraws at startup, cost startup time, and pollute ;; it does some non-trivial work to set up the toolbar before we can
;; the logs. I get around it by suppressing it until we can load it ;; disable it. To side-step this work, I disable the function and call it
;; manually, later (in the `startup--load-user-init-file' advice below). ;; later (see `startup--load-user-init-file@undo-hacks').
(advice-add #'tool-bar-setup :override #'ignore)
;; PERF,UX: site-lisp files are often obnoxiously noisy (emitting load
;; messages or other output to *Messages* or stdout). These queue
;; unnecessary redraws at startup which impact startup time depending on
;; window system. It also pollutes the logs. By suppressing it now, I can
;; load it myself, later, in a more controlled way (see
;; `startup--load-user-init-file@undo-hacks').
(put 'site-run-file 'initial-value site-run-file) (put 'site-run-file 'initial-value site-run-file)
(setq site-run-file nil) (setq site-run-file nil)
(define-advice startup--load-user-init-file (:around (fn &rest args) undo-inhibit-vars) (define-advice startup--load-user-init-file (:around (fn &rest args) undo-hacks)
(let (--init--) "Undo Doom's startup optimizations to prep for the user's session."
(let (init)
(unwind-protect (unwind-protect
(progn (progn
;; COMPAT: Onces startup is sufficiently complete, undo some
;; optimizations to reduce the scope of potential edge cases.
(advice-remove #'load-file #'load-file@silence)
(advice-remove #'tool-bar-setup #'ignore)
(add-transient-hook! 'tool-bar-mode (tool-bar-setup))
(when (setq site-run-file (get 'site-run-file 'initial-value)) (when (setq site-run-file (get 'site-run-file 'initial-value))
(let ((inhibit-startup-screen inhibit-startup-screen)) (let ((inhibit-startup-screen inhibit-startup-screen))
(letf! (defun load (file &optional noerror _nomessage &rest args) (letf! ((defun load-file (file) (load file nil 'nomessage))
(apply load file noerror t args)) (defun load (file &optional noerror _nomessage &rest args)
(apply load file noerror t args)))
(load site-run-file t t)))) (load site-run-file t t))))
;; Then startup as normal. (apply fn args) ; start up as normal
(apply fn args) (setq init t))
(setq --init-- t)) (when (or (not init) init-file-had-error)
(when (or (not --init--) init-file-had-error)
;; If we don't undo our inhibit-{message,redisplay} and there's an ;; If we don't undo our inhibit-{message,redisplay} and there's an
;; error, we'll see nothing but a blank Emacs frame. ;; error, we'll see nothing but a blank Emacs frame.
(doom--reset-inhibited-vars-h)) (doom--reset-inhibited-vars-h))
;; COMPAT: Once startup is sufficiently complete, undo our earlier
;; optimizations to reduce the scope of potential edge cases.
(advice-remove #'tool-bar-setup #'ignore)
(add-transient-hook! 'tool-bar-mode (tool-bar-setup))
(unless (default-toplevel-value 'mode-line-format) (unless (default-toplevel-value 'mode-line-format)
(setq-default mode-line-format (get 'mode-line-format 'initial-value)))))) (setq-default mode-line-format (get 'mode-line-format 'initial-value))))))