feat(template): async hook execution - #4102
Conversation
P1 — pre_hook runs asynchronously before output generationsrc/theme/template_engine.cpp:1655-1658 runHook() is used for both preHook and postHook. For index == 0, both are enqueued The caller invokes runHook(entry.preHook) before rendering outputs, then
Only post-hooks should be dispatched asynchronously, or the pre-hook must be P1 — Superseded generations leave uncancelled hooks runningsrc/theme/template_apply_service.cpp:330-334 Once a hook is enqueued, it has no generation identifier or cancellation token. If A slow hook from generation A can therefore finish after generation B and leave B may complete first, followed by A overwriting the final state. Disable/undo The coalescing logic only coalesces ApplyRequests; it does not coalesce or cancel P1 — Built-in/community-only applications complete before hooks finishsrc/theme/template_apply_service.cpp:313-315, 330-334 The new waitIdle() call is after the user-template section. The earlier return for Built-in-only and community-only configurations still enqueue asynchronous hooks,
while those hooks are still running. This affects the normal built-in-template path and means ColorsChanged can be P2 — index > 0 is not a sequencing barriersrc/theme/template_engine.cpp:1655-1661 Entries are sorted by index, but a positive-index synchronous hook does not wait Thus an index = 1 hook can run before an earlier index = 0 hook completes. The P2 — Shutdown drains the entire queued hook backlogsrc/theme/hook_runner.cpp:64-68 Workers exit only after the queue is empty. During shutdown, all queued hooks After rapid theme changes, this can make shell exit wait for every stale hook in |
|
Thanks for the detailed thorough review! I'll push the updated implementation shortly. |
89c43ef to
5870ea4
Compare
5870ea4 to
eba56e2
Compare
|
Thanks, tweaked it a bit to my liking and added 'hook_async' |
Summary
Adds asynchronous template hook execution via a thread pool (
HookRunner) to eliminate the performance bottleneck where slow post-hooks (e.g., Spicetify'sapply) block all subsequent templates from rendering.Additionally, introduces request coalescing to avoid redundant work during rapid theme switches, and uses the existing
indexfield to allow opt-out of async behavior for templates requiring deterministic sequencing.Motivation
Applying a theme with all user templates enabled took ~9.5s because a single slow
post_hook(Spicetify) blocked the entire template queue. Templates after Spicetify (starship, zathura, zed, zen) waited ~8s for it to finish, despite being completely independent.The template engine itself is fast (~1s for 15 templates); the bottleneck was synchronous hook execution in a single-threaded worker loop.
Type of Change
Related Issue
N/A
Testing
Benchmarks
Before: upstream-builtin2.log
After: async1-builtin2.log
Builtin templates (18 templates, 19 files):
Rapid theme switching (5x in 250ms):
Benchmark scripts
I used LLM to make these benchmark scripts and revised it myself :
https://gist.github.com/oldirtty/338f2d0a4dae48b88c60748f86ec4068
builtin_template_benchmark.pyBenchmarks builtin templates (reads enabled IDs from ~/.local/state/noctalia/settings.toml)test_coalescing.shTests rapid theme switching coalescingManual Coverage
Screenshots / Videos
BEFORE
b4.mp4
AFTER
after.mp4
Checklist
CONTRIBUTING.md.just formatwith clang-format v22+ installed, or this PR has no code changes.assets/translations/en.json, or this PR adds no new user-facing strings.Additional Notes
Breaking Change
Hooks from templates with
index = 0(default) now execute asynchronously. The order of hook execution between different templates is no longer guaranteed.To opt out and force synchronous execution, set
index > 0in the template configuration. Templates withindex > 0execute hooks synchronously in index order, providing deterministic sequencing when needed.Benchmarking
output_path_dynamic(e.g., emacs, niri, hyprland) are not tracked in benchmarks