knip
Version:
Find and fix unused dependencies, exports and files in your TypeScript and JavaScript projects
176 lines (175 loc) • 6.68 kB
JavaScript
import { PerformanceObserver, performance } from 'node:perf_hooks';
import { memoryUsage } from 'node:process';
import { parseArgs } from 'node:util';
import { getStats } from "./math.js";
import { Table } from "./table.js";
const { values } = parseArgs({
strict: false,
options: {
performance: { type: 'boolean' },
'performance-fn': { type: 'string', multiple: true },
memory: { type: 'boolean' },
'memory-realtime': { type: 'boolean' },
},
});
const timerifyOnlyFnName = values['performance-fn'];
const isMemoryRealtime = !!values['memory-realtime'];
const isTimerifyFunctions = !!values.performance || !!timerifyOnlyFnName;
const isMemoryUsageEnabled = !!values.memory || isMemoryRealtime;
export const timerify = (fn, name = fn.name) => {
if (!isTimerifyFunctions)
return fn;
if (timerifyOnlyFnName && !timerifyOnlyFnName.includes(name))
return fn;
return performance.timerify(Object.defineProperty(fn, 'name', { get: () => name }));
};
const getMemInfo = (label) => {
const usage = memoryUsage();
return { label, heapUsed: usage.heapUsed, heapTotal: usage.heapTotal, rss: usage.rss };
};
const twoFixed = (value) => (typeof value === 'number' ? value.toFixed(2) : value);
const inMB = (bytes) => bytes / 1024 / 1024;
const keys = ['heapUsed', 'heapTotal', 'rss'];
const logHead = () => console.log(['phase', ...keys].map(key => key.padStart(10)).join(' '));
const log = (memInfo) => console.log([memInfo.label.padStart(10), ...keys.map(key => twoFixed(inMB(memInfo[key])).padStart(10))].join(' '));
class Performance {
isEnabled;
isTimerifyFunctions;
isMemoryUsageEnabled;
startTime = 0;
endTime = 0;
perfEntries = [];
memEntries = [];
perfId;
memId;
fnObserver;
memObserver;
constructor({ isTimerifyFunctions = false, isMemoryUsageEnabled = false }) {
this.isEnabled = isTimerifyFunctions || isMemoryUsageEnabled;
this.isTimerifyFunctions = isTimerifyFunctions;
this.isMemoryUsageEnabled = isMemoryUsageEnabled;
this.startTime = performance.now();
const instanceId = Math.floor(performance.now() * 100);
this.perfId = `perf-${instanceId}`;
this.memId = `mem-${instanceId}`;
if (isTimerifyFunctions) {
this.fnObserver = new PerformanceObserver(items => {
for (const entry of items.getEntries()) {
this.perfEntries.push(entry);
}
});
this.fnObserver.observe({ type: 'function' });
}
if (isMemoryUsageEnabled) {
this.memObserver = new PerformanceObserver(items => {
for (const entry of items.getEntries()) {
this.memEntries.push(entry);
}
});
this.memObserver.observe({ type: 'mark' });
if (isMemoryRealtime)
logHead();
this.addMemoryMark('start');
}
}
setMark(name) {
const id = `${this.perfId}:${name}`;
performance.mark(`${id}:start`);
}
clearMark(name) {
const id = `${this.perfId}:${name}`;
performance.mark(`${id}:end`);
performance.measure(id, `${id}:start`, `${id}:end`);
performance.clearMarks(`${id}:start`);
performance.clearMarks(`${id}:end`);
}
async flush() {
this.setMark('_flush');
await new Promise(resolve => setTimeout(resolve, 1));
this.clearMark('_flush');
}
getPerfEntriesByName() {
return this.perfEntries.reduce((entries, entry) => {
const name = entry.name.replace(`${this.perfId}:`, '');
entries[name] = entries[name] ?? [];
entries[name].push(entry.duration);
return entries;
}, {});
}
getTimerifiedFunctionsTable() {
const entriesByName = this.getPerfEntriesByName();
const totalDuration = this.getCurrentDurationInMs();
const table = new Table({ header: true });
for (const [name, values] of Object.entries(entriesByName)) {
const stats = getStats(values);
table.row();
table.cell('Name', name);
table.cell('size', values.length);
table.cell('min', stats.min, twoFixed);
table.cell('max', stats.max, twoFixed);
table.cell('median', stats.median, twoFixed);
table.cell('sum', stats.sum, twoFixed);
table.cell('%', (stats.sum / totalDuration) * 100, v => (typeof v === 'number' ? `${v.toFixed(0)}%` : ''));
}
table.sort('sum|desc');
return table.toString();
}
addMemoryMark(label) {
if (!this.isMemoryUsageEnabled)
return;
const id = `${this.memId}:${label}`;
const detail = getMemInfo(label);
performance.mark(id, { detail });
if (isMemoryRealtime)
log(detail);
}
getMemoryUsageTable() {
const table = new Table({ header: true });
let prevHeapUsed = 0;
let peakHeapUsed = 0;
let peakRss = 0;
for (const entry of this.memEntries) {
if (!entry.detail)
continue;
const { label, heapUsed, rss } = entry.detail;
const delta = heapUsed - prevHeapUsed;
if (heapUsed > peakHeapUsed)
peakHeapUsed = heapUsed;
if (rss > peakRss)
peakRss = rss;
table.row();
table.cell('Phase', label);
table.cell('heapUsed', inMB(heapUsed), twoFixed);
table.cell('rss', inMB(rss), twoFixed);
table.cell('Δheap', prevHeapUsed === 0 ? '' : `${delta > 0 ? '+' : ''}${twoFixed(inMB(delta))}`, String);
prevHeapUsed = heapUsed;
}
table.row();
table.cell('Phase', 'peak');
table.cell('heapUsed', inMB(peakHeapUsed), twoFixed);
table.cell('rss', inMB(peakRss), twoFixed);
table.cell('Δheap', '');
return table.toString();
}
getCurrentDurationInMs() {
return performance.now() - this.startTime;
}
getMemHeapUsage() {
return memoryUsage().heapUsed;
}
getCurrentMemUsageInMb() {
return twoFixed(inMB(this.getMemHeapUsage()));
}
async finalize() {
if (!this.isEnabled)
return;
this.addMemoryMark('end');
await this.flush();
}
reset() {
this.perfEntries = [];
this.fnObserver?.disconnect();
this.memObserver?.disconnect();
}
}
export const perfObserver = new Performance({ isTimerifyFunctions, isMemoryUsageEnabled });