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
}