From 72115f83fe2da972068d82815baace8c63e2fab1 Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Tue, 17 Mar 2026 07:25:39 -0400 Subject: [PATCH 1/4] Use correct binary unit prefixes (KiB/MiB/GiB) instead of SI (KB/MB/GB) Both nice_size() in Python and bytesToString() in TypeScript divide by 1024 (binary) but were labeling with SI prefixes. Since the values are binary, the correct IEC labels are KiB, MiB, GiB, TiB. --- client/src/utils/utils.ts | 8 ++++---- lib/galaxy/util/__init__.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/client/src/utils/utils.ts b/client/src/utils/utils.ts index 3a02cd41a47..189e60c4715 100644 --- a/client/src/utils/utils.ts +++ b/client/src/utils/utils.ts @@ -218,16 +218,16 @@ export function bytesToString(size: number, normalFont = true, numPlaces = 1) { if (size >= tb) { size = size / tb; - unit = "TB"; + unit = "TiB"; } else if (size >= gb) { size = size / gb; - unit = "GB"; + unit = "GiB"; } else if (size >= mb) { size = size / mb; - unit = "MB"; + unit = "MiB"; } else if (size >= kb) { size = size / kb; - unit = "KB"; + unit = "KiB"; } else if (size > 0) { unit = "b"; } else { diff --git a/lib/galaxy/util/__init__.py b/lib/galaxy/util/__init__.py index e133f8cba3f..5611227e028 100644 --- a/lib/galaxy/util/__init__.py +++ b/lib/galaxy/util/__init__.py @@ -1558,11 +1558,11 @@ def nice_size(size: Union[float, int, str, Decimal]) -> str: >>> nice_size(100) '100 bytes' >>> nice_size(10000) - '9.8 KB' + '9.8 KiB' >>> nice_size(1000000) - '976.6 KB' + '976.6 KiB' >>> nice_size(100000000) - '95.4 MB' + '95.4 MiB' """ try: size = float(size) @@ -1572,7 +1572,7 @@ def nice_size(size: Union[float, int, str, Decimal]) -> str: if prefix == "": return f"{int(size)} bytes" else: - return f"{size:.1f} {prefix}B" + return f"{size:.1f} {prefix}iB" def size_to_bytes(size): From bd0cb7b66e3fe550502f48c9e519c0e985d885e2 Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Tue, 17 Mar 2026 19:58:55 -0400 Subject: [PATCH 2/4] Use SI units (KB/MB/GB) with 1000-based math instead of binary (KiB/MiB/GiB) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch nice_size(), bytesToString(), and size_to_bytes() from dividing/multiplying by 1024 to 1000. Users expect familiar SI labels and values matching what they see in macOS Finder, cloud storage, etc. — not IEC binary units. --- client/src/utils/utils.ts | 10 +++++----- lib/galaxy/util/__init__.py | 30 +++++++++++++++--------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/client/src/utils/utils.ts b/client/src/utils/utils.ts index 189e60c4715..0ecbd9ae596 100644 --- a/client/src/utils/utils.ts +++ b/client/src/utils/utils.ts @@ -197,7 +197,7 @@ export function roundToDecimalPlaces(number: number, numPlaces: number) { return parseFloat(number.toFixed(numPlaces)); } -const kb = 1024; +const kb = 1000; const mb = kb * kb; const gb = mb * kb; const tb = gb * kb; @@ -218,16 +218,16 @@ export function bytesToString(size: number, normalFont = true, numPlaces = 1) { if (size >= tb) { size = size / tb; - unit = "TiB"; + unit = "TB"; } else if (size >= gb) { size = size / gb; - unit = "GiB"; + unit = "GB"; } else if (size >= mb) { size = size / mb; - unit = "MiB"; + unit = "MB"; } else if (size >= kb) { size = size / kb; - unit = "KiB"; + unit = "KB"; } else if (size > 0) { unit = "b"; } else { diff --git a/lib/galaxy/util/__init__.py b/lib/galaxy/util/__init__.py index 5611227e028..e936a1cbcfb 100644 --- a/lib/galaxy/util/__init__.py +++ b/lib/galaxy/util/__init__.py @@ -1558,21 +1558,21 @@ def nice_size(size: Union[float, int, str, Decimal]) -> str: >>> nice_size(100) '100 bytes' >>> nice_size(10000) - '9.8 KiB' + '10.0 KB' >>> nice_size(1000000) - '976.6 KiB' + '1.0 MB' >>> nice_size(100000000) - '95.4 MiB' + '100.0 MB' """ try: size = float(size) except ValueError: return "??? bytes" - size, prefix = metric_prefix(size, 1024) + size, prefix = metric_prefix(size, 1000) if prefix == "": return f"{int(size)} bytes" else: - return f"{size:.1f} {prefix}iB" + return f"{size:.1f} {prefix}B" def size_to_bytes(size): @@ -1586,15 +1586,15 @@ def size_to_bytes(size): >>> size_to_bytes('10 bytes') 10 >>> size_to_bytes('4k') - 4096 + 4000 >>> size_to_bytes('2.2 TB') - 2418925581107 + 2200000000000 >>> size_to_bytes('.01 TB') - 10995116277 + 10000000000 >>> size_to_bytes('1.b') 1 >>> size_to_bytes('1.2E2k') - 122880 + 120000 """ # The following number regexp is based on https://stackoverflow.com/questions/385558/extract-float-double-value/385597#385597 size_re = re.compile(r"(?P(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?)\s*(?P[eptgmk]?(b|bytes?)?)?$") @@ -1606,17 +1606,17 @@ def size_to_bytes(size): if multiple == "" or multiple.startswith("b"): return int(number) elif multiple.startswith("k"): - return int(number * 1024) + return int(number * 1000) elif multiple.startswith("m"): - return int(number * 1024**2) + return int(number * 1000**2) elif multiple.startswith("g"): - return int(number * 1024**3) + return int(number * 1000**3) elif multiple.startswith("t"): - return int(number * 1024**4) + return int(number * 1000**4) elif multiple.startswith("p"): - return int(number * 1024**5) + return int(number * 1000**5) elif multiple.startswith("e"): - return int(number * 1024**6) + return int(number * 1000**6) else: raise ValueError(f"Unknown multiplier '{multiple}' in '{size}'") From 3aba09b28e854bd8624f570fb9cc11724ed654f4 Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Wed, 18 Mar 2026 13:09:36 -0400 Subject: [PATCH 3/4] Fix client test data to use SI-round values after 1000-based math switch Test data used multiples of 1024 (7168, 21504, 1048576, etc.) which produced non-round numbers when divided by 1000. Switched to multiples of 1000 so the formatted output stays clean. --- client/src/components/Masthead/QuotaMeter.test.ts | 4 ++-- .../User/DiskUsage/DiskUsageSummary.test.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/client/src/components/Masthead/QuotaMeter.test.ts b/client/src/components/Masthead/QuotaMeter.test.ts index cd34871345e..2b165841e34 100644 --- a/client/src/components/Masthead/QuotaMeter.test.ts +++ b/client/src/components/Masthead/QuotaMeter.test.ts @@ -64,13 +64,13 @@ describe("QuotaMeter.vue", () => { it("shows total usage when there is no quota", async () => { { - const user = { ...FAKE_USER, total_disk_usage: 7168 }; + const user = { ...FAKE_USER, total_disk_usage: 7000 }; const config = { enable_quotas: false }; const wrapper = await createQuotaMeterWrapper(config, user); expect(wrapper.find("span").text()).toBe("Using 7 KB"); } { - const user = { ...FAKE_USER, total_disk_usage: 21504, quota: "unlimited" }; + const user = { ...FAKE_USER, total_disk_usage: 21000, quota: "unlimited" }; const config = { enable_quotas: true }; const wrapper = await createQuotaMeterWrapper(config, user); expect(wrapper.find("span").text()).toBe("Using 21 KB"); diff --git a/client/src/components/User/DiskUsage/DiskUsageSummary.test.ts b/client/src/components/User/DiskUsage/DiskUsageSummary.test.ts index d232ff0e1bf..cb773911186 100644 --- a/client/src/components/User/DiskUsage/DiskUsageSummary.test.ts +++ b/client/src/components/User/DiskUsage/DiskUsageSummary.test.ts @@ -20,16 +20,16 @@ const quotaUsageClassSelector = ".quota-usage"; const basicDiskUsageSummaryId = "#basic-disk-usage-summary"; const fakeUserWithQuota = getFakeRegisteredUser({ - total_disk_usage: 1048576, - quota_bytes: 104857600, + total_disk_usage: 1000000, + quota_bytes: 100000000, quota_percent: 1, }); const fakeQuotaUsages: UserQuotaUsageData[] = [ { quota_source_label: "Default", - quota_bytes: 104857600, - total_disk_usage: 1048576, + quota_bytes: 100000000, + total_disk_usage: 1000000, }, ]; @@ -92,8 +92,8 @@ describe("DiskUsageSummary.vue", () => { const updatedFakeQuotaUsages: UserQuotaUsageData[] = [ { quota_source_label: "Default", - quota_bytes: 104857600, - total_disk_usage: 2097152, + quota_bytes: 100000000, + total_disk_usage: 2000000, }, ]; server.use( From 2cdb78d2cd8f2f0d90d823da4e185d3c0e79de72 Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Wed, 18 Mar 2026 13:09:43 -0400 Subject: [PATCH 4/4] Add binary mode to nice_size/size_to_bytes for memory contexts We can keep or drop this, but this adapts nice_size and size_to_bytes with a binary=True option that uses 1024-based math and KiB/MiB/GiB labels. Wired it up in the memory-related callsites (cgroup, meminfo, univa runner) where binary units are more appropriate than SI. --- .../job_metrics/instrumenters/cgroup.py | 4 +- .../job_metrics/instrumenters/meminfo.py | 2 +- lib/galaxy/jobs/runners/univa.py | 4 +- lib/galaxy/util/__init__.py | 40 +++++++++++++------ test/unit/job_metrics/test_job_metrics.py | 4 +- 5 files changed, 35 insertions(+), 19 deletions(-) diff --git a/lib/galaxy/job_metrics/instrumenters/cgroup.py b/lib/galaxy/job_metrics/instrumenters/cgroup.py index 4f1796c9da8..a391686a6ba 100644 --- a/lib/galaxy/job_metrics/instrumenters/cgroup.py +++ b/lib/galaxy/job_metrics/instrumenters/cgroup.py @@ -68,7 +68,7 @@ TITLES = { CONVERSION = { "memory.oom_control.oom_kill_disable": lambda x: "No" if x == 1 else "Yes", "memory.oom_control.under_oom": lambda x: "Yes" if x == 1 else "No", - "memory.peak": lambda x: nice_size(x), + "memory.peak": lambda x: nice_size(x, binary=True), "cpuacct.usage": lambda x: formatting.seconds_to_str(x / 10**9), # convert nanoseconds "cpu.stat.system_usec": lambda x: formatting.seconds_to_str(x / 10**6), # convert microseconds "cpu.stat.usage_usec": lambda x: formatting.seconds_to_str(x / 10**6), # convert microseconds @@ -114,7 +114,7 @@ class CgroupPluginFormatter(formatting.JobMetricFormatter): return formatting.FormattedMetric(title, CONVERSION[key](value)) elif key.endswith("_bytes"): try: - return formatting.FormattedMetric(title, nice_size(value)) + return formatting.FormattedMetric(title, nice_size(value, binary=True)) except ValueError: pass else: diff --git a/lib/galaxy/job_metrics/instrumenters/meminfo.py b/lib/galaxy/job_metrics/instrumenters/meminfo.py index 9e1d9be1de2..c9d6998f008 100644 --- a/lib/galaxy/job_metrics/instrumenters/meminfo.py +++ b/lib/galaxy/job_metrics/instrumenters/meminfo.py @@ -17,7 +17,7 @@ MEMINFO_TITLES = {"memtotal": "Total System Memory", "swaptotal": "Total System class MemInfoFormatter(formatting.JobMetricFormatter): def format(self, key: str, value: Any) -> formatting.FormattedMetric: title = MEMINFO_TITLES.get(key, key) - return formatting.FormattedMetric(title, util.nice_size(value * 1000)) # kB = *1000, KB = *1024 - wikipedia + return formatting.FormattedMetric(title, util.nice_size(value * 1024, binary=True)) class MemInfoPlugin(InstrumentPlugin): diff --git a/lib/galaxy/jobs/runners/univa.py b/lib/galaxy/jobs/runners/univa.py index 920083266c3..54b7c4556fc 100644 --- a/lib/galaxy/jobs/runners/univa.py +++ b/lib/galaxy/jobs/runners/univa.py @@ -277,7 +277,7 @@ class UnivaJobRunner(DRMAAJobRunner): # qdel 100 137 user@mail extinfo["time_wasted"] = _parse_time(qacct["wallclock"]) - extinfo["memory_wasted"] = size_to_bytes(qacct["maxvmem"]) + extinfo["memory_wasted"] = size_to_bytes(qacct["maxvmem"], binary=True) extinfo["slots"] = int(qacct["slots"]) # deleted_by @@ -608,7 +608,7 @@ def _parse_native_specs(job_id, native_spec): # parse memory m = re.search(r"mem=([\d.]+[KGMT]?)[\s,]*", native_spec) if m is not None: - mem = size_to_bytes(m.group(1)) + mem = size_to_bytes(m.group(1), binary=True) # mem = _parse_mem(m.group(1)) if mem is None: log.error(f"DRMAAUniva: job {job_id} has unparsable memory native spec {native_spec}") diff --git a/lib/galaxy/util/__init__.py b/lib/galaxy/util/__init__.py index e936a1cbcfb..a8d6d37de61 100644 --- a/lib/galaxy/util/__init__.py +++ b/lib/galaxy/util/__init__.py @@ -1551,7 +1551,7 @@ def shorten_with_metric_prefix(amount: int) -> str: return str(amount) -def nice_size(size: Union[float, int, str, Decimal]) -> str: +def nice_size(size: Union[float, int, str, Decimal], binary: bool = False) -> str: """ Returns a readably formatted string with the size @@ -1563,19 +1563,30 @@ def nice_size(size: Union[float, int, str, Decimal]) -> str: '1.0 MB' >>> nice_size(100000000) '100.0 MB' + >>> nice_size(1024, binary=True) + '1.0 KiB' + >>> nice_size(1048576, binary=True) + '1.0 MiB' """ try: size = float(size) except ValueError: return "??? bytes" - size, prefix = metric_prefix(size, 1000) - if prefix == "": - return f"{int(size)} bytes" + if binary: + size, prefix = metric_prefix(size, 1024) + if prefix == "": + return f"{int(size)} bytes" + else: + return f"{size:.1f} {prefix}iB" else: - return f"{size:.1f} {prefix}B" + size, prefix = metric_prefix(size, 1000) + if prefix == "": + return f"{int(size)} bytes" + else: + return f"{size:.1f} {prefix}B" -def size_to_bytes(size): +def size_to_bytes(size, binary: bool = False): """ Returns a number of bytes (as integer) if given a reasonably formatted string with the size @@ -1595,7 +1606,12 @@ def size_to_bytes(size): 1 >>> size_to_bytes('1.2E2k') 120000 + >>> size_to_bytes('4k', binary=True) + 4096 + >>> size_to_bytes('1 MB', binary=True) + 1048576 """ + base = 1024 if binary else 1000 # The following number regexp is based on https://stackoverflow.com/questions/385558/extract-float-double-value/385597#385597 size_re = re.compile(r"(?P(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?)\s*(?P[eptgmk]?(b|bytes?)?)?$") size_match = size_re.match(size.lower()) @@ -1606,17 +1622,17 @@ def size_to_bytes(size): if multiple == "" or multiple.startswith("b"): return int(number) elif multiple.startswith("k"): - return int(number * 1000) + return int(number * base) elif multiple.startswith("m"): - return int(number * 1000**2) + return int(number * base**2) elif multiple.startswith("g"): - return int(number * 1000**3) + return int(number * base**3) elif multiple.startswith("t"): - return int(number * 1000**4) + return int(number * base**4) elif multiple.startswith("p"): - return int(number * 1000**5) + return int(number * base**5) elif multiple.startswith("e"): - return int(number * 1000**6) + return int(number * base**6) else: raise ValueError(f"Unknown multiplier '{multiple}' in '{size}'") diff --git a/test/unit/job_metrics/test_job_metrics.py b/test/unit/job_metrics/test_job_metrics.py index a80df5e9b41..bb9acb053b8 100644 --- a/test/unit/job_metrics/test_job_metrics.py +++ b/test/unit/job_metrics/test_job_metrics.py @@ -47,7 +47,7 @@ def test_job_metrics_format_cgroup(): "memory.limit_in_bytes", 9223372036854771712, assert_title="Memory limit on cgroup (MEM)", - assert_value="8.0 EB", + assert_value="8.0 EiB", ) _assert_format( "cgroup", @@ -61,7 +61,7 @@ def test_job_metrics_format_cgroup(): "memory.peak", 45097156608, assert_title="Max memory usage recorded", - assert_value="42.0 GB", + assert_value="42.0 GiB", )