diff --git a/src/common/utils/__tests__/convert-memory.test.ts b/src/common/utils/__tests__/convert-memory.test.ts new file mode 100644 index 0000000000..5acc10e5ab --- /dev/null +++ b/src/common/utils/__tests__/convert-memory.test.ts @@ -0,0 +1,92 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { bytesToUnits, unitsToBytes } from "../convertMemory"; + +describe("unitsToBytes", () => { + it("without any units, just parse as float", () => { + expect(unitsToBytes("1234")).toBe(1234); + }); + + it("given unrelated data, return NaN", () => { + expect(unitsToBytes("I am not a number")).toBeNaN(); + }); + + it("given unrelated data, but has number, return that", () => { + expect(unitsToBytes("I am not a number, but this is 0.1")).toBe(0.1); + }); +}); + +describe("bytesToUnits", () => { + it("should return N/A for invalid bytes", () => { + expect(bytesToUnits(-1)).toBe("N/A"); + expect(bytesToUnits(Infinity)).toBe("N/A"); + expect(bytesToUnits(NaN)).toBe("N/A"); + }); + + it("given a number within the magnitude of 0..124, format with B", () => { + expect(bytesToUnits(100)).toBe("100.0B"); + }); + + it("given a number within the magnitude of 1024..1024^2, format with KiB", () => { + expect(bytesToUnits(1024)).toBe("1.0KiB"); + expect(bytesToUnits(2048)).toBe("2.0KiB"); + expect(bytesToUnits(1900)).toBe("1.9KiB"); + expect(bytesToUnits(50*1024 + 1)).toBe("50.0KiB"); + }); + + it("given a number within the magnitude of 1024^2..1024^3, format with MiB", () => { + expect(bytesToUnits(1024**2)).toBe("1.0MiB"); + expect(bytesToUnits(2048**2)).toBe("4.0MiB"); + expect(bytesToUnits(1900 * 1024)).toBe("1.9MiB"); + expect(bytesToUnits(50*(1024 ** 2) + 1)).toBe("50.0MiB"); + }); + + it("given a number within the magnitude of 1024^3..1024^4, format with GiB", () => { + expect(bytesToUnits(1024**3)).toBe("1.0GiB"); + expect(bytesToUnits(2048**3)).toBe("8.0GiB"); + expect(bytesToUnits(1900 * 1024 ** 2)).toBe("1.9GiB"); + expect(bytesToUnits(50*(1024 ** 3) + 1)).toBe("50.0GiB"); + }); + + it("given a number within the magnitude of 1024^4..1024^5, format with TiB", () => { + expect(bytesToUnits(1024**4)).toBe("1.0TiB"); + expect(bytesToUnits(2048**4)).toBe("16.0TiB"); + expect(bytesToUnits(1900 * 1024 ** 3)).toBe("1.9TiB"); + expect(bytesToUnits(50*(1024 ** 4) + 1)).toBe("50.0TiB"); + }); + + it("given a number within the magnitude of 1024^5..1024^6, format with PiB", () => { + expect(bytesToUnits(1024**5)).toBe("1.0PiB"); + expect(bytesToUnits(2048**5)).toBe("32.0PiB"); + expect(bytesToUnits(1900 * 1024 ** 4)).toBe("1.9PiB"); + expect(bytesToUnits(50*(1024 ** 5) + 1)).toBe("50.0PiB"); + }); + + it("given a number within the magnitude of 1024^6.., format with EiB", () => { + expect(bytesToUnits(1024**6)).toBe("1.0EiB"); + expect(bytesToUnits(2048**6)).toBe("64.0EiB"); + expect(bytesToUnits(1900 * 1024 ** 5)).toBe("1.9EiB"); + expect(bytesToUnits(50*(1024 ** 6) + 1)).toBe("50.0EiB"); + expect(bytesToUnits(1024**8)).toBe("1048576.0EiB"); + }); +}); + +describe("bytesToUnits -> unitsToBytes", () => { + it("given an input, round trip to the same value, given enough precision", () => { + expect(unitsToBytes(bytesToUnits(123))).toBe(123); + expect(unitsToBytes(bytesToUnits(1024**0 + 1, { precision: 2 }))).toBe(1024**0 + 1); + expect(unitsToBytes(bytesToUnits(1024**1 + 2, { precision: 3 }))).toBe(1024**1 + 2); + expect(unitsToBytes(bytesToUnits(1024**2 + 3, { precision: 6 }))).toBe(1024**2 + 3); + expect(unitsToBytes(bytesToUnits(1024**3 + 4, { precision: 9 }))).toBe(1024**3 + 4); + expect(unitsToBytes(bytesToUnits(1024**4 + 5, { precision: 12 }))).toBe(1024**4 + 5); + expect(unitsToBytes(bytesToUnits(1024**5 + 6, { precision: 16 }))).toBe(1024**5 + 6); + expect(unitsToBytes(bytesToUnits(1024**6 + 7, { precision: 20 }))).toBe(1024**6 + 7); + }); + + it("given an invalid input, round trips to NaN", () => { + expect(unitsToBytes(bytesToUnits(-1))).toBeNaN(); + }); +}); diff --git a/src/common/utils/convertMemory.ts b/src/common/utils/convertMemory.ts index 7fed4502b6..d06b33d64a 100644 --- a/src/common/utils/convertMemory.ts +++ b/src/common/utils/convertMemory.ts @@ -3,35 +3,59 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ +import assert from "assert"; +import * as iter from "./iter"; + // Helper to convert memory from units Ki, Mi, Gi, Ti, Pi to bytes and vise versa -const base = 1024; -const suffixes = ["K", "M", "G", "T", "P", "E"]; // Equivalents: Ki, Mi, Gi, Ti, Pi, Ei +const baseMagnitude = 1024; +const maxMagnitude = ["EiB", baseMagnitude ** 6] as const; +const magnitudes = new Map([ + ["B", 1] as const, + ["KiB", baseMagnitude ** 1] as const, + ["MiB", baseMagnitude ** 2] as const, + ["GiB", baseMagnitude ** 3] as const, + ["TiB", baseMagnitude ** 4] as const, + ["PiB", baseMagnitude ** 5] as const, + maxMagnitude, +]); +const unitRegex = /(?[0-9]+(\.[0-9]*)?)(?(B|[KMGTPE]iB))?/; -export function unitsToBytes(value: string) { - if (!suffixes.some(suffix => value.includes(suffix))) { - return parseFloat(value); +export function unitsToBytes(value: string): number { + const unitsMatch = value.match(unitRegex); + + if (!unitsMatch?.groups) { + return NaN; } - - const suffix = value.replace(/[0-9]|i|\./g, ""); - const index = suffixes.indexOf(suffix); - return parseInt( - (parseFloat(value) * Math.pow(base, index + 1)).toFixed(1), - ); + const parsedValue = parseFloat(unitsMatch.groups.value); + + if (!unitsMatch.groups?.suffix) { + return parsedValue; + } + + const magnitude = magnitudes.get(unitsMatch.groups.suffix as never); + + assert(magnitude, "UnitRegex is wrong some how"); + + return parseInt((parsedValue * magnitude).toFixed(1)); } -export function bytesToUnits(bytes: number, precision = 1) { - const sizes = ["B", ...suffixes]; - const index = Math.floor(Math.log(bytes) / Math.log(base)); +export interface BytesToUnitesOptions { + /** + * The number of decimal places. MUST be an integer. MUST be in the range [0, 20]. + * @default 1 + */ + precision?: number; +} - if (!bytes) { +export function bytesToUnits(bytes: number, { precision = 1 }: BytesToUnitesOptions = {}): string { + if (bytes <= 0 || isNaN(bytes) || !isFinite(bytes)) { return "N/A"; } - if (index === 0) { - return `${bytes}${sizes[index]}`; - } + const index = Math.floor(Math.log(bytes) / Math.log(baseMagnitude)); + const [suffix, magnitude] = iter.nth(magnitudes.entries(), index) ?? maxMagnitude; - return `${(bytes / (1024 ** index)).toFixed(precision)}${sizes[index]}i`; + return `${(bytes / magnitude).toFixed(precision)}${suffix}`; } diff --git a/src/common/utils/iter.ts b/src/common/utils/iter.ts index 9a26162d8f..e1a9f41724 100644 --- a/src/common/utils/iter.ts +++ b/src/common/utils/iter.ts @@ -171,6 +171,23 @@ export function join(src: Iterable, connector = ","): string { return reduce(src, (acc, cur) => `${acc}${connector}${cur}`, ""); } +/** + * Returns the next value after iterating over the iterable `index` times. + * + * For example: `nth(["a", "b"], 0)` will return `"a"` + * For example: `nth(["a", "b"], 1)` will return `"b"` + * For example: `nth(["a", "b"], 2)` will return `undefined` + */ +export function nth(src: Iterable, index: number): T | undefined { + const iteree = src[Symbol.iterator](); + + while (index-- > 0) { + iteree.next(); + } + + return iteree.next().value; +} + /** * Iterate through `src` and return `true` if `fn` returns a thruthy value for every yielded value. * Otherwise, return `false`. This function short circuits. diff --git a/src/renderer/components/+cluster/cluster-metrics.tsx b/src/renderer/components/+cluster/cluster-metrics.tsx index 4e964d4096..fd69842c41 100644 --- a/src/renderer/components/+cluster/cluster-metrics.tsx +++ b/src/renderer/components/+cluster/cluster-metrics.tsx @@ -73,7 +73,7 @@ const NonInjectedClusterMetrics = observer(({ clusterOverviewStore: { metricType label: ({ index }, data) => { const value = data.datasets[0].data[index] as ChartPoint; - return bytesToUnits(parseInt(value.y as string), 3); + return bytesToUnits(parseInt(value.y as string), { precision: 3 }); }, }, }, diff --git a/src/renderer/components/+nodes/route.tsx b/src/renderer/components/+nodes/route.tsx index 3d4e17cec5..2be66f7ec8 100644 --- a/src/renderer/components/+nodes/route.tsx +++ b/src/renderer/components/+nodes/route.tsx @@ -127,7 +127,7 @@ export class NodesRoute extends React.Component { metricNames: ["workloadMemoryUsage", "memoryAllocatableCapacity"], formatters: [ ([usage, capacity]) => `${(usage * 100 / capacity).toFixed(2)}%`, - ([usage]) => bytesToUnits(usage, 3), + ([usage]) => bytesToUnits(usage, { precision: 3 }), ], }); } @@ -139,7 +139,7 @@ export class NodesRoute extends React.Component { metricNames: ["fsUsage", "fsSize"], formatters: [ ([usage, capacity]) => `${(usage * 100 / capacity).toFixed(2)}%`, - ([usage]) => bytesToUnits(usage, 3), + ([usage]) => bytesToUnits(usage, { precision: 3 }), ], }); } diff --git a/src/renderer/components/+workloads-pods/pod-details-list.tsx b/src/renderer/components/+workloads-pods/pod-details-list.tsx index 12939a4a1f..6b12e44193 100644 --- a/src/renderer/components/+workloads-pods/pod-details-list.tsx +++ b/src/renderer/components/+workloads-pods/pod-details-list.tsx @@ -75,7 +75,7 @@ export class PodDetailsList extends React.Component { renderMemoryUsage(id: string, usage: number) { const { maxMemory } = this.props; const tooltip = ( -

Memory: {Math.ceil(usage * 100 / maxMemory)}%
{bytesToUnits(usage, 3)}

+

Memory: {Math.ceil(usage * 100 / maxMemory)}%
{bytesToUnits(usage, { precision: 3 })}

); if (!maxMemory) return usage ? bytesToUnits(usage) : 0; diff --git a/src/renderer/components/chart/bar-chart.tsx b/src/renderer/components/chart/bar-chart.tsx index e5bb95566b..c4f466468f 100644 --- a/src/renderer/components/chart/bar-chart.tsx +++ b/src/renderer/components/chart/bar-chart.tsx @@ -195,7 +195,7 @@ export const memoryOptions: ChartOptions = { const { label, data } = datasets[datasetIndex]; const value = data[index] as ChartPoint; - return `${label}: ${bytesToUnits(parseInt(value.y.toString()), 3)}`; + return `${label}: ${bytesToUnits(parseInt(value.y.toString()), { precision: 3 })}`; }, }, },