bench
This commit is contained in:
126
scripts/benchmark/instrument.js
Normal file
126
scripts/benchmark/instrument.js
Normal file
@@ -0,0 +1,126 @@
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user