diff --git a/docs/PreviewFeatures.md b/docs/PreviewFeatures.md index c1027f92d9..7558c46afc 100644 --- a/docs/PreviewFeatures.md +++ b/docs/PreviewFeatures.md @@ -1,4 +1,4 @@ -GPreview features +Preview features ========= > [!IMPORTANT] > diff --git a/docs/Settings.md b/docs/Settings.md index 2ca0d5c62d..81b17bdadb 100644 --- a/docs/Settings.md +++ b/docs/Settings.md @@ -141,6 +141,7 @@ These parameters are accessed by calling [GetParam](./api/GetParam.md) or [SetPa | `QUIC_PARAM_GLOBAL_STATISTICS_V2_SIZES`
12 | uint32_t[] | Get-only | Array of well-known sizes for each version of the QUIC_STATISTICS_V2 struct. The output array length is variable; pass a buffer of uint32_t and check BufferLength for the number of sizes returned. See GetParam documentation for usage details. | | `QUIC_PARAM_GLOBAL_VERSION_NEGOTIATION_ENABLED`
(preview) | uint8_t (BOOLEAN) | Both | Globally enable the version negotiation extension for all client and server connections. | | `QUIC_PARAM_GLOBAL_STATELESS_RETRY_CONFIG`
13 | [QUIC_STATELESS_RETRY_CONFIG](./api/QUIC_STATELESS_RETRY_CONFIG.md) | Set-Only | Configure the stateless retry token secret, key algorithm, and key rotation interval. The secret length *must* match the AEAD algorithm key length. | +| `QUIC_PARAM_GLOBAL_WORKER_STATISTICS`
15 (preview) | QUIC_WORKER_STATISTICS_LIST | Get-only | Per-worker active and wall time statistics. The output length is variable; pass a `QUIC_WORKER_STATISTICS_LIST` buffer and use the double-call pattern to determine the required size. Not supported in kernel mode. See [GetParam](./api/GetParam.md#quic_param_global_worker_statistics) for usage details. | ## Registration Parameters diff --git a/docs/api/GetParam.md b/docs/api/GetParam.md index dd7f303f4d..6806305613 100644 --- a/docs/api/GetParam.md +++ b/docs/api/GetParam.md @@ -103,6 +103,51 @@ uint32_t NumSizes = BufferLength / sizeof(uint32_t); See also: [Settings.md](../Settings.md#global-parameters) +## QUIC_PARAM_GLOBAL_WORKER_STATISTICS + +**Preview:** This parameter is in [preview](../PreviewFeatures.md). + +Returns per-worker statistics for the global worker pool, allowing an application to observe how busy each worker thread is. The returned buffer is a `QUIC_WORKER_STATISTICS_LIST` header followed by one `QUIC_WORKER_STATISTICS` entry per worker: + +- `CumulativeActiveTimeUs` - total time (in microseconds) the worker spent active (not idle). +- `CumulativeWallTimeUs` - wall time (in microseconds) since the worker thread started. +- `IdealProcessor` - the CPU the worker's partition is affinitized to. + +Dividing `CumulativeActiveTimeUs` by `CumulativeWallTimeUs` yields the worker's utilization; sampling the values over an interval yields the utilization for that interval. + +- **Type:** `QUIC_WORKER_STATISTICS_LIST` +- **Get-only** +- **Variable-length:** Use a double-call pattern to determine the required buffer size. + +Iterate the entries using the `WorkerStatsSize` stride, more fields may be added to `QUIC_WORKER_STATISTICS` in the future. + +**Sample usage:** +```c +uint32_t BufferLength = 0; +QUIC_STATUS Status = + MsQuic->GetParam(NULL, QUIC_PARAM_GLOBAL_WORKER_STATISTICS, &BufferLength, NULL); + +if (Status != QUIC_STATUS_BUFFER_TOO_SMALL) { + // Unexpected, handle errors +} + +QUIC_WORKER_STATISTICS_LIST* List = + (QUIC_WORKER_STATISTICS_LIST*)malloc(BufferLength); + +Status = MsQuic->GetParam(NULL, QUIC_PARAM_GLOBAL_WORKER_STATISTICS, &BufferLength, List); +if (!QUIC_SUCCEEDED(Status)) { + // Handle errors +} +uint8_t* Entry = (uint8_t*)List + sizeof(QUIC_WORKER_STATISTICS_LIST); +for (uint32_t i = 0; i < List->WorkerCount; i++) { + QUIC_WORKER_STATISTICS* WorkerStats = (QUIC_WORKER_STATISTICS*)Entry; + // Use Worker->CumulativeActiveTimeUs, etc. + Entry += List->WorkerStatsSize; +} +``` + +See also: [Settings.md](../Settings.md#global-parameters) + # See Also [Settings](../Settings.md#api-object-parameters)
diff --git a/src/core/library.c b/src/core/library.c index 20dd458fab..02e2d64972 100644 --- a/src/core/library.c +++ b/src/core/library.c @@ -1539,6 +1539,83 @@ QuicLibrarySetGlobalParam( return Status; } +// +// Fills the caller's buffer with the per-worker statistics from the worker pool. +// +_IRQL_requires_max_(PASSIVE_LEVEL) +QUIC_STATUS +QuicLibraryGetGlobalWorkerStatistics( + _Inout_ uint32_t* BufferLength, + _Out_writes_bytes_opt_(*BufferLength) + void* Buffer + ) +{ +#ifdef _KERNEL_MODE + // + // Worker statistics are not supported in kernel mode, where the worker + // threads are not owned by the platform worker pool. + // + UNREFERENCED_PARAMETER(BufferLength); + UNREFERENCED_PARAMETER(Buffer); + return QUIC_STATUS_NOT_SUPPORTED; +#else + QUIC_STATUS Status; + + // + // Ensure the worker pool is not swapped / deleted concurrently + // (e.g. by MsQuicExecutionDelete). + // + CxPlatLockAcquire(&MsQuicLib.Lock); + CXPLAT_WORKER_POOL* WorkerPool = MsQuicLib.WorkerPool; + if (WorkerPool == NULL || !CxPlatWorkerPoolAddRef(WorkerPool, CXPLAT_WORKER_POOL_REF_STATS)) { + CxPlatLockRelease(&MsQuicLib.Lock); + return QUIC_STATUS_INVALID_STATE; + } + CxPlatLockRelease(&MsQuicLib.Lock); + + uint32_t WorkerCount = CxPlatWorkerPoolGetCount(WorkerPool); + uint32_t RequiredSize = + sizeof(QUIC_WORKER_STATISTICS_LIST) + + WorkerCount * sizeof(QUIC_WORKER_STATISTICS); + + if (*BufferLength < RequiredSize) { + *BufferLength = RequiredSize; + Status = QUIC_STATUS_BUFFER_TOO_SMALL; + goto Release; + } + + if (Buffer == NULL) { + Status = QUIC_STATUS_INVALID_PARAMETER; + goto Release; + } + + QUIC_WORKER_STATISTICS_LIST* List = (QUIC_WORKER_STATISTICS_LIST*)Buffer; + List->WorkerCount = WorkerCount; + List->WorkerStatsSize = sizeof(QUIC_WORKER_STATISTICS); + + QUIC_WORKER_STATISTICS* Stats = + (QUIC_WORKER_STATISTICS*)((uint8_t*)Buffer + sizeof(QUIC_WORKER_STATISTICS_LIST)); + + const uint64_t Now = CxPlatTimeUs64(); + for (uint32_t i = 0; i < WorkerCount; i++) { + CXPLAT_WORKER_STATISTICS WorkerStats = {0}; + CxPlatWorkerPoolGetStatistics(WorkerPool, i, Now, &WorkerStats); + Stats[i].IdealProcessor = WorkerStats.IdealProcessor; + Stats[i].CumulativeActiveTimeUs = WorkerStats.CumulativeActiveTimeUs; + Stats[i].CumulativeWallTimeUs = WorkerStats.CumulativeWallTimeUs; + } + + *BufferLength = RequiredSize; + Status = QUIC_STATUS_SUCCESS; + +Release: + + CxPlatWorkerPoolRelease(WorkerPool, CXPLAT_WORKER_POOL_REF_STATS); + + return Status; +#endif // _KERNEL_MODE +} + _IRQL_requires_max_(PASSIVE_LEVEL) _Success_(return == QUIC_STATUS_SUCCESS) QUIC_STATUS @@ -1914,6 +1991,10 @@ QuicLibraryGetGlobalParam( break; } + case QUIC_PARAM_GLOBAL_WORKER_STATISTICS: + Status = QuicLibraryGetGlobalWorkerStatistics(BufferLength, Buffer); + break; + default: Status = QUIC_STATUS_INVALID_PARAMETER; break; @@ -2916,14 +2997,24 @@ MsQuicExecutionCreate( // // Clean up any previous worker pool and create a new one. // - CxPlatWorkerPoolDelete(MsQuicLib.WorkerPool, CXPLAT_WORKER_POOL_REF_EXTERNAL); - MsQuicLib.WorkerPool = + CxPlatLockAcquire(&MsQuicLib.Lock); + CXPLAT_WORKER_POOL* OldWorkerPool = MsQuicLib.WorkerPool; + MsQuicLib.WorkerPool = NULL; + CxPlatLockRelease(&MsQuicLib.Lock); + + CxPlatWorkerPoolDelete(OldWorkerPool, CXPLAT_WORKER_POOL_REF_EXTERNAL); + + CXPLAT_WORKER_POOL* NewWorkerPool = CxPlatWorkerPoolCreateExternal(Count, Configs, Executions); - if (MsQuicLib.WorkerPool == NULL) { + + if (NewWorkerPool == NULL) { Status = QUIC_STATUS_OUT_OF_MEMORY; + } else { + CxPlatLockAcquire(&MsQuicLib.Lock); + MsQuicLib.WorkerPool = NewWorkerPool; + MsQuicLib.CustomExecutions = TRUE; + CxPlatLockRelease(&MsQuicLib.Lock); } - - MsQuicLib.CustomExecutions = TRUE; } QuicTraceEvent( @@ -2951,9 +3042,13 @@ MsQuicExecutionDelete( UNREFERENCED_PARAMETER(Count); UNREFERENCED_PARAMETER(Executions); - CxPlatWorkerPoolDelete(MsQuicLib.WorkerPool, CXPLAT_WORKER_POOL_REF_EXTERNAL); + CxPlatLockAcquire(&MsQuicLib.Lock); + CXPLAT_WORKER_POOL* WorkerPool = MsQuicLib.WorkerPool; MsQuicLib.WorkerPool = NULL; MsQuicLib.CustomExecutions = FALSE; + CxPlatLockRelease(&MsQuicLib.Lock); + + CxPlatWorkerPoolDelete(WorkerPool, CXPLAT_WORKER_POOL_REF_EXTERNAL); QuicTraceEvent( ApiExit, diff --git a/src/inc/msquic.h b/src/inc/msquic.h index e30f816217..e9dc4be54c 100644 --- a/src/inc/msquic.h +++ b/src/inc/msquic.h @@ -370,6 +370,27 @@ typedef struct QUIC_XDP_MAP_CONFIG { #endif // QUIC_API_ENABLE_PREVIEW_FEATURES +#ifdef QUIC_API_ENABLE_PREVIEW_FEATURES + +// +// Per-worker statistics. +// +typedef struct QUIC_WORKER_STATISTICS { + uint64_t CumulativeActiveTimeUs; // total time the worker spent active (not idle) + uint64_t CumulativeWallTimeUs; // wall time since the worker thread started + uint16_t IdealProcessor; // CPU the partition is affinitized to + + // -- append-only beyond this point -- +} QUIC_WORKER_STATISTICS; + +typedef struct QUIC_WORKER_STATISTICS_LIST { + uint32_t WorkerCount; // number of QUIC_WORKER_STATISTICS entries that follow + uint32_t WorkerStatsSize; // sizeof(QUIC_WORKER_STATISTICS) as written by this msquic + // QUIC_WORKER_STATISTICS Workers[WorkerCount]; // stride == WorkerStatsSize +} QUIC_WORKER_STATISTICS_LIST; + +#endif // QUIC_API_ENABLE_PREVIEW_FEATURES + typedef struct QUIC_REGISTRATION_CONFIG { // All fields may be NULL/zero. const char* AppName; QUIC_EXECUTION_PROFILE ExecutionProfile; @@ -1008,6 +1029,7 @@ void #define QUIC_PARAM_GLOBAL_STATELESS_RETRY_CONFIG 0x0100000D // QUIC_STATELESS_RETRY_CONFIG #ifdef QUIC_API_ENABLE_PREVIEW_FEATURES #define QUIC_PARAM_GLOBAL_XDP_MAP_CONFIG 0x0100000E // QUIC_XDP_MAP_CONFIG[] +#define QUIC_PARAM_GLOBAL_WORKER_STATISTICS 0x0100000F // QUIC_WORKER_STATISTICS_LIST #endif // diff --git a/src/inc/quic_platform.h b/src/inc/quic_platform.h index 2426a34486..2a337c61dd 100644 --- a/src/inc/quic_platform.h +++ b/src/inc/quic_platform.h @@ -510,6 +510,7 @@ typedef enum CXPLAT_WORKER_POOL_REF { CXPLAT_WORKER_POOL_REF_RAW, CXPLAT_WORKER_POOL_REF_WINSOCK, CXPLAT_WORKER_POOL_REF_TOOL, + CXPLAT_WORKER_POOL_REF_STATS, // Transient ref for querying worker statistics CXPLAT_WORKER_POOL_REF_COUNT @@ -576,6 +577,23 @@ CxPlatWorkerPoolWorkerPoll( _In_ QUIC_EXECUTION* Execution ); +typedef struct CXPLAT_WORKER_STATISTICS { + uint64_t CumulativeActiveTimeUs; // Time the worker spent active (not idle). + uint64_t CumulativeWallTimeUs; // Wall time since the worker thread started. + uint16_t IdealProcessor; // CPU the worker is affinitized to. +} CXPLAT_WORKER_STATISTICS; + +// +// Gets the statistics for a worker in the pool +// +void +CxPlatWorkerPoolGetStatistics( + _In_ CXPLAT_WORKER_POOL* WorkerPool, + _In_ uint32_t Index, + _In_ uint64_t TimeNow, + _Out_ CXPLAT_WORKER_STATISTICS* Stats + ); + // // Supports more dynamic operations, but must be submitted to the platform worker // to manage. diff --git a/src/platform/platform_worker.c b/src/platform/platform_worker.c index 1ac0e1dc2d..ca11681ab3 100644 --- a/src/platform/platform_worker.c +++ b/src/platform/platform_worker.c @@ -102,6 +102,27 @@ typedef struct QUIC_CACHEALIGN CXPLAT_WORKER { // BOOLEAN Running; + // + // Statistics tracking for active time measurement. + // + struct { + // + // Timestamp (in microseconds) when the worker thread started. + // + uint64_t StartedTimeUs; + + // + // Timestamp (in microseconds) when the current active period started, + // or 0 when the worker is currently idle (waiting or yielding). + // + uint64_t ActiveStartTimeUs; + + // + // Cumulative time (in microseconds) the worker was active (not waiting or yielding). + // + uint64_t CumulativeActiveTimeUs; + } Stats; + } CXPLAT_WORKER; typedef struct CXPLAT_WORKER_POOL { @@ -728,18 +749,55 @@ CxPlatProcessDynamicPoolAllocators( CxPlatLockRelease(&Worker->ECLock); } +// +// Marks the end of an active period, folding the elapsed active time into the +// cumulative total and marking the worker idle (ActiveStartTimeUs == 0). +// +void +CxPlatWorkerStatsPause( + _Inout_ CXPLAT_WORKER* Worker + ) +{ + if (Worker->Stats.ActiveStartTimeUs != 0) { + Worker->Stats.CumulativeActiveTimeUs += + CxPlatTimeDiff64(Worker->Stats.ActiveStartTimeUs, CxPlatTimeUs64()); + Worker->Stats.ActiveStartTimeUs = 0; + } +} + +// +// Marks the start of an active period. +// +void +CxPlatWorkerStatsResume( + _Inout_ CXPLAT_WORKER* Worker + ) +{ + Worker->Stats.ActiveStartTimeUs = CxPlatTimeUs64(); +} + void CxPlatProcessEvents( _In_ CXPLAT_WORKER* Worker ) { CXPLAT_CQE Cqes[16]; + + if (Worker->State.WaitTime > 0) { + CxPlatWorkerStatsPause(Worker); + } + uint32_t CqeCount = CxPlatEventQDequeue( &Worker->EventQ, Cqes, ARRAYSIZE(Cqes), Worker->State.WaitTime); + + if (Worker->State.WaitTime > 0) { + CxPlatWorkerStatsResume(Worker); + } + uint32_t CurrentCqeCount = CqeCount; CXPLAT_CQE* CurrentCqe = Cqes; @@ -796,6 +854,8 @@ CXPLAT_THREAD_CALLBACK(CxPlatWorkerThread, Context) Worker->State.ThreadID = CxPlatCurThreadID(); Worker->Running = TRUE; + Worker->Stats.StartedTimeUs = CxPlatTimeUs64(); + Worker->Stats.ActiveStartTimeUs = Worker->Stats.StartedTimeUs; while (!Worker->StoppedThread) { @@ -816,7 +876,9 @@ CXPLAT_THREAD_CALLBACK(CxPlatWorkerThread, Context) if (Worker->State.NoWorkCount == 0) { Worker->State.LastWorkTime = Worker->State.TimeNow; } else if (Worker->State.NoWorkCount > CXPLAT_WORKER_IDLE_WORK_THRESHOLD_COUNT) { + CxPlatWorkerStatsPause(Worker); CxPlatSchedulerYield(); + CxPlatWorkerStatsResume(Worker); Worker->State.NoWorkCount = 0; } @@ -826,6 +888,7 @@ CXPLAT_THREAD_CALLBACK(CxPlatWorkerThread, Context) } } + CxPlatWorkerStatsPause(Worker); Worker->Running = FALSE; #if DEBUG @@ -839,3 +902,28 @@ CXPLAT_THREAD_CALLBACK(CxPlatWorkerThread, Context) CXPLAT_THREAD_RETURN(0); } + +void +CxPlatWorkerPoolGetStatistics( + _In_ CXPLAT_WORKER_POOL* WorkerPool, + _In_ uint32_t Index, + _In_ uint64_t TimeNow, + _Out_ CXPLAT_WORKER_STATISTICS* Stats + ) +{ + CXPLAT_FRE_ASSERT(Index < WorkerPool->WorkerCount); + + CXPLAT_WORKER* Worker = &WorkerPool->Workers[Index]; + + Stats->IdealProcessor = Worker->IdealProcessor; + Stats->CumulativeWallTimeUs = CxPlatTimeDiff64(Worker->Stats.StartedTimeUs, TimeNow); + Stats->CumulativeActiveTimeUs = Worker->Stats.CumulativeActiveTimeUs; + + // + // If the worker is currently active, include the in-progress active period. + // + const uint64_t ActiveStartTimeUs = Worker->Stats.ActiveStartTimeUs; + if (ActiveStartTimeUs != 0 && ActiveStartTimeUs < TimeNow) { + Stats->CumulativeActiveTimeUs += CxPlatTimeDiff64(ActiveStartTimeUs, TimeNow); + } +} diff --git a/src/rs/ffi/linux_bindings.rs b/src/rs/ffi/linux_bindings.rs index 410ec4efae..6fec75c9ca 100644 --- a/src/rs/ffi/linux_bindings.rs +++ b/src/rs/ffi/linux_bindings.rs @@ -176,6 +176,7 @@ pub const QUIC_PARAM_GLOBAL_STATELESS_RESET_KEY: u32 = 16777227; pub const QUIC_PARAM_GLOBAL_STATISTICS_V2_SIZES: u32 = 16777228; pub const QUIC_PARAM_GLOBAL_STATELESS_RETRY_CONFIG: u32 = 16777229; pub const QUIC_PARAM_GLOBAL_XDP_MAP_CONFIG: u32 = 16777230; +pub const QUIC_PARAM_GLOBAL_WORKER_STATISTICS: u32 = 16777231; pub const QUIC_PARAM_CONFIGURATION_SETTINGS: u32 = 50331648; pub const QUIC_PARAM_CONFIGURATION_TICKET_KEYS: u32 = 50331649; pub const QUIC_PARAM_CONFIGURATION_VERSION_SETTINGS: u32 = 50331650; @@ -519,6 +520,42 @@ const _: () = { }; #[repr(C)] #[derive(Debug, Copy, Clone)] +pub struct QUIC_WORKER_STATISTICS { + pub CumulativeActiveTimeUs: u64, + pub CumulativeWallTimeUs: u64, + pub IdealProcessor: u16, +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of QUIC_WORKER_STATISTICS"][::std::mem::size_of::() - 24usize]; + ["Alignment of QUIC_WORKER_STATISTICS"] + [::std::mem::align_of::() - 8usize]; + ["Offset of field: QUIC_WORKER_STATISTICS::CumulativeActiveTimeUs"] + [::std::mem::offset_of!(QUIC_WORKER_STATISTICS, CumulativeActiveTimeUs) - 0usize]; + ["Offset of field: QUIC_WORKER_STATISTICS::CumulativeWallTimeUs"] + [::std::mem::offset_of!(QUIC_WORKER_STATISTICS, CumulativeWallTimeUs) - 8usize]; + ["Offset of field: QUIC_WORKER_STATISTICS::IdealProcessor"] + [::std::mem::offset_of!(QUIC_WORKER_STATISTICS, IdealProcessor) - 16usize]; +}; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct QUIC_WORKER_STATISTICS_LIST { + pub WorkerCount: u32, + pub WorkerStatsSize: u32, +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of QUIC_WORKER_STATISTICS_LIST"] + [::std::mem::size_of::() - 8usize]; + ["Alignment of QUIC_WORKER_STATISTICS_LIST"] + [::std::mem::align_of::() - 4usize]; + ["Offset of field: QUIC_WORKER_STATISTICS_LIST::WorkerCount"] + [::std::mem::offset_of!(QUIC_WORKER_STATISTICS_LIST, WorkerCount) - 0usize]; + ["Offset of field: QUIC_WORKER_STATISTICS_LIST::WorkerStatsSize"] + [::std::mem::offset_of!(QUIC_WORKER_STATISTICS_LIST, WorkerStatsSize) - 4usize]; +}; +#[repr(C)] +#[derive(Debug, Copy, Clone)] pub struct QUIC_REGISTRATION_CONFIG { pub AppName: *const ::std::os::raw::c_char, pub ExecutionProfile: QUIC_EXECUTION_PROFILE, diff --git a/src/rs/ffi/win_bindings.rs b/src/rs/ffi/win_bindings.rs index 76289d8edd..fde4d36891 100644 --- a/src/rs/ffi/win_bindings.rs +++ b/src/rs/ffi/win_bindings.rs @@ -170,6 +170,7 @@ pub const QUIC_PARAM_GLOBAL_STATELESS_RESET_KEY: u32 = 16777227; pub const QUIC_PARAM_GLOBAL_STATISTICS_V2_SIZES: u32 = 16777228; pub const QUIC_PARAM_GLOBAL_STATELESS_RETRY_CONFIG: u32 = 16777229; pub const QUIC_PARAM_GLOBAL_XDP_MAP_CONFIG: u32 = 16777230; +pub const QUIC_PARAM_GLOBAL_WORKER_STATISTICS: u32 = 16777231; pub const QUIC_PARAM_CONFIGURATION_SETTINGS: u32 = 50331648; pub const QUIC_PARAM_CONFIGURATION_TICKET_KEYS: u32 = 50331649; pub const QUIC_PARAM_CONFIGURATION_VERSION_SETTINGS: u32 = 50331650; @@ -517,6 +518,42 @@ const _: () = { }; #[repr(C)] #[derive(Debug, Copy, Clone)] +pub struct QUIC_WORKER_STATISTICS { + pub CumulativeActiveTimeUs: u64, + pub CumulativeWallTimeUs: u64, + pub IdealProcessor: u16, +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of QUIC_WORKER_STATISTICS"][::std::mem::size_of::() - 24usize]; + ["Alignment of QUIC_WORKER_STATISTICS"] + [::std::mem::align_of::() - 8usize]; + ["Offset of field: QUIC_WORKER_STATISTICS::CumulativeActiveTimeUs"] + [::std::mem::offset_of!(QUIC_WORKER_STATISTICS, CumulativeActiveTimeUs) - 0usize]; + ["Offset of field: QUIC_WORKER_STATISTICS::CumulativeWallTimeUs"] + [::std::mem::offset_of!(QUIC_WORKER_STATISTICS, CumulativeWallTimeUs) - 8usize]; + ["Offset of field: QUIC_WORKER_STATISTICS::IdealProcessor"] + [::std::mem::offset_of!(QUIC_WORKER_STATISTICS, IdealProcessor) - 16usize]; +}; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct QUIC_WORKER_STATISTICS_LIST { + pub WorkerCount: u32, + pub WorkerStatsSize: u32, +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of QUIC_WORKER_STATISTICS_LIST"] + [::std::mem::size_of::() - 8usize]; + ["Alignment of QUIC_WORKER_STATISTICS_LIST"] + [::std::mem::align_of::() - 4usize]; + ["Offset of field: QUIC_WORKER_STATISTICS_LIST::WorkerCount"] + [::std::mem::offset_of!(QUIC_WORKER_STATISTICS_LIST, WorkerCount) - 0usize]; + ["Offset of field: QUIC_WORKER_STATISTICS_LIST::WorkerStatsSize"] + [::std::mem::offset_of!(QUIC_WORKER_STATISTICS_LIST, WorkerStatsSize) - 4usize]; +}; +#[repr(C)] +#[derive(Debug, Copy, Clone)] pub struct QUIC_REGISTRATION_CONFIG { pub AppName: *const ::std::os::raw::c_char, pub ExecutionProfile: QUIC_EXECUTION_PROFILE, diff --git a/src/test/MsQuicTests.h b/src/test/MsQuicTests.h index 8e1b8e3e54..577d103817 100644 --- a/src/test/MsQuicTests.h +++ b/src/test/MsQuicTests.h @@ -96,6 +96,7 @@ void QuicTestGetPerfCounters(); #ifdef QUIC_API_ENABLE_PREVIEW_FEATURES void QuicTestValidateEncryptDecryptPerfCounters(); void QuicTestConnQueueDelayStatistics(); +void QuicTestGetWorkerStatistics(); #endif void QuicTestVersionSettings(); void QuicTestValidateParamApi(); diff --git a/src/test/bin/quic_gtest.cpp b/src/test/bin/quic_gtest.cpp index ae8f2e99e2..0041b894f3 100644 --- a/src/test/bin/quic_gtest.cpp +++ b/src/test/bin/quic_gtest.cpp @@ -462,6 +462,15 @@ TEST(ParameterValidation, ConnQueueDelayStatistics) { QuicTestConnQueueDelayStatistics(); } } + +TEST(ParameterValidation, ValidateGetWorkerStatistics) { + TestLogger Logger("QuicTestGetWorkerStatistics"); + if (TestingKernelMode) { + ASSERT_TRUE(InvokeKernelTest(FUNC(QuicTestGetWorkerStatistics))); + } else { + QuicTestGetWorkerStatistics(); + } +} #endif // QUIC_API_ENABLE_PREVIEW_FEATURES TEST(ParameterValidation, ValidateConfiguration) { diff --git a/src/test/bin/winkernel/control.cpp b/src/test/bin/winkernel/control.cpp index ef493eb9e4..d70f102313 100644 --- a/src/test/bin/winkernel/control.cpp +++ b/src/test/bin/winkernel/control.cpp @@ -466,6 +466,7 @@ ExecuteTestRequest( #ifdef QUIC_API_ENABLE_PREVIEW_FEATURES RegisterTestFunction(QuicTestValidateEncryptDecryptPerfCounters); RegisterTestFunction(QuicTestConnQueueDelayStatistics); + RegisterTestFunction(QuicTestGetWorkerStatistics); #endif RegisterTestFunction(QuicTestValidateConfiguration); RegisterTestFunction(QuicTestValidateListener); diff --git a/src/test/lib/ApiTest.cpp b/src/test/lib/ApiTest.cpp index 13b1208dd9..6952141eab 100644 --- a/src/test/lib/ApiTest.cpp +++ b/src/test/lib/ApiTest.cpp @@ -6116,6 +6116,105 @@ QuicTestGetPerfCounters() TEST_EQUAL(BufferLength, (sizeof(uint64_t) * (QUIC_PERF_COUNTER_MAX - 4))); } +#ifdef QUIC_API_ENABLE_PREVIEW_FEATURES +void +QuicTestGetWorkerStatistics() +{ + // + // A registration must exist to ensure the global worker pool is created. + // + MsQuicRegistration Registration; + TEST_QUIC_SUCCEEDED(Registration.GetInitStatus()); + +#ifdef _KERNEL_MODE + // + // Worker statistics are not supported in kernel mode. + // + uint32_t UnsupportedLength = 0; + TEST_EQUAL( + MsQuic->GetParam( + nullptr, + QUIC_PARAM_GLOBAL_WORKER_STATISTICS, + &UnsupportedLength, + nullptr), + QUIC_STATUS_NOT_SUPPORTED); +#else + // + // Test getting the required size with zero-length buffer. + // + uint32_t BufferLength = 0; + TEST_EQUAL( + MsQuic->GetParam( + nullptr, + QUIC_PARAM_GLOBAL_WORKER_STATISTICS, + &BufferLength, + nullptr), + QUIC_STATUS_BUFFER_TOO_SMALL); + + TEST_TRUE(BufferLength >= sizeof(QUIC_WORKER_STATISTICS_LIST)); + + // + // Test with a buffer that is too small (only the header). + // + QUIC_WORKER_STATISTICS_LIST SmallBuffer = {0}; + uint32_t SmallLength = sizeof(QUIC_WORKER_STATISTICS_LIST); + QUIC_STATUS Status = + MsQuic->GetParam( + nullptr, + QUIC_PARAM_GLOBAL_WORKER_STATISTICS, + &SmallLength, + &SmallBuffer); + if (SmallLength > sizeof(QUIC_WORKER_STATISTICS_LIST)) { + // + // More than one worker, so the header-only buffer is too small. + // + TEST_EQUAL(Status, QUIC_STATUS_BUFFER_TOO_SMALL); + } + + // + // Allocate the correct size and get all stats. + // + BufferLength = 0; + MsQuic->GetParam( + nullptr, + QUIC_PARAM_GLOBAL_WORKER_STATISTICS, + &BufferLength, + nullptr); + + UniquePtrArray Buffer(new(std::nothrow) uint8_t[BufferLength]); + TEST_NOT_EQUAL(Buffer.get(), nullptr); + + TEST_QUIC_SUCCEEDED( + MsQuic->GetParam( + nullptr, + QUIC_PARAM_GLOBAL_WORKER_STATISTICS, + &BufferLength, + Buffer.get())); + + QUIC_WORKER_STATISTICS_LIST* List = (QUIC_WORKER_STATISTICS_LIST*)Buffer.get(); + TEST_TRUE(List->WorkerCount > 0); + TEST_EQUAL(List->WorkerStatsSize, sizeof(QUIC_WORKER_STATISTICS)); + TEST_EQUAL( + BufferLength, + sizeof(QUIC_WORKER_STATISTICS_LIST) + List->WorkerCount * sizeof(QUIC_WORKER_STATISTICS)); + + // + // Validate each worker's statistics. + // + QUIC_WORKER_STATISTICS* Stats = + (QUIC_WORKER_STATISTICS*)(Buffer.get() + sizeof(QUIC_WORKER_STATISTICS_LIST)); + for (uint32_t i = 0; i < List->WorkerCount; i++) { + TEST_TRUE(Stats[i].CumulativeWallTimeUs > 0); + // + // Active time must not exceed wall time. + // + TEST_TRUE(Stats[i].CumulativeActiveTimeUs <= Stats[i].CumulativeWallTimeUs); + } +#endif // _KERNEL_MODE +} + +#endif // QUIC_API_ENABLE_PREVIEW_FEATURES + #ifdef QUIC_API_ENABLE_PREVIEW_FEATURES void QuicTestValidateEncryptDecryptPerfCounters()