Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 54 additions & 2 deletions KeyStats.Windows/KeyStats/Helpers/ActiveWindowManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ public static class ActiveWindowManager
private static IntPtr _lastWindowHandle = IntPtr.Zero;
private static ActiveAppInfo _lastAppInfo = ActiveAppInfo.Unknown;

// Cache process name + display name by process ID to avoid repeated
// Process.GetProcessById / FileVersionInfo lookups inside the hook callback.
private static readonly Dictionary<uint, (string ProcessName, string DisplayName)> _processCache = new();
private const int MaxProcessCacheSize = 64;

/// <summary>
/// Gets the foreground app identity for attribution and display.
/// </summary>
Expand Down Expand Up @@ -71,6 +76,39 @@ public static ActiveAppInfo GetActiveAppInfo()
}
}

/// <summary>
/// Resolves app info from a pre-captured window handle and process ID.
/// Use this when hWnd/pid were captured in a time-critical path (e.g. hook callback)
/// and the expensive resolution is deferred to a background thread.
/// </summary>
public static ActiveAppInfo ResolveAppInfo(IntPtr hWnd, uint processId)
{
if (hWnd == IntPtr.Zero || processId == 0)
{
return ActiveAppInfo.Unknown;
}

try
{
lock (_lock)
{
if (hWnd == _lastWindowHandle && _lastAppInfo.IsKnown)
{
return _lastAppInfo;
}

var appInfo = BuildAppInfo(hWnd, processId);
_lastWindowHandle = hWnd;
_lastAppInfo = appInfo;
return appInfo;
}
}
catch
{
return ActiveAppInfo.Unknown;
}
}

/// <summary>
/// Backward-compatible accessor when only process identity is needed.
/// </summary>
Expand All @@ -83,14 +121,28 @@ private static ActiveAppInfo BuildAppInfo(IntPtr windowHandle, uint processId)
{
var windowTitle = GetWindowTitle(windowHandle);

// Use cached process identity when available to avoid expensive
// Process.GetProcessById + FileVersionInfo on every window switch.
if (_processCache.TryGetValue(processId, out var cached))
{
var displayName = ResolveDisplayName(cached.ProcessName, cached.DisplayName, windowTitle);
return new ActiveAppInfo(cached.ProcessName, displayName, windowTitle, processId, windowHandle);
}

try
{
using var process = Process.GetProcessById((int)processId);
var processName = NormalizeProcessName(process.ProcessName);
var fileDisplayName = GetFileDisplayName(process, processName);
var displayName = ResolveDisplayName(processName, fileDisplayName, windowTitle);

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());

}
_processCache[processId] = (processName, fileDisplayName);

var resolvedDisplayName = ResolveDisplayName(processName, fileDisplayName, windowTitle);
return new ActiveAppInfo(processName, resolvedDisplayName, windowTitle, processId, windowHandle);
}
catch
{
Expand Down
33 changes: 33 additions & 0 deletions KeyStats.Windows/KeyStats/Helpers/NativeInterop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,39 @@ public static short LoWord(int dword)
[DllImport("user32.dll", SetLastError = true)]
public static extern int GetWindowTextLength(IntPtr hWnd);

public const int WM_QUIT = 0x0012;

[StructLayout(LayoutKind.Sequential)]
public struct MSG
{
public IntPtr hwnd;
public uint message;
public IntPtr wParam;
public IntPtr lParam;
public uint time;
public POINT pt;
}

[DllImport("user32.dll")]
public static extern int GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);

[DllImport("user32.dll")]
public static extern bool TranslateMessage(ref MSG lpMsg);

[DllImport("user32.dll")]
public static extern IntPtr DispatchMessage(ref MSG lpMsg);

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool PostThreadMessage(uint threadId, uint msg, IntPtr wParam, IntPtr lParam);

[DllImport("kernel32.dll")]
public static extern uint GetCurrentThreadId();

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetCursorPos(out POINT lpPoint);

[DllImport("dwmapi.dll", PreserveSig = true)]
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int dwAttribute, ref int pvAttribute, int cbAttribute);

Expand Down
Loading