Skip to content

fix(windows): reduce hook callback latency to prevent mouse drag lag#86

Merged
debugtheworldbot merged 3 commits intomainfrom
fix/windows-mouse-hook-perf
Mar 29, 2026
Merged

fix(windows): reduce hook callback latency to prevent mouse drag lag#86
debugtheworldbot merged 3 commits intomainfrom
fix/windows-mouse-hook-perf

Conversation

@debugtheworldbot
Copy link
Copy Markdown
Owner

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 calls Process.GetProcessById() + FileVersionInfo which involve cross-process queries and disk IO. Windows enforces a strict timeout (~200ms) on WH_MOUSE_LL low-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:

  1. InputMonitorService.cs — Keep GetActiveAppInfo() synchronous in the hook (for correct app attribution), but defer event dispatch (Invoke) to ThreadPool.

  2. ActiveWindowManager.cs — Add a process-level cache (_processCache) by process ID, so BuildAppInfo() skips Process.GetProcessById + FileVersionInfo for 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.cs
  • KeyStats.Windows/KeyStats/Helpers/ActiveWindowManager.cs

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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>
@debugtheworldbot debugtheworldbot force-pushed the fix/windows-mouse-hook-perf branch from ab5328f to 7aaaecc Compare March 29, 2026 03:58
@debugtheworldbot debugtheworldbot merged commit 222b0da into main Mar 29, 2026
@debugtheworldbot debugtheworldbot deleted the fix/windows-mouse-hook-perf branch March 29, 2026 09:08
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.

1 participant