< Summary

Information
Class: AmbientServices.CallContextServiceProfiler
Assembly: AmbientServices
File(s): /home/runner/work/AmbientServices/AmbientServices/AmbientServices/DefaultImplementation/BasicAmbientServiceProfiler.cs
Tag: 332_35464845198
Line coverage
100%
Covered lines: 26
Uncovered lines: 0
Coverable lines: 26
Total lines: 563
Line coverage: 100%
Branch coverage
87%
Covered branches: 7
Total branches: 8
Branch coverage: 87.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_ProfilerStatistics()100%11100%
.ctor(...)100%11100%
OnSystemSwitched(...)75%44100%
Dispose(...)100%44100%
Dispose()100%11100%

File(s)

/home/runner/work/AmbientServices/AmbientServices/AmbientServices/DefaultImplementation/BasicAmbientServiceProfiler.cs

#LineLine coverage
 1using System;
 2using System.Collections.Concurrent;
 3using System.Collections.Generic;
 4using System.Text;
 5using System.Text.RegularExpressions;
 6using System.Threading;
 7using System.Threading.Tasks;
 8
 9namespace AmbientServices;
 10
 11/// <summary>
 12/// A basic default implementation of <see cref="IAmbientServiceProfiler"/> that tracks the active system per call conte
 13/// </summary>
 14/// <remarks>
 15/// <pitch>The zero-configuration, in-process profiler used unless overridden.  It adds only a single <see cref="AsyncLo
 16/// <pledge><see cref="IAmbientServiceProfiler"/></pledge>
 17/// <plan>
 18/// Holds the currently-active system for each call context in an <see cref="AsyncLocal{T}"/> of <see cref="CallContextA
 19/// </plan>
 20/// <priority>
 21/// <see cref="IAmbientServiceProfiler"/>
 22/// 1. Bounded per-switch cost over in-band analysis: a switch is one <see cref="AsyncLocal{T}"/> write, one timestamp, 
 23/// </priority>
 24/// </remarks>
 25[DefaultAmbientService]
 26internal class BasicAmbientServiceProfiler : IAmbientServiceProfiler
 27{
 28    private readonly ConcurrentHashSet<IAmbientServiceProfilerNotificationSink> _notificationSinks = new();
 29    private readonly AsyncLocal<CallContextActiveSystemData> _activeSystem;
 30
 31    public BasicAmbientServiceProfiler()
 32    {
 33        _activeSystem = new AsyncLocal<CallContextActiveSystemData>();
 34    }
 35
 36    public string? CurrentSystem => _activeSystem.Value.Group;
 37
 38    public void SwitchSystem(string? system, string? updatedPreviousSystem = null)
 39    {
 40        CallContextActiveSystemData oldSystem = _activeSystem.Value;
 41        // value not yet initialized? // note that this is a struct so it can't be null, so we need to initialize this t
 42        if (oldSystem.RawGroup == null) oldSystem = new CallContextActiveSystemData(null, AmbientClock.Ticks);
 43        CallContextActiveSystemData newSystem = new(system);
 44        _activeSystem.Value = newSystem;
 45        // call all the notification sinks
 46        foreach (IAmbientServiceProfilerNotificationSink notificationSink in _notificationSinks)
 47        {
 48            notificationSink.OnSystemSwitched(newSystem.StartStopwatchTimestamp, newSystem.Group, oldSystem.StartStopwat
 49        }
 50    }
 51    public void ResetForkedCallContext()
 52    {
 53        // Re-stamp the active system to the default, started now, with NO sink notification.  Not notifying is the whol
 54        // point: it discards the inherited [parentStart, now) span (the parent keeps its own copy and records that span
 55        // itself) instead of duplicating it onto every fork, which is what inflates the default group's aggregate time.
 56        _activeSystem.Value = new CallContextActiveSystemData(null, AmbientClock.Ticks);
 57    }
 58    public bool RegisterSystemSwitchedNotificationSink(IAmbientServiceProfilerNotificationSink sink)
 59    {
 60        return _notificationSinks.Add(sink);
 61    }
 62    public bool DeregisterSystemSwitchedNotificationSink(IAmbientServiceProfilerNotificationSink sink)
 63    {
 64        return _notificationSinks.Remove(sink);
 65    }
 66}
 67
 68/// <summary>
 69/// A struct that holds information about which system is currently active in a call context.
 70/// </summary>
 71internal struct CallContextActiveSystemData
 72{
 73    /// <summary>
 74    /// The currently-active system or system group identifier.
 75    /// </summary>
 76    public string Group => string.IsNullOrEmpty(RawGroup) ? "" : RawGroup;
 77    /// <summary>
 78    /// The currently-active system or system group identifier (null if this struct is default).
 79    /// </summary>
 80    internal string RawGroup { get; }
 81    /// <summary>
 82    /// The stopwatch timestamp when this system or group became active.
 83    /// Based on <see cref="AmbientClock.Ticks"/>.
 84    /// </summary>
 85    public long StartStopwatchTimestamp { get; private set; }
 86
 87    /// <summary>
 88    /// Constructs a CallContextActiveSystemData with the specified system, starting right now.
 89    /// </summary>
 90    /// <param name="system">The identifier for the active system (or system group).</param>
 91    public CallContextActiveSystemData(string? system)
 92    {
 93        RawGroup = system ?? "";
 94        StartStopwatchTimestamp = AmbientClock.Ticks;
 95    }
 96    /// <summary>
 97    /// Constructs a CallContextActiveSystemData with the specified system (or system group) and start timestamp.
 98    /// </summary>
 99    /// <param name="system">The identifier for the active system (or system group).</param>
 100    /// <param name="startStopwatchTimestamp">The start timestamp, which presumably originated from a previous get of <s
 101    public CallContextActiveSystemData(string? system, long startStopwatchTimestamp)
 102    {
 103        RawGroup = system ?? "";
 104        StartStopwatchTimestamp = startStopwatchTimestamp;
 105    }
 106}
 107
 108/// <summary>
 109/// A half-open stopwatch-tick interval [<see cref="Start"/>, <see cref="End"/>) during which one system or system group
 110/// </summary>
 111internal readonly struct StopwatchInterval
 112{
 113    /// <summary>Gets the inclusive start stopwatch timestamp.</summary>
 114    public long Start { get; }
 115    /// <summary>Gets the exclusive end stopwatch timestamp.</summary>
 116    public long End { get; }
 117    /// <summary>Gets the length of the interval in stopwatch ticks (never negative for a valid interval).</summary>
 118    public long Length => End - Start;
 119    /// <summary>Constructs a StopwatchInterval.</summary>
 120    public StopwatchInterval(long start, long end)
 121    {
 122        Start = start;
 123        End = end;
 124    }
 125}
 126
 127/// <summary>
 128/// A thread-safe collector that turns the self-contained switch intervals broadcast by an <see cref="IAmbientServicePro
 129/// </summary>
 130/// <remarks>
 131/// <plan>
 132/// Stores every completed interval per group in a <see cref="ConcurrentQueue{T}"/> keyed by group, plus the currently-a
 133/// Completed intervals are reconstructed purely from each switch event's old/new start timestamps and the ended-system 
 134/// At report time it copies each group's intervals into a list, optionally appends in-flight intervals ending at "now",
 135/// </plan>
 136/// </remarks>
 137internal sealed class ServiceProfileSampleCollector
 138{
 139    private readonly ConcurrentDictionary<string, ConcurrentQueue<StopwatchInterval>> _intervalsByGroup = new();
 140    private readonly ConcurrentDictionary<object, CallContextActiveSystemData> _activeByContext = new();
 141
 142    /// <summary>
 143    /// Records a completed interval for the system just ended in the specified call context and marks the new system ac
 144    /// </summary>
 145    /// <param name="contextKey">An opaque marker identifying the call context the switch happened in.</param>
 146    /// <param name="oldStart">The stopwatch timestamp when the just-ended system became active.</param>
 147    /// <param name="newStart">The stopwatch timestamp when the new system became active (the end of the just-ended inte
 148    /// <param name="newGroup">The (already group-transformed) system that is now active.</param>
 149    /// <param name="endedGroup">The (already group-transformed) system that just ended, as known to the call context th
 150    /// <param name="revisedEndedGroup">An optional (already group-transformed) replacement identity for the just-ended 
 151    public void RecordSwitch(object contextKey, long oldStart, long newStart, string newGroup, string endedGroup, string
 152    {
 153        string justEndedGroup = revisedEndedGroup ?? endedGroup;
 154        AddInterval(justEndedGroup, oldStart, newStart);
 155        _activeByContext[contextKey] = new CallContextActiveSystemData(newGroup, newStart);
 156    }
 157    /// <summary>
 158    /// Seeds the system that is considered active for the specified call context before any switch has occurred (used s
 159    /// </summary>
 160    public void SeedActive(object contextKey, string group, long startStopwatchTimestamp)
 161    {
 162        _activeByContext[contextKey] = new CallContextActiveSystemData(group, startStopwatchTimestamp);
 163    }
 164    /// <summary>
 165    /// Converts every currently-active (in-flight) system into a completed interval ending at the specified timestamp, 
 166    /// </summary>
 167    public void FinalizeActive(long endStopwatchTimestamp)
 168    {
 169        foreach (KeyValuePair<object, CallContextActiveSystemData> kvp in _activeByContext)
 170        {
 171            AddInterval(kvp.Value.Group, kvp.Value.StartStopwatchTimestamp, endStopwatchTimestamp);
 172        }
 173        _activeByContext.Clear();
 174    }
 175    /// <summary>
 176    /// Produces a per-group statistics snapshot.
 177    /// </summary>
 178    /// <param name="includeActive">Whether to include currently-active (not-yet-ended) systems as intervals ending at <
 179    /// <param name="nowStopwatchTimestamp">The timestamp to use as the end of in-flight intervals.</param>
 180    public IEnumerable<AmbientServiceProfilerAccumulator> GetStatistics(bool includeActive, long nowStopwatchTimestamp)
 181    {
 182        Dictionary<string, List<StopwatchInterval>> byGroup = new(StringComparer.Ordinal);
 183        foreach (KeyValuePair<string, ConcurrentQueue<StopwatchInterval>> kvp in _intervalsByGroup)
 184        {
 185            byGroup[kvp.Key] = new List<StopwatchInterval>(kvp.Value.ToArray());
 186        }
 187        if (includeActive)
 188        {
 189            foreach (KeyValuePair<object, CallContextActiveSystemData> kvp in _activeByContext)
 190            {
 191                if (!byGroup.TryGetValue(kvp.Value.Group, out List<StopwatchInterval>? list))
 192                {
 193                    list = new List<StopwatchInterval>();
 194                    byGroup[kvp.Value.Group] = list;
 195                }
 196                list.Add(new StopwatchInterval(kvp.Value.StartStopwatchTimestamp, nowStopwatchTimestamp));
 197            }
 198        }
 199        foreach (KeyValuePair<string, List<StopwatchInterval>> kvp in byGroup)
 200        {
 201            long total = 0;
 202            foreach (StopwatchInterval interval in kvp.Value) total += interval.Length;
 203            yield return new AmbientServiceProfilerAccumulator(kvp.Key, total, UnionStopwatchTicks(kvp.Value), kvp.Value
 204        }
 205    }
 206    private void AddInterval(string group, long start, long end)
 207    {
 208        _intervalsByGroup.GetOrAdd(group, _ => new ConcurrentQueue<StopwatchInterval>()).Enqueue(new StopwatchInterval(s
 209    }
 210    /// <summary>
 211    /// Computes the total length of the union of the specified intervals (overlapping intervals counted once) using an 
 212    /// </summary>
 213    internal static long UnionStopwatchTicks(List<StopwatchInterval> intervals)
 214    {
 215        if (intervals.Count == 0) return 0;
 216        intervals.Sort((a, b) => a.Start.CompareTo(b.Start));
 217        long union = 0;
 218        long mergedStart = intervals[0].Start;
 219        long mergedEnd = intervals[0].End;
 220        for (int i = 1; i < intervals.Count; ++i)
 221        {
 222            StopwatchInterval interval = intervals[i];
 223            if (interval.Start > mergedEnd)
 224            {
 225                // disjoint (sequential) from the current merged interval: close it out and start a new one
 226                union += mergedEnd - mergedStart;
 227                mergedStart = interval.Start;
 228                mergedEnd = interval.End;
 229            }
 230            else if (interval.End > mergedEnd)
 231            {
 232                // overlapping (concurrent): extend the current merged interval
 233                mergedEnd = interval.End;
 234            }
 235        }
 236        union += mergedEnd - mergedStart;
 237        return union;
 238    }
 239}
 240
 241/// <summary>
 242/// A class that tracks service profile statistics across multiple call contexts in a process or a single time window.
 243/// </summary>
 244/// <remarks>
 245/// <pitch>The process-wide / time-window view: every call context's switches roll into one breakdown, so concurrent bac
 246/// <pledge><see cref="IAmbientServiceProfile"/></pledge>
 247/// <pledge>Live reads of <see cref="ProfilerStatistics"/> include only completed intervals; in-flight systems are folde
 248/// <plan>Subscribes to the whole-process <see cref="IAmbientServiceProfiler"/> and delegates accumulation to a <see cre
 249/// </remarks>
 250internal class ProcessOrSingleTimeWindowServiceProfiler : IAmbientServiceProfile, IAmbientServiceProfilerNotificationSin
 251{
 252    private readonly IAmbientServiceProfiler _profiler;
 253    private readonly Regex? _systemToGroupTransform;
 254    private readonly AsyncLocal<object> _callContextKey;
 255    private readonly ServiceProfileSampleCollector _collector;
 256    private bool _disposedValue;
 257
 258    public string ScopeName { get; }
 259
 260    public IEnumerable<AmbientServiceProfilerAccumulator> ProfilerStatistics => _collector.GetStatistics(false, AmbientC
 261
 262    public ProcessOrSingleTimeWindowServiceProfiler(IAmbientServiceProfiler metrics, string scopeName, Regex? systemGrou
 263    {
 264        _profiler = metrics;
 265        ScopeName = scopeName;
 266        _systemToGroupTransform = systemGroupTransform;
 267        _collector = new ServiceProfileSampleCollector();
 268        _callContextKey = new AsyncLocal<object>();
 269        _profiler.RegisterSystemSwitchedNotificationSink(this);
 270    }
 271    internal static string GroupSystem(Regex? transform, string system)
 272    {
 273        if (transform == null) return system;
 274        Match match = transform.Match(system);
 275        StringBuilder group = new();
 276        GroupCollection groups = match.Groups;
 277        for (int groupNumber = 1; groupNumber < groups.Count; ++groupNumber)
 278        {
 279            Group matchGroup = groups[groupNumber];
 280            if (!matchGroup.Success) continue;
 281            group.Append(matchGroup.Value);
 282        }
 283        return group.ToString();
 284    }
 285    /// <summary>
 286    /// Notifies the notification sink that the system has switched.
 287    /// </summary>
 288    /// <remarks>
 289    /// This function will be called whenever the service profiler is told that the currently-processing system has swit
 290    /// Note that the previously-executing system may or may not be revised at this time.
 291    /// Such revisions can be used to distinguish between processing that resulted in success or failure, or other simil
 292    /// </remarks>
 293    /// <param name="newSystemStartStopwatchTimestamp">The stopwatch timestamp when the new system started.</param>
 294    /// <param name="newSystem">The identifier for the system that is starting to run.</param>
 295    /// <param name="oldSystemStartStopwatchTimestamp">The stopwatch timestamp when the old system started running.</par
 296    /// <param name="oldSystem">The identifier for the system that has just finished running, as known to the call conte
 297    /// <param name="revisedOldSystem">An optional revised name for the system that has just finished running that overr
 298    public void OnSystemSwitched(long newSystemStartStopwatchTimestamp, string newSystem, long oldSystemStartStopwatchTi
 299    {
 300        // assign a call context key for the current call context if we haven't assigned one yet
 301        if (_callContextKey.Value == null) _callContextKey.Value = new object();
 302        string newGroup = GroupSystem(_systemToGroupTransform, newSystem);
 303        string endedGroup = GroupSystem(_systemToGroupTransform, oldSystem);
 304        string? revisedEndedGroup = (revisedOldSystem == null) ? null : GroupSystem(_systemToGroupTransform, revisedOldS
 305        _collector.RecordSwitch(_callContextKey.Value, oldSystemStartStopwatchTimestamp, newSystemStartStopwatchTimestam
 306    }
 307
 308    protected virtual void Dispose(bool disposing)
 309    {
 310        if (!_disposedValue)
 311        {
 312            if (disposing)
 313            {
 314                // TODO: dispose managed state (managed objects)
 315                _profiler.DeregisterSystemSwitchedNotificationSink(this);
 316            }
 317
 318            // TODO: free unmanaged resources (unmanaged objects) and override finalizer
 319            // TODO: set large fields to null
 320            _disposedValue = true;
 321        }
 322    }
 323
 324    // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
 325    // ~ProcessingDistributionAccumulator()
 326    // {
 327    //     // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
 328    //     Dispose(disposing: false);
 329    // }
 330
 331    public void Dispose()
 332    {
 333        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
 334        Dispose(disposing: true);
 335        GC.SuppressFinalize(this);
 336    }
 337
 338    internal void CloseSampling()
 339    {
 340        _profiler.DeregisterSystemSwitchedNotificationSink(this);
 341        // now that we have an end, add the time spent on each still-active system to the collection
 342        _collector.FinalizeActive(AmbientClock.Ticks);
 343    }
 344}
 345
 346/// <summary>
 347/// A class that distributes system switch notifications to the sinks scoped to a single call context subtree.
 348/// </summary>
 349/// <remarks>
 350/// <pitch>The per-call-context fan-out hub: an <see cref="AsyncLocal{T}"/>-held instance receives the whole process's s
 351/// <pledge><see cref="IAmbientServiceProfilerNotificationSink"/></pledge>
 352/// </remarks>
 353internal class ScopeOnSystemSwitchedDistributor : IAmbientServiceProfilerNotificationSink
 354{
 355    private readonly ConcurrentHashSet<IAmbientServiceProfilerNotificationSink> _notificationSinks = new();
 356    /// <summary>
 357    /// Notifies the notification sink that the system has switched.
 358    /// </summary>
 359    /// <remarks>
 360    /// This function will be called whenever the service profiler is told that the currently-processing system has swit
 361    /// Note that the previously-executing system may or may not be revised at this time.
 362    /// Such revisions can be used to distinguish between processing that resulted in success or failure, or other simil
 363    /// </remarks>
 364    /// <param name="newSystemStartStopwatchTimestamp">The stopwatch timestamp when the new system started.</param>
 365    /// <param name="newSystem">The identifier for the system that is starting to run.</param>
 366    /// <param name="oldSystemStartStopwatchTimestamp">The stopwatch timestamp when the old system started running.</par
 367    /// <param name="oldSystem">The identifier for the system that has just finished running, as known to the call conte
 368    /// <param name="revisedOldSystem">An optional revised name for the system that has just finished running that overr
 369    public void OnSystemSwitched(long newSystemStartStopwatchTimestamp, string newSystem, long oldSystemStartStopwatchTi
 370    {
 371        foreach (IAmbientServiceProfilerNotificationSink notificationSink in _notificationSinks)
 372        {
 373            notificationSink.OnSystemSwitched(newSystemStartStopwatchTimestamp, newSystem, oldSystemStartStopwatchTimest
 374        }
 375    }
 376
 377    public bool RegisterSystemSwitchedNotificationSink(IAmbientServiceProfilerNotificationSink sink)
 378    {
 379        return _notificationSinks.Add(sink);
 380    }
 381    public bool DeregisterSystemSwitchedNotificationSink(IAmbientServiceProfilerNotificationSink sink)
 382    {
 383        return _notificationSinks.Remove(sink);
 384    }
 385}
 386
 387/// <summary>
 388/// A class that tracks service profile statistics for a specific call context (and the contexts it forks).
 389/// </summary>
 390/// <remarks>
 391/// <pitch>The per-request view: profiles one operation and the parallel contexts it spawns, so a request that fans work
 392/// <pledge><see cref="IAmbientServiceProfile"/></pledge>
 393/// <pledge>Reads of <see cref="ProfilerStatistics"/> include the currently-active (in-flight) system(s) as intervals en
 394/// <plan>Subscribes to a call-context-scoped <see cref="ScopeOnSystemSwitchedDistributor"/> and delegates to a <see cre
 395/// </remarks>
 396internal class CallContextServiceProfiler : IAmbientServiceProfile, IAmbientServiceProfilerNotificationSink, IDisposable
 397{
 398    private readonly ScopeOnSystemSwitchedDistributor _distributor;
 399    private readonly Regex? _systemGroupTransform;
 400    private readonly ServiceProfileSampleCollector _collector;
 401    private readonly AsyncLocal<object> _callContextKey;
 402    private bool _disposedValue;
 403
 404    public string ScopeName { get; }
 405
 2406    public IEnumerable<AmbientServiceProfilerAccumulator> ProfilerStatistics => _collector.GetStatistics(true, AmbientCl
 407
 408    /// <summary>
 409    /// Constructs a CallContextServiceProfiler.
 410    /// </summary>
 411    /// <param name="distributor">A <see cref="ScopeOnSystemSwitchedDistributor"/> to hook into to receive system change
 412    /// <param name="scopeName">The name of the call context being tracked.</param>
 413    /// <param name="systemGroupTransform">A <see cref="Regex"/> string to transform the processor into a system group.<
 414    /// <param name="startSystem">The optional starting system.</param>
 2415    public CallContextServiceProfiler(ScopeOnSystemSwitchedDistributor distributor, string scopeName, Regex? systemGroup
 416    {
 2417        _distributor = distributor;
 2418        _systemGroupTransform = systemGroupTransform;
 2419        ScopeName = scopeName;
 2420        _collector = new ServiceProfileSampleCollector();
 2421        _callContextKey = new AsyncLocal<object>();
 422        // seed the active system for the creating call context so a read before the first switch reports the starting s
 2423        object contextKey = new();
 2424        _callContextKey.Value = contextKey;
 2425        _collector.SeedActive(contextKey, ProcessOrSingleTimeWindowServiceProfiler.GroupSystem(_systemGroupTransform, st
 2426        distributor.RegisterSystemSwitchedNotificationSink(this);
 2427    }
 428    /// <summary>
 429    /// Notifies the notification sink that the system has switched.
 430    /// </summary>
 431    /// <remarks>
 432    /// This function will be called whenever the service profiler is told that the currently-processing system has swit
 433    /// Note that the previously-executing system may or may not be revised at this time.
 434    /// Such revisions can be used to distinguish between processing that resulted in success or failure, or other simil
 435    /// </remarks>
 436    /// <param name="newSystemStartStopwatchTimestamp">The stopwatch timestamp when the new system started.</param>
 437    /// <param name="newSystem">The identifier for the system that is starting to run.</param>
 438    /// <param name="oldSystemStartStopwatchTimestamp">The stopwatch timestamp when the old system started running.</par
 439    /// <param name="oldSystem">The identifier for the system that has just finished running, as known to the call conte
 440    /// <param name="revisedOldSystem">An optional revised name for the system that has just finished running that overr
 441    public void OnSystemSwitched(long newSystemStartStopwatchTimestamp, string newSystem, long oldSystemStartStopwatchTi
 442    {
 443        // assign a call context key for the current call context if we haven't assigned one yet (forked children may in
 2444        if (_callContextKey.Value == null) _callContextKey.Value = new object();
 2445        string newGroup = ProcessOrSingleTimeWindowServiceProfiler.GroupSystem(_systemGroupTransform, newSystem);
 2446        string endedGroup = ProcessOrSingleTimeWindowServiceProfiler.GroupSystem(_systemGroupTransform, oldSystem);
 2447        string? revisedEndedGroup = (revisedOldSystem == null) ? null : ProcessOrSingleTimeWindowServiceProfiler.GroupSy
 2448        _collector.RecordSwitch(_callContextKey.Value, oldSystemStartStopwatchTimestamp, newSystemStartStopwatchTimestam
 2449    }
 450
 451    protected virtual void Dispose(bool disposing)
 452    {
 2453        if (!_disposedValue)
 454        {
 2455            if (disposing)
 456            {
 457                // TODO: dispose managed state (managed objects)
 2458                _distributor.DeregisterSystemSwitchedNotificationSink(this);
 459            }
 460
 461            // TODO: free unmanaged resources (unmanaged objects) and override finalizer
 462            // TODO: set large fields to null
 2463            _disposedValue = true;
 464        }
 2465    }
 466
 467    // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
 468    // ~CallContextServiceProfiler()
 469    // {
 470    //     // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
 471    //     Dispose(disposing: false);
 472    // }
 473
 474    public void Dispose()
 475    {
 476        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
 2477        Dispose(disposing: true);
 2478        GC.SuppressFinalize(this);
 2479    }
 480}
 481
 482/// <summary>
 483/// A class that tracks service profile statistics for a moving time window.
 484/// </summary>
 485/// <remarks>
 486/// <pitch>Continuous reporting: rotates a fresh <see cref="ProcessOrSingleTimeWindowServiceProfiler"/> every window per
 487/// <plan>Drives rotation with an <see cref="AmbientEventTimer"/> on the window period; on each tick it atomically swaps
 488/// </remarks>
 489internal class TimeWindowServiceProfiler : IDisposable
 490{
 491    private readonly string _scopeNamePrefix;
 492    private readonly AmbientEventTimer _timeWindowRotator;
 493    private ProcessOrSingleTimeWindowServiceProfiler? _timeWindowCallContextCollector;  // interlocked
 494    private bool _disposedValue;
 495
 496    /// <summary>
 497    /// Constructs a TimeWindowProcessingDistributionTracker.
 498    /// </summary>
 499    /// <param name="metrics">A <see cref="IAmbientServiceProfiler"/> to hook into to receive processor change events.</
 500    /// <param name="scopeNamePrefix">A <see cref="TimeSpan"/> indicating the size of the window.</param>
 501    /// <param name="windowPeriod">A <see cref="TimeSpan"/> indicating how often reports are desired.</param>
 502    /// <param name="onWindowComplete">An async delegate that receives a <see cref="IAmbientServiceProfile"/> at the end
 503    /// <param name="systemGroupTransform">A <see cref="Regex"/> string to transform the system into a system group.</pa
 504    public TimeWindowServiceProfiler(IAmbientServiceProfiler metrics, string scopeNamePrefix, TimeSpan windowPeriod, Fun
 505    {
 506        if (onWindowComplete == null) throw new ArgumentNullException(nameof(onWindowComplete), "Time Window Collection 
 507        _scopeNamePrefix = scopeNamePrefix;
 508        using (Rotate(metrics, windowPeriod, systemGroupTransform)) { }
 509        _timeWindowRotator = new AmbientEventTimer(windowPeriod);
 510        _timeWindowRotator.Elapsed +=
 511            async (sender, handler) =>
 512            {
 513                ProcessOrSingleTimeWindowServiceProfiler? oldAccumulator = Rotate(metrics, windowPeriod, systemGroupTran
 514                if (oldAccumulator != null)
 515                {
 516                    await onWindowComplete(oldAccumulator);
 517                }
 518            };
 519        _timeWindowRotator.AutoReset = true;
 520        _timeWindowRotator.Enabled = true;
 521    }
 522
 523    private ProcessOrSingleTimeWindowServiceProfiler? Rotate(IAmbientServiceProfiler metrics, TimeSpan windowPeriod, Reg
 524    {
 525        string windowName = WindowScope.WindowId(AmbientClock.UtcNow, windowPeriod);
 526        string newAccumulatorScopeName = _scopeNamePrefix + windowName + "(" + WindowScope.WindowSize(windowPeriod) + ")
 527        ProcessOrSingleTimeWindowServiceProfiler newAccumulator = new(metrics, newAccumulatorScopeName, systemGroupTrans
 528        ProcessOrSingleTimeWindowServiceProfiler? oldAccumulator = Interlocked.Exchange(ref _timeWindowCallContextCollec
 529        // close out the old accumulator
 530        oldAccumulator?.CloseSampling();
 531        return oldAccumulator;
 532    }
 533
 534    protected virtual void Dispose(bool disposing)
 535    {
 536        if (!_disposedValue)
 537        {
 538            if (disposing)
 539            {
 540                // TODO: dispose managed state (managed objects)
 541                _timeWindowRotator.Dispose();
 542            }
 543
 544            // TODO: free unmanaged resources (unmanaged objects) and override finalizer
 545            // TODO: set large fields to null
 546            _disposedValue = true;
 547        }
 548    }
 549
 550    // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
 551    // ~ScopeProcessingDistributionTracker()
 552    // {
 553    //     // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
 554    //     Dispose(disposing: false);
 555    // }
 556
 557    public void Dispose()
 558    {
 559        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
 560        Dispose(disposing: true);
 561        GC.SuppressFinalize(this);
 562    }
 563}