Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Benchmark and improve performance. #359

Merged
merged 9 commits into from
May 25, 2021
14 changes: 14 additions & 0 deletions benchmark/benchmarks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { BenchmarkCase } from './types/BenchmarkCase';
import * as compareLargeArrays from './cases/compareLargeArrays';
import * as compareLargeObjects from './cases/compareLargeObjects';
import * as compareLongStrings from './cases/compareLongStrings';

const benchmarks: BenchmarkCase[] = [
compareLargeArrays,
compareLargeObjects,
compareLongStrings
];

export {
benchmarks
};
39 changes: 39 additions & 0 deletions benchmark/cases/compareLargeArrays.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { BenchmarkFunction } from '../types/BenchmarkFunction';
import { BenchmarkSetup } from '../types/BenchmarkSetup';
import { compare } from '../../lib/comparisons/typeAware/compare';
import { randomObject } from 'zufall';

const name = 'compare large arrays';

let testArray: any[] = [];

const setup: BenchmarkSetup = function ({ scale }): void {
testArray = Array.from({ length: scale });

for (let i = 0; i < scale; i++) {
testArray[i] = randomObject();
}
};

const benchmark: BenchmarkFunction = async function (): Promise<void> {
compare(testArray, testArray);
};

const scales = [
100,
200,
300,
400,
500,
600,
1_000
];
const unit = 'array items';

export {
benchmark,
name,
scales,
setup,
unit
};
33 changes: 33 additions & 0 deletions benchmark/cases/compareLargeObjects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { BenchmarkFunction } from '../types/BenchmarkFunction';
import { BenchmarkSetup } from '../types/BenchmarkSetup';
import { compare } from '../../lib/comparisons/typeAware/compare';
import { randomObjectWithDepth } from 'zufall';

const name = 'compare large objects';

let testObjectLeft: any = {};
let testObjectRight: any = {};

const getNewTestObject = function ({ scale }: { scale: number }): any {
return randomObjectWithDepth(3, Math.round(scale ** (1 / 3)));
};

const setup: BenchmarkSetup = function ({ scale }): void {
testObjectLeft = getNewTestObject({ scale });
testObjectRight = getNewTestObject({ scale });
};

const benchmark: BenchmarkFunction = async function (): Promise<void> {
compare(testObjectLeft, testObjectRight);
};

const scales = [ 100, 200, 300, 400, 500, 600, 1_000 ];
const unit = 'object keys';

export {
benchmark,
name,
scales,
setup,
unit
};
50 changes: 50 additions & 0 deletions benchmark/cases/compareLongStrings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { BenchmarkFunction } from '../types/BenchmarkFunction';
import { BenchmarkSetup } from '../types/BenchmarkSetup';
import { compare } from '../../lib/comparisons/typeAware/compare';
import { randomWord } from 'zufall';

const name = 'compare long strings';

let testStringLeft = '';
let testStringRight = '';

const getNewTestString = function ({ scale }: { scale: number }): string {
let testString = '';

for (let i = 0; i < scale; i++) {
testString += randomWord();
if (Math.random() > 0.9) {
testString += '\n';
}
}

return testString;
};

const setup: BenchmarkSetup = function ({ scale }): void {
testStringLeft = getNewTestString({ scale });
testStringRight = getNewTestString({ scale });
};

const benchmark: BenchmarkFunction = async function (): Promise<void> {
compare(testStringLeft, testStringRight);
};

const scales = [
10,
20,
30,
40,
50,
60,
100
];
const unit = 'characters';

export {
benchmark,
name,
scales,
setup,
unit
};
51 changes: 51 additions & 0 deletions benchmark/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/* eslint-disable no-console */
import { benchmarks } from './benchmarks';
import { calculateLinearRegressionDeviations } from './util/calculateLinearRegressionDeviations';
import { formatResults } from './util/formatResults';
import fs from 'fs';
import path from 'path';
import { runAveragedBenchmark } from './util/runAveragedBenchmark';

// eslint-disable-next-line @typescript-eslint/no-floating-promises
(async (): Promise<void> => {
console.log('Running benchmarks.');

let benchmarkOutput = 'Benchmarks Results\n\n';

try {
for (const benchmark of benchmarks) {
console.log(`Running benchmark '${benchmark.name}'`);
benchmarkOutput += `Benchmark ${benchmark.name}:\n`;

const results = [];

for (const scale of benchmark.scales) {
console.log(` Scale ${scale}`);
const { averageTime, deviation } = await runAveragedBenchmark({
benchmark,
scale,
howManyTimes: 20
});

results.push({
scale,
unit: benchmark.unit,
time: averageTime,
deviation
});
}

const resultsWithDeviations = calculateLinearRegressionDeviations({ benchmarkResults: results });
const formattedResults = formatResults({ results: resultsWithDeviations, precision: 2 });

console.log(formattedResults);
benchmarkOutput += formattedResults;
benchmarkOutput += '\n';
}
} catch (ex: unknown) {
console.log('An exception occured during benchmarks.', { ex });
}

await fs.promises.writeFile(path.join(__dirname, '..', 'benchmark_output.txt'), benchmarkOutput, 'utf-8');
})();
/* eslint-enable no-console */
16 changes: 16 additions & 0 deletions benchmark/types/BenchmarkCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { BenchmarkFunction } from './BenchmarkFunction';
import { BenchmarkSetup } from './BenchmarkSetup';
import { BenchmarkTeardown } from './BenchmarkTeardown';

