From 77fa6bf978ecb7ef8727509fe109aa30c384c604 Mon Sep 17 00:00:00 2001 From: Paz Barda Date: Sun, 24 Mar 2024 10:34:13 +0200 Subject: [PATCH 1/7] feat(src): [CH24] added DataTable incl unit test Signed-off-by: Paz Barda --- .../generic-cpu/models/data-table.test.ts | 53 ++++++++++++++++++ src/lib/generic-cpu/models/data-table.ts | 54 +++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 src/__tests__/generic-cpu/models/data-table.test.ts create mode 100644 src/lib/generic-cpu/models/data-table.ts diff --git a/src/__tests__/generic-cpu/models/data-table.test.ts b/src/__tests__/generic-cpu/models/data-table.test.ts new file mode 100644 index 0000000..d5e1581 --- /dev/null +++ b/src/__tests__/generic-cpu/models/data-table.test.ts @@ -0,0 +1,53 @@ +import {DataTable} from '../../../lib/generic-cpu/models/data-table'; + +describe('lib/generic-cpu/models:', () => { + describe('DataTable', () => { + describe('init: ', () => { + it('valid input data.', () => { + const inputData = [ + ['id', 'grade', 'age'], + ['6565464', '89', '12'], + ['1230985', '91', '11'], + ['1237899', '100', '12'], + ]; + const dataTable = new DataTable(inputData); + expect(dataTable.getSize()).toEqual(3); + expect(dataTable.getColumnData('id')).toEqual([ + 6565464, 1230985, 1237899, + ]); + expect(dataTable.getColumnData('grade')).toEqual([89, 91, 100]); + expect(dataTable.getColumnData('age')).toEqual([12, 11, 12]); + }); + it('missing header.', () => { + const inputData = [ + ['id', '', 'age'], + ['6565464', '89', '12'], + ['1230985', '91', '11'], + ['1237899', '100', '12'], + ]; + const constructorCall = () => new DataTable(inputData); + expect(constructorCall).toThrow(Error); + }); + it('missing value.', () => { + const inputData = [ + ['id', 'grade', 'age'], + ['6565464', '', '12'], + ['1230985', '91', '11'], + ['1237899', '100', '12'], + ]; + const constructorCall = () => new DataTable(inputData); + expect(constructorCall).toThrow(Error); + }); + it('invalid value (NaN).', () => { + const inputData = [ + ['id', 'grade', 'age'], + ['6565464', '89', '12'], + ['1230985', 'qewrwrt', '11'], + ['1237899', '100', '12'], + ]; + const constructorCall = () => new DataTable(inputData); + expect(constructorCall).toThrow(Error); + }); + }); + }); +}); diff --git a/src/lib/generic-cpu/models/data-table.ts b/src/lib/generic-cpu/models/data-table.ts new file mode 100644 index 0000000..a61315e --- /dev/null +++ b/src/lib/generic-cpu/models/data-table.ts @@ -0,0 +1,54 @@ +// TODO PB -- standard IF errors +// TODO PB -- validate table alignment (same row number for each column) + +export class DataTable { + private dataMap: Map = new Map(); + + constructor(data: string[][]) { + this.dataMap = this.getValidatedDataMap(data); + } + + getColumnData(columnName: string): number[] { + const colData = this.dataMap.get(columnName); + if (colData === undefined) { + throw new Error(`Column ${columnName} not found in data table`); + } + return colData; + } + + getSize(): number { + return this.dataMap.size; + } + + private getValidatedDataMap(data: string[][]): Map { + const dataMap: Map = new Map(); + for (let colIndex = 0; colIndex < data[0].length; colIndex++) { + const header = this.getValidatedHeader(data[0][colIndex]); + const colData: number[] = []; + for (let rowIndex = 1; rowIndex < data.length; rowIndex++) { + colData[rowIndex - 1] = this.getValidatedNumber( + data[rowIndex][colIndex] + ); + } + console.log(`header = ${header}`); + console.log(`colData = ${colData}`); + dataMap.set(header, colData); + } + return dataMap; + } + + private getValidatedHeader(headerString: string): string { + if (typeof headerString !== 'string' || headerString.trim() === '') { + throw new Error('Invalid header'); + } + return headerString; + } + + private getValidatedNumber(str: string): number { + const parsedValue = parseFloat(str); + if (isNaN(parsedValue)) { + throw new Error(`Failed to parse '${str}' as a number`); + } + return parsedValue; + } +} From 033d6eef4cc267dd22b40fd6b4b591936ff1b0c8 Mon Sep 17 00:00:00 2001 From: Paz Barda Date: Sun, 24 Mar 2024 12:07:57 +0200 Subject: [PATCH 2/7] feat(src): [CH24] csv directory reader incl unit test Signed-off-by: Paz Barda --- .../helpers/csv-directory-reader.test.ts | 31 ++++++++++++++ .../generic-cpu/helpers/test-data/prod1.csv | 15 +++++++ .../generic-cpu/helpers/test-data/prod2.csv | 15 +++++++ .../generic-cpu/helpers/test-data/simple1.csv | 4 ++ .../generic-cpu/helpers/test-data/simple2.csv | 4 ++ .../helpers/csv-directory-reader.ts | 40 +++++++++++++++++++ 6 files changed, 109 insertions(+) create mode 100644 src/__tests__/generic-cpu/helpers/csv-directory-reader.test.ts create mode 100644 src/__tests__/generic-cpu/helpers/test-data/prod1.csv create mode 100644 src/__tests__/generic-cpu/helpers/test-data/prod2.csv create mode 100644 src/__tests__/generic-cpu/helpers/test-data/simple1.csv create mode 100644 src/__tests__/generic-cpu/helpers/test-data/simple2.csv create mode 100644 src/lib/generic-cpu/helpers/csv-directory-reader.ts diff --git a/src/__tests__/generic-cpu/helpers/csv-directory-reader.test.ts b/src/__tests__/generic-cpu/helpers/csv-directory-reader.test.ts new file mode 100644 index 0000000..159008a --- /dev/null +++ b/src/__tests__/generic-cpu/helpers/csv-directory-reader.test.ts @@ -0,0 +1,31 @@ +import {CsvDirectoryReader} from '../../../lib/generic-cpu/helpers/csv-directory-reader'; + +describe('lib/generic-cpu/helpers:', () => { + describe('CsvDirectoryReader', () => { + describe('init: ', () => { + it('valid', () => { + const csvDirectoryReader: CsvDirectoryReader = new CsvDirectoryReader( + __dirname + '/test-data/' + ); + const expected1 = { + simple1: [ + ['CPU_perc', 'Throughput_reqPerSec', 'POWER_W'], + ['0', '0', '70'], + ['10', '600000', '90'], + ['20', '800000', '100'], + ], + }; + const expected2 = { + simple2: [ + ['CPU_perc', 'Throughput_reqPerSec', 'POWER_W'], + ['0', '0', '90'], + ['10', '700000', '100'], + ['20', '900000', '110'], + ], + }; + expect(csvDirectoryReader.read(['simple1'])).toEqual(expected1); + expect(csvDirectoryReader.read(['simple2'])).toEqual(expected2); + }); + }); + }); +}); diff --git a/src/__tests__/generic-cpu/helpers/test-data/prod1.csv b/src/__tests__/generic-cpu/helpers/test-data/prod1.csv new file mode 100644 index 0000000..ded68cd --- /dev/null +++ b/src/__tests__/generic-cpu/helpers/test-data/prod1.csv @@ -0,0 +1,15 @@ +CPU_perc,Throughput_reqPerSec,POWER_W +0,0,70 +10,600000,90 +20,800000,100 +30,1100000,120 +40,1400000,130 +50,1800000,150 +60,2200000,170 +70,2500000,180 +80,2600000,180 +80,2700000,180 +90,2750000,180 +100,2800000,180 + + diff --git a/src/__tests__/generic-cpu/helpers/test-data/prod2.csv b/src/__tests__/generic-cpu/helpers/test-data/prod2.csv new file mode 100644 index 0000000..2000606 --- /dev/null +++ b/src/__tests__/generic-cpu/helpers/test-data/prod2.csv @@ -0,0 +1,15 @@ +CPU_perc,Throughput_reqPerSec,POWER_W +0,0,70 +10,600000,90 +20,900000,110 +30,1200000,130 +40,1500000,140 +50,1900000,170 +60,2300000,180 +70,2600000,190 +80,2700000,200 +80,2800000,210 +90,2850000,220 +100,2900000,220 + + diff --git a/src/__tests__/generic-cpu/helpers/test-data/simple1.csv b/src/__tests__/generic-cpu/helpers/test-data/simple1.csv new file mode 100644 index 0000000..f546909 --- /dev/null +++ b/src/__tests__/generic-cpu/helpers/test-data/simple1.csv @@ -0,0 +1,4 @@ +CPU_perc,Throughput_reqPerSec,POWER_W +0,0,70 +10,600000,90 +20,800000,100 \ No newline at end of file diff --git a/src/__tests__/generic-cpu/helpers/test-data/simple2.csv b/src/__tests__/generic-cpu/helpers/test-data/simple2.csv new file mode 100644 index 0000000..ce42586 --- /dev/null +++ b/src/__tests__/generic-cpu/helpers/test-data/simple2.csv @@ -0,0 +1,4 @@ +CPU_perc,Throughput_reqPerSec,POWER_W +0,0,90 +10,700000,100 +20,900000,110 \ No newline at end of file diff --git a/src/lib/generic-cpu/helpers/csv-directory-reader.ts b/src/lib/generic-cpu/helpers/csv-directory-reader.ts new file mode 100644 index 0000000..1fc5fda --- /dev/null +++ b/src/lib/generic-cpu/helpers/csv-directory-reader.ts @@ -0,0 +1,40 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +export class CsvDirectoryReader { + private directoryPath: string; + + constructor(directoryPath: string) { + this.validateDirectoryPath(directoryPath); + this.directoryPath = directoryPath; + } + + read(fileNames: string[]): {[key: string]: string[][]} { + const filesMapping: {[key: string]: string[][]} = {}; + for (const fileName of fileNames) { + const filePath = path.join(this.directoryPath, fileName + '.csv'); + const fileContent = fs.readFileSync(filePath, 'utf8'); + filesMapping[fileName] = this.parseCSV(fileContent); + } + return filesMapping; + } + + private validateDirectoryPath(directoryPath: string) { + if ( + !fs.existsSync(directoryPath) || + !fs.lstatSync(directoryPath).isDirectory() + ) { + throw new Error('Invalid directory path: ' + directoryPath); + } + } + + private parseCSV(csvString: string): string[][] { + const rows: string[] = csvString.split('\n'); + const result: string[][] = []; + for (const row of rows) { + const columns: string[] = row.split(','); + result.push(columns); + } + return result; + } +} From e7f73ef6aa980f8fcd0af3ea6ff403abc8c51ff7 Mon Sep 17 00:00:00 2001 From: Paz Barda Date: Sun, 24 Mar 2024 13:26:52 +0200 Subject: [PATCH 3/7] feat(src): [CH24] added linear interpolator incl unit tests Signed-off-by: Paz Barda --- .../helpers/linear-interpolator.test.ts | 36 ++++++++++++++ .../helpers/linear-interpolator.ts | 48 +++++++++++++++++++ src/lib/generic-cpu/models/data-table.ts | 2 - 3 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/generic-cpu/helpers/linear-interpolator.test.ts create mode 100644 src/lib/generic-cpu/helpers/linear-interpolator.ts diff --git a/src/__tests__/generic-cpu/helpers/linear-interpolator.test.ts b/src/__tests__/generic-cpu/helpers/linear-interpolator.test.ts new file mode 100644 index 0000000..45718fb --- /dev/null +++ b/src/__tests__/generic-cpu/helpers/linear-interpolator.test.ts @@ -0,0 +1,36 @@ +import {LinearInterpolator} from '../../../lib/generic-cpu/helpers/linear-interpolator'; + +describe('lib/generic-cpu/helpers:', () => { + describe('LinearInterpolator', () => { + describe('sanity: ', () => { + const linearnterpolator: LinearInterpolator = new LinearInterpolator(); + const lookupSeries = [10, 20, 40, 100]; + const valueSeries = [1000, 2000, 4000, 10000]; + it('out of range low', () => { + expect( + linearnterpolator.interpolate(5, lookupSeries, valueSeries) + ).toEqual(500); + }); + it('out of range high', () => { + expect( + linearnterpolator.interpolate(150, lookupSeries, valueSeries) + ).toEqual(10000); + }); + it('no interpolation needed', () => { + expect( + linearnterpolator.interpolate(40, lookupSeries, valueSeries) + ).toEqual(4000); + }); + it('interpolation', () => { + expect( + linearnterpolator.interpolate(50, lookupSeries, valueSeries) + ).toEqual(5000); + }); + it('interpolation with percision', () => { + expect( + linearnterpolator.interpolate(20.01, lookupSeries, valueSeries) + ).toBeCloseTo(2001); + }); + }); + }); +}); diff --git a/src/lib/generic-cpu/helpers/linear-interpolator.ts b/src/lib/generic-cpu/helpers/linear-interpolator.ts new file mode 100644 index 0000000..1d7cfdf --- /dev/null +++ b/src/lib/generic-cpu/helpers/linear-interpolator.ts @@ -0,0 +1,48 @@ +// TODO PB -- IDF stadard errors + +export class LinearInterpolator { + interpolate( + lookupKey: number, + lookupSeries: number[], + valueSeries: number[] + ): number { + // TODO PB -- validate all values in inputs are positive + let x0: number; + let x1: number; + let y0: number; + let y1: number; + if (lookupKey > lookupSeries[lookupSeries.length - 1]) { + return valueSeries[valueSeries.length - 1]; + } else if (lookupKey < lookupSeries[0]) { + x0 = 0; + x1 = lookupSeries[0]; + y0 = 0; + y1 = valueSeries[0]; + } else { + const [leftIndex, rightIndex] = this.findBoundaryIndexes( + lookupKey, + lookupSeries + ); + x0 = lookupSeries[leftIndex]; + x1 = lookupSeries[rightIndex]; + y0 = valueSeries[leftIndex]; + y1 = valueSeries[rightIndex]; + } + const interpolatedValue = y0 + (lookupKey - x0) * ((y1 - y0) / (x1 - x0)); + return interpolatedValue; + } + + private findBoundaryIndexes( + lookupValue: number, + lookupSeries: number[] + ): [number, number] { + for (let i = 0; i < lookupSeries.length - 1; i++) { + if (lookupValue >= lookupSeries[i] && lookupValue < lookupSeries[i + 1]) { + return [i, i + 1]; + } + } + throw new Error( + `Failed to find boundary indexes for ${lookupValue} in ${lookupSeries}` + ); + } +} diff --git a/src/lib/generic-cpu/models/data-table.ts b/src/lib/generic-cpu/models/data-table.ts index a61315e..e46f5c6 100644 --- a/src/lib/generic-cpu/models/data-table.ts +++ b/src/lib/generic-cpu/models/data-table.ts @@ -30,8 +30,6 @@ export class DataTable { data[rowIndex][colIndex] ); } - console.log(`header = ${header}`); - console.log(`colData = ${colData}`); dataMap.set(header, colData); } return dataMap; From aee14557d3ad9c90fd8447aee1cbd89697b33c3c Mon Sep 17 00:00:00 2001 From: Paz Barda Date: Sun, 24 Mar 2024 18:17:34 +0200 Subject: [PATCH 4/7] feat(src): [CH24] added power calculator incl unit tests Signed-off-by: Paz Barda --- .../helpers/csv-directory-reader.test.ts | 2 +- .../helpers/linear-interpolator.test.ts | 2 +- .../helpers/power-calculator.test.ts | 35 ++++++ .../generic-cpu/helpers/test-data/prod1.csv | 4 +- .../generic-cpu/helpers/test-data/prod2.csv | 6 +- .../generic-cpu/helpers/test-data/simple1.csv | 0 .../generic-cpu/helpers/test-data/simple2.csv | 0 .../generic-cpu/models/data-table.test.ts | 2 +- .../generic-cpu/helpers/power-calculator.ts | 116 ++++++++++++++++++ 9 files changed, 157 insertions(+), 10 deletions(-) rename src/__tests__/{ => unit/lib}/generic-cpu/helpers/csv-directory-reader.test.ts (90%) rename src/__tests__/{ => unit/lib}/generic-cpu/helpers/linear-interpolator.test.ts (92%) create mode 100644 src/__tests__/unit/lib/generic-cpu/helpers/power-calculator.test.ts rename src/__tests__/{ => unit/lib}/generic-cpu/helpers/test-data/prod1.csv (91%) rename src/__tests__/{ => unit/lib}/generic-cpu/helpers/test-data/prod2.csv (84%) rename src/__tests__/{ => unit/lib}/generic-cpu/helpers/test-data/simple1.csv (100%) rename src/__tests__/{ => unit/lib}/generic-cpu/helpers/test-data/simple2.csv (100%) rename src/__tests__/{ => unit/lib}/generic-cpu/models/data-table.test.ts (95%) create mode 100644 src/lib/generic-cpu/helpers/power-calculator.ts diff --git a/src/__tests__/generic-cpu/helpers/csv-directory-reader.test.ts b/src/__tests__/unit/lib/generic-cpu/helpers/csv-directory-reader.test.ts similarity index 90% rename from src/__tests__/generic-cpu/helpers/csv-directory-reader.test.ts rename to src/__tests__/unit/lib/generic-cpu/helpers/csv-directory-reader.test.ts index 159008a..28f7408 100644 --- a/src/__tests__/generic-cpu/helpers/csv-directory-reader.test.ts +++ b/src/__tests__/unit/lib/generic-cpu/helpers/csv-directory-reader.test.ts @@ -1,4 +1,4 @@ -import {CsvDirectoryReader} from '../../../lib/generic-cpu/helpers/csv-directory-reader'; +import {CsvDirectoryReader} from '../../../../../lib/generic-cpu/helpers/csv-directory-reader'; describe('lib/generic-cpu/helpers:', () => { describe('CsvDirectoryReader', () => { diff --git a/src/__tests__/generic-cpu/helpers/linear-interpolator.test.ts b/src/__tests__/unit/lib/generic-cpu/helpers/linear-interpolator.test.ts similarity index 92% rename from src/__tests__/generic-cpu/helpers/linear-interpolator.test.ts rename to src/__tests__/unit/lib/generic-cpu/helpers/linear-interpolator.test.ts index 45718fb..0fe73e2 100644 --- a/src/__tests__/generic-cpu/helpers/linear-interpolator.test.ts +++ b/src/__tests__/unit/lib/generic-cpu/helpers/linear-interpolator.test.ts @@ -1,4 +1,4 @@ -import {LinearInterpolator} from '../../../lib/generic-cpu/helpers/linear-interpolator'; +import {LinearInterpolator} from '../../../../../lib/generic-cpu/helpers/linear-interpolator'; describe('lib/generic-cpu/helpers:', () => { describe('LinearInterpolator', () => { diff --git a/src/__tests__/unit/lib/generic-cpu/helpers/power-calculator.test.ts b/src/__tests__/unit/lib/generic-cpu/helpers/power-calculator.test.ts new file mode 100644 index 0000000..462852d --- /dev/null +++ b/src/__tests__/unit/lib/generic-cpu/helpers/power-calculator.test.ts @@ -0,0 +1,35 @@ +import {CsvDirectoryReader} from '../../../../../lib/generic-cpu/helpers/csv-directory-reader'; +import {LinearInterpolator} from '../../../../../lib/generic-cpu/helpers/linear-interpolator'; +import {PowerCalculator} from '../../../../../lib/generic-cpu/helpers/power-calculator'; + +describe('lib/generic-cpu/helpers:', () => { + describe('CsvDirectoryReader', () => { + describe('sanity: ', () => { + const powerCalculator: PowerCalculator = new PowerCalculator( + ['prod1', 'prod2'], + new CsvDirectoryReader(__dirname + '/test-data/'), + new LinearInterpolator() + ); + it('calculate', () => { + expect(powerCalculator.calculate(15, 'prod1')).toEqual(95); + }); + it('simulate', () => { + expect(powerCalculator.simulate(15, 'prod1', 'prod2')).toEqual(110); + }); + it('calculate processor not found', () => { + const calculateCall = () => powerCalculator.calculate(15, 'prodX'); + expect(calculateCall).toThrow(Error); + }); + it('simulate physical processor not found', () => { + const simulateCall = () => + powerCalculator.simulate(15, 'prodX', 'prod2'); + expect(simulateCall).toThrow(Error); + }); + it('simulate simualted processor not found', () => { + const simulateCall = () => + powerCalculator.simulate(15, 'prod1', 'prodX'); + expect(simulateCall).toThrow(Error); + }); + }); + }); +}); diff --git a/src/__tests__/generic-cpu/helpers/test-data/prod1.csv b/src/__tests__/unit/lib/generic-cpu/helpers/test-data/prod1.csv similarity index 91% rename from src/__tests__/generic-cpu/helpers/test-data/prod1.csv rename to src/__tests__/unit/lib/generic-cpu/helpers/test-data/prod1.csv index ded68cd..726db98 100644 --- a/src/__tests__/generic-cpu/helpers/test-data/prod1.csv +++ b/src/__tests__/unit/lib/generic-cpu/helpers/test-data/prod1.csv @@ -10,6 +10,4 @@ CPU_perc,Throughput_reqPerSec,POWER_W 80,2600000,180 80,2700000,180 90,2750000,180 -100,2800000,180 - - +100,2800000,180 \ No newline at end of file diff --git a/src/__tests__/generic-cpu/helpers/test-data/prod2.csv b/src/__tests__/unit/lib/generic-cpu/helpers/test-data/prod2.csv similarity index 84% rename from src/__tests__/generic-cpu/helpers/test-data/prod2.csv rename to src/__tests__/unit/lib/generic-cpu/helpers/test-data/prod2.csv index 2000606..98451ec 100644 --- a/src/__tests__/generic-cpu/helpers/test-data/prod2.csv +++ b/src/__tests__/unit/lib/generic-cpu/helpers/test-data/prod2.csv @@ -1,7 +1,7 @@ CPU_perc,Throughput_reqPerSec,POWER_W 0,0,70 10,600000,90 -20,900000,110 +20,700000,110 30,1200000,130 40,1500000,140 50,1900000,170 @@ -10,6 +10,4 @@ CPU_perc,Throughput_reqPerSec,POWER_W 80,2700000,200 80,2800000,210 90,2850000,220 -100,2900000,220 - - +100,2900000,220 \ No newline at end of file diff --git a/src/__tests__/generic-cpu/helpers/test-data/simple1.csv b/src/__tests__/unit/lib/generic-cpu/helpers/test-data/simple1.csv similarity index 100% rename from src/__tests__/generic-cpu/helpers/test-data/simple1.csv rename to src/__tests__/unit/lib/generic-cpu/helpers/test-data/simple1.csv diff --git a/src/__tests__/generic-cpu/helpers/test-data/simple2.csv b/src/__tests__/unit/lib/generic-cpu/helpers/test-data/simple2.csv similarity index 100% rename from src/__tests__/generic-cpu/helpers/test-data/simple2.csv rename to src/__tests__/unit/lib/generic-cpu/helpers/test-data/simple2.csv diff --git a/src/__tests__/generic-cpu/models/data-table.test.ts b/src/__tests__/unit/lib/generic-cpu/models/data-table.test.ts similarity index 95% rename from src/__tests__/generic-cpu/models/data-table.test.ts rename to src/__tests__/unit/lib/generic-cpu/models/data-table.test.ts index d5e1581..c4e2775 100644 --- a/src/__tests__/generic-cpu/models/data-table.test.ts +++ b/src/__tests__/unit/lib/generic-cpu/models/data-table.test.ts @@ -1,4 +1,4 @@ -import {DataTable} from '../../../lib/generic-cpu/models/data-table'; +import {DataTable} from '../../../../../lib/generic-cpu/models/data-table'; describe('lib/generic-cpu/models:', () => { describe('DataTable', () => { diff --git a/src/lib/generic-cpu/helpers/power-calculator.ts b/src/lib/generic-cpu/helpers/power-calculator.ts new file mode 100644 index 0000000..ab0a37b --- /dev/null +++ b/src/lib/generic-cpu/helpers/power-calculator.ts @@ -0,0 +1,116 @@ +// TODO PB -- clean code + +import {DataTable} from '../models/data-table'; +import {CsvDirectoryReader} from './csv-directory-reader'; +import {LinearInterpolator} from './linear-interpolator'; + +export class PowerCalculator { + private CPU_UTIL_COL_NAME = 'CPU_perc'; + private THROUGHPUT_COL_NAME = 'Throughput_reqPerSec'; + private POWER_COL_NAME = 'POWER_W'; + private prodNameToDataTable: Map = new Map< + string, + DataTable + >(); + private linearInterpolator: LinearInterpolator; + constructor( + productNames: string[], + csvDirectortyReader: CsvDirectoryReader, + linearInterpolator: LinearInterpolator + ) { + this.prodNameToDataTable = this.getProdNameToDataTable( + productNames, + csvDirectortyReader + ); + this.linearInterpolator = linearInterpolator; + } + + calculate(cpuUtil: number, processorName: string) { + const powerCurveDataTable = this.prodNameToDataTable.get(processorName); + if (powerCurveDataTable === undefined) { + throw new Error(`no power curve available for ${processorName}`); + } + const cpuUtilSeries = powerCurveDataTable.getColumnData( + this.CPU_UTIL_COL_NAME + ); + const powerSeries = powerCurveDataTable.getColumnData(this.POWER_COL_NAME); + const power = this.linearInterpolator.interpolate( + cpuUtil, + cpuUtilSeries, + powerSeries + ); + return power; + } + + simulate( + physicalCpuUtil: number, + physicalProcessorName: string, + simulatedProcessorName: string + ) { + const physicalPowerCurveDataTable = this.prodNameToDataTable.get( + physicalProcessorName + ); + if (physicalPowerCurveDataTable === undefined) { + throw new Error(`no power curve available for ${physicalProcessorName}`); + } + const simulatedPowerCurveDataTable = this.prodNameToDataTable.get( + simulatedProcessorName + ); + if (simulatedPowerCurveDataTable === undefined) { + throw new Error( + `no power curve available for ${simulatedPowerCurveDataTable}` + ); + } + // interpolate TP + const physicalThrouputSeries = physicalPowerCurveDataTable.getColumnData( + this.THROUGHPUT_COL_NAME + ); + const physicalCpuUtilSeries = physicalPowerCurveDataTable.getColumnData( + this.CPU_UTIL_COL_NAME + ); + const throughput = this.linearInterpolator.interpolate( + physicalCpuUtil, + physicalCpuUtilSeries, + physicalThrouputSeries + ); + // interpolate simulated CPU util + const simulatedThroughputSeries = + simulatedPowerCurveDataTable.getColumnData(this.THROUGHPUT_COL_NAME); + const simulatedCpuUtilSeries = simulatedPowerCurveDataTable.getColumnData( + this.CPU_UTIL_COL_NAME + ); + const simulatedCpuUtil = this.linearInterpolator.interpolate( + throughput, + simulatedThroughputSeries, + simulatedCpuUtilSeries + ); + // interpolate simulated power + const simulatedPowerSeries = simulatedPowerCurveDataTable.getColumnData( + this.POWER_COL_NAME + ); + const power = this.linearInterpolator.interpolate( + simulatedCpuUtil, + simulatedCpuUtilSeries, + simulatedPowerSeries + ); + return power; + } + + private getProdNameToDataTable( + productNames: string[], + csvDirectortyReader: CsvDirectoryReader + ): Map { + const prodNameToDataTable: Map = new Map< + string, + DataTable + >(); + const prodNameToCsvContent = csvDirectortyReader.read(productNames); + for (const key in prodNameToCsvContent) { + if (Object.prototype.hasOwnProperty.call(prodNameToCsvContent, key)) { + const content: string[][] = prodNameToCsvContent[key]; + prodNameToDataTable.set(key, new DataTable(content)); + } + } + return prodNameToDataTable; + } +} From a498bfa501c2c00d0f60b74eca9717eb52fb9583 Mon Sep 17 00:00:00 2001 From: Paz Barda Date: Mon, 25 Mar 2024 13:18:19 +0200 Subject: [PATCH 5/7] feat(src): [CH24] integrated all generic-cpu components into an index.ts Signed-off-by: Paz Barda --- src/lib/generic-cpu/index.ts | 57 ++++++++++++++++++++++++++++++++++++ src/lib/index.ts | 1 + 2 files changed, 58 insertions(+) create mode 100644 src/lib/generic-cpu/index.ts diff --git a/src/lib/generic-cpu/index.ts b/src/lib/generic-cpu/index.ts new file mode 100644 index 0000000..6a06e6e --- /dev/null +++ b/src/lib/generic-cpu/index.ts @@ -0,0 +1,57 @@ +import {PluginInterface} from '../../interfaces'; +import {ConfigParams, PluginParams} from '../../types/common'; +import {CsvDirectoryReader} from './helpers/csv-directory-reader'; +import {LinearInterpolator} from './helpers/linear-interpolator'; +import {PowerCalculator} from './helpers/power-calculator'; + +export const GenericCPU = (globalConfig: ConfigParams): PluginInterface => { + const metadata = { + kind: 'execute', + }; + + /** + * TODO PB - doc + */ + const execute = async (inputs: PluginParams[]) => { + const powerCurvesRootDir = globalConfig['power-curves-root-dir']; + const physicalProcessor = globalConfig['physical-processor']; + const simulatedProcessor = globalConfig['simulated-processor']; + const processorNames = shouldSimulate(simulatedProcessor) + ? [physicalProcessor, simulatedProcessor] + : [physicalProcessor]; + const powerCalculator = new PowerCalculator( + processorNames, + new CsvDirectoryReader(powerCurvesRootDir), + new LinearInterpolator() + ); + + return inputs.map((input: PluginParams) => { + const cpuUtil = input['cpu/utilization']; + const duration = input['duration']; + const power = shouldSimulate(simulatedProcessor) + ? powerCalculator.simulate( + cpuUtil, + physicalProcessor, + simulatedProcessor + ) + : powerCalculator.calculate(cpuUtil, physicalProcessor); + return { + ...input, + 'cpu/energy': calculateEnergy(power, duration), + }; + }); + }; + + const shouldSimulate = (simulatedProcessor: string) => { + return simulatedProcessor !== undefined; + }; + + const calculateEnergy = (power: number, duration: number) => { + return (power * (duration / 3600)) / 1000; + }; + + return { + metadata, + execute, + }; +}; diff --git a/src/lib/index.ts b/src/lib/index.ts index f18d984..27cf202 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -14,3 +14,4 @@ export {Sum} from './sum'; export {Coefficient} from './coefficient'; export {Divide} from './divide'; export {Regex} from './regex'; +export {GenericCPU} from './generic-cpu'; From 13d1d7edc604af44843f9656295aa6da5e7dc5d0 Mon Sep 17 00:00:00 2001 From: Paz Barda Date: Mon, 25 Mar 2024 14:23:56 +0200 Subject: [PATCH 6/7] feat(src): added e2e tests for generic cpu Signed-off-by: Paz Barda --- .../unit/lib/generic-cpu/index.test.ts | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 src/__tests__/unit/lib/generic-cpu/index.test.ts diff --git a/src/__tests__/unit/lib/generic-cpu/index.test.ts b/src/__tests__/unit/lib/generic-cpu/index.test.ts new file mode 100644 index 0000000..8c70922 --- /dev/null +++ b/src/__tests__/unit/lib/generic-cpu/index.test.ts @@ -0,0 +1,149 @@ +import {GenericCPU} from '../../../../lib'; +import {PluginParams} from '../../../../types/common'; + +describe('lib/generic-cpu/helpers:', () => { + describe('CsvDirectoryReader', () => { + describe('sanity: ', () => { + it('calculate-no-interpolation', async () => { + const globalConfig = { + 'power-curves-root-dir': __dirname + '/helpers/test-data/', + 'physical-processor': 'prod1', + }; + const inputs: PluginParams[] = [ + { + duration: 1, + 'cpu/utilization': 20, + timestamp: '2021-01-01T00:00:00Z', + }, + { + duration: 1, + 'cpu/utilization': 30, + timestamp: '2021-01-01T00:00:01Z', + }, + { + duration: 1, + 'cpu/utilization': 90, + timestamp: '2021-01-01T00:00:02Z', + }, + ]; + const expected: PluginParams[] = [ + { + duration: 1, + 'cpu/utilization': 20, // => 100 W + timestamp: '2021-01-01T00:00:00Z', + 'cpu/energy': 0.000027777777777777776, // (100 * 1/3600) / 1000 + }, + { + duration: 1, + 'cpu/utilization': 30, // => 120 W + timestamp: '2021-01-01T00:00:01Z', + 'cpu/energy': 0.000033333333333333335, // (120 * 1/3600) / 1000 + }, + { + duration: 1, + 'cpu/utilization': 90, // => 180 W + timestamp: '2021-01-01T00:00:02Z', + 'cpu/energy': 0.00005, // (120 * 1/3600) / 1000 + }, + ]; + const genericCpu = GenericCPU(globalConfig); + const actual = await genericCpu.execute(inputs); + expect(actual).toEqual(expected); + }); + + it('calculate-with-interpolation', async () => { + const globalConfig = { + 'power-curves-root-dir': __dirname + '/helpers/test-data/', + 'physical-processor': 'prod1', + }; + const inputs: PluginParams[] = [ + { + duration: 1, + 'cpu/utilization': 15, + timestamp: '2021-01-01T00:00:00Z', + }, + { + duration: 1, + 'cpu/utilization': 24, + timestamp: '2021-01-01T00:00:01Z', + }, + { + duration: 1, + 'cpu/utilization': 61, + timestamp: '2021-01-01T00:00:02Z', + }, + ]; + const expected: PluginParams[] = [ + { + duration: 1, + 'cpu/utilization': 15, // => 95 W + timestamp: '2021-01-01T00:00:00Z', + 'cpu/energy': 0.00002638888888888889, // (95 * 1/3600) / 1000 + }, + { + duration: 1, + 'cpu/utilization': 24, // => 108 W + timestamp: '2021-01-01T00:00:01Z', + 'cpu/energy': 0.000029999999999999997, // (108 * 1/3600) / 1000 + }, + { + duration: 1, + 'cpu/utilization': 61, // => 171 W + timestamp: '2021-01-01T00:00:02Z', + 'cpu/energy': 0.0000475, // (171 * 1/3600) / 1000 + }, + ]; + const genericCpu = GenericCPU(globalConfig); + const actual = await genericCpu.execute(inputs); + expect(actual).toEqual(expected); + }); + it('simulate-with-interpolation', async () => { + const globalConfig = { + 'power-curves-root-dir': __dirname + '/helpers/test-data/', + 'physical-processor': 'prod1', + 'simulated-processor': 'prod2', + }; + const inputs: PluginParams[] = [ + { + duration: 1, + 'cpu/utilization': 20, + timestamp: '2021-01-01T00:00:00Z', + }, + { + duration: 1, + 'cpu/utilization': 55, + timestamp: '2021-01-01T00:00:01Z', + }, + { + duration: 1, + 'cpu/utilization': 61, + timestamp: '2021-01-01T00:00:02Z', + }, + ]; + const expected: PluginParams[] = [ + { + duration: 1, + 'cpu/utilization': 20, // => 800000 req/sec => 22% => 114 W + timestamp: '2021-01-01T00:00:00Z', + 'cpu/energy': 0.000031666666666666666, // (114 * 1/3600) / 1000 + }, + { + duration: 1, + 'cpu/utilization': 55, // => 2000000 req/sec => 52.5% => 172.5 W + timestamp: '2021-01-01T00:00:01Z', + 'cpu/energy': 0.00004791666666666667, // (172.5 * 1/3600) / 1000 + }, + { + duration: 1, + 'cpu/utilization': 61, // => 2230000 req/sec => 58.25% => 178.25 W + timestamp: '2021-01-01T00:00:02Z', + 'cpu/energy': 0.00004951388888888889, // (178.25 * 1/3600) / 1000 + }, + ]; + const genericCpu = GenericCPU(globalConfig); + const actual = await genericCpu.execute(inputs); + expect(actual).toEqual(expected); + }); + }); + }); +}); From 244cc471feddb6b56d57fed2a154322764f5462b Mon Sep 17 00:00:00 2001 From: Paz Barda Date: Tue, 26 Mar 2024 13:11:25 +0200 Subject: [PATCH 7/7] feat(src): [CH24] todos and tweaks Signed-off-by: Paz Barda --- .../helpers/linear-interpolator.test.ts | 25 ++++++++++++++ .../helpers/linear-interpolator.ts | 17 ++++++++-- .../generic-cpu/helpers/power-calculator.ts | 34 +++++++++---------- src/lib/generic-cpu/index.ts | 3 +- src/lib/generic-cpu/models/data-table.ts | 27 ++++++++++++--- 5 files changed, 80 insertions(+), 26 deletions(-) diff --git a/src/__tests__/unit/lib/generic-cpu/helpers/linear-interpolator.test.ts b/src/__tests__/unit/lib/generic-cpu/helpers/linear-interpolator.test.ts index 0fe73e2..15a701a 100644 --- a/src/__tests__/unit/lib/generic-cpu/helpers/linear-interpolator.test.ts +++ b/src/__tests__/unit/lib/generic-cpu/helpers/linear-interpolator.test.ts @@ -6,6 +6,8 @@ describe('lib/generic-cpu/helpers:', () => { const linearnterpolator: LinearInterpolator = new LinearInterpolator(); const lookupSeries = [10, 20, 40, 100]; const valueSeries = [1000, 2000, 4000, 10000]; + const invalidLookupSeries = [10, -20, 40, 100]; + const invalidValueSeries = [1000, 2000, -4000, 10000]; it('out of range low', () => { expect( linearnterpolator.interpolate(5, lookupSeries, valueSeries) @@ -31,6 +33,29 @@ describe('lib/generic-cpu/helpers:', () => { linearnterpolator.interpolate(20.01, lookupSeries, valueSeries) ).toBeCloseTo(2001); }); + it('negative lookup key', () => { + const interpolateCall = () => + linearnterpolator.interpolate(-20.01, lookupSeries, valueSeries); + expect(interpolateCall).toThrow(Error); + }); + it('negative item in lookup series', () => { + const interpolateCall = () => + linearnterpolator.interpolate( + 20.01, + invalidLookupSeries, + valueSeries + ); + expect(interpolateCall).toThrow(Error); + }); + it('negative item in value series', () => { + const interpolateCall = () => + linearnterpolator.interpolate( + 20.01, + lookupSeries, + invalidValueSeries + ); + expect(interpolateCall).toThrow(Error); + }); }); }); }); diff --git a/src/lib/generic-cpu/helpers/linear-interpolator.ts b/src/lib/generic-cpu/helpers/linear-interpolator.ts index 1d7cfdf..f663657 100644 --- a/src/lib/generic-cpu/helpers/linear-interpolator.ts +++ b/src/lib/generic-cpu/helpers/linear-interpolator.ts @@ -1,4 +1,5 @@ -// TODO PB -- IDF stadard errors +import {ERRORS} from '../../../util/errors'; +const {InputValidationError} = ERRORS; export class LinearInterpolator { interpolate( @@ -6,7 +7,9 @@ export class LinearInterpolator { lookupSeries: number[], valueSeries: number[] ): number { - // TODO PB -- validate all values in inputs are positive + this.validatePositiveNumbers([lookupKey], 'lookupKey'); + this.validatePositiveNumbers(lookupSeries, 'lookupSeries'); + this.validatePositiveNumbers(valueSeries, 'valueSeries'); let x0: number; let x1: number; let y0: number; @@ -41,8 +44,16 @@ export class LinearInterpolator { return [i, i + 1]; } } - throw new Error( + throw new InputValidationError( `Failed to find boundary indexes for ${lookupValue} in ${lookupSeries}` ); } + + private validatePositiveNumbers(numbersArray: number[], arrayName: string) { + if (numbersArray.some(num => num < 0)) { + throw new InputValidationError( + `${arrayName} contains negative value(s): ${numbersArray}` + ); + } + } } diff --git a/src/lib/generic-cpu/helpers/power-calculator.ts b/src/lib/generic-cpu/helpers/power-calculator.ts index ab0a37b..0a20acf 100644 --- a/src/lib/generic-cpu/helpers/power-calculator.ts +++ b/src/lib/generic-cpu/helpers/power-calculator.ts @@ -1,4 +1,5 @@ -// TODO PB -- clean code +import {ERRORS} from '../../../util/errors'; +const {InputValidationError} = ERRORS; import {DataTable} from '../models/data-table'; import {CsvDirectoryReader} from './csv-directory-reader'; @@ -26,10 +27,8 @@ export class PowerCalculator { } calculate(cpuUtil: number, processorName: string) { - const powerCurveDataTable = this.prodNameToDataTable.get(processorName); - if (powerCurveDataTable === undefined) { - throw new Error(`no power curve available for ${processorName}`); - } + const powerCurveDataTable = + this.getPowerCurveDataTableOrThrowError(processorName); const cpuUtilSeries = powerCurveDataTable.getColumnData( this.CPU_UTIL_COL_NAME ); @@ -47,20 +46,11 @@ export class PowerCalculator { physicalProcessorName: string, simulatedProcessorName: string ) { - const physicalPowerCurveDataTable = this.prodNameToDataTable.get( + const physicalPowerCurveDataTable = this.getPowerCurveDataTableOrThrowError( physicalProcessorName ); - if (physicalPowerCurveDataTable === undefined) { - throw new Error(`no power curve available for ${physicalProcessorName}`); - } - const simulatedPowerCurveDataTable = this.prodNameToDataTable.get( - simulatedProcessorName - ); - if (simulatedPowerCurveDataTable === undefined) { - throw new Error( - `no power curve available for ${simulatedPowerCurveDataTable}` - ); - } + const simulatedPowerCurveDataTable = + this.getPowerCurveDataTableOrThrowError(simulatedProcessorName); // interpolate TP const physicalThrouputSeries = physicalPowerCurveDataTable.getColumnData( this.THROUGHPUT_COL_NAME @@ -113,4 +103,14 @@ export class PowerCalculator { } return prodNameToDataTable; } + + private getPowerCurveDataTableOrThrowError(prodName: string): DataTable { + const powerCurveDataTable = this.prodNameToDataTable.get(prodName); + if (powerCurveDataTable === undefined) { + throw new InputValidationError( + `no power curve available for ${prodName}` + ); + } + return powerCurveDataTable; + } } diff --git a/src/lib/generic-cpu/index.ts b/src/lib/generic-cpu/index.ts index 6a06e6e..ce165e9 100644 --- a/src/lib/generic-cpu/index.ts +++ b/src/lib/generic-cpu/index.ts @@ -10,7 +10,8 @@ export const GenericCPU = (globalConfig: ConfigParams): PluginInterface => { }; /** - * TODO PB - doc + * Calculate CPU energy consumption based on the processor in use and measured CPU utilization values. + * Utilize power curves under the hood */ const execute = async (inputs: PluginParams[]) => { const powerCurvesRootDir = globalConfig['power-curves-root-dir']; diff --git a/src/lib/generic-cpu/models/data-table.ts b/src/lib/generic-cpu/models/data-table.ts index e46f5c6..c1e7c80 100644 --- a/src/lib/generic-cpu/models/data-table.ts +++ b/src/lib/generic-cpu/models/data-table.ts @@ -1,17 +1,20 @@ -// TODO PB -- standard IF errors -// TODO PB -- validate table alignment (same row number for each column) +import {ERRORS} from '../../../util/errors'; +const {InputValidationError} = ERRORS; export class DataTable { private dataMap: Map = new Map(); constructor(data: string[][]) { + this.validateInputDataStructure(data); this.dataMap = this.getValidatedDataMap(data); } getColumnData(columnName: string): number[] { const colData = this.dataMap.get(columnName); if (colData === undefined) { - throw new Error(`Column ${columnName} not found in data table`); + throw new InputValidationError( + `Column ${columnName} not found in data table` + ); } return colData; } @@ -20,6 +23,20 @@ export class DataTable { return this.dataMap.size; } + private validateInputDataStructure(data: string[][]) { + if (data === undefined || data.length === 0) { + throw new InputValidationError('input data is undefined or empty'); + } + const expectedRowLength = data[0].length; + data.forEach(row => { + if (row.length !== expectedRowLength) { + throw new InputValidationError( + 'input data misalignment: different row length' + ); + } + }); + } + private getValidatedDataMap(data: string[][]): Map { const dataMap: Map = new Map(); for (let colIndex = 0; colIndex < data[0].length; colIndex++) { @@ -37,7 +54,7 @@ export class DataTable { private getValidatedHeader(headerString: string): string { if (typeof headerString !== 'string' || headerString.trim() === '') { - throw new Error('Invalid header'); + throw new InputValidationError('Invalid header'); } return headerString; } @@ -45,7 +62,7 @@ export class DataTable { private getValidatedNumber(str: string): number { const parsedValue = parseFloat(str); if (isNaN(parsedValue)) { - throw new Error(`Failed to parse '${str}' as a number`); + throw new InputValidationError(`Failed to parse '${str}' as a number`); } return parsedValue; }