Files
dashlibstorage/scripts/benchmark/instrument.js
2026-09-22 08:31:01 +02:00

127 lines
3.5 KiB
JavaScript

import { performance } from 'node:perf_hooks';
export class MetricsCollector {
constructor() {
this.entries = new Map();
}
record(name, durationMs, error = null) {
let entry = this.entries.get(name);
if (!entry) {
entry = {
name,
durations: [],
count: 0,
errors: 0,
totalMs: 0,
};
this.entries.set(name, entry);
}
entry.count += 1;
entry.totalMs += durationMs;
entry.durations.push(durationMs);
if (error) {
entry.errors += 1;
}
}
getStats(name) {
const entry = this.entries.get(name);
if (!entry || entry.count === 0) return null;
const sorted = [...entry.durations].sort((a, b) => a - b);
const count = entry.count;
const totalMs = entry.totalMs;
const avgMs = totalMs / count;
const minMs = sorted[0];
const maxMs = sorted[count - 1];
const p50Index = Math.min(Math.floor(count * 0.5), count - 1);
const p95Index = Math.min(Math.floor(count * 0.95), count - 1);
const p99Index = Math.min(Math.floor(count * 0.99), count - 1);
const p50Ms = sorted[p50Index];
const p95Ms = sorted[p95Index];
const p99Ms = sorted[p99Index];
return {
name,
count,
errors: entry.errors,
totalMs,
avgMs,
minMs,
maxMs,
p50Ms,
p95Ms,
p99Ms,
};
}
getAllStats() {
const results = [];
for (const name of this.entries.keys()) {
results.push(this.getStats(name));
}
return results.sort((a, b) => a.name.localeCompare(b.name));
}
}
/**
* Wraps a LibDashStore instance so all method calls across all namespaces
* are intercepted and measured with high-resolution performance timers.
*
* @param {import('../src/index.js').default} store
* @param {MetricsCollector} metrics
* @returns {import('../src/index.js').default} Instrumented store
*/
export function instrumentStore(store, metrics = new MetricsCollector()) {
const ignoredProperties = new Set(['provider', 'pool', 'constructor']);
for (const [namespaceKey, namespaceVal] of Object.entries(store)) {
if (ignoredProperties.has(namespaceKey)) continue;
// Direct root function like store.purge or store.initialize
if (typeof namespaceVal === 'function') {
const originalFn = namespaceVal.bind(store);
store[namespaceKey] = async (...args) => {
const start = performance.now();
let error = null;
try {
return await originalFn(...args);
} catch (err) {
error = err;
throw err;
} finally {
const duration = performance.now() - start;
metrics.record(`store.${namespaceKey}`, duration, error);
}
};
continue;
}
if (typeof namespaceVal === 'object' && namespaceVal !== null) {
for (const [methodKey, methodVal] of Object.entries(namespaceVal)) {
if (typeof methodVal === 'function') {
const originalMethod = methodVal.bind(namespaceVal);
namespaceVal[methodKey] = async (...args) => {
const start = performance.now();
let error = null;
try {
return await originalMethod(...args);
} catch (err) {
error = err;
throw err;
} finally {
const duration = performance.now() - start;
metrics.record(`${namespaceKey}.${methodKey}`, duration, error);
}
};
}
}
}
}
return { store, metrics };
}