diff --git a/package.json b/package.json index 65644c9..1a9c325 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "build": "pnpm clean && ts-node scripts/build.ts && tsc --emitDeclarationOnly --outDir dist", "clean": "rm -rf dist", "prepublishOnly": "pnpm build", - "release": "release-it" + "release": "release-it", + "start": "node --expose-gc -r ts-node/register src/index.ts" }, "license": "MIT", "devDependencies": { diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..a309420 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,45 @@ +import { Offsets, Options } from "./types"; + +export const OPTIONS = { + cpu: { + chunkSize: 100, + compareSize: 10, + rangePercent: 10, + }, + ram: { + chunkSize: 5, + compareSize: 5, + rangePercent: 5, + }, + general: { + substractSelf: true, + allowGc: true, + } +} satisfies Options; + +export const OFFSETS = { + async: { + cpu: { + min: 0, + max: 0, + median: 0, + }, + ram: { + min: 0, + max: 0, + median: 0, + }, + }, + sync: { + cpu: { + min: 0, + max: 0, + median: 0, + }, + ram: { + min: 0, + max: 0, + median: 0, + }, + } +} satisfies Offsets; diff --git a/src/createStores.ts b/src/createStores.ts new file mode 100644 index 0000000..83c9d77 --- /dev/null +++ b/src/createStores.ts @@ -0,0 +1,27 @@ +import { Options, Stores } from "./types"; + +// memoize store by options and reuse it if possible +export function createStores(options: Options): Stores { + return { + cpu: { + chunk: { + array: new Uint32Array(new ArrayBuffer(options.cpu.chunkSize * 4)), + index: 0 + }, + main: { + array: new Uint32Array(new ArrayBuffer(options.cpu.chunkSize * 4)), + index: 0 + }, + }, + ram: { + chunk: { + array: new Uint32Array(new ArrayBuffer(options.ram.chunkSize * 4)), + index: 0 + }, + main: { + array: new Uint32Array(new ArrayBuffer(options.ram.chunkSize * 4)), + index: 0 + }, + }, + }; +} diff --git a/src/getAllOffsets.ts b/src/getAllOffsets.ts new file mode 100644 index 0000000..d187d85 --- /dev/null +++ b/src/getAllOffsets.ts @@ -0,0 +1,18 @@ +import { getOffset } from "./getOffset"; +import { Offsets, Options, Stores } from "./types"; + +export async function getAllOffsets( + stores: Stores, + options: Options +): Promise { + return { + async: { + cpu: await getOffset({ type: "async", mode: "cpu" }, stores, options), + ram: await getOffset({ type: "async", mode: "ram" }, stores, options), + }, + sync: { + cpu: await getOffset({ type: "sync", mode: "cpu" }, stores, options), + ram: await getOffset({ type: "sync", mode: "ram" }, stores, options), + }, + }; +} diff --git a/src/getCpuStats.ts b/src/getCpuStats.ts new file mode 100644 index 0000000..e9ae5dd --- /dev/null +++ b/src/getCpuStats.ts @@ -0,0 +1,20 @@ +import { getMedian } from "./getMedian"; +import { getMinMax } from "./getMinMax"; +import { positive } from "./positive"; +import { OffsetData, Offsets, Store } from "./types"; + +export function getCpuStats( + { array, index }: Store, + { mode, type }: OffsetData, + offsets: Offsets +) { + const median = getMedian(array, index); + const { min, max } = getMinMax(array, index); + const ctx = offsets[type][mode]; + + return { + median: positive(median - ctx.median), + min: positive(min - ctx.min), + max: positive(max - ctx.max) + }; +} diff --git a/src/getMedian.ts b/src/getMedian.ts new file mode 100644 index 0000000..de583b7 --- /dev/null +++ b/src/getMedian.ts @@ -0,0 +1,9 @@ +export function getMedian( + array: Uint32Array, + length: number +) { + return array + .slice(0, length) + .sort() + .at(Math.floor(length / 2)) as number; +} diff --git a/src/getMinMax.ts b/src/getMinMax.ts new file mode 100644 index 0000000..c7ab22a --- /dev/null +++ b/src/getMinMax.ts @@ -0,0 +1,16 @@ +export function getMinMax( + array: Uint32Array, + length: number +) { + let min = array[0]; + let max = array[0]; + + for(let i = 1; i < length; ++i) { + const value = array[i]; + + if(value < min) min = value; + if(value > max) max = value; + } + + return { min, max }; +} diff --git a/src/getOffset.ts b/src/getOffset.ts new file mode 100644 index 0000000..1151a00 --- /dev/null +++ b/src/getOffset.ts @@ -0,0 +1,50 @@ +import { OFFSETS } from "./constants"; +import { getCpuStats } from "./getCpuStats"; +import { getMedian } from "./getMedian"; +import { getMinMax } from "./getMinMax"; +import { getRamStats } from "./getRamStats"; +import { measure } from "./measure"; +import { OffsetData, Options, Stores } from "./types"; + +// TODO: use "run" function to get rid of duplication +// TODO: run as many times as needed to get to zero +export async function getOffset( + { type, mode }: OffsetData, + stores: Stores, + options: Options +) { + const { chunk, main } = stores[mode]; + const { chunkSize, compareSize, rangePercent } = options[mode]; + const getStats = mode === "cpu" ? getCpuStats: getRamStats; + const fn = type === "async" ? async () => { /* */ }: () => { /* */ }; + + main.index = -1; + chunk.index = -1; + + while(true as any) { + if(chunk.index === chunkSize) { + main.array[++main.index] = getMedian(chunk.array, chunk.index); + chunk.index = -1; + + if(main.index >= compareSize) { + const { min, max } = getMinMax( + main.array.slice(main.index - compareSize), + compareSize + ); + + if((max - (max / 100 * rangePercent)) <= min) { + break; + } + } + } + + if(main.index === chunkSize) { + main.array[0] = getMedian(main.array, main.index); + main.index = 0; + } + + await measure({ fn, mode, store: chunk }, options); + } + + return getStats(main, { mode, type: fn instanceof Promise ? "async": "sync" }, OFFSETS); +} diff --git a/src/getOptions.ts b/src/getOptions.ts new file mode 100644 index 0000000..a4b122d --- /dev/null +++ b/src/getOptions.ts @@ -0,0 +1,23 @@ +import { OPTIONS } from "./constants"; +import { DeepPartial, Options } from "./types"; + +export function getOptions( + partialOptions?: DeepPartial +) { + return { + cpu: { + ...OPTIONS.cpu, + ...(partialOptions?.cpu || {}) + }, + ram: { + ...OPTIONS.ram, + ...(partialOptions?.ram || {}) + }, + general: { + ...OPTIONS.general, + ...(partialOptions?.general || {}) + } + } satisfies Options; +} + + diff --git a/src/getRamStats.ts b/src/getRamStats.ts new file mode 100644 index 0000000..8dec8e0 --- /dev/null +++ b/src/getRamStats.ts @@ -0,0 +1,20 @@ +import { getMedian } from "./getMedian"; +import { getMinMax } from "./getMinMax"; +import { positive } from "./positive"; +import { OffsetData, Offsets, Store } from "./types"; + +export function getRamStats( + { array, index }: Store, + { mode, type }: OffsetData, + offsets: Offsets +) { + const median = getMedian(array, index); + const { min, max } = getMinMax(array, index); + const ctx = offsets[type][mode]; + + return { + median: positive(median - ctx.median), + min: positive(min - ctx.min), + max: positive(max - ctx.max) + }; +} diff --git a/src/index.ts b/src/index.ts index 96c280a..080708c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,13 @@ -export const add = (...n: number[]) => n.reduce((acc, v) => acc + v, 0); +import { preset } from "./preset"; + +const defaultSuite = preset(); +const callibration = defaultSuite({ + emptySync: () => { /* */ }, + emptyAsync: async () => { /* */ }, +}); + +(async () => { + for await (const result of callibration()) { + console.log(result); + } +})(); diff --git a/src/measure.ts b/src/measure.ts new file mode 100644 index 0000000..97eb311 --- /dev/null +++ b/src/measure.ts @@ -0,0 +1,28 @@ +import { MeasureData, Options } from "./types"; + +export async function measure( + { fn, mode, store }: MeasureData, + { general: { allowGc } }: Options +) { + const isAsync = fn instanceof Promise; + + if(mode === "cpu") { + const start = process.hrtime.bigint(); + + isAsync ? (await fn()): fn(); + + const end = process.hrtime.bigint(); + + store.array[++store.index] = Math.round(Number(end - start)); + } else { + allowGc && global.gc?.(); + + const start = process.memoryUsage().heapUsed; + + isAsync ? (await fn()): fn(); + + const end = process.memoryUsage().heapUsed; + + store.array[++store.index] = Math.round(Number(end - start)); + } +} diff --git a/src/positive.ts b/src/positive.ts new file mode 100644 index 0000000..24a8cef --- /dev/null +++ b/src/positive.ts @@ -0,0 +1,3 @@ +export function positive(value: number) { + return value < 0 ? 0: value; +} diff --git a/src/preset.ts b/src/preset.ts new file mode 100644 index 0000000..e350047 --- /dev/null +++ b/src/preset.ts @@ -0,0 +1,33 @@ +import { createStores } from "./createStores"; +import { getAllOffsets } from "./getAllOffsets"; +import { getOptions } from "./getOptions"; +import { runBenchmark } from "./runBenchmark"; +import { DeepPartial, Options, Benchmarks } from "./types"; + +export function preset(partialOptions?: DeepPartial) { + const options = getOptions(partialOptions); + const stores = createStores(options); + + return function createSuite< + $Benchmarks extends Benchmarks + >( + benchmarks: $Benchmarks + ) { + return async function* runSuite() { + const offsets = await getAllOffsets(stores, options); + + console.log(offsets); + + for(const benchmarkName in benchmarks) { + options.general.allowGc && global.gc?.(); + + yield await runBenchmark( + benchmarks[benchmarkName], + stores, + offsets, + options + ); + } + } + } +} diff --git a/src/run.ts b/src/run.ts new file mode 100644 index 0000000..5c783e3 --- /dev/null +++ b/src/run.ts @@ -0,0 +1,50 @@ +import { getCpuStats } from "./getCpuStats"; +import { getMedian } from "./getMedian"; +import { getMinMax } from "./getMinMax"; +import { getRamStats } from "./getRamStats"; +import { measure } from "./measure"; +import { Benchmark, OffsetData, Offsets, Options, Stores } from "./types"; + +export async function run( + fn: Benchmark, + mode: OffsetData["mode"], + stores: Stores, + offsets: Offsets, + options: Options +) { + const { chunk, main } = stores[mode]; + const { chunkSize, compareSize, rangePercent } = options[mode]; + const getStats = mode === "cpu" + ? getCpuStats + : getRamStats; + + main.index = -1; + chunk.index = -1; + + while(true as any) { + if(chunk.index === chunkSize) { + main.array[++main.index] = getMedian(chunk.array, chunk.index); + chunk.index = -1; + + if(main.index >= compareSize) { + const { min, max } = getMinMax( + main.array.slice(main.index - compareSize), + compareSize + ); + + if((max - (max / 100 * rangePercent)) <= min) { + break; + } + } + } + + if(main.index === chunkSize) { + main.array[0] = getMedian(main.array, main.index); + main.index = 0; + } + + await measure({ fn, mode, store: chunk }, options); + } + + return getStats(main, { mode, type: fn instanceof Promise ? "async": "sync" }, offsets); +} diff --git a/src/runBenchmark.ts b/src/runBenchmark.ts new file mode 100644 index 0000000..9ee47a5 --- /dev/null +++ b/src/runBenchmark.ts @@ -0,0 +1,14 @@ +import { run } from "./run"; +import { Benchmark, Offsets, Options, Stores } from "./types"; + +export async function runBenchmark( + benchmark: Benchmark, + stores: Stores, + offsets: Offsets, + options: Options +) { + return { + cpu: await run(benchmark, "cpu", stores, offsets, options), + ram: await run(benchmark, "ram", stores, offsets, options) + }; +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..afb5bfe --- /dev/null +++ b/src/types.ts @@ -0,0 +1,84 @@ +export type Obj< + $Input extends string | number | symbol = string, + $Output = unknown +> = Record<$Input, $Output>; + +export type Options = { + cpu: { + chunkSize: number; + compareSize: number; + rangePercent: number; + }; + ram: { + chunkSize: number; + compareSize: number; + rangePercent: number; + }; + general: { + substractSelf: boolean; + allowGc: boolean; + }; +}; + +export type DeepPartial< + $Object extends Obj +> = Partial<{ + [$Key in keyof $Object]: $Object[$Key] extends Obj + ? Partial<$Object[$Key]> + : $Object[$Key]; +}>; + +export type Fn< + $Input extends unknown[], + $Output +> = (...props: $Input) => $Output; + +export type Either<$Options extends unknown[]> = $Options[number]; + +export type Benchmark = Fn<[], Either<[void, Promise]>>; + +export type Benchmarks = Obj; + +export type Store = { + array: Uint32Array; + index: number; +}; + +export type Stores = { + cpu: { + chunk: Store; + main: Store; + }; + ram: { + chunk: Store; + main: Store; + }; +}; + +export type OffsetData = { + type: "sync" | "async"; + mode: "cpu" | "ram"; +}; + +export type MeasureData = { + fn: Benchmark; + mode: OffsetData["mode"]; + store: Store; +}; + +export type Offset = { + min: number; + max: number; + median: number; +}; + +export type Offsets = { + async: { + cpu: Offset; + ram: Offset; + }, + sync: { + cpu: Offset; + ram: Offset; + }, +};