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

@ -40,53 +40,47 @@
;; See core/autoload/packages.el for more functions.
(defvar doom-init-p nil
"Non-nil if doom is done initializing (once `doom-post-init-hook' is done). If
this is nil after Emacs has started something is wrong.")
"Non-nil if `doom-initialize' has run.")
(defvar doom-init-modules-p nil
"Non-nil if `doom-initialize-modules' has run.")
(defvar doom-init-time nil
"The time it took, in seconds, for DOOM Emacs to initialize.")
(defvar doom-modules
(make-hash-table :test #'equal :size 100 :rehash-threshold 1.0)
(defvar doom-modules ()
"A hash table of enabled modules. Set by `doom-initialize-modules'.")
(defvar doom-modules-dirs
(list (expand-file-name "modules/" doom-private-dir) doom-modules-dir)
"A list of module root directories. Order determines priority.")
(defvar doom-psuedo-module-dirs
(list doom-private-dir)
(defvar doom-psuedo-module-dirs (list doom-private-dir)
"Additional paths for modules that are outside of `doom-modules-dirs'.
`doom//reload-autoloads', `doom//byte-compile' and `doom-initialize-packages'
will include the directories in this list.")
`doom//reload-doom-autoloads', `doom//byte-compile' and
`doom-initialize-packages' will include the directories in this list.")
(defvar doom-packages ()
"A list of enabled packages. Each element is a sublist, whose CAR is the
package's name as a symbol, and whose CDR is the plist supplied to its
`package!' declaration. Set by `doom-initialize-packages'.")
(defvar doom-core-packages
'(persistent-soft use-package quelpa async)
(defvar doom-core-packages '(persistent-soft use-package quelpa async)
"A list of packages that must be installed (and will be auto-installed if
missing) and shouldn't be deleted.")
(defvar doom-disabled-packages ()
"A list of packages that should be ignored by `def-package!'.")
(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.")
(defvar doom-site-load-path load-path
"The starting load-path, before it is altered by `doom-initialize'.")
(defvar doom-autoload-file (concat doom-local-dir "autoloads.el")
"Where `doom//reload-autoloads' will generate its autoloads file.")
"Where `doom//reload-doom-autoloads' will generate its core autoloads file.")
(defvar doom-packages-file (concat doom-cache-dir "packages.el")
"Where to cache `load-path', `Info-directory-list', `doom-disabled-packages'
and `auto-mode-alist'.")
(defvar doom-package-autoload-file (concat doom-local-dir "autoloads.pkg.el")
"Where `doom//reload-package-autoloads' will generate its package.el autoloads
file.")
(defvar doom-reload-hook nil
"A list of hooks to run when `doom//reload-load-path' is called.")
@ -150,22 +144,6 @@ and `auto-mode-alist'.")
(file-relative-name load-file-name doom-emacs-dir)
(abbreviate-file-name load-file-name))))
(defun doom|refresh-cache ()
"Refresh `doom-packages-file', which caches `load-path',
`Info-directory-list', `doom-disabled-packages', `auto-mode-alist' and
`package-activated-list'."
(doom-initialize-packages 'internal)
(let ((coding-system-for-write 'emacs-internal))
(with-temp-file doom-packages-file
(insert ";;; -*- lexical-binding:t -*-\n"
";; This file was autogenerated by `doom|refresh-cache', DO NOT EDIT!\n")
(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)))))
(defun doom|display-benchmark (&optional return-p)
"Display a benchmark, showing number of packages and modules, and how quickly
they were loaded at startup.
@ -195,17 +173,59 @@ session, with a different init.el, like so:
'window-setup-hook))
;;
;; Bootstrap helpers
;;
(defun doom-ensure-packages-initialized (&optional force-p)
"Make sure package.el is initialized."
(when (or force-p (not package--initialized))
(require 'package)
(setq package-activated-list nil
package--initialized nil)
(let (byte-compile-warnings)
(condition-case _
(quiet! (package-initialize))
('error (package-refresh-contents)
(setq doom--refreshed-p t)
(package-initialize))))))
(defun doom-ensure-core-packages ()
"Make sure `doom-core-packages' are installed."
(when-let* ((core-packages (cl-remove-if #'package-installed-p doom-core-packages)))
(message "Installing core packages")
(unless doom--refreshed-p
(package-refresh-contents))
(dolist (package core-packages)
(let ((inhibit-message t))
(package-install package))
(if (package-installed-p package)
(message "✓ Installed %s" package)
(error "✕ Couldn't install %s" package)))
(message "Installing core packages...done")))
(defun doom-ensure-core-directories ()
"Make sure all Doom's essential local directories (in and including
`doom-local-dir') exist."
(dolist (dir (list doom-local-dir doom-etc-dir doom-cache-dir doom-packages-dir))
(unless (file-directory-p dir)
(make-directory dir t))))
;;
;; Bootstrap API
;;
(autoload 'doom//reload-doom-autoloads "autoload/modules" nil t)
(autoload 'doom//reload-package-autoloads "autoload/modules" nil t)
(defun doom-initialize (&optional force-p)
"Bootstrap Doom, if it hasn't already (or if FORCE-P is non-nil).
The bootstrap process involves making sure the essential directories exist, core
packages are installed, `doom-autoload-file' is loaded, `doom-packages-file'
cache exists (and is loaded) and, finally, loads your private init.el (which
should contain your `doom!' block).
The bootstrap process involves making sure 1) the essential directories exist,
2) the core packages are installed, 3) `doom-autoload-file' and
`doom-package-autoload-file' exist and have been loaded, and 4) Doom's core
files are loaded.
If the cache exists, much of this function isn't run, which substantially
reduces startup time.
@ -228,71 +248,54 @@ Module load order is determined by your `doom!' block. See `doom-modules-dirs'
for a list of all recognized module trees. Order defines precedence (from most
to least)."
(when (or force-p (not doom-init-p))
(when (and (or force-p noninteractive)
(file-exists-p doom-packages-file))
(message "Deleting packages.el cache")
(delete-file doom-packages-file))
(unless (load doom-packages-file 'noerror 'nomessage 'nosuffix)
;; Ensure core folders exist, otherwise we get errors
(dolist (dir (list doom-local-dir doom-etc-dir doom-cache-dir doom-packages-dir))
(unless (file-directory-p dir)
(make-directory dir t)))
;; Ensure plugins have been initialized
(require 'package)
(setq package-activated-list nil
package--initialized nil)
(let (byte-compile-warnings)
(condition-case _
(package-initialize)
('error (package-refresh-contents)
(setq doom--refreshed-p t)
(package-initialize))))
;; Ensure core packages are installed
(when-let* ((core-packages (cl-remove-if #'package-installed-p doom-core-packages)))
(message "Installing core packages")
(unless doom--refreshed-p
(package-refresh-contents))
(dolist (package core-packages)
(let ((inhibit-message t))
(package-install package))
(if (package-installed-p package)
(message "✓ Installed %s" package)
(error "✕ Couldn't install %s" package)))
(message "Installing core packages...done"))
(unless noninteractive
(add-hook 'doom-pre-init-hook #'doom|refresh-cache)))
;; Load autoloads file
(doom-initialize-autoloads))
;; Set this to prevent infinite recursive calls to `doom-initialize'
(setq doom-init-p t)
;; `doom-autoload-file' tells Emacs where to load all its autoloaded
;; functions from. This includes everything in core/autoload/*.el and all
;; the autoload files in your enabled modules.
(unless (doom-initialize-autoloads doom-autoload-file force-p)
(doom-ensure-core-directories)
(doom-ensure-packages-initialized force-p)
(doom-ensure-core-packages)
;; Regenerate `doom-autoload-file', which tells Doom where to find all its
;; module autoloaded functions.
(unless (or force-p noninteractive)
(doom//reload-doom-autoloads)))
;; Loads `doom-package-autoload-file', which caches `load-path',
;; `auto-mode-alist', `Info-directory-list', `doom-disabled-packages' and
;; `package-activated-list'. A big reduction in startup time.
(unless (doom-initialize-autoloads doom-package-autoload-file force-p)
(unless (or force-p noninteractive)
(doom//reload-package-autoloads))))
;; Initialize Doom core
(unless noninteractive
(require 'core-ui)
(require 'core-editor)
(require 'core-projects)
(require 'core-keybinds))
;; Bootstrap Doom
(unless doom-init-p
(require 'core-keybinds)))
(defun doom-initialize-modules (&optional force-p)
"Loads the init.el in `doom-private-dir' and sets up hooks for a healthy
session of Dooming. Will noop if used more than once, unless FORCE-P is
non-nil."
(when (or force-p (not doom-init-modules-p))
;; Set `doom-init-modules-p' early, so `doom-pre-init-hook' won't infinitely
;; recurse by accident if any of them need `doom-initialize-modules'.
(setq doom-init-modules-p t)
(unless noninteractive
(add-hook! 'doom-reload-hook
#'(doom|refresh-cache doom|display-benchmark))
(add-hook! 'emacs-startup-hook
#'(doom|post-init doom|display-benchmark)))
(run-hooks 'doom-pre-init-hook)
(when doom-private-dir
(load (concat doom-private-dir "init") t t)))
(setq doom-init-p t))
(let ((load-prefer-newer t))
(load (expand-file-name "init" doom-private-dir)
'noerror 'nomessage)))))
(defun doom-initialize-autoloads ()
"Tries to load `doom-autoload-file', otherwise throws an error (unless in a
noninteractive session)."
(unless
(condition-case-unless-debug e
(load (substring doom-autoload-file 0 -3) 'noerror 'nomessage)
(error
(funcall (if noninteractive #'warn #'error)
"Autoload error: %s -> %s"
(car e) (error-message-string e))))
(unless noninteractive
(error "No autoloads file! Run make autoloads"))))
(defun doom-initialize-autoloads (file &optional clear-p)
"Tries to load FILE (an autoloads file). Otherwise tries to regenerate it. If
CLEAR-P is non-nil, regenerate it anyway."
(unless clear-p
(load (file-name-sans-extension file) 'noerror 'nomessage)))
(defun doom-initialize-packages (&optional force-p)
"Ensures that Doom's package management system, package.el and quelpa are
@ -306,8 +309,8 @@ Use this before any of package.el, quelpa or Doom's package management's API to
ensure all the necessary package metadata is initialized and available for
them."
(with-temp-buffer ; prevent buffer-local settings from propagating
;; Prefer uncompiled files to reduce stale code issues
(let ((load-prefer-newer t))
(let ((load-prefer-newer t) ; reduce stale code issues
(doom-modules (doom-module-table)))
;; package.el and quelpa handle themselves if their state changes during
;; the current session, but if you change an packages.el file in a module,
;; there's no non-trivial way to detect that, so we give you a way to
@ -454,6 +457,37 @@ added, if the file exists."
collect (plist-get plist :path))
(cl-remove-if-not #'file-directory-p doom-psuedo-module-dirs)))
(defun doom-module-table (&optional modules)
"Converts MODULES (a malformed plist) into a hash table of modules, fit for
`doom-modules'. If MODULES is omitted, it will fetch your module mplist from the
`doom!' block in your private init.el file."
(let* ((doom-modules (make-hash-table :test #'equal
:size (if modules (length modules) 100)
:rehash-threshold 1.0)))
(when (null modules)
(let ((init-file (expand-file-name "init.el" doom-private-dir)))
(if (not (file-exists-p init-file))
(error "%s doesn't exist" (abbreviate-file-name init-file))
(with-temp-buffer
(insert-file-contents init-file)
(when (re-search-forward "^\\s-*\\((doom! \\)" nil t)
(goto-char (match-beginning 1))
(setq modules (cdr (sexp-at-point))))))
(unless modules
(error "Couldn't gather module list from %s" init-file))))
(if (eq modules t) (setq modules nil))
(let (category)
(dolist (m modules)
(cond ((keywordp m) (setq category m))
((not category) (error "No module category specified for %s" m))
((let ((module (if (listp m) (car m) m))
(flags (if (listp m) (cdr m))))
(if-let* ((path (doom-module-locate-path category module)))
(doom-module-set category module :flags flags :path path)
(when doom-debug-mode
(message "Couldn't find the %s %s module" category module))))))))
doom-modules))
;;
;; Use-package modifications
@ -506,25 +540,40 @@ added, if the file exists."
(defmacro doom! (&rest modules)
"Bootstraps DOOM Emacs and its modules.
MODULES is an malformed plist of modules to load."
(let (init-forms config-forms file-name-handler-alist)
(let (module)
(dolist (m modules)
(cond ((keywordp m) (setq module m))
((not module) (error "No namespace specified in `doom!' for %s" m))
((let ((submodule (if (listp m) (car m) m))
(flags (if (listp m) (cdr m))))
(let ((path (doom-module-locate-path module submodule)))
(if (not path)
(when doom-debug-mode
(message "Couldn't find the %s %s module" module submodule))
(doom-module-set module submodule :flags flags :path path)
(push `(let ((doom--current-module ',(cons module submodule)))
(load! init ,path t))
init-forms)
(push `(let ((doom--current-module ',(cons module submodule)))
(load! config ,path t))
config-forms))))))))
The bootstrap process involves making sure the essential directories exist, core
packages are installed, `doom-autoload-file' is loaded, `doom-packages-file'
cache exists (and is loaded) and, finally, loads your private init.el (which
should contain your `doom!' block).
If the cache exists, much of this function isn't run, which substantially
reduces startup time.
The overall load order of Doom is as follows:
~/.emacs.d/init.el
~/.emacs.d/core/core.el
`doom-pre-init-hook'
~/.doom.d/init.el
Module init.el files
`doom-init-hook'
Module config.el files
~/.doom.d/config.el
`after-init-hook'
`emacs-startup-hook'
`doom-post-init-hook' (at end of `emacs-startup-hook')
Module load order is determined by your `doom!' block. See `doom-modules-dirs'
for a list of all recognized module trees. Order defines precedence (from most
to least)."
(let ((doom-modules (doom-module-table (or modules t)))
init-forms config-forms file-name-handler-alist)
(maphash (lambda (key value)
(let ((path (plist-get value :path)))
(push `(let ((doom--current-module ',key)) (load! init ,path t))
init-forms)
(push `(let ((doom--current-module ',key)) (load! config ,path t))
config-forms)))
doom-modules)
`(let (file-name-handler-alist)
(setq doom-modules ',doom-modules)
,@(nreverse init-forms)
@ -533,8 +582,9 @@ MODULES is an malformed plist of modules to load."
(let ((doom--stage 'config))
,@(nreverse config-forms)
(when doom-private-dir
(load ,(concat doom-private-dir "config")
t (not doom-debug-mode))))))))
(let ((load-prefer-newer t))
(load ,(expand-file-name "config" doom-private-dir)
t (not doom-debug-mode)))))))))
(defmacro def-package! (name &rest plist)
"A thin wrapper around `use-package'."