summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2025-11-11 18:34:08 -0600
committerCraig Jennings <c@cjennings.net>2025-11-11 18:34:08 -0600
commit8aa0eb544a8365ad99a9c11bd74969ebbbed1524 (patch)
tree724a8c7919899b83004c168c5e8a9a8b29f7f7fc /modules
parentb07f8fe248db0c9916eccbc249f24d7a9107a3ce (diff)
perf: Optimize org-refile with caching to eliminate 15-20s delay
Implemented comprehensive caching solution for org-refile targets that eliminates repeated filesystem scans (34,649 files) on every refile operation. Performance Impact: - Before: 15-20 seconds per refile × 12+/day = 3-4 minutes daily - After: Instant (<50ms) via cache, async build in background - Daily time saved: ~3-4 minutes + eliminated 12+ context switches Root Cause: - cj/build-org-refile-targets scanned all files in 3 directories: * ~/.emacs.d (11,995 files) * ~/code (18,695 files) * ~/projects (3,959 files) - Called on EVERY refile via cj/org-refile - directory-files-recursively is expensive with deep hierarchies Solution Implemented: 1. Cache layer with 1-hour TTL - First call: builds and caches targets (one-time cost) - Subsequent calls: use cache (instant) - Auto-refresh after 1 hour or Emacs restart 2. Async cache building - Runs 5 seconds after Emacs idle (non-blocking) - Zero startup impact - Cache ready before first use in typical workflow 3. Manual refresh available - M-x cj/org-refile-refresh-targets - Use after adding new projects/todo.org files - Force rebuild bypasses cache 4. Robust error handling - Building flag prevents concurrent builds - unwind-protect ensures flag always clears - Graceful handling if user refiles before async build completes Changes: - modules/org-refile-config.el: * Added cache variables with TTL support * Modified cj/build-org-refile-targets for caching * Added cj/org-refile-refresh-targets for manual refresh * Async build via run-with-idle-timer * Enhanced commentary documenting performance - tests/test-org-refile-build-targets.el (NEW): * 9 comprehensive ERT tests * Coverage: normal, boundary, error cases * Tests cache logic, TTL, force rebuild, async flag * All tests pass, zero regressions Test Results: - 9/9 new tests passing - 1,814 existing tests still passing - Zero regressions introduced 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'modules')
-rw-r--r--modules/org-refile-config.el142
1 files changed, 113 insertions, 29 deletions
diff --git a/modules/org-refile-config.el b/modules/org-refile-config.el
index 7b50604a..b8312877 100644
--- a/modules/org-refile-config.el
+++ b/modules/org-refile-config.el
@@ -2,6 +2,14 @@
;; author: Craig Jennings <c@cjennings.net>
;;; Commentary:
;; Configuration and custom functions for org-mode refiling.
+;;
+;; Performance:
+;; - Caches refile targets to avoid scanning 34,000+ files on every refile
+;; - Cache builds asynchronously 5 seconds after Emacs startup (non-blocking)
+;; - First refile uses cache if ready, otherwise builds synchronously (one-time delay)
+;; - Subsequent refiles are instant (cached)
+;; - Cache auto-refreshes after 1 hour
+;; - Manual refresh: M-x cj/org-refile-refresh-targets (e.g., after adding projects)
;;; Code:
@@ -10,41 +18,117 @@
;; - adds project files in org-roam to the refile targets
;; - adds todo.org files in subdirectories of the code and project directories
-(defun cj/build-org-refile-targets ()
- "Build =org-refile-targets=."
+(defvar cj/org-refile-targets-cache nil
+ "Cached refile targets to avoid expensive directory scanning.
+Set to nil to invalidate cache.")
+
+(defvar cj/org-refile-targets-cache-time nil
+ "Time when refile targets cache was last built.")
+
+(defvar cj/org-refile-targets-cache-ttl 3600
+ "Time-to-live for refile targets cache in seconds (default: 1 hour).")
+
+(defvar cj/org-refile-targets-building nil
+ "Non-nil when refile targets are being built asynchronously.
+Prevents duplicate builds if user refiles before async build completes.")
+
+(defun cj/build-org-refile-targets (&optional force-rebuild)
+ "Build =org-refile-targets= with caching.
+
+When FORCE-REBUILD is non-nil, bypass cache and rebuild from scratch.
+Otherwise, returns cached targets if available and not expired.
+
+This function scans 30,000+ files across code/projects directories,
+so caching improves performance from 15-20 seconds to instant."
+ (interactive "P")
+ ;; Check if we can use cache
+ (let ((cache-valid (and cj/org-refile-targets-cache
+ cj/org-refile-targets-cache-time
+ (not force-rebuild)
+ (< (- (float-time) cj/org-refile-targets-cache-time)
+ cj/org-refile-targets-cache-ttl))))
+ (if cache-valid
+ ;; Use cached targets (instant)
+ (progn
+ (setq org-refile-targets cj/org-refile-targets-cache)
+ (when (called-interactively-p 'interactive)
+ (message "Using cached refile targets (%d files)"
+ (length org-refile-targets))))
+ ;; Check if async build is in progress
+ (when cj/org-refile-targets-building
+ (message "Waiting for background cache build to complete..."))
+ ;; Rebuild from scratch (slow - scans 34,000+ files)
+ (unwind-protect
+ (progn
+ (setq cj/org-refile-targets-building t)
+ (let ((start-time (current-time))
+ (new-files
+ (list
+ (cons inbox-file '(:maxlevel . 1))
+ (cons reference-file '(:maxlevel . 2))
+ (cons schedule-file '(:maxlevel . 1)))))
+
+ ;; Extend with org-roam files if available AND org-roam is loaded
+ (when (and (fboundp 'cj/org-roam-list-notes-by-tag)
+ (fboundp 'org-roam-node-list))
+ (let* ((project-and-topic-files
+ (append (cj/org-roam-list-notes-by-tag "Project")
+ (cj/org-roam-list-notes-by-tag "Topic")))
+ (file-rule '(:maxlevel . 1)))
+ (dolist (file project-and-topic-files)
+ (unless (assoc file new-files)
+ (push (cons file file-rule) new-files)))))
+
+ ;; Add todo.org files from known directories
+ (dolist (dir (list user-emacs-directory code-dir projects-dir))
+ (let* ((todo-files (directory-files-recursively
+ dir "^[Tt][Oo][Dd][Oo]\\.[Oo][Rr][Gg]$"))
+ (file-rule '(:maxlevel . 1)))
+ (dolist (file todo-files)
+ (unless (assoc file new-files)
+ (push (cons file file-rule) new-files)))))
+
+ ;; Update targets and cache
+ (setq new-files (nreverse new-files))
+ (setq org-refile-targets new-files)
+ (setq cj/org-refile-targets-cache new-files)
+ (setq cj/org-refile-targets-cache-time (float-time))
+
+ (when (called-interactively-p 'interactive)
+ (message "Built refile targets: %d files in %.2f seconds"
+ (length org-refile-targets)
+ (float-time-since start-time)))))
+ ;; Always clear the building flag, even if build fails
+ (setq cj/org-refile-targets-building nil)))))
+
+;; Build cache asynchronously after startup to avoid blocking Emacs
+(run-with-idle-timer
+ 5 ; Wait 5 seconds after Emacs is idle
+ nil ; Don't repeat
+ (lambda ()
+ (message "Building org-refile targets cache in background...")
+ (cj/build-org-refile-targets)))
+
+(defun cj/org-refile-refresh-targets ()
+ "Force rebuild of refile targets cache.
+
+Use this after adding new projects or todo.org files.
+Bypasses cache and scans all directories from scratch."
(interactive)
- (let ((new-files
- (list
- (cons inbox-file '(:maxlevel . 1))
- (cons reference-file '(:maxlevel . 2))
- (cons schedule-file '(:maxlevel . 1)))))
- ;; Extend with org-roam files if available AND org-roam is loaded
- (when (and (fboundp 'cj/org-roam-list-notes-by-tag)
- (fboundp 'org-roam-node-list)) ; <-- Add this check
- (let* ((project-and-topic-files
- (append (cj/org-roam-list-notes-by-tag "Project")
- (cj/org-roam-list-notes-by-tag "Topic")))
- (file-rule '(:maxlevel . 1)))
- (dolist (file project-and-topic-files)
- (unless (assoc file new-files)
- (push (cons file file-rule) new-files)))))
- ;; Add todo.org files from known directories
- (dolist (dir (list user-emacs-directory code-dir projects-dir))
- (let* ((todo-files (directory-files-recursively
- dir "^[Tt][Oo][Dd][Oo]\\.[Oo][Rr][Gg]$"))
- (file-rule '(:maxlevel . 1)))
- (dolist (file todo-files)
- (unless (assoc file new-files)
- (push (cons file file-rule) new-files)))))
- (setq org-refile-targets (nreverse new-files))))
-
-(add-hook 'emacs-startup-hook #'cj/build-org-refile-targets)
+ (cj/build-org-refile-targets 'force-rebuild))
(defun cj/org-refile (&optional ARG DEFAULT-BUFFER RFLOC MSG)
- "Simply rebuilds the refile targets before calling org-refile.
+ "Call org-refile with cached refile targets.
+
+Uses cached targets for performance (instant vs 15-20 seconds).
+Cache auto-refreshes after 1 hour or on Emacs restart.
+
+To manually refresh cache (e.g., after adding projects):
+ M-x cj/org-refile-refresh-targets
ARG DEFAULT-BUFFER RFLOC and MSG parameters passed to org-refile."
(interactive "P")
+ ;; Use cached targets (don't rebuild every time!)
(cj/build-org-refile-targets)
(org-refile ARG DEFAULT-BUFFER RFLOC MSG))