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): add very basic benchmarking #95

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/benchmark/.mocharc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extension": [
"ts"
],
"spec": "**/*.test.ts",
"require": [
"ts-node/register"
],
"recursive": true
}
2 changes: 2 additions & 0 deletions packages/benchmark/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Benchmark Package
General benchmarking library to return the time in milleseconds between start and stop. Includes stats package to get various analytics.
6 changes: 6 additions & 0 deletions packages/benchmark/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// .eslintrc.js in the new package
module.exports = {
root:true,
extends: ['@repo/eslint-config/library.js'],
};

43 changes: 43 additions & 0 deletions packages/benchmark/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "@repo/benchmark",
"version": "0.0.1",
"description": "",
"main": "index.js",
"scripts": {
"build": "tsc -b",
"build:check": "tsc --noEmit",
"watch": "tsc -b --watch",
"fix": "pnpm format && pnpm lint",
"format": "prettier --write src",
"format:check": "prettier src --check",
"lint": "eslint --fix",
"lint:check": "eslint",
"check": "pnpm format:check && pnpm lint:check && pnpm build:check",
"test": "mocha",
"coverage": "nyc mocha",
"test:watch": "mocha --watch"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
},
"exports": {
".": "./dist/index.js"
},
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/chai": "^4.3.17",
"@types/mocha": "^10.0.7",
"chai": "^4.5.0",
"eslint": "^8.57.0",
"mocha": "^10.7.0",
"nyc": "^17.0.0",
"prettier": "^3.3.3",
"source-map-support": "^0.5.21",
"ts-node": "^10.9.2",
"typescript": "^5.5.4"
}
}
96 changes: 96 additions & 0 deletions packages/benchmark/src/benchmark.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { expect } from "chai";
import { Benchmark } from "./benchmark";
import { BenchmarkStats } from "./stats";

describe("Benchmark", () => {
let benchmark: Benchmark;

beforeEach(() => {
benchmark = new Benchmark();
});

it("should start and end a benchmark event correctly", async () => {
benchmark.start("testEvent", 0);
const duration = benchmark.end("testEvent", 1);
expect(duration).to.be.a("number");
expect(duration).to.be.greaterThan(0);
});

it("should throw an error if end is called without start", () => {
expect(() => benchmark.end("nonExistentEvent")).to.throw(
Error,
'Benchmark for event "nonExistentEvent" not started. Call start() before end().',
);
});

it("should handle multiple events independently", () => {
benchmark.start("event1", 0);
benchmark.start("event2", 0);

const duration1 = benchmark.end("event1", 1);
expect(duration1).to.be.a("number");
expect(duration1).to.be.greaterThan(0);

const duration2 = benchmark.end("event2", 1);
expect(duration2).to.be.a("number");
expect(duration2).to.be.greaterThan(0);
});

it("should throw an error if the same event is started twice without ending", () => {
benchmark.start("duplicateEvent");
expect(() => benchmark.start("duplicateEvent")).to.not.throw();
expect(() => benchmark.end("duplicateEvent")).to.not.throw();
});
});

describe("BenchmarkStats", () => {
let benchmarkStats: BenchmarkStats;

beforeEach(() => {
benchmarkStats = new BenchmarkStats();
});

it("should start and end a benchmark event correctly", () => {
benchmarkStats.start("testEvent", 0);
const duration = benchmarkStats.end("testEvent", 1);
expect(duration).to.be.a("number");
expect(duration).to.be.greaterThan(0);
});

it("should return correct stats for events", () => {
benchmarkStats.start("event1");
benchmarkStats.end("event1");
benchmarkStats.start("event2");
benchmarkStats.end("event2");

const stats = benchmarkStats.getStats();
expect(stats.total).to.equal(2);
expect(stats.oldest).to.be.a("number");
expect(stats.newest).to.be.a("number");
expect(stats.average).to.be.a("number");
expect(stats.fastest).to.be.a("number");
expect(stats.slowest).to.be.a("number");
});

it("should handle events with specific integer timestamps correctly", () => {
const startTime1 = 1000;
const endTime1 = 2000;
const startTime2 = 3000;
const endTime2 = 4000;

benchmarkStats.start("event1", startTime1);
benchmarkStats.end("event1", endTime1);
benchmarkStats.start("event2", startTime2);
benchmarkStats.end("event2", endTime2);

const stats = benchmarkStats.getStats();
expect(stats.total).to.equal(2);
expect(stats.oldest).to.equal(endTime1 - startTime1);
expect(stats.newest).to.equal(endTime2 - startTime2);
expect(stats.average).to.equal(
(endTime1 - startTime1 + endTime2 - startTime2) / 2,
);
expect(stats.fastest).to.equal(endTime1 - startTime1);
expect(stats.slowest).to.equal(endTime2 - startTime2);
});
});
46 changes: 46 additions & 0 deletions packages/benchmark/src/benchmark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* A class to benchmark events by tracking their start and end times.
*/
import { IBenchmark } from "./types";

