tests: use custom benchmarking solution instead of outdated jest-bench package (#4468)
* Remove jest-bench dependency and use custom solutions * review * Update tests/bench/calculateRank.bench.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Alexandr <qwerty541zxc@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
8 files changed
+157
-303
No files matched your search
@@ -1,8 +1,8 @@
|
||||
import { benchmarkSuite } from "jest-bench";
|
||||
import api from "../../api/index.js";
|
||||
import axios from "axios";
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
import { jest } from "@jest/globals";
|
||||
import { it, jest } from "@jest/globals";
|
||||
import { runAndLogStats } from "./utils.js";
|
||||
|
||||
const stats = {
|
||||
name: "Anurag Hazra",
|
||||
@@ -69,10 +69,10 @@ const faker = (query, data) => {
|
||||
return { req, res };
|
||||
};
|
||||
|
||||
benchmarkSuite("test /api", {
|
||||
["simple request"]: async () => {
|
||||
it("test /api", async () => {
|
||||
await runAndLogStats("test /api", async () => {
|
||||
const { req, res } = faker({}, data_stats);
|
||||
|
||||
await api(req, res);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,9 @@
|
||||
import { benchmarkSuite } from "jest-bench";
|
||||
import { calculateRank } from "../../src/calculateRank.js";
|
||||
import { it } from "@jest/globals";
|
||||
import { runAndLogStats } from "./utils.js";
|
||||
|
||||
benchmarkSuite("calculateRank", {
|
||||
["calculateRank"]: () => {
|
||||
it("calculateRank", async () => {
|
||||
await runAndLogStats("calculateRank", () => {
|
||||
calculateRank({
|
||||
all_commits: false,
|
||||
commits: 1300,
|
||||
@@ -13,5 +14,5 @@ benchmarkSuite("calculateRank", {
|
||||
stars: 600000,
|
||||
followers: 50000,
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
import { benchmarkSuite } from "jest-bench";
|
||||
import gist from "../../api/gist.js";
|
||||
import axios from "axios";
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
import { jest } from "@jest/globals";
|
||||
import { it, jest } from "@jest/globals";
|
||||
import { runAndLogStats } from "./utils.js";
|
||||
|
||||
const gist_data = {
|
||||
data: {
|
||||
@@ -34,8 +34,8 @@ const gist_data = {
|
||||
const mock = new MockAdapter(axios);
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, gist_data);
|
||||
|
||||
benchmarkSuite("test /api/gist", {
|
||||
["simple request"]: async () => {
|
||||
it("test /api/gist", async () => {
|
||||
await runAndLogStats("test /api/gist", async () => {
|
||||
const req = {
|
||||
query: {
|
||||
id: "bbfce31e0217a3689c8d961a356cb10d",
|
||||
@@ -47,5 +47,5 @@ benchmarkSuite("test /api/gist", {
|
||||
};
|
||||
|
||||
await gist(req, res);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
import { benchmarkSuite } from "jest-bench";
|
||||
import pin from "../../api/pin.js";
|
||||
import axios from "axios";
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
import { jest } from "@jest/globals";
|
||||
import { it, jest } from "@jest/globals";
|
||||
import { runAndLogStats } from "./utils.js";
|
||||
|
||||
const data_repo = {
|
||||
repository: {
|
||||
@@ -32,8 +32,8 @@ const data_user = {
|
||||
const mock = new MockAdapter(axios);
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
|
||||
|
||||
benchmarkSuite("test /api/pin", {
|
||||
["simple request"]: async () => {
|
||||
it("test /api/pin", async () => {
|
||||
await runAndLogStats("test /api/pin", async () => {
|
||||
const req = {
|
||||
query: {
|
||||
username: "anuraghazra",
|
||||
@@ -46,5 +46,5 @@ benchmarkSuite("test /api/pin", {
|
||||
};
|
||||
|
||||
await pin(req, res);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
const DEFAULT_RUNS = 1000;
|
||||
const DEFAULT_WARMUPS = 50;
|
||||
|
||||
/**
|
||||
* Formats a duration in nanoseconds to a compact human-readable string.
|
||||
* @param {bigint} ns Duration in nanoseconds.
|
||||
* @returns {string} Formatted time string.
|
||||
*/
|
||||
const formatTime = (ns) => {
|
||||
if (ns < 1_000n) {
|
||||
return `${ns}ns`;
|
||||
}
|
||||
if (ns < 1_000_000n) {
|
||||
return `${Number(ns) / 1_000}µs`;
|
||||
}
|
||||
if (ns < 1_000_000_000n) {
|
||||
return `${(Number(ns) / 1_000_000).toFixed(3)}ms`;
|
||||
}
|
||||
return `${(Number(ns) / 1_000_000_000).toFixed(3)}s`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Measures synchronous or async function execution time.
|
||||
* @param {Function} fn Function to measure.
|
||||
* @returns {Promise<bigint>} elapsed nanoseconds
|
||||
*/
|
||||
const measurePerformance = async (fn) => {
|
||||
const start = process.hrtime.bigint();
|
||||
const ret = fn();
|
||||
if (ret instanceof Promise) {
|
||||
await ret;
|
||||
}
|
||||
const end = process.hrtime.bigint();
|
||||
return end - start;
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes basic & extended statistics.
|
||||
* @param {bigint[]} samples Array of samples in nanoseconds.
|
||||
* @returns {object} Stats
|
||||
*/
|
||||
const computeStats = (samples) => {
|
||||
const sorted = [...samples].sort((a, b) => (a < b ? -1 : 1));
|
||||
const toNumber = (b) => Number(b); // safe for typical short benches
|
||||
const n = sorted.length;
|
||||
const sum = sorted.reduce((a, b) => a + b, 0n);
|
||||
const avg = Number(sum) / n;
|
||||
const median =
|
||||
n % 2
|
||||
? toNumber(sorted[(n - 1) / 2])
|
||||
: (toNumber(sorted[n / 2 - 1]) + toNumber(sorted[n / 2])) / 2;
|
||||
const p = (q) => {
|
||||
const idx = Math.min(n - 1, Math.floor((q / 100) * n));
|
||||
return toNumber(sorted[idx]);
|
||||
};
|
||||
const min = toNumber(sorted[0]);
|
||||
const max = toNumber(sorted[n - 1]);
|
||||
const variance =
|
||||
sorted.reduce((acc, v) => acc + (toNumber(v) - avg) ** 2, 0) / n;
|
||||
const stdev = Math.sqrt(variance);
|
||||
|
||||
return {
|
||||
runs: n,
|
||||
min,
|
||||
max,
|
||||
average: avg,
|
||||
median,
|
||||
p75: p(75),
|
||||
p95: p(95),
|
||||
p99: p(99),
|
||||
stdev,
|
||||
totalTime: toNumber(sum),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Benchmark a function.
|
||||
* @param {string} fnName Name of the function (for logging).
|
||||
* @param {Function} fn Function to benchmark.
|
||||
* @param {object} [opts] Options.
|
||||
* @param {number} [opts.runs] Number of measured runs.
|
||||
* @param {number} [opts.warmup] Warm-up iterations (not measured).
|
||||
* @param {boolean} [opts.trimOutliers] Drop top & bottom 1% before stats.
|
||||
* @returns {Promise<object>} Stats (nanoseconds for core metrics).
|
||||
*/
|
||||
export const runAndLogStats = async (
|
||||
fnName,
|
||||
fn,
|
||||
{ runs = DEFAULT_RUNS, warmup = DEFAULT_WARMUPS, trimOutliers = false } = {},
|
||||
) => {
|
||||
if (runs <= 0) {
|
||||
throw new Error("Number of runs must be positive.");
|
||||
}
|
||||
|
||||
// Warm-up
|
||||
for (let i = 0; i < warmup; i++) {
|
||||
const ret = fn();
|
||||
if (ret instanceof Promise) {
|
||||
await ret;
|
||||
}
|
||||
}
|
||||
|
||||
const samples = [];
|
||||
for (let i = 0; i < runs; i++) {
|
||||
samples.push(await measurePerformance(fn));
|
||||
}
|
||||
|
||||
let processed = samples;
|
||||
if (trimOutliers && samples.length > 10) {
|
||||
const sorted = [...samples].sort((a, b) => (a < b ? -1 : 1));
|
||||
const cut = Math.max(1, Math.floor(sorted.length * 0.01));
|
||||
processed = sorted.slice(cut, sorted.length - cut);
|
||||
}
|
||||
|
||||
const stats = computeStats(processed);
|
||||
|
||||
const fmt = (ns) => formatTime(BigInt(Math.round(ns)));
|
||||
console.log(
|
||||
`${fnName} | runs=${stats.runs} avg=${fmt(stats.average)} median=${fmt(
|
||||
stats.median,
|
||||
)} p95=${fmt(stats.p95)} min=${fmt(stats.min)} max=${fmt(
|
||||
stats.max,
|
||||
)} stdev=${fmt(stats.stdev)}`,
|
||||
);
|
||||
|
||||
return stats;
|
||||
};
|
||||
Reference in new issue
Block a user