< Summary

Information
Class: SwitchBlade.Services.WindowReconciler
Assembly: SwitchBlade
File(s): D:\a\switchblade\switchblade\Services\WindowReconciler.cs
Tag: 203_23722840422
Line coverage
100%
Covered lines: 147
Uncovered lines: 0
Coverable lines: 147
Total lines: 231
Line coverage: 100%
Branch coverage
100%
Covered branches: 60
Total branches: 60
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
Reconcile(...)100%2626100%
PopulateIcons(...)100%1616100%
AddToCache(...)100%11100%
RemoveFromCache(...)100%11100%
AddToCacheInternal(...)100%88100%
RemoveFromCacheInternal(...)100%1010100%
get_CacheCount()100%11100%
GetHwndCacheCount()100%11100%
GetProviderCacheCount()100%11100%

File(s)

D:\a\switchblade\switchblade\Services\WindowReconciler.cs

#LineLine coverage
 1using System.Collections.Generic;
 2using System.Linq;
 3using SwitchBlade.Contracts;
 4
 5namespace SwitchBlade.Services
 6{
 7    public class WindowReconciler : IWindowReconciler
 8    {
 9        private readonly IIconService? _iconService;
 7910        private readonly Dictionary<IntPtr, List<WindowItem>> _windowItemCache = new();
 11        // Performance optimization: Secondary index for O(1) provider lookups
 7912        private readonly Dictionary<IWindowProvider, HashSet<WindowItem>> _providerItems = new();
 7913        private readonly object _lock = new();
 14
 15        private readonly ILogger? _logger;
 16
 7917        public WindowReconciler(IIconService? iconService, ILogger? logger = null)
 7918        {
 7919            _iconService = iconService;
 7920            _logger = logger;
 7921        }
 22
 23        public List<WindowItem> Reconcile(IList<WindowItem> incomingItems, IWindowProvider provider)
 8224        {
 8225            lock (_lock)
 8226            {
 8227                var resolvedItems = new List<WindowItem>();
 8228                var claimedItems = new HashSet<WindowItem>();
 29
 30                // Performance optimization: O(1) lookup instead of O(N) SelectMany scan
 31                HashSet<WindowItem> unusedCacheItems;
 8232                if (_providerItems.TryGetValue(provider, out var existing))
 2433                {
 2434                    unusedCacheItems = new HashSet<WindowItem>(existing);
 2435                }
 36                else
 5837                {
 5838                    unusedCacheItems = new HashSet<WindowItem>();
 5839                }
 40
 41441                foreach (var incoming in incomingItems)
 8442                {
 8443                    WindowItem? match = null;
 44
 8445                    if (_windowItemCache.TryGetValue(incoming.Hwnd, out var candidates))
 3846                    {
 8347                        match = candidates.FirstOrDefault(w => w.Title == incoming.Title && !claimedItems.Contains(w))
 6048                             ?? candidates.FirstOrDefault(w => !claimedItems.Contains(w));
 3849                    }
 50
 8451                    if (match != null)
 3152                    {
 53                        // Remove from hash sets BEFORE mutating Title (which affects hash code)
 3154                        unusedCacheItems.Remove(match);
 55
 3156                        if (match.Source != null && _providerItems.TryGetValue(match.Source, out var providerSet))
 3057                        {
 3058                            providerSet.Remove(match);
 3059                        }
 60
 3161                        match.Title = incoming.Title;
 3162                        match.ProcessName = incoming.ProcessName;
 3163                        match.Source ??= provider;
 64
 65                        // Add back to provider sets if applicable
 3166                        if (match.Source != null)
 3167                        {
 3168                            if (!_providerItems.TryGetValue(match.Source, out providerSet))
 169                            {
 170                                providerSet = new HashSet<WindowItem>();
 171                                _providerItems[match.Source] = providerSet;
 172                            }
 3173                            providerSet.Add(match);
 3174                        }
 75
 76                        // Icon population is now async - do NOT call PopulateIconIfMissing here
 77
 3178                        resolvedItems.Add(match);
 3179                        claimedItems.Add(match);
 3180                    }
 81                    else
 5382                    {
 5383                        incoming.ResetBadgeAnimation();
 5384                        incoming.Source = provider;
 85                        // Icon population is now async - do NOT call PopulateIconIfMissing here
 86
 87                        // Lock-free internal method — we already hold _lock
 5388                        AddToCacheInternal(incoming);
 89
 5390                        resolvedItems.Add(incoming);
 5391                        claimedItems.Add(incoming);
 5392                    }
 8493                }
 94
 95                // Cleanup unused items using lock-free internal method
 26096                foreach (var unused in unusedCacheItems)
 797                {
 798                    RemoveFromCacheInternal(unused);
 799                }
 100
 82101                return resolvedItems;
 102            }
 82103        }
 104
 105        public void PopulateIcons(IEnumerable<WindowItem> items)
 32106        {
 59107            if (_iconService == null) return;
 108
 109            // No lock needed here - items are already reconciled and local to this list
 110            // Icon extraction is thread-safe and cached
 5111            int count = 0;
 5112            long start = System.Diagnostics.Stopwatch.GetTimestamp();
 113
 29114            foreach (var item in items)
 7115            {
 7116                if (item.Icon == null && !string.IsNullOrEmpty(item.ExecutablePath))
 4117                {
 118                    try
 4119                    {
 4120                        item.Icon = _iconService.GetIcon(item.ExecutablePath);
 2121                        count++;
 2122                    }
 2123                    catch (Exception ex)
 2124                    {
 2125                        _logger?.LogError($"Failed to populate icon for {item.ExecutablePath}", ex);
 2126                    }
 4127                }
 7128            }
 129
 5130            if (count > 0 && _logger != null && _logger.IsDebugEnabled)
 1131            {
 1132                var elapsed = System.Diagnostics.Stopwatch.GetElapsedTime(start);
 1133                _logger.Log($"[Perf] Populated {count} icons in {elapsed.TotalMilliseconds:F2}ms");
 1134            }
 32135        }
 136
 137        public void AddToCache(WindowItem item)
 9138        {
 9139            lock (_lock)
 9140            {
 9141                AddToCacheInternal(item);
 9142            }
 9143        }
 144
 145        public void RemoveFromCache(WindowItem item)
 4146        {
 4147            lock (_lock)
 4148            {
 4149                RemoveFromCacheInternal(item);
 4150            }
 4151        }
 152
 153        /// <summary>
 154        /// Lock-free internal method for adding to cache.
 155        /// Caller must hold _lock.
 156        /// </summary>
 157        private void AddToCacheInternal(WindowItem item)
 62158        {
 159            // 1. Update HWND lookup
 62160            if (!_windowItemCache.TryGetValue(item.Hwnd, out var list))
 54161            {
 54162                list = new List<WindowItem>();
 54163                _windowItemCache[item.Hwnd] = list;
 54164            }
 62165            if (!list.Contains(item))
 60166                list.Add(item);
 167
 168            // 2. Update Provider lookup
 62169            if (item.Source != null)
 60170            {
 60171                if (!_providerItems.TryGetValue(item.Source, out var set))
 42172                {
 42173                    set = new HashSet<WindowItem>();
 42174                    _providerItems[item.Source] = set;
 42175                }
 60176                set.Add(item);
 60177            }
 62178        }
 179
 180        /// <summary>
 181        /// Lock-free internal method for removing from cache.
 182        /// Caller must hold _lock.
 183        /// </summary>
 184        private void RemoveFromCacheInternal(WindowItem item)
 11185        {
 186            // 1. Remove from HWND lookup
 11187            if (_windowItemCache.TryGetValue(item.Hwnd, out var list))
 11188            {
 11189                list.Remove(item);
 11190                if (list.Count == 0)
 11191                    _windowItemCache.Remove(item.Hwnd);
 11192            }
 193
 194            // 2. Remove from Provider lookup
 11195            if (item.Source != null && _providerItems.TryGetValue(item.Source, out var set))
 9196            {
 9197                set.Remove(item);
 9198                if (set.Count == 0)
 3199                    _providerItems.Remove(item.Source);
 9200            }
 11201        }
 202
 203        public int CacheCount
 204        {
 205            get
 1206            {
 1207                lock (_lock)
 1208                {
 2209                    return _windowItemCache.Count + _providerItems.Values.Sum(s => s.Count);
 210                }
 1211            }
 212        }
 213
 214        public int GetHwndCacheCount()
 9215        {
 9216            lock (_lock)
 9217            {
 16218                return _windowItemCache.Values.Sum(l => l.Count);
 219            }
 9220        }
 221
 222        public int GetProviderCacheCount()
 4223        {
 4224            lock (_lock)
 4225            {
 7226                return _providerItems.Values.Sum(s => s.Count);
 227            }
 4228        }
 229
 230    }
 231}