CLI: Stats command - fix incorrect CPU % reporting (#40627)
David Bennett committed
May 29, 2026 at 11:12 UTC
262a2696e15cf87e42dd0f96e605537c104bbfe4
4 files changed
+298
-86
src/windows/wslc/core/AsyncExecution.h
new
+100
@@ -0,0 +1,100 @@
1
+// Copyright (C) Microsoft Corporation. All rights reserved.
2
+
3
+/*++
4
+
5
+Module Name:
6
+
7
+ AsyncExecution.h
8
+
9
+Abstract:
10
+
11
+ Provides ForEachAsync, a generic helper for executing a work callback
12
+ over a collection concurrently in bounded batches using std::async.
13
+
14
+--*/
15
+#pragma once
16
+
17
+#include <algorithm>
18
+#include <future>
19
+#include <optional>
20
+#include <utility>
21
+#include <vector>
22
+#include <wil/result_macros.h>
23
+
24
+namespace wsl::windows::wslc {
25
+
26
+// Invokes onWork for each element in items concurrently, in batches of batchSize.
27
+// Results are delivered serially to onSuccess. Errors are delivered serially to onError.
28
+//
29
+// This keeps wall time proportional to ceil(N / batchSize) rather than N for operations
30
+// that have inherent per-item latency (e.g. network or IPC calls).
31
+//
32
+// Note: worker threads have no guaranteed per-thread initialization (e.g. COM). Callers
33
+// whose onWork requires per-thread setup (such as CoInitializeEx) are responsible for
34
+// performing it at the start of the onWork lambda.
35
+//
36
+// TWork : TItem -> TResult (called concurrently)
37
+// TSuccess: TResult -> void (called serially)
38
+// TError : (TItem, wil::ResultException) -> void (called serially)
39
+template <typename TItem, typename TWork, typename TSuccess, typename TError>
40
+void ForEachAsync(const std::vector<TItem>& items, TWork onWork, TSuccess onSuccess, TError onError, size_t batchSize = 10)
41
+{
42
+ WI_ASSERT(batchSize > 0);
43
+ THROW_HR_IF(E_INVALIDARG, batchSize == 0);
44
+
45
+ using TResult = decltype(onWork(std::declval<TItem>()));
46
+
47
+ struct BatchResult
48
+ {
49
+ explicit BatchResult(TItem capturedItem) : item(std::move(capturedItem))
50
+ {
51
+ }
52
+
53
+ TItem item;
54
+ std::optional<TResult> result;
55
+ wil::ResultException error{S_OK};
56
+ bool hasError{false};
57
+ };
58
+
59
+ for (size_t batchStart = 0; batchStart < items.size(); batchStart += batchSize)
60
+ {
61
+ const size_t batchEnd = std::min(batchStart + batchSize, items.size());
62
+
63
+ std::vector<std::future<BatchResult>> futures;
64
+ futures.reserve(batchEnd - batchStart);
65
+
66
+ for (size_t i = batchStart; i < batchEnd; ++i)
67
+ {
68
+ const auto& item = items[i];
69
+ futures.push_back(std::async(std::launch::async, [&onWork, item]() -> BatchResult {
70
+ BatchResult result{item};
71
+ try
72
+ {
73
+ result.result = onWork(item);
74
+ }
75
+ catch (const wil::ResultException& ex)
76
+ {
77
+ result.hasError = true;
78
+ result.error = ex;
79
+ }
80
+ return result;
81
+ }));
82
+ }
83
+
84
+ for (auto& future : futures)
85
+ {
86
+ auto batchResult = future.get();
87
+
88
+ if (batchResult.hasError)
89
+ {
90
+ onError(batchResult.item, batchResult.error);
91
+ }
92
+ else if (batchResult.result.has_value())
93
+ {
94
+ onSuccess(*batchResult.result);
95
+ }
96
+ }
97
+ }
98
+}
99
+
100
+} // namespace wsl::windows::wslc
src/windows/wslc/tasks/ContainerTasks.cpp
+97
-85
@@ -13,6 +13,7 @@ Abstract:
13
--*/
14
#include "Argument.h"
15
#include "ArgumentValidation.h"
16
+#include "AsyncExecution.h"
17
#include "CLIExecutionContext.h"
18
#include "ContainerModel.h"
19
#include "ContainerService.h"
@@ -61,6 +62,76 @@ std::string FormatBytes(uint64_t bytes)
62
}
63
}
64
65
+nlohmann::json ComputeContainerStatsJson(const wsl::windows::common::docker_schema::ContainerStats& stats)
66
+{
67
+ // Calculate CPU %
68
+ // Formula matches Docker CLI: https://github.com/docker/cli/blob/master/cli/command/container/stats_helpers.go
69
+ double cpuPercent = 0.0;
70
+ const auto cpuDelta =
71
+ static_cast<double>(stats.cpu_stats.cpu_usage.total_usage) - static_cast<double>(stats.precpu_stats.cpu_usage.total_usage);
72
+ const auto systemDelta = static_cast<double>(stats.cpu_stats.system_cpu_usage) - static_cast<double>(stats.precpu_stats.system_cpu_usage);
73
+ if (systemDelta > 0.0 && cpuDelta > 0.0)
74
+ {
75
+ uint32_t onlineCpus = stats.cpu_stats.online_cpus;
76
+ if (onlineCpus == 0 && stats.cpu_stats.cpu_usage.percpu_usage.has_value())
77
+ {
78
+ onlineCpus = static_cast<uint32_t>(stats.cpu_stats.cpu_usage.percpu_usage->size());
79
+ }
80
+
81
+ cpuPercent = (cpuDelta / systemDelta) * static_cast<double>(onlineCpus) * 100.0;
82
+ }
83
+
84
+ // Calculate memory %
85
+ double memPercent = 0.0;
86
+ if (stats.memory_stats.limit > 0)
87
+ {
88
+ memPercent = (static_cast<double>(stats.memory_stats.usage) / static_cast<double>(stats.memory_stats.limit)) * 100.0;
89
+ }
90
+
91
+ // Aggregate network I/O
92
+ uint64_t netRxBytes = 0;
93
+ uint64_t netTxBytes = 0;
94
+ if (stats.networks.has_value())
95
+ {
96
+ for (const auto& [iface, netStats] : *stats.networks)
97
+ {
98
+ netRxBytes += netStats.rx_bytes;
99
+ netTxBytes += netStats.tx_bytes;
100
+ }
101
+ }
102
+
103
+ // Aggregate block I/O
104
+ uint64_t blkReadBytes = 0;
105
+ uint64_t blkWriteBytes = 0;
106
+ if (stats.blkio_stats.io_service_bytes_recursive.has_value())
107
+ {
108
+ for (const auto& entry : *stats.blkio_stats.io_service_bytes_recursive)
109
+ {
110
+ if (_stricmp(entry.op.c_str(), "read") == 0)
111
+ {
112
+ blkReadBytes += entry.value;
113
+ }
114
+ else if (_stricmp(entry.op.c_str(), "write") == 0)
115
+ {
116
+ blkWriteBytes += entry.value;
117
+ }
118
+ }
119
+ }
120
+
121
+ const auto& containerName = stats.name.empty() ? stats.id : stats.name;
122
+
123
+ return {
124
+ {"ID", stats.id},
125
+ {"Name", containerName},
126
+ {"CPUPerc", std::format("{:.2f}%", cpuPercent)},
127
+ {"MemUsage", std::format("{} / {}", FormatBytes(stats.memory_stats.usage), FormatBytes(stats.memory_stats.limit))},
128
+ {"MemPerc", std::format("{:.2f}%", memPercent)},
129
+ {"NetIO", std::format("{} / {}", FormatBytes(netRxBytes), FormatBytes(netTxBytes))},
130
+ {"BlockIO", std::format("{} / {}", FormatBytes(blkReadBytes), FormatBytes(blkWriteBytes))},
131
+ {"PIDs", stats.pids_stats.current},
132
+ };
133
+}
134
+
135
} // namespace
136
137
namespace wsl::windows::wslc::task {
@@ -495,100 +566,41 @@ void ShowContainerStats(CLIExecutionContext& context)
566
}
567
}
568
498
- // Build stats as a json array first for later filtering or display either as json or table format.
569
+ // Fetch stats for all containers concurrently in batches. The Docker engine blocks for ~1s
570
+ // per request to collect a valid precpu_stats sample, so issuing requests in parallel keeps
571
+ // wall time proportional to ceil(N / batchSize) rather than N.
572
nlohmann::json statsJson = nlohmann::json::array();
500
- for (const auto& containerId : containers)
501
- {
502
- wsl::windows::common::docker_schema::ContainerStats stats;
503
- try
504
- {
505
- stats = ContainerService::Stats(session, WideToMultiByte(containerId));
506
- }
507
- catch (const wil::ResultException& ex)
508
- {
573
+ wsl::windows::wslc::ForEachAsync<std::wstring>(
574
+ containers,
575
+ // Work to be done for each container ID on a separate thread.
576
+ [&session](const std::wstring& containerId) {
577
+ // ContainerService::Stats makes COM calls, so we must ensure COM is initialized on this thread.
578
+ auto comCleanup = wil::CoInitializeEx(COINIT_MULTITHREADED);
579
+ return ComputeContainerStatsJson(ContainerService::Stats(session, WideToMultiByte(containerId)));
580
+ },
581
+ // On Success
582
+ [&](const nlohmann::json& entry) { statsJson.push_back(entry); },
583
+ // On Error
584
+ [&](const std::wstring& containerId, wil::ResultException error) {
585
if (!userSpecifiedContainers)
586
{
511
- // If the user did not explicitly specify a container then there may be expected
512
- // race conditions between listing containers and querying stats.
513
- switch (ex.GetErrorCode())
587
+ switch (error.GetErrorCode())
588
{
589
case RPC_E_DISCONNECTED:
590
case WSLC_E_CONTAINER_NOT_FOUND:
517
- continue;
518
- }
519
- }
520
-
521
- LOG_HR_MSG(ex.GetErrorCode(), "Failed to get stats for container %ws", containerId.c_str());
522
- throw;
523
- }
524
-
525
- // Calculate CPU %
526
- double cpuPercent = 0.0;
527
- const auto cpuDelta = static_cast<double>(stats.cpu_stats.cpu_usage.total_usage) -
528
- static_cast<double>(stats.precpu_stats.cpu_usage.total_usage);
529
- const auto systemDelta =
530
- static_cast<double>(stats.cpu_stats.system_cpu_usage) - static_cast<double>(stats.precpu_stats.system_cpu_usage);
531
- const auto onlineCpus = stats.cpu_stats.online_cpus > 0 ? stats.cpu_stats.online_cpus : 1u;
532
- if (systemDelta > 0.0 && cpuDelta >= 0.0)
533
- {
534
- cpuPercent = (cpuDelta / systemDelta) * static_cast<double>(onlineCpus) * 100.0;
535
- }
536
-
537
- // Calculate memory %
538
- double memPercent = 0.0;
539
- if (stats.memory_stats.limit > 0)
540
- {
541
- memPercent = (static_cast<double>(stats.memory_stats.usage) / static_cast<double>(stats.memory_stats.limit)) * 100.0;
542
- }
543
-
544
- // Aggregate network I/O
545
- uint64_t netRxBytes = 0;
546
- uint64_t netTxBytes = 0;
547
- if (stats.networks.has_value())
548
- {
549
- for (const auto& [iface, netStats] : *stats.networks)
550
- {
551
- netRxBytes += netStats.rx_bytes;
552
- netTxBytes += netStats.tx_bytes;
553
- }
554
- }
555
-
556
- // Aggregate block I/O
557
- uint64_t blkReadBytes = 0;
558
- uint64_t blkWriteBytes = 0;
559
- if (stats.blkio_stats.io_service_bytes_recursive.has_value())
560
- {
561
- for (const auto& entry : *stats.blkio_stats.io_service_bytes_recursive)
562
- {
563
- if (_stricmp(entry.op.c_str(), "read") == 0)
564
- {
565
- blkReadBytes += entry.value;
566
- }
567
- else if (_stricmp(entry.op.c_str(), "write") == 0)
568
- {
569
- blkWriteBytes += entry.value;
591
+ // Container disappeared between list and stats fetch, and
592
+ // the user did not specify these containers, so silently skip.
593
+ return;
594
}
595
}
572
- }
596
574
- const auto& containerName = stats.name.empty() ? stats.id : stats.name;
575
- const auto cpuPercentStr = std::format("{:.2f}%", cpuPercent);
576
- const auto memPercentStr = std::format("{:.2f}%", memPercent);
577
- const auto memUsage = std::format("{} / {}", FormatBytes(stats.memory_stats.usage), FormatBytes(stats.memory_stats.limit));
578
- const auto netIo = std::format("{} / {}", FormatBytes(netRxBytes), FormatBytes(netTxBytes));
579
- const auto blkIo = std::format("{} / {}", FormatBytes(blkReadBytes), FormatBytes(blkWriteBytes));
580
-
581
- statsJson.push_back({
582
- {"ID", stats.id},
583
- {"Name", containerName},
584
- {"CPUPerc", cpuPercentStr},
585
- {"MemUsage", memUsage},
586
- {"MemPerc", memPercentStr},
587
- {"NetIO", netIo},
588
- {"BlockIO", blkIo},
589
- {"PIDs", stats.pids_stats.current},
590
- });
591
- }
597
+ // Failure to retrieve a container should stop execution with
598
+ // no container information displayed.
599
+ LOG_HR_MSG(error.GetErrorCode(), "Failed to get stats for container %ws", containerId.c_str());
600
+ throw error;
601
+ },
602
+ 10 // Batch Size - chosen to be around typical expected container use while protecting against extreme cases.
603
+ );
604
605
FormatType format = FormatType::Table; // Default is table
606
if (context.Args.Contains(ArgType::Format))
src/windows/wslcsession/DockerHTTPClient.cpp
+4
-1
@@ -340,7 +340,10 @@ docker_schema::ContainerStats DockerHTTPClient::ContainerStats(const std::string
340
{
341
auto url = URL::Create("/containers/{}/stats", Id);
342
url.SetParameter("stream", false);
343
- url.SetParameter("one-shot", true);
343
+
344
+ // Intentionally omit one-shot=true: the Docker engine blocks internally for ~1s
345
+ // to collect a prior sample, and returns a single response with both cpu_stats and
346
+ // precpu_stats correctly populated — giving a valid delta for CPU % calculation.
347
return Transaction<EmptyRequest, docker_schema::ContainerStats>(verb::get, url);
348
}
349
test/windows/wslc/WSLCCLIExecutionUnitTests.cpp
+97
@@ -18,6 +18,7 @@ Abstract:
18
19
#include "SessionModel.h"
20
21
+#include "AsyncExecution.h"
22
#include "Command.h"
23
#include "RootCommand.h"
24
#include "ContainerCommand.h"
@@ -407,4 +408,100 @@ class WSLCCLIExecutionUnitTests
408
}
409
}
410
};
411
+
412
+class ForEachAsyncUnitTests
413
+{
414
+ WSLC_TEST_CLASS(ForEachAsyncUnitTests)
415
+
416
+ TEST_METHOD(ForEachAsync_SuccessCallbackInvokedForAllItems)
417
+ {
418
+ const std::vector<int> items = {1, 2, 3, 4, 5};
419
+ std::vector<int> results;
420
+
421
+ ForEachAsync<int>(
422
+ items,
423
+ [](int item) { return item * 2; },
424
+ [&](int result) { results.push_back(result); },
425
+ [](int /*item*/, wil::ResultException /*error*/) { VERIFY_FAIL(L"Unexpected error"); });
426
+
427
+ VERIFY_ARE_EQUAL(items.size(), results.size());
428
+ for (int item : items)
429
+ {
430
+ VERIFY_IS_TRUE(std::find(results.begin(), results.end(), item * 2) != results.end());
431
+ }
432
+ }
433
+
434
+ TEST_METHOD(ForEachAsync_ErrorCallbackInvokedOnFailure)
435
+ {
436
+ const std::vector<int> items = {1, 2, 3};
437
+ std::vector<int> failedItems;
438
+ std::vector<int> succeededItems;
439
+
440
+ ForEachAsync<int>(
441
+ items,
442
+ [](int item) -> int {
443
+ if (item == 2)
444
+ {
445
+ THROW_HR(E_FAIL);
446
+ }
447
+ return item;
448
+ },
449
+ [&](int result) { succeededItems.push_back(result); },
450
+ [&](int item, wil::ResultException /*error*/) { failedItems.push_back(item); });
451
+
452
+ VERIFY_ARE_EQUAL(1u, failedItems.size());
453
+ VERIFY_ARE_EQUAL(2, failedItems[0]);
454
+ VERIFY_ARE_EQUAL(2u, succeededItems.size());
455
+ }
456
+
457
+ TEST_METHOD(ForEachAsync_EmptyInputProducesNoCallbacks)
458
+ {
459
+ const std::vector<int> items;
460
+ bool successCalled = false;
461
+ bool errorCalled = false;
462
+
463
+ ForEachAsync<int>(
464
+ items,
465
+ [](int item) { return item; },
466
+ [&](int /*result*/) { successCalled = true; },
467
+ [&](int /*item*/, wil::ResultException /*error*/) { errorCalled = true; });
468
+
469
+ VERIFY_IS_FALSE(successCalled);
470
+ VERIFY_IS_FALSE(errorCalled);
471
+ }
472
+
473
+ TEST_METHOD(ForEachAsync_BatchSizeOfOneProcessesAllItems)
474
+ {
475
+ const std::vector<int> items = {10, 20, 30, 40, 50};
476
+ std::vector<int> results;
477
+
478
+ ForEachAsync<int>(
479
+ items,
480
+ [](int item) { return item; },
481
+ [&](int result) { results.push_back(result); },
482
+ [](int /*item*/, wil::ResultException /*error*/) { VERIFY_FAIL(L"Unexpected error"); },
483
+ /*batchSize=*/1);
484
+
485
+ VERIFY_ARE_EQUAL(items.size(), results.size());
486
+ for (int item : items)
487
+ {
488
+ VERIFY_IS_TRUE(std::find(results.begin(), results.end(), item) != results.end());
489
+ }
490
+ }
491
+
492
+ TEST_METHOD(ForEachAsync_ErrorInOnErrorPropagatesThrow)
493
+ {
494
+ const std::vector<int> items = {1};
495
+
496
+ VERIFY_THROWS_SPECIFIC(
497
+ ForEachAsync<int>(
498
+ items,
499
+ [](int /*item*/) -> int { THROW_HR(E_ACCESSDENIED); },
500
+ [](int /*result*/) {},
501
+ [](int /*item*/, wil::ResultException error) { throw error; }),
502
+ wil::ResultException,
503
+ [](const wil::ResultException& ex) { return ex.GetErrorCode() == E_ACCESSDENIED; });
504
+ }
505
+};
506
+
507
} // namespace WSLCCLIExecutionUnitTests