Add ContainerStats runtime implmentation (#40475)
David Bennett committed
May 8, 2026 at 16:22 UTC
50b93bb06e5e15f48ca5ea37738936d41f28a364
7 files changed
+232
-1
src/windows/inc/docker_schema.h
+76
@@ -552,4 +552,80 @@ struct CreateImageProgress
552
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(CreateImageProgress, status, id, progressDetail, errorDetail);
553
};
554
555
+// Container stats (GET /containers/{id}/stats?stream=false)
556
+// See: https://docs.docker.com/reference/api/engine/version/v1.52/#tag/Container/operation/ContainerStats
557
+
558
+struct ContainerStatsCpuUsage
559
+{
560
+ uint64_t total_usage{};
561
+ std::optional<std::vector<uint64_t>> percpu_usage;
562
+ uint64_t usage_in_kernelmode{};
563
+ uint64_t usage_in_usermode{};
564
+
565
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerStatsCpuUsage, total_usage, percpu_usage, usage_in_kernelmode, usage_in_usermode);
566
+};
567
+
568
+struct ContainerStatsCpuStats
569
+{
570
+ ContainerStatsCpuUsage cpu_usage;
571
+ uint64_t system_cpu_usage{};
572
+ uint32_t online_cpus{};
573
+
574
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerStatsCpuStats, cpu_usage, system_cpu_usage, online_cpus);
575
+};
576
+
577
+struct ContainerStatsMemoryStats
578
+{
579
+ uint64_t usage{};
580
+ uint64_t limit{};
581
+
582
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerStatsMemoryStats, usage, limit);
583
+};
584
+
585
+struct ContainerStatsNetworkEntry
586
+{
587
+ uint64_t rx_bytes{};
588
+ uint64_t rx_packets{};
589
+ uint64_t tx_bytes{};
590
+ uint64_t tx_packets{};
591
+
592
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerStatsNetworkEntry, rx_bytes, rx_packets, tx_bytes, tx_packets);
593
+};
594
+
595
+struct ContainerStatsBlkioEntry
596
+{
597
+ std::string op;
598
+ uint64_t value{};
599
+
600
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerStatsBlkioEntry, op, value);
601
+};
602
+
603
+struct ContainerStatsBlkioStats
604
+{
605
+ std::optional<std::vector<ContainerStatsBlkioEntry>> io_service_bytes_recursive;
606
+
607
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerStatsBlkioStats, io_service_bytes_recursive);
608
+};
609
+
610
+struct ContainerStatsPidsStats
611
+{
612
+ uint64_t current{};
613
+
614
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerStatsPidsStats, current);
615
+};
616
+
617
+struct ContainerStats
618
+{
619
+ std::string id;
620
+ std::string name;
621
+ ContainerStatsCpuStats cpu_stats;
622
+ ContainerStatsCpuStats precpu_stats;
623
+ ContainerStatsMemoryStats memory_stats;
624
+ std::optional<std::map<std::string, ContainerStatsNetworkEntry>> networks;
625
+ ContainerStatsBlkioStats blkio_stats;
626
+ ContainerStatsPidsStats pids_stats;
627
+
628
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ContainerStats, id, name, cpu_stats, precpu_stats, memory_stats, networks, blkio_stats, pids_stats);
629
+};
630
+
631
} // namespace wsl::windows::common::docker_schema
src/windows/service/inc/wslc.idl
+1
@@ -527,6 +527,7 @@ interface IWSLCContainer : IUnknown
527
HRESULT GetName([out, string] LPSTR* Name);
528
HRESULT GetLabels([out, size_is(, *Count)] WSLCLabelInformation** Labels, [out] ULONG* Count);
529
HRESULT Kill([in] WSLCSignal Signal);
530
+ HRESULT Stats([out] LPSTR* Output);
531
}
532
533
typedef enum _WSLCDeletedImageType
src/windows/wslcsession/DockerHTTPClient.cpp
+8
@@ -385,6 +385,14 @@ docker_schema::InspectContainer DockerHTTPClient::InspectContainer(const std::st
385
return Transaction<EmptyRequest, docker_schema::InspectContainer>(verb::get, URL::Create("/containers/{}/json", Id));
386
}
387
388
+docker_schema::ContainerStats DockerHTTPClient::ContainerStats(const std::string& Id)
389
+{
390
+ auto url = URL::Create("/containers/{}/stats", Id);
391
+ url.SetParameter("stream", false);
392
+ url.SetParameter("one-shot", true);
393
+ return Transaction<EmptyRequest, docker_schema::ContainerStats>(verb::get, url);
394
+}
395
+
396
docker_schema::InspectExec DockerHTTPClient::InspectExec(const std::string& Id)
397
{
398
return Transaction<EmptyRequest, docker_schema::InspectExec>(verb::get, URL::Create("/exec/{}/json", Id));
src/windows/wslcsession/DockerHTTPClient.h
+2
-1
@@ -136,6 +136,7 @@ public:
136
void DeleteContainer(const std::string& Id, bool Force, bool DeleteVolumes = false);
137
void SignalContainer(const std::string& Id, std::optional<WSLCSignal> Signal);
138
common::docker_schema::InspectContainer InspectContainer(const std::string& Id);
139
+ common::docker_schema::ContainerStats ContainerStats(const std::string& Id);
140
common::docker_schema::InspectExec InspectExec(const std::string& Id);
141
wil::unique_socket AttachContainer(const std::string& Id, const std::optional<std::string>& DetachKeys);
142
void ResizeContainerTty(const std::string& Id, ULONG Rows, ULONG Columns);
@@ -291,4 +292,4 @@ private:
292
HANDLE m_exitingEvent;
293
wil::srwlock m_lock;
294
};
294
-} // namespace wsl::windows::service::wslc
\ No newline at end of file
295
+} // namespace wsl::windows::service::wslc
src/windows/wslcsession/WSLCContainer.cpp
+25
@@ -1732,6 +1732,19 @@ void WSLCContainerImpl::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle
1732
}
1733
}
1734
1735
+void WSLCContainerImpl::Stats(LPSTR* Output) const
1736
+{
1737
+ auto lock = m_lock.lock_shared();
1738
+
1739
+ try
1740
+ {
1741
+ auto stats = m_dockerClient.ContainerStats(m_id);
1742
+ std::string json = wsl::shared::ToJson(stats);
1743
+ *Output = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(json.c_str()).release();
1744
+ }
1745
+ CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to get stats for container '%hs'", m_id.c_str());
1746
+}
1747
+
1748
std::unique_ptr<RelayedProcessIO> WSLCContainerImpl::CreateRelayedProcessIO(wil::unique_handle&& stream, WSLCProcessFlags flags)
1749
{
1750
// Create one pipe for each STD handle.
@@ -2021,6 +2034,18 @@ HRESULT WSLCContainer::Inspect(LPSTR* Output)
2034
return CallImpl(&WSLCContainerImpl::Inspect, Output);
2035
}
2036
2037
+HRESULT WSLCContainer::Stats(LPSTR* Output)
2038
+try
2039
+{
2040
+ COMServiceExecutionContext context;
2041
+
2042
+ RETURN_HR_IF(E_POINTER, Output == nullptr);
2043
+
2044
+ *Output = nullptr;
2045
+ return CallImpl(&WSLCContainerImpl::Stats, Output);
2046
+}
2047
+CATCH_RETURN();
2048
+
2049
HRESULT WSLCContainer::Delete(WSLCDeleteFlags Flags)
2050
try
2051
{
src/windows/wslcsession/WSLCContainer.h
+2
@@ -102,6 +102,7 @@ public:
102
void Exec(_In_ const WSLCProcessOptions* Options, LPCSTR DetachKeys, _Out_ IWSLCProcess** Process);
103
void Inspect(LPSTR* Output) const;
104
void Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail) const;
105
+ void Stats(LPSTR* Output) const;
106
void GetLabels(WSLCLabelInformation** Labels, ULONG* Count) const;
107
108
void CopyTo(IWSLCContainer** Container) const;
@@ -228,6 +229,7 @@ public:
229
IFACEMETHOD(GetId)(_Out_ WSLCContainerId Id) override;
230
IFACEMETHOD(GetName)(_Out_ LPSTR* Name) override;
231
IFACEMETHOD(GetLabels)(_Out_ WSLCLabelInformation** Labels, _Out_ ULONG* Count) override;
232
+ IFACEMETHOD(Stats)(_Out_ LPSTR* Output) override;
233
234
IFACEMETHOD(InterfaceSupportsErrorInfo)(REFIID riid);
235
test/windows/WSLCTests.cpp
+118
@@ -7797,6 +7797,124 @@ class WSLCTests
7797
}
7798
}
7799
7800
+ WSLC_TEST_METHOD(ContainerStats_RunningContainer)
7801
+ {
7802
+ // Start a long-lived detached container on a bridged network so network stats are populated.
7803
+ WSLCContainerLauncher launcher("debian:latest", "wslc-test-stats", {"sleep", "60"}, {}, WSLCContainerNetworkTypeBridged);
7804
+
7805
+ auto runningContainer = launcher.Launch(*m_defaultSession, WSLCContainerStartFlagsNone);
7806
+ auto cleanup = wil::scope_exit([&]() { runningContainer.SetDeleteOnClose(true); });
7807
+
7808
+ wil::com_ptr<IWSLCContainer> container;
7809
+ VERIFY_SUCCEEDED(m_defaultSession->OpenContainer("wslc-test-stats", &container));
7810
+
7811
+ wil::unique_cotaskmem_ansistring output;
7812
+ VERIFY_SUCCEEDED(container->Stats(&output));
7813
+ VERIFY_IS_NOT_NULL(output.get());
7814
+ VERIFY_IS_FALSE(std::string(output.get()).empty());
7815
+
7816
+ const auto stats = wsl::shared::FromJson<wsl::windows::common::docker_schema::ContainerStats>(output.get());
7817
+
7818
+ // cpu_stats
7819
+ // The VM has been running so system_cpu_usage is non-zero.
7820
+ VERIFY_IS_GREATER_THAN(stats.cpu_stats.system_cpu_usage, 0ull);
7821
+
7822
+ // The container process itself has consumed some CPU.
7823
+ VERIFY_IS_GREATER_THAN(stats.cpu_stats.cpu_usage.total_usage, 0ull);
7824
+
7825
+ // Kernel + user time together must not exceed total CPU time.
7826
+ VERIFY_IS_LESS_THAN_OR_EQUAL(
7827
+ stats.cpu_stats.cpu_usage.usage_in_kernelmode + stats.cpu_stats.cpu_usage.usage_in_usermode, stats.cpu_stats.cpu_usage.total_usage);
7828
+
7829
+ // The session was created with 4 CPUs.
7830
+ VERIFY_IS_GREATER_THAN(stats.cpu_stats.online_cpus, 0u);
7831
+
7832
+ // precpu_stats
7833
+ // precpu_stats is a prior snapshot; its total must not exceed the current total.
7834
+ VERIFY_IS_LESS_THAN_OR_EQUAL(stats.precpu_stats.cpu_usage.total_usage, stats.cpu_stats.cpu_usage.total_usage);
7835
+ VERIFY_IS_LESS_THAN_OR_EQUAL(stats.precpu_stats.system_cpu_usage, stats.cpu_stats.system_cpu_usage);
7836
+
7837
+ // memory_stats
7838
+ // Limit is the VM memory ceiling — must be non-zero.
7839
+ VERIFY_IS_GREATER_THAN(stats.memory_stats.limit, 0ull);
7840
+
7841
+ // The sleep process occupies at least some memory.
7842
+ VERIFY_IS_GREATER_THAN(stats.memory_stats.usage, 0ull);
7843
+
7844
+ // Usage must never exceed the reported limit.
7845
+ VERIFY_IS_LESS_THAN_OR_EQUAL(stats.memory_stats.usage, stats.memory_stats.limit);
7846
+
7847
+ // pids_stats
7848
+ // At minimum the sleep process itself must be counted.
7849
+ VERIFY_IS_GREATER_THAN(stats.pids_stats.current, 0ull);
7850
+
7851
+ // networks
7852
+ // A bridged container always has at least one network interface.
7853
+ VERIFY_IS_TRUE(stats.networks.has_value());
7854
+ VERIFY_IS_FALSE(stats.networks->empty());
7855
+
7856
+ // Every interface entry must have consistent packet/byte counts
7857
+ // (bytes >= 0 is trivially true for unsigned, but packets imply bytes >= 0 too).
7858
+ for (const auto& [iface, net] : *stats.networks)
7859
+ {
7860
+ VERIFY_IS_FALSE(iface.empty());
7861
+
7862
+ // If packets were received/sent, the byte count must also be non-zero.
7863
+ if (net.rx_packets > 0)
7864
+ {
7865
+ VERIFY_IS_GREATER_THAN(net.rx_bytes, 0ull);
7866
+ }
7867
+ if (net.tx_packets > 0)
7868
+ {
7869
+ VERIFY_IS_GREATER_THAN(net.tx_bytes, 0ull);
7870
+ }
7871
+ }
7872
+
7873
+ // blkio_stats
7874
+ // io_service_bytes_recursive may be absent for a container with no disk I/O,
7875
+ // but if present every entry must have a non-empty operation name.
7876
+ if (stats.blkio_stats.io_service_bytes_recursive.has_value())
7877
+ {
7878
+ for (const auto& entry : *stats.blkio_stats.io_service_bytes_recursive)
7879
+ {
7880
+ VERIFY_IS_FALSE(entry.op.empty());
7881
+ }
7882
+ }
7883
+ }
7884
+
7885
+ WSLC_TEST_METHOD(ContainerStats_NullOutputPointer)
7886
+ {
7887
+ WSLCContainerLauncher launcher("debian:latest", "wslc-test-stats-null", {"sleep", "60"}, {}, WSLCContainerNetworkTypeBridged);
7888
+ auto runningContainer = launcher.Launch(*m_defaultSession, WSLCContainerStartFlagsNone);
7889
+ auto cleanup = wil::scope_exit([&]() { runningContainer.SetDeleteOnClose(true); });
7890
+
7891
+ wil::com_ptr<IWSLCContainer> container;
7892
+ VERIFY_SUCCEEDED(m_defaultSession->OpenContainer("wslc-test-stats-null", &container));
7893
+
7894
+ // Passing nullptr for Output must fail.
7895
+ VERIFY_FAILED(container->Stats(nullptr));
7896
+ }
7897
+
7898
+ WSLC_TEST_METHOD(ContainerStats_CreatedContainer_ReturnsZeroedStats)
7899
+ {
7900
+ // A created-but-not-started container returns zeroed stats from Docker rather than an error.
7901
+ WSLCContainerLauncher launcher("debian:latest", "wslc-test-stats-created", {}, {}, WSLCContainerNetworkTypeBridged);
7902
+ auto [result, runningContainer] = launcher.CreateNoThrow(*m_defaultSession);
7903
+ VERIFY_SUCCEEDED(result);
7904
+
7905
+ wil::com_ptr<IWSLCContainer> container;
7906
+ VERIFY_SUCCEEDED(m_defaultSession->OpenContainer("wslc-test-stats-created", &container));
7907
+ auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(container->Delete(WSLCDeleteFlagsForce)); });
7908
+
7909
+ wil::unique_cotaskmem_ansistring output;
7910
+ VERIFY_SUCCEEDED(container->Stats(&output));
7911
+ VERIFY_IS_NOT_NULL(output.get());
7912
+
7913
+ // A non-running container has no active processes.
7914
+ auto stats = wsl::shared::FromJson<wsl::windows::common::docker_schema::ContainerStats>(output.get());
7915
+ VERIFY_ARE_EQUAL(0ull, stats.pids_stats.current);
7916
+ }
7917
+
7918
WSLC_TEST_METHOD(InvalidNames)
7919
{
7920
auto expectInvalidArg = [&](const std::string& name) {