132 lines
3.9 KiB
TypeScript
132 lines
3.9 KiB
TypeScript
/**
|
|
* Opt-in performance harness. Nothing in src/ imports this file, so the app
|
|
* runs without it. Load it with `yarn perf` (or `node --import ./tools/perf-hooks.mjs`).
|
|
*/
|
|
import mongoose from "mongoose";
|
|
import { monitorEventLoopDelay, performance } from "node:perf_hooks";
|
|
import express from "express";
|
|
|
|
type Metric = {
|
|
count: number;
|
|
totalMs: number;
|
|
maxMs: number;
|
|
over100Ms: number;
|
|
};
|
|
|
|
type RequestMetric = {
|
|
count: number;
|
|
totalMs: number;
|
|
maxMs: number;
|
|
over500Ms: number;
|
|
};
|
|
|
|
const dbMetrics = new Map<string, Metric>();
|
|
const requestMetrics = new Map<string, RequestMetric>();
|
|
|
|
const eventLoop = monitorEventLoopDelay({ resolution: 20 });
|
|
eventLoop.enable();
|
|
|
|
// --- Database timings -------------------------------------------------------
|
|
// Mongoose emits these for every query, so no route code has to be touched.
|
|
export function register() {
|
|
mongoose.connection.on('connected', () => {
|
|
const client = mongoose.connection.getClient();
|
|
|
|
client.on('commandStarted', (event: any) => {
|
|
(event as any).__startedAt = performance.now();
|
|
});
|
|
|
|
client.on('commandSucceeded', (event: any) => {
|
|
recordDb(`${event.commandName}.${Object.keys(event.command ?? {})[0] ?? 'unknown'}`, event);
|
|
});
|
|
|
|
client.on('commandFailed', (event: any) => {
|
|
recordDb(`${event.commandName}.failed`, event);
|
|
});
|
|
});
|
|
}
|
|
|
|
function recordDb(label: string, event: any) {
|
|
const startedAt = event.__startedAt;
|
|
if (typeof startedAt !== 'number') return;
|
|
|
|
const durationMs = performance.now() - startedAt;
|
|
|
|
let metric = dbMetrics.get(label);
|
|
if (!metric) {
|
|
metric = { count: 0, totalMs: 0, maxMs: 0, over100Ms: 0 };
|
|
dbMetrics.set(label, metric);
|
|
}
|
|
|
|
metric.count++;
|
|
metric.totalMs += durationMs;
|
|
metric.maxMs = Math.max(metric.maxMs, durationMs);
|
|
if (durationMs > 100) metric.over100Ms++;
|
|
}
|
|
|
|
// --- Request timings --------------------------------------------------------
|
|
// Installed by patching express.json() from the loader, so index.ts stays clean.
|
|
export function requestTimer() {
|
|
const originalJson = express.json.bind(express);
|
|
|
|
express.json = ((...args: any[]) => {
|
|
const parser = (originalJson as any)(...args);
|
|
return (req: any, res: any, next: any) => {
|
|
const started = performance.now();
|
|
|
|
res.on("finish", () => {
|
|
const durationMs = performance.now() - started;
|
|
const key = `${req.method} ${req.route?.path ?? req.path} ${res.statusCode}`;
|
|
|
|
let metric = requestMetrics.get(key);
|
|
if (!metric) {
|
|
metric = { count: 0, totalMs: 0, maxMs: 0, over500Ms: 0 };
|
|
requestMetrics.set(key, metric);
|
|
}
|
|
const current = metric;
|
|
|
|
current.count++;
|
|
current.totalMs += durationMs;
|
|
current.maxMs = Math.max(current.maxMs, durationMs);
|
|
if (durationMs > 500) current.over500Ms++;
|
|
});
|
|
|
|
parser(req, res, next);
|
|
};
|
|
}) as typeof express.json;
|
|
}
|
|
|
|
// --- Reporting --------------------------------------------------------------
|
|
function summarize<T extends { count: number; totalMs: number; maxMs: number }>(entries: [string, T][]) {
|
|
return Object.fromEntries(
|
|
entries.map(([key, value]) => [
|
|
key,
|
|
{
|
|
count: value.count,
|
|
avgMs: Number((value.totalMs / value.count).toFixed(2)),
|
|
maxMs: Number(value.maxMs.toFixed(2)),
|
|
},
|
|
]),
|
|
);
|
|
}
|
|
|
|
setInterval(() => {
|
|
if (requestMetrics.size === 0 && dbMetrics.size === 0) return;
|
|
|
|
console.log(JSON.stringify({
|
|
type: "performance_summary",
|
|
timestamp: new Date().toISOString(),
|
|
requests: summarize([...requestMetrics.entries()]),
|
|
database: summarize([...dbMetrics.entries()]),
|
|
eventLoopDelayMs: {
|
|
mean: Number((eventLoop.mean / 1e6 || 0).toFixed(2)),
|
|
p95: Number((eventLoop.percentile(95) / 1e6).toFixed(2)),
|
|
max: Number((eventLoop.max / 1e6).toFixed(2)),
|
|
},
|
|
}));
|
|
|
|
requestMetrics.clear();
|
|
dbMetrics.clear();
|
|
eventLoop.reset();
|
|
}, 10_000).unref();
|