From b6f406cdb69bf11921b85d1c799f596919773f1c Mon Sep 17 00:00:00 2001 From: Jorge Javier Araya Navarro Date: Mon, 11 May 2026 08:37:25 -0600 Subject: [PATCH 1/6] feat(windows): add backpack.cmd, benchmarks, and perf gear Windows batch CLI mirrors Unix backpack (ensure, gc, bench); avoid env names that shadow Emacs emacs_dir. Add A/B benchmark harness under test/benchmark; hook bin/backpack and AGENTS. :os windows gear tunes pipes, LSP-sized buffers, NTFS stat, display, and resolves POSIX bash via backpack--windows-posix-bash-executable (skip Store shim). Emacs 29.x gets windows-legacy leaf. --- AGENTS.md | 146 +++++++++- README.md | 18 ++ bench.el | 49 ++++ bin/backpack | 16 +- bin/backpack.cmd | 82 ++++++ lisp/backpack-inventory.el | 3 +- lisp/backpack-platform.el | 23 ++ lisp/backpack.el | 1 + lisp/gears/os/windows.el | 120 ++++++++ test/benchmark/backpack-bench-defs.el | 354 +++++++++++++++++++++++ test/benchmark/backpack-bench-harness.el | 242 ++++++++++++++++ test/benchmark/run.el | 105 +++++++ 12 files changed, 1152 insertions(+), 7 deletions(-) create mode 100644 bench.el create mode 100644 bin/backpack.cmd create mode 100644 lisp/gears/os/windows.el create mode 100644 test/benchmark/backpack-bench-defs.el create mode 100644 test/benchmark/backpack-bench-harness.el create mode 100644 test/benchmark/run.el diff --git a/AGENTS.md b/AGENTS.md index 6d5786f..5ec8ec5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -80,6 +80,7 @@ emacs-backpack/ ├── early-init.el # Emacs entry point; loads backpack.el, calls backpack-start ├── ensure.el # Batch-mode package sync (backpack ensure) ├── gc.el # Batch-mode orphan package cleanup (backpack gc) +├── bench.el # Batch-mode benchmark entry (backpack bench) │ ├── lisp/ # Core Backpack Emacs Lisp library │ ├── backpack.el # Main module: bootstrap, startup optimisations, orchestration @@ -120,6 +121,8 @@ emacs-backpack/ │ │ └── vterm.el │ ├── ai/ │ │ └── anvil.el +│ └── os/ +│ └── windows.el │ └── editing/ │ ├── c.el │ ├── cmake.el @@ -149,13 +152,20 @@ emacs-backpack/ │ └── leaf-keywords.el/ # Extended leaf keywords │ ├── bin/ -│ └── backpack # Shell CLI (backpack ensure, backpack gc) +│ ├── backpack # Shell CLI for Unix (ensure, gc, bench) +│ └── backpack.cmd # Batch CLI for Windows (ensure, gc, bench) │ ├── test/ │ ├── all-tests.el # Test runner │ ├── startup-time.el # Startup time benchmark -│ └── pouch/ -│ └── backpack-pouch.el # Unit tests for gear!/gearp! and backpack--extract-gear-form +│ ├── pouch/ +│ │ └── backpack-pouch.el # Unit tests for gear!/gearp! and backpack--extract-gear-form +│ ├── treesit/ +│ │ └── backpack-treesit.el # Unit tests for tree-sitter grammar handling +│ └── benchmark/ # A/B performance benchmark suite (backpack bench) +│ ├── backpack-bench-harness.el # Timing, stats, table & history output +│ ├── backpack-bench-defs.el # Windows-focused A/B benchmark pairs +│ └── run.el # Batch entry point for `backpack bench' │ ├── etc/scripts/ │ ├── prepare-and-run.sh # Test helper: copy config to tmpdir, run tests @@ -236,7 +246,8 @@ activation are guaranteed to run. ### Batch sync mode (`backpack ensure`) ``` -bin/backpack ensure +bin/backpack ensure # Unix +bin\backpack.cmd ensure # Windows → emacs --batch -l ensure.el ├─ Sets backpack-mode to 'sync ├─ Loads backpack.el, user init.el @@ -250,13 +261,27 @@ bin/backpack ensure ### Garbage collection (`backpack gc`) ``` -bin/backpack gc [--dry-run] +bin/backpack gc [--dry-run] # Unix +bin\backpack.cmd gc [--dry-run] # Windows → emacs --batch -l gc.el ├─ Collects declared packages from gear files ├─ Compares against installed packages └─ Deletes orphaned packages (or reports in dry-run) ``` +### Benchmarks (`backpack bench`) + +``` +bin/backpack bench # Unix +bin\backpack.cmd bench # Windows + → emacs --batch -l bench.el + ├─ Captures the real repo root before backpack-defaults rebinds user-emacs-directory + ├─ Loads lisp/backpack.el (elpaca warnings suppressed -- bench runs without packages) + ├─ Loads test/benchmark/run.el which registers specs and runs the suite + ├─ Prints a human-readable A/B table to stdout + └─ Appends a timestamped .eld history file under .cache/etc/benchmarks/ +``` + ## Gear File Conventions ### Structure of a gear file @@ -429,6 +454,28 @@ restores it. The side window routing uses `backpack--display-eldoc-side-window`, registered in `display-buffer-alist` for the `*eldoc*` buffer. +#### windows (os) + +The windows gear applies performance optimizations for GNU Emacs running natively +on Windows. It is opt-in — users add `(gear! :os windows)` to their configuration. + +Settings applied (grouped by category): + +- **Process/subprocess**: `w32-pipe-read-delay` → 0, `w32-pipe-buffer-size` → 64KB, + `w32-quote-process-args` → t, `process-connection-type` → nil (pipes, not ptys), + `read-process-output-max` → 64KB +- **File I/O**: `w32-get-true-file-attributes` → nil, `w32-get-true-file-link-count` → nil +- **Display**: `inhibit-compacting-font-caches` → t, `fast-but-imprecise-scrolling` → t, + `cursor-in-non-selected-windows` → nil, `highlight-nonselected-windows` → nil, + `auto-mode-case-fold` → nil +- **Bidi**: suppressed by default (`left-to-right`), opt out with the `bidi` flag +- **Native comp**: `native-comp-defer-time` → 1.0 +- **Network**: `url-http-attempt-keepalives` → t +- **Shell**: auto-detects bash (Git for Windows / MSYS2) and sets `shell-file-name` + +The `bidi` flag is **opt-in** (bidi is suppressed by default on Windows for performance). +Users who need RTL text support add `(gear! :os (windows bidi))`. + #### anvil (ai) The anvil gear exposes Emacs as an MCP (Model Context Protocol) server so AI @@ -556,6 +603,95 @@ The `devenv.nix` environment provides Emacs versions 29.1, 29.2, 29.3, 29.4, 30.1, and rolling. CI runs `devenv test` which executes `etc/scripts/for-each-emacs.sh` to test against all versions. +## Benchmarks + +The benchmark suite is separate from the ERT unit tests. It lives under +`test/benchmark/` and is invoked via the `bench` CLI subcommand. Unlike +`devenv test`, the benchmark suite is **not** run automatically in CI; it +is a developer tool for validating that performance tweaks (in particular +those in the `:os windows` gear) actually earn their line of code. + +### Running + +```sh +bin/backpack bench # Unix +bin\backpack.cmd bench # Windows +``` + +Useful environment variables (all optional): + +| Variable | Effect | +|------------------------------|-----------------------------------------------------------| +| `BACKPACK_BENCH_FILTER` | Substring match; only run benchmarks whose name contains it | +| `BACKPACK_BENCH_ITERATIONS` | Override the iteration count of every registered bench | +| `BACKPACK_BENCH_NO_SAVE` | Non-empty string skips writing the `.eld` history file | + +A full run takes a few minutes on Windows (the subprocess benches dominate). +For iterative work, use `BACKPACK_BENCH_FILTER=subprocess` (or similar) and a +small `BACKPACK_BENCH_ITERATIONS=3`. + +### What is measured + +Every benchmark is an **A/B pair**: variant A runs with stock Emacs defaults, +variant B with the values installed by Backpack's `:os windows` gear. The +harness reports the delta (both absolute milliseconds and percent) so you +can see at a glance whether a knob is helping, hurting, or merely noise. + +The benches exercise the three dimensions the Windows gear targets: + +| Benchmark | Primary knobs probed | +|---------------------------------|-------------------------------------------------------------------------------------| +| `bench-startup` | Cold startup cost of a child `emacs --batch -Q` loading early-init.el | +| `bench-file-attributes` | `w32-get-true-file-attributes`, `w32-get-true-file-link-count` | +| `bench-directory-scan` | Same, at recursive-walk scale | +| `bench-load-path-probe` | `file-name-handler-alist`, `auto-mode-case-fold` | +| `bench-auto-mode-match` | `auto-mode-case-fold` / `case-fold-search` during `auto-mode-alist` matching | +| `bench-subprocess-echo` | `process-connection-type`, `w32-pipe-read-delay`, `w32-pipe-buffer-size` | +| `bench-subprocess-large-output` | `w32-pipe-buffer-size`, `read-process-output-max`, `process-adaptive-read-buffering` | + +On non-Windows hosts the W32-specific variables are declared special via +stub `defvar`s so `let`-binding them compiles clean; the underlying C code +simply ignores them. Those benches therefore report ~0 delta, which is a +cheap smoke check that the gear is not accidentally regressing other +platforms. + +### Output + +Two outputs are produced on every run: + +1. A human-readable ASCII table printed to stdout. +2. A timestamped plist file under `.cache/etc/benchmarks/bench-YYYYMMDD-HHMMSS.eld` + containing the full per-variant stats (min, max, median, mean, stddev, n) + together with environment metadata (timestamp, system type, Emacs version, + native-comp availability, host name). These files are `read`-able Elisp + so they can be diffed, plotted, or aggregated externally. + +### Adding a new benchmark + +Register via `backpack-bench-defbench` in `test/benchmark/backpack-bench-defs.el`: + +```elisp +(backpack-bench-defbench bench-my-new-thing + :doc "One-line description of what is measured." + :iterations 15 + :warmup 2 + :available-p (lambda () (eq system-type 'windows-nt)) ; optional + :setup (lambda () ...) ; optional one-shot fixture + :run-a (lambda () ...) ; stock defaults variant + :run-b (lambda () ...)) ; Windows-optimized variant +``` + +Guidelines: + +- Keep per-iteration work in the single-digit-ms to low-hundred-ms range so + noise does not dwarf the signal. Very short bodies should loop internally. +- `let`-bind the variables the benchmark is measuring *inside* `:run-a` / + `:run-b`, not in `:setup` -- this keeps the two variants symmetric and + makes the bench source easy to read. +- Use `backpack-emacs-dir` (not `user-emacs-directory`) for any path that + must resolve to the repo root; `backpack-defaults.el` rebinds + `user-emacs-directory` to live inside `.cache/` early in startup. + ## Key Conventions and Rules ### General diff --git a/README.md b/README.md index efa7450..23a6dec 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,24 @@ git clone --recurse-submodules https://jardin.jorgearaya.dev/zecRiYjpjFnZWFhygfV git clone --recurse-submodules https://github.com/shackra/backpack.git ~/.emacs.d ``` +On Windows, use `backpack.cmd` instead of `backpack`: + +``` +backpack.cmd ensure +backpack.cmd gc --dry-run +``` + +Make sure `emacs.exe` is on your PATH. + +On Windows, use `backpack.cmd` instead of `backpack`: + +``` +backpack.cmd ensure +backpack.cmd gc --dry-run +``` + +Make sure `emacs.exe` is on your PATH. + ### Writing your initial configuration > [!IMPORTANT] diff --git a/bench.el b/bench.el new file mode 100644 index 0000000..ea362b7 --- /dev/null +++ b/bench.el @@ -0,0 +1,49 @@ +;;; bench.el --- Backpack benchmarking entry point -*- lexical-binding: t; -*- +;; +;; Usage: emacs --batch --eval "(setq user-emacs-directory \"/path/to/backpack/\")" -l bench.el +;; +;; Invoked by `bin/backpack bench' (POSIX) and `bin\backpack.cmd bench' (Windows). +;; The benchmark suite does *not* need elpaca or any user gears to be loaded: +;; it performs its own minimal setup so results are reproducible regardless of +;; what is pinned in the user's init.el. +;; +;; The actual benchmarks live under `test/benchmark/' so they participate in the +;; same code-review and history as the unit tests. + +;;; Code: + +;; Capture the original user-emacs-directory *before* loading backpack.el. +;; backpack-defaults.el deliberately rebinds `user-emacs-directory' to the +;; .cache/ subdir to keep $HOME tidy, which would break path resolution here +;; if we relied on it after the load. +(defvar backpack-bench--repo-root + (file-name-as-directory (expand-file-name user-emacs-directory)) + "Absolute path to the Backpack source tree, captured before backpack.el loads.") + +;; Mirror the path dance ensure.el / gc.el do, so base-packages libraries are +;; on the load-path for anything the benchmark harness might pull in. +(add-to-list 'load-path (expand-file-name "base-packages/leaf.el" backpack-bench--repo-root)) +(add-to-list 'load-path (expand-file-name "base-packages/leaf-keywords.el" backpack-bench--repo-root)) +(add-to-list 'load-path (expand-file-name "lisp" backpack-bench--repo-root)) +(add-to-list 'load-path (expand-file-name "test/benchmark" backpack-bench--repo-root)) + +;; Suppress the "Elpaca is not installed" display-warning that backpack.el +;; emits in normal mode when the user hasn't run `backpack ensure' yet. +;; The benchmark suite doesn't need elpaca: its microbenches exercise +;; variables and Lisp primitives, not packages. Requiring `warnings' +;; before mutating `warning-suppress-log-types' avoids the "defining as +;; dynamic an already lexical var" complaint under lexical-binding. +(require 'warnings) +(add-to-list 'warning-suppress-log-types '(backpack)) +(let ((backpack-file (expand-file-name "lisp/backpack.el" + backpack-bench--repo-root))) + (load backpack-file nil nil nil t)) + +;; Kick off the suite. run.el executes at load time and writes its own +;; output; we just forward its exit status. +(load (expand-file-name "test/benchmark/run.el" backpack-bench--repo-root) + nil nil nil t) + +(kill-emacs 0) + +;;; bench.el ends here diff --git a/bin/backpack b/bin/backpack index b67198d..4fd8ca1 100755 --- a/bin/backpack +++ b/bin/backpack @@ -12,6 +12,7 @@ if [ -z "$ACTION" ]; then echo "Available actions:" >&2 echo " ensure Install and build all packages (without activation)" >&2 echo " gc [--dry-run] Remove orphaned packages no longer needed by configuration" >&2 + echo " bench Run the Backpack benchmark suite (A/B vs :os windows)" >&2 exit 1 fi @@ -55,8 +56,21 @@ elif [ "$ACTION" = "gc" ]; then -l "$BACKPACK_DIR/gc.el" exit $? +elif [ "$ACTION" = "bench" ]; then + echo "Backpack: Running benchmark suite..." + echo "Backpack directory: $BACKPACK_DIR" + echo "" + echo "Tip: set BACKPACK_BENCH_FILTER=substring to run a subset" + echo " set BACKPACK_BENCH_ITERATIONS=N to override the default sample count" + echo "" + # Run Emacs in batch mode with bench.el + emacs --batch \ + --eval "(setq user-emacs-directory \"$BACKPACK_DIR/\")" \ + -l "$BACKPACK_DIR/bench.el" + exit $? + else echo "Error: unknown action '$ACTION'" >&2 - echo "Available actions: ensure, gc" >&2 + echo "Available actions: ensure, gc, bench" >&2 exit 1 fi diff --git a/bin/backpack.cmd b/bin/backpack.cmd new file mode 100644 index 0000000..2003fca --- /dev/null +++ b/bin/backpack.cmd @@ -0,0 +1,82 @@ +@echo off +setlocal enabledelayedexpansion + +if "%~1"=="" goto :usage +set "ACTION=%~1" +set "FLAG=%~2" + +REM --- Resolve Backpack root from script location --- +set "BACKPACK_DIR=%~dp0.." +for %%I in ("%BACKPACK_DIR%") do set "BACKPACK_DIR=%%~fI" + +REM --- Verify installation --- +if not exist "%BACKPACK_DIR%\ensure.el" ( + echo Error: This script must be run from the Backpack installation directory>&2 + echo Expected to find ensure.el in: %BACKPACK_DIR%>&2 + exit /b 1 +) +if not exist "%BACKPACK_DIR%\base-packages" ( + echo Error: This script must be run from the Backpack installation directory>&2 + echo Expected to find base-packages\ in: %BACKPACK_DIR%>&2 + exit /b 1 +) + +REM --- Convert backslashes to forward slashes for Emacs. +REM NOTE: do not name this BACKPACK_EMACS_DIR or anything that case- +REM insensitively matches "emacs_dir" -- on Windows that would shadow +REM Emacs's own reserved emacs_dir variable (see src/emacs.c +REM decode_env_path) and break data-directory / exec-directory +REM resolution, causing fatal "charsets: No such file or directory". --- +set "BACKPACK_DIR_FWD=%BACKPACK_DIR:\=/%" + +REM --- Defensive: clear any inherited emacs_dir so a parent shell that +REM happens to export it cannot poison Emacs's path resolution. --- +set "emacs_dir=" + +if "%ACTION%"=="ensure" goto :ensure +if "%ACTION%"=="gc" goto :gc +if "%ACTION%"=="bench" goto :bench +echo Error: unknown action '%ACTION%'>&2 +echo Available actions: ensure, gc, bench>&2 +exit /b 1 + +:ensure +echo Backpack: Synchronizing packages... +echo Backpack directory: %BACKPACK_DIR% +echo. +emacs --batch --eval "(setq user-emacs-directory \"%BACKPACK_DIR_FWD%/\")" -l "%BACKPACK_DIR_FWD%/ensure.el" +exit /b %errorlevel% + +:gc +set "DRY_RUN=nil" +if "%FLAG%"=="--dry-run" set "DRY_RUN=t" +if "%FLAG%"=="-n" set "DRY_RUN=t" +if "!DRY_RUN!"=="t" ( + echo Backpack: Garbage collection ^(dry run^)... +) else ( + echo Backpack: Garbage collection... +) +echo Backpack directory: %BACKPACK_DIR% +echo. +emacs --batch --eval "(setq user-emacs-directory \"%BACKPACK_DIR_FWD%/\")" --eval "(setq backpack-gc-dry-run !DRY_RUN!)" -l "%BACKPACK_DIR_FWD%/gc.el" +exit /b %errorlevel% + +:bench +echo Backpack: Running benchmark suite... +echo Backpack directory: %BACKPACK_DIR% +echo. +echo Tip: set BACKPACK_BENCH_FILTER=substring to run a subset +echo set BACKPACK_BENCH_ITERATIONS=N to override the default sample count +echo. +emacs --batch --eval "(setq user-emacs-directory \"%BACKPACK_DIR_FWD%/\")" -l "%BACKPACK_DIR_FWD%/bench.el" +exit /b %errorlevel% + +:usage +echo Error: missing argument>&2 +echo Usage: backpack ^ [flags]>&2 +echo.>&2 +echo Available actions:>&2 +echo ensure Install and build all packages ^(without activation^)>&2 +echo gc [--dry-run] Remove orphaned packages no longer needed>&2 +echo bench Run the Backpack benchmark suite ^(A/B vs :os windows^)>&2 +exit /b 1 \ No newline at end of file diff --git a/lisp/backpack-inventory.el b/lisp/backpack-inventory.el index 12278cc..042930e 100644 --- a/lisp/backpack-inventory.el +++ b/lisp/backpack-inventory.el @@ -20,13 +20,14 @@ (:email . "Email clients and utilities") (:term . "Terminal emulators and shell integration") (:ai . "AI and model context tools") + (:os . "Operating system optimizations") (:editing . "Programming language support and file type modes")) "Brief descriptions for each pouch category.") ;; Preferred display order for pouches (pouches not listed here ;; appear at the end in alphabetical order). (defvar backpack-inventory--pouch-order - '(:config :ui :completion :tools :ai :term :checkers :email :editing) + '(:config :ui :completion :tools :ai :os :term :checkers :email :editing) "Preferred display order for pouches.") ;;; Faces diff --git a/lisp/backpack-platform.el b/lisp/backpack-platform.el index b8004ab..7d22d82 100644 --- a/lisp/backpack-platform.el +++ b/lisp/backpack-platform.el @@ -55,5 +55,28 @@ (setenv "HOME" realhome) (setq abbreviated-home-dir nil))) +(defun backpack--windows-posix-bash-executable () + "Return bash.exe suitable for POSIX subprocesses on Windows, or nil. + +Prefer Git for Windows and MSYS2 over the Store `WindowsApps' bash shim, +which mishandles `bash -c' argument passing for subprocesses." + (when backpack--system-windows-p + (or (catch 'backpack--got-bash + (dolist (p (list (when-let ((pf (getenv "ProgramFiles"))) + (expand-file-name "Git/bin/bash.exe" pf)) + (when-let ((pfx86 (getenv "ProgramFiles(x86)"))) + (expand-file-name "Git/bin/bash.exe" pfx86)) + "c:/msys64/usr/bin/bash.exe" + "c:/msys64/bin/bash.exe")) + (when (and p (file-exists-p p) + (not (string-match-p "[/\\\\]WindowsApps[/\\\\]" p))) + (throw 'backpack--got-bash p)))) + (catch 'backpack--got-bash + (dolist (dir (split-string (or (getenv "PATH") "") path-separator t)) + (let ((exe (expand-file-name "bash.exe" dir))) + (when (and (file-exists-p exe) + (not (string-match-p "[/\\\\]WindowsApps[/\\\\]" exe))) + (throw 'backpack--got-bash exe)))))))) + (provide 'backpack-platform) ;;; backpack-platform.el ends here \ No newline at end of file diff --git a/lisp/backpack.el b/lisp/backpack.el index a321597..b1503f9 100644 --- a/lisp/backpack.el +++ b/lisp/backpack.el @@ -370,6 +370,7 @@ The behavior depends on `backpack-mode': (load (expand-file-name "gears/term/eshell" backpack-core-dir)) (load (expand-file-name "gears/ai/anvil" backpack-core-dir)) (load (expand-file-name "gears/editing/assembler" backpack-core-dir)) + (load (expand-file-name "gears/os/windows" backpack-core-dir)) (load (expand-file-name "gears/editing/c" backpack-core-dir)) (load (expand-file-name "gears/editing/cmake" backpack-core-dir)) (load (expand-file-name "gears/editing/clojure" backpack-core-dir)) diff --git a/lisp/gears/os/windows.el b/lisp/gears/os/windows.el new file mode 100644 index 0000000..e12650e --- /dev/null +++ b/lisp/gears/os/windows.el @@ -0,0 +1,120 @@ +;; -*- lexical-binding: t; -*- +(leaf windows + :when (gearp! :os windows) + :doc "Performance optimizations for native Windows. + +For maximum throughput, also add an exclusion in Windows Defender for +the Emacs install directory (e.g. C:/tools/emacs/) and your Backpack +cache (~/.emacs.d/.cache/). Defender's real-time scan otherwise re- +scans every .el/.elc/.eln file Emacs touches, and that single change +typically dominates startup and `backpack ensure' time more than every +Lisp-level setting in this gear combined." + :leaf-defer nil + :config + + ;; ─── Process / subprocess ─── + ;; Windows CreateProcess + pipe IPC is slow. + ;; w32-pipe-read-delay default > 0 is for ancient DOS programs. + (when (boundp 'w32-pipe-read-delay) + (setq w32-pipe-read-delay 0)) + ;; Larger pipe + read buffers cut syscalls per LSP message. Servers + ;; like gopls, rust-analyzer, and clangd routinely emit > 256 KB + ;; JSON-RPC frames during workspace indexing. 256 KB pairs well + ;; with a 1 MiB read buffer without inflating per-subprocess RSS. + (when (boundp 'w32-pipe-buffer-size) + (setq w32-pipe-buffer-size (* 256 1024))) + (when (boundp 'w32-quote-process-args) + (setq w32-quote-process-args t)) + ;; Use pipes instead of ptys — Windows has no real ptys, + ;; conpty adds overhead. Safe: eshell is Elisp, vterm manages + ;; its own pty internally. + (setq-default process-connection-type nil) + ;; LSP-mode and eglot both recommend >= 1 MiB on Windows. + (setq read-process-output-max (* 1024 1024)) + + ;; ─── File I/O ─── + ;; NTFS stat is 5-10× slower; skip owner/mode lookups for local + ;; files. The 'local value preserves accurate attributes for + ;; remote/Tramp paths (e.g. \\\\server\\share, /ssh:host:/path) + ;; where the cost is dominated by the network round-trip anyway. + (when (boundp 'w32-get-true-file-attributes) + (setq w32-get-true-file-attributes 'local)) + (when (boundp 'w32-get-true-file-link-count) + (setq w32-get-true-file-link-count nil)) + + ;; ─── Display / rendering ─── + ;; Windows font rendering (Uniscribe/DirectWrite) is expensive; + ;; don't let GC compact font caches and force re-rasterization + (setq inhibit-compacting-font-caches t) + (setq fast-but-imprecise-scrolling t) + (when (boundp 'redisplay-skip-fontification-on-input) + (setq redisplay-skip-fontification-on-input t)) + (setq cursor-in-non-selected-windows nil + highlight-nonselected-windows nil) + ;; For LTR-only users: skip bidi algorithm entirely + ;; Opt out with: (gear! :os (windows bidi)) + (unless (gearp! :os windows bidi) + (setq bidi-display-reordering 'left-to-right + bidi-paragraph-direction 'left-to-right + bidi-inhibit-bpa t)) + ;; Case-insensitive FS: no point re-scanning auto-mode-alist + (setq auto-mode-case-fold nil) + + ;; ─── Native compilation ─── + ;; Windows I/O is slower; defer async compilation to reduce contention + (when (boundp 'native-comp-defer-time) + (setq native-comp-defer-time 1.0)) + + ;; ─── Network ─── + ;; Reuse TLS connections — avoid repeated CryptoAPI handshake overhead + (setq url-http-attempt-keepalives t) + + ;; ─── Shell auto-detection ─── + ;; Prefer bash (Git for Windows / MSYS2) if available; + ;; fall back to cmdproxy.exe (bundled with Emacs) + (when (and (or (null shell-file-name) + (string-match-p "cmd\\(proxy\\)?\\.exe" shell-file-name))) + (when-let ((bash (backpack--windows-posix-bash-executable))) + (setq shell-file-name bash + shell-command-switch "-c")))) + +(leaf windows-legacy + :when (gearp! :os windows) + :emacs< "30.1" + :doc "Defensive Windows perf settings for Emacs 29.x. + +These variables are already auto-detected or correctly defaulted in +Emacs 30.1, so setting them there is redundant. On 29.1-29.4 the +defaults are the same in stock Emacs, but vendor builds and downstream +patches occasionally diverge — pinning the values here guarantees the +performance characteristics regardless of how the binary was built. + +All sets are guarded with `boundp' so an unexpectedly-stripped build +cannot signal a void-variable error during gear initialization." + :leaf-defer nil + :config + + ;; ─── Display / rendering ─── + ;; Use the native GDI+ image decoder instead of the bundled libpng / + ;; libjpeg / libtiff path: faster, fewer DLL dependencies, and no + ;; CVE risk from stale image libraries. Auto-detected as t in 30.1 + ;; whenever GDI+ loads; pin it for 29.x where some builds leave it nil. + (when (boundp 'w32-use-native-image-API) + (setq w32-use-native-image-API t)) + ;; Keep per-frame double buffering on globally — disabling it causes + ;; visible tearing on modern Windows compositors. Default is nil in + ;; both 29.x and 30.x; explicit for safety. + (when (boundp 'w32-disable-double-buffering) + (setq w32-disable-double-buffering nil)) + ;; Keep the modern Uniscribe shaping path (added in Emacs 25); the + ;; legacy fallback is slower and renders some scripts incorrectly. + (when (boundp 'w32-disable-new-uniscribe-apis) + (setq w32-disable-new-uniscribe-apis nil)) + + ;; ─── Process / subprocess ─── + ;; Adaptive read buffering throttles output from chatty subprocesses + ;; (compilers, LSP servers). On Windows the heuristic interacts + ;; badly with pipe IPC and starves large bursts; force it off so + ;; `read-process-output-max' / `w32-pipe-buffer-size' can do their job. + (when (boundp 'process-adaptive-read-buffering) + (setq process-adaptive-read-buffering nil))) \ No newline at end of file diff --git a/test/benchmark/backpack-bench-defs.el b/test/benchmark/backpack-bench-defs.el new file mode 100644 index 0000000..165c7e5 --- /dev/null +++ b/test/benchmark/backpack-bench-defs.el @@ -0,0 +1,354 @@ +;;; backpack-bench-defs.el --- Windows-focused benchmarks -*- lexical-binding: t; -*- + +;; Copyright (C) 2026 Jorge Javier Araya Navarro and Backpack contributors + +;;; Commentary: + +;; This file defines the A/B benchmark pairs that exercise the variables +;; flipped by the `:os windows' gear. The A side represents the stock +;; Emacs defaults; the B side represents Backpack's Windows-optimized +;; values. On non-Windows hosts most W32 variables do not exist or have +;; no effect, so those benches will report ~0 delta -- that is by design +;; and serves as a cheap smoke check that the gear is not regressing +;; other platforms. +;; +;; Startup is measured by spawning a fresh child Emacs process: the two +;; variants differ only in whether the Windows performance knobs are +;; pre-set via --eval before the Backpack early-init.el runs. + +;;; Code: + +(require 'cl-lib) +(require 'backpack-bench-harness) + +;; Declare Windows-only dynamic variables as special so `let'-binding them +;; on non-Windows hosts does not create a lexical binding and does not +;; produce "unused lexical variable" byte-compilation warnings. +(defvar w32-get-true-file-attributes) +(defvar w32-get-true-file-link-count) +(defvar w32-pipe-read-delay) +(defvar w32-pipe-buffer-size) +(defvar w32-quote-process-args) + +;; --------------------------------------------------------------------------- +;; Fixtures +;; --------------------------------------------------------------------------- + +;; Note: `user-emacs-directory' has been rebound by backpack-defaults.el to +;; live inside .cache/; `backpack-emacs-dir' is the real repo root and the +;; correct anchor for locating repo-relative fixtures like base-packages/. + +(defvar backpack-bench--sample-files nil + "Cached list of .el files used by the file-attribute benchmarks.") + +(defun backpack-bench--collect-sample-files () + "Return (and cache) up to 200 .el files under `base-packages/'." + (or backpack-bench--sample-files + (setq backpack-bench--sample-files + (let* ((root (expand-file-name "base-packages" + backpack-emacs-dir)) + (all (and (file-directory-p root) + (directory-files-recursively root "\\.el\\'")))) + (seq-take (sort (copy-sequence all) #'string<) 200))))) + +(defvar backpack-bench--directory-scan-root nil + "Directory scanned by the `directory-files-recursively' benchmark.") + +(defun backpack-bench--directory-scan-root () + "Return (and cache) the directory used for recursive-scan benches." + (or backpack-bench--directory-scan-root + (setq backpack-bench--directory-scan-root + (expand-file-name "base-packages" backpack-emacs-dir)))) + +(defvar backpack-bench--probe-libs + '("cl-lib" "subr-x" "seq" "map" "pcase" "project" "xref" + "bytecomp" "json" "rx" "url" "dired" "diff" "imenu" + "thingatpt" "ert" "benchmark" "package" "eldoc" "flymake") + "Libraries probed by `bench-load-path-probe'.") + +(defvar backpack-bench--random-names + (let ((exts '(".el" ".py" ".rs" ".go" ".c" ".cpp" ".js" ".ts" ".tsx" + ".html" ".css" ".json" ".yaml" ".toml" ".md" ".txt" + ".lua" ".hs" ".sh" ".nix"))) + (cl-loop for i from 0 below 500 + collect (format "/tmp/project/sub%d/file-%d%s" + (mod i 20) i + (nth (mod (* i 7) (length exts)) exts)))) + "Deterministic list of 500 synthetic filenames used for auto-mode matching.") + +(defvar backpack-bench--large-output-file nil + "Path to the file used for `bench-subprocess-large-output'.") + +(defun backpack-bench--ensure-large-file () + "Create (if needed) and return the ~2.2 MB fixture file." + (or (and backpack-bench--large-output-file + (file-exists-p backpack-bench--large-output-file) + backpack-bench--large-output-file) + (let* ((dir (expand-file-name "bench" backpack-nonessential-dir)) + (file (expand-file-name "large-output.txt" dir))) + (make-directory dir t) + (unless (and (file-exists-p file) + (> (file-attribute-size (file-attributes file)) + (* 2 1024 1024))) + (with-temp-file file + (dotimes (_ 50000) + (insert "the quick brown fox jumps over the lazy dog\n")))) + (setq backpack-bench--large-output-file file) + file))) + +;; --------------------------------------------------------------------------- +;; Helpers used inside benchmark thunks +;; --------------------------------------------------------------------------- + +(defmacro backpack-bench--with-w32-file-attrs (attrs-val link-val &rest body) + "Bind W32 file-attribute variables to ATTRS-VAL and LINK-VAL around BODY. + +On non-Windows hosts, the let-bindings still take effect but have no +observable impact; the C-level code simply does not consult them." + (declare (indent 2)) + `(let ((w32-get-true-file-attributes ,attrs-val) + (w32-get-true-file-link-count ,link-val)) + ,@body)) + +(defmacro backpack-bench--with-process-io (pipe-delay pipe-buf conn-type + adaptive read-max + &rest body) + "Bind Windows process IO knobs around BODY." + (declare (indent 5)) + `(let ((w32-pipe-read-delay ,pipe-delay) + (w32-pipe-buffer-size ,pipe-buf) + (process-connection-type ,conn-type) + (process-adaptive-read-buffering ,adaptive) + (read-process-output-max ,read-max)) + ,@body)) + +;; --------------------------------------------------------------------------- +;; 1. file-attributes -- w32-get-true-file-attributes / -link-count +;; --------------------------------------------------------------------------- + +(backpack-bench-defbench bench-file-attributes + :doc "200 `file-attributes' calls over .el files. +A: stock (t / t) B: Windows-optimized ('local / nil)" + :iterations 15 + :setup (lambda () (backpack-bench--collect-sample-files)) + :run-a (lambda () + (backpack-bench--with-w32-file-attrs t t + (dolist (f (backpack-bench--collect-sample-files)) + (file-attributes f)))) + :run-b (lambda () + (backpack-bench--with-w32-file-attrs 'local nil + (dolist (f (backpack-bench--collect-sample-files)) + (file-attributes f))))) + +;; --------------------------------------------------------------------------- +;; 2. directory-files-recursively -- full tree walk +;; --------------------------------------------------------------------------- + +(backpack-bench-defbench bench-directory-scan + :doc "`directory-files-recursively' over base-packages/. +A: stock B: Windows-optimized w32 file-attribute vars" + :iterations 10 + :setup (lambda () (backpack-bench--directory-scan-root)) + :run-a (lambda () + (backpack-bench--with-w32-file-attrs t t + (directory-files-recursively + (backpack-bench--directory-scan-root) "\\.el\\'"))) + :run-b (lambda () + (backpack-bench--with-w32-file-attrs 'local nil + (directory-files-recursively + (backpack-bench--directory-scan-root) "\\.el\\'")))) + +;; --------------------------------------------------------------------------- +;; 3. load-path probing -- file-name-handler-alist / auto-mode-case-fold +;; --------------------------------------------------------------------------- + +(defun backpack-bench--do-probe-libs (handler-alist case-fold) + "Helper: probe every library in `backpack-bench--probe-libs'. + +HANDLER-ALIST is bound as `file-name-handler-alist' and CASE-FOLD as +`auto-mode-case-fold' across the probes." + (let ((file-name-handler-alist handler-alist) + (auto-mode-case-fold case-fold)) + (dolist (lib backpack-bench--probe-libs) + (locate-library lib)))) + +(defun backpack-bench--stock-handler-alist () + "Return the full default `file-name-handler-alist'. +Backpack strips this list at startup (see the Doom-style hack in +`lisp/backpack.el'); the original is preserved on the variable's +`initial-value' property. Falls back to the current top-level value +if the property is missing (e.g. when running this file standalone)." + (or (get 'file-name-handler-alist 'initial-value) + (default-toplevel-value 'file-name-handler-alist))) + +(backpack-bench-defbench bench-load-path-probe + :doc "`locate-library' for 20 core libraries. +A: full file-name-handler-alist, case-fold t +B: stripped handler list, case-fold nil" + :iterations 20 + :run-a (lambda () + (backpack-bench--do-probe-libs + (backpack-bench--stock-handler-alist) t)) + :run-b (lambda () + (backpack-bench--do-probe-libs nil nil))) + +;; --------------------------------------------------------------------------- +;; 4. auto-mode-alist matching -- auto-mode-case-fold +;; --------------------------------------------------------------------------- +;; +;; This benchmark simulates the inner loop of `set-auto-mode' by walking +;; `auto-mode-alist' for each synthetic filename. Windows native Emacs +;; pays the case-folding cost for every buffer visit, so toggling +;; `auto-mode-case-fold' (and therefore `case-fold-search' inside the +;; matcher) is a realistic microbench. + +(defun backpack-bench--match-names (case-fold) + (let ((case-fold-search case-fold)) + (dolist (fn backpack-bench--random-names) + (cl-block match + (dolist (entry auto-mode-alist) + (when (and (consp entry) + (stringp (car entry)) + (string-match (car entry) fn)) + (cl-return-from match))))))) + +(backpack-bench-defbench bench-auto-mode-match + :doc "Match 500 filenames against `auto-mode-alist'. +A: case-fold t B: case-fold nil" + :iterations 20 + :run-a (lambda () (backpack-bench--match-names t)) + :run-b (lambda () (backpack-bench--match-names nil))) + +;; --------------------------------------------------------------------------- +;; 5. Subprocess spawn (many short calls) -- process-connection-type etc. +;; --------------------------------------------------------------------------- + +(defun backpack-bench--platform-echo-args () + "Return (PROGRAM ARGS...) that prints a short string and exits." + (cond + ((eq system-type 'windows-nt) + (list (or (getenv "ComSpec") "cmd.exe") "/c" "echo" "hello")) + (t + (list "sh" "-c" "printf hello")))) + +(defconst backpack-bench--echo-spawn-count 20 + "Number of `echo' spawns per iteration of `bench-subprocess-echo'. + +Windows CreateProcess is expensive (>20 ms per call on healthy +hardware, >200 ms with an active Defender scan). Keeping the per- +iteration spawn count modest means the full suite finishes in a few +minutes even on slower machines.") + +(backpack-bench-defbench bench-subprocess-echo + :doc "Spawn a trivial `echo' N times synchronously (N=20). +A: stock pipe vars (slow delay, small buffer, pty where possible) +B: Windows-optimized pipe vars (0 delay, 256 KiB buf, pipes)" + :iterations 8 + :warmup 1 + :run-a (lambda () + (backpack-bench--with-process-io 50 4096 t t 4096 + (let ((argv (backpack-bench--platform-echo-args))) + (dotimes (_ backpack-bench--echo-spawn-count) + (with-temp-buffer + (apply #'call-process (car argv) nil t nil (cdr argv))))))) + :run-b (lambda () + (backpack-bench--with-process-io 0 (* 256 1024) nil nil (* 1024 1024) + (let ((argv (backpack-bench--platform-echo-args))) + (dotimes (_ backpack-bench--echo-spawn-count) + (with-temp-buffer + (apply #'call-process (car argv) nil t nil (cdr argv)))))))) + +;; --------------------------------------------------------------------------- +;; 6. Subprocess large output -- pipe buffer / read-process-output-max +;; --------------------------------------------------------------------------- + +(defun backpack-bench--platform-cat-args (file) + "Return argv for dumping FILE to stdout on the current platform." + (cond + ((eq system-type 'windows-nt) + (list (or (getenv "ComSpec") "cmd.exe") "/c" "type" + (subst-char-in-string ?/ ?\\ file))) + (t + (list "sh" "-c" (format "cat %s" (shell-quote-argument file)))))) + +(backpack-bench-defbench bench-subprocess-large-output + :doc "Read ~2.2 MB from a subprocess into a buffer. +A: 4 KiB pipe buffer, 4 KiB read-max, adaptive buffering on +B: 256 KiB pipe buffer, 1 MiB read-max, adaptive buffering off" + :iterations 8 + :warmup 1 + :setup (lambda () (backpack-bench--ensure-large-file)) + :run-a (lambda () + (backpack-bench--with-process-io 50 4096 t t 4096 + (let ((argv (backpack-bench--platform-cat-args + (backpack-bench--ensure-large-file)))) + (with-temp-buffer + (apply #'call-process (car argv) nil t nil (cdr argv)))))) + :run-b (lambda () + (backpack-bench--with-process-io 0 (* 256 1024) nil nil (* 1024 1024) + (let ((argv (backpack-bench--platform-cat-args + (backpack-bench--ensure-large-file)))) + (with-temp-buffer + (apply #'call-process (car argv) nil t nil (cdr argv))))))) + +;; --------------------------------------------------------------------------- +;; 7. Startup -- spawn a fresh child Emacs through early-init.el +;; --------------------------------------------------------------------------- + +(defun backpack-bench--emacs-binary () + "Return the path to the Emacs binary that invoked us." + (expand-file-name invocation-name invocation-directory)) + +(defvar backpack-bench--startup-stock-eval + "(setq user-emacs-directory %S)" + "--eval expression for the stock-variant child Emacs.") + +(defvar backpack-bench--startup-windows-eval + "(progn (setq user-emacs-directory %S) + (when (boundp 'w32-pipe-read-delay) (setq w32-pipe-read-delay 0)) + (when (boundp 'w32-pipe-buffer-size) (setq w32-pipe-buffer-size (* 256 1024))) + (when (boundp 'w32-get-true-file-attributes) (setq w32-get-true-file-attributes 'local)) + (when (boundp 'w32-get-true-file-link-count) (setq w32-get-true-file-link-count nil)) + (when (boundp 'w32-quote-process-args) (setq w32-quote-process-args t)) + (setq auto-mode-case-fold nil + fast-but-imprecise-scrolling t + inhibit-compacting-font-caches t + process-adaptive-read-buffering nil + process-connection-type nil + read-process-output-max (* 1024 1024)))" + "--eval expression that pre-installs the `:os windows' gear knobs.") + +(defun backpack-bench--run-startup (eval-form) + "Spawn a batch child Emacs through Backpack's early-init.el. + +The outer `backpack-bench--time' already measures wall clock, so this +function just has to run the subprocess synchronously and ignore its +output. The child is invoked with `-Q' so its cost is entirely +attributable to Backpack's own code." + (let* ((emacs (backpack-bench--emacs-binary)) + (eid (expand-file-name "." backpack-emacs-dir)) + (early-init (expand-file-name "early-init.el" backpack-emacs-dir))) + (with-temp-buffer + (apply #'call-process + emacs nil t nil + (list "--batch" "-Q" + "--eval" (format eval-form eid) + "-l" early-init + "--eval" "(kill-emacs 0)"))))) + +(backpack-bench-defbench bench-startup + :doc "Spawn a child `emacs --batch -Q' and run Backpack's early-init.el. +A: no Windows perf knobs set +B: Windows perf knobs pre-installed via --eval" + :iterations 5 + :warmup 1 + :run-a (lambda () + (backpack-bench--run-startup + backpack-bench--startup-stock-eval)) + :run-b (lambda () + (backpack-bench--run-startup + backpack-bench--startup-windows-eval))) + +(provide 'backpack-bench-defs) + +;;; backpack-bench-defs.el ends here diff --git a/test/benchmark/backpack-bench-harness.el b/test/benchmark/backpack-bench-harness.el new file mode 100644 index 0000000..b0f7fed --- /dev/null +++ b/test/benchmark/backpack-bench-harness.el @@ -0,0 +1,242 @@ +;;; backpack-bench-harness.el --- Benchmark harness for Backpack -*- lexical-binding: t; -*- + +;; Copyright (C) 2026 Jorge Javier Araya Navarro and Backpack contributors + +;;; Commentary: + +;; Small wrapper around `benchmark.el' that adds: +;; +;; * warm-up iterations (discarded), so the JIT/native-comp path is hot +;; * per-sample GC so one outlier cannot poison subsequent samples +;; * min / median / mean / stddev reporting +;; * an A/B runner that runs each benchmark twice -- once against the +;; stock Emacs defaults, once against the Backpack Windows-optimized +;; values -- and reports the delta, which is the interesting number. +;; +;; The harness is batch-safe (no hard dependency on a GUI frame) and +;; platform-agnostic. Benchmarks that only make sense on Windows can +;; advertise an `:available-p' predicate; on other platforms they are +;; reported as skipped rather than producing meaningless numbers. + +;;; Code: + +(require 'cl-lib) + +(defvar backpack-bench-default-iterations 20 + "Default number of timed samples per benchmark variant.") + +(defvar backpack-bench-default-warmup 3 + "Default number of warm-up iterations (discarded) per variant.") + +(defvar backpack-bench--spec-registry nil + "Alist of (NAME . SPEC) for benchmarks registered via `backpack-bench-defbench'.") + +(defmacro backpack-bench-defbench (name &rest spec) + "Register a benchmark called NAME with SPEC plist. + +Supported keys: + :doc documentation string + :iterations integer (overrides `backpack-bench-default-iterations') + :warmup integer (overrides `backpack-bench-default-warmup') + :available-p thunk -- return non-nil if the benchmark can run here + :setup thunk run once before each variant (may create fixtures) + :run-a thunk measuring the stock-defaults variant + :run-b thunk measuring the Windows-optimized variant + +Both :run-a and :run-b take no arguments and return an arbitrary +value (ignored)." + (declare (indent 1)) + `(progn + (setf (alist-get ',name backpack-bench--spec-registry) + (list :name ',name ,@spec)) + ',name)) + +(defun backpack-bench--median (samples) + "Return the median of SAMPLES (a non-empty list of numbers)." + (let* ((sorted (sort (copy-sequence samples) #'<)) + (n (length sorted))) + (if (cl-oddp n) + (float (nth (/ n 2) sorted)) + (/ (+ (nth (1- (/ n 2)) sorted) + (nth (/ n 2) sorted)) + 2.0)))) + +(defun backpack-bench--stats (samples) + "Return a plist of :min :median :mean :stddev :n for SAMPLES." + (let* ((n (length samples)) + (total (cl-reduce #'+ samples :initial-value 0.0)) + (mean (if (zerop n) 0.0 (/ total n))) + (variance (if (< n 2) + 0.0 + (/ (cl-reduce + (lambda (acc x) (+ acc (expt (- x mean) 2))) + samples :initial-value 0.0) + (float (1- n)))))) + (list :min (if samples (apply #'min samples) 0.0) + :max (if samples (apply #'max samples) 0.0) + :median (if samples (backpack-bench--median samples) 0.0) + :mean mean + :stddev (sqrt variance) + :n n))) + +(defun backpack-bench--time (thunk iterations warmup) + "Run THUNK ITERATIONS times after WARMUP discarded runs. +Return a list of wall-clock seconds per run." + (dotimes (_ warmup) + (funcall thunk)) + (let (samples) + (dotimes (_ iterations) + (garbage-collect) + (let ((t0 (current-time))) + (funcall thunk) + (push (float-time (time-subtract (current-time) t0)) samples))) + (nreverse samples))) + +(defun backpack-bench--platform-skip-p (spec) + "Return non-nil if SPEC declares it cannot run on the current platform." + (let ((pred (plist-get spec :available-p))) + (and pred (not (funcall pred))))) + +(defun backpack-bench-run (spec) + "Run benchmark SPEC (plist) and return a result plist. + +Return value has the shape: + + (:name NAME :doc DOC :iterations N + :a (:min .. :median .. :mean .. :stddev .. :n ..) + :b (:min .. :median .. :mean .. :stddev .. :n ..)) + +or, if the benchmark is unavailable on the current platform, + + (:name NAME :skipped t :reason STRING)." + (let* ((name (plist-get spec :name)) + (iter (or (plist-get spec :iterations) + backpack-bench-default-iterations)) + (warm (or (plist-get spec :warmup) + backpack-bench-default-warmup)) + (setup (plist-get spec :setup)) + (run-a (plist-get spec :run-a)) + (run-b (plist-get spec :run-b))) + (message " %-32s" (symbol-name name)) + (cond + ((backpack-bench--platform-skip-p spec) + (message " skipped (not available on %s)" system-type) + (list :name name :skipped t + :reason (format "unavailable on %s" system-type) + :doc (plist-get spec :doc))) + ((not (and (functionp run-a) (functionp run-b))) + (error "Benchmark %s is missing :run-a or :run-b" name)) + (t + (when setup (funcall setup)) + (let* ((samples-a (backpack-bench--time run-a iter warm)) + (samples-b (backpack-bench--time run-b iter warm)) + (stats-a (backpack-bench--stats samples-a)) + (stats-b (backpack-bench--stats samples-b)) + (med-a (plist-get stats-a :median)) + (med-b (plist-get stats-b :median)) + (pct (if (> med-a 0) + (* 100.0 (/ (- med-b med-a) med-a)) + 0.0))) + (message " A median %8.3f ms B median %8.3f ms delta %+6.1f%%" + (* 1000 med-a) (* 1000 med-b) pct) + (list :name name + :doc (plist-get spec :doc) + :iterations iter + :warmup warm + :a stats-a + :b stats-b)))))) + +(defun backpack-bench--format-cell (value) + "Format VALUE (seconds) for table output, as milliseconds." + (format "%10.3f" (* 1000 value))) + +(defun backpack-bench-format-table (results) + "Return a pretty ASCII table string for RESULTS. + +RESULTS is a list of plists as returned by `backpack-bench-run'." + (let* ((header (format "%-32s %10s %10s %10s %10s %10s %8s" + "benchmark" + "A med ms" "A sd ms" + "B med ms" "B sd ms" + "delta ms" "delta %")) + (divider (make-string (length header) ?-)) + (rows + (mapcar + (lambda (r) + (cond + ((plist-get r :skipped) + (format "%-32s %s" + (symbol-name (plist-get r :name)) + (or (plist-get r :reason) "(skipped)"))) + (t + (let* ((a (plist-get r :a)) + (b (plist-get r :b)) + (med-a (plist-get a :median)) + (med-b (plist-get b :median)) + (delta (- med-b med-a)) + (pct (if (> med-a 0) + (* 100.0 (/ delta med-a)) + 0.0))) + (format "%-32s %s %s %s %s %s %+7.1f%%" + (symbol-name (plist-get r :name)) + (backpack-bench--format-cell med-a) + (backpack-bench--format-cell (plist-get a :stddev)) + (backpack-bench--format-cell med-b) + (backpack-bench--format-cell (plist-get b :stddev)) + (format "%+10.3f" (* 1000 delta)) + pct))))) + results))) + (mapconcat #'identity + (append (list header divider) rows) + "\n"))) + +(defun backpack-bench-save-results (results output-file) + "Save RESULTS to OUTPUT-FILE as a single readable plist. + +The file also embeds environment metadata (timestamp, system type, +Emacs version, native-comp availability) so historical runs can be +diffed meaningfully." + (make-directory (file-name-directory output-file) t) + (let ((record + (list :timestamp (format-time-string "%Y-%m-%dT%H:%M:%S%z") + :system-type (symbol-name system-type) + :system-configuration system-configuration + :emacs-version emacs-version + :native-compile (and (fboundp 'native-comp-available-p) + (native-comp-available-p)) + :host (system-name) + :results results))) + (with-temp-file output-file + (let ((print-level nil) + (print-length nil) + (print-circle t)) + (insert ";; -*- mode: emacs-lisp; -*-\n") + (insert ";; Backpack benchmark run\n") + (insert (format ";; %s -- %s / Emacs %s\n\n" + (plist-get record :timestamp) + (plist-get record :system-type) + (plist-get record :emacs-version))) + (pp record (current-buffer)))) + output-file)) + +(defun backpack-bench-run-all (&optional filter) + "Run every registered benchmark and return the list of result plists. + +If FILTER is a non-nil symbol or string, only benchmarks whose name +contains FILTER (as a substring) are executed. Useful for iterating +on one bench without paying for the whole suite." + (let ((results '())) + (dolist (entry backpack-bench--spec-registry) + (let* ((spec (cdr entry)) + (name (symbol-name (plist-get spec :name)))) + (when (or (null filter) + (string-match-p (if (symbolp filter) + (symbol-name filter) + filter) + name)) + (push (backpack-bench-run spec) results)))) + (nreverse results))) + +(provide 'backpack-bench-harness) + +;;; backpack-bench-harness.el ends here diff --git a/test/benchmark/run.el b/test/benchmark/run.el new file mode 100644 index 0000000..0817e99 --- /dev/null +++ b/test/benchmark/run.el @@ -0,0 +1,105 @@ +;;; run.el --- Entry point for `backpack bench' benchmark runs -*- lexical-binding: t; -*- + +;; Copyright (C) 2026 Jorge Javier Araya Navarro and Backpack contributors + +;;; Commentary: + +;; Loaded by `bench.el' (at repo root) under `emacs --batch'. Responsible +;; for registering every benchmark, executing the suite, printing a +;; human-readable table to stdout, and appending a machine-readable +;; timestamped record under `.cache/etc/benchmarks/'. +;; +;; Optional environment variables: +;; BACKPACK_BENCH_FILTER -- substring; only run matching benches +;; BACKPACK_BENCH_ITERATIONS -- override default per-variant iterations +;; BACKPACK_BENCH_NO_SAVE -- if set to a non-empty string, skip saving +;; the results file (useful in CI) + +;;; Code: + +(let ((bench-dir (file-name-directory (or load-file-name buffer-file-name)))) + (add-to-list 'load-path bench-dir)) + +(require 'backpack-bench-harness) +(require 'backpack-bench-defs) + +;; `user-emacs-directory' has already been rebound by backpack-defaults.el to +;; point inside .cache/; `backpack-emacs-dir' is the real repo root. We write +;; benchmark history next to the other Backpack data files. +(defvar backpack-bench-output-dir + (expand-file-name "benchmarks" backpack-data-dir) + "Directory under which timestamped benchmark result files are written.") + +(defun backpack-bench--output-file () + "Return the path for the current run's result file." + (expand-file-name + (format "bench-%s.eld" (format-time-string "%Y%m%d-%H%M%S")) + backpack-bench-output-dir)) + +(defun backpack-bench--maybe-override-iterations () + "Honor BACKPACK_BENCH_ITERATIONS, if set and numeric. + +When set, the override forcibly replaces the iteration count of every +registered benchmark -- otherwise specs that declare their own +`:iterations' would silently ignore the flag, which is surprising +during development." + (when-let* ((raw (getenv "BACKPACK_BENCH_ITERATIONS")) + (n (and (string-match-p "\\`[0-9]+\\'" raw) + (string-to-number raw)))) + (setq backpack-bench-default-iterations n) + (dolist (entry backpack-bench--spec-registry) + (setcdr entry (plist-put (cdr entry) :iterations n))) + (message "Overriding iterations to %d (every bench)" n))) + +(defun backpack-bench--filter () + "Return the active filter string (from BACKPACK_BENCH_FILTER) or nil." + (let ((raw (getenv "BACKPACK_BENCH_FILTER"))) + (and raw (not (string-empty-p raw)) raw))) + +(defun backpack-bench--banner () + "Print a short environment banner to stdout." + (message "") + (message "========================================") + (message "Backpack benchmark run") + (message "========================================") + (message "") + (message " emacs : %s" emacs-version) + (message " system : %s (%s)" + system-configuration (symbol-name system-type)) + (message " native-comp : %s" + (if (and (fboundp 'native-comp-available-p) + (native-comp-available-p)) + "yes" "no")) + (message " iterations : %d (override via BACKPACK_BENCH_ITERATIONS)" + backpack-bench-default-iterations) + (when-let* ((f (backpack-bench--filter))) + (message " filter : %s" f)) + (message "")) + +(defun backpack-bench--execute () + (backpack-bench--maybe-override-iterations) + (backpack-bench--banner) + (message "Running benchmarks (A = stock defaults, B = :os windows gear):") + (message "") + (let* ((results (backpack-bench-run-all (backpack-bench--filter))) + (table (backpack-bench-format-table results))) + (message "") + (message "Results:") + (message "") + (message "%s" table) + (message "") + (cond + ((getenv "BACKPACK_BENCH_NO_SAVE") + (message "(Skipping result file; BACKPACK_BENCH_NO_SAVE is set.)")) + (t + (let ((out (backpack-bench--output-file))) + (backpack-bench-save-results results out) + (message "History written to: %s" out)))) + (message "") + results)) + +(backpack-bench--execute) + +(provide 'run) + +;;; run.el ends here From 22761e5ca338707a7274a4634bc29f211d4dd63e Mon Sep 17 00:00:00 2001 From: Jorge Javier Araya Navarro Date: Mon, 11 May 2026 08:38:24 -0600 Subject: [PATCH 2/6] fix(term): run Eat on native Windows Advice make-process replaces eat's /usr/bin/env sh bootstrap with Git/MSYS bash. Patch stty script: quote $1 in the shift test, wrap stty with || true for pipe IPC, pass bash +m. Exec inner shell as argv tokens so paths with spaces are not split by sh -c. --- lisp/gears/term/eshell.el | 58 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/lisp/gears/term/eshell.el b/lisp/gears/term/eshell.el index 58b4031..4460471 100644 --- a/lisp/gears/term/eshell.el +++ b/lisp/gears/term/eshell.el @@ -131,6 +131,60 @@ Prefers vterm when active, then eat, then eshell." (not (gearp! :term eshell -eat))) :ensure (eat :host codeberg :repo "akib/emacs-eat" :ref "c8d54d649872bfe7b2b9f49ae5c2addbf12d3b99") + :preface + ;; Eat hardcodes "/usr/bin/env" "sh" for a stty+exec bootstrap; that path + ;; does not exist on native Windows. Git Bash / MSYS2 provide bash.exe. + (defun backpack--make-process-eat-windows (orig &rest plist) + "Around-advice for `make-process'. +Rewrite eat's POSIX-only bootstrap command when bash is available." + (let ((cmd (plist-get plist :command)) + (bash (backpack--windows-posix-bash-executable))) + (if (and backpack--system-windows-p + bash + (listp cmd) + (>= (length cmd) 4) + (equal (car cmd) "/usr/bin/env") + (equal (nth 1 cmd) "sh") + (equal (nth 2 cmd) "-c") + (stringp (nth 3 cmd)) + (string-match-p "\\bstty\\b" (nth 3 cmd))) + (let* ((tail (nthcdr 3 cmd)) + (script (car tail)) + (dotdot (cadr tail)) + (rest (cddr tail)) + (script-fixed + (if (stringp script) + (let ((s (replace-regexp-in-string " 2>[^ ;]+" "" script))) + ;; Eat uses `[ $1 = .. ]' with unquoted `$1'; a spaced path + ;; becomes multiple words → `[: too many arguments]'. + (setq s (replace-regexp-in-string + "if \\[ \\$1 = \\.\\. \\]; then shift; fi" + "if [ \"$1\" = .. ]; then shift; fi" + s)) + ;; Pipe IPC is not a tty — stty ioctl fails; do not abort. + (replace-regexp-in-string + "^\\(stty[^;]+\\);" + "(\\1) || true;" + s)) + script)) + (rest-fixed + (if (and (equal dotdot "..") + (= (length rest) 4) + (equal (nth 0 rest) "/usr/bin/env") + (equal (nth 1 rest) "sh") + (equal (nth 2 rest) "-c") + (stringp (nth 3 rest)) + (let ((inner (nth 3 rest))) + (or (file-exists-p inner) + (file-exists-p (expand-file-name inner))))) + ;; Avoid `sh -c c:/Program Files/...' word-splitting; `+m' + ;; quiets job-control complaints when stdin is not a pty. + (list (nth 3 rest) "-i" "+m") + rest)) + (tail-fixed (cons script-fixed (cons dotdot rest-fixed)))) + (apply orig (plist-put (copy-tree plist) :command + (cons bash (cons "-c" tail-fixed))))) + (apply orig plist)))) :bind ("C-c t a" . backpack/eat-toggle) :custom @@ -139,6 +193,10 @@ Prefers vterm when active, then eat, then eshell." ;; Enable automatic line rewrapping on window resize (eat-enable-auto-line-translation . t) :config + (when-let ((bash (backpack--windows-posix-bash-executable))) + (setq eat-shell bash) + (advice-remove #'make-process #'backpack--make-process-eat-windows) + (advice-add #'make-process :around #'backpack--make-process-eat-windows)) ;; Integrate eat with eshell -- this makes eshell handle TUI/curses ;; applications correctly by delegating rendering to eat. (eat-eshell-mode +1) From 1468eaddafddba075d04b0d0cda63b825f6f2f68 Mon Sep 17 00:00:00 2001 From: Jorge Javier Araya Navarro Date: Mon, 11 May 2026 08:38:31 -0600 Subject: [PATCH 3/6] fix(spellchecking): expose flyspell when jinx cannot load --- lisp/gears/checkers/spellchecking.el | 128 ++++++++++++++++++++++++++- 1 file changed, 125 insertions(+), 3 deletions(-) diff --git a/lisp/gears/checkers/spellchecking.el b/lisp/gears/checkers/spellchecking.el index 5b9273b..4e16dee 100644 --- a/lisp/gears/checkers/spellchecking.el +++ b/lisp/gears/checkers/spellchecking.el @@ -1,11 +1,133 @@ -(require 'backpack-pouch) +;; -*- lexical-binding: t; -*- + +;;; Backend resolution +;; +;; The spellchecking gear supports two backends: +;; +;; jinx -- a fast just-in-time spell-checker backed by Enchant. +;; Requires a C compiler and Enchant development headers +;; on this machine the first time `jinx-mode' runs, +;; because jinx ships its spellcheck primitives as a C +;; dynamic module that it compiles lazily. +;; +;; flyspell -- Emacs's built-in spell-checker driving an external +;; `hunspell' process. No dynamic module, no compiler, +;; no Enchant -- just a binary on PATH. The fallback +;; used when jinx cannot possibly build (e.g. on +;; locked-down Windows laptops where Enchant + a build +;; toolchain are not installable). +;; +;; At gear load time we probe which backend is feasible and cache the +;; answer for the rest of the session. Users can force the fallback +;; even on a machine where jinx would work by adding the `-jinx' flag +;; to the gear: `(gear! :checkers (spellchecking -jinx))'. + +(defvar backpack--spellchecking-backend 'unresolved + "Cached backend choice for the spellchecking gear. +One of the symbols `jinx', `flyspell', `none', or `unresolved' +\(the initial sentinel value before `backpack-spellchecking-backend' +has been called for the first time).") + +(defun backpack--spellchecking-jinx-feasible-p () + "Return non-nil iff jinx's dynamic module can plausibly build and load. +Checks: dynamic modules supported by this Emacs, a C compiler on +`exec-path', and Enchant development headers discoverable either +via pkg-config or at the standard Unix include paths. Never +invokes the compiler itself." + (and module-file-suffix + (seq-find #'executable-find '("gcc" "clang" "cc")) + (or (and (executable-find "pkg-config") + (zerop (call-process "pkg-config" nil nil nil + "--exists" "enchant-2"))) + (file-readable-p "/usr/include/enchant-2/enchant.h") + (file-readable-p "/usr/local/include/enchant-2/enchant.h")))) + +(defun backpack--spellchecking-hunspell-feasible-p () + "Return non-nil iff `hunspell' is reachable and has at least one dictionary. +Runs `hunspell -D' once to enumerate available dictionaries; if +that section is empty we treat the backend as infeasible so we do +not activate `flyspell-mode' against a dictionary-less hunspell +\(which would error in every buffer on first word check)." + (when-let* ((exe (executable-find "hunspell"))) + (with-temp-buffer + (when (zerop (call-process exe nil t nil "-D")) + (goto-char (point-min)) + ;; `hunspell -D' prints a SEARCH PATH block, then an + ;; "AVAILABLE DICTIONARIES" header, then one dict per line, + ;; then a "LOADED DICTIONARY" footer. A non-empty section + ;; between the header and the footer means at least one + ;; dictionary is loadable. + (and (re-search-forward "AVAILABLE DICTIONARIES" nil t) + (progn (forward-line 1) + (not (or (eobp) + (looking-at-p "LOADED DICTIONARY:"))))))))) + +(defun backpack-spellchecking-backend () + "Resolve and cache the spell-check backend to use this session. +Return value is one of the symbols `jinx', `flyspell', or `none'. +Honours the `-jinx' opt-out flag on the spellchecking gear: +when that flag is set, jinx is skipped even if it would otherwise +be feasible, and the resolver falls through to flyspell." + (when (eq backpack--spellchecking-backend 'unresolved) + (setq backpack--spellchecking-backend + (cond + ((and (not (gearp! :checkers spellchecking -jinx)) + (backpack--spellchecking-jinx-feasible-p)) + 'jinx) + ((backpack--spellchecking-hunspell-feasible-p) + 'flyspell) + (t 'none))) + (message "Backpack spellchecking: backend=%s" + backpack--spellchecking-backend)) + backpack--spellchecking-backend) + +;;; Backends (leaf jinx - :doc "a fast just-in-time spell-checker" + :doc "a fast just-in-time spell-checker (Enchant-backed). + +Activates automatically when a C compiler and Enchant development +headers are available. On machines where that toolchain is not +installable (e.g. corporate Windows laptops), the spellchecking +gear falls back to `flyspell' + `hunspell' -- see the flyspell +leaf below. Add the `-jinx' flag to force the fallback even +where jinx would work: `(gear! :checkers (spellchecking -jinx))'." :ensure (jinx :ref "55c844066601ec1a4741ff8f459ff6321f6935b7") - :when (gearp! :checkers spellchecking) + :when (and (gearp! :checkers spellchecking) + (eq (backpack-spellchecking-backend) 'jinx)) + :doctor (("cc" . "C compiler for jinx's dynamic module (gcc/clang/cc)") + ("pkg-config" . "locates Enchant development headers")) :hook ((text-mode-hook prog-mode-hook) . jinx-mode) :bind ("C-c ." . jinx-correct) :custom (jinx-camel-modes . '(prog-mode)) (jinx-delay . 0.01)) + +(leaf flyspell + :doc "built-in spell-checker fallback driving hunspell directly. + +Activates automatically when jinx is not feasible on this machine +but `hunspell' is on PATH with at least one dictionary installed. +Chocolatey's `hunspell.portable' package ships the binary without +dictionaries -- drop `en_US.aff' and `en_US.dic' (from LibreOffice +or Firefox dictionary extensions) next to `hunspell.exe', or set +the `DICPATH' environment variable to a folder containing them, +before launching Emacs." + :when (and (gearp! :checkers spellchecking) + (eq (backpack-spellchecking-backend) 'flyspell)) + :doctor ("hunspell" . "spell-check backend (on Windows: `choco install hunspell.portable')") + :hook + (text-mode-hook . flyspell-mode) + (prog-mode-hook . flyspell-prog-mode) + ;; `flyspell-correct-word-before-point' is a safe no-op when + ;; `flyspell-mode' is off, so a global binding is harmless and + ;; keeps C-c . as "correct the nearest misspelling" regardless + ;; of which backend resolved. It mirrors the jinx leaf above. + :bind ("C-c ." . flyspell-correct-word-before-point) + :custom + ;; We resolved feasibility with `executable-find' above, so leaving + ;; this as the bare name lets ispell re-resolve against `exec-path' + ;; at runtime -- which is also what ispell expects. + (ispell-program-name . "hunspell") + (ispell-really-hunspell . t) + (ispell-dictionary . "en_US")) From 48c7c76e6385049a1d4bc1de96fef1a82a96cd5e Mon Sep 17 00:00:00 2001 From: Jorge Javier Araya Navarro Date: Mon, 11 May 2026 08:59:27 -0600 Subject: [PATCH 4/6] fix: Remove (repeated) paragraph in Readme --- README.md | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/README.md b/README.md index 23a6dec..efa7450 100644 --- a/README.md +++ b/README.md @@ -29,24 +29,6 @@ git clone --recurse-submodules https://jardin.jorgearaya.dev/zecRiYjpjFnZWFhygfV git clone --recurse-submodules https://github.com/shackra/backpack.git ~/.emacs.d ``` -On Windows, use `backpack.cmd` instead of `backpack`: - -``` -backpack.cmd ensure -backpack.cmd gc --dry-run -``` - -Make sure `emacs.exe` is on your PATH. - -On Windows, use `backpack.cmd` instead of `backpack`: - -``` -backpack.cmd ensure -backpack.cmd gc --dry-run -``` - -Make sure `emacs.exe` is on your PATH. - ### Writing your initial configuration > [!IMPORTANT] From 15145e6b7922c5b1083b759d9d5293a6688255cb Mon Sep 17 00:00:00 2001 From: Jorge Javier Araya Navarro Date: Sat, 16 May 2026 12:54:34 -0600 Subject: [PATCH 5/6] perf(windows): add 5 missing performance hacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - gc-cons-threshold 64 MiB (16 MiB too aggressive for LSP bursts) - w32-disable-abort-dialog (prevent zombie crash dialogs) - create-lockfiles nil (NTFS symlink emulation overhead) - jit-lock-defer-time 0.05 (reduce font rendering stutter) - global-so-long-mode (long-line protection, 2-3× slower on Windows) --- early-init.el | 10 +++++++++- lisp/gears/os/windows.el | 22 +++++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/early-init.el b/early-init.el index 8652dfc..676a5a4 100644 --- a/early-init.el +++ b/early-init.el @@ -15,6 +15,14 @@ (load backpack nil nil nil t)) (setq user-init-file (expand-file-name "early-init" user-emacs-directory)) (setq load-prefer-newer t) - (setq gc-cons-threshold (* 16 1024 1024)))) + (if backpack--system-windows-p + ;; ─── Garbage collection ─── + ;; Windows pipe/IPC buffers are large (256 KB + 1 MiB read-max); + ;; the default 16 MiB post-init threshold triggers GC too often + ;; during LSP indexing bursts. 64 MiB is a good balance. + ;; See: https://emacs-lsp.github.io/lsp-mode/page/performance/ + ;; https://emacsredux.com/blog/2025/03/28/speed-up-emacs-startup-by-tweaking-the-gc-settings/ + (setq gc-cons-threshold (* 64 1024 1024)) + (setq gc-cons-threshold (* 16 1024 1024))))) (backpack-start (not noninteractive)) diff --git a/lisp/gears/os/windows.el b/lisp/gears/os/windows.el index e12650e..cc1d58a 100644 --- a/lisp/gears/os/windows.el +++ b/lisp/gears/os/windows.el @@ -31,6 +31,11 @@ Lisp-level setting in this gear combined." (setq-default process-connection-type nil) ;; LSP-mode and eglot both recommend >= 1 MiB on Windows. (setq read-process-output-max (* 1024 1024)) + ;; Suppress Windows abort dialog — prevents zombie dialogs from + ;; blocking the process on async native-comp crashes. + ;; See: https://lists.defectivebydesign.org/archive/html/emacs-diffs/2022-12/msg00541.html + (when (boundp 'w32-disable-abort-dialog) + (setq w32-disable-abort-dialog t)) ;; ─── File I/O ─── ;; NTFS stat is 5-10× slower; skip owner/mode lookups for local @@ -41,6 +46,12 @@ Lisp-level setting in this gear combined." (setq w32-get-true-file-attributes 'local)) (when (boundp 'w32-get-true-file-link-count) (setq w32-get-true-file-link-count nil)) + ;; NTFS lock files = extra stat + symlink emulation per save; + ;; rarely useful on single-user Windows machines. + ;; Opt back in with: (gear! :os (windows lockfiles)) + ;; See: https://www.gnu.org/software/emacs/manual/html_node/elisp/File-Locks.html + (unless (gearp! :os windows lockfiles) + (setq create-lockfiles nil)) ;; ─── Display / rendering ─── ;; Windows font rendering (Uniscribe/DirectWrite) is expensive; @@ -49,6 +60,10 @@ Lisp-level setting in this gear combined." (setq fast-but-imprecise-scrolling t) (when (boundp 'redisplay-skip-fontification-on-input) (setq redisplay-skip-fontification-on-input t)) + ;; Defer fontification 50ms while input pending — complements + ;; redisplay-skip-fontification-on-input at the JIT-lock level. + ;; See: https://www.emacswiki.org/emacs/FontLockSpeed + (setq jit-lock-defer-time 0.05) (setq cursor-in-non-selected-windows nil highlight-nonselected-windows nil) ;; For LTR-only users: skip bidi algorithm entirely @@ -59,6 +74,11 @@ Lisp-level setting in this gear combined." bidi-inhibit-bpa t)) ;; Case-insensitive FS: no point re-scanning auto-mode-alist (setq auto-mode-case-fold nil) + ;; Long-line protection — Windows rendering is 2-3× slower on + ;; long lines (minified JS/JSON, log files). + ;; See: https://200ok.ch/posts/2020-09-29_comprehensive_guide_on_handling_long_lines_in_emacs.html + (when (fboundp 'global-so-long-mode) + (global-so-long-mode 1)) ;; ─── Native compilation ─── ;; Windows I/O is slower; defer async compilation to reduce contention @@ -117,4 +137,4 @@ cannot signal a void-variable error during gear initialization." ;; badly with pipe IPC and starves large bursts; force it off so ;; `read-process-output-max' / `w32-pipe-buffer-size' can do their job. (when (boundp 'process-adaptive-read-buffering) - (setq process-adaptive-read-buffering nil))) \ No newline at end of file + (setq process-adaptive-read-buffering nil))) From 5a9fa35209cc7ea79c74246be52cf5208e039a84 Mon Sep 17 00:00:00 2001 From: Jorge Javier Araya Navarro Date: Sat, 16 May 2026 12:59:55 -0600 Subject: [PATCH 6/6] fix: Turning on support for Windows makes sense only if the user is on Windows --- lisp/gears/os/windows.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/gears/os/windows.el b/lisp/gears/os/windows.el index cc1d58a..1088c17 100644 --- a/lisp/gears/os/windows.el +++ b/lisp/gears/os/windows.el @@ -1,6 +1,6 @@ ;; -*- lexical-binding: t; -*- (leaf windows - :when (gearp! :os windows) + :when (and (gearp! :os windows) backpack--system-windows-p) :doc "Performance optimizations for native Windows. For maximum throughput, also add an exclusion in Windows Defender for @@ -99,7 +99,7 @@ Lisp-level setting in this gear combined." shell-command-switch "-c")))) (leaf windows-legacy - :when (gearp! :os windows) + :when (and (gearp! :os windows) backpack--system-windows-p) :emacs< "30.1" :doc "Defensive Windows perf settings for Emacs 29.x.