refactor: move startup optimizations to doom.el

I move our hackiest and least offensive startup optimizations to core,
so they're easy for me to keep track of (they'll likely change often,
between major Emacs releases), to keep them from affecting non-Doom
profiles, and make it easy for readers to use as a reference.
This commit is contained in:
Henrik Lissner 2022-09-13 13:08:37 +02:00
parent 9ac167fb84
commit 42d88421ba
No known key found for this signature in database
GPG key ID: B60957CA074D39A3
3 changed files with 248 additions and 242 deletions

View file

@ -31,158 +31,113 @@
;; cause stuttering/freezes.
(setq gc-cons-threshold most-positive-fixnum)
(eval-and-compile
;; PERF: Don't use precious startup time checking mtime on elisp bytecode.
;; Ensuring correctness is 'doom sync's job, not the interactive session's.
;; Still, stale byte-code will cause *heavy* losses in startup efficiency.
(setq load-prefer-newer noninteractive))
;; UX: If debug mode is on, be more verbose about loaded files.
(setq force-load-messages init-file-debug)
;; PERF: Employ various startup optimizations. This benefits all sessions,
;; including noninteractive ones...
(unless (or (daemonp) ; ...but be more liberal in daemon sessions
init-file-debug ; ...and don't interfere with the debugger
(boundp 'doom-version)) ; ...or if doom is already loaded
;; PERF: `file-name-handler-alist' is consulted on each `require', `load' and
;; various path/io functions (like `expand-file-name' or `file-remote-p').
;; You get a noteable, boost to startup times by unsetting this.
(let ((old-file-name-handler-alist file-name-handler-alist))
(setq file-name-handler-alist
;; HACK: If the bundled elisp for this Emacs install isn't
;; byte-compiled (but is compressed), then leave the gzip file
;; handler there so Emacs won't forget how to read read them.
;;
;; calc-loaddefs.el is our heuristic for this because it is built-in
;; to all supported versions of Emacs, and calc.el explicitly loads
;; it uncompiled. This ensures that the only other, possible
;; fallback would be calc-loaddefs.el.gz.
(if (eval-when-compile
(locate-file-internal "calc-loaddefs.el" load-path nil))
nil
(list (rassq 'jka-compr-handler file-name-handler-alist))))
;; ...but restore `file-name-handler-alist' later, because it is needed for
;; handling encrypted or compressed files, among other things.
(defun doom-reset-file-handler-alist-h ()
(setq file-name-handler-alist
;; Merge instead of overwrite because there may have been changes to
;; `file-name-handler-alist' since startup we want to preserve.
(delete-dups (append file-name-handler-alist
old-file-name-handler-alist))))
(add-hook 'emacs-startup-hook #'doom-reset-file-handler-alist-h 101))
;; PERF: Site files tend to use `load-file', which emits "Loading X..."
;; messages in the echo area. Writing to the echo-area triggers a redisplay,
;; which can be expensive during startup. This can also cause an ugly flash
;; of white when first creating the frame. This attempts try to avoid both.
(define-advice load-file (:override (file) silence)
(load file nil :nomessage))
;; FIX: ...Then undo our `load-file' advice later, as to limit the scope of
;; any edge cases it may possibly introduce.
(define-advice startup--load-user-init-file (:before (&rest _) init-doom)
(advice-remove #'load-file #'load-file@silence)))
;;
;;; Detect `user-emacs-directory'
;; Prevent recursive profile processing, in case you're loading a Doom profile.
(unless (boundp 'doom-version)
;; Not using `command-switch-alist' to process --profile and --init-directory
;; was intentional. `command-switch-alist' is processed too late at startup to
;; change `user-emacs-directory' in time.
;; DEPRECATED: Backported from Emacs 29.
(let ((initdir (or (cadr (member "--init-directory" command-line-args))
(getenv-internal "EMACSDIR"))))
(when initdir
;; FIX: Discard the switch to prevent "invalid option" errors later.
(push (cons "--init-directory" (lambda (_) (pop argv))) command-switch-alist)
(setq user-emacs-directory (expand-file-name initdir))))
(let ((profile (or (cadr (member "--profile" command-line-args))
(getenv-internal "DOOMPROFILE"))))
(when profile
;; FIX: Discard the switch to prevent "invalid option" errors later.
(push (cons "--profile" (lambda (_) (pop argv))) command-switch-alist)
;; While processing the requested profile, Doom loosely expects
;; `user-emacs-directory' to be changed. If it doesn't, then you're using
;; profiles.el as a glorified, runtime dir-locals.el (which is fine, if
;; intended).
(catch 'found
(let ((profiles-file (expand-file-name "profiles.el" user-emacs-directory)))
(when (file-exists-p profiles-file)
(with-temp-buffer
(let ((coding-system-for-read 'utf-8-auto))
(insert-file-contents profiles-file))
(condition-case-unless-debug e
(let ((profile-data (cdr (assq (intern profile) (read (current-buffer))))))
(dolist (var profile-data (if profile-data (throw 'found t)))
(if (eq (car var) 'env)
(dolist (env (cdr var)) (setenv (car env) (cdr env)))
(set (car var) (cdr var)))))
(error (error "Failed to parse profiles.el: %s" (error-message-string e))))))
;; If the requested profile isn't in profiles.el, then see if
;; $EMACSDIR/profiles/$DOOMPROFILE exists. These are implicit
;; profiles, where `emacs --profile foo` will be equivalent to `emacs
;; --init-directory $EMACSDIR/profile/foo', if that directory exists.
(let ((profile-dir
(expand-file-name
profile (or (getenv-internal "DOOMPROFILESDIR")
(expand-file-name "profiles/" user-emacs-directory)))))
(when (file-directory-p profile-dir)
(setq user-emacs-directory profile-dir)
(throw 'found t)))
(user-error "No %S profile found" profile)))
(when init-file-debug
(message "Selected profile: %s" profile))
;; Ensure the selected profile persists through the session
(setenv "DOOMPROFILE" profile))))
;; PERF: Don't use precious startup time checking mtime on elisp bytecode.
;; Ensuring correctness is 'doom sync's job, not the interactive session's.
;; Still, stale byte-code will cause *heavy* losses in startup efficiency.
(setq load-prefer-newer noninteractive)
;;
;;; Bootstrap
(let (init-file)
;; Load the heart of Doom Emacs
(if (load (expand-file-name "lisp/doom" user-emacs-directory) 'noerror 'nomessage)
;; ...and prepare for an interactive session.
(if noninteractive
(require 'doom-cli)
(setq init-file (expand-file-name "doom-start" doom-core-dir)))
;; ...but if that fails, then this is likely not a Doom config.
(setq early-init-file (expand-file-name "early-init" user-emacs-directory))
(load early-init-file 'noerror 'nomessage))
(or
;; PERF: `file-name-handler-alist' is consulted often. Unsetting it offers a
;; notable saving in startup time.
(let (file-name-handler-alist)
;; FEAT: First, we process --init-directory and --profile to detect what
;; `user-emacs-directory' to load from. I avoid using
;; `command-switch-alist' to process --profile and --init-directory because
;; it is processed too late to change `user-emacs-directory' in time.
;; We hijack Emacs' initfile resolver to inject our own entry point. Why do
;; this? Because:
;;
;; - It spares Emacs the effort of looking for/loading useless initfiles, like
;; ~/.emacs and ~/_emacs. And skips ~/.emacs.d/init.el, which won't exist if
;; you're using Doom (fyi: doom hackers or chemacs users could then use
;; $EMACSDIR as their $DOOMDIR, if they wanted).
;; - Later, 'doom sync' will dynamically generate its bootstrap file, which
;; will be important for Doom's profile system later. Until then, we'll use
;; lisp/doom-start.el.
;; - A "fallback" initfile can be trivially specified, in case the
;; bootstrapper is missing (if the user hasn't run 'doom sync' or is a
;; first-timer). This is an opportunity to display a "safe mode" environment
;; that's less intimidating and more helpful than the broken state errors
;; would've left Emacs in, otherwise.
;; - A generated config allows for a file IO optimized startup.
(define-advice startup--load-user-init-file (:filter-args (args) init-doom)
"Initialize Doom Emacs in an interactive session."
(list (lambda ()
(or init-file
(expand-file-name "init.el" user-emacs-directory)))
(when (boundp 'doom-profiles-dir)
(lambda ()
(expand-file-name "safe-mode@static/init.el" doom-profiles-dir)))
(caddr args))))
;; REVIEW: Backported from Emacs 29. Remove when 28 support is dropped.
(let ((initdir (or (cadr (member "--init-directory" command-line-args))
(getenv-internal "EMACSDIR"))))
(when initdir
;; FIX: Discard the switch to prevent "invalid option" errors later.
(push (cons "--init-directory" (lambda (_) (pop argv))) command-switch-alist)
(setq user-emacs-directory (expand-file-name initdir))))
;; Initialize a known profile, if requested.
(let ((profile (or (cadr (member "--profile" command-line-args))
(getenv-internal "DOOMPROFILE"))))
(when profile
;; FIX: Discard the switch to prevent "invalid option" errors later.
(push (cons "--profile" (lambda (_) (pop argv))) command-switch-alist)
;; While processing the requested profile, Doom loosely expects
;; `user-emacs-directory' to be changed. If it doesn't, then you're using
;; profiles.el as a glorified, runtime dir-locals.el (which is fine, if
;; intended).
(catch 'found
(let ((profiles-file (expand-file-name "profiles.el" user-emacs-directory)))
(when (file-exists-p profiles-file)
(with-temp-buffer
(let ((coding-system-for-read 'utf-8-auto))
(insert-file-contents profiles-file))
(condition-case-unless-debug e
(let ((profile-data (cdr (assq (intern profile) (read (current-buffer))))))
(dolist (var profile-data (if profile-data (throw 'found t)))
(if (eq (car var) 'env)
(dolist (env (cdr var)) (setenv (car env) (cdr env)))
(set (car var) (cdr var)))))
(error (error "Failed to parse profiles.el: %s" (error-message-string e))))))
;; If the requested profile isn't in profiles.el, then see if
;; $EMACSDIR/profiles/$DOOMPROFILE exists. These are implicit
;; profiles, where `emacs --profile foo` will be equivalent to `emacs
;; --init-directory $EMACSDIR/profile/foo', if that directory exists.
(let ((profile-dir
(expand-file-name
profile (or (getenv-internal "DOOMPROFILESDIR")
(expand-file-name "profiles/" user-emacs-directory)))))
(when (file-directory-p profile-dir)
(setq user-emacs-directory profile-dir)
(throw 'found t)))
(user-error "No %S profile found" profile)))
(when init-file-debug
(message "Selected profile: %s" profile))
;; Ensure the selected profile persists through the session
(setenv "DOOMPROFILE" profile)))
;; PERF: When `load'ing or `require'ing files, each permutation of
;; `load-suffixes' and `load-file-rep-suffixes' (then `load-suffixes' +
;; `load-file-rep-suffixes') is used to locate the file. Each permutation
;; is a file op, which is normally very fast, but they can add up over the
;; hundreds/thousands of files Emacs needs to load.
;;
;; To reduce that burden -- and since Doom doesn't load any dynamic modules
;; -- I remove `.so' from `load-suffixes' and pass the `must-suffix' arg to
;; `load'. See the docs of `load' for details.
(or (let ((load-suffixes '(".elc" ".el")))
;; Load the heart of Doom Emacs.
(if (load (expand-file-name "lisp/doom" user-emacs-directory)
'noerror 'nomessage nil 'must-suffix)
;; ...and prepare for the rest of the session.
(if noninteractive
(doom-require 'doom-cli)
;; HACK: This advice hijacks Emacs' initfile resolver to replace
;; $EMACSDIR/init.el (and ~/.emacs or ~/_emacs) with a
;; a Doom-provided init file. Later, this file will be
;; generated by 'doom sync' for the active Doom profile;
;; `doom-start' is its stand-in until that's implemented.
;;
;; This effort spares Emacs the overhead of searching for
;; initfiles we don't care about, enables savvier hackers to
;; use $EMACSDIR as their $DOOMDIR, and gives us an opportunity
;; to fall back to a "safe mode", so we can present a more
;; user-friendly failure state.
(define-advice startup--load-user-init-file (:filter-args (args) init-doom)
"Initialize Doom Emacs in an interactive session."
(list (lambda ()
(file-name-concat doom-core-dir "doom-start"))
(lambda ()
(file-name-concat doom-profiles-dir "safe-mode" "init.el"))
(caddr args))))))
;; Failing that, assume we're loading a non-Doom config and prepare.
(ignore
(setq early-init-file (expand-file-name "early-init" user-emacs-directory)
;; I make no assumptions about the config we're about to load, so
;; to limit side-effects, undo any leftover optimizations:
load-prefer-newer t))))
;; Then continue on to the config/profile we want to load.
(load early-init-file 'noerror 'nomessage nil 'must-suffix))
;;; early-init.el ends here