Skip to content

Commit 4d52ba5

Browse files
committed
fix: taskbar widget UIA polling causes high CPU (Closes #827)
- Stop blocking the UI thread with Wait() on taskbar UIA queries - Single-flight background queries with a 3s rect cache - Back off proportionally (up to 60s) when a query indicates the taskbar UIA tree is in a slow state
1 parent c0d4717 commit 4d52ba5

1 file changed

Lines changed: 100 additions & 89 deletions

File tree

‎FluentFlyoutWPF/Windows/TaskbarWindow.xaml.cs‎

Lines changed: 100 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using FluentFlyoutWPF.Classes;
77
using FluentFlyoutWPF.Classes.Utils;
88
using System.Runtime.InteropServices;
9+
using System.Diagnostics;
910
using System.Text;
1011
using System.Windows;
1112
using System.Windows.Automation;
@@ -32,16 +33,12 @@ public partial class TaskbarWindow : Window
3233
private readonly double _scale = 0.9;
3334

3435
private IntPtr _trayHandle;
35-
private AutomationElement? _widgetElement;
36-
private AutomationElement? _trayElement;
37-
private AutomationElement? _taskbarFrameElement;
3836
// reference to main window for flyout functions
3937
private MainWindow? _mainWindow;
4038
private int _lastSelectedMonitor = -1;
4139
private IntPtr _lastTaskbarHandle;
4240
private bool _positionUpdateInProgress;
4341
private bool _isClosing;
44-
private readonly Dictionary<string, Task> _pendingAutomationTasks = [];
4542

4643
private GlobalSystemMediaTransportControlsSessionPlaybackStatus? _lastPlaybackStatus;
4744
private DispatcherTimer? _autoHideTimer;
@@ -360,10 +357,7 @@ private void ResetTaskbarCachesIfHandleChanged(IntPtr taskbarHandle)
360357

361358
_lastTaskbarHandle = taskbarHandle;
362359
_trayHandle = IntPtr.Zero;
363-
_widgetElement = null;
364-
_trayElement = null;
365-
_taskbarFrameElement = null;
366-
_pendingAutomationTasks.Clear();
360+
_xamlElementStates.Clear();
367361
}
368362

369363
private void CalculateAndSetPosition(IntPtr taskbarHandle, IntPtr taskbarWindowHandle, bool isMainTaskbarSelected)
@@ -776,101 +770,121 @@ public void RefreshAppVolumeTooltip()
776770
Widget.RefreshAppVolumeTooltip();
777771
}
778772

779-
private (bool, Rect) GetTaskbarXamlElementRect(IntPtr taskbarHandle, ref AutomationElement? elementCache, string elementName)
773+
/// <summary>
774+
/// Per-element cache for taskbar XAML element rects queried via UI Automation.
775+
/// All queries run on a background thread; the UI thread never blocks on them.
776+
/// </summary>
777+
private sealed class XamlElementState
778+
{
779+
public AutomationElement? Element;
780+
public Rect Rect = Rect.Empty;
781+
public bool HasRect;
782+
public int QueryInFlight; // 0/1 - Interlocked
783+
public DateTime LastQueryCompleted = DateTime.MinValue;
784+
public long QueryDurationMs;
785+
}
786+
787+
private readonly Dictionary<string, XamlElementState> _xamlElementStates = new();
788+
789+
/// <summary>
790+
/// Returns the cached bounding rect of a taskbar XAML element, refreshing it in the
791+
/// background when stale. Never blocks the caller; if no rect is available yet the
792+
/// caller falls back to its default positioning. The stale-limit keeps the rect fresh
793+
/// without querying the taskbar's UI Automation tree on every timer tick.
794+
/// </summary>
795+
private (bool, Rect) GetTaskbarXamlElementRect(IntPtr taskbarHandle, string elementName)
780796
{
781797
if (taskbarHandle == IntPtr.Zero)
782798
return (false, Rect.Empty);
783799

784-
try
800+
if (!_xamlElementStates.TryGetValue(elementName, out var state))
801+
_xamlElementStates[elementName] = state = new XamlElementState();
802+
803+
// reset if monitor changed
804+
if (_lastSelectedMonitor != SettingsManager.Current.TaskbarWidgetSelectedMonitor)
785805
{
786-
// reset if monitor changed
787-
if (_lastSelectedMonitor != SettingsManager.Current.TaskbarWidgetSelectedMonitor)
788-
elementCache = null;
806+
state.HasRect = false;
807+
state.Element = null;
808+
}
789809

790-
// find widget in XAML
791-
if (elementCache == null)
792-
{
793-
if (_pendingAutomationTasks.TryGetValue(elementName, out var pendingTask) && !pendingTask.IsCompleted)
794-
return (false, Rect.Empty);
810+
bool needsRefresh = !state.HasRect
811+
|| DateTime.Now.Subtract(state.LastQueryCompleted) > TimeSpan.FromSeconds(3);
795812

796-
AutomationElement? found = null;
797-
var findTask = Task.Run(() =>
798-
{
799-
var root = AutomationElement.FromHandle(taskbarHandle);
800-
found = root.FindFirst(TreeScope.Descendants,
801-
new PropertyCondition(AutomationElement.AutomationIdProperty, elementName));
802-
});
803-
_pendingAutomationTasks[elementName] = findTask;
813+
// if the last query took a long time, the taskbar's UIA tree is in a slow state -
814+
// back off proportionally so a creeping walk does not keep consuming a core
815+
long backoffMs = state.QueryDurationMs > 300
816+
? Math.Min(state.QueryDurationMs * 10, 60000)
817+
: 3000;
818+
bool inBackoff = DateTime.Now.Subtract(state.LastQueryCompleted).TotalMilliseconds < backoffMs;
804819

805-
if (!findTask.Wait(1000))
806-
{
807-
Logger.Warn("Timeout querying taskbar XAML element: " + elementName);
808-
return (false, Rect.Empty);
809-
}
820+
if (inBackoff)
821+
return state.HasRect ? (true, state.Rect) : (false, Rect.Empty);
810822

811-
// Propagate any exception from the background thread
812-
findTask.GetAwaiter().GetResult();
813-
elementCache = found;
814-
}
823+
if (needsRefresh
824+
&& Interlocked.CompareExchange(ref state.QueryInFlight, 1, 0) == 0)
825+
{
826+
StartXamlElementQuery(taskbarHandle, elementName, state);
827+
}
815828

816-
if (elementCache == null) // widget most likely disabled
817-
return (false, Rect.Empty);
829+
return state.HasRect ? (true, state.Rect) : (false, Rect.Empty);
830+
}
818831

832+
private void StartXamlElementQuery(IntPtr taskbarHandle, string elementName, XamlElementState state)
833+
{
834+
Task.Run(() =>
835+
{
836+
var sw = Stopwatch.StartNew();
837+
Rect rect = Rect.Empty;
838+
AutomationElement? found = null;
819839
try
820840
{
821-
if (_pendingAutomationTasks.TryGetValue(elementName, out var pendingTask) && !pendingTask.IsCompleted)
841+
var root = AutomationElement.FromHandle(taskbarHandle);
842+
found = root.FindFirst(TreeScope.Descendants,
843+
new PropertyCondition(AutomationElement.AutomationIdProperty, elementName));
844+
if (found != null)
822845
{
823-
elementCache = null;
824-
return (false, Rect.Empty);
846+
try
847+
{
848+
rect = found.Current.BoundingRectangle;
849+
}
850+
catch (ElementNotAvailableException)
851+
{
852+
found = null;
853+
}
825854
}
855+
}
856+
catch (Exception ex)
857+
{
858+
Logger.Warn(ex, "Error querying taskbar XAML element: " + elementName);
859+
}
860+
long durationMs = sw.ElapsedMilliseconds;
826861

827-
var cachedElement = elementCache;
828-
var boundsTask = Task.Run(() => cachedElement.Current.BoundingRectangle);
829-
_pendingAutomationTasks[elementName] = boundsTask;
830-
831-
if (!boundsTask.Wait(500))
862+
// complete the query on the UI thread
863+
Dispatcher.BeginInvoke(() =>
864+
{
865+
if (found == null || rect == Rect.Empty)
832866
{
833-
Logger.Warn("Timeout getting bounds for taskbar XAML element: " + elementName);
834-
elementCache = null;
835-
return (false, Rect.Empty);
867+
// element is gone/disabled; drop the cache and back off before retrying
868+
state.HasRect = false;
869+
state.Element = null;
870+
state.LastQueryCompleted = DateTime.Now;
836871
}
837-
838-
Rect elementRect = boundsTask.GetAwaiter().GetResult();
839-
840-
if (elementRect == Rect.Empty) // widget shown before but most likely disabled now
872+
else
841873
{
842-
elementCache = null; // reset cache
843-
return (false, Rect.Empty);
874+
// no change -> keep existing element reference to avoid stale handles
875+
state.Element = found;
876+
state.Rect = rect;
877+
state.HasRect = true;
878+
state.LastQueryCompleted = DateTime.Now;
844879
}
845880

846-
return (true, elementRect);
847-
}
848-
catch (ElementNotAvailableException)
849-
{
850-
// element became stale, reset cache
851-
Logger.Warn("Taskbar XAML element became stale, resetting cache: " + elementName);
852-
elementCache = null;
853-
return (false, Rect.Empty);
854-
}
855-
}
856-
catch (COMException ex)
857-
{
858-
Logger.Warn(ex, "COM error retrieving taskbar XAML element Rect: " + elementName);
859-
elementCache = null; // reset cache on error
860-
return (false, Rect.Empty);
861-
}
862-
catch (ElementNotAvailableException)
863-
{
864-
Logger.Warn("Taskbar XAML element not available, resetting cache: " + elementName);
865-
elementCache = null;
866-
return (false, Rect.Empty);
867-
}
868-
catch (Exception ex)
869-
{
870-
Logger.Error(ex, "Error retrieving taskbar XAML element Rect: " + elementName);
871-
elementCache = null; // reset cache on error
872-
return (false, Rect.Empty);
873-
}
881+
state.QueryDurationMs = durationMs;
882+
Interlocked.Exchange(ref state.QueryInFlight, 0);
883+
884+
// apply the refreshed position now that the rect may have changed
885+
UpdatePosition();
886+
}, DispatcherPriority.Background);
887+
});
874888
}
875889

876890
/// <summary>
@@ -881,17 +895,17 @@ public void RefreshAppVolumeTooltip()
881895
/// <see cref="Rect.Empty"/> if not found.</returns>
882896
private (bool, Rect) GetTaskbarWidgetRect(IntPtr taskbarHandle)
883897
{
884-
return GetTaskbarXamlElementRect(taskbarHandle, ref _widgetElement, "WidgetsButton");
898+
return GetTaskbarXamlElementRect(taskbarHandle, "WidgetsButton");
885899
}
886900

887901
private (bool, Rect) GetSystemTrayRect(IntPtr taskbarHandle)
888902
{
889-
return GetTaskbarXamlElementRect(taskbarHandle, ref _trayElement, "SystemTrayIcon");
903+
return GetTaskbarXamlElementRect(taskbarHandle, "SystemTrayIcon");
890904
}
891905

892906
private (bool, Rect) GetTaskbarFrameRect(IntPtr taskbarHandle)
893907
{
894-
return GetTaskbarXamlElementRect(taskbarHandle, ref _taskbarFrameElement, "TaskbarFrame");
908+
return GetTaskbarXamlElementRect(taskbarHandle, "TaskbarFrame");
895909
}
896910

897911
protected override void OnClosed(EventArgs e)
@@ -900,10 +914,7 @@ protected override void OnClosed(EventArgs e)
900914
_timer.Stop();
901915
_autoHideTimer?.Stop();
902916
_autoHideTimer = null;
903-
_widgetElement = null;
904-
_trayElement = null;
905-
_taskbarFrameElement = null;
906-
_pendingAutomationTasks.Clear();
917+
_xamlElementStates.Clear();
907918
base.OnClosed(e);
908919
}
909920
}

0 commit comments

Comments
 (0)