Redesign Doom bootstrap, caching & autoload generation logic

The autoloads file has been split into doom-autoload-file and
doom-package-autoload-file. The former is for Doom's modules and
standard library; the latter is for compiling all package autoloads like
load-path and auto-mode-alist (among other things).

This reduced my startup speed from ~1s to ~0.5s
This commit is contained in:
Henrik Lissner 2018-05-24 19:00:41 +02:00
parent 3dd291a675
commit 8746c12fae
No known key found for this signature in database
GPG key ID: 5F6C0EA160557395
4 changed files with 413 additions and 259 deletions

View file

@ -1,149 +1,255 @@
;;; core/autoload/modules.el -*- lexical-binding: t; -*-
;;;###autoload
(defun doom//reload ()
"Reload your private Doom config. Experimental!"
(interactive)
(when (file-exists-p doom-packages-file)
(delete-file doom-packages-file))
(cond ((and noninteractive (not (daemonp)))
(doom-initialize)
(doom//reload-autoloads)
(require 'server)
(when (server-running-p)
(message "Reloading active Emacs session...")
(server-eval-at server-name '(doom//reload))))
(autoload 'print! "autoload/message" nil 'macro)
(autoload 'printerr! "autoload/message" nil 'macro)
((let ((load-prefer-newer t)
doom-init-p)
(setq doom-modules (make-hash-table :test #'equal :size 100 :rehash-threshold 1.0))
(doom-initialize t)
(message "%d packages reloaded" (length package-alist))
(run-hooks 'doom-reload-hook)))))
(defun doom--server-eval (body)
(require 'server)
(when (server-running-p)
(server-eval-at server-name body)))
;;;###autoload
(defun doom//reload (&optional force-p)
"Reloads your config. This is experimental!
If called from a noninteractive session, this will try to communicate with a
live server (if one is found) to tell it to run this function.
If called from an interactive session, tries to reload autoloads files (if
necessary), reinistalize doom (via `doom-initialize') and reloads your private
init.el and config.el. Then runs `doom-reload-hook'."
(interactive)
(unless doom--inhibit-reload
(cond ((and noninteractive (not (daemonp)))
(require 'server)
(if (not (server-running-p))
(doom//reload-autoloads force-p)
(print! "Reloading active Emacs session...")
(print!
(bold "%%s")
(if (server-eval-at server-name '(doom//reload))
(green "Done!")
(red "There were issues!")))))
((let ((load-prefer-newer t))
(doom//reload-autoloads force-p)
(doom-initialize t)
(doom-initialize-modules t)
(print! (green "%d packages reloaded" (length package-alist)))
(run-hooks 'doom-reload-hook))))))
;;
;; Autoload file generation
;;
(defvar doom-autoload-excluded-packages '(marshal gh)
"Packages that have silly or destructive autoload files that try to load
everyone in the universe and their dog, causing errors that make babies cry. No
one wants that.")
(defun doom--byte-compile (file)
(let ((short-name (file-name-nondirectory file)))
(condition-case-unless-debug ex
(when (byte-compile-file file)
(load (byte-compile-dest-file file) nil t)
(unless noninteractive
(message "Finished compiling %s" short-name)))
('error
(doom-delete-autoloads-file file)
(error "Error in %s: %s -- %s"
short-name
(car ex) (error-message-string ex))))))
;;;###autoload
(defun doom-delete-autoloads-file (file)
"Delete FILE (an autoloads file), and delete the accompanying *.elc file, if
it exists."
(or (stringp file)
(signal 'wrong-type-argument (list 'stringp file)))
(when (file-exists-p file)
(delete-file file)
(ignore-errors (delete-file (byte-compile-dest-file file)))
(print! "Deleted old %s" (file-name-nondirectory file))))
;;;###autoload
(defun doom//reload-autoloads (&optional file force-p)
"Reloads FILE (an autoload file), if it needs reloading.
FILE should be one of `doom-autoload-file' or `doom-package-autoload-file'. If
it is nil, it will try to reload both. If FORCE-P (universal argument) do it
even if it doesn't need reloading!"
(interactive
(list nil current-prefix-arg))
(or (null file)
(stringp file)
(signal 'wrong-type-argument (list 'stringp file)))
(cond ((equal file doom-autoload-file)
(doom//reload-doom-autoloads force-p))
((equal file doom-package-autoload-file)
(doom//reload-package-autoloads force-p))
((progn
(doom//reload-doom-autoloads force-p)
(doom//reload-package-autoloads force-p)))))
(defvar generated-autoload-load-name)
;;;###autoload
(defun doom//reload-autoloads (&optional force)
"Refreshes the autoloads.el file, specified by `doom-autoload-file'.
(defun doom//reload-doom-autoloads (&optional force-p)
"Refreshes the autoloads.el file, specified by `doom-autoload-file', if
necessary (or if FORCE-P is non-nil).
It scans and reads core/autoload/*.el, modules/*/*/autoload.el and
modules/*/*/autoload/*.el, and generates an autoloads file at the path specified
by `doom-autoload-file'. This file tells Emacs where to find lazy-loaded
functions.
modules/*/*/autoload/*.el, and generates `doom-autoload-file'. This file tells
Emacs where to find lazy-loaded functions.
This should be run whenever init.el or an autoload file is modified. Running
'make autoloads' from the commandline executes this command."
This should be run whenever your `doom!' block, or a module autoload file, is
modified."
(interactive)
;; This function must not use autoloaded functions or external dependencies.
;; It must assume nothing is set up!
(let ((default-directory doom-emacs-dir)
(let ((doom-modules (doom-module-table))
(default-directory doom-emacs-dir)
(targets
(file-expand-wildcards
(expand-file-name "autoload/*.el" doom-core-dir)))
(generate-autoload-section-continuation "")
(generate-autoload-section-header "")
(generate-autoload-section-trailer "")
(doom--stage 'autoloads)
outdated)
(expand-file-name "autoload/*.el" doom-core-dir))))
(dolist (path (doom-module-load-path))
(let ((auto-dir (expand-file-name "autoload" path))
(auto-file (expand-file-name "autoload.el" path)))
(when (file-exists-p auto-file)
(push auto-file targets))
(when (file-directory-p auto-dir)
(dolist (file (doom-files-under auto-dir :match "\\.el$"))
(dolist (file (doom-files-in auto-dir :match "\\.el$" :full t))
(push file targets)))))
(when (file-exists-p doom-autoload-file)
(delete-file doom-autoload-file)
(ignore-errors (delete-file (byte-compile-dest-file doom-autoload-file)))
(message "Deleted old autoloads.el"))
(message "Generating new autoloads.el")
(dolist (file (mapcar #'file-truename (reverse targets)))
(let ((generated-autoload-load-name (file-name-sans-extension file)))
(message
(cond ((not (doom-file-cookie-p file))
"⚠ Ignoring %s")
((update-file-autoloads file nil doom-autoload-file)
"✕ Nothing in %s")
("✓ Scanned %s"))
(if (file-in-directory-p file default-directory)
(file-relative-name file)
(abbreviate-file-name file)))))
(make-directory (file-name-directory doom-autoload-file) t)
(let ((buf (find-file-noselect doom-autoload-file t))
(load-path (append doom-psuedo-module-dirs
doom-modules-dirs
load-path))
case-fold-search)
;; FIXME Make me faster
(unwind-protect
(with-current-buffer buf
(goto-char (point-min))
(insert ";;; -*- lexical-binding:t -*-\n"
";; This file is autogenerated by `doom//reload-autoloads', DO NOT EDIT !!\n\n")
(if (and (not force-p)
(file-exists-p doom-autoload-file)
(not (cl-loop for file in targets
if (file-newer-than-file-p file doom-autoload-file)
return t)))
(ignore (print! (green "Doom core autoloads is up-to-date"))
(doom-initialize-autoloads doom-autoload-file))
(doom-delete-autoloads-file doom-autoload-file)
;; in case the buffer is open somewhere and modified
(when-let* ((buf (find-buffer-visiting doom-autoload-file)))
(with-current-buffer buf
(set-buffer-modified-p nil))
(kill-buffer buf))
(message "Generating new autoloads.el")
(dolist (file (nreverse targets))
(let* ((file (file-truename file))
(generated-autoload-load-name (file-name-sans-extension file))
(noninteractive (not doom-debug-mode)))
(print!
(cond ((not (doom-file-cookie-p file))
"⚠ Ignoring %s")
((update-file-autoloads file nil doom-autoload-file)
(yellow "✕ Nothing in %%s"))
((green "✓ Scanned %%s")))
(if (file-in-directory-p file default-directory)
(file-relative-name file)
(abbreviate-file-name file)))))
(make-directory (file-name-directory doom-autoload-file) t)
(let ((buf (find-file-noselect doom-autoload-file t))
case-fold-search)
(unwind-protect
(with-current-buffer buf
(goto-char (point-min))
(insert ";;; -*- lexical-binding:t -*-\n"
";; This file is autogenerated by `doom//reload-doom-autoloads', DO NOT EDIT !!\n\n")
(save-excursion
;; Replace autoload paths (only for module autoloads) with
;; absolute paths for faster resolution during load and
;; simpler `load-path'
(let ((load-path (append doom-psuedo-module-dirs
doom-modules-dirs
load-path))
cache)
(save-excursion
(while (re-search-forward "^\\s-*(autoload\\s-+'[^ ]+\\s-+\"\\([^\"]*\\)\"" nil t)
(let ((path (match-string 1)))
(replace-match
(or (cdr (assoc path cache))
(when-let* ((libpath (locate-library path))
(libpath (file-name-sans-extension libpath)))
(push (cons path (abbreviate-file-name libpath)) cache)
libpath)
path)
t t nil 1)))
(print! (green "✓ Autoload paths expanded")))))
;; Remove byte-compile inhibiting file variables so we can
;; byte-compile the file.
(when (re-search-forward "^;; no-byte-compile: t\n" nil t)
(replace-match "" t t))
;; Byte compile it to give the file a chance to reveal errors.
(save-buffer)
(doom--byte-compile doom-autoload-file)
(when (and noninteractive (not (daemonp)))
(doom--server-eval `(load-file ,doom-autoload-file)))
t)
(kill-buffer buf))))))
;; Replace autoload paths (only for module autoloads) with
;; absolute paths for faster resolution during load and simpler
;; `load-path'
(save-excursion
(let (cache)
(while (re-search-forward "^\\s-*(autoload\\s-+'[^ ]+\\s-+\"\\([^\"]*\\)\"" nil t)
(let ((path (match-string 1)))
(replace-match
(or (cdr (assoc path cache))
(when-let* ((libpath (locate-library path))
(libpath (file-name-sans-extension libpath)))
(push (cons path (abbreviate-file-name libpath)) cache)
libpath)
(progn
(warn "Couldn't find absolute path for: %s" path)
path))
t t nil 1))))
(message "✓ Autoload paths expanded"))
;;;###autoload
(defun doom//reload-package-autoloads (&optional force-p)
"Compiles `doom-package-autoload-file' from the autoloads files of all
installed packages. It also caches `load-path', `Info-directory-list',
`doom-disabled-packages', `package-activated-list' and `auto-mode-alist'.
;; insert package autoloads
(save-excursion
(dolist (spec package-alist)
(let ((pkg (car spec)))
(unless (memq pkg doom-autoload-excluded-packages)
(let ((file
(abbreviate-file-name
(concat (package--autoloads-file-name (cadr spec)) ".el"))))
(insert "(let ((load-file-name " (prin1-to-string file) "))\n")
(insert-file-contents file)
(while (re-search-forward "^\\(?:;;\\(.*\n\\)\\|\n\\)" nil t)
(unless (nth 8 (syntax-ppss))
(replace-match "" t t)))
(unless (bolp) (insert "\n"))
(insert ")\n")))))
(message "✓ Package autoloads included"))
Will do nothing if none of your installed packages have been modified. If
FORCE-P (universal argument) is non-nil, regenerate it anyway.
;; Remove `load-path' and `auto-mode-alist' modifications (most
;; of them, at least); they are cached elsewhere, so these are
;; unnecessary overhead.
(while (re-search-forward (concat "^\\s-*(\\(add-to-list\\s-+'\\(?:load-path\\|auto-mode-alist\\)\\)")
nil t)
(beginning-of-line)
(skip-chars-forward " \t")
(kill-sexp))
(message "✓ load-path/auto-mode-alist entries removed")
This should be run whenever your `doom!' block or update your packages."
(interactive)
(if (and (not force-p)
(file-exists-p doom-package-autoload-file)
(not (cl-loop initially do (doom-ensure-packages-initialized t)
for (_pkg desc) in package-alist
for autoload-file = (concat (package--autoloads-file-name desc) ".el")
if (file-newer-than-file-p autoload-file doom-package-autoload-file)
return t)))
(ignore (print! (green "Doom package autoloads is up-to-date"))
(doom-initialize-autoloads doom-package-autoload-file))
(doom-delete-autoloads-file doom-package-autoload-file)
(with-temp-file doom-package-autoload-file
(insert ";;; -*- lexical-binding:t -*-\n"
";; This file is autogenerated by `doom//reload-package-autoloads', DO NOT EDIT !!\n\n")
;; insert package autoloads
(save-excursion
(dolist (spec package-alist)
(cl-destructuring-bind (pkg desc) spec
(unless (memq pkg doom-autoload-excluded-packages)
(let ((file
(abbreviate-file-name
(concat (package--autoloads-file-name desc) ".el"))))
(when (file-exists-p file)
(insert "(let ((load-file-name " (prin1-to-string file) "))\n")
(insert-file-contents file)
(while (re-search-forward "^\\(?:;;\\(.*\n\\)\\|\n\\)" nil t)
(unless (nth 8 (syntax-ppss))
(replace-match "" t t)))
(unless (bolp) (insert "\n"))
(insert ")\n"))))))
(print! (green "✓ Package autoloads included"))
;; Cache the important and expensive-to-initialize state here.
(doom-initialize-packages 'internal)
(prin1 `(setq load-path ',load-path
auto-mode-alist ',auto-mode-alist
Info-directory-list ',Info-directory-list
doom-disabled-packages ',doom-disabled-packages
package-activated-list ',package-activated-list)
(current-buffer))
(print! (green "✓ Cached package state")))
;; Remove `load-path' and `auto-mode-alist' modifications (most of them,
;; at least); they are cached later, so all those membership checks are
;; unnecessary overhead.
(while (re-search-forward "^\\s-*\\((\\(?:add-to-list\\|when (boundp \\)\\s-+'\\(?:load-path\\|auto-mode-alist\\)\\)" nil t)
(goto-char (match-beginning 1))
(kill-sexp))
(print! (green "✓ Removed load-path/auto-mode-alist entries")))
(doom--byte-compile doom-package-autoload-file)
(when (and noninteractive (not (daemonp)))
(doom--server-eval `(load-file ,doom-package-autoload-file)))
t))
;; Remove byte-compile inhibiting file variables so we can
;; byte-compile the file.
(when (re-search-forward "^;; no-byte-compile: t\n$" nil t)
(replace-match "" t t))
(save-buffer)
;; Byte compile it to give the file a chance to reveal errors.
(condition-case-unless-debug ex
(quiet! (byte-compile-file doom-autoload-file 'load))
('error
(delete-file doom-autoload-file)
(message "Deleting autoloads file!")
(error "Error in autoloads.el: %s -- %s"
(car ex) (error-message-string ex))))
(message "Done!"))
(kill-buffer buf)))))
;;
;; Byte compilation
;;
;;;###autoload
(defun doom//byte-compile (&optional modules recompile-p)