From 0c53c88367c2af8e56fc32d14afe19104d515b0d Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 30 Oct 2025 00:16:31 +0100 Subject: [PATCH] progress-bar: use dynamic size units --- src/libmain/progress-bar.cc | 67 +++++++++++++++++++++++++--- src/libutil-tests/util.cc | 53 ++++++++++++++++++++++ src/libutil/include/nix/util/util.hh | 36 +++++++++++++++ src/libutil/util.cc | 63 ++++++++++++++++++++++---- 4 files changed, 205 insertions(+), 14 deletions(-) diff --git a/src/libmain/progress-bar.cc b/src/libmain/progress-bar.cc index edec8460d..6cefae6be 100644 --- a/src/libmain/progress-bar.cc +++ b/src/libmain/progress-bar.cc @@ -467,8 +467,6 @@ public: std::string getStatus(State & state) { - auto MiB = 1024.0 * 1024.0; - std::string res; auto renderActivity = @@ -516,6 +514,65 @@ public: return s; }; + auto renderSizeActivity = [&](ActivityType type, const std::string & itemFmt = "%s") { + auto & act = state.activitiesByType[type]; + uint64_t done = act.done, expected = act.done, running = 0, failed = act.failed; + for (auto & j : act.its) { + done += j.second->done; + expected += j.second->expected; + running += j.second->running; + failed += j.second->failed; + } + + expected = std::max(expected, act.expected); + + std::optional commonUnit; + std::string s; + + if (running || done || expected || failed) { + if (running) + if (expected != 0) { + commonUnit = getCommonSizeUnit({(int64_t) running, (int64_t) done, (int64_t) expected}); + s = + fmt(ANSI_BLUE "%s" ANSI_NORMAL "/" ANSI_GREEN "%s" ANSI_NORMAL "/%s", + commonUnit ? renderSizeWithoutUnit(running, *commonUnit) : renderSize(running), + commonUnit ? renderSizeWithoutUnit(done, *commonUnit) : renderSize(done), + commonUnit ? renderSizeWithoutUnit(expected, *commonUnit) : renderSize(expected)); + } else { + commonUnit = getCommonSizeUnit({(int64_t) running, (int64_t) done}); + s = + fmt(ANSI_BLUE "%s" ANSI_NORMAL "/" ANSI_GREEN "%s" ANSI_NORMAL, + commonUnit ? renderSizeWithoutUnit(running, *commonUnit) : renderSize(running), + commonUnit ? renderSizeWithoutUnit(done, *commonUnit) : renderSize(done)); + } + else if (expected != done) + if (expected != 0) { + commonUnit = getCommonSizeUnit({(int64_t) done, (int64_t) expected}); + s = + fmt(ANSI_GREEN "%s" ANSI_NORMAL "/%s", + commonUnit ? renderSizeWithoutUnit(done, *commonUnit) : renderSize(done), + commonUnit ? renderSizeWithoutUnit(expected, *commonUnit) : renderSize(expected)); + } else { + commonUnit = getSizeUnit(done); + s = fmt(ANSI_GREEN "%s" ANSI_NORMAL, renderSizeWithoutUnit(done, *commonUnit)); + } + else { + commonUnit = getSizeUnit(done); + s = fmt(done ? ANSI_GREEN "%s" ANSI_NORMAL : "%s", renderSizeWithoutUnit(done, *commonUnit)); + } + + if (commonUnit) + s = fmt("%s %siB", s, getSizeUnitSuffix(*commonUnit)); + + s = fmt(itemFmt, s); + + if (failed) + s += fmt(" (" ANSI_RED "%s failed" ANSI_NORMAL ")", renderSize(failed)); + } + + return s; + }; + auto showActivity = [&](ActivityType type, const std::string & itemFmt, const std::string & numberFmt = "%d", double unit = 1) { auto s = renderActivity(type, itemFmt, numberFmt, unit); @@ -529,7 +586,7 @@ public: showActivity(actBuilds, "%s built"); auto s1 = renderActivity(actCopyPaths, "%s copied"); - auto s2 = renderActivity(actCopyPath, "%s MiB", "%.1f", MiB); + auto s2 = renderSizeActivity(actCopyPath); if (!s1.empty() || !s2.empty()) { if (!res.empty()) @@ -545,12 +602,12 @@ public: } } - showActivity(actFileTransfer, "%s MiB DL", "%.1f", MiB); + renderSizeActivity(actFileTransfer, "%s DL"); { auto s = renderActivity(actOptimiseStore, "%s paths optimised"); if (s != "") { - s += fmt(", %.1f MiB / %d inodes freed", state.bytesLinked / MiB, state.filesLinked); + s += fmt(", %s / %d inodes freed", renderSize(state.bytesLinked), state.filesLinked); if (!res.empty()) res += ", "; res += s; diff --git a/src/libutil-tests/util.cc b/src/libutil-tests/util.cc index 32114d9da..a299cd978 100644 --- a/src/libutil-tests/util.cc +++ b/src/libutil-tests/util.cc @@ -146,6 +146,59 @@ TEST(string2Int, trivialConversions) ASSERT_EQ(string2Int("-100"), -100); } +/* ---------------------------------------------------------------------------- + * getSizeUnit + * --------------------------------------------------------------------------*/ + +TEST(getSizeUnit, misc) +{ + ASSERT_EQ(getSizeUnit(0), SizeUnit::Base); + ASSERT_EQ(getSizeUnit(100), SizeUnit::Base); + ASSERT_EQ(getSizeUnit(100), SizeUnit::Base); + ASSERT_EQ(getSizeUnit(972), SizeUnit::Base); + ASSERT_EQ(getSizeUnit(973), SizeUnit::Base); // FIXME: should round down + ASSERT_EQ(getSizeUnit(1024), SizeUnit::Base); + ASSERT_EQ(getSizeUnit(-1024), SizeUnit::Base); + ASSERT_EQ(getSizeUnit(1024 * 1024), SizeUnit::Kilo); + ASSERT_EQ(getSizeUnit(1100 * 1024), SizeUnit::Mega); + ASSERT_EQ(getSizeUnit(2ULL * 1024 * 1024 * 1024), SizeUnit::Giga); + ASSERT_EQ(getSizeUnit(2100ULL * 1024 * 1024 * 1024), SizeUnit::Tera); +} + +/* ---------------------------------------------------------------------------- + * getCommonSizeUnit + * --------------------------------------------------------------------------*/ + +TEST(getCommonSizeUnit, misc) +{ + ASSERT_EQ(getCommonSizeUnit({0}), SizeUnit::Base); + ASSERT_EQ(getCommonSizeUnit({0, 100}), SizeUnit::Base); + ASSERT_EQ(getCommonSizeUnit({100, 0}), SizeUnit::Base); + ASSERT_EQ(getCommonSizeUnit({100, 1024 * 1024}), std::nullopt); + ASSERT_EQ(getCommonSizeUnit({1024 * 1024, 100}), std::nullopt); + ASSERT_EQ(getCommonSizeUnit({1024 * 1024, 1024 * 1024}), SizeUnit::Kilo); + ASSERT_EQ(getCommonSizeUnit({2100ULL * 1024 * 1024 * 1024, 2100ULL * 1024 * 1024 * 1024}), SizeUnit::Tera); +} + +/* ---------------------------------------------------------------------------- + * renderSizeWithoutUnit + * --------------------------------------------------------------------------*/ + +TEST(renderSizeWithoutUnit, misc) +{ + ASSERT_EQ(renderSizeWithoutUnit(0, SizeUnit::Base, true), " 0.0"); + ASSERT_EQ(renderSizeWithoutUnit(100, SizeUnit::Base, true), " 0.1"); + ASSERT_EQ(renderSizeWithoutUnit(100, SizeUnit::Base), "0.1"); + ASSERT_EQ(renderSizeWithoutUnit(972, SizeUnit::Base, true), " 0.9"); + ASSERT_EQ(renderSizeWithoutUnit(973, SizeUnit::Base, true), " 1.0"); // FIXME: should round down + ASSERT_EQ(renderSizeWithoutUnit(1024, SizeUnit::Base, true), " 1.0"); + ASSERT_EQ(renderSizeWithoutUnit(-1024, SizeUnit::Base, true), " -1.0"); + ASSERT_EQ(renderSizeWithoutUnit(1024 * 1024, SizeUnit::Kilo, true), "1024.0"); + ASSERT_EQ(renderSizeWithoutUnit(1100 * 1024, SizeUnit::Mega, true), " 1.1"); + ASSERT_EQ(renderSizeWithoutUnit(2ULL * 1024 * 1024 * 1024, SizeUnit::Giga, true), " 2.0"); + ASSERT_EQ(renderSizeWithoutUnit(2100ULL * 1024 * 1024 * 1024, SizeUnit::Tera, true), " 2.1"); +} + /* ---------------------------------------------------------------------------- * renderSize * --------------------------------------------------------------------------*/ diff --git a/src/libutil/include/nix/util/util.hh b/src/libutil/include/nix/util/util.hh index 1234937b4..ffec8f1a4 100644 --- a/src/libutil/include/nix/util/util.hh +++ b/src/libutil/include/nix/util/util.hh @@ -99,6 +99,42 @@ N string2IntWithUnitPrefix(std::string_view s) throw UsageError("'%s' is not an integer", s); } +// Base also uses 'K', because it should also displayed as KiB => 100 Bytes => 0.1 KiB +#define NIX_UTIL_SIZE_UNITS \ + NIX_UTIL_DEFINE_SIZE_UNIT(Base, 'K') \ + NIX_UTIL_DEFINE_SIZE_UNIT(Kilo, 'K') \ + NIX_UTIL_DEFINE_SIZE_UNIT(Mega, 'M') \ + NIX_UTIL_DEFINE_SIZE_UNIT(Giga, 'G') \ + NIX_UTIL_DEFINE_SIZE_UNIT(Tera, 'T') \ + NIX_UTIL_DEFINE_SIZE_UNIT(Peta, 'P') \ + NIX_UTIL_DEFINE_SIZE_UNIT(Exa, 'E') \ + NIX_UTIL_DEFINE_SIZE_UNIT(Zetta, 'Z') \ + NIX_UTIL_DEFINE_SIZE_UNIT(Yotta, 'Y') + +enum class SizeUnit { +#define NIX_UTIL_DEFINE_SIZE_UNIT(name, suffix) name, + NIX_UTIL_SIZE_UNITS +#undef NIX_UTIL_DEFINE_SIZE_UNIT +}; + +constexpr inline auto sizeUnits = std::to_array({ +#define NIX_UTIL_DEFINE_SIZE_UNIT(name, suffix) SizeUnit::name, + NIX_UTIL_SIZE_UNITS +#undef NIX_UTIL_DEFINE_SIZE_UNIT +}); + +SizeUnit getSizeUnit(int64_t value); + +/** + * Returns the unit if all values would be rendered using the same unit + * otherwise returns `std::nullopt`. + */ +std::optional getCommonSizeUnit(std::initializer_list values); + +std::string renderSizeWithoutUnit(int64_t value, SizeUnit unit, bool align = false); + +char getSizeUnitSuffix(SizeUnit unit); + /** * Pretty-print a byte value, e.g. 12433615056 is rendered as `11.6 * GiB`. If `align` is set, the number will be right-justified by diff --git a/src/libutil/util.cc b/src/libutil/util.cc index f14bc63ac..d75aa4d67 100644 --- a/src/libutil/util.cc +++ b/src/libutil/util.cc @@ -132,17 +132,62 @@ std::optional string2Float(const std::string_view s) template std::optional string2Float(const std::string_view s); template std::optional string2Float(const std::string_view s); +static const int64_t conversionNumber = 1024; + +SizeUnit getSizeUnit(int64_t value) +{ + auto unit = sizeUnits.begin(); + uint64_t absValue = std::abs(value); + while (absValue > conversionNumber && unit < sizeUnits.end()) { + unit++; + absValue /= conversionNumber; + } + return *unit; +} + +std::optional getCommonSizeUnit(std::initializer_list values) +{ + assert(values.size() > 0); + + auto it = values.begin(); + SizeUnit unit = getSizeUnit(*it); + it++; + + for (; it != values.end(); it++) { + if (unit != getSizeUnit(*it)) { + return std::nullopt; + } + } + + return unit; +} + +std::string renderSizeWithoutUnit(int64_t value, SizeUnit unit, bool align) +{ + // bytes should also displayed as KiB => 100 Bytes => 0.1 KiB + auto power = std::max>(1, std::to_underlying(unit)); + double denominator = std::pow(conversionNumber, power); + double result = (double) value / denominator; + return fmt(align ? "%6.1f" : "%.1f", result); +} + +char getSizeUnitSuffix(SizeUnit unit) +{ + switch (unit) { +#define NIX_UTIL_DEFINE_SIZE_UNIT(name, suffix) \ + case SizeUnit::name: \ + return suffix; + NIX_UTIL_SIZE_UNITS +#undef NIX_UTIL_DEFINE_SIZE_UNIT + } + + assert(false); +} + std::string renderSize(int64_t value, bool align) { - static const std::array prefixes{{'K', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'}}; - size_t power = 0; - double abs_value = std::abs(value); - while (abs_value > 1024 && power < prefixes.size()) { - ++power; - abs_value /= 1024; - } - double res = (double) value / std::pow(1024.0, power); - return fmt(align ? "%6.1f %ciB" : "%.1f %ciB", power == 0 ? res / 1024 : res, prefixes.at(power)); + SizeUnit unit = getSizeUnit(value); + return fmt("%s %ciB", renderSizeWithoutUnit(value, unit, align), getSizeUnitSuffix(unit)); } bool hasPrefix(std::string_view s, std::string_view prefix)