/* ========================================================================
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Tracing;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using NUnit.Framework;
using Opc.Ua.Client.TestFramework;
using Opc.Ua.Server.TestFramework;
using Opc.Ua.Tests;
using Quickstarts.ReferenceServer;
using ISession = Opc.Ua.Client.ISession;
using MonitoredItem = Opc.Ua.Client.MonitoredItem;
using Subscription = Opc.Ua.Client.Subscription;
#if NET10_0_OR_GREATER
namespace Opc.Ua.Sessions.Tests
{
///
/// Explicit subscription-driven ArrayPool diagnostics for issue #3988.
///
[TestFixture]
[Explicit]
[NonParallelizable]
[Category("LoadTest")]
[Category("ArrayPool")]
[SetCulture("en-us")]
[SetUICulture("en-us")]
public sealed class SubscriptionArrayPoolDiagnosticsLoadTests : ClientTestFramework
{
private const int kParallelPublishThreshold = 256;
private const int kPublishingIntervalMs = 50;
private const int kWriterIntervalMs = 10;
private const int kScenarioDurationMs = 6000;
private const int kTrackedNodeCount = 1;
private const int kTcpMinBufferSize = 8192;
private const int kBufferCookieLength = 1;
private const int kOutstandingToleranceCount = 8;
private const int kPeakOutstandingMultiplier = 8;
private int m_transportMaxBufferSize = 65535;
public SubscriptionArrayPoolDiagnosticsLoadTests()
: base(Utils.UriSchemeOpcTcp, NUnitTelemetryContext.Create(logLevel: LogLevel.Warning))
{
SingleSession = false;
UseSamplingGroupsInReferenceNodeManager = false;
}
[Test]
[TestCase(65535, TestName = "SubscriptionArrayPoolDiagnostics_MaxBufferSize65535")]
[TestCase(65536, TestName = "SubscriptionArrayPoolDiagnostics_MaxBufferSize65536")]
public async Task SubscriptionPublishesKeepArrayPoolBalancedAsync(int maxBufferSize)
{
ConfigureFixture(maxBufferSize);
try
{
await OneTimeSetUpAsync().ConfigureAwait(false);
ArrayPoolDiagnosticScenario[] scenarios =
[
new(
"serial-threshold-minus-one",
kParallelPublishThreshold - 1,
kScenarioDurationMs,
kWriterIntervalMs,
kPublishingIntervalMs,
SlowConsumerDelayMs: 0,
SlowConsumerEveryNthSession: 0),
new(
"parallel-threshold-plus-one",
kParallelPublishThreshold + 1,
kScenarioDurationMs,
kWriterIntervalMs,
kPublishingIntervalMs,
SlowConsumerDelayMs: 0,
SlowConsumerEveryNthSession: 0),
new(
"parallel-threshold-plus-one-slow-consumer",
kParallelPublishThreshold + 1,
kScenarioDurationMs,
kWriterIntervalMs,
kPublishingIntervalMs,
SlowConsumerDelayMs: 5,
SlowConsumerEveryNthSession: 8)
];
foreach (ArrayPoolDiagnosticScenario scenario in scenarios)
{
await RunScenarioAsync(scenario).ConfigureAwait(false);
}
}
finally
{
await OneTimeTearDownAsync().ConfigureAwait(false);
}
}
public override async Task OneTimeSetUpAsync()
{
await OneTimeSetUpCoreAsync(securityNone: true).ConfigureAwait(false);
ClientFixture.Config.TransportQuotas!.MaxBufferSize = m_transportMaxBufferSize;
ClientFixture.SessionTimeout = 300_000;
ClientFixture.OperationTimeout = 90_000;
}
public override async Task CreateReferenceServerFixtureAsync(
bool enableTracing,
bool disableActivityLogging,
bool securityNone)
{
ServerFixture = new ServerFixture(
t => new ReferenceServer(t),
enableTracing,
disableActivityLogging)
{
UriScheme = UriScheme,
SecurityNone = securityNone,
AutoAccept = true,
AllNodeManagers = AllNodeManagers,
OperationLimits = true,
UseSamplingGroupsInReferenceNodeManager = UseSamplingGroupsInReferenceNodeManager,
TransportBindingRegistry = TransportBindingRegistry
};
await ServerFixture.LoadConfigurationAsync(PkiRoot).ConfigureAwait(false);
ServerFixture.Config.TransportQuotas!.MaxBufferSize = m_transportMaxBufferSize;
ServerFixture.Config.TransportQuotas.MaxMessageSize = TransportQuotaMaxMessageSize;
ServerFixture.Config.TransportQuotas.MaxByteStringLength = ServerFixture
.Config
.TransportQuotas
.MaxStringLength = TransportQuotaMaxStringLength;
ServerFixture.Config.TransportQuotas.SecurityTokenLifetime = SecurityTokenLifetime;
void AddExplicitUserTokenPolicies(string securityPolicyUri)
{
if (SecurityPolicies.Default.GetInfo(securityPolicyUri) == null)
{
return;
}
ServerFixture.Config.ServerConfiguration.UserTokenPolicies +=
new UserTokenPolicy(UserTokenType.UserName)
{
SecurityPolicyUri = securityPolicyUri
};
ServerFixture.Config.ServerConfiguration.UserTokenPolicies +=
new UserTokenPolicy(UserTokenType.Certificate)
{
SecurityPolicyUri = securityPolicyUri
};
ServerFixture.Config.ServerConfiguration.UserTokenPolicies +=
new UserTokenPolicy(UserTokenType.IssuedToken)
{
IssuedTokenType = Profiles.JwtUserToken,
PolicyId = Profiles.JwtUserToken,
SecurityPolicyUri = securityPolicyUri
};
}
ServerFixture.Config.ServerConfiguration!.UserTokenPolicies +=
new UserTokenPolicy(UserTokenType.UserName);
ServerFixture.Config.ServerConfiguration.UserTokenPolicies +=
new UserTokenPolicy(UserTokenType.Certificate);
ServerFixture.Config.ServerConfiguration.UserTokenPolicies +=
new UserTokenPolicy(UserTokenType.IssuedToken)
{
IssuedTokenType = Profiles.JwtUserToken
};
AddExplicitUserTokenPolicies(SecurityPolicies.Basic128Rsa15);
foreach (string securityPolicyUri in GetSupportedEccPolicyUris())
{
AddExplicitUserTokenPolicies(securityPolicyUri);
}
ServerFixture.Config.ServerConfiguration.MaxChannelCount = MaxChannelCount;
ServerFixture.Config.ServerConfiguration.MaxSessionCount = MaxSessionCount;
ServerFixture.Config.ServerConfiguration.MaxFailedAuthenticationAttempts =
MaxFailedAuthenticationAttempts;
ServerFixture.Config.ServerConfiguration.MaxRequestThreadCount =
MaxRequestThreadCount;
ServerFixture.Config.ServerConfiguration.MinRequestThreadCount =
MinRequestThreadCount;
ServerFixture.Config.ServerConfiguration.MaxSubscriptionCount =
MaxSubscriptionCount;
ServerFixture.Config.ServerConfiguration.MaxQueuedRequestCount = 100000;
ReferenceServer = await ServerFixture.StartAsync().ConfigureAwait(false);
ReferenceServer.TokenValidator = TokenValidator;
ServerFixturePort = ServerFixture.Port;
}
private void ConfigureFixture(int maxBufferSize)
{
m_transportMaxBufferSize = maxBufferSize;
MaxChannelCount = 323;
MaxSessionCount = 320;
MaxSubscriptionCount = 320;
MaxFailedAuthenticationAttempts = 0;
MaxRequestThreadCount = 200;
MinRequestThreadCount = 50;
}
private async Task RunScenarioAsync(ArrayPoolDiagnosticScenario scenario)
{
var readers = new ConcurrentBag();
var writerErrors = new ConcurrentQueue();
long notificationCount = 0;
long callbackCount = 0;
int writeCount = 0;
bool readersClosed = false;
CancellationTokenSource? writerCts = null;
Task? writerTask = null;
ISession? writerSession = null;
try
{
writerSession = await ClientFixture
.ConnectAsync(ServerUrl, SecurityPolicies.None)
.ConfigureAwait(false);
IDictionary nodes = GetTestSetStaticMassNumeric(
writerSession.NamespaceUris);
KeyValuePair[] monitoredNodes = [.. nodes
.Where(static value => value.Value == typeof(uint))
.Take(kTrackedNodeCount)];
if (monitoredNodes.Length < kTrackedNodeCount)
{
Assert.Ignore("No writable UInt32 nodes were found for the subscription load harness.");
}
await CreateReaderSessionsAsync(
scenario,
monitoredNodes,
readers,
() => Interlocked.Increment(ref callbackCount),
count => Interlocked.Add(ref notificationCount, count))
.ConfigureAwait(false);
await Task.Delay(TimeSpan.FromMilliseconds(500)).ConfigureAwait(false);
int trackedMinimumBufferSize =
GetTrackedMinimumBufferSize(m_transportMaxBufferSize);
using var listener = new ArrayPoolEventListener(trackedMinimumBufferSize);
long allocatedBytesBefore = GetTotalAllocatedBytes();
int gen2Before = GC.CollectionCount(2);
writerCts = new CancellationTokenSource();
// CA2025 false positive: writerTask is awaited in the timed run and
// again in finally before writerSession is closed/disposed.
// TODO: Refactor the harness shape once CA2025 recognizes this pattern.
#pragma warning disable CA2025
writerTask = RunWriterAsync(
writerSession,
monitoredNodes,
scenario,
writerErrors,
() => Interlocked.Increment(ref writeCount),
writerCts.Token);
#pragma warning restore CA2025
await Task.Delay(
TimeSpan.FromMilliseconds(scenario.DurationMs))
.ConfigureAwait(false);
writerCts.Cancel();
try
{
await writerTask.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Expected when the timed run ends.
}
await Task.Delay(
TimeSpan.FromMilliseconds(
(scenario.PublishingIntervalMs * 4) +
(scenario.SlowConsumerDelayMs * 8)))
.ConfigureAwait(false);
await CloseSessionsAsync(readers.Select(static value => value.Session))
.ConfigureAwait(false);
readersClosed = true;
await CloseSessionAsync(writerSession).ConfigureAwait(false);
writerSession = null;
bool quiesced = await WaitForQuiescenceAsync(
listener,
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(20))
.ConfigureAwait(false);
ArrayPoolMetrics metrics = listener.CreateMetrics();
long allocatedBytesAfter = GetTotalAllocatedBytes();
int gen2After = GC.CollectionCount(2);
WriteScenarioResults(
scenario,
writeCount,
callbackCount,
notificationCount,
allocatedBytesAfter - allocatedBytesBefore,
gen2After - gen2Before,
quiesced,
writerErrors,
metrics);
Assert.That(writeCount, Is.GreaterThan(0), "The writer produced no writes.");
Assert.That(callbackCount, Is.GreaterThan(0), "No data change callbacks were observed.");
Assert.That(notificationCount, Is.GreaterThan(0), "No monitored item notifications were observed.");
Assert.That(quiesced, Is.True, "ArrayPool activity did not quiesce after the scenario.");
Assert.That(metrics.RentedCount, Is.GreaterThan(0), "No tracked ArrayPool rents were observed.");
Assert.That(metrics.ReturnedCount, Is.GreaterThan(0), "No tracked ArrayPool returns were observed.");
Assert.That(
Math.Abs(metrics.OutstandingCount),
Is.LessThanOrEqualTo(kOutstandingToleranceCount),
"Tracked ArrayPool rent/return balance did not settle after quiescence.");
Assert.That(
Math.Abs(metrics.OutstandingBytes),
Is.LessThanOrEqualTo(
(long)kOutstandingToleranceCount * metrics.MaxObservedBufferSize),
"Tracked ArrayPool outstanding bytes did not settle after quiescence.");
Assert.That(
metrics.PeakOutstandingCount,
Is.LessThanOrEqualTo(
scenario.PublishQueueCount * kPeakOutstandingMultiplier),
"Tracked ArrayPool outstanding buffers grew beyond the session-scaled envelope.");
}
finally
{
writerCts?.Cancel();
if (writerTask != null)
{
try
{
await writerTask.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Expected during cleanup.
}
}
writerCts?.Dispose();
if (!readersClosed)
{
await CloseSessionsAsync(readers.Select(static value => value.Session))
.ConfigureAwait(false);
}
if (writerSession != null)
{
await CloseSessionAsync(writerSession).ConfigureAwait(false);
}
}
}
private async Task CreateReaderSessionsAsync(
ArrayPoolDiagnosticScenario scenario,
KeyValuePair[] monitoredNodes,
ConcurrentBag readers,
Action onCallback,
Action onNotification)
{
const int maxConcurrentConnects = 24;
using var gate = new SemaphoreSlim(maxConcurrentConnects, maxConcurrentConnects);
var tasks = new List(scenario.PublishQueueCount);
for (int sessionIndex = 0; sessionIndex < scenario.PublishQueueCount; sessionIndex++)
{
int localSessionIndex = sessionIndex;
tasks.Add(Task.Run(async () =>
{
await gate.WaitAsync().ConfigureAwait(false);
try
{
ISession session = await ClientFixture
.ConnectAsync(ServerUrl, SecurityPolicies.None)
.ConfigureAwait(false);
session.MinPublishRequestCount = 1;
session.MaxPublishRequestCount = 1;
var subscription = new Subscription(session.DefaultSubscription)
{
PublishingEnabled = true,
PublishingInterval = scenario.PublishingIntervalMs,
SequentialPublishing = true,
DisableMonitoredItemCache = true
};
foreach (KeyValuePair monitoredNode in monitoredNodes)
{
var item = new MonitoredItem(subscription.DefaultItem)
{
StartNodeId = monitoredNode.Key,
AttributeId = Attributes.Value,
MonitoringMode = MonitoringMode.Reporting,
SamplingInterval = 0,
QueueSize = 1
};
subscription.AddItem(item);
}
bool isSlowConsumer = scenario.SlowConsumerDelayMs > 0 &&
scenario.SlowConsumerEveryNthSession > 0 &&
localSessionIndex % scenario.SlowConsumerEveryNthSession == 0;
subscription.FastDataChangeCallback = (_, notification, _) =>
{
onCallback();
onNotification(notification.MonitoredItems.Count);
if (isSlowConsumer)
{
Thread.Sleep(scenario.SlowConsumerDelayMs);
}
};
session.AddSubscription(subscription);
await subscription.CreateAsync().ConfigureAwait(false);
readers.Add(new ReaderSessionBundle(session, subscription));
}
finally
{
gate.Release();
}
}));
}
await Task.WhenAll(tasks).ConfigureAwait(false);
}
private async Task RunWriterAsync(
ISession writerSession,
KeyValuePair[] monitoredNodes,
ArrayPoolDiagnosticScenario scenario,
ConcurrentQueue writerErrors,
Action onWrite,
CancellationToken ct)
{
WriteValue[] nodesToWrite = [.. monitoredNodes
.Select(static value => new WriteValue
{
NodeId = value.Key,
AttributeId = Attributes.Value,
Value = new DataValue(new Variant((uint)0))
})];
uint nextValue = 0;
while (!ct.IsCancellationRequested)
{
nextValue++;
for (int i = 0; i < nodesToWrite.Length; i++)
{
nodesToWrite[i].Value = new DataValue(new Variant(nextValue));
}
try
{
await writerSession.WriteAsync(null, nodesToWrite, ct).ConfigureAwait(false);
onWrite();
}
catch (OperationCanceledException)
{
throw;
}
catch (ServiceResultException sre)
when (sre.StatusCode == StatusCodes.BadRequestInterrupted ||
sre.StatusCode == StatusCodes.BadSecureChannelClosed ||
sre.StatusCode == StatusCodes.BadSessionIdInvalid)
{
writerErrors.Enqueue(sre.StatusCode.ToString());
}
catch (Exception ex)
{
writerErrors.Enqueue(ex.GetType().Name + ":" + ex.Message);
}
await Task.Delay(
TimeSpan.FromMilliseconds(scenario.WriterIntervalMs),
ct)
.ConfigureAwait(false);
}
}
private static async Task CloseSessionsAsync(IEnumerable sessions)
{
ISession[] sessionArray = [.. sessions
.Where(static value => value != null)
.Distinct()];
if (sessionArray.Length == 0)
{
return;
}
await Task.WhenAll(sessionArray.Select(CloseSessionAsync)).ConfigureAwait(false);
}
private static async Task CloseSessionAsync(ISession session)
{
try
{
await session.CloseAsync().ConfigureAwait(false);
}
catch
{
// Best-effort cleanup for an explicit diagnostics harness.
}
finally
{
session.Dispose();
}
}
private static async Task WaitForQuiescenceAsync(
ArrayPoolEventListener listener,
TimeSpan stablePeriod,
TimeSpan timeout)
{
long deadline = Stopwatch.GetTimestamp() +
(long)(timeout.TotalSeconds * Stopwatch.Frequency);
while (Stopwatch.GetTimestamp() < deadline)
{
if (listener.GetIdleDuration() >= stablePeriod)
{
return true;
}
await Task.Delay(250).ConfigureAwait(false);
}
return listener.GetIdleDuration() >= stablePeriod;
}
private void WriteScenarioResults(
ArrayPoolDiagnosticScenario scenario,
int writeCount,
long callbackCount,
long notificationCount,
long allocatedBytesDelta,
int gen2CollectionsDelta,
bool quiesced,
ConcurrentQueue writerErrors,
ArrayPoolMetrics metrics)
{
TestContext.Out.WriteLine(
$"ARRAYPOOL_DIAGNOSTIC scenario={scenario.Name}; " +
$"maxBufferSize={m_transportMaxBufferSize}; " +
$"publishQueues={scenario.PublishQueueCount}; " +
$"slowConsumerDelayMs={scenario.SlowConsumerDelayMs}; " +
$"slowConsumerEveryNthSession={scenario.SlowConsumerEveryNthSession}; " +
$"writes={writeCount}; " +
$"callbacks={callbackCount}; " +
$"notifications={notificationCount}; " +
$"allocatedBytesDelta={allocatedBytesDelta}; " +
$"gen2CollectionsDelta={gen2CollectionsDelta}; " +
$"quiesced={quiesced}; " +
$"rented={metrics.RentedCount}; " +
$"returned={metrics.ReturnedCount}; " +
$"allocated={metrics.AllocatedCount}; " +
$"dropped={metrics.DroppedCount}; " +
$"trimmed={metrics.TrimmedCount}; " +
$"outstanding={metrics.OutstandingCount}; " +
$"outstandingBytes={metrics.OutstandingBytes}; " +
$"peakOutstanding={metrics.PeakOutstandingCount}; " +
$"peakOutstandingBytes={metrics.PeakOutstandingBytes}; " +
$"maxObservedBufferSize={metrics.MaxObservedBufferSize}; " +
$"writerErrors={writerErrors.Count}");
foreach ((int bufferSize, long count) in metrics.BufferSizeHistogram)
{
TestContext.Out.WriteLine(
$"ARRAYPOOL_DIAGNOSTIC_BUFFER scenario={scenario.Name}; " +
$"bufferSize={bufferSize}; " +
$"count={count}");
}
if (!writerErrors.IsEmpty)
{
string[] errors = [.. writerErrors
.Distinct()
.OrderBy(static value => value, StringComparer.Ordinal)
.Take(5)];
TestContext.Out.WriteLine(
"ARRAYPOOL_DIAGNOSTIC_ERRORS scenario=" +
scenario.Name +
"; samples=" +
string.Join(", ", errors));
}
}
private static long GetTotalAllocatedBytes()
{
return GC.GetTotalAllocatedBytes(true);
}
private static int RoundUpToPowerOfTwo(int value)
{
int result = 1;
while (result < value && result != 0)
{
result <<= 1;
}
return result;
}
private static int GetTrackedMinimumBufferSize(int configuredMaxBufferSize)
{
int normalizedDataSize = Math.Max(
kTcpMinBufferSize,
GetSuggestedBufferSize(configuredMaxBufferSize));
return RoundUpToPowerOfTwo(normalizedDataSize + kBufferCookieLength);
}
private static int GetSuggestedBufferSize(int size)
{
int currentBucket = RoundUpToPowerOfTwo(size);
int rentBucket = RoundUpToPowerOfTwo(size + kBufferCookieLength);
if (currentBucket != rentBucket)
{
return currentBucket - kBufferCookieLength;
}
return size;
}
private sealed record ArrayPoolDiagnosticScenario(
string Name,
int PublishQueueCount,
int DurationMs,
int WriterIntervalMs,
int PublishingIntervalMs,
int SlowConsumerDelayMs,
int SlowConsumerEveryNthSession);
private sealed record ReaderSessionBundle(
ISession Session,
Subscription Subscription);
private sealed record ArrayPoolMetrics(
long RentedCount,
long ReturnedCount,
long AllocatedCount,
long DroppedCount,
long TrimmedCount,
long OutstandingCount,
long OutstandingBytes,
long PeakOutstandingCount,
long PeakOutstandingBytes,
int MaxObservedBufferSize,
IReadOnlyList> BufferSizeHistogram);
private sealed class ArrayPoolEventListener : EventListener
{
private const string kArrayPoolEventSourceName = "System.Buffers.ArrayPoolEventSource";
private readonly int m_minTrackedBufferSize;
private readonly ConcurrentDictionary m_bufferSizes = new();
private readonly ConcurrentDictionary m_outstandingBuffers = new();
private long m_allocatedCount;
private long m_droppedCount;
private long m_maxObservedBufferSize;
private long m_outstandingBytes;
private long m_outstandingCount;
private long m_peakOutstandingBytes;
private long m_peakOutstandingCount;
private long m_rentedCount;
private long m_returnedCount;
private long m_trimmedCount;
private long m_lastActivityTimestamp = Stopwatch.GetTimestamp();
private EventSource? m_source;
public ArrayPoolEventListener(int minTrackedBufferSize)
{
m_minTrackedBufferSize = minTrackedBufferSize;
}
public ArrayPoolMetrics CreateMetrics()
{
IReadOnlyList> histogram =
[
.. m_bufferSizes
.OrderBy(static value => value.Key)
.Select(static value => new KeyValuePair(value.Key, value.Value))
];
return new ArrayPoolMetrics(
RentedCount: Interlocked.Read(ref m_rentedCount),
ReturnedCount: Interlocked.Read(ref m_returnedCount),
AllocatedCount: Interlocked.Read(ref m_allocatedCount),
DroppedCount: Interlocked.Read(ref m_droppedCount),
TrimmedCount: Interlocked.Read(ref m_trimmedCount),
OutstandingCount: Interlocked.Read(ref m_outstandingCount),
OutstandingBytes: Interlocked.Read(ref m_outstandingBytes),
PeakOutstandingCount: Interlocked.Read(ref m_peakOutstandingCount),
PeakOutstandingBytes: Interlocked.Read(ref m_peakOutstandingBytes),
MaxObservedBufferSize: (int)Interlocked.Read(ref m_maxObservedBufferSize),
BufferSizeHistogram: histogram);
}
public TimeSpan GetIdleDuration()
{
long elapsedTicks = Stopwatch.GetTimestamp() -
Interlocked.Read(ref m_lastActivityTimestamp);
return TimeSpan.FromSeconds(
(double)elapsedTicks / Stopwatch.Frequency);
}
protected override void OnEventSourceCreated(EventSource eventSource)
{
if (eventSource.Name == kArrayPoolEventSourceName)
{
m_source = eventSource;
EnableEvents(eventSource, EventLevel.Verbose);
}
}
protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
string eventName = eventData.EventName ??
eventData.EventId.ToString(CultureInfo.InvariantCulture);
if (!TryGetPayloadInt32(eventData, "bufferSize", fallbackIndex: 1, out int bufferSize) ||
bufferSize < m_minTrackedBufferSize)
{
return;
}
UpdateMaxObservedBufferSize(bufferSize);
Interlocked.Exchange(ref m_lastActivityTimestamp, Stopwatch.GetTimestamp());
switch (eventName)
{
case "BufferRented":
Interlocked.Increment(ref m_rentedCount);
m_bufferSizes.AddOrUpdate(bufferSize, 1, static (_, count) => count + 1);
if (TryGetBufferKey(eventData, out BufferKey rentedKey) &&
m_outstandingBuffers.TryAdd(rentedKey, bufferSize))
{
AddOutstanding(bufferSize);
}
break;
case "BufferReturned":
if (TryGetBufferKey(eventData, out BufferKey returnedKey) &&
m_outstandingBuffers.TryRemove(returnedKey, out int rentedBufferSize))
{
Interlocked.Increment(ref m_returnedCount);
RemoveOutstanding(rentedBufferSize);
}
break;
case "BufferAllocated":
Interlocked.Increment(ref m_allocatedCount);
break;
case "BufferDropped":
Interlocked.Increment(ref m_droppedCount);
break;
case "BufferTrimmed":
Interlocked.Increment(ref m_trimmedCount);
break;
}
}
public override void Dispose()
{
if (m_source != null)
{
DisableEvents(m_source);
}
base.Dispose();
}
private static bool TryGetBufferKey(
EventWrittenEventArgs eventData,
out BufferKey key)
{
if (TryGetPayloadInt32(eventData, "bufferId", fallbackIndex: 0, out int bufferId) &&
TryGetPayloadInt32(eventData, "poolId", fallbackIndex: 2, out int poolId))
{
key = new BufferKey(poolId, bufferId);
return true;
}
key = default;
return false;
}
private static bool TryGetPayloadInt32(
EventWrittenEventArgs eventData,
string payloadName,
int fallbackIndex,
out int result)
{
result = 0;
IList? payloadNames = eventData.PayloadNames;
IList