< Summary

Information
Class: AmbientServices.CallContextSurveyManager
Assembly: AmbientServices
File(s): /home/runner/work/AmbientServices/AmbientServices/AmbientServices/DefaultImplementation/BasicAmbientBottleneckDetector.cs
Tag: 332_35464845198
Line coverage
100%
Covered lines: 22
Uncovered lines: 0
Coverable lines: 22
Total lines: 786
Line coverage: 100%
Branch coverage
100%
Covered branches: 10
Total branches: 10
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%22100%
get_CallContextDistributor()100%22100%
AmbientServices.IAmbientBottleneckExitNotificationSink.BottleneckExited(...)100%11100%
CreateCallContextSurveyor(...)100%11100%
Dispose(...)100%66100%
Dispose()100%11100%

File(s)

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

#LineLine coverage
 1using System;
 2using System.Collections.Concurrent;
 3using System.Collections.Generic;
 4using System.Diagnostics;
 5using System.Linq;
 6using System.Runtime.Versioning;
 7using System.Text.RegularExpressions;
 8using System.Threading;
 9using System.Threading.Tasks;
 10
 11namespace AmbientServices;
 12
 13/// <summary>
 14/// A basic default implementation of <see cref="IAmbientBottleneckDetector"/> that stamps accesses and broadcasts them 
 15/// </summary>
 16/// <remarks>
 17/// <pitch>The zero-configuration, in-process bottleneck detector used unless overridden.  Each access costs one accesso
 18/// <pledge><see cref="IAmbientBottleneckDetector"/></pledge>
 19/// <plan>
 20/// Entirely stateless except for the sink set (a <see cref="ConcurrentHashSet{T}"/>, so registration is idempotent and 
 21/// </plan>
 22/// <priority>
 23/// <see cref="IAmbientBottleneckDetector"/>
 24/// 1. Bounded per-access cost over in-band analysis: entering a bottleneck costs one accessor allocation, two timestamp
 25/// </priority>
 26/// </remarks>
 27[DefaultAmbientService]
 28internal class BasicAmbientBottleneckDetector : IAmbientBottleneckDetector
 29{
 30    private readonly ConcurrentHashSet<IAmbientBottleneckExitNotificationSink> _notificationSinks = new();
 31
 32    public BasicAmbientBottleneckDetector()
 33    {
 34    }
 35
 36    public AmbientBottleneckAccessor EnterBottleneck(AmbientBottleneck bottleneck)
 37    {
 38        AmbientBottleneckAccessor access = new(this, bottleneck, AmbientClock.Ticks);
 39        foreach (IAmbientBottleneckExitNotificationSink notificationSink in _notificationSinks)
 40        {
 41            IAmbientBottleneckEnterNotificationSink? enterSink = notificationSink as IAmbientBottleneckEnterNotification
 42            enterSink?.BottleneckEntered(access);
 43        }
 44        return access;
 45    }
 46    internal void LeaveBottleneck(AmbientBottleneckAccessor ambientBottleneckAccess)
 47    {
 48        foreach (IAmbientBottleneckExitNotificationSink notificationSink in _notificationSinks)
 49        {
 50            notificationSink.BottleneckExited(ambientBottleneckAccess);
 51        }
 52    }
 53
 54    public bool RegisterAccessNotificationSink(IAmbientBottleneckExitNotificationSink sink)
 55    {
 56        return _notificationSinks.Add(sink);
 57    }
 58    public bool DeregisterAccessNotificationSink(IAmbientBottleneckExitNotificationSink sink)
 59    {
 60        return _notificationSinks.Remove(sink);
 61    }
 62}
 63/// <summary>
 64/// A class that routes bottleneck exit notifications to surveyors scoped to the call context that created them.
 65/// </summary>
 66/// <remarks>
 67/// <pitch>The per-call-context tap on the bottleneck exit stream: one process-wide registration with the detector, from
 68/// <pledge><see cref="IAmbientBottleneckExitNotificationSink"/></pledge>
 69/// <plan>Registers itself with the detector once at construction and holds an <see cref="AsyncLocal{T}"/> of per-contex
 70/// </remarks>
 71internal class CallContextSurveyManager : IAmbientBottleneckExitNotificationSink, IDisposable
 72{
 73    private readonly IAmbientBottleneckDetector? _bottleneckDetector;
 74    private readonly AsyncLocal<CallContextAccessNotificationDistributor> _callContextSurveyors;
 75    private bool _disposed;
 76
 277    public CallContextSurveyManager(IAmbientBottleneckDetector? bottleneckDetector)
 78    {
 279        _callContextSurveyors = new AsyncLocal<CallContextAccessNotificationDistributor>();
 280        if (bottleneckDetector != null)
 81        {
 282            _bottleneckDetector = bottleneckDetector;
 283            bottleneckDetector.RegisterAccessNotificationSink(this);
 84        }
 285    }
 86
 87    private CallContextAccessNotificationDistributor CallContextDistributor
 88    {
 89        get
 90        {
 291            CallContextAccessNotificationDistributor? callContextDistributor = _callContextSurveyors.Value;
 292            if (callContextDistributor == null)
 93            {
 294                _callContextSurveyors.Value = callContextDistributor = new CallContextAccessNotificationDistributor();
 95            }
 296            return callContextDistributor;
 97        }
 98    }
 99
 100    void IAmbientBottleneckExitNotificationSink.BottleneckExited(AmbientBottleneckAccessor bottleneckAccessor)
 101    {
 2102        CallContextDistributor.BottleneckExited(bottleneckAccessor);
 2103    }
 104
 105    internal IAmbientBottleneckSurveyor CreateCallContextSurveyor(string? scopeName, Regex? allow, Regex? block)
 106    {
 2107        ScopedBottleneckSurveyor surveyor = new(scopeName, CallContextDistributor, allow, block);
 2108        return surveyor;
 109    }
 110
 111    protected virtual void Dispose(bool disposing)
 112    {
 2113        if (!_disposed)
 114        {
 2115            if (disposing)
 116            {
 2117                _bottleneckDetector?.DeregisterAccessNotificationSink(this);
 118            }
 119
 120            // TODO: free unmanaged resources (unmanaged objects) and override finalizer
 121            // TODO: set large fields to null
 2122            _disposed = true;
 123        }
 2124    }
 125
 126    // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
 127    // ~CallContextSurveyManager()
 128    // {
 129    //     // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
 130    //     Dispose(disposing: false);
 131    // }
 132
 133    public void Dispose()
 134    {
 135        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
 2136        Dispose(disposing: true);
 2137        GC.SuppressFinalize(this);
 2138    }
 139}
 140
 141/// <summary>
 142/// A class that fans bottleneck exit notifications out to the sinks registered within one call context.
 143/// </summary>
 144/// <remarks>
 145/// <pitch>The fan-out node for one call context: surveyors register here instead of with the process-wide detector, so 
 146/// <pledge><see cref="IAmbientBottleneckExitNotificationSink"/></pledge>
 147/// <plan>A <see cref="ConcurrentHashSet{T}"/> of sinks with synchronous fan-out; idempotent registration.</plan>
 148/// </remarks>
 149internal class CallContextAccessNotificationDistributor : IAmbientBottleneckExitNotificationSink
 150{
 151    private readonly ConcurrentHashSet<IAmbientBottleneckExitNotificationSink> _notificationSinks = new();
 152
 153    public CallContextAccessNotificationDistributor()
 154    {
 155    }
 156    public bool RegisterAccessNotificationSink(IAmbientBottleneckExitNotificationSink sink)
 157    {
 158        return _notificationSinks.Add(sink);
 159    }
 160    public bool DeregisterAccessNotificationSink(IAmbientBottleneckExitNotificationSink sink)
 161    {
 162        return _notificationSinks.Remove(sink);
 163    }
 164    public void BottleneckExited(AmbientBottleneckAccessor bottleneckAccessor)
 165    {
 166        foreach (IAmbientBottleneckExitNotificationSink notificationSink in _notificationSinks)
 167        {
 168            notificationSink.BottleneckExited(bottleneckAccessor);
 169        }
 170    }
 171}
 172
 173/// <summary>
 174/// A class that accumulates a bottleneck survey for a single scope (a call context, or anything the caller brackets wit
 175/// </summary>
 176/// <remarks>
 177/// <pitch>The survey collector for an explicitly-bracketed scope: construct it to start listening, dispose it to stop, 
 178/// <pledge><see cref="IAmbientBottleneckSurveyor"/></pledge>
 179/// <pledge><see cref="IAmbientBottleneckExitNotificationSink"/></pledge>
 180/// <pledge>Only accesses whose bottleneck identifier passes the allow/block regex filters (block wins over allow; a mis
 181/// <plan>Registers with either a per-call-context distributor or the process-wide detector at construction, and deregis
 182/// </remarks>
 183internal class ScopedBottleneckSurveyor : IAmbientBottleneckSurveyor, IAmbientBottleneckExitNotificationSink
 184{
 185    private readonly CallContextAccessNotificationDistributor? _callContextDistributor;
 186    private readonly IAmbientBottleneckDetector? _bottleneckDetector;
 187    private readonly Regex? _allow;
 188    private readonly Regex? _block;
 189    private readonly Dictionary<string, AmbientBottleneckAccessor> _bottleneckAccesses;
 190    private bool _disposedValue;
 191
 192    public ScopedBottleneckSurveyor(string? scopeName, CallContextAccessNotificationDistributor? callContextDistributor,
 193    {
 194        ScopeName = scopeName ?? "";
 195        _allow = allow;
 196        _block = block;
 197        _bottleneckAccesses = new Dictionary<string, AmbientBottleneckAccessor>();
 198        if (callContextDistributor != null)
 199        {
 200            _callContextDistributor = callContextDistributor;
 201            callContextDistributor.RegisterAccessNotificationSink(this);
 202        }
 203    }
 204
 205    public ScopedBottleneckSurveyor(string? scopeName, IAmbientBottleneckDetector? bottleneckDetector, Regex? allow, Reg
 206    {
 207        ScopeName = scopeName ?? "";
 208        _allow = allow;
 209        _block = block;
 210        _bottleneckAccesses = new Dictionary<string, AmbientBottleneckAccessor>();
 211        if (bottleneckDetector != null)
 212        {
 213            _bottleneckDetector = bottleneckDetector;
 214            bottleneckDetector.RegisterAccessNotificationSink(this);
 215        }
 216    }
 217
 218    public string ScopeName { get; }
 219
 220    public AmbientBottleneckAccessor? MostUtilizedBottleneck => _bottleneckAccesses.Values.Max();
 221
 222    public IEnumerable<AmbientBottleneckAccessor> GetMostUtilizedBottlenecks(int count)
 223    {
 224        return _bottleneckAccesses.Values.OrderBy(m => m.Utilization).Take(count);
 225    }
 226
 227    public void BottleneckExited(AmbientBottleneckAccessor? bottleneckAccessor)
 228    {
 229#if NET5_0_OR_GREATER
 230        ArgumentNullException.ThrowIfNull(bottleneckAccessor);
 231#else
 232        if (bottleneckAccessor is null) throw new ArgumentNullException(nameof(bottleneckAccessor));
 233#endif
 234        string bottleneckId = bottleneckAccessor.Bottleneck.Id;
 235        // is this bottleneck being surveyed?
 236        bool blocked = _block?.IsMatch(bottleneckId) ?? false;
 237        bool allowed = !blocked && (_allow?.IsMatch(bottleneckId) ?? true);
 238        if (allowed)
 239        {
 240            AmbientBottleneckAccessor? metric;
 241            if (_bottleneckAccesses.TryGetValue(bottleneckId, out metric))
 242            {
 243                _bottleneckAccesses[bottleneckId] = metric.Combine(bottleneckAccessor);
 244            }
 245            else
 246            {
 247                _bottleneckAccesses.Add(bottleneckId, bottleneckAccessor);
 248            }
 249        }
 250    }
 251
 252    protected virtual void Dispose(bool disposing)
 253    {
 254        if (!_disposedValue)
 255        {
 256            if (disposing)
 257            {
 258                _bottleneckDetector?.DeregisterAccessNotificationSink(this);
 259                _callContextDistributor?.DeregisterAccessNotificationSink(this);
 260            }
 261
 262            // TODO: free unmanaged resources (unmanaged objects) and override finalizer
 263            // TODO: set large fields to null
 264            _disposedValue = true;
 265        }
 266    }
 267
 268    // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
 269    // ~ScopeBottleneckAnalyzer()
 270    // {
 271    //     // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
 272    //     Dispose(disposing: false);
 273    // }
 274
 275    public void Dispose()
 276    {
 277        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
 278        Dispose(disposing: true);
 279        GC.SuppressFinalize(this);
 280    }
 281}
 282/// <summary>
 283/// A class that produces a bottleneck survey for each successive time window, delivering each completed window to a cal
 284/// </summary>
 285/// <remarks>
 286/// <pitch>Periodic bottleneck reporting: every window of the configured size yields a finished <see cref="IAmbientBottl
 287/// <pledge><see cref="IAmbientBottleneckExitNotificationSink"/></pledge>
 288/// <pledge><see cref="IAmbientBottleneckEnterNotificationSink"/></pledge>
 289/// <plan>An <see cref="AmbientEventTimer"/> rotates the current <see cref="TimeWindowBottleneckSurvey"/> atomically (vi
 290/// </remarks>
 291internal class TimeWindowSurveyManager : IAmbientBottleneckExitNotificationSink, IAmbientBottleneckEnterNotificationSink
 292{
 293    private readonly IAmbientBottleneckDetector? _bottleneckDetector;
 294    private readonly AmbientEventTimer _timer;
 295    private TimeWindowBottleneckSurvey _currentWindowSurvey;   // interlocked
 296    private bool _disposedValue;
 297
 298    public TimeWindowSurveyManager(TimeSpan windowSize, Func<IAmbientBottleneckSurvey, Task> onWindowComplete, IAmbientB
 299    {
 300        TimeWindowBottleneckSurvey initialSurvey = new(allow, block, AmbientClock.Ticks, windowSize);
 301        _currentWindowSurvey = initialSurvey;
 302        void rotateTimeWindow(object? s, System.Timers.ElapsedEventArgs e)
 303        {
 304            TimeWindowBottleneckSurvey survey = new(allow, block, AmbientClock.Ticks, windowSize);
 305            TimeWindowBottleneckSurvey oldAnalyzer = Interlocked.Exchange(ref _currentWindowSurvey, survey);
 306            // copy all the accesses still in progress
 307            survey.SwitchAnalyzer(oldAnalyzer);
 308            onWindowComplete(oldAnalyzer);
 309        }
 310        AmbientEventTimer timer = new();
 311        timer.AutoReset = true;
 312        timer.Elapsed += rotateTimeWindow;
 313        timer.Interval = windowSize.TotalMilliseconds;
 314        timer.Enabled = true;
 315        _timer = timer;
 316        if (bottleneckDetector != null)
 317        {
 318            _bottleneckDetector = bottleneckDetector;
 319            bottleneckDetector.RegisterAccessNotificationSink(this);
 320        }
 321    }
 322    public void BottleneckEntered(AmbientBottleneckAccessor? bottleneckAccessor)
 323    {
 324#if NET5_0_OR_GREATER
 325        ArgumentNullException.ThrowIfNull(bottleneckAccessor);
 326#else
 327        if (bottleneckAccessor is null) throw new ArgumentNullException(nameof(bottleneckAccessor));
 328#endif
 329        _currentWindowSurvey.BottleneckEntered(bottleneckAccessor);
 330    }
 331
 332    public void BottleneckExited(AmbientBottleneckAccessor? bottleneckAccessor)
 333    {
 334#if NET5_0_OR_GREATER
 335        ArgumentNullException.ThrowIfNull(bottleneckAccessor);
 336#else
 337        if (bottleneckAccessor is null) throw new ArgumentNullException(nameof(bottleneckAccessor));
 338#endif
 339        _currentWindowSurvey.BottleneckExited(bottleneckAccessor);
 340    }
 341
 342
 343    protected virtual void Dispose(bool disposing)
 344    {
 345        if (!_disposedValue)
 346        {
 347            if (disposing)
 348            {
 349                _timer.Dispose();
 350                _bottleneckDetector?.DeregisterAccessNotificationSink(this);
 351            }
 352
 353            // TODO: free unmanaged resources (unmanaged objects) and override finalizer
 354            // TODO: set large fields to null
 355            _disposedValue = true;
 356        }
 357    }
 358
 359    // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
 360    // ~TimeWindowSurveyor()
 361    // {
 362    //     // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
 363    //     Dispose(disposing: false);
 364    // }
 365
 366    public void Dispose()
 367    {
 368        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
 369        Dispose(disposing: true);
 370        GC.SuppressFinalize(this);
 371    }
 372}
 373/// <summary>
 374/// A class that accumulates the bottleneck survey for one time window.
 375/// </summary>
 376/// <remarks>
 377/// <pitch>One window's worth of bottleneck accounting, charging each window only the usage that occurred within it even
 378/// <pledge><see cref="IAmbientBottleneckSurvey"/></pledge>
 379/// <pledge>Only accesses whose bottleneck identifier passes the allow/block regex filters are accumulated, and reported
 380/// <plan>
 381/// Accesses accumulate in a <see cref="ConcurrentDictionary{TKey,TValue}"/> keyed by bottleneck identifier, combining r
 382/// </plan>
 383/// </remarks>
 384internal class TimeWindowBottleneckSurvey : IAmbientBottleneckExitNotificationSink, IAmbientBottleneckSurvey
 385{
 386    private readonly Regex? _allow;
 387    private readonly Regex? _block;
 388    private readonly long _windowStartStopwatchTicks;
 389    private readonly ConcurrentDictionary<string, AmbientBottleneckAccessor> _metrics;
 390    private readonly ConcurrentDictionary<string, (long, double)> _startAccessCountAndLimitUsage;
 391
 392    public TimeWindowBottleneckSurvey(Regex? allow, Regex? block, long stopwatchTicks, TimeSpan windowSize)
 393    {
 394        string windowName = WindowScope.WindowId(AmbientClock.UtcNow, windowSize);
 395        ScopeName = "TimeWindow " + windowName + "(" + WindowScope.WindowSize(windowSize) + ")";
 396        _allow = allow;
 397        _block = block;
 398        _windowStartStopwatchTicks = stopwatchTicks;
 399        _metrics = new ConcurrentDictionary<string, AmbientBottleneckAccessor>();
 400        _startAccessCountAndLimitUsage = new ConcurrentDictionary<string, (long, double)>();
 401    }
 402
 403    public string ScopeName { get; }
 404
 405    public AmbientBottleneckAccessor? MostUtilizedBottleneck => _metrics.Values.Max();
 406
 407    public IEnumerable<AmbientBottleneckAccessor> GetMostUtilizedBottlenecks(int count)
 408    {
 409        return _metrics.Values.OrderBy(m => m.Utilization).Take(count);
 410    }
 411
 412    internal void SwitchAnalyzer(TimeWindowBottleneckSurvey oldAnalyzer)
 413    {
 414        if (oldAnalyzer != null)
 415        {
 416            long nowStopwatchTicks = AmbientClock.Ticks;
 417            // enumerate the accessors that are still open
 418            foreach (AmbientBottleneckAccessor access in oldAnalyzer._metrics.Values)
 419            {
 420                string bottleneckId = access.Bottleneck.Id;
 421                (long, double) accessCountAndLimitUsage;
 422                if (!_startAccessCountAndLimitUsage.TryGetValue(bottleneckId, out accessCountAndLimitUsage)) accessCount
 423                (AmbientBottleneckAccessor, AmbientBottleneckAccessor?) records = access.Split(_windowStartStopwatchTick
 424                // replace the entry in the old dictionary
 425                oldAnalyzer._metrics[bottleneckId] = records.Item1;
 426                // add the new half into the new window.  note that when the original instance (which we don't own) fini
 427                if (records.Item2 is not null) _metrics.AddOrUpdate(bottleneckId, records.Item2, (s, m) => m.Combine(rec
 428                // update the starting access count and the limit usage for the new window
 429                _startAccessCountAndLimitUsage[bottleneckId] = (access.AccessCount, access.LimitUsed);
 430            }
 431        }
 432    }
 433
 434    public void BottleneckEntered(AmbientBottleneckAccessor bottleneckAccessor)
 435    {
 436#if NET5_0_OR_GREATER
 437        ArgumentNullException.ThrowIfNull(bottleneckAccessor);
 438#else
 439        if (bottleneckAccessor is null) throw new ArgumentNullException(nameof(bottleneckAccessor));
 440#endif
 441        // is this bottleneck being surveyed?
 442        string bottleneckId = bottleneckAccessor.Bottleneck.Id;
 443        bool blocked = _block?.IsMatch(bottleneckId) ?? false;
 444        bool allowed = !blocked && (_allow?.IsMatch(bottleneckId) ?? true);
 445        if (allowed)
 446        {
 447            _metrics.AddOrUpdate(bottleneckId, bottleneckAccessor, (s, m) => m.Combine(bottleneckAccessor));
 448        }
 449    }
 450
 451    public void BottleneckExited(AmbientBottleneckAccessor bottleneckAccessor)
 452    {
 453#if NET5_0_OR_GREATER
 454        ArgumentNullException.ThrowIfNull(bottleneckAccessor);
 455#else
 456        if (bottleneckAccessor is null) throw new ArgumentNullException(nameof(bottleneckAccessor));
 457#endif
 458        // is this bottleneck being surveyed?
 459        string bottleneckId = bottleneckAccessor.Bottleneck.Id;
 460        bool blocked = _block?.IsMatch(bottleneckId) ?? false;
 461        bool allowed = !blocked && (_allow?.IsMatch(bottleneckId) ?? true);
 462        if (allowed)
 463        {
 464            _metrics.AddOrUpdate(bottleneckId, bottleneckAccessor, (s, m) => m.Combine(bottleneckAccessor));
 465        }
 466    }
 467}
 468/// <summary>
 469/// A class that accumulates a bottleneck survey for the whole process, across all threads and call contexts.
 470/// </summary>
 471/// <remarks>
 472/// <pitch>The everything-since-construction survey: useful for short-lived processes measured start to finish; for long
 473/// <pledge><see cref="IAmbientBottleneckSurveyor"/></pledge>
 474/// <pledge><see cref="IAmbientBottleneckExitNotificationSink"/></pledge>
 475/// <pledge>Only accesses whose bottleneck identifier passes the allow/block regex filters are accumulated.  Accumulatio
 476/// <plan>Registers directly with the detector at construction (deregistering on disposal) and accumulates into a <see c
 477/// </remarks>
 478#if NET5_0_OR_GREATER
 479[UnsupportedOSPlatform("browser")]
 480#endif
 481internal class ProcessBottleneckSurveyor : IAmbientBottleneckExitNotificationSink, IAmbientBottleneckSurveyor
 482{
 483    private readonly IAmbientBottleneckDetector? _bottleneckDetector;
 484    private readonly Regex? _allow;
 485    private readonly Regex? _block;
 486    private readonly ConcurrentDictionary<string, AmbientBottleneckAccessor> _metrics;
 487    private bool _disposedValue;
 488
 489    public ProcessBottleneckSurveyor(string? processScopeName, IAmbientBottleneckDetector? bottleneckDetector, Regex? al
 490    {
 491        System.Diagnostics.Process process = Process.GetCurrentProcess();
 492        ScopeName = (string.IsNullOrEmpty(processScopeName) ? FormattableString.Invariant($"Process {process.ProcessName
 493        _allow = allow;
 494        _block = block;
 495        _metrics = new ConcurrentDictionary<string, AmbientBottleneckAccessor>();
 496        if (bottleneckDetector != null)
 497        {
 498            _bottleneckDetector = bottleneckDetector;
 499            bottleneckDetector.RegisterAccessNotificationSink(this);
 500        }
 501    }
 502
 503    public string ScopeName { get; }
 504
 505    public AmbientBottleneckAccessor? MostUtilizedBottleneck => _metrics.Values.Max();
 506
 507    public IEnumerable<AmbientBottleneckAccessor> GetMostUtilizedBottlenecks(int count)
 508    {
 509        return _metrics.Values.OrderBy(m => m.Utilization).Take(count);
 510    }
 511
 512    public void BottleneckExited(AmbientBottleneckAccessor? bottleneckAccessor)
 513    {
 514#if NET5_0_OR_GREATER
 515        ArgumentNullException.ThrowIfNull(bottleneckAccessor);
 516#else
 517        if (bottleneckAccessor is null) throw new ArgumentNullException(nameof(bottleneckAccessor));
 518#endif
 519        string bottleneckId = bottleneckAccessor.Bottleneck.Id;
 520        // is this bottleneck being surveyed?
 521        bool blocked = _block?.IsMatch(bottleneckId) ?? false;
 522        bool allowed = !blocked && (_allow?.IsMatch(bottleneckId) ?? true);
 523        if (allowed)
 524        {
 525            _metrics.AddOrUpdate(bottleneckId, bottleneckAccessor, (s, m) => m.Combine(bottleneckAccessor));
 526        }
 527    }
 528
 529    protected virtual void Dispose(bool disposing)
 530    {
 531        if (!_disposedValue)
 532        {
 533            if (disposing)
 534            {
 535                _bottleneckDetector?.DeregisterAccessNotificationSink(this);
 536            }
 537
 538            // TODO: free unmanaged resources (unmanaged objects) and override finalizer
 539            // TODO: set large fields to null
 540            _disposedValue = true;
 541        }
 542    }
 543
 544    // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
 545    // ~ProcessBottleneckSurveyor()
 546    // {
 547    //     // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
 548    //     Dispose(disposing: false);
 549    // }
 550
 551    public void Dispose()
 552    {
 553        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
 554        Dispose(disposing: true);
 555        GC.SuppressFinalize(this);
 556    }
 557}
 558
 559/// <summary>
 560/// A class that routes bottleneck exit notifications to surveyors scoped to the thread that created them.
 561/// </summary>
 562/// <remarks>
 563/// <pitch>The per-thread analog of <see cref="CallContextSurveyManager"/>: one process-wide registration with the detec
 564/// <pledge><see cref="IAmbientBottleneckExitNotificationSink"/></pledge>
 565/// <plan>Registers itself with the detector once at construction and holds a <see cref="ThreadLocal{T}"/> of per-thread
 566/// </remarks>
 567internal class ThreadSurveyManager : IAmbientBottleneckExitNotificationSink, IDisposable
 568{
 569    private readonly IAmbientBottleneckDetector? _bottleneckDetector;
 570    private ThreadLocal<ThreadAccessDistributor>? _threadDistributors;
 571    private bool disposedValue;
 572
 573    public ThreadSurveyManager(IAmbientBottleneckDetector? bottleneckDetector)
 574    {
 575        _threadDistributors = new ThreadLocal<ThreadAccessDistributor>();
 576        if (bottleneckDetector != null)
 577        {
 578            _bottleneckDetector = bottleneckDetector;
 579            bottleneckDetector.RegisterAccessNotificationSink(this);
 580        }
 581    }
 582
 583    /// <summary>
 584    /// Gets the calling thread's distributor, or null if this manager has been disposed.
 585    /// </summary>
 586    private ThreadAccessDistributor? ThreadDistributor
 587    {
 588        get
 589        {
 590            // read the field once: Dispose swaps it out concurrently, so re-reading it could see the live instance and 
 591            ThreadLocal<ThreadAccessDistributor>? threadDistributors = _threadDistributors;
 592            if (threadDistributors == null) return null;
 593            try
 594            {
 595                ThreadAccessDistributor? threadDistributor = threadDistributors.Value;
 596                if (threadDistributor == null)
 597                {
 598                    threadDistributor = new ThreadAccessDistributor();
 599                    threadDistributors.Value = threadDistributor;
 600                }
 601                return threadDistributor;
 602            }
 603            catch (ObjectDisposedException)
 604            {
 605                // Dispose reached the thread-local between the read above and this access.  ThreadLocal exposes no way 
 606                // disposal that does not race it in exactly the same way, so the exception is the only signal available
 607                // a lock here would put one on the hot path of every bottleneck exit.  There is no surveyor left to rou
 608                return null;
 609            }
 610        }
 611    }
 612    void IAmbientBottleneckExitNotificationSink.BottleneckExited(AmbientBottleneckAccessor bottleneckAccessor)
 613    {
 614        // a null distributor means this manager was disposed while the notification was in flight; dropping it is corre
 615        // would surface as an ObjectDisposedException inside an unrelated caller's accessor Dispose
 616        ThreadDistributor?.BottleneckExited(bottleneckAccessor);
 617    }
 618
 619    internal ThreadBottleneckSurveyor CreateThreadSurveyor(string? scopeName, Regex? allow, Regex? block)
 620    {
 621        ThreadAccessDistributor? threadDistributor = ThreadDistributor;
 622        // unlike a notification, creating a surveyor from a disposed manager is a caller error rather than a lost race
 623#if NET7_0_OR_GREATER
 624        ObjectDisposedException.ThrowIf(threadDistributor == null, this);
 625#else
 626        if (threadDistributor == null) throw new ObjectDisposedException(nameof(ThreadSurveyManager));
 627#endif
 628        ThreadBottleneckSurveyor surveyor = new(scopeName, threadDistributor, allow, block);
 629        return surveyor;
 630    }
 631
 632    protected virtual void Dispose(bool disposing)
 633    {
 634        if (!disposedValue)
 635        {
 636            if (disposing)
 637            {
 638                _bottleneckDetector?.DeregisterAccessNotificationSink(this);
 639                // swap the storage away before disposing it: a notification that already passed the null check keeps a 
 640                // but every notification arriving after this point sees null and is dropped instead of faulting on a di
 641                ThreadLocal<ThreadAccessDistributor>? threadDistributors = Interlocked.Exchange(ref _threadDistributors,
 642                threadDistributors?.Dispose();
 643            }
 644
 645            // TODO: free unmanaged resources (unmanaged objects) and override finalizer
 646            // TODO: set large fields to null
 647            disposedValue = true;
 648        }
 649    }
 650
 651    // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
 652    // ~ThreadSurveyManager()
 653    // {
 654    //     // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
 655    //     Dispose(disposing: false);
 656    // }
 657
 658    public void Dispose()
 659    {
 660        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
 661        Dispose(disposing: true);
 662        GC.SuppressFinalize(this);
 663    }
 664}
 665/// <summary>
 666/// A class that fans bottleneck exit notifications out to the sinks registered by one thread.
 667/// </summary>
 668/// <remarks>
 669/// <pitch>The fan-out node for one thread: surveyors register here instead of with the process-wide detector, so their 
 670/// <pledge><see cref="IAmbientBottleneckExitNotificationSink"/></pledge>
 671/// <plan>A <see cref="ConcurrentHashSet{T}"/> of sinks with synchronous fan-out; idempotent registration.</plan>
 672/// </remarks>
 673internal class ThreadAccessDistributor : IAmbientBottleneckExitNotificationSink
 674{
 675    private readonly ConcurrentHashSet<IAmbientBottleneckExitNotificationSink> _notificationSinks = new();
 676
 677    public ThreadAccessDistributor()
 678    {
 679    }
 680    public void BottleneckExited(AmbientBottleneckAccessor bottleneckAccessor)
 681    {
 682        foreach (IAmbientBottleneckExitNotificationSink notificationSink in _notificationSinks)
 683        {
 684            notificationSink.BottleneckExited(bottleneckAccessor);
 685        }
 686    }
 687    public bool RegisterAccessNotificationSink(IAmbientBottleneckExitNotificationSink sink)
 688    {
 689        return _notificationSinks.Add(sink);
 690    }
 691    public bool DeregisterAccessNotificationSink(IAmbientBottleneckExitNotificationSink sink)
 692    {
 693        return _notificationSinks.Remove(sink);
 694    }
 695}
 696/// <summary>
 697/// A class that accumulates a bottleneck survey for a single thread.
 698/// </summary>
 699/// <remarks>
 700/// <pitch>The survey collector for one thread's accesses, bracketed by construction and disposal; the default scope nam
 701/// <pledge><see cref="IAmbientBottleneckSurveyor"/></pledge>
 702/// <pledge><see cref="IAmbientBottleneckExitNotificationSink"/></pledge>
 703/// <pledge>Only accesses whose bottleneck identifier passes the allow/block regex filters are accumulated.  Because del
 704/// <plan>Registers with its thread's <see cref="ThreadAccessDistributor"/> at construction (deregistering on disposal) 
 705/// </remarks>
 706internal class ThreadBottleneckSurveyor : IAmbientBottleneckSurveyor, IAmbientBottleneckExitNotificationSink
 707{
 708    private readonly ThreadAccessDistributor _threadDistributor;
 709    private readonly Regex? _allow;
 710    private readonly Regex? _block;
 711    private readonly Dictionary<string, AmbientBottleneckAccessor> _metrics;
 712    private bool _disposedValue;
 713
 714    public ThreadBottleneckSurveyor(string? scopeName, ThreadAccessDistributor threadDistributor, Regex? allow, Regex? b
 715    {
 716        ScopeName = (string.IsNullOrEmpty(scopeName) ? (string.IsNullOrEmpty(Thread.CurrentThread.Name) ? $"Thread {Envi
 717        _allow = allow;
 718        _block = block;
 719        _metrics = new Dictionary<string, AmbientBottleneckAccessor>();
 720        _threadDistributor = threadDistributor;
 721        threadDistributor.RegisterAccessNotificationSink(this);
 722    }
 723
 724    public string ScopeName { get; }
 725
 726    public AmbientBottleneckAccessor? MostUtilizedBottleneck => _metrics.Values.Max();
 727
 728    public IEnumerable<AmbientBottleneckAccessor> GetMostUtilizedBottlenecks(int count)
 729    {
 730        return _metrics.Values.OrderBy(m => m.Utilization).Take(count);
 731    }
 732
 733    public void BottleneckExited(AmbientBottleneckAccessor bottleneckAccessor)
 734    {
 735#if NET5_0_OR_GREATER
 736        ArgumentNullException.ThrowIfNull(bottleneckAccessor);
 737#else
 738        if (bottleneckAccessor is null) throw new ArgumentNullException(nameof(bottleneckAccessor));
 739#endif
 740        string bottleneckId = bottleneckAccessor.Bottleneck.Id;
 741        // is this bottleneck being surveyed?
 742        bool blocked = _block?.IsMatch(bottleneckId) ?? false;
 743        bool allowed = !blocked && (_allow?.IsMatch(bottleneckId) ?? true);
 744        if (allowed)
 745        {
 746            AmbientBottleneckAccessor? metric;
 747            if (_metrics.TryGetValue(bottleneckId, out metric))
 748            {
 749                _metrics[bottleneckId] = metric.Combine(bottleneckAccessor);
 750            }
 751            else
 752            {
 753                _metrics.Add(bottleneckId, bottleneckAccessor);
 754            }
 755        }
 756    }
 757
 758    protected virtual void Dispose(bool disposing)
 759    {
 760        if (!_disposedValue)
 761        {
 762            if (disposing)
 763            {
 764                _threadDistributor.DeregisterAccessNotificationSink(this);
 765            }
 766
 767            // TODO: free unmanaged resources (unmanaged objects) and override finalizer
 768            // TODO: set large fields to null
 769            _disposedValue = true;
 770        }
 771    }
 772
 773    // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
 774    // ~ThreadBottleneckAnalyzer()
 775    // {
 776    //     // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
 777    //     Dispose(disposing: false);
 778    // }
 779
 780    public void Dispose()
 781    {
 782        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
 783        Dispose(disposing: true);
 784        GC.SuppressFinalize(this);
 785    }
 786}