fix(windows): reduce hook callback latency to prevent mouse drag lag#86
fix(windows): reduce hook callback latency to prevent mouse drag lag#86debugtheworldbot merged 3 commits intomainfrom
Conversation
There was a problem hiding this comment.
Code Review
This pull request implements a process information cache in ActiveWindowManager to minimize expensive lookups during window switches and refactors InputMonitorService for clarity. A review comment suggests improving the cache eviction logic by removing the oldest entry rather than clearing the entire cache to maintain consistent performance.
| return new ActiveAppInfo(processName, displayName, windowTitle, processId, windowHandle); | ||
| if (_processCache.Count >= MaxProcessCacheSize) | ||
| { | ||
| _processCache.Clear(); |
There was a problem hiding this comment.
The current cache eviction strategy of clearing the entire cache is suboptimal. When the cache is full, this causes a performance spike as all cached items are lost and need to be recreated. A better approach is to evict only one item. Assuming a modern .NET runtime where Dictionary<TKey, TValue> preserves insertion order, you can remove the oldest entry by removing the first key.
_processCache.Remove(_processCache.Keys.First());…oval Three root causes addressed: 1. Hook on UI thread: Low-level hooks (WH_MOUSE_LL / WH_KEYBOARD_LL) were installed on the WPF UI thread. Any UI stall (layout, GC, Dispatcher.Invoke) would delay the hook callback, eventually causing Windows to skip or silently remove the hook. Now hooks run on a dedicated background thread with its own message loop. 2. Expensive work in hook callback: GetActiveAppInfo() called Process.GetProcessById + FileVersionInfo synchronously in the hook callback. Now only lightweight hWnd + processId capture happens in the callback; full resolution is deferred to ThreadPool via the new ActiveWindowManager.ResolveAppInfo() method. Process-level cache further reduces cold-miss cost. 3. No recovery from silent hook removal: Windows can silently stop delivering events to a timed-out hook, with no notification. Added a watchdog timer that monitors cursor position vs hook activity and automatically reinstalls hooks when they appear dead. Amp-Thread-ID: https://ampcode.com/threads/T-019d37a1-587a-77ab-bdea-b9e2048057d6 Co-authored-by: Amp <amp@ampcode.com>
ab5328f to
7aaaecc
Compare
Problem
Users report mouse drag becoming unresponsive/laggy after using KeyStats for a while on Windows 11 with high polling-rate mice (e.g. Razer DeathAdder V3).
Root Cause
ActiveWindowManager.GetActiveAppInfo()can be expensive on cache miss — it callsProcess.GetProcessById()+FileVersionInfowhich involve cross-process queries and disk IO. Windows enforces a strict timeout (~200ms) onWH_MOUSE_LLlow-level hook callbacks; exceeding it causes Windows to skip or uninstall the hook, resulting in mouse drag lag.Fix
Two changes, both in the hook's hot path:
InputMonitorService.cs— KeepGetActiveAppInfo()synchronous in the hook (for correct app attribution), but defer event dispatch (Invoke) toThreadPool.ActiveWindowManager.cs— Add a process-level cache (_processCache) by process ID, soBuildAppInfo()skipsProcess.GetProcessById+FileVersionInfofor already-seen processes. Combined with the existing window-handle cache, most hook calls now resolve via fast dictionary lookups with no cross-process overhead.Changed Files
KeyStats.Windows/KeyStats/Services/InputMonitorService.csKeyStats.Windows/KeyStats/Helpers/ActiveWindowManager.cs