/ src / modules / cmdpal / Core / Microsoft.CmdPal.Core.ViewModels / BatchUpdateManager.cs
BatchUpdateManager.cs
  1  // Copyright (c) Microsoft Corporation
  2  // The Microsoft Corporation licenses this file to you under the MIT license.
  3  // See the LICENSE file in the project root for more information.
  4  
  5  using System.Collections.Concurrent;
  6  using Microsoft.CmdPal.Core.Common;
  7  using Microsoft.CmdPal.Core.Common.Helpers;
  8  
  9  namespace Microsoft.CmdPal.Core.ViewModels;
 10  
 11  internal static class BatchUpdateManager
 12  {
 13      private const int ExpectedBatchSize = 32;
 14  
 15      // 30 ms chosen empirically to balance responsiveness and batching:
 16      // - Keeps perceived latency low (< ~50 ms) for user-visible updates.
 17      // - Still allows multiple COM/background events to be coalesced into a single batch.
 18      private static readonly TimeSpan BatchDelay = TimeSpan.FromMilliseconds(30);
 19      private static readonly ConcurrentQueue<IBatchUpdateTarget> DirtyQueue = [];
 20      private static readonly Timer Timer = new(static _ => Flush(), null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
 21  
 22      private static InterlockedBoolean _isFlushScheduled;
 23  
 24      /// <summary>
 25      /// Enqueue a target for batched processing. Safe to call from any thread (including COM callbacks).
 26      /// </summary>
 27      public static void Queue(IBatchUpdateTarget target)
 28      {
 29          if (!target.TryMarkBatchQueued())
 30          {
 31              return; // already queued in current batch window
 32          }
 33  
 34          DirtyQueue.Enqueue(target);
 35          TryScheduleFlush();
 36      }
 37  
 38      private static void TryScheduleFlush()
 39      {
 40          if (!_isFlushScheduled.Set())
 41          {
 42              return;
 43          }
 44  
 45          if (DirtyQueue.IsEmpty)
 46          {
 47              _isFlushScheduled.Clear();
 48  
 49              if (DirtyQueue.IsEmpty)
 50              {
 51                  return;
 52              }
 53  
 54              if (!_isFlushScheduled.Set())
 55              {
 56                  return;
 57              }
 58          }
 59  
 60          try
 61          {
 62              Timer.Change(BatchDelay, Timeout.InfiniteTimeSpan);
 63          }
 64          catch (Exception ex)
 65          {
 66              _isFlushScheduled.Clear();
 67              CoreLogger.LogError("Failed to arm batch timer.", ex);
 68          }
 69      }
 70  
 71      private static void Flush()
 72      {
 73          try
 74          {
 75              var drained = new List<IBatchUpdateTarget>(ExpectedBatchSize);
 76              while (DirtyQueue.TryDequeue(out var item))
 77              {
 78                  drained.Add(item);
 79              }
 80  
 81              if (drained.Count == 0)
 82              {
 83                  return;
 84              }
 85  
 86              // LOAD BEARING:
 87              // ApplyPendingUpdates must run on a background thread.
 88              // The VM itself is responsible for marshaling UI notifications to its _uiScheduler.
 89              ApplyBatch(drained);
 90          }
 91          catch (Exception ex)
 92          {
 93              // Don't kill the timer thread.
 94              CoreLogger.LogError("Batch flush failed.", ex);
 95          }
 96          finally
 97          {
 98              _isFlushScheduled.Clear();
 99              TryScheduleFlush();
100          }
101      }
102  
103      private static void ApplyBatch(List<IBatchUpdateTarget> items)
104      {
105          // Runs on the Timer callback thread (ThreadPool). That's fine: background work only.
106          foreach (var item in items)
107          {
108              // Allow re-queueing immediately if more COM events arrive during apply.
109              item.ClearBatchQueued();
110  
111              try
112              {
113                  item.ApplyPendingUpdates();
114              }
115              catch (Exception ex)
116              {
117                  CoreLogger.LogError("Failed to apply pending updates for a batched target.", ex);
118              }
119          }
120      }
121  }
122  
123  internal interface IBatchUpdateTarget
124  {
125      /// <summary>UI scheduler (used by targets internally for UI marshaling). Kept here for diagnostics / consistency.</summary>
126      TaskScheduler UIScheduler { get; }
127  
128      /// <summary>Apply any coalesced updates. Must be safe to call on a background thread.</summary>
129      void ApplyPendingUpdates();
130  
131      /// <summary>De-dupe gate: returns true only for the first enqueue until cleared.</summary>
132      bool TryMarkBatchQueued();
133  
134      /// <summary>Clear the de-dupe gate so the item can be queued again.</summary>
135      void ClearBatchQueued();
136  }