export class Benchmark implements IBenchmark {
private events: Map<string, number>;

/**
* Initializes a new instance of the Benchmark class.
*/
constructor() {
this.events = new Map();
}

/**
* Starts tracking an event by storing its start time.
*
* @param {string} eventName - The name of the event to start tracking.
* @param {number} [now=Date.now()] - The current time in milliseconds. Defaults to the current time.
*/
start(eventName: string, now: number = Date.now()): void {
this.events.set(eventName, now);
}

/**
* Ends tracking an event and calculates its duration.
*
* @param {string} eventName - The name of the event to end tracking.
* @param {number} [now=Date.now()] - The current time in milliseconds. Defaults to the current time.
* @returns {number | undefined} The duration of the event in milliseconds, or undefined if the event was not started.
* @throws Will throw an error if the event was not started before calling this method.
*/
end(eventName: string, now: number = Date.now()): number | undefined {
const startTime = this.events.get(eventName);
if (startTime === undefined) {
throw new Error(
`Benchmark for event "${eventName}" not started. Call start() before end().`,
);
}
const endTime = now;
const duration = endTime - startTime;
this.events.delete(eventName);
return duration;
}
}
3 changes: 3 additions & 0 deletions packages/benchmark/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./benchmark";
export * from "./stats";
export * from "./types";
86 changes: 86 additions & 0 deletions packages/benchmark/src/stats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Benchmark } from "./benchmark";

import { IBenchmark } from "./types";

export class BenchmarkStats implements IBenchmark {
private benchmark: Benchmark;
private eventDurations: Map<string, number>;

constructor(benchmark: Benchmark = new Benchmark()) {
this.benchmark = benchmark;
this.eventDurations = new Map();
}

/**
* Starts a new benchmark event.
* @param {string} eventName - The name of the event to start.
*/
start(eventName: string, now: number = Date.now()): void {
this.benchmark.start(eventName, now);
}

/**
* Ends a benchmark event and records its duration.
* @param {string} eventName - The name of the event to stop.
* @returns {number | undefined} The duration of the event in milliseconds, or undefined if the event was not started.
*/
end(eventName: string, now: number = Date.now()): number | undefined {
const duration = this.benchmark.end(eventName, now);
if (duration !== undefined) {
this.eventDurations.set(eventName, duration);
}
return duration;
}

/**
* Provides statistics about the currently tracked events.
*
* @returns {object} An object containing statistics about the events.
*/
getStats(): {
total: number;
oldest: number | null;
newest: number | null;
average: number | null;
fastest: number | null;
slowest: number | null;
} {
const total = this.eventDurations.size;

if (total === 0) {
return {
total,
oldest: null,
newest: null,
average: null,
fastest: null,
slowest: null,
};
}

let oldest = Number.MAX_VALUE;
let newest = Number.MIN_VALUE;
let totalDuration = 0;
let fastest = Number.MAX_VALUE;
let slowest = Number.MIN_VALUE;

for (const duration of this.eventDurations.values()) {
totalDuration += duration;
if (duration < fastest) fastest = duration;
if (duration > slowest) slowest = duration;
if (duration < oldest) oldest = duration;
if (duration > newest) newest = duration;
}

const average = totalDuration / total;

return {
total,
oldest,
newest,
average,
fastest,
slowest,
};
}
}
19 changes: 19 additions & 0 deletions packages/benchmark/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export interface IBenchmark {
/**
* Starts tracking an event by storing its start time.
*
* @param {string} eventName - The name of the event to start tracking.
* @param {number} [now=Date.now()] - The current time in milliseconds. Defaults to the current time.
*/
start(eventName: string, now?: number): void;

/**
* Ends tracking an event and calculates its duration.
*
* @param {string} eventName - The name of the event to end tracking.
* @param {number} [now=Date.now()] - The current time in milliseconds. Defaults to the current time.
* @returns {number | undefined} The duration of the event in milliseconds, or undefined if the event was not started.
* @throws Will throw an error if the event was not started before calling this method.
*/
end(eventName: string, now?: number): number | undefined;
}
6 changes: 6 additions & 0 deletions packages/benchmark/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends":"@repo/typescript-config/base.json",
"compilerOptions": {
"outDir": "./dist" /* Specify an output folder for all emitted files. */
}
}
1 change: 1 addition & 0 deletions packages/indexer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@across-protocol/constants": "^3.1.20",
"@across-protocol/contracts": "^3.0.16",
"@across-protocol/sdk": "^3.3.23",
"@repo/benchmark": "workspace:*",
"@repo/error-handling": "workspace:*",
"@repo/webhooks": "workspace:*",
"@types/express": "^4.17.21",
Expand Down
Loading
Loading