fix: freezing+side-effects on M-x or C-h {f,v}

To understand this issue, you have to understand these two things:

1. Doom builds an init file which combines all its autoloads (for
   packages and modules), and Doom's bootstrapper (which loads modules,
   $DOOMDIR, etc). This init file is byte-compiled.

2. When Emacs byte-compiles elisp, docstrings are lazy-loaded (by
   embedding them in the elc as commented text to be retrieved later).
   This is generally done to save on memory.

Now the issue: when these lazy-loaded docstrings are retrieved, Emacs
may evaluate the whole file to find it, including Doom's bootstrap
process, reloading all its files, the user's config files, and running
all its startup hooks. Not only is this terribly expensive, reloading
these files may have disastrous effects.

One such effect is compounded by Marginalia, which invokes this
docstring fetch process (by calling the `documentation` function in
`marginalia--function-doc`) for *each* symbol in the `M-x` or `C-h
{v,f}` completion lists, which means Doom re-bootstraps multiple times
and rapidly, causing Emacs to totally lock up.

The solution is to simply gate the expensive part of the initfile so it
doesn't run more than once, at startup, and when `doom/reload` is
called. The rest of the file loads instantly.

Still, this is a bit flimsy. I'll think of a more elegant solution
later.
This commit is contained in:
Henrik Lissner 2022-09-20 01:09:37 +02:00
parent 5c9672a28a
commit 2c14eff7f1
No known key found for this signature in database
GPG key ID: B60957CA074D39A3
2 changed files with 62 additions and 45 deletions

View file

@ -369,52 +369,64 @@ Defaults to the profile at `doom-profile-default'."
(let ((v (version-to-list doom-version)) (let ((v (version-to-list doom-version))
(ref (doom-call-process "git" "-C" (doom-path doom-emacs-dir) "rev-parse" "HEAD")) (ref (doom-call-process "git" "-C" (doom-path doom-emacs-dir) "rev-parse" "HEAD"))
(branch (doom-call-process "git" "-C" (doom-path doom-emacs-dir) "branch" "--show-current"))) (branch (doom-call-process "git" "-C" (doom-path doom-emacs-dir) "branch" "--show-current")))
`(,@(cl-loop for var in doom-autoloads-cached-vars ;; FIX: The `doom-init-time' guard protects us from a nefarious edge case in
if (boundp var) ;; which Emacs' interpreter, while lazy-loading docstrings in
collect `(set-default ',var ',(symbol-value var))) ;; byte-compiled elisp, ends up re-evaluating the whole file. This can
(setplist 'doom-version ;; happen rapidly, multiple times, if something loads these docstrings (by
'(major ,(nth 0 v) ;; calling the `documentation' function) rapidly, which is the case for
minor ,(nth 1 v) ;; `marginalia' and each symbol in the M-x and describe-* command
build ,(nth 2 v) ;; completion lists. By guarding the expensive part of this file, this
tag ,(cadr (split-string doom-version "-" t)) ;; process becomes instant.
ref ,(if (zerop (car ref)) (cdr ref)) `((unless doom-init-time
branch ,(if (zerop (car branch)) (cdr branch))))))) ,@(cl-loop for var in doom-autoloads-cached-vars
if (boundp var)
collect `(set-default ',var ',(symbol-value var)))
(setplist 'doom-version
'(major ,(nth 0 v)
minor ,(nth 1 v)
build ,(nth 2 v)
tag ,(cadr (split-string doom-version "-" t))
ref ,(if (zerop (car ref)) (cdr ref))
branch ,(if (zerop (car branch)) (cdr branch))))))))
(defun doom-profile--generate-load-modules () (defun doom-profile--generate-load-modules ()
(let ((module-list (cddr (doom-module-list)))) (let ((module-list (doom-module-list)))
`((set 'doom-disabled-packages ',doom-disabled-packages) ;; FIX: Same as above (see `doom-profile--generate-init-vars').
(set 'doom-modules ',doom-modules) `((unless doom-init-time
;; Cache module state and flags in symbol plists for quick lookup by (set 'doom-disabled-packages ',doom-disabled-packages)
;; `modulep!' later. (set 'doom-modules ',doom-modules)
,@(cl-loop for (category . modules) in (seq-group-by #'car (doom-module-list)) (defvar doom-modules-list ',(cddr module-list))
collect `(setplist ',category ;; Cache module state and flags in symbol plists for quick lookup by
(quote ,(cl-loop for (_ . module) in modules ;; `modulep!' later.
nconc `(,module ,(get category module)))))) ,@(cl-loop for (category . modules) in (seq-group-by #'car (doom-module-list))
(doom-run-hooks 'doom-before-modules-init-hook) collect `(setplist ',category
;; TODO: Until these files are byte-compiler-ready, I must use `load' (quote ,(cl-loop for (_ . module) in modules
;; instead of `require', as to not invite the byte-compiler to load them nconc `(,module ,(get category module))))))
;; while this init file is compiled. (doom-run-hooks 'doom-before-modules-init-hook)
(doom-load ,(doom-path doom-core-dir "doom-keybinds")) ;; TODO: Until these files are byte-compiler-ready, I must use `load'
(doom-load ,(doom-path doom-core-dir "doom-ui")) ;; instead of `require', as to not invite the byte-compiler to load them
(doom-load ,(doom-path doom-core-dir "doom-projects")) ;; while this init file is compiled.
(doom-load ,(doom-path doom-core-dir "doom-editor")) (doom-load ,(doom-path doom-core-dir "doom-keybinds"))
,@(cl-loop for (cat . mod) in module-list (doom-load ,(doom-path doom-core-dir "doom-ui"))
if (doom-module-locate-path cat mod (concat doom-module-init-file ".el")) (doom-load ,(doom-path doom-core-dir "doom-projects"))
collect `(let ((doom--current-module '(,cat . ,mod)) (doom-load ,(doom-path doom-core-dir "doom-editor"))
(doom--current-flags ',(doom-module-get cat mod :flags))) ,@(cl-loop for (cat . mod) in module-list
(doom-load ,it))) if (doom-module-locate-path cat mod (concat doom-module-init-file ".el"))
(doom-run-hooks 'doom-after-modules-init-hook) collect `(let ((doom--current-module '(,cat . ,mod))
(doom-run-hooks 'doom-before-modules-config-hook) (doom--current-flags ',(doom-module-get cat mod :flags)))
,@(cl-loop for (cat . mod) in module-list (doom-load ,it)))
if (doom-module-locate-path cat mod (concat doom-module-config-file ".el")) (doom-run-hooks 'doom-after-modules-init-hook)
collect `(let ((doom--current-module '(,cat . ,mod)) (doom-run-hooks 'doom-before-modules-config-hook)
(doom--current-flags ',(doom-module-get cat mod :flags))) ,@(cl-loop for (cat . mod) in module-list
(doom-load ,it))) if (doom-module-locate-path cat mod (concat doom-module-config-file ".el"))
(doom-run-hooks 'doom-after-modules-config-hook) collect `(let ((doom--current-module '(,cat . ,mod))
(let ((old-custom-file custom-file)) (doom--current-flags ',(doom-module-get cat mod :flags)))
(doom-load ,(doom-path doom-user-dir doom-module-config-file) 'noerror) (doom-load ,it)))
(when (eq custom-file old-custom-file) (doom-run-hooks 'doom-after-modules-config-hook)
(doom-load custom-file 'noerror)))))) (let ((old-custom-file custom-file))
(doom-load ,(doom-path doom-user-dir doom-module-config-file) 'noerror)
(when (eq custom-file old-custom-file)
(doom-load custom-file 'noerror)))))))
(defun doom-profile--generate-doom-autoloads () (defun doom-profile--generate-doom-autoloads ()
(doom-autoloads--scan (doom-autoloads--scan

View file

@ -358,7 +358,12 @@ If RETURN-P, return the message as a string instead of displaying it."
;; Compiling them in one place is a big reduction in startup ;; Compiling them in one place is a big reduction in startup
;; time, and by keeping a history of them, you get a snapshot ;; time, and by keeping a history of them, you get a snapshot
;; of your config in time. ;; of your config in time.
(file-name-concat doom-profile-dir (format "init.%d.elc" emacs-major-version)))) (file-name-concat
doom-profile-dir (format "init.%d.elc" emacs-major-version)))
;; If the config is being reloaded, let's pretend it hasn't be
;; initialized by unsetting this (see note in
;; `doom-profile--generate-load-modules' for details).
doom-init-time)
;; If `user-init-file' is t, then `load' will store the name of ;; If `user-init-file' is t, then `load' will store the name of
;; the file that it loads into `user-init-file'. ;; the file that it loads into `user-init-file'.
(setq user-init-file t) (setq user-init-file t)