2020-12-10 14:48:14 +01:00
// @ts-nocheck
2020-04-24 16:10:40 +01:00
'use strict'
/ * *
* This code is based on ` latency-monitor ` ( https : //github.com/mlucool/latency-monitor) by `mlucool` (https://github.com/mlucool), available under Apache License 2.0 (https://github.com/mlucool/latency-monitor/blob/master/LICENSE)
* /
2021-01-18 17:07:30 +01:00
const EventEmitter = require ( 'events' )
2020-04-24 16:10:40 +01:00
const VisibilityChangeEmitter = require ( './visibility-change-emitter' )
const debug = require ( 'debug' ) ( 'latency-monitor:LatencyMonitor' )
/ * *
* @ typedef { Object } SummaryObject
2020-10-06 14:59:43 +02:00
* @ 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
2020-12-10 14:48:14 +01:00
*
* @ typedef { Object } LatencyMonitorOptions
* @ property { number } [ latencyCheckIntervalMs = 500 ] - How often to add a latency check event ( ms )
* @ property { number } [ dataEmitIntervalMs = 5000 ] - How often to summarize latency check events . null or 0 disables event firing
* @ property { Function } [ asyncTestFn ] - What cb - style async function to use
* @ property { number } [ latencyRandomPercentage = 5 ] - What percent ( + / - ) o f l a t e n c y C h e c k I n t e r v a l M s s h o u l d w e r a n d o m l y u s e ? T h i s h e l p s a v o i d a l i g n m e n t t o o t h e r e v e n t s .
2020-04-24 16:10:40 +01:00
* /
/ * *
* 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.
*
2020-12-10 14:48:14 +01:00
* @ extends { EventEmitter }
*
2020-04-24 16:10:40 +01:00
* 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 ) ) ;
* /
class LatencyMonitor extends EventEmitter {
/ * *
2020-12-10 14:48:14 +01:00
* @ class
* @ param { LatencyMonitorOptions } [ options ]
2020-10-06 14:59:43 +02:00
* /
2020-04-24 16:10:40 +01:00
constructor ( { latencyCheckIntervalMs , dataEmitIntervalMs , asyncTestFn , latencyRandomPercentage } = { } ) {
super ( )
const 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
2021-04-15 09:40:02 +02:00
that . dataEmitIntervalMs = ( dataEmitIntervalMs === null || dataEmitIntervalMs === 0 )
? undefined
2020-04-24 16:10:40 +01:00
: 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
2021-01-27 09:45:31 +01:00
if ( globalThis . process && globalThis . process . hrtime ) { // eslint-disable-line no-undef
2020-04-24 16:10:40 +01:00
debug ( 'Using process.hrtime for timing' )
2021-01-27 09:45:31 +01:00
that . now = globalThis . process . hrtime // eslint-disable-line no-undef
2020-04-24 16:10:40 +01:00
that . getDeltaMS = ( startTime ) => {
const 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' && window . performance && window . performance . now ) {
debug ( 'Using performance.now for timing' )
that . now = window . performance . now . bind ( window . performance )
that . getDeltaMS = ( startTime ) => Math . round ( that . now ( ) - startTime )
} else {
debug ( 'Using Date.now for timing' )
that . now = Date . now
that . getDeltaMS = ( startTime ) => 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 VisibilityChangeEmitter ( )
2020-12-10 14:48:14 +01:00
2020-04-24 16:10:40 +01:00
that . _visibilityChangeEmitter . on ( 'visibilityChange' , ( pageInFocus ) => {
if ( pageInFocus ) {
that . _startTimers ( )
} else {
that . _emitSummary ( )
that . _stopTimers ( )
}
} )
}
if ( ! that . _visibilityChangeEmitter || that . _visibilityChangeEmitter . isVisible ( ) ) {
that . _startTimers ( )
}
}
/ * *
2020-10-06 14:59:43 +02:00
* Start internal timers
*
* @ private
* /
2020-04-24 16:10:40 +01:00
_startTimers ( ) {
// Timer already started, ignore this
if ( this . _checkLatencyID ) {
return
}
this . _checkLatency ( )
if ( this . dataEmitIntervalMs ) {
this . _emitIntervalID = setInterval ( ( ) => this . _emitSummary ( ) , this . dataEmitIntervalMs )
if ( typeof this . _emitIntervalID . unref === 'function' ) {
this . _emitIntervalID . unref ( ) // Doesn't block exit
}
}
}
/ * *
2020-10-06 14:59:43 +02:00
* Stop internal timers
*
* @ private
* /
2020-04-24 16:10:40 +01:00
_stopTimers ( ) {
if ( this . _checkLatencyID ) {
clearTimeout ( this . _checkLatencyID )
this . _checkLatencyID = undefined
}
if ( this . _emitIntervalID ) {
clearInterval ( this . _emitIntervalID )
this . _emitIntervalID = undefined
}
}
/ * *
2020-10-06 14:59:43 +02:00
* Emit summary only if there were events . It might not have any events if it was forced via a page hidden / show
*
* @ private
* /
2020-04-24 16:10:40 +01:00
_emitSummary ( ) {
const summary = this . getSummary ( )
if ( summary . events > 0 ) {
this . emit ( 'data' , summary )
}
}
/ * *
2020-10-06 14:59:43 +02:00
* 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 }
* /
2020-04-24 16:10:40 +01:00
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!!
const latency = {
events : this . _latencyData . events ,
minMs : this . _latencyData . minMs ,
maxMs : this . _latencyData . maxMs ,
2021-04-15 09:40:02 +02:00
avgMs : this . _latencyData . events
? this . _latencyData . totalMs / this . _latencyData . events
2020-04-24 16:10:40 +01:00
: Number . POSITIVE _INFINITY ,
lengthMs : this . getDeltaMS ( this . _latencyData . startTime )
}
this . _latencyData = this . _initLatencyData ( ) // Clear
debug ( 'Summary: %O' , latency )
return latency
}
/ * *
2020-10-06 14:59:43 +02:00
* 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
* /
2020-04-24 16:10:40 +01:00
_checkLatency ( ) {
const that = this
// Randomness is needed to avoid alignment by accident to regular things in the event loop
const 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
const localData = {
deltaOffset : Math . ceil ( that . latencyCheckIntervalMs + randomness ) ,
startTime : that . now ( )
}
const cb = ( ) => {
// We are already stopped, ignore this datapoint
if ( ! this . _checkLatencyID ) {
return
}
const 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 ( ( ) => {
// 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 ( typeof this . _checkLatencyID . unref === 'function' ) {
this . _checkLatencyID . unref ( ) // Doesn't block exit
}
}
_initLatencyData ( ) {
return {
startTime : this . now ( ) ,
minMs : Number . POSITIVE _INFINITY ,
maxMs : Number . NEGATIVE _INFINITY ,
events : 0 ,
totalMs : 0
}
}
}
function isBrowser ( ) {
return typeof window !== 'undefined'
}
module . exports = LatencyMonitor