mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-06 02:19:51 +02:00
Compare commits
5 Commits
dev/crutka
...
dev/migrie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e47c5e1467 | ||
|
|
9435d79e8e | ||
|
|
a391f49d70 | ||
|
|
89a481399c | ||
|
|
f28d9b55af |
@@ -324,12 +324,24 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
switch (propertyName)
|
||||
{
|
||||
case nameof(Command):
|
||||
Command.PropertyChanged -= Command_PropertyChanged;
|
||||
var oldCommand = Command;
|
||||
oldCommand.PropertyChanged -= Command_PropertyChanged;
|
||||
var command = model.Command;
|
||||
Command = new(command, PageContext);
|
||||
Command.InitializeProperties();
|
||||
Command.PropertyChanged += Command_PropertyChanged;
|
||||
|
||||
// The previous CommandViewModel subscribed to its extension
|
||||
// model's PropChanged event (a WinRT/COM event). Replacing the
|
||||
// reference is not enough: that subscription roots the old
|
||||
// CommandViewModel - and the COM object, icon view models, and
|
||||
// ComWrappers behind it - keeping them all alive forever. For
|
||||
// fallback items the Command is swapped on every keystroke, so
|
||||
// failing to clean it up leaks an entire view-model + COM tree
|
||||
// per character typed. SafeCleanup() unsubscribes the COM
|
||||
// PropChanged handler so the old graph can be collected.
|
||||
oldCommand.SafeCleanup();
|
||||
|
||||
// Extensions based on Command Palette SDK < 0.3 CommandItem class won't notify when Title changes because Command
|
||||
// or Command.Name change. This is a workaround to ensure that the Title is always up-to-date for extensions with old SDK.
|
||||
_itemTitle = model.Title;
|
||||
|
||||
@@ -10,16 +10,18 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
/// <summary>
|
||||
/// An elastic pool of dedicated background threads for running blocking work
|
||||
/// off the ThreadPool. Starts with <c>minThreads</c> always-alive threads and
|
||||
/// expands up to <c>maxThreads</c> on demand. Threads above the minimum exit
|
||||
/// automatically after <c>idleTimeout</c> with no work. Items are processed
|
||||
/// FIFO; cancelled items are skipped at dequeue time.
|
||||
/// expands up to <c>maxThreads</c> on demand. Once created, threads are kept
|
||||
/// alive for the lifetime of the pool and reused — they are never shrunk back
|
||||
/// below the high-water mark. Terminating a managed thread leaks per-thread
|
||||
/// native CLR/CRT bookkeeping (EventPipeThread, CrstStatic, vcrt PTD), so
|
||||
/// recreating threads on each work burst would grow native memory without
|
||||
/// bound. Items are processed FIFO; cancelled items are skipped at dequeue time.
|
||||
/// </summary>
|
||||
internal sealed partial class DedicatedThreadPool : IDisposable
|
||||
{
|
||||
private const int DrainTimeoutMs = 3000;
|
||||
|
||||
private readonly BlockingCollection<Action> _workQueue = new();
|
||||
private readonly int _minThreads;
|
||||
private readonly int _maxThreads;
|
||||
private readonly TimeSpan _idleTimeout;
|
||||
private readonly string _name;
|
||||
@@ -41,7 +43,6 @@ internal sealed partial class DedicatedThreadPool : IDisposable
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(minThreads);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(maxThreads, minThreads);
|
||||
|
||||
_minThreads = minThreads;
|
||||
_maxThreads = maxThreads;
|
||||
_name = name;
|
||||
_idleTimeout = idleTimeout ?? TimeSpan.FromSeconds(30);
|
||||
@@ -103,28 +104,16 @@ internal sealed partial class DedicatedThreadPool : IDisposable
|
||||
}
|
||||
|
||||
// TryTake timed out (no work for idleTimeout).
|
||||
// If the pool is shutting down, exit; otherwise keep this thread
|
||||
// alive and re-park in TryTake. We intentionally never shrink below
|
||||
// the high-water mark: terminating a managed thread leaks per-thread
|
||||
// native CLR/CRT structures (EventPipeThread, CrstStatic, vcrt PTD)
|
||||
// that the runtime does not reclaim, so threads are reused for the
|
||||
// lifetime of the pool instead of being recreated on the next burst.
|
||||
if (_workQueue.IsCompleted)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Try to shrink: exit if we're above the minimum.
|
||||
// CAS ensures exactly one thread wins each decrement race.
|
||||
while (true)
|
||||
{
|
||||
var count = _threadCount;
|
||||
if (count <= _minThreads)
|
||||
{
|
||||
break; // At minimum — stay alive.
|
||||
}
|
||||
|
||||
if (Interlocked.CompareExchange(ref _threadCount, count - 1, count) == count)
|
||||
{
|
||||
return; // Decremented successfully — this thread exits.
|
||||
}
|
||||
|
||||
// Another thread changed _threadCount concurrently; retry.
|
||||
}
|
||||
}
|
||||
|
||||
Interlocked.Decrement(ref _threadCount);
|
||||
|
||||
@@ -42,7 +42,12 @@ internal sealed partial class FallbackUpdateManager : IDisposable
|
||||
// Per-batch cap on sibling workers
|
||||
private static readonly int MaxWorkersPerBatch = Math.Max(2, Environment.ProcessorCount / 2);
|
||||
|
||||
private readonly ConcurrentDictionary<string, InflightCounter> _inflightFallbacks = new();
|
||||
// Keyed by the TopLevelViewModel instance (reference identity), NOT by command.Id.
|
||||
// A fallback item's Id is derived from its Title for extensions that don't supply a
|
||||
// stable id, and that Title changes on every keystroke. Keying by the mutable Id would
|
||||
// allocate a fresh entry (+ InflightCounter) per keystroke that never gets reclaimed,
|
||||
// leaking unboundedly while typing. The VM instance is stable for the command's lifetime.
|
||||
private readonly ConcurrentDictionary<TopLevelViewModel, InflightCounter> _inflightFallbacks = new();
|
||||
|
||||
// Dedicated background threads for fallback COM/RPC calls so they never block the
|
||||
// ThreadPool. Stuck extensions consume a dedicated thread, not a pool thread.
|
||||
@@ -95,7 +100,7 @@ internal sealed partial class FallbackUpdateManager : IDisposable
|
||||
}
|
||||
|
||||
var command = commands[i];
|
||||
var counter = _inflightFallbacks.GetOrAdd(command.Id, static _ => new InflightCounter());
|
||||
var counter = _inflightFallbacks.GetOrAdd(command, static _ => new InflightCounter());
|
||||
if (!counter.TryClaim(MaxInflightPerFallback))
|
||||
{
|
||||
// At capacity — store this query as a pending retry so it runs
|
||||
|
||||
Reference in New Issue
Block a user