UNPKG

@contrast/route-metrics

Version:

`route-metrics` allows server performance, exclusive of network time, to be compared on a route-by-route basis. It was created to compare server performance with and without `@contrast/agent` being loaded and active.

203 lines (175 loc) 7.08 kB
'use strict'; const perf_hooks = require('perf_hooks'); const {PerformanceObserver: PerfObserver, monitorEventLoopDelay} = perf_hooks; const WeightedEMA = require('./ema'); const emaAlphaForCpu = 0.1; const emaAlphaForMem = 0.2; // maybe even higher? const defaultOpts = { tsCallback: () => undefined, gcCallback: undefined, elCallback: undefined, eventloopResolution: 20, // sampling rate in ms histogramPercentiles: [50, 75, 90, 95, 99], }; // param: interval (1 second min?, don't allow too often). // param: eventloop timer resolution (undocumented option, 20ms?) (10 is node default) // returns: handle? class TimeSeries { /** * @param {number} ms the number of milliseconds for the interval timer. gc and * eventloop stats are collected on this interval. * @param {object} opts options as follows * - tsCallback() called for the cpu and memory stats on the `ms` interval. * - gcCallback() if provided, enables gc time-series metrics, called on `ms` interval. * - elCallback() if provided, enables eventloop time-series metrics, ditto * - histogramPercentiles eventloop histogram percentiles reported */ constructor(ms, opts) { this.ms = ms; this.opts = Object.assign({}, defaultOpts, opts); // // time-series setup. // this.tsCallback = this.opts.tsCallback; // don't do garbage collection or eventloop lag monitoring unless requested. // they are both relatively expensive operations. if (this.opts.gcCallback) { this.observer = this.setupPerfObserver(); this.gcCallback = this.opts.gcCallback; } if (this.opts.elCallback) { const options = {resolution: this.opts.eventloopResolution}; this.eventloopHistogram = this.enableEventloopMonitoring(options); this.elCallback = this.opts.elCallback; } if (this.observer) { this.gcCount = 0; this.gcTotalTime = 0; this.observer.observe({entryTypes: ['measure', 'gc'], buffered: true}); } if (this.eventloopHistogram) { this.percentiles = this.opts.histogramPercentiles; this.eventloopHistogram.enable(); } this.prevCpu = process.cpuUsage(); this.cpuUserEMA = new WeightedEMA(emaAlphaForCpu, this.prevCpu.user); this.cpuSystemEMA = new WeightedEMA(emaAlphaForCpu, this.prevCpu.system); const memoryUsage = process.memoryUsage(); // rss and heapTotal are just reported as their current values but heapUsed // and external are averaged. this.memHeapUsedEMA = new WeightedEMA(emaAlphaForMem, memoryUsage.heapUsed); this.memExternalEMA = new WeightedEMA(emaAlphaForMem, memoryUsage.external); // // interval handler lightly formats collected stats. // const intervalHandler = () => { if (this.gcCallback) { // have any garbage collections occurred since the last interval? if (this.gcCount) { this.gcCallback({count: this.gcCount, totalTime: this.gcTotalTime}); this.gcCount = 0; this.gcTotalTime = 0; } } if (this.elCallback) { // why not just use eventloopHistogram.percentiles? because it does not // return consistent percentiles. const percents = {}; for (const pc of this.percentiles) { percents[pc] = this.eventloopHistogram.percentile(pc); } this.elCallback(percents); } // get cpu usage for the interval and calculate weighted moving averages. // these are microsecond values. const cpu = process.cpuUsage(); const cpuUser = cpu.user - this.prevCpu.user; const cpuSystem = cpu.system - this.prevCpu.system; const cpuUserAvg = this.cpuUserEMA.update(cpuUser); const cpuSystemAvg = this.cpuSystemEMA.update(cpuSystem); this.prevCpu = cpu; // get memory usage. these values are in bytes. // this should not be called very often as it's expensive. // https://nodejs.org/api/process.html#processmemoryusagerss // it's also not particularly useful except 1) to compare with/without // an agent and 2) observe a memory leak. neither of those requires // high resolution observations. const mem = process.memoryUsage(); const cpuAndMem = { cpuUser, cpuSystem, cpuUserAvg, cpuSystemAvg, rss: mem.rss, heapTotal: mem.heapTotal, heapUsed: mem.heapUsed, external: mem.external, arrayBuffers: mem.arrayBuffers, }; this.tsCallback(cpuAndMem); }; // run one interval immediately so there is no window without data. intervalHandler(); this.interval = setInterval(intervalHandler, this.ms); this.interval.unref(); // finally, if we get a SIGINT, write one more set of stats. we don't // actually exit on SIGINT (expecting a SIGKILL or something else to // finish off the process). this is primarily for benchmarking to assure // that timeseries entries can be written at the end of a run. // // N.B. this doesn't get called on Windows 11 (and maybe other windows) so // another solution will be required if we need a windows equivalent. process.once('SIGINT', () => { // we don't want to generate additional timeseries entries after this. // should we write status entry that we received SIGINT? probably, but // not doing now to avoid it possibly preventing one of the time-series // writes from completing. clearInterval(this.interval); intervalHandler(); }); } setupPerfObserver() { const observer = new PerfObserver((list) => { const entries = list.getEntries(); for (const entry of entries) { if (entry.entryType === 'gc') { if (this.verbose) { // node 16 deprecated these in entry and added them to detail const {kind, flags} = entry.detail ? entry.detail : entry; // eslint-disable-next-line no-console console.log(`perf gc: ${entry.duration} (${gcTypes[kind]}) flags: ${flags}`); } this.gcCount += 1; // duration is in milliseconds this.gcTotalTime += entry.duration; } } }); return observer; } enableEventloopMonitoring(options) { // eventloop delay is in nanoseconds return perf_hooks.monitorEventLoopDelay(options); } disable() { clearInterval(this.interval); } setupEventloopMonitoring() { // histogram reports nanosecs const histogram = monitorEventLoopDelay({resolution: this.eventloopResolution}); return histogram; } } const gcTypes = { [perf_hooks.constants.NODE_PERFORMANCE_GC_MINOR]: 'minor', // 1 [perf_hooks.constants.NODE_PERFORMANCE_GC_MAJOR]: 'major', // 2 [perf_hooks.constants.NODE_PERFORMANCE_GC_INCREMENTAL]: 'incr', // 4 [perf_hooks.constants.NODE_PERFORMANCE_GC_WEAKCB]: 'weak', // 8 }; module.exports = TimeSeries; if (require.main === module) { const thing = new TimeSeries(1000, {gc: false}); setTimeout(function() { thing.disable(); }, 2500); }