@samitouri / QOSAMI-WSL / commits / 39551c56

wslc: consolidate size formatting on FormatHumanReadableSize (#41429)

Consolidate size formatting into FormatHumanReadableSize Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

ggarzia-MSFT committed Aug 25, 2026 at 11:20 UTC 39551c56f59502031fe39ec2a8ee54a72ad39c85
7 files changed +91 -102
src/windows/common/string.cpp
+8 -27
@@ -375,45 +375,26 @@ std::optional<uint64_t> wsl::windows::common::string::ParseStorageSize(std::wstr
375 return static_cast<uint64_t>(bytes);
376 }
377
378 -std::wstring wsl::windows::common::string::FormatStorageSize(uint64_t Bytes, StorageSizeUnit Unit, uint32_t DecimalPlaces, bool IncludeSpace)
378 +std::wstring wsl::windows::common::string::FormatHumanReadableSize(uint64_t Bytes, uint32_t Precision, StorageSizeUnit Unit)
379 {
380 - constexpr size_t c_unitCount = 6;
381 - constexpr std::array<std::wstring_view, c_unitCount> c_decimalUnits{L"B", L"KB", L"MB", L"GB", L"TB", L"PB"};
382 - constexpr std::array<std::wstring_view, c_unitCount> c_binaryUnits{L"B", L"KiB", L"MiB", L"GiB", L"TiB", L"PiB"};
380 + constexpr size_t c_unitCount = 9;
381 + constexpr std::array<std::wstring_view, c_unitCount> c_decimalUnits{
382 + L"B", L"kB", L"MB", L"GB", L"TB", L"PB", L"EB", L"ZB", L"YB"};
383 + constexpr std::array<std::wstring_view, c_unitCount> c_binaryUnits{
384 + L"B", L"KiB", L"MiB", L"GiB", L"TiB", L"PiB", L"EiB", L"ZiB", L"YiB"};
385
386 const double base = Unit == StorageSizeUnit::Decimal ? 1000.0 : 1024.0;
387 const auto& units = Unit == StorageSizeUnit::Decimal ? c_decimalUnits : c_binaryUnits;
388
387 - double value = static_cast<double>(Bytes);
389 + auto value = static_cast<double>(Bytes);
390 size_t unitIndex = 0;
391 while (value >= base && unitIndex + 1 < c_unitCount)
392 {
393 value /= base;
392 - ++unitIndex;
393 - }
394 -
395 - const auto formattedValue = unitIndex == 0 ? std::to_wstring(Bytes) : std::format(L"{:.{}f}", value, DecimalPlaces);
396 - return std::format(L"{}{}{}", formattedValue, IncludeSpace ? L" " : L"", units[unitIndex]);
397 -}
398 -
399 -std::wstring wsl::windows::common::string::FormatBytes(uint64_t Bytes)
400 -{
401 - return FormatStorageSize(Bytes, StorageSizeUnit::Decimal, 2, true);
402 -}
403 -
404 -std::wstring wsl::windows::common::string::FormatHumanReadableSize(uint64_t Bytes, uint32_t Precision)
405 -{
406 - constexpr std::wstring_view c_units[] = {L"B", L"kB", L"MB", L"GB", L"TB", L"PB", L"EB", L"ZB", L"YB"};
407 -
408 - auto value = static_cast<double>(Bytes);
409 - size_t unitIndex = 0;
410 - while (value >= 1000.0 && unitIndex + 1 < std::size(c_units))
411 - {
412 - value /= 1000.0;
394 unitIndex++;
395 }
396
416 - return std::format(L"{:.{}g}{}", value, Precision, c_units[unitIndex]);
397 + return std::format(L"{:.{}g}{}", value, Precision, units[unitIndex]);
398 }
399
400 std::wstring wsl::windows::common::string::TruncateId(_In_ std::wstring_view id, bool shortenLength)
src/windows/common/string.hpp
+4 -7
@@ -31,13 +31,10 @@ enum class StorageSizeUnit
31
32 std::optional<uint64_t> ParseStorageSize(std::wstring_view String, StorageSizeUnit Unit);
33
34 -std::wstring FormatStorageSize(uint64_t Bytes, StorageSizeUnit Unit, uint32_t DecimalPlaces, bool IncludeSpace = false);
35 -
36 -std::wstring FormatBytes(uint64_t Bytes);
37 -
38 -// Formats a size as base 1000 with no space and the given number of significant digits
39 -// (119856765 -> "120MB" at precision 3, "119.9MB" at 4).
40 -std::wstring FormatHumanReadableSize(uint64_t Bytes, uint32_t Precision = 3);
34 +// Formats a size with the given number of significant digits and no space before the unit, matching
35 +// docker's go-units. Decimal uses base 1000 (kB, MB, GB) and Binary uses base 1024 (KiB, MiB, GiB).
36 +// 119856765 -> "120MB" at precision 3, "119.9MB" at precision 4.
37 +std::wstring FormatHumanReadableSize(uint64_t Bytes, uint32_t Precision = 3, StorageSizeUnit Unit = StorageSizeUnit::Decimal);
38
39 std::vector<std::string> InitializeStringSet(_In_count_(BufferSize) LPCSTR Buffer, _In_ SIZE_T BufferSize);
40
src/windows/wslc/services/ImageProgressCallback.cpp
+6 -4
@@ -20,7 +20,9 @@ Abstract:
20 namespace wsl::windows::wslc::services {
21 using namespace wsl::shared;
22 using namespace wsl::windows::common::vt;
23 -using wsl::windows::common::string::FormatBytes;
23 +using wsl::windows::common::string::FormatHumanReadableSize;
24 +
25 +constexpr uint32_t c_progressPrecision = 4;
26
27 auto ImageProgressCallback::MoveToLine(int line)
28 {
@@ -133,18 +135,18 @@ std::wstring ImageProgressCallback::GenerateStatusLine(LPCSTR status, LPCSTR id,
135
136 // Docker's reported total is an estimate of the compressed layer size, so the actual bytes
137 // transferred can exceed it. Drop the total in that case to avoid displaying a count over 100%.
136 - auto progress = FormatBytes(current);
138 + auto progress = FormatHumanReadableSize(current, c_progressPrecision);
139
140 if (current <= total)
141 {
140 - progress += std::format(L"/{}", FormatBytes(total));
142 + progress += std::format(L"/{}", FormatHumanReadableSize(total, c_progressPrecision));
143 }
144
145 line = std::format(L"{}: {} [{}] {}", safeId, safeStatus, bar, progress);
146 }
147 else if (current != 0)
148 {
147 - line = std::format(L"{}: {} {}", safeId, safeStatus, FormatBytes(current));
149 + line = std::format(L"{}: {} {}", safeId, safeStatus, FormatHumanReadableSize(current, c_progressPrecision));
150 }
151 else
152 {
src/windows/wslc/tasks/ContainerTasks.cpp
+22 -9
@@ -36,12 +36,25 @@ using namespace wsl::windows::common::wslutil;
36 using namespace wsl::windows::wslc::execution;
37 using namespace wsl::windows::wslc::models;
38 using namespace wsl::windows::wslc::services;
39 -using wsl::windows::common::string::FormatBytes;
40 -using wsl::windows::common::string::FormatStorageSize;
39 +using wsl::windows::common::string::FormatHumanReadableSize;
40 using wsl::windows::common::string::StorageSizeUnit;
41
42 namespace {
43
44 +// Docker reports memory in binary units and network and block IO in decimal units.
45 +constexpr uint32_t c_statsMemoryPrecision = 4;
46 +constexpr uint32_t c_statsIoPrecision = 3;
47 +
48 +std::string FormatStatsMemory(uint64_t Bytes)
49 +{
50 + return WideToMultiByte(FormatHumanReadableSize(Bytes, c_statsMemoryPrecision, StorageSizeUnit::Binary));
51 +}
52 +
53 +std::string FormatStatsIo(uint64_t Bytes)
54 +{
55 + return WideToMultiByte(FormatHumanReadableSize(Bytes, c_statsIoPrecision));
56 +}
57 +
58 nlohmann::json ComputeContainerStatsJson(const wsl::windows::common::docker_schema::ContainerStats& stats)
59 {
60 // Calculate CPU %
@@ -99,18 +112,15 @@ nlohmann::json ComputeContainerStatsJson(const wsl::windows::common::docker_sche
112 }
113
114 const auto& containerName = stats.name.empty() ? stats.id : stats.name;
102 - const auto formatBinaryBytes = [](uint64_t bytes) {
103 - return WideToMultiByte(FormatStorageSize(bytes, StorageSizeUnit::Binary, 2, true));
104 - };
115
116 return {
117 {"ID", stats.id},
118 {"Name", containerName},
119 {"CPUPerc", std::format("{:.2f}%", cpuPercent)},
110 - {"MemUsage", std::format("{} / {}", formatBinaryBytes(stats.memory_stats.usage), formatBinaryBytes(stats.memory_stats.limit))},
120 + {"MemUsage", std::format("{} / {}", FormatStatsMemory(stats.memory_stats.usage), FormatStatsMemory(stats.memory_stats.limit))},
121 {"MemPerc", std::format("{:.2f}%", memPercent)},
112 - {"NetIO", std::format("{} / {}", formatBinaryBytes(netRxBytes), formatBinaryBytes(netTxBytes))},
113 - {"BlockIO", std::format("{} / {}", formatBinaryBytes(blkReadBytes), formatBinaryBytes(blkWriteBytes))},
122 + {"NetIO", std::format("{} / {}", FormatStatsIo(netRxBytes), FormatStatsIo(netTxBytes))},
123 + {"BlockIO", std::format("{} / {}", FormatStatsIo(blkReadBytes), FormatStatsIo(blkWriteBytes))},
124 {"PIDs", stats.pids_stats.current},
125 };
126 }
@@ -119,6 +129,8 @@ nlohmann::json ComputeContainerStatsJson(const wsl::windows::common::docker_sche
129
130 namespace wsl::windows::wslc::task {
131
132 +constexpr uint32_t c_reclaimedSpacePrecision = 4;
133 +
134 static bool TryInspectContainer(Terminal& terminal, Session& session, const std::string& containerId, std::optional<wslc_schema::InspectContainer>& inspectData)
135 {
136 try
@@ -1069,6 +1081,7 @@ void PruneContainers(CLIExecutionContext& context)
1081 }
1082
1083 context.Terminal.Output(L"\n");
1072 - context.Terminal.Output(L"{}\n", Localization::WSLCCLI_ContainerPruneSpaceReclaimedBytes(FormatBytes(result.SpaceReclaimed)));
1084 + context.Terminal.Output(
1085 + L"{}\n", Localization::WSLCCLI_ContainerPruneSpaceReclaimedBytes(FormatHumanReadableSize(result.SpaceReclaimed, c_reclaimedSpacePrecision)));
1086 }
1087 } // namespace wsl::windows::wslc::task
src/windows/wslc/tasks/ImageTasks.cpp
+4 -2
@@ -34,10 +34,11 @@ using namespace wsl::windows::common::wslutil;
34 using namespace wsl::windows::wslc::execution;
35 using namespace wsl::windows::wslc::models;
36 using namespace wsl::windows::wslc::services;
37 -using wsl::windows::common::string::FormatBytes;
37
38 namespace wsl::windows::wslc::task {
39
40 +constexpr uint32_t c_reclaimedSpacePrecision = 4;
41 +
42 namespace {
43
44 class DECLSPEC_UUID("91EF98A7-99A8-41C2-893C-43CDFB7DB69F") WSLCImageLoadCallback
@@ -438,6 +439,7 @@ void PruneImages(CLIExecutionContext& context)
439 }
440
441 context.Terminal.Output(L"\n");
441 - context.Terminal.Output(L"{}\n", Localization::WSLCCLI_ImagePruneSpaceReclaimedBytes(FormatBytes(result.SpaceReclaimed)));
442 + context.Terminal.Output(
443 + L"{}\n", Localization::WSLCCLI_ImagePruneSpaceReclaimedBytes(FormatHumanReadableSize(result.SpaceReclaimed, c_reclaimedSpacePrecision)));
444 }
445 } // namespace wsl::windows::wslc::task
src/windows/wslcsession/WSLCSession.cpp
+4 -3
@@ -30,7 +30,7 @@ using io::MultiHandleWait;
30 using io::OverlappedIOHandle;
31 using io::WriteHandle;
32 using wsl::shared::Localization;
33 -using wsl::windows::common::string::FormatBytes;
33 +using wsl::windows::common::string::FormatHumanReadableSize;
34 using wsl::windows::service::wslc::UserCOMCallback;
35 using wsl::windows::service::wslc::UserHandle;
36 using wsl::windows::service::wslc::WSLCExecutionContext;
@@ -42,6 +42,7 @@ constexpr auto c_containerdSocket = "/run/containerd/containerd.sock";
42 constexpr auto c_storageVhdFilename = wsl::windows::wslc::DefaultStorageVhdName;
43 constexpr DWORD c_processTerminateTimeoutMs = 30 * 1000;
44 constexpr DWORD c_processKillTimeoutMs = 10 * 1000;
45 +constexpr uint32_t c_progressPrecision = 4;
46
47 // Default grace period to keep an otherwise-idle VM running before tearing it down (used when the
48 // session's IdleTimeoutSec setting is 0/unset). This avoids thrashing the VM (repeated
@@ -1432,8 +1433,8 @@ try
1433 {
1434 auto currentBytes = static_cast<ULONGLONG>(std::max<int64_t>(entry.current, 0));
1435 auto totalBytes = static_cast<ULONGLONG>(std::max<int64_t>(entry.total, 0));
1435 - auto current = FormatBytes(currentBytes);
1436 - auto total = FormatBytes(totalBytes);
1436 + auto current = FormatHumanReadableSize(currentBytes, c_progressPrecision);
1437 + auto total = FormatHumanReadableSize(totalBytes, c_progressPrecision);
1438 reportProgress(std::format("{}{} {} / {}", logPrefix(it->second), entry.id, current, total), entry.id.c_str(), currentBytes, totalBytes);
1439 }
1440 else if (reportedSteps.insert(entry.id).second)
test/windows/StringUnitTests.cpp
+43 -50
@@ -4,9 +4,7 @@
4 #include "Common.h"
5 #include "string.hpp"
6
7 -using wsl::windows::common::string::FormatBytes;
7 using wsl::windows::common::string::FormatHumanReadableSize;
9 -using wsl::windows::common::string::FormatStorageSize;
8 using wsl::windows::common::string::ParseStorageSize;
9 using wsl::windows::common::string::StorageSizeUnit;
10
@@ -16,8 +14,7 @@ struct StorageSizeFormatCase
14 {
15 uint64_t Bytes;
16 StorageSizeUnit Unit;
19 - uint32_t DecimalPlaces;
20 - bool IncludeSpace;
17 + uint32_t Precision;
18 std::wstring Expected;
19 };
20
@@ -25,8 +22,7 @@ struct StorageSizeTextRoundTripCase
22 {
23 std::wstring Text;
24 StorageSizeUnit Unit;
28 - uint32_t DecimalPlaces;
29 - bool IncludeSpace;
25 + uint32_t Precision;
26 };
27
28 void VerifyDockerStorageSize(const std::string& Input, StorageSizeUnit Unit, std::optional<uint64_t> Expected)
@@ -206,34 +202,29 @@ class StringUnitTests
202 }
203 }
204
209 - TEST_METHOD(FormatStorageSize_UsesRequestedPrecision)
205 + // Memory sizes are rendered in binary units with four significant digits and no space, matching
206 + // docker's units.BytesSize.
207 + TEST_METHOD(FormatHumanReadableSize_SupportsBinaryUnits)
208 {
209 const std::vector<StorageSizeFormatCase> TestCases{
212 - {0, StorageSizeUnit::Decimal, 0, false, L"0B"},
213 - {999, StorageSizeUnit::Decimal, 2, true, L"999 B"},
214 - {1'000, StorageSizeUnit::Decimal, 0, false, L"1KB"},
215 - {119'856'765, StorageSizeUnit::Decimal, 0, false, L"120MB"},
216 - {119'856'765, StorageSizeUnit::Decimal, 1, false, L"119.9MB"},
217 - {119'856'765, StorageSizeUnit::Decimal, 2, false, L"119.86MB"},
218 - {1'000'000'000'000ULL, StorageSizeUnit::Decimal, 2, false, L"1.00TB"},
219 - {1'000'000'000'000'000ULL, StorageSizeUnit::Decimal, 2, false, L"1.00PB"},
220 - {1'000'000'000'000'000'000ULL, StorageSizeUnit::Decimal, 2, false, L"1000.00PB"},
221 - {1'023, StorageSizeUnit::Binary, 2, true, L"1023 B"},
222 - {1'024, StorageSizeUnit::Binary, 0, false, L"1KiB"},
223 - {1'536, StorageSizeUnit::Binary, 1, false, L"1.5KiB"},
224 - {1'610'612'736, StorageSizeUnit::Binary, 0, false, L"2GiB"},
225 - {1'610'612'736, StorageSizeUnit::Binary, 1, false, L"1.5GiB"},
226 - {1ULL << 40, StorageSizeUnit::Binary, 2, false, L"1.00TiB"},
227 - {1ULL << 50, StorageSizeUnit::Binary, 2, false, L"1.00PiB"},
228 - {1ULL << 60, StorageSizeUnit::Binary, 2, false, L"1024.00PiB"},
210 + {0, StorageSizeUnit::Binary, 4, L"0B"},
211 + {1'023, StorageSizeUnit::Binary, 4, L"1023B"},
212 + {1'024, StorageSizeUnit::Binary, 4, L"1KiB"},
213 + {1'536, StorageSizeUnit::Binary, 4, L"1.5KiB"},
214 + {44'000, StorageSizeUnit::Binary, 4, L"42.97KiB"},
215 + {1'610'612'736, StorageSizeUnit::Binary, 4, L"1.5GiB"},
216 + {8ULL << 30, StorageSizeUnit::Binary, 4, L"8GiB"},
217 + {1ULL << 40, StorageSizeUnit::Binary, 4, L"1TiB"},
218 + {1ULL << 50, StorageSizeUnit::Binary, 4, L"1PiB"},
219 + {1'536, StorageSizeUnit::Binary, 3, L"1.5KiB"},
220 + {1'000, StorageSizeUnit::Decimal, 4, L"1kB"},
221 + {1'610'612'736, StorageSizeUnit::Decimal, 4, L"1.611GB"},
222 };
223
224 for (const auto& TestCase : TestCases)
225 {
233 - VERIFY_ARE_EQUAL(TestCase.Expected, FormatStorageSize(TestCase.Bytes, TestCase.Unit, TestCase.DecimalPlaces, TestCase.IncludeSpace));
226 + VERIFY_ARE_EQUAL(TestCase.Expected, FormatHumanReadableSize(TestCase.Bytes, TestCase.Precision, TestCase.Unit));
227 }
235 -
236 - VERIFY_ARE_EQUAL(std::wstring{L"119.86 MB"}, FormatBytes(119'856'765));
228 }
229
230 // Image sizes are rendered with three significant digits, base 1000, no space, and "kB" rather
@@ -278,24 +269,26 @@ class StringUnitTests
269
270 TEST_METHOD(StorageSize_BytesToTextRoundTrips)
271 {
281 - const auto VerifyRoundTrip = [](uint64_t Bytes, StorageSizeUnit Unit, uint32_t DecimalPlaces, bool IncludeSpace = false) {
282 - const auto text = FormatStorageSize(Bytes, Unit, DecimalPlaces, IncludeSpace);
272 + // The parser accepts suffixes up to peta, matching docker's unit map, so the round trip is
273 + // only defined below one exabyte.
274 + const auto VerifyRoundTrip = [](uint64_t Bytes, StorageSizeUnit Unit, uint32_t Precision) {
275 + const auto text = FormatHumanReadableSize(Bytes, Precision, Unit);
276 VERIFY_ARE_EQUAL(std::optional<uint64_t>{Bytes}, ParseStorageSize(text, Unit));
277 };
278
286 - VerifyRoundTrip(0, StorageSizeUnit::Decimal, 0);
287 - VerifyRoundTrip(32, StorageSizeUnit::Decimal, 0, true);
288 - VerifyRoundTrip(1'500, StorageSizeUnit::Decimal, 1);
289 - VerifyRoundTrip(1'536, StorageSizeUnit::Binary, 1);
290 - VerifyRoundTrip(1'000'000'000'000'000'000ULL, StorageSizeUnit::Decimal, 2);
291 - VerifyRoundTrip(1ULL << 60, StorageSizeUnit::Binary, 2);
279 + VerifyRoundTrip(0, StorageSizeUnit::Decimal, 3);
280 + VerifyRoundTrip(32, StorageSizeUnit::Decimal, 3);
281 + VerifyRoundTrip(1'500, StorageSizeUnit::Decimal, 3);
282 + VerifyRoundTrip(1'536, StorageSizeUnit::Binary, 3);
283 + VerifyRoundTrip(1'250'000'000'000ULL, StorageSizeUnit::Decimal, 4);
284 + VerifyRoundTrip(1ULL << 50, StorageSizeUnit::Binary, 4);
285
286 uint64_t decimalFactor = 1'000;
287 uint64_t binaryFactor = 1'024;
288 for (size_t index = 0; index < 5; ++index)
289 {
297 - VerifyRoundTrip(32 * decimalFactor, StorageSizeUnit::Decimal, 0);
298 - VerifyRoundTrip(32 * binaryFactor, StorageSizeUnit::Binary, 0, true);
290 + VerifyRoundTrip(32 * decimalFactor, StorageSizeUnit::Decimal, 3);
291 + VerifyRoundTrip(32 * binaryFactor, StorageSizeUnit::Binary, 3);
292 decimalFactor *= 1'000;
293 binaryFactor *= 1'024;
294 }
@@ -304,18 +297,18 @@ class StringUnitTests
297 TEST_METHOD(StorageSize_TextToBytesRoundTrips)
298 {
299 const std::vector<StorageSizeTextRoundTripCase> TestCases{
307 - {L"0B", StorageSizeUnit::Decimal, 0, false},
308 - {L"32 B", StorageSizeUnit::Decimal, 0, true},
309 - {L"32KB", StorageSizeUnit::Decimal, 0, false},
310 - {L"32.5MB", StorageSizeUnit::Decimal, 1, false},
311 - {L"1GB", StorageSizeUnit::Decimal, 0, false},
312 - {L"1.25TB", StorageSizeUnit::Decimal, 2, false},
313 - {L"1PB", StorageSizeUnit::Decimal, 0, false},
314 - {L"32KiB", StorageSizeUnit::Binary, 0, false},
315 - {L"1.5MiB", StorageSizeUnit::Binary, 1, false},
316 - {L"1GiB", StorageSizeUnit::Binary, 0, false},
317 - {L"1.25 TiB", StorageSizeUnit::Binary, 2, true},
318 - {L"1PiB", StorageSizeUnit::Binary, 0, false},
300 + {L"0B", StorageSizeUnit::Decimal, 3},
301 + {L"32B", StorageSizeUnit::Decimal, 3},
302 + {L"32kB", StorageSizeUnit::Decimal, 3},
303 + {L"32.5MB", StorageSizeUnit::Decimal, 3},
304 + {L"1GB", StorageSizeUnit::Decimal, 3},
305 + {L"1.25TB", StorageSizeUnit::Decimal, 3},
306 + {L"1PB", StorageSizeUnit::Decimal, 3},
307 + {L"32KiB", StorageSizeUnit::Binary, 3},
308 + {L"1.5MiB", StorageSizeUnit::Binary, 3},
309 + {L"1GiB", StorageSizeUnit::Binary, 3},
310 + {L"1.25TiB", StorageSizeUnit::Binary, 3},
311 + {L"1PiB", StorageSizeUnit::Binary, 3},
312 };
313
314 for (const auto& TestCase : TestCases)
@@ -323,7 +316,7 @@ class StringUnitTests
316 const auto bytes = ParseStorageSize(TestCase.Text, TestCase.Unit);
317 VERIFY_IS_TRUE(bytes.has_value());
318
326 - const auto text = FormatStorageSize(bytes.value(), TestCase.Unit, TestCase.DecimalPlaces, TestCase.IncludeSpace);
319 + const auto text = FormatHumanReadableSize(bytes.value(), TestCase.Precision, TestCase.Unit);
320 VERIFY_ARE_EQUAL(TestCase.Text, text);
321 VERIFY_ARE_EQUAL(bytes, ParseStorageSize(text, TestCase.Unit));
322 }