aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-20 23:26:44 -0400
committerCraig Jennings <c@cjennings.net>2026-06-20 23:26:44 -0400
commit3fd28987f7f1e4da226d650166cc64e71e30e645 (patch)
tree5c686e8ecd2fd74f343719d9d2957ec16609db7d
parente71e0d71646c06a348c5febeee3fe1840a6452a3 (diff)
downloaddotemacs-3fd28987f7f1e4da226d650166cc64e71e30e645.tar.gz
dotemacs-3fd28987f7f1e4da226d650166cc64e71e30e645.zip
perf: re-enable native-comp JIT and hand GC to gcmh
early-init.el disabled JIT native compilation with (setq native-comp-deferred-compilation nil), the obsolete alias of native-comp-jit-compilation. Despite the comment, setting it nil turns JIT off entirely rather than making it synchronous. Most modules then ran interpreted for the daemon's lifetime, and the native-comp-speed/jobs settings in system-defaults.el were dead. The "Selecting deleted buffer" async race that prompted the disable was an Emacs 28/29 issue. This is 30.2. I re-enabled it with native-comp-jit-compilation t and silent async warnings. GC was pinned at the stock 800KB: early-init restored it post-startup and the minibuffer setup/exit hooks bounced back to it. That's Emacs's bare-editor default, far too low for 184 packages, so GC pauses fired often during completion, agenda, and LSP/AI work. I replaced both hand-rolled mechanisms with gcmh, which keeps the threshold at 1GB during activity and collects on idle. Verified a clean full launch in a throwaway daemon (JIT on, gcmh active, no backtrace) and gcmh's threshold cycle in batch.
-rw-r--r--early-init.el38
-rw-r--r--init.el1
-rw-r--r--modules/gcmh-config.el30
-rw-r--r--modules/system-defaults.el19
-rw-r--r--tests/test-system-defaults-functions.el14
-rw-r--r--tests/test-system-defaults.el13
6 files changed, 62 insertions, 53 deletions
diff --git a/early-init.el b/early-init.el
index d45faef79..7ae814734 100644
--- a/early-init.el
+++ b/early-init.el
@@ -60,14 +60,23 @@
(lambda ()
(setq debug-on-error nil)))
-;; ------------------------------ Bug Workarounds ------------------------------
-
-;; Disable async native compilation to prevent "Selecting deleted buffer" errors
-;; This is a known issue in Emacs 30.x where async compilation buffers get
-;; deleted before the compilation process completes. Synchronous compilation
-;; is slower initially but avoids these race conditions.
-(setq native-comp-deferred-compilation nil) ;; Disable async/deferred compilation
-(setq native-comp-async-report-warnings-errors nil) ;; Silence async warnings
+;; ------------------------------ Native Compilation ---------------------------
+
+;; Enable JIT native compilation. Packages are natively compiled on first load
+;; (asynchronously, in the background) and cached as .eln for later sessions.
+;; This was previously disabled via `(setq native-comp-deferred-compilation
+;; nil)' -- the obsolete alias of `native-comp-jit-compilation'. Despite the old
+;; comment, setting it nil turns JIT OFF entirely (not "synchronous"), so most
+;; modules ran interpreted for the daemon's lifetime and the
+;; `native-comp-speed'/jobs settings in system-defaults.el were dead. The old
+;; "Selecting deleted buffer" async race that prompted the disable was an Emacs
+;; 28/29 issue; this is 30.2.
+(setq native-comp-jit-compilation t)
+
+;; Log async-compile warnings to the *Async-native-compile-log* buffer rather
+;; than popping a window. (system-defaults.el also routes `comp' display-warnings
+;; to a file via `cj/log-comp-warning'.)
+(setq native-comp-async-report-warnings-errors 'silent)
;; ------------------------------- Load Freshness ------------------------------
;; Prefer newer .el source over stale .elc byte-compiled files. Without this,
@@ -99,11 +108,13 @@ local repos."
:group 'cj)
;; ---------------------------- Startup Performance ----------------------------
-;; increases garbage collection threshold and turns off file-name-handler
-;; during startup and restores the settings once emacs has loaded.
+;; Bump the GC threshold and turn off the file-name-handler during startup for
+;; speed. The file-name-handler is restored once Emacs has loaded. The GC
+;; threshold is deliberately NOT restored here -- `gcmh' (configured in
+;; system-defaults.el) owns `gc-cons-threshold' for the rest of the session,
+;; keeping it high during activity and collecting on idle. Restoring the stock
+;; 800KB here would fight gcmh and bring back frequent GC pauses.
-(defvar cj/orig-gc-cons-threshold gc-cons-threshold
- "Temporary variable to allow restoration of value post-startup.")
(setq gc-cons-threshold most-positive-fixnum)
(defvar cj/orig-file-name-handler-alist file-name-handler-alist
@@ -112,8 +123,7 @@ local repos."
(add-hook 'emacs-startup-hook
(lambda ()
- (setq gc-cons-threshold cj/orig-gc-cons-threshold
- file-name-handler-alist cj/orig-file-name-handler-alist)))
+ (setq file-name-handler-alist cj/orig-file-name-handler-alist)))
;; ------------------------------ Site Start Files -----------------------------
;; don't load site-start or default.el files
diff --git a/init.el b/init.el
index 227b8396c..cf6b75bd4 100644
--- a/init.el
+++ b/init.el
@@ -26,6 +26,7 @@
(require 'host-environment) ;; convenience functions re: host environment
(require 'keyboard-compat) ;; terminal/GUI keyboard compatibility
(require 'system-defaults) ;; native comp; log; unicode, backup, exec path
+(require 'gcmh-config) ;; garbage collection strategy (gcmh)
(require 'keybindings) ;; system-wide keybindings and keybinding discovery
;; -------------------------- Utilities And Libraries --------------------------
diff --git a/modules/gcmh-config.el b/modules/gcmh-config.el
new file mode 100644
index 000000000..beceb1a01
--- /dev/null
+++ b/modules/gcmh-config.el
@@ -0,0 +1,30 @@
+;;; gcmh-config.el --- Garbage collection strategy via gcmh -*- lexical-binding: t -*-
+
+;;; Commentary:
+;; gcmh (the Garbage Collector Magic Hack) owns `gc-cons-threshold' for the
+;; session. It keeps the threshold very high while you are active so GC never
+;; pauses mid-edit, then drops it and collects on idle, when a pause is
+;; invisible. This replaces the old hand-rolled scheme -- a stock-800KB restore
+;; in early-init.el plus a minibuffer setup/exit bump -- which pinned GC at
+;; 800000 (Emacs's bare-editor default), far too low for a config this size and
+;; the cause of frequent GC pauses during completion, agenda builds, and LSP/AI
+;; activity.
+;;
+;; Kept in its own module, not system-defaults.el: that module is pre-loaded by
+;; the comp-errors test harness, which has no package system, so an `:ensure'
+;; package there errors at load time. early-init.el bumps the threshold to
+;; `most-positive-fixnum' for startup and deliberately does not restore it;
+;; `gcmh-mode' takes ownership from here on.
+
+;;; Code:
+
+(use-package gcmh
+ :ensure t
+ :demand t
+ :config
+ (setq gcmh-idle-delay 'auto ; scale the idle GC delay to GC cost
+ gcmh-high-cons-threshold (* 1 1024 1024 1024)) ; 1 GB during activity
+ (gcmh-mode 1))
+
+(provide 'gcmh-config)
+;;; gcmh-config.el ends here
diff --git a/modules/system-defaults.el b/modules/system-defaults.el
index 0062a82cf..6d9c811a6 100644
--- a/modules/system-defaults.el
+++ b/modules/system-defaults.el
@@ -212,18 +212,13 @@ appears only once per session."
(setq custom-safe-themes t) ;; treat all themes as safe (stop asking)
(setq server-client-instructions nil) ;; I already know what to do when done with the frame
-;; ------------------ Reduce Garbage Collections In Minibuffer -----------------
-
-(defun cj/minibuffer-setup-hook ()
- "Hook to prevent garbage collection while user's in minibuffer."
- (setq gc-cons-threshold most-positive-fixnum))
-
-(defun cj/minibuffer-exit-hook ()
- "Hook to trigger garbage collection when exiting minibuffer."
- (setq gc-cons-threshold 800000))
-
-(add-hook 'minibuffer-setup-hook #'cj/minibuffer-setup-hook)
-(add-hook 'minibuffer-exit-hook #'cj/minibuffer-exit-hook)
+;; ----------------------------- Garbage Collection ----------------------------
+;; GC is managed by gcmh in modules/gcmh-config.el: it keeps gc-cons-threshold
+;; high during activity and collects on idle, replacing the old stock-800KB
+;; scheme (an early-init restore plus a minibuffer setup/exit bump). gcmh lives
+;; in its own module rather than here because system-defaults.el is pre-loaded
+;; by the comp-errors test harness, which has no package system -- an `:ensure'
+;; package loaded here would error at load time and break those tests.
;; ----------------------------- Bookmark Settings -----------------------------
diff --git a/tests/test-system-defaults-functions.el b/tests/test-system-defaults-functions.el
index a5210be01..2562ff6aa 100644
--- a/tests/test-system-defaults-functions.el
+++ b/tests/test-system-defaults-functions.el
@@ -79,20 +79,6 @@
(should (eq (cj/disabled) nil))
(should (commandp #'cj/disabled)))
-;;; cj/minibuffer-setup-hook / cj/minibuffer-exit-hook
-
-(ert-deftest test-system-defaults-minibuffer-setup-inflates-gc-threshold ()
- "Normal: entering the minibuffer raises `gc-cons-threshold' to most-positive-fixnum."
- (let ((gc-cons-threshold 800000))
- (cj/minibuffer-setup-hook)
- (should (= gc-cons-threshold most-positive-fixnum))))
-
-(ert-deftest test-system-defaults-minibuffer-exit-restores-gc-threshold ()
- "Normal: leaving the minibuffer restores `gc-cons-threshold' to 800000."
- (let ((gc-cons-threshold most-positive-fixnum))
- (cj/minibuffer-exit-hook)
- (should (= gc-cons-threshold 800000))))
-
;;; unpropertize-kill-ring
(ert-deftest test-system-defaults-unpropertize-kill-ring-strips-properties ()
diff --git a/tests/test-system-defaults.el b/tests/test-system-defaults.el
index 928124f56..f653e1fbb 100644
--- a/tests/test-system-defaults.el
+++ b/tests/test-system-defaults.el
@@ -63,19 +63,6 @@ test clears it first to capture the path derived from the sandbox."
(expand-file-name dir)))
(should (string-suffix-p "backups" (directory-file-name dir)))))))
-;;; minibuffer GC hooks
-
-(ert-deftest test-system-defaults-minibuffer-gc-hooks-registered ()
- "Normal: the minibuffer GC raise/restore hooks are installed.
-Their bodies are tested in test-system-defaults-functions.el; this asserts
-they are actually wired onto the minibuffer hooks."
- (test-system-defaults--with-load-environment
- (let ((minibuffer-setup-hook nil)
- (minibuffer-exit-hook nil))
- (test-system-defaults--load)
- (should (memq 'cj/minibuffer-setup-hook minibuffer-setup-hook))
- (should (memq 'cj/minibuffer-exit-hook minibuffer-exit-hook)))))
-
;;; Customize-save warning
(ert-deftest test-system-defaults-customize-save-warns-once ()