diff --git a/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj b/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj index 40351864618018..7351b0a3a4555d 100644 --- a/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj +++ b/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj @@ -131,6 +131,7 @@ + diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Interop/msquic_generated.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Interop/msquic_generated.cs index b8c0b092df1df7..ad781e6ddd7c7e 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Interop/msquic_generated.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Interop/msquic_generated.cs @@ -929,6 +929,7 @@ internal enum QUIC_PERFORMANCE_COUNTERS PATH_FAILURE, SEND_STATELESS_RESET, SEND_STATELESS_RETRY, + CONN_LOAD_REJECT, MAX, } diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/NetEventSource.Quic.Counters.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/NetEventSource.Quic.Counters.cs new file mode 100644 index 00000000000000..93ec7e7532c7f3 --- /dev/null +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/NetEventSource.Quic.Counters.cs @@ -0,0 +1,249 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Tracing; +using System.Diagnostics.Metrics; +using System.Net.Quic; + +using Microsoft.Quic; +using static Microsoft.Quic.MsQuic; + +namespace System.Net +{ + internal sealed partial class NetEventSource + { + private static Meter s_meter = new Meter("Private.InternalDiagnostics.System.Net.Quic.MsQuic"); + private static long s_countersLastFetched; + private static readonly long[] s_counters = new long[(int)QUIC_PERFORMANCE_COUNTERS.MAX]; + public static readonly ObservableCounter s_CONN_CREATED = s_meter.CreateObservableCounter( + name: "msquic.connection.created", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.CONN_CREATED), + unit: "{connection}", + description: "New connections allocated"); + + public static readonly ObservableCounter s_CONN_HANDSHAKE_FAIL = s_meter.CreateObservableCounter( + name: "msquic.connection.handshake_failures", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.CONN_HANDSHAKE_FAIL), + unit: "{connection}", + description: "Connections that failed during handshake"); + + public static readonly ObservableCounter s_CONN_APP_REJECT = s_meter.CreateObservableCounter( + name: "msquic.connection.app_rejected", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.CONN_APP_REJECT), + unit: "{connection}", + description: "Connections rejected by the application"); + + public static readonly ObservableCounter s_CONN_LOAD_REJECT = s_meter.CreateObservableCounter( + name: "msquic.connection.load_rejected", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.CONN_LOAD_REJECT), + unit: "{connection}", + description: "Connections rejected due to worker load."); + + public static readonly ObservableCounter s_CONN_RESUMED = s_meter.CreateObservableCounter( + name: "msquic.connection.resumed", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.CONN_RESUMED), + unit: "{connection}", + description: "Connections resumed"); + + public static readonly ObservableGauge s_CONN_ACTIVE = s_meter.CreateObservableGauge( + name: "msquic.connection.allocated", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.CONN_ACTIVE), + unit: "{connection}", + description: "Connections currently allocated"); + + public static readonly ObservableGauge s_CONN_CONNECTED = s_meter.CreateObservableGauge( + name: "msquic.connection.connected", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.CONN_CONNECTED), + unit: "{connection}", + description: "Connections currently in the connected state"); + + public static readonly ObservableCounter s_CONN_PROTOCOL_ERRORS = s_meter.CreateObservableCounter( + name: "msquic.connection.protocol_errors", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.CONN_PROTOCOL_ERRORS), + unit: "{connection}", + description: "Connections shutdown with a protocol error"); + + public static readonly ObservableCounter s_CONN_NO_ALPN = s_meter.CreateObservableCounter( + name: "msquic.connection.no_alpn", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.CONN_NO_ALPN), + unit: "{connection}", + description: "Connection attempts with no matching ALPN"); + + public static readonly ObservableGauge s_STRM_ACTIVE = s_meter.CreateObservableGauge( + name: "msquic.stream.allocated", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.STRM_ACTIVE), + unit: "{stream}", + description: "Current streams allocated"); + + public static readonly ObservableCounter s_PKTS_SUSPECTED_LOST = s_meter.CreateObservableCounter( + name: "msquic.packet.suspected_lost", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.PKTS_SUSPECTED_LOST), + unit: "{packet}", + description: "Packets suspected lost"); + + public static readonly ObservableCounter s_PKTS_DROPPED = s_meter.CreateObservableCounter( + name: "msquic.packet.dropped", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.PKTS_DROPPED), + unit: "{packet}", + description: "Packets dropped for any reason"); + + public static readonly ObservableCounter s_PKTS_DECRYPTION_FAIL = s_meter.CreateObservableCounter( + name: "msquic.packet.decryption_failures", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.PKTS_DECRYPTION_FAIL), + unit: "{packet}", + description: "Packets with decryption failures"); + + public static readonly ObservableCounter s_UDP_RECV = s_meter.CreateObservableCounter( + name: "msquic.udp.recv_datagrams", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.UDP_RECV), + unit: "{datagram}", + description: "UDP datagrams received"); + + public static readonly ObservableCounter s_UDP_SEND = s_meter.CreateObservableCounter( + name: "msquic.udp.send_datagrams", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.UDP_SEND), + unit: "{datagram}", + description: "UDP datagrams sent"); + + public static readonly ObservableCounter s_UDP_RECV_BYTES = s_meter.CreateObservableCounter( + name: "msquic.udp.recv_bytes", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.UDP_RECV_BYTES), + unit: "By", + description: "UDP payload bytes received"); + + public static readonly ObservableCounter s_UDP_SEND_BYTES = s_meter.CreateObservableCounter( + name: "msquic.udp.send_bytes", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.UDP_SEND_BYTES), + unit: "By", + description: "UDP payload bytes sent"); + + public static readonly ObservableCounter s_UDP_RECV_EVENTS = s_meter.CreateObservableCounter( + name: "msquic.udp.recv_events", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.UDP_RECV_EVENTS), + unit: "{event}", + description: "UDP receive events"); + + public static readonly ObservableCounter s_UDP_SEND_CALLS = s_meter.CreateObservableCounter( + name: "msquic.udp.send_calls", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.UDP_SEND_CALLS), + unit: "{call}", + description: "UDP send API calls"); + + public static readonly ObservableCounter s_APP_SEND_BYTES = s_meter.CreateObservableCounter( + name: "msquic.app.send_bytes", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.APP_SEND_BYTES), + unit: "By", + description: "Bytes sent by applications"); + + public static readonly ObservableCounter s_APP_RECV_BYTES = s_meter.CreateObservableCounter( + name: "msquic.app.recv_bytes", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.APP_RECV_BYTES), + unit: "By", + description: "Bytes received by applications"); + + public static readonly ObservableGauge s_CONN_QUEUE_DEPTH = s_meter.CreateObservableGauge( + name: "msquic.threadpool.conn_queue_depth", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.CONN_QUEUE_DEPTH), + unit: "{connection}", + description: "Current connections queued for processing"); + + public static readonly ObservableGauge s_CONN_OPER_QUEUE_DEPTH = s_meter.CreateObservableGauge( + name: "msquic.threadpool.conn_oper_queue_depth", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.CONN_OPER_QUEUE_DEPTH), + unit: "{operation}", + description: "Current connection operations queued"); + + public static readonly ObservableCounter s_CONN_OPER_QUEUED = s_meter.CreateObservableCounter( + name: "msquic.threadpool.conn_oper_queued", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.CONN_OPER_QUEUED), + unit: "{operation}", + description: "New connection operations queued"); + + public static readonly ObservableCounter s_CONN_OPER_COMPLETED = s_meter.CreateObservableCounter( + name: "msquic.threadpool.conn_oper_completed", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.CONN_OPER_COMPLETED), + unit: "{operation}", + description: "Connection operations processed"); + + public static readonly ObservableGauge s_WORK_OPER_QUEUE_DEPTH = s_meter.CreateObservableGauge( + name: "msquic.threadpool.work_oper_queue_depth", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.WORK_OPER_QUEUE_DEPTH), + unit: "{operation}", + description: "Current worker operations queued"); + + public static readonly ObservableCounter s_WORK_OPER_QUEUED = s_meter.CreateObservableCounter( + name: "msquic.threadpool.work_oper_queued", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.WORK_OPER_QUEUED), + unit: "{operation}", + description: "New worker operations queued"); + + public static readonly ObservableCounter s_WORK_OPER_COMPLETED = s_meter.CreateObservableCounter( + name: "msquic.threadpool.work_oper_completed", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.WORK_OPER_COMPLETED), + unit: "{operation}", + description: "Worker operations processed"); + + public static readonly ObservableCounter s_PATH_VALIDATED = s_meter.CreateObservableCounter( + name: "msquic.datapath.path_validated", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.PATH_VALIDATED), + unit: "{challenge}", + description: "Successful path challenges"); + + public static readonly ObservableCounter s_PATH_FAILURE = s_meter.CreateObservableCounter( + name: "msquic.datapath.path_failure", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.PATH_FAILURE), + unit: "{challenge}", + description: "Unsuccessful path challenges"); + + public static readonly ObservableCounter s_SEND_STATELESS_RESET = s_meter.CreateObservableCounter( + name: "msquic.datapath.send_stateless_reset", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.SEND_STATELESS_RESET), + unit: "{packet}", + description: "Stateless reset packets sent ever"); + + public static readonly ObservableCounter s_SEND_STATELESS_RETRY = s_meter.CreateObservableCounter( + name: "msquic.datapath.send_stateless_retry", + observeValue: () => GetCounterValue(QUIC_PERFORMANCE_COUNTERS.SEND_STATELESS_RETRY), + unit: "{packet}", + description: "Stateless retry packets sent"); + + [NonEvent] + private static void UpdateCounters() + { + if (!MsQuicApi.IsQuicSupported) + { + // Avoid calling into MsQuic if not supported (or not initialized yet) + return; + } + + unsafe + { + fixed (long* pCounters = s_counters) + { + uint size = (uint)s_counters.Length * sizeof(long); + MsQuicApi.Api.ApiTable->GetParam(null, QUIC_PARAM_GLOBAL_PERF_COUNTERS, &size, (byte*)pCounters); + } + } + } + + [NonEvent] + private static long GetCounterValue(QUIC_PERFORMANCE_COUNTERS counter) + { + // + // We wan't to avoid refreshing the counter values array for each counter callback, + // so we refresh the counters array only once every 50ms. This should be enough time + // for all the counters to be queried and at the same time but still low enough to not + // confuse any monitoring tool as their polling rate is usually in seconds. + // + if (s_countersLastFetched == 0 || Stopwatch.GetElapsedTime(s_countersLastFetched).TotalMilliseconds > 50) + { + UpdateCounters(); + s_countersLastFetched = Stopwatch.GetTimestamp(); + } + + return s_counters[(int)counter]; + } + } +} diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicCipherSuitesPolicyTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicCipherSuitesPolicyTests.cs index 459b9bce810dca..2d95e08c743923 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicCipherSuitesPolicyTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicCipherSuitesPolicyTests.cs @@ -8,7 +8,7 @@ namespace System.Net.Quic.Tests { - [Collection(nameof(DisableParallelization))] + [Collection(nameof(QuicTestCollection))] [ConditionalClass(typeof(QuicTestBase), nameof(QuicTestBase.IsSupported), nameof(QuicTestBase.IsNotArm32CoreClrStressTest))] [SkipOnPlatform(TestPlatforms.Windows, "CipherSuitesPolicy is not supported on Windows")] public class MsQuicCipherSuitesPolicyTests : QuicTestBase @@ -77,4 +77,4 @@ await Assert.ThrowsAsync(() => TestConnection( )); } } -} \ No newline at end of file +} diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicPlatformDetectionTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicPlatformDetectionTests.cs index 16f267fc719535..891e6c7350218b 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicPlatformDetectionTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicPlatformDetectionTests.cs @@ -8,6 +8,7 @@ namespace System.Net.Quic.Tests { + [Collection(nameof(QuicTestCollection))] public class MsQuicPlatformDetectionTests : QuicTestBase { public MsQuicPlatformDetectionTests(ITestOutputHelper output) : base(output) { } diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicRemoteExecutorTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicRemoteExecutorTests.cs index 051ead9b3bb2ec..90cec1a5237ae8 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicRemoteExecutorTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicRemoteExecutorTests.cs @@ -12,7 +12,7 @@ namespace System.Net.Quic.Tests { - [Collection(nameof(DisableParallelization))] + [Collection(nameof(QuicTestCollection))] [ConditionalClass(typeof(QuicTestBase), nameof(QuicTestBase.IsSupported), nameof(QuicTestBase.IsNotArm32CoreClrStressTest))] public class MsQuicRemoteExecutorTests : QuicTestBase { diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs index 4a11909d30bfef..31177d6c15e046 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs @@ -46,7 +46,7 @@ public void Dispose() } } - [Collection(nameof(DisableParallelization))] + [Collection(nameof(QuicTestCollection))] [ConditionalClass(typeof(QuicTestBase), nameof(QuicTestBase.IsSupported), nameof(QuicTestBase.IsNotArm32CoreClrStressTest))] public class MsQuicTests : QuicTestBase, IClassFixture { diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicConnectionTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicConnectionTests.cs index 98d72124f0048c..f7c70196e6d15e 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicConnectionTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicConnectionTests.cs @@ -13,7 +13,7 @@ namespace System.Net.Quic.Tests { using Configuration = System.Net.Test.Common.Configuration; - [Collection(nameof(DisableParallelization))] + [Collection(nameof(QuicTestCollection))] [ConditionalClass(typeof(QuicTestBase), nameof(QuicTestBase.IsSupported), nameof(QuicTestBase.IsNotArm32CoreClrStressTest))] public sealed class QuicConnectionTests : QuicTestBase { diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs index d9e27a9e394c4c..0de2863f902f24 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs @@ -13,7 +13,7 @@ namespace System.Net.Quic.Tests { - [Collection(nameof(DisableParallelization))] + [Collection(nameof(QuicTestCollection))] [ConditionalClass(typeof(QuicTestBase), nameof(QuicTestBase.IsSupported), nameof(QuicTestBase.IsNotArm32CoreClrStressTest))] public sealed class QuicListenerTests : QuicTestBase { diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamConnectedStreamConformanceTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamConnectedStreamConformanceTests.cs index e224bf75c55312..2f8b7a66ff6c8d 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamConnectedStreamConformanceTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamConnectedStreamConformanceTests.cs @@ -14,7 +14,7 @@ namespace System.Net.Quic.Tests { - [Collection(nameof(DisableParallelization))] + [Collection(nameof(QuicTestCollection))] [ConditionalClass(typeof(QuicTestBase), nameof(QuicTestBase.IsSupported), nameof(QuicTestBase.IsNotArm32CoreClrStressTest))] public sealed class QuicStreamConformanceTests : ConnectedStreamConformanceTests { diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamTests.cs index 72d0995823edc3..e82bd1ea9e8c94 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamTests.cs @@ -12,7 +12,7 @@ namespace System.Net.Quic.Tests { - [Collection(nameof(DisableParallelization))] + [Collection(nameof(QuicTestCollection))] [ConditionalClass(typeof(QuicTestBase), nameof(QuicTestBase.IsSupported), nameof(QuicTestBase.IsNotArm32CoreClrStressTest))] public sealed class QuicStreamTests : QuicTestBase { @@ -1211,16 +1211,16 @@ async ValueTask ReleaseOnReadsClosedAsync() private const int SmallestPayload = 1; private const int SmallPayload = 1024; - private const int BufferPayload = 64*1024; - private const int BufferPlusPayload = 64*1024+1; - private const int BigPayload = 1024*1024*1024; + private const int BufferPayload = 64 * 1024; + private const int BufferPlusPayload = 64 * 1024 + 1; + private const int BigPayload = 1024 * 1024 * 1024; public static IEnumerable PayloadSizeAndTwoBools() { - var boolValues = new [] { true, false }; + var boolValues = new[] { true, false }; var payloadValues = !PlatformDetection.IsInHelix ? - new [] { SmallestPayload, SmallPayload, BufferPayload, BufferPlusPayload, BigPayload } : - new [] { SmallestPayload, SmallPayload, BufferPayload, BufferPlusPayload }; + new[] { SmallestPayload, SmallPayload, BufferPayload, BufferPlusPayload, BigPayload } : + new[] { SmallestPayload, SmallPayload, BufferPayload, BufferPlusPayload }; return from payload in payloadValues from bool1 in boolValues diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs index 79992aef5f16c7..d85cf0e5ed3d9b 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs @@ -43,29 +43,7 @@ public abstract class QuicTestBase : IDisposable public const int PassingTestTimeoutMilliseconds = 4 * 60 * 1000; public static TimeSpan PassingTestTimeout => TimeSpan.FromMilliseconds(PassingTestTimeoutMilliseconds); - static unsafe QuicTestBase() - { - // If any of the reflection bellow breaks due to changes in "System.Net.Quic.MsQuicApi", also check and fix HttpStress project as it uses the same hack. - Type msQuicApiType = Type.GetType("System.Net.Quic.MsQuicApi, System.Net.Quic"); - - string msQuicLibraryVersion = (string)msQuicApiType.GetProperty("MsQuicLibraryVersion", BindingFlags.NonPublic | BindingFlags.Static).GetGetMethod(true).Invoke(null, Array.Empty()); - Console.WriteLine($"MsQuic {(IsSupported ? "supported" : "not supported")} and using '{msQuicLibraryVersion}'."); - - if (IsSupported) - { - object msQuicApiInstance = msQuicApiType.GetProperty("Api", BindingFlags.NonPublic | BindingFlags.Static).GetGetMethod(true).Invoke(null, Array.Empty()); - QUIC_API_TABLE* apiTable = (QUIC_API_TABLE*)(Pointer.Unbox(msQuicApiType.GetProperty("ApiTable").GetGetMethod().Invoke(msQuicApiInstance, Array.Empty()))); - QUIC_SETTINGS settings = default(QUIC_SETTINGS); - settings.IsSet.MaxWorkerQueueDelayUs = 1; - settings.MaxWorkerQueueDelayUs = 2_500_000u; // 2.5s, 10x the default - if (MsQuic.StatusFailed(apiTable->SetParam(null, MsQuic.QUIC_PARAM_GLOBAL_SETTINGS, (uint)sizeof(QUIC_SETTINGS), (byte*)&settings))) - { - Console.WriteLine($"Unable to set MsQuic MaxWorkerQueueDelayUs."); - } - } - } - - public unsafe QuicTestBase(ITestOutputHelper output) + public QuicTestBase(ITestOutputHelper output) { _output = output; } diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestCollection.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestCollection.cs new file mode 100644 index 00000000000000..aac056df7cc16d --- /dev/null +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestCollection.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.DotNet.XUnitExtensions; +using System; +using System.Net.Quic; +using System.Reflection; +using System.Text; +using System.Linq; +using Xunit; +using Xunit.Abstractions; + +using Microsoft.Quic; +using static Microsoft.Quic.MsQuic; + +[CollectionDefinition(nameof(QuicTestCollection), DisableParallelization = true)] +public unsafe class QuicTestCollection : ICollectionFixture, IDisposable +{ + public static bool IsSupported => QuicListener.IsSupported && QuicConnection.IsSupported; + + public QuicTestCollection() + { + string msQuicLibraryVersion = GetMsQuicLibraryVersion(); + // If any of the reflection bellow breaks due to changes in "System.Net.Quic.MsQuicApi", also check and fix HttpStress project as it uses the same hack. + Console.WriteLine($"MsQuic {(IsSupported ? "supported" : "not supported")} and using '{msQuicLibraryVersion}'."); + + if (IsSupported) + { + QUIC_SETTINGS settings = default(QUIC_SETTINGS); + settings.IsSet.MaxWorkerQueueDelayUs = 1; + settings.MaxWorkerQueueDelayUs = 2_500_000u; // 2.5s, 10x the default + if (MsQuic.StatusFailed(GetApiTable()->SetParam(null, MsQuic.QUIC_PARAM_GLOBAL_SETTINGS, (uint)sizeof(QUIC_SETTINGS), (byte*)&settings))) + { + Console.WriteLine($"Unable to set MsQuic MaxWorkerQueueDelayUs."); + } + } + } + + public unsafe void Dispose() + { + if (!IsSupported) + { + return; + } + + long[] counters = new long[(int)QUIC_PERFORMANCE_COUNTERS.MAX]; + int countersAvailable; + + int status; + fixed (long* pCounters = counters) + { + uint size = (uint)counters.Length * sizeof(long); + status = GetApiTable()->GetParam(null, QUIC_PARAM_GLOBAL_PERF_COUNTERS, &size, (byte*)pCounters); + countersAvailable = (int)size / sizeof(long); + } + + if (StatusFailed(status)) + { + System.Console.WriteLine($"Failed to read MsQuic counters: {status}"); + return; + } + + StringBuilder sb = new StringBuilder(); + sb.AppendLine("MsQuic Counters:"); + + int maxlen = Enum.GetNames(typeof(QUIC_PERFORMANCE_COUNTERS)).Max(s => s.Length); + void DumpCounter(QUIC_PERFORMANCE_COUNTERS counter) + { + var name = $"{counter}:".PadRight(maxlen + 1); + var index = (int)counter; + var value = index < countersAvailable ? counters[(int)counter].ToString() : "N/A"; + sb.AppendLine($" {counter} {value}"); + } + + DumpCounter(QUIC_PERFORMANCE_COUNTERS.CONN_CREATED); + DumpCounter(QUIC_PERFORMANCE_COUNTERS.CONN_HANDSHAKE_FAIL); + DumpCounter(QUIC_PERFORMANCE_COUNTERS.CONN_APP_REJECT); + DumpCounter(QUIC_PERFORMANCE_COUNTERS.CONN_LOAD_REJECT); + + System.Console.WriteLine(sb.ToString()); + } + + private static string? GetMsQuicLibraryVersion() + { + Type msQuicApiType = Type.GetType("System.Net.Quic.MsQuicApi, System.Net.Quic"); + + return (string)msQuicApiType.GetProperty("MsQuicLibraryVersion", BindingFlags.NonPublic | BindingFlags.Static).GetGetMethod(true).Invoke(null, Array.Empty()); + } + + private static QUIC_API_TABLE* GetApiTable() + { + Type msQuicApiType = Type.GetType("System.Net.Quic.MsQuicApi, System.Net.Quic"); + object msQuicApiInstance = msQuicApiType.GetProperty("Api", BindingFlags.NonPublic | BindingFlags.Static).GetGetMethod(true).Invoke(null, Array.Empty()); + return (QUIC_API_TABLE*)(Pointer.Unbox(msQuicApiType.GetProperty("ApiTable").GetGetMethod().Invoke(msQuicApiInstance, Array.Empty()))); + } +}