UNPKG

latency-monitor

Version:

A generic latency monitor for node/browers

311 lines (258 loc) 13.5 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _events = require('events'); var _events2 = _interopRequireDefault(_events); var _get = require('lodash/get'); var _get2 = _interopRequireDefault(_get); var _isFunction = require('lodash/isFunction'); var _isFunction2 = _interopRequireDefault(_isFunction); var _VisibilityChangeEmitter = require('./VisibilityChangeEmitter'); var _VisibilityChangeEmitter2 = _interopRequireDefault(_VisibilityChangeEmitter); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /* global window */ var debug = require('debug')('latency-monitor:LatencyMonitor'); /** * @typedef {Object} SummaryObject * @property {Number} events How many events were called * @property {Number} minMS What was the min time for a cb to be called * @property {Number} maxMS What was the max time for a cb to be called * @property {Number} avgMs What was the average time for a cb to be called * @property {Number} lengthMs How long this interval was in ms */ /** * A class to monitor latency of any async function which works in a browser or node. This works by periodically calling * the asyncTestFn and timing how long it takes the callback to be called. It can also periodically emit stats about this. * This can be disabled and stats can be pulled via setting dataEmitIntervalMs = 0. * * The default implementation is an event loop latency monitor. This works by firing periodic events into the event loop * and timing how long it takes to get back. * * @example * const monitor = new LatencyMonitor(); * monitor.on('data', (summary) => console.log('Event Loop Latency: %O', summary)); * * @example * const monitor = new LatencyMonitor({latencyCheckIntervalMs: 1000, dataEmitIntervalMs: 60000, asyncTestFn:ping}); * monitor.on('data', (summary) => console.log('Ping Pong Latency: %O', summary)); */ var LatencyMonitor = function (_EventEmitter) { _inherits(LatencyMonitor, _EventEmitter); /** * @param {Number} [latencyCheckIntervalMs=500] How often to add a latency check event (ms) * @param {Number} [dataEmitIntervalMs=5000] How often to summarize latency check events. null or 0 disables event firing * @param {function} [asyncTestFn] What cb-style async function to use * @param {Number} [latencyRandomPercentage=5] What percent (+/-) of latencyCheckIntervalMs should we randomly use? This helps avoid alignment to other events. */ function LatencyMonitor() { var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, latencyCheckIntervalMs = _ref.latencyCheckIntervalMs, dataEmitIntervalMs = _ref.dataEmitIntervalMs, asyncTestFn = _ref.asyncTestFn, latencyRandomPercentage = _ref.latencyRandomPercentage; _classCallCheck(this, LatencyMonitor); var _this = _possibleConstructorReturn(this, (LatencyMonitor.__proto__ || Object.getPrototypeOf(LatencyMonitor)).call(this)); var that = _this; // 0 isn't valid here, so its ok to use || that.latencyCheckIntervalMs = latencyCheckIntervalMs || 500; // 0.5s that.latencyRandomPercentage = latencyRandomPercentage || 10; that._latecyCheckMultiply = 2 * (that.latencyRandomPercentage / 100.0) * that.latencyCheckIntervalMs; that._latecyCheckSubtract = that._latecyCheckMultiply / 2; that.dataEmitIntervalMs = dataEmitIntervalMs === null || dataEmitIntervalMs === 0 ? undefined : dataEmitIntervalMs || 5 * 1000; // 5s debug('latencyCheckIntervalMs: %s dataEmitIntervalMs: %s', that.latencyCheckIntervalMs, that.dataEmitIntervalMs); if (that.dataEmitIntervalMs) { debug('Expecting ~%s events per summary', that.latencyCheckIntervalMs / that.dataEmitIntervalMs); } else { debug('Not emitting summaries'); } that.asyncTestFn = asyncTestFn; // If there is no asyncFn, we measure latency // If process: use high resolution timer if (process && process.hrtime) { debug('Using process.hrtime for timing'); that.now = process.hrtime; that.getDeltaMS = function (startTime) { var hrtime = that.now(startTime); return hrtime[0] * 1000 + hrtime[1] / 1000000; }; // Let's try for a timer that only monotonically increases } else if (typeof window !== 'undefined' && (0, _get2.default)(window, 'performance.now')) { debug('Using performance.now for timing'); that.now = window.performance.now.bind(window.performance); that.getDeltaMS = function (startTime) { return Math.round(that.now() - startTime); }; } else { debug('Using Date.now for timing'); that.now = Date.now; that.getDeltaMS = function (startTime) { return that.now() - startTime; }; } that._latencyData = that._initLatencyData(); // We check for isBrowser because of browsers set max rates of timeouts when a page is hidden, // so we fall back to another library // See: http://stackoverflow.com/questions/6032429/chrome-timeouts-interval-suspended-in-background-tabs if (isBrowser()) { that._visibilityChangeEmitter = new _VisibilityChangeEmitter2.default(); that._visibilityChangeEmitter.on('visibilityChange', function (pageInFocus) { if (pageInFocus) { that._startTimers(); } else { that._emitSummary(); that._stopTimers(); } }); } if (!that._visibilityChangeEmitter || that._visibilityChangeEmitter.isVisible()) { that._startTimers(); } return _this; } /** * Start internal timers * @private */ _createClass(LatencyMonitor, [{ key: '_startTimers', value: function _startTimers() { var _this2 = this; // Timer already started, ignore this if (this._checkLatencyID) { return; } this._checkLatency(); if (this.dataEmitIntervalMs) { this._emitIntervalID = setInterval(function () { return _this2._emitSummary(); }, this.dataEmitIntervalMs); if ((0, _isFunction2.default)(this._emitIntervalID.unref)) { this._emitIntervalID.unref(); // Doesn't block exit } } } /** * Stop internal timers * @private */ }, { key: '_stopTimers', value: function _stopTimers() { if (this._checkLatencyID) { clearTimeout(this._checkLatencyID); this._checkLatencyID = undefined; } if (this._emitIntervalID) { clearInterval(this._emitIntervalID); this._emitIntervalID = undefined; } } /** * Emit summary only if there were events. It might not have any events if it was forced via a page hidden/show * @private */ }, { key: '_emitSummary', value: function _emitSummary() { var summary = this.getSummary(); if (summary.events > 0) { this.emit('data', summary); } } /** * Calling this function will end the collection period. If a timing event was already fired and somewhere in the queue, * it will not count for this time period * @returns {SummaryObject} */ }, { key: 'getSummary', value: function getSummary() { // We might want to adjust for the number of expected events // Example: first 1 event it comes back, then such a long blocker that the next emit check comes // Then this fires - looks like no latency!! var latency = { events: this._latencyData.events, minMs: this._latencyData.minMs, maxMs: this._latencyData.maxMs, avgMs: this._latencyData.events ? this._latencyData.totalMs / this._latencyData.events : Number.POSITIVE_INFINITY, lengthMs: this.getDeltaMS(this._latencyData.startTime) }; this._latencyData = this._initLatencyData(); // Clear debug('Summary: %O', latency); return latency; } /** * Randomly calls an async fn every roughly latencyCheckIntervalMs (plus some randomness). If no async fn is found, * it will simply report on event loop latency. * * @private */ }, { key: '_checkLatency', value: function _checkLatency() { var _this3 = this; var that = this; // Randomness is needed to avoid alignment by accident to regular things in the event loop var randomness = Math.random() * that._latecyCheckMultiply - that._latecyCheckSubtract; // We use this to ensure that in case some overlap somehow, we don't take the wrong startTime/offset var localData = { deltaOffset: Math.ceil(that.latencyCheckIntervalMs + randomness), startTime: that.now() }; var cb = function cb() { // We are already stopped, ignore this datapoint if (!_this3._checkLatencyID) { return; } var deltaMS = that.getDeltaMS(localData.startTime) - localData.deltaOffset; that._checkLatency(); // Start again ASAP // Add the data point. If this gets complex, refactor it that._latencyData.events++; that._latencyData.minMs = Math.min(that._latencyData.minMs, deltaMS); that._latencyData.maxMs = Math.max(that._latencyData.maxMs, deltaMS); that._latencyData.totalMs += deltaMS; debug('MS: %s Data: %O', deltaMS, that._latencyData); }; debug('localData: %O', localData); this._checkLatencyID = setTimeout(function () { // This gets rid of including event loop if (that.asyncTestFn) { // Clear timing related things localData.deltaOffset = 0; localData.startTime = that.now(); that.asyncTestFn(cb); } else { // setTimeout is not more accurate than 1ms, so this will ensure positive numbers. Add 1 to emitted data to remove. // This is not the best, but for now it'll be just fine. This isn't meant to be sub ms accurate. localData.deltaOffset -= 1; // If there is no function to test, we mean check latency which is a special case that is really cb => cb() // We avoid that for the few extra function all overheads. Also, we want to keep the timers different cb(); } }, localData.deltaOffset); if ((0, _isFunction2.default)(this._checkLatencyID.unref)) { this._checkLatencyID.unref(); // Doesn't block exit } } }, { key: '_initLatencyData', value: function _initLatencyData() { return { startTime: this.now(), minMs: Number.POSITIVE_INFINITY, maxMs: Number.NEGATIVE_INFINITY, events: 0, totalMs: 0 }; } }]); return LatencyMonitor; }(_events2.default); function isBrowser() { return typeof window !== 'undefined'; } exports.default = LatencyMonitor; //# sourceMappingURL=LatencyMonitor.js.map