[YUNIKORN-2140] Improve presentation of resources (#146)
Format resources with units instead of simple integers.
CPU: m, (bare), k, M, G, T, P, E (powers of 10, no units)
Memory: B, KiB, MiB, GiB, TiB, PiB, EiB (powers of 2, byte units)
Ephemeral storage: B, KB, MB, GB, TB, PB, EB (powers of 10, byte units)
All others: (bare), k, M, G, T, P, E (powers of 10, no units)
Replace n/a with 0 for values that are not given. Use n/a if the
resource is not set at all.
Closes: #146
Signed-off-by: Wilfred Spiegelenburg <wilfreds@apache.org>
diff --git a/src/app/services/scheduler/scheduler.service.spec.ts b/src/app/services/scheduler/scheduler.service.spec.ts
index 3e10742..2a73c3a 100644
--- a/src/app/services/scheduler/scheduler.service.spec.ts
+++ b/src/app/services/scheduler/scheduler.service.spec.ts
@@ -20,25 +20,129 @@
import {TestBed} from '@angular/core/testing';
import {EnvconfigService} from '@app/services/envconfig/envconfig.service';
import {MockEnvconfigService} from '@app/testing/mocks';
-import {configureTestSuite} from 'ng-bullet';
import {SchedulerService} from './scheduler.service';
+import {SchedulerResourceInfo} from '@app/models/resource-info.model';
+import {NOT_AVAILABLE} from '@app/utils/constants';
describe('SchedulerService', () => {
let service: SchedulerService;
- configureTestSuite(() => {
+ beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [SchedulerService, { provide: EnvconfigService, useValue: MockEnvconfigService }],
});
- });
-
- beforeEach(() => {
service = TestBed.inject(SchedulerService);
});
it('should create the service', () => {
expect(service).toBeTruthy();
+
+ });
+
+ it('should format SchedulerResourceInfo correctly', () => {
+ type TestCase = {
+ description: string;
+ schedulerResourceInfo: SchedulerResourceInfo;
+ expected: string;
+ };
+
+ const testCases: TestCase[] = [
+ {
+ description: 'test simple resourceInfo',
+ schedulerResourceInfo: {
+ 'memory': 1024,
+ 'vcore': 2,
+ },
+ expected: 'Memory: 1 KiB, CPU: 2m'
+ },
+ {
+ description: 'test undefined resourceInfo',
+ schedulerResourceInfo : undefined as any,
+ expected: `${NOT_AVAILABLE}`
+ },
+ {
+ description: 'test empty resourceInfo',
+ schedulerResourceInfo : {} as any,
+ expected: `${NOT_AVAILABLE}`
+ },
+ {
+ description: 'Test zero values',
+ schedulerResourceInfo: {
+ 'memory': 0,
+ 'vcore': 0,
+ 'ephemeral-storage': 0,
+ 'hugepages-2Mi': 0,
+ 'hugepages-1Gi': 0,
+ 'pods': 0
+ },
+ expected: 'Memory: 0 B, CPU: 0, pods: 0, ephemeral-storage: 0 B, hugepages-1Gi: 0 B, hugepages-2Mi: 0 B'
+ },
+ {
+ description: 'Test resource ordering',
+ schedulerResourceInfo: {
+ 'ephemeral-storage': 2048,
+ 'memory': 1024,
+ 'vcore': 2,
+ 'TPU': 30000,
+ 'GPU': 40000,
+ 'hugepages-2Mi':2097152,
+ 'hugepages-1Gi':1073741824,
+ 'pods': 10000
+ },
+ expected: 'Memory: 1 KiB, CPU: 2m, pods: 10k, ephemeral-storage: 2.05 kB, GPU: 40k, hugepages-1Gi: 1 GiB, hugepages-2Mi: 2 MiB, TPU: 30k'
+ }
+ ];
+
+ testCases.forEach((testCase: TestCase) => {
+ const result = (service as any).formatResource(testCase.schedulerResourceInfo); // ignore type typecheck to access private method
+ expect(result).toEqual(testCase.expected);
+ });
+ });
+
+
+ it('should format SchedulerResourceInfo percentage correctly', () => {
+ type TestCase = {
+ description: string;
+ schedulerResourceInfo: SchedulerResourceInfo;
+ expected: string;
+ };
+
+ const testCases: TestCase[] = [
+ {
+ description: 'test simple resourceInfo',
+ schedulerResourceInfo: {
+ 'memory': 10,
+ 'vcore': 50,
+ },
+ expected: 'Memory: 10%, CPU: 50%'
+ },
+ {
+ description: 'test undefined resourceInfo',
+ schedulerResourceInfo : undefined as any,
+ expected: `${NOT_AVAILABLE}`
+ },
+ {
+ description: 'test empty resourceInfo',
+ schedulerResourceInfo : {} as any,
+ expected: `${NOT_AVAILABLE}`
+ },
+ {
+ description: 'Test zero values and will only show memory and cpu',
+ schedulerResourceInfo: {
+ 'memory': 0,
+ 'vcore': 0,
+ 'pods': 0,
+ 'ephemeral-storage': 0,
+ },
+ expected: 'Memory: 0%, CPU: 0%'
+ }
+ ];
+
+ testCases.forEach((testCase: TestCase) => {
+ const result = (service as any).formatPercent(testCase.schedulerResourceInfo); // ignore type typecheck to access private method
+ expect(result).toEqual(testCase.expected);
+ });
});
});
diff --git a/src/app/services/scheduler/scheduler.service.ts b/src/app/services/scheduler/scheduler.service.ts
index 3d14f91..044ea3b 100644
--- a/src/app/services/scheduler/scheduler.service.ts
+++ b/src/app/services/scheduler/scheduler.service.ts
@@ -325,65 +325,76 @@
}
private formatResource(resource: SchedulerResourceInfo): string {
- const formatted = [];
+ const formatted: string[] = [];
+ if (resource) {
+ // Object.keys() didn't guarantee the order of keys, sort it before iterate.
+ Object.keys(resource).sort(this.resourcesCompareFn).forEach((key) => {
+ let value = resource[key];
+ let formattedKey = key;
+ let formattedValue : string;
- if (resource && resource.memory !== undefined) {
- formatted.push(`Memory: ${CommonUtil.formatBytes(resource.memory)}`);
- } else {
- formatted.push(`Memory: ${NOT_AVAILABLE}`);
- }
-
- if (resource && resource.vcore !== undefined) {
- formatted.push(`CPU: ${CommonUtil.formatCount(resource.vcore)}`);
- } else {
- formatted.push(`CPU: ${NOT_AVAILABLE}`);
- }
-
- if (resource){
- Object.keys(resource).forEach((key) => {
switch(key){
case "memory":
- case "vcore":{
+ formattedKey = "Memory";
+ formattedValue = CommonUtil.formatMemoryBytes(value);
break;
- }
- case "ephemeral-storage":{
- if (resource[`ephemeral-storage`] == 0) {
- formatted.push(`ephemeral-storage: ${NOT_AVAILABLE}`);
- }else{
- formatted.push(`ephemeral-storage: ${CommonUtil.formatBytes(resource[key])}`);
+ case "vcore":
+ formattedKey = "CPU";
+ formattedValue = CommonUtil.formatCpuCore(value);
+ break;
+ case "ephemeral-storage":
+ formattedValue = CommonUtil.formatEphemeralStorageBytes(value);
+ break;
+ default:
+ if (key.startsWith('hugepages-')) {
+ formattedValue = CommonUtil.formatMemoryBytes(value);
+ } else{
+ formattedValue = CommonUtil.formatOtherResource(value);
}
break;
- }
- default:{
- if (resource[key] == 0) {
- formatted.push(`${key}: ${NOT_AVAILABLE}`);
- }else{
- formatted.push(`${key}: ${CommonUtil.formatOtherResource(resource[key])}`);
- }
- break;
- }
- }
+ }
+ formatted.push(`${formattedKey}: ${formattedValue}`);
});
}
-
+
+ if (formatted.length === 0) {
+ return NOT_AVAILABLE;
+ }
return formatted.join(', ');
+ }
+
+ private resourcesCompareFn(a: string, b: string): number {
+ // define the order of resources
+ const resourceOrder: { [key: string]: number } = {
+ "memory": 1,
+ "vcore": 2,
+ "pods": 3,
+ "ephemeral-storage": 4
+ };
+ const orderA = a in resourceOrder ? resourceOrder[a] : Number.MAX_SAFE_INTEGER;
+ const orderB = b in resourceOrder ? resourceOrder[b] : Number.MAX_SAFE_INTEGER;
+
+ if (orderA !== orderB) {
+ return orderA - orderB; // Resources in the order defined above
+ } else {
+ return a.localeCompare(b); // Other resources will be in lexicographic order
+ }
}
private formatPercent(resource: SchedulerResourceInfo): string {
const formatted = [];
- if (resource && resource.memory !== undefined) {
- formatted.push(`Memory: ${CommonUtil.formatPercent(resource.memory)}`);
- } else {
- formatted.push(`Memory: ${NOT_AVAILABLE}`);
+ if (resource) {
+ if (resource.memory !== undefined){
+ formatted.push(`Memory: ${CommonUtil.formatPercent(resource.memory)}`);
+ }
+ if (resource.vcore !== undefined){
+ formatted.push(`CPU: ${CommonUtil.formatPercent(resource.vcore)}`);
+ }
}
-
- if (resource && resource.vcore !== undefined) {
- formatted.push(`CPU: ${CommonUtil.formatPercent(resource.vcore)}`);
- } else {
- formatted.push(`CPU: ${NOT_AVAILABLE}`);
+ if (formatted.length === 0) {
+ return NOT_AVAILABLE;
}
-
return formatted.join(', ');
}
diff --git a/src/app/utils/common.util.spec.ts b/src/app/utils/common.util.spec.ts
index ddf25eb..831e057 100644
--- a/src/app/utils/common.util.spec.ts
+++ b/src/app/utils/common.util.spec.ts
@@ -23,16 +23,39 @@
expect(CommonUtil.createUniqId).toBeTruthy();
});
- it('should have formatBytes method', () => {
- expect(CommonUtil.formatBytes).toBeTruthy();
+ it('checking formatMemoryBytes method result', () => {
+ const inputs: number[] = [0, 100, 1100, 1200000, 1048576000, 1300000000, 1400000000000, 1500000000000000, 1500000000000000000];
+ const expected: string[] = ['0 B', '100 B', '1.07 KiB', '1.14 MiB', '1,000 MiB', '1.21 GiB', '1.27 TiB', '1.33 PiB', '1.3 EiB'];
+ for (let index = 0; index < inputs.length; index = index + 1) {
+ expect(CommonUtil.formatMemoryBytes(inputs[index])).toEqual(expected[index]);
+ expect(CommonUtil.formatMemoryBytes(inputs[index].toString())).toEqual(expected[index]);
+ }
});
- it('checking formatBytes method result', () => {
- const inputs: number[] = [100, 1100, 1200000, 1300000000, 1400000000000, 1500000000000000];
- const expected: string[] = ['100.0 bytes', '1.1 kB', '1.2 MB', '1.3 GB', '1.4 TB', '1.5 PB'];
+ it('checking formatEphemeralStorageBytes method result', () => {
+ const inputs: number[] = [0, 100, 1100, 1200000, 1048576000, 1300000000, 1400000000000, 1500000000000000, 1500000000000000000];
+ const expected: string[] = ['0 B', '100 B', '1.1 kB', '1.2 MB', '1.05 GB', '1.3 GB', '1.4 TB', '1.5 PB', '1.5 EB'];
for (let index = 0; index < inputs.length; index = index + 1) {
- expect(CommonUtil.formatBytes(inputs[index])).toEqual(expected[index]);
- expect(CommonUtil.formatBytes(inputs[index].toString())).toEqual(expected[index]);
+ expect(CommonUtil.formatEphemeralStorageBytes(inputs[index])).toEqual(expected[index]);
+ expect(CommonUtil.formatEphemeralStorageBytes(inputs[index].toString())).toEqual(expected[index]);
+ }
+ });
+
+ it('checking formatCpuCore method result', () => {
+ const inputs: number[] = [0, 100, 1000, 1555, 1555555, 1555555555, 1555555555555, 1555555555555555, 1555555555555555555, 1555555555555555555555];
+ const expected: string[] = ['0','100m', '1', '1.56', '1.56k', '1.56M', '1.56G', '1.56T', '1.56P', '1.56E'];
+ for (let index = 0; index < inputs.length; index = index + 1) {
+ expect(CommonUtil.formatCpuCore(inputs[index])).toEqual(expected[index]);
+ expect(CommonUtil.formatCpuCore(inputs[index].toString())).toEqual(expected[index]);
+ }
+ });
+
+ it('checking formatOtherResource method result', () => {
+ const inputs: number[] = [0, 100, 1000, 1555, 1555555, 1555555555, 1555555555555, 1555555555555555, 1555555555555555555];
+ const expected: string[] = ['0','100', '1k', '1.56k', '1.56M', '1.56G', '1.56T', '1.56P', '1.56E'];
+ for (let index = 0; index < inputs.length; index = index + 1) {
+ expect(CommonUtil.formatOtherResource(inputs[index])).toEqual(expected[index]);
+ expect(CommonUtil.formatOtherResource(inputs[index].toString())).toEqual(expected[index]);
}
});
});
diff --git a/src/app/utils/common.util.ts b/src/app/utils/common.util.ts
index fc1e094..4c71722 100644
--- a/src/app/utils/common.util.ts
+++ b/src/app/utils/common.util.ts
@@ -30,34 +30,55 @@
return uniqid;
}
- static formatBytes(value: number | string): string {
- const units: readonly string[] = ['kB', 'MB', 'GB', 'TB', 'PB'];
- let unit: string = 'bytes';
- let toValue = +value
+ static formatMemoryBytes(value: number | string): string {
+ const units: readonly string[] = ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB'];
+ let unit: string = 'B';
+ let toValue = +value;
+ for (let i = 0, unitslen = units.length; toValue / 1024 >= 1 && i < unitslen;i = i + 1) {
+ toValue = toValue / 1024;
+ unit = units[i];
+ }
+ return `${toValue.toLocaleString(undefined, { maximumFractionDigits: 2 })} ${unit}`;
+ }
+
+ static formatEphemeralStorageBytes(value: number | string): string {
+ const units: readonly string[] = ['kB', 'MB', 'GB', 'TB', 'PB', 'EB'];
+ let unit: string = 'B';
+ let toValue = +value;
for (let i = 0, unitslen = units.length; toValue / 1000 >= 1 && i < unitslen;i = i + 1) {
toValue = toValue / 1000;
unit = units[i];
}
- return `${toValue.toFixed(1)} ${unit}`;
+ return `${toValue.toLocaleString(undefined, { maximumFractionDigits: 2 })} ${unit}`;
}
static isEmpty(arg: object | any[]): boolean {
return Object.keys(arg).length === 0;
}
- static formatCount(value: number | string): string {
- const unit = 'K';
- const toValue = +value;
- if (toValue >= 10000) {
- return `${(toValue / 1000).toLocaleString(undefined, { maximumFractionDigits: 1 })} ${unit}`;
+ static formatCpuCore(value: number | string): string {
+ const units: readonly string[] = ['m', '', 'k', 'M', 'G', 'T', 'P', 'E'];
+ let unit: string = '';
+ let toValue = +value;
+ if (toValue > 0) {
+ unit = units[0];
}
-
- return toValue.toLocaleString();
+ for (let i = 1, unitslen = units.length; toValue / 1000 >= 1 && i < unitslen;i = i + 1) {
+ toValue = toValue / 1000;
+ unit = units[i];
+ }
+ return `${toValue.toLocaleString(undefined, { maximumFractionDigits: 2 })}${unit}`;
}
static formatOtherResource(value: number | string): string {
- const toValue = +value;
- return toValue.toLocaleString();
+ const units: readonly string[] = ['k', 'M', 'G', 'T', 'P', 'E'];
+ let unit: string = '';
+ let toValue = +value;
+ for (let i = 0, unitslen = units.length; toValue / 1000 >= 1 && i < unitslen;i = i + 1) {
+ toValue = toValue / 1000;
+ unit = units[i];
+ }
+ return `${toValue.toLocaleString(undefined, { maximumFractionDigits: 2 })}${unit}`;
}
static resourceColumnFormatter(value: string): string {