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
4 changes: 4 additions & 0 deletions PowerToys.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,10 @@
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/cmdpal/Tests/Microsoft.CmdPal.UI.UnitTests/Microsoft.CmdPal.UI.UnitTests.csproj">
Comment thread
jiripolasek marked this conversation as resolved.
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/cmdpal/Tests/Microsoft.CmdPal.UI.ViewModels.UnitTests/Microsoft.CmdPal.UI.ViewModels.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
Expand Down
8 changes: 4 additions & 4 deletions src/modules/cmdpal/CommandPalette - no UI tests.slnf
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
{
"solution": {
"path": "..\\..\\..\\PowerToys.slnx",
"projects": [
Expand All @@ -19,20 +19,20 @@
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Apps.UnitTests\\Microsoft.CmdPal.Ext.Apps.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Bookmarks.UnitTests\\Microsoft.CmdPal.Ext.Bookmarks.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Calc.UnitTests\\Microsoft.CmdPal.Ext.Calc.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests\\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests\\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Indexer.UnitTests\\Microsoft.CmdPal.Ext.Indexer.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.PerformanceMonitor.UnitTests\\Microsoft.CmdPal.Ext.PerformanceMonitor.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Registry.UnitTests\\Microsoft.CmdPal.Ext.Registry.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.WindowsSettings.UnitTests\\Microsoft.CmdPal.Ext.WindowsSettings.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests\\Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Shell.UnitTests\\Microsoft.CmdPal.Ext.Shell.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.System.UnitTests\\Microsoft.CmdPal.Ext.System.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.TimeDate.UnitTests\\Microsoft.CmdPal.Ext.TimeDate.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.UnitTestsBase\\Microsoft.CmdPal.Ext.UnitTestBase.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.WebSearch.UnitTests\\Microsoft.CmdPal.Ext.WebSearch.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.WindowWalker.UnitTests\\Microsoft.CmdPal.Ext.WindowWalker.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.WindowsSettings.UnitTests\\Microsoft.CmdPal.Ext.WindowsSettings.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.UI.UnitTests\\Microsoft.CmdPal.UI.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.UI.ViewModels.UnitTests\\Microsoft.CmdPal.UI.ViewModels.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.UITests\\Microsoft.CmdPal.UITests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CommandPalette.Extensions.Toolkit.UnitTests\\Microsoft.CommandPalette.Extensions.Toolkit.UnitTests.csproj",
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.Actions\\Microsoft.CmdPal.Ext.Actions.csproj",
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.Apps\\Microsoft.CmdPal.Ext.Apps.csproj",
Expand Down
1 change: 1 addition & 0 deletions src/modules/cmdpal/CommandPalette.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.WebSearch.UnitTests\\Microsoft.CmdPal.Ext.WebSearch.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.WindowWalker.UnitTests\\Microsoft.CmdPal.Ext.WindowWalker.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.WindowsSettings.UnitTests\\Microsoft.CmdPal.Ext.WindowsSettings.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.UI.UnitTests\\Microsoft.CmdPal.UI.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.UI.ViewModels.UnitTests\\Microsoft.CmdPal.UI.ViewModels.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.UITests\\Microsoft.CmdPal.UITests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CommandPalette.Extensions.Toolkit.UnitTests\\Microsoft.CommandPalette.Extensions.Toolkit.UnitTests.csproj",
Expand Down
32 changes: 22 additions & 10 deletions src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/AdaptiveCache`2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,15 @@ internal sealed class AdaptiveCache<TKey, TValue>
private readonly ConcurrentStack<CacheEntry> _pool = [];
private readonly WaitCallback _maintenanceCallback;

// ConcurrentDictionary.Count acquires every stripe lock. Keep an approximate count so
// cache maintenance never makes the XAML UI thread wait for all dictionary locks.
private int _entryCount;
private long _currentTick;
private long _lastDecayTicks = DateTime.UtcNow.Ticks;
private InterlockedBoolean _maintenanceSwitch = new(false);

internal int ApproximateCount => Volatile.Read(ref _entryCount);

public AdaptiveCache(int capacity = 384, TimeSpan? decayInterval = null, double decayFactor = 0.5)
{
_capacity = capacity;
Expand Down Expand Up @@ -66,7 +71,11 @@ public TValue GetOrAdd<TArg>(TKey key, Func<TKey, TArg, TValue> factory, TArg ar
var tick = Interlocked.Increment(ref _currentTick);
newEntry.Initialize(key, value, 1.0, tick);

if (!_map.TryAdd(key, newEntry))
if (_map.TryAdd(key, newEntry))
{
Interlocked.Increment(ref _entryCount);
}
else
{
newEntry.Clear();
_pool.Push(newEntry);
Expand Down Expand Up @@ -117,7 +126,11 @@ public void Add(TKey key, TValue value)

newEntry.Initialize(key, value, 1.0, tick);

if (!_map.TryAdd(key, newEntry))
if (_map.TryAdd(key, newEntry))
{
Interlocked.Increment(ref _entryCount);
}
else
{
newEntry.Clear();
_pool.Push(newEntry);
Expand All @@ -133,6 +146,7 @@ public bool TryRemove(TKey key)
{
if (_map.TryRemove(key, out var evicted))
{
Interlocked.Decrement(ref _entryCount);
evicted.Clear();
_pool.Push(evicted);
return true;
Expand All @@ -143,7 +157,9 @@ public bool TryRemove(TKey key)

public void Clear()
{
foreach (var key in _map.Keys)
// Enumerate the dictionary rather than _map.Keys: the enumerator is lock-free,
// while Keys snapshots under every stripe lock.
foreach (var (key, _) in _map)
{
TryRemove(key);
}
Expand All @@ -153,7 +169,7 @@ public void Clear()

private bool ShouldMaintenanceRun()
{
return _map.Count > _capacity || (DateTime.UtcNow.Ticks - Interlocked.Read(ref _lastDecayTicks)) > _decayInterval.Ticks;
return ApproximateCount > _capacity || (DateTime.UtcNow.Ticks - Interlocked.Read(ref _lastDecayTicks)) > _decayInterval.Ticks;
}

private void TryRunMaintenance()
Expand Down Expand Up @@ -184,13 +200,9 @@ private void PerformCleanup()

var score = CalculateScore(entry, currentTick);

if (score < 0.1 || _map.Count > _capacity)
if (score < 0.1 || ApproximateCount > _capacity)
{
if (_map.TryRemove(key, out var evicted))
{
evicted.Clear();
_pool.Push(evicted);
}
TryRemove(key);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Microsoft.CmdPal.UI.UnitTests;

[TestClass]
public class AdaptiveCacheTests
{
[TestMethod]
public void ApproximateCountChangesOnlyForSuccessfulMutations()
{
var cache = new AdaptiveCache<int, int>(capacity: 8);

cache.Add(1, 1);
cache.Add(1, 2);
_ = cache.GetOrAdd(1, static (key, _) => key, 0);

Assert.AreEqual(1, cache.ApproximateCount);
Assert.IsFalse(cache.TryRemove(2));
Assert.AreEqual(1, cache.ApproximateCount);
Assert.IsTrue(cache.TryRemove(1));
Assert.AreEqual(0, cache.ApproximateCount);

_ = cache.GetOrAdd(2, static (key, _) => key, 0);
Assert.AreEqual(1, cache.ApproximateCount);

cache.Clear();
Assert.AreEqual(0, cache.ApproximateCount);
}

[TestMethod]
[Timeout(15_000)]
public async Task ConcurrentCleanupAndFailedLoadsKeepApproximateCountConsistent()
{
const int capacity = 1;
const int workerCount = 8;
const int itemsPerWorker = 256;
var cache = new AdaptiveCache<int, Task<int>>(capacity, TimeSpan.FromHours(1));
var faultRemovals = new ConcurrentBag<Task>();

cache.Add(-2, Task.FromResult(-2));
cache.Add(-1, Task.FromResult(-1));

Assert.IsTrue(
SpinWait.SpinUntil(() => cache.ApproximateCount <= capacity, TimeSpan.FromSeconds(5)),
"Capacity cleanup did not run.");

var workers = Enumerable.Range(0, workerCount)
.Select(worker => Task.Run(() =>
{
for (var item = 0; item < itemsPerWorker; item++)
{
var key = (worker * itemsPerWorker) + item;

if (item % 4 == 0)
{
var completionSource = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
cache.Add(key, completionSource.Task);

faultRemovals.Add(completionSource.Task.ContinueWith(
completedTask =>
{
_ = completedTask.Exception;
cache.TryRemove(key);
},
CancellationToken.None,
TaskContinuationOptions.OnlyOnFaulted,
TaskScheduler.Default));

completionSource.SetException(new InvalidOperationException("Icon load failed."));
}
else
{
_ = cache.GetOrAdd(key, static (currentKey, _) => Task.FromResult(currentKey), 0);
}

_ = cache.TryGet(key, out _);
}
}))
.ToArray();

await Task.WhenAll(workers);
await Task.WhenAll(faultRemovals);

cache.Clear();

Assert.AreEqual(0, cache.ApproximateCount);
for (var key = 0; key < workerCount * itemsPerWorker; key++)
{
Assert.IsFalse(cache.TryGet(key, out _), $"Cache still contains key {key} after Clear.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />

<PropertyGroup>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>Microsoft.CmdPal.UI.UnitTests</RootNamespace>
<OutputPath>$(RepoRoot)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MSTest" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Microsoft.CmdPal.Common\Microsoft.CmdPal.Common.csproj" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\..\Microsoft.CmdPal.UI\Helpers\AdaptiveCache`2.cs" Link="Helpers\AdaptiveCache`2.cs" />
</ItemGroup>
</Project>