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()