interface BenchmarkCase {
name: string;
unit: string;
benchmark: BenchmarkFunction;
scales: number[];
setup?: BenchmarkSetup;
teardown?: BenchmarkTeardown;
}

export type {
BenchmarkCase
};
5 changes: 5 additions & 0 deletions benchmark/types/BenchmarkFunction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type BenchmarkFunction = (parameters: { scale: number }) => Promise<void>;

export type {
BenchmarkFunction
};
10 changes: 10 additions & 0 deletions benchmark/types/BenchmarkResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
interface BenchmarkResult {
scale: number;
unit: string;
time: number;
deviation: number;
}

export type {
BenchmarkResult
};
9 changes: 9 additions & 0 deletions benchmark/types/BenchmarkResultWithRegressionDeviation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { BenchmarkResult } from './BenchmarkResult';

interface BenchmarkResultWithRegressionDeviation extends BenchmarkResult {
regressionDeviation: number;
}

export type {
BenchmarkResultWithRegressionDeviation
};
5 changes: 5 additions & 0 deletions benchmark/types/BenchmarkSetup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type BenchmarkSetup = (parameters: { scale: number }) => void | Promise<void>;

export type {
BenchmarkSetup
};
5 changes: 5 additions & 0 deletions benchmark/types/BenchmarkTeardown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type BenchmarkTeardown = (parameters: { scale: number }) => void | Promise<void>;

export type {
BenchmarkTeardown
};
15 changes: 15 additions & 0 deletions benchmark/util/calculateLinearRegressionDeviations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { BenchmarkResult } from '../types/BenchmarkResult';
import { BenchmarkResultWithRegressionDeviation } from '../types/BenchmarkResultWithRegressionDeviation';

const calculateLinearRegressionDeviations = function ({ benchmarkResults }: {
benchmarkResults: BenchmarkResult[];
}): BenchmarkResultWithRegressionDeviation[] {
return benchmarkResults.map((result): BenchmarkResultWithRegressionDeviation => ({
...result,
regressionDeviation: (result.time / (result.scale * (benchmarkResults[0].time / benchmarkResults[0].scale))) - 1
}));
};

export {
calculateLinearRegressionDeviations
};
66 changes: 66 additions & 0 deletions benchmark/util/formatResults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { BenchmarkResultWithRegressionDeviation } from '../types/BenchmarkResultWithRegressionDeviation';
import { oneLine } from 'common-tags';

const formatLine = function ({
scale, unit, time, deviation, regressionDeviation, scaleLength, unitLength, timeLength, precision
}: {
scale: number;
unit: string;
time: number;
deviation: number;
regressionDeviation: number;
scaleLength: number;
unitLength: number;
timeLength: number;
precision: number;
}): string {
return oneLine`
${String(scale).padStart(scaleLength)}${(unit ? ` ${unit}` : '').padStart(unitLength + 1)}:
${String(time.toFixed(2)).padStart(timeLength + 3)}ms
[+/-${(Math.abs(deviation) * 100).toFixed(precision)}%]
[${regressionDeviation >= 0 ? '+' : ''}${(regressionDeviation * 100).toFixed(precision)}%]
`;
};

const formatResults = function ({ results, precision }: {
results: BenchmarkResultWithRegressionDeviation[];
precision: number;
}): string {
const scaleLength = results.reduce((length, next): number => {
const nextLength = String(next.scale).length;

return nextLength > length ? nextLength : length;
}, 0);
const unitLength = results.reduce((length, next): number => {
const nextLength = String(next.unit || '').length;

return nextLength > length ? nextLength : length;
}, 0);
const timeLength = results.reduce((length, next): number => {
const nextLength = String(Math.floor(next.time)).length;

return nextLength > length ? nextLength : length;
}, 0);

let formattedOutput = '';

for (const { scale, unit, time, deviation, regressionDeviation } of results) {
formattedOutput += `${formatLine({
scale,
unit,
time,
deviation,
regressionDeviation,
scaleLength,
unitLength,
timeLength,
precision
})}\n`;
}

return formattedOutput;
};

export {
formatResults
};
49 changes: 49 additions & 0 deletions benchmark/util/runAveragedBenchmark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { BenchmarkCase } from '../types/BenchmarkCase';
import { measureTime } from 'measure-time';

const runAveragedBenchmark = async function ({ benchmark, scale, howManyTimes }: {
benchmark: BenchmarkCase;
scale: number;
howManyTimes: number;
}): Promise<{ averageTime: number; deviation: number }> {
const results = [];

for (let i = 0; i < howManyTimes; i++) {
// eslint-disable-next-line no-console
console.log(` Average run ${i}`);

// eslint-disable-next-line mocha/no-top-level-hooks
await benchmark.setup?.({ scale });

const getMeasuredTime = measureTime();

await benchmark.benchmark({ scale });

const times = getMeasuredTime();

// eslint-disable-next-line mocha/no-top-level-hooks
await benchmark.teardown?.({ scale });

results.push(times.millisecondsTotal);
}

const averageTime = results.reduce((acc, next): number => acc + next, 0) / results.length,
deviation = results.reduce((maxDeviation, next): number => {
const currentDeviation = (next / averageTime) - 1;

if (Math.abs(currentDeviation) > Math.abs(maxDeviation)) {
return currentDeviation;
}

return maxDeviation;
}, 0);

return {
averageTime,
deviation
};
};

export {
runAveragedBenchmark
};
Loading