Skip to content

Conversation

@seflue
Copy link
Contributor

@seflue seflue commented Dec 27, 2025

Summary

This PR introduces progressive file loading to eliminate startup blocking on large org directories. Instead of parsing all files synchronously (which could freeze the editor for multiple seconds with larger orgfile collections), files now load in batches with yields between them, keeping the UI responsive.

Motivation

As a tmux user who frequently opens and closes Neovim instances, and with a large collection of org files, startup blocking was a constant friction point. As the maintainer of telescope-orgmode.nvim, I also needed a public API that enables progressive file loading - allowing plugins to show immediate results to users, even on first startup. The goal is a smooth, non-blocking experience throughout.

This PR focuses on the core loading infrastructure. Follow-up PRs for progressive agenda loading may come later.

Key improvements

  • Prioritized buffer loading: Files already open in buffers load first
  • Batched loading with yields: Remaining files load progressively with 1ms yields between batches
  • Async clock preloading: Clock headline search runs asynchronously in batches after files load, with sync fallback if accessed early
  • New plugin API: Public methods for plugins to load files progressively with callbacks

Related Issues

TODO Will be added later.

Changes

  • Add OrgFiles:scan() for fast file metadata retrieval without parsing
  • Add OrgFiles:load_progressive() with batching, yielding, and callbacks
  • Add OrgFiles:request_load() as unified, idempotent loading entry point
  • Add OrgFiles:get_clocked_headline_async() for non-blocking clock search
  • Preload clock asynchronously after files load (with sync fallback if accessed early)
  • Add public API methods that can be used by plugins:
    • scan_files()
    • load_files()
    • on_file_loaded()
    • on_files_loaded()
    • is_files_loaded()
    • get_files_progress()
  • Add :Org profiling command for performance debugging
  • Add comprehensive tests for progressive loading and clock initialization

Checklist

I confirm that I have:

  • Followed the
    Conventional Commits
    specification
    (e.g., feat: add new feature, fix: correct bug,
    docs: update documentation).
  • My PR title also follows the conventional commits specification.
  • Updated relevant documentation, if necessary.
  • Thoroughly tested my changes.
  • Added tests (if applicable) and verified existing tests pass with
    make test.
  • Checked for breaking changes and documented them, if any.

@seflue seflue force-pushed the perf/progressive-file-loading branch 2 times, most recently from b98721c to 6953f29 Compare December 28, 2025 01:07
@kristijanhusak
Copy link
Member

Thanks for the PR! A lot of things are going on here. I did a quick overview of the PR, but didn't go into the details. I'd like to avoid adding all of this if possible.
Since the emphasis seems to be on preventing the blocking of the editor, can you test if this patch with master branch does anything for your use case?

diff --git a/lua/orgmode/files/init.lua b/lua/orgmode/files/init.lua
index 93dad13..29d129a 100644
--- a/lua/orgmode/files/init.lua
+++ b/lua/orgmode/files/init.lua
@@ -70,7 +70,7 @@ function OrgFiles:load(force)
       end
       return orgfile
     end)
-  end, self:_files(true), 50):next(function()
+  end, self:_files(true), 30):next(function()
     self.load_state = 'loaded'
     return self
   end)
diff --git a/lua/orgmode/utils/promise.lua b/lua/orgmode/utils/promise.lua
index 06227b7..a3d384b 100644
--- a/lua/orgmode/utils/promise.lua
+++ b/lua/orgmode/utils/promise.lua
@@ -366,7 +366,9 @@ function Promise.map(callback, list, concurrency)
         :next(function(...)
           results[i] = ...
           processing = processing - 1
-          processNext()
+          vim.defer_fn(function()
+            processNext()
+          end, 1)
         end)
         :catch(function(...)
           reject(...)

You can tweak the number in files/init.lua to see if there are any changes if the number is bigger or smaller, just for testing purposes.

seflue added 13 commits January 25, 2026 14:01
Add progressive file loading with configurable ordering and per-file
callbacks for incremental UI updates:

- load_progressive(opts) loads files in smart order (mtime/name/custom)
- on_file_loaded callback per file with index/total for progress
- on_complete callback when all files loaded
- current_buffer_first prioritizes active buffer (default: true)
- filter option for pre-load filtering
- get_load_progress() returns current loading state

Enables consumers like telescope-orgmode to show results incrementally
as files load, improving perceived performance for large file collections.
Expose progressive loading capabilities through public Org API:
- scan_files() for fast metadata retrieval
- load_files() for progressive loading with callbacks
- on_file_loaded/on_files_loaded() for event registration
- is_files_loaded/get_files_progress() for state tracking

Adds org_async_loading config option (default: false).
Enhanced the progressive loading profiler to show batch-level metrics:
- Wall Time: cumulative time since load start
- Duration: how long each batch took to process
- Yield Gap: time between batches (during vim.defer_fn yield)
- Size: total file size per batch
- Mem Δ: Lua memory delta (shows GC activity)

Added diagnostic options:
- first_batch_size: use smaller first batch to measure init overhead
- yield_ms: configurable yield timeout (default 1ms)

This helps diagnose performance issues by showing where time is spent
during progressive file loading.
Introduce smart batching:
- load the opened buffers in the first batch
- by default load the rest in one second batch
- expose API, where caller can set batch sizes

Opened buffers are loaded always first, prioritizing them seems to be
almost always the desired behavior.
Replace inline profiling in init.lua with a standalone profiler module
that observes events. Main code now emits lightweight profiling events
via emit.profile() instead of calling profiler methods directly.

Key changes:
- Add lua/orgmode/utils/profiler.lua with display and plugin API
- Add profiling event type and emit helper
- Remove ~200 lines of profiler code from init.lua and clock
- Add profiling.enabled config option (default: false)
- Add unit tests for payload passthrough, timing, and event ordering

The event-based design means:
- Zero overhead when profiling disabled (no listener registered)
- Main code has no profiler imports
- Plugins can profile their own code via Profiler.create_session()
Replace blocking clock search with time-boxed async batches.
Uses 50ms budget per batch to keep UI responsive during navigation.
@seflue seflue force-pushed the perf/progressive-file-loading branch from 2d7f88b to 72ff28e Compare January 25, 2026 13:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants