From c4af934878ab6202dcbd196b143d6c2021972c78 Mon Sep 17 00:00:00 2001 From: Guillaume Hetier Date: Tue, 30 Jun 2026 15:39:49 -0700 Subject: [PATCH 1/6] Add QUIC_PARAM_GLOBAL_WORKER_STATISTICS for per-worker statistics Adds a preview global parameter to retrieve per-worker active and wall time statistics. Active time is tracked via timestamps in the platform worker loop. Kernel mode returns QUIC_STATUS_NOT_SUPPORTED. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/core/library.c | 63 ++++++++++++++++++ src/inc/msquic.h | 22 +++++++ src/inc/quic_platform.h | 16 +++++ src/platform/platform_worker.c | 60 +++++++++++++++++ src/rs/ffi/linux_bindings.rs | 37 +++++++++++ src/rs/ffi/win_bindings.rs | 37 +++++++++++ src/test/MsQuicTests.h | 1 + src/test/bin/quic_gtest.cpp | 9 +++ src/test/bin/winkernel/control.cpp | 1 + src/test/lib/ApiTest.cpp | 101 +++++++++++++++++++++++++++++ 10 files changed, 347 insertions(+) diff --git a/src/core/library.c b/src/core/library.c index 20dd458fab..ae41418611 100644 --- a/src/core/library.c +++ b/src/core/library.c @@ -1539,6 +1539,56 @@ QuicLibrarySetGlobalParam( return Status; } +#ifndef _KERNEL_MODE +// +// 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 + ) +{ + if (MsQuicLib.WorkerPool == NULL) { + return QUIC_STATUS_INVALID_STATE; + } + + uint32_t WorkerCount = CxPlatWorkerPoolGetCount(MsQuicLib.WorkerPool); + uint32_t RequiredSize = + sizeof(QUIC_WORKER_STATISTICS_LIST) + + WorkerCount * sizeof(QUIC_WORKER_STATISTICS); + + if (*BufferLength < RequiredSize) { + *BufferLength = RequiredSize; + return QUIC_STATUS_BUFFER_TOO_SMALL; + } + + if (Buffer == NULL) { + return QUIC_STATUS_INVALID_PARAMETER; + } + + 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)); + + for (uint32_t i = 0; i < WorkerCount; i++) { + CXPLAT_WORKER_STATISTICS WorkerStats = {0}; + CxPlatWorkerPoolGetStatistics(MsQuicLib.WorkerPool, i, &WorkerStats); + Stats[i].IdealProcessor = WorkerStats.IdealProcessor; + Stats[i].CumulativeActiveTimeUs = WorkerStats.CumulativeActiveTimeUs; + Stats[i].CumulativeWallTimeUs = WorkerStats.CumulativeWallTimeUs; + } + + *BufferLength = RequiredSize; + return QUIC_STATUS_SUCCESS; +} +#endif // !_KERNEL_MODE + _IRQL_requires_max_(PASSIVE_LEVEL) _Success_(return == QUIC_STATUS_SUCCESS) QUIC_STATUS @@ -1914,6 +1964,19 @@ QuicLibraryGetGlobalParam( break; } + case QUIC_PARAM_GLOBAL_WORKER_STATISTICS: { +#ifdef _KERNEL_MODE + // + // Worker statistics are not supported in kernel mode, where the worker + // threads are not owned by the platform worker pool. + // + Status = QUIC_STATUS_NOT_SUPPORTED; +#else + Status = QuicLibraryGetGlobalWorkerStatistics(BufferLength, Buffer); +#endif + break; + } + default: Status = QUIC_STATUS_INVALID_PARAMETER; break; 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..88e11f3d44 100644 --- a/src/inc/quic_platform.h +++ b/src/inc/quic_platform.h @@ -576,6 +576,22 @@ 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, + _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..f7460c1eb1 100644 --- a/src/platform/platform_worker.c +++ b/src/platform/platform_worker.c @@ -102,6 +102,26 @@ 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. + // + 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 { @@ -734,12 +754,23 @@ CxPlatProcessEvents( ) { CXPLAT_CQE Cqes[16]; + + if (Worker->State.WaitTime > 0) { + Worker->Stats.CumulativeActiveTimeUs += + CxPlatTimeDiff64(Worker->Stats.ActiveStartTimeUs, CxPlatTimeUs64()); + } + uint32_t CqeCount = CxPlatEventQDequeue( &Worker->EventQ, Cqes, ARRAYSIZE(Cqes), Worker->State.WaitTime); + + if (Worker->State.WaitTime > 0) { + Worker->Stats.ActiveStartTimeUs = CxPlatTimeUs64(); + } + uint32_t CurrentCqeCount = CqeCount; CXPLAT_CQE* CurrentCqe = Cqes; @@ -796,6 +827,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 +849,10 @@ 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) { + Worker->Stats.CumulativeActiveTimeUs += + CxPlatTimeDiff64(Worker->Stats.ActiveStartTimeUs, CxPlatTimeUs64()); CxPlatSchedulerYield(); + Worker->Stats.ActiveStartTimeUs = CxPlatTimeUs64(); Worker->State.NoWorkCount = 0; } @@ -826,6 +862,8 @@ CXPLAT_THREAD_CALLBACK(CxPlatWorkerThread, Context) } } + Worker->Stats.CumulativeActiveTimeUs += + CxPlatTimeDiff64(Worker->Stats.ActiveStartTimeUs, CxPlatTimeUs64()); Worker->Running = FALSE; #if DEBUG @@ -839,3 +877,25 @@ CXPLAT_THREAD_CALLBACK(CxPlatWorkerThread, Context) CXPLAT_THREAD_RETURN(0); } + +void +CxPlatWorkerPoolGetStatistics( + _In_ CXPLAT_WORKER_POOL* WorkerPool, + _In_ uint32_t Index, + _Out_ CXPLAT_WORKER_STATISTICS* Stats + ) +{ + CXPLAT_DBG_ASSERT(Index < WorkerPool->WorkerCount); + + const uint64_t Now = CxPlatTimeUs64(); + const CXPLAT_WORKER* Worker = &WorkerPool->Workers[Index]; + + Stats->IdealProcessor = Worker->IdealProcessor; + Stats->CumulativeWallTimeUs = CxPlatTimeDiff64(Worker->Stats.StartedTimeUs, Now); + Stats->CumulativeActiveTimeUs = Worker->Stats.CumulativeActiveTimeUs; + + const uint64_t ActiveStartTime = Worker->Stats.ActiveStartTimeUs; + if (ActiveStartTime < Now) { + Stats->CumulativeActiveTimeUs += CxPlatTimeDiff64(ActiveStartTime, Now); + } +} 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..92378acca5 100644 --- a/src/test/lib/ApiTest.cpp +++ b/src/test/lib/ApiTest.cpp @@ -6116,6 +6116,107 @@ 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); + + uint8_t* Buffer = new(std::nothrow) uint8_t[BufferLength]; + TEST_NOT_EQUAL(Buffer, nullptr); + + TEST_QUIC_SUCCEEDED( + MsQuic->GetParam( + nullptr, + QUIC_PARAM_GLOBAL_WORKER_STATISTICS, + &BufferLength, + Buffer)); + + QUIC_WORKER_STATISTICS_LIST* List = (QUIC_WORKER_STATISTICS_LIST*)Buffer; + 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 + 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); + } + + delete[] Buffer; +#endif // _KERNEL_MODE +} + +#endif // QUIC_API_ENABLE_PREVIEW_FEATURES + #ifdef QUIC_API_ENABLE_PREVIEW_FEATURES void QuicTestValidateEncryptDecryptPerfCounters() From 3a92d5e2fea1b54573ea1c6c69a1633d62a3937b Mon Sep 17 00:00:00 2001 From: Guillaume Hetier Date: Mon, 13 Jul 2026 13:33:19 -0700 Subject: [PATCH 2/6] Address review feedback: pause/resume stats helpers, KM NOT_SUPPORTED, UniquePtrArray Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/core/library.c | 23 +++++++------- src/platform/platform_worker.c | 56 +++++++++++++++++++++++++--------- src/test/lib/ApiTest.cpp | 12 +++----- 3 files changed, 58 insertions(+), 33 deletions(-) diff --git a/src/core/library.c b/src/core/library.c index ae41418611..bbdafc90fd 100644 --- a/src/core/library.c +++ b/src/core/library.c @@ -1539,7 +1539,6 @@ QuicLibrarySetGlobalParam( return Status; } -#ifndef _KERNEL_MODE // // Fills the caller's buffer with the per-worker statistics from the worker pool. // @@ -1551,6 +1550,15 @@ QuicLibraryGetGlobalWorkerStatistics( 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 if (MsQuicLib.WorkerPool == NULL) { return QUIC_STATUS_INVALID_STATE; } @@ -1586,8 +1594,8 @@ QuicLibraryGetGlobalWorkerStatistics( *BufferLength = RequiredSize; return QUIC_STATUS_SUCCESS; +#endif // _KERNEL_MODE } -#endif // !_KERNEL_MODE _IRQL_requires_max_(PASSIVE_LEVEL) _Success_(return == QUIC_STATUS_SUCCESS) @@ -1964,18 +1972,9 @@ QuicLibraryGetGlobalParam( break; } - case QUIC_PARAM_GLOBAL_WORKER_STATISTICS: { -#ifdef _KERNEL_MODE - // - // Worker statistics are not supported in kernel mode, where the worker - // threads are not owned by the platform worker pool. - // - Status = QUIC_STATUS_NOT_SUPPORTED; -#else + case QUIC_PARAM_GLOBAL_WORKER_STATISTICS: Status = QuicLibraryGetGlobalWorkerStatistics(BufferLength, Buffer); -#endif break; - } default: Status = QUIC_STATUS_INVALID_PARAMETER; diff --git a/src/platform/platform_worker.c b/src/platform/platform_worker.c index f7460c1eb1..ca6065121e 100644 --- a/src/platform/platform_worker.c +++ b/src/platform/platform_worker.c @@ -112,7 +112,8 @@ typedef struct QUIC_CACHEALIGN CXPLAT_WORKER { uint64_t StartedTimeUs; // - // Timestamp (in microseconds) when the current active period started. + // Timestamp (in microseconds) when the current active period started, + // or 0 when the worker is currently idle (waiting or yielding). // uint64_t ActiveStartTimeUs; @@ -748,6 +749,33 @@ 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 @@ -756,8 +784,7 @@ CxPlatProcessEvents( CXPLAT_CQE Cqes[16]; if (Worker->State.WaitTime > 0) { - Worker->Stats.CumulativeActiveTimeUs += - CxPlatTimeDiff64(Worker->Stats.ActiveStartTimeUs, CxPlatTimeUs64()); + CxPlatWorkerStatsPause(Worker); } uint32_t CqeCount = @@ -768,7 +795,7 @@ CxPlatProcessEvents( Worker->State.WaitTime); if (Worker->State.WaitTime > 0) { - Worker->Stats.ActiveStartTimeUs = CxPlatTimeUs64(); + CxPlatWorkerStatsResume(Worker); } uint32_t CurrentCqeCount = CqeCount; @@ -849,10 +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) { - Worker->Stats.CumulativeActiveTimeUs += - CxPlatTimeDiff64(Worker->Stats.ActiveStartTimeUs, CxPlatTimeUs64()); + CxPlatWorkerStatsPause(Worker); CxPlatSchedulerYield(); - Worker->Stats.ActiveStartTimeUs = CxPlatTimeUs64(); + CxPlatWorkerStatsResume(Worker); Worker->State.NoWorkCount = 0; } @@ -862,8 +888,7 @@ CXPLAT_THREAD_CALLBACK(CxPlatWorkerThread, Context) } } - Worker->Stats.CumulativeActiveTimeUs += - CxPlatTimeDiff64(Worker->Stats.ActiveStartTimeUs, CxPlatTimeUs64()); + CxPlatWorkerStatsPause(Worker); Worker->Running = FALSE; #if DEBUG @@ -885,17 +910,20 @@ CxPlatWorkerPoolGetStatistics( _Out_ CXPLAT_WORKER_STATISTICS* Stats ) { - CXPLAT_DBG_ASSERT(Index < WorkerPool->WorkerCount); + CXPLAT_FRE_ASSERT(Index < WorkerPool->WorkerCount); const uint64_t Now = CxPlatTimeUs64(); - const CXPLAT_WORKER* Worker = &WorkerPool->Workers[Index]; + CXPLAT_WORKER* Worker = &WorkerPool->Workers[Index]; Stats->IdealProcessor = Worker->IdealProcessor; Stats->CumulativeWallTimeUs = CxPlatTimeDiff64(Worker->Stats.StartedTimeUs, Now); Stats->CumulativeActiveTimeUs = Worker->Stats.CumulativeActiveTimeUs; - const uint64_t ActiveStartTime = Worker->Stats.ActiveStartTimeUs; - if (ActiveStartTime < Now) { - Stats->CumulativeActiveTimeUs += CxPlatTimeDiff64(ActiveStartTime, Now); + // + // If the worker is currently active, include the in-progress active period. + // + const uint64_t ActiveStartTimeUs = Worker->Stats.ActiveStartTimeUs; + if (ActiveStartTimeUs != 0 && ActiveStartTimeUs < Now) { + Stats->CumulativeActiveTimeUs += CxPlatTimeDiff64(ActiveStartTimeUs, Now); } } diff --git a/src/test/lib/ApiTest.cpp b/src/test/lib/ApiTest.cpp index 92378acca5..6952141eab 100644 --- a/src/test/lib/ApiTest.cpp +++ b/src/test/lib/ApiTest.cpp @@ -6181,17 +6181,17 @@ QuicTestGetWorkerStatistics() &BufferLength, nullptr); - uint8_t* Buffer = new(std::nothrow) uint8_t[BufferLength]; - TEST_NOT_EQUAL(Buffer, 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)); + Buffer.get())); - QUIC_WORKER_STATISTICS_LIST* List = (QUIC_WORKER_STATISTICS_LIST*)Buffer; + 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( @@ -6202,7 +6202,7 @@ QuicTestGetWorkerStatistics() // Validate each worker's statistics. // QUIC_WORKER_STATISTICS* Stats = - (QUIC_WORKER_STATISTICS*)(Buffer + sizeof(QUIC_WORKER_STATISTICS_LIST)); + (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); // @@ -6210,8 +6210,6 @@ QuicTestGetWorkerStatistics() // TEST_TRUE(Stats[i].CumulativeActiveTimeUs <= Stats[i].CumulativeWallTimeUs); } - - delete[] Buffer; #endif // _KERNEL_MODE } From b8e81fb0854170abf45ea6e6c27586f24c23cfa7 Mon Sep 17 00:00:00 2001 From: Guillaume Hetier Date: Mon, 13 Jul 2026 19:49:31 -0700 Subject: [PATCH 3/6] Serialize worker pool pointer access under the library lock Protect QUIC_PARAM_GLOBAL_WORKER_STATISTICS from concurrent teardown of the platform worker pool. The query now captures MsQuicLib.WorkerPool and takes a rundown reference (CXPLAT_WORKER_POOL_REF_STATS) under the library lock, releasing the lock before doing work on the referenced pool. To keep the pointer from being freed under a reader, MsQuicExecutionCreate and MsQuicExecutionDelete now mutate MsQuicLib.WorkerPool under the same lock and never leave it dangling (null under the lock, delete/create outside). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/core/library.c | 56 ++++++++++++++++++++++++++++++++--------- src/inc/quic_platform.h | 1 + 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/core/library.c b/src/core/library.c index bbdafc90fd..bca1279700 100644 --- a/src/core/library.c +++ b/src/core/library.c @@ -1559,22 +1559,34 @@ QuicLibraryGetGlobalWorkerStatistics( UNREFERENCED_PARAMETER(Buffer); return QUIC_STATUS_NOT_SUPPORTED; #else - if (MsQuicLib.WorkerPool == NULL) { + 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(MsQuicLib.WorkerPool); + uint32_t WorkerCount = CxPlatWorkerPoolGetCount(WorkerPool); uint32_t RequiredSize = sizeof(QUIC_WORKER_STATISTICS_LIST) + WorkerCount * sizeof(QUIC_WORKER_STATISTICS); if (*BufferLength < RequiredSize) { *BufferLength = RequiredSize; - return QUIC_STATUS_BUFFER_TOO_SMALL; + Status = QUIC_STATUS_BUFFER_TOO_SMALL; + goto Release; } if (Buffer == NULL) { - return QUIC_STATUS_INVALID_PARAMETER; + Status = QUIC_STATUS_INVALID_PARAMETER; + goto Release; } QUIC_WORKER_STATISTICS_LIST* List = (QUIC_WORKER_STATISTICS_LIST*)Buffer; @@ -1586,14 +1598,20 @@ QuicLibraryGetGlobalWorkerStatistics( for (uint32_t i = 0; i < WorkerCount; i++) { CXPLAT_WORKER_STATISTICS WorkerStats = {0}; - CxPlatWorkerPoolGetStatistics(MsQuicLib.WorkerPool, i, &WorkerStats); + CxPlatWorkerPoolGetStatistics(WorkerPool, i, &WorkerStats); Stats[i].IdealProcessor = WorkerStats.IdealProcessor; Stats[i].CumulativeActiveTimeUs = WorkerStats.CumulativeActiveTimeUs; Stats[i].CumulativeWallTimeUs = WorkerStats.CumulativeWallTimeUs; } *BufferLength = RequiredSize; - return QUIC_STATUS_SUCCESS; + Status = QUIC_STATUS_SUCCESS; + +Release: + + CxPlatWorkerPoolRelease(WorkerPool, CXPLAT_WORKER_POOL_REF_STATS); + + return Status; #endif // _KERNEL_MODE } @@ -2978,14 +2996,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( @@ -3013,9 +3041,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/quic_platform.h b/src/inc/quic_platform.h index 88e11f3d44..92228fdeef 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 From 613e221f283b4706d5be194d9745be354318b72f Mon Sep 17 00:00:00 2001 From: Guillaume Hetier Date: Fri, 17 Jul 2026 17:55:31 -0700 Subject: [PATCH 4/6] Add documentation --- docs/PreviewFeatures.md | 2 +- docs/Settings.md | 1 + docs/api/GetParam.md | 45 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) 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)
From cf68e51c8836c8ca6594196d96bd4ec3cf6768b3 Mon Sep 17 00:00:00 2001 From: Guillaume Hetier Date: Mon, 20 Jul 2026 12:10:19 +0200 Subject: [PATCH 5/6] Snap time only once on stat query --- src/core/library.c | 3 ++- src/inc/quic_platform.h | 1 + src/platform/platform_worker.c | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/core/library.c b/src/core/library.c index bca1279700..02e2d64972 100644 --- a/src/core/library.c +++ b/src/core/library.c @@ -1596,9 +1596,10 @@ QuicLibraryGetGlobalWorkerStatistics( 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, &WorkerStats); + CxPlatWorkerPoolGetStatistics(WorkerPool, i, Now, &WorkerStats); Stats[i].IdealProcessor = WorkerStats.IdealProcessor; Stats[i].CumulativeActiveTimeUs = WorkerStats.CumulativeActiveTimeUs; Stats[i].CumulativeWallTimeUs = WorkerStats.CumulativeWallTimeUs; diff --git a/src/inc/quic_platform.h b/src/inc/quic_platform.h index 92228fdeef..2a337c61dd 100644 --- a/src/inc/quic_platform.h +++ b/src/inc/quic_platform.h @@ -590,6 +590,7 @@ void CxPlatWorkerPoolGetStatistics( _In_ CXPLAT_WORKER_POOL* WorkerPool, _In_ uint32_t Index, + _In_ uint64_t TimeNow, _Out_ CXPLAT_WORKER_STATISTICS* Stats ); diff --git a/src/platform/platform_worker.c b/src/platform/platform_worker.c index ca6065121e..d2d483d683 100644 --- a/src/platform/platform_worker.c +++ b/src/platform/platform_worker.c @@ -907,12 +907,12 @@ 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); - const uint64_t Now = CxPlatTimeUs64(); CXPLAT_WORKER* Worker = &WorkerPool->Workers[Index]; Stats->IdealProcessor = Worker->IdealProcessor; From 58bb6d6bc61b13e207cf81fbc897d00178b365a7 Mon Sep 17 00:00:00 2001 From: Guillaume Hetier Date: Mon, 20 Jul 2026 12:24:15 +0200 Subject: [PATCH 6/6] build fix --- src/platform/platform_worker.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/platform/platform_worker.c b/src/platform/platform_worker.c index d2d483d683..ca11681ab3 100644 --- a/src/platform/platform_worker.c +++ b/src/platform/platform_worker.c @@ -916,14 +916,14 @@ CxPlatWorkerPoolGetStatistics( CXPLAT_WORKER* Worker = &WorkerPool->Workers[Index]; Stats->IdealProcessor = Worker->IdealProcessor; - Stats->CumulativeWallTimeUs = CxPlatTimeDiff64(Worker->Stats.StartedTimeUs, Now); + 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 < Now) { - Stats->CumulativeActiveTimeUs += CxPlatTimeDiff64(ActiveStartTimeUs, Now); + if (ActiveStartTimeUs != 0 && ActiveStartTimeUs < TimeNow) { + Stats->CumulativeActiveTimeUs += CxPlatTimeDiff64(ActiveStartTimeUs, TimeNow); } }