import { LoggerManager , PerformanceTimer } from "@here/harp-utils" ; import * as THREE from "three" ; const logger = LoggerManager . instance . create ( "Statistics" ) ; /**
 * A simple ring buffer to store the last `n` values of the timer. The buffer works on
 * a First-In-First-Out (FIFO) basis.
 */ export class RingBuffer < T > { buffer : T [ ] ; size : number ; head : number ; tail : number ; param capacity constructor ( readonly capacity ) { this . buffer = new Array ( capacity ) ; this . capacity = capacity ; this . head = this . tail = this . size = 0 ; } /**
     * Clears the contents, removes all elements.
     */ clear ( ) { this . head = this . tail = this . size = 0 ; } param data enqOne ( data ) { let next = this . head + 1 ; if ( next >= this . capacity ) { next = 0 ; } if ( this . size < this . capacity ) { this . size ++ ; } this . buffer [ this . head ] = data ; this . head = next ; if ( this . size === this . capacity ) { this . tail = this . head ; } } param data enq ( ... data ) { for ( const v of data ) { this . enqOne ( v ) ; } } /**
     * Obtains the oldest element (FIFO). May throw an exception if a buffer underrun occurs.
     * Before calling this method, make sure that `size > 0`.
     */ deq ( ) { if ( this . size === 0 ) { throw new Error ( "Ringbuffer underrun" ) ; } const data = this . buffer [ this . tail ] ; let next = this . tail + 1 ; if ( next >= this . capacity ) { next = 0 ; } if ( this . size > 0 ) { this . size -- ; } this . tail = next ; return data ; } /**
     * Obtains the oldest element (FIFO) without removing it. Throws an exception if a buffer is
     * empty. Before calling this method, make sure that `size > 0`.
     */ get top ( ) : T { if ( this . size === 0 ) { throw new Error ( "Ringbuffer underrun" ) ; } return this . buffer [ this . tail ] ; } /**
     * Obtains the latest element (LIFO) without removing it. Throws an exception if a buffer is
     * empty. Before calling this method, make sure that `size > 0`.
     */ get bottom ( ) : T { if ( this . size === 0 ) { throw new Error ( "Ringbuffer underrun" ) ; } let previous = this . head - 1 ; if ( previous < 0 ) { previous = this . capacity - 1 ; } return this . buffer [ previous ] ; } /**
     * Creates an iterator for the buffer.
     */ iterator ( ) { return new RingBuffer . Iterator < T > ( this ) ; } /**
     * Returns a copy of the buffer, where the elements are properly sorted from oldest to newest.
     */ asArray ( ) { const array = new Array < T > ( ) ; for ( let i = 0 ; i < this . size ; i ++ ) { array . push ( this . buffer [ ( this . tail + i ) % this . capacity ] ) ; } return array ; } } export namespace RingBuffer { /**
     * A local class for RingBuffer<T>
     */ export class Iterator < T > { param m_buffer param m_index constructor ( private readonly m_buffer , private m_index = 0 ) { } /**
         * Gets the iterator's current value. This function does not fail even if an overrun occurs.
         * To detect an overrun, watch the result for [[next]].
         */ get value ( ) : T { return this . m_buffer . buffer [ ( this . m_buffer . tail + this . m_index ) % this . m_buffer . capacity ] ; } returns next ( ) { this . m_index ++ ; return this . m_index < this . m_buffer . size ; } } } remarks internal export interface Timer { readonly name : string ; readonly value ? : number ; /**
     * Resets value to be able to start again.
     */ reset ( ) : void ; /**
     * Starts the timer. Returns the current time, based on `Performance.now()`.
     */ start ( ) : number ; /**
     * Stops the timer. Requires that the timer has started.
     */ stop ( ) : number ; returns now ( ) : number ; param val setValue ( val ) : void ; } internal export class SimpleTimer implements Timer { /** `true` if timer has been started. */ running = false ; private m_currentValue ? : number ; constructor ( public statistics , readonly name ) { } /**
     * Gets the latest measurement. This function may return `undefined` if no measurement
     * was done.
     */ get value ( ) : number | undefined { return this . m_currentValue ; } param val setValue ( val ) { this . m_currentValue = val ; } /**
     * Resets the value to be able to start again.
     */ reset ( ) { this . m_currentValue = undefined ; } /**
     * Starts the timer. Returns the current time, based on `Performance.now()`.
     */ start ( ) { if ( ! this . statistics . enabled ) { return - 1 ; } if ( this . running ) { throw new Error ( "Timer '" + this . name + "' is already running" ) ; } this . running = true ; return ( this . m_currentValue = PerformanceTimer . now ( ) ) ; } /**
     * Stops the timer. Requires that the timer has started.
     */ stop ( ) { if ( ! this . statistics . enabled ) { return - 1 ; } if ( ! this . running ) { throw new Error ( "Timer '" + this . name + "' has not been started" ) ; } else { const t = PerformanceTimer . now ( ) - ( this . m_currentValue ?? 0 ) ; this . m_currentValue = t ; this . setValue ( t ) ; this . running = false ; return t ; } } returns now ( ) { if ( ! this . statistics . enabled ) { return - 1 ; } if ( ! this . running ) { throw new Error ( "Timer '" + this . name + "' has not been started" ) ; } else { const t = PerformanceTimer . now ( ) - ( this . m_currentValue ?? 0 ) ; return t ; } } } internal export interface Stats { /**
     * The lowest value in the array.
     */ min : number ; /**
     * The highest value in the array.
     */ max : number ; /**
     * The average duration of all values in the array.
     */ avg : number ; /**
     * The median duration of all values in the array.
     */ median : number ; /**
     * The 75th percentile median of all values in the array.
     */ median75 : number ; /**
     * The 90th percentile median of all values in the array.
     */ median90 : number ; /**
     * The 95th percentile median of all values in the array.
     */ median95 : number ; /**
     * The 97th percentile median of all values in the array.
     */ median97 : number ; /**
     * The 99th percentile median of all values in the array.
     */ median99 : number ; /**
     * The 99.9th percentile median of all values in the array.
     */ median999 : number ; /**
     * The number of values in the array.
     */ numSamples : number ; } internal export class SampledTimer extends SimpleTimer { /**
     * The number of times the timer has reset.
     */ numResets = 0 ; /**
     * Maximum samples until the statistics are reset and updated, which may destroy a median
     * computation.
     */ maxNumSamples = 1000 ; /**
     * The array of sampled values, its length cannot exceed `maxNumSamples`.
     */ samples = new RingBuffer < number > ( this . maxNumSamples ) ; param statistics param name constructor ( public statistics , readonly name ) { super ( statistics , name ) ; } @override
      reset ( ) { super . reset ( ) ; this . getStats ( ) ; this . samples . clear ( ) ; this . numResets ++ ; } param val @override
      setValue ( val ) { super . setValue ( val ) ; if ( val !== undefined ) { this . samples . enqOne ( val ) ; } } /**
     * Updates the `min`, `max`, `avg`, and `median` values. Currently, this function is expensive,
     * as it requires a copy of the sampled values.
     */ getStats ( ) { return computeArrayStats ( this . samples . asArray ( ) ) ; } } ignore remarks param { number [ ] } samples returns { ( Stats | undefined ) } internal export function computeArrayStats ( samples ) { if ( samples . length === 0 ) { return undefined ; } samples . sort ( ( a , b ) => { return a - b ; } ) ; const min = samples [ 0 ] ; const max = samples [ samples . length - 1 ] ; let median ; let median75 ; let median90 ; let median95 ; let median97 ; let median99 ; let median999 ; if ( samples . length === 1 ) { median75 = median90 = median95 = median97 = median99 = median999 = median = samples [ 0 ] ; } else if ( samples . length === 2 ) { median = samples [ 0 ] * 0.5 + samples [ 1 ] * 0.5 ; median75 = median90 = median95 = median97 = median99 = median999 = samples [ 1 ] ; } else { const mid = Math . floor ( samples . length / 2 ) ; median = samples . length % 2 === 0 ? samples [ mid - 1 ] * 0.5 + samples [ mid ] * 0.5 : samples [ mid ] ; const mid75 = Math . round ( samples . length * 0.75 ) - 1 ; median75 = samples [ mid75 ] ; const mid90 = Math . round ( samples . length * 0.9 ) - 1 ; median90 = samples [ mid90 ] ; const mid95 = Math . round ( samples . length * 0.95 ) - 1 ; median95 = samples [ mid95 ] ; const mid97 = Math . round ( samples . length * 0.97 ) - 1 ; median97 = samples [ mid97 ] ; const mid99 = Math . round ( samples . length * 0.99 ) - 1 ; median99 = samples [ mid99 ] ; const mid999 = Math . round ( samples . length * 0.999 ) - 1 ; median999 = samples [ mid999 ] ; } let sum = 0 ; for ( let i = 0 , l = samples . length ; i < l ; i ++ ) { sum += samples [ i ] ; } const avg = sum / samples . length ; return { min , max , avg , median , median75 , median90 , median95 , median97 , median99 , median999 , numSamples : samples . length } ; } ignore remarks param { number [ ] } samples returns { ( Stats | undefined ) } internal export function computeArrayAverage ( samples ) { if ( samples . length === 0 ) { return undefined ; } let sum = 0 ; for ( let i = 0 , l = samples . length ; i < l ; i ++ ) { sum += samples [ i ] ; } const avg = sum / samples . length ; return avg ; } remarks internal export class MultiStageTimer { private currentStage : string | undefined ; param statistics param name param stages constructor ( private readonly statistics , readonly name , public stages ) { if ( stages . length < 1 ) { throw new Error ( "MultiStageTimer needs stages" ) ; } stages . forEach ( stage => { if ( ! statistics . hasTimer ( stage ) ) { throw new Error ( "Unknown timer: " + stage ) ; } } ) ; } /**
     * Gets the timer value for the last stage. If the `MultiStageTimer` did not finish its
     * last stage, the value is `undefined`.
     */ get value ( ) : number | undefined { return this . statistics . getTimer ( this . stages [ this . stages . length - 1 ] ) . value ; } /**
     * Resets the timers across all stages.
     */ reset ( ) { if ( ! this . statistics . enabled ) { return ; } this . stages . forEach ( stage => { this . statistics . getTimer ( stage ) . reset ( ) ; } ) ; } /**
     * Starts the `MultiStageTimer` at its first stage.
     */ start ( ) { this . stage = this . stages [ 0 ] ; return this . statistics . getTimer ( this . stages [ 0 ] ) . value ?? - 1 ; } /**
     * Stops the `MultiStageTimer`. Returns the measurement of the last stage, which may be
     * `undefined` if not all stages started.
     */ stop ( ) { this . stage = undefined ; return this . value !== undefined ? this . value : - 1 ; } /**
     * Gets the current stage.
     */ get stage ( ) : string | undefined { return this . currentStage ; } param stage set stage ( stage ) { if ( this . currentStage === stage ) { return ; } if ( this . statistics . enabled && this . currentStage !== undefined ) { this . statistics . getTimer ( this . currentStage ) . stop ( ) ; } this . currentStage = stage ; if ( this . statistics . enabled && this . currentStage !== undefined ) { this . statistics . getTimer ( this . currentStage ) . start ( ) ; } } } remarks internal export class Statistics { private readonly timers : Map < string , Timer > ; private readonly nullTimer : Timer ; param name param enabled constructor ( public name ? , public enabled = false ) { this . timers = new Map < string , Timer > ( ) ; this . nullTimer = new SimpleTimer ( this , "<null>" ) ; } param name createTimer ( name , keepSamples = true ) { const timer = keepSamples ? new SampledTimer ( this , name ) : new SimpleTimer ( this , name ) ; return this . addTimer ( timer ) ; } param timer addTimer ( timer ) { if ( this . timers . get ( timer . name ) !== undefined ) { throw new Error ( "Duplicate timer name: '" + timer . name + "'" ) ; } this . timers . set ( timer . name , timer ) ; return timer ; } param name getTimer ( name ) { if ( ! this . enabled ) { return this . nullTimer ; } const t = this . timers . get ( name ) ; return t === undefined ? this . nullTimer : t ; } param name returns hasTimer ( name ) { const t = this . timers . get ( name ) ; return t !== undefined ; } /**
     * Resets all timers.
     */ reset ( ) { this . timers . forEach ( ( timer ) => { timer . reset ( ) ; } ) ; } param header param footer log ( header ? , footer ? ) { if ( header !== undefined || this . name !== undefined ) { logger . log ( header !== undefined ? header : this . name ) ; } let maxNameLength = 0 ; this . timers . forEach ( ( timer ) => { maxNameLength = Math . max ( maxNameLength , timer . name . length ) ; } ) ; const print = ( v ) => { return v !== undefined ? v . toFixed ( 5 ) : "?" ; } ; this . timers . forEach ( ( timer ) => { let s = timer . name + ": " + " " . repeat ( maxNameLength - timer . name . length ) ; s += print ( timer . value ) ; if ( timer instanceof SampledTimer ) { const simpleStats = timer . getStats ( ) ; if ( simpleStats !== undefined ) { s += `  [ min=${ print ( simpleStats . min ) }, max=${ print ( simpleStats . max ) }, ` + `avg=${ print ( simpleStats . avg ) }, med=${ print ( simpleStats . median ) }, ` + `med95=${ print ( simpleStats . median95 ) }, med99=${ print ( simpleStats . median99 ) }, ` + `N=${ print ( simpleStats . numSamples ) } ]` ; } } logger . log ( s ) ; } ) ; if ( footer !== undefined ) { logger . log ( footer ) ; } } } internal export class FrameStats { readonly entries : Map < string , number > = new Map ( ) ; messages ? : string [ ] = undefined ; param name returns getValue ( name ) { return this . entries . get ( name ) ; } param name param name setValue ( name , value ) { this . entries . set ( name , value ) ; } param name param name addValue ( name , value ) { const oldValue = this . entries . get ( name ) ; this . entries . set ( name , value + ( oldValue === undefined ? 0 : oldValue ) ) ; } param message addMessage ( message ) { if ( this . messages === undefined ) { this . messages = [ ] ; } this . messages . push ( message ) ; } /**
     * Reset all known performance values to `0` and the messages to `undefined`.
     */ reset ( ) { this . entries . forEach ( ( value , name ) => { this . entries . set ( name , 0 ) ; } ) ; this . messages = undefined ; } } ignore remarks Instead of passing around an array of objects, we store the frame statistics as an object of
 * arrays. This allows convenient computations from  RingBuffer ,
  export class FrameStatsArray { readonly frameEntries : Map < string , RingBuffer < number > > = new Map ( ) ; readonly messages : RingBuffer < string [ ] | undefined > ; constructor ( readonly capacity = 0 ) { this . messages = new RingBuffer < string [ ] | undefined > ( capacity ) ; } get length ( ) : number { return this . messages . size ; } reset ( ) { this . frameEntries . forEach ( ( buffer , name ) => { buffer . clear ( ) ; } ) ; this . messages . clear ( ) ; } addFrame ( frameStats ) { const currentSize = this . length ; const frameEntries = this . frameEntries ; frameStats . entries . forEach ( ( value , name ) => { let buffer = frameEntries . get ( name ) ; if ( buffer === undefined ) { buffer = new RingBuffer < number > ( this . capacity ) ; for ( let i = 0 ; i < currentSize ; i ++ ) { buffer . enqOne ( 0 ) ; } this . frameEntries . set ( name , buffer ) ; } buffer . enqOne ( value ) ; } ) ; this . messages . enq ( frameStats . messages ) ; } /**
     * Prints all values to the console.
     */ log ( ) { let maxNameLength = 0 ; this . frameEntries . forEach ( ( buffer , name ) => { maxNameLength = Math . max ( maxNameLength , name . length ) ; } ) ; const print = ( v ) => { return v !== undefined ? v . toFixed ( 5 ) : "?" ; } ; this . frameEntries . forEach ( ( buffer , name ) => { let s = name + ": " + " " . repeat ( maxNameLength - name . length ) ; const simpleStats = computeArrayStats ( buffer . asArray ( ) ) ; if ( simpleStats !== undefined ) { s += `  [ min=${ print ( simpleStats . min ) }, max=${ print ( simpleStats . max ) }, ` + `avg=${ print ( simpleStats . avg ) }, med=${ print ( simpleStats . median ) }, ` + `med95=${ print ( simpleStats . median95 ) }, med99=${ print ( simpleStats . median99 ) }, ` + `N=${ print ( simpleStats . numSamples ) } ]` ; } logger . log ( s ) ; } ) ; } } /**
 * Chrome's MemoryInfo interface.
 */ interface ChromeMemoryInfo { totalJSHeapSize : number ; usedJSHeapSize : number ; jsHeapSizeLimit : number ; } internal export interface SimpleFrameStatistics { configs : Map < string , string > ; appResults : Map < string , number > ; frames : Map < string , number | number [ ] > ; messages : Array < string [ ] | undefined > ; frameStats ? : Map < string , Stats | undefined > ; zoomLevelLabels ? : string [ ] ; zoomLevelData ? : Map < string , number | number [ ] > ; } remarks Maintains the current. Implemented as an instance for easy access.
 *
 *  FrameStats , which holds all individual performance numbers.
 *
 *  internal export class PerformanceStatistics { readonly type { boolean } memberof get isFull ( ) : boolean { return this . m_frameEvents . length >= this . maxNumFrames ; } /**
     * Global instance to the instance. The current instance can be overridden by creating a new
     * `PerformanceStatistics`.
     */ static get instance ( ) : PerformanceStatistics { if ( PerformanceStatistics . m_instance === undefined ) { PerformanceStatistics . m_instance = new PerformanceStatistics ( false , 0 ) ; } return PerformanceStatistics . m_instance ; } private static m_instance ? : PerformanceStatistics = undefined ; type { FrameStats } memberof readonly currentFrame : FrameStats = new FrameStats ( ) ; ignore get frameEvents ( ) : FrameStatsArray { return this . m_frameEvents ; } type { ( Map < string , number > ) } readonly appResults : Map < string , number > = new Map ( ) ; type { ( Map < string , string > ) } memberof readonly configs : Map < string , string > = new Map ( ) ; private readonly m_frameEvents : FrameStatsArray ; param { boolean } enabled param { number } maxNumFrames memberof constructor ( public enabled = true , public maxNumFrames = 1000 ) { PerformanceStatistics . m_instance = this ; this . m_frameEvents = new FrameStatsArray ( maxNumFrames ) ; } memberof clear ( ) { this . clearFrames ( ) ; this . configs . clear ( ) ; this . appResults . clear ( ) ; } memberof clearFrames ( ) { this . m_frameEvents . reset ( ) ; this . currentFrame . reset ( ) ; } param { THREE . WebGLInfo } webGlInfo addWebGLInfo ( webGlInfo ) { if ( webGlInfo . render !== undefined ) { this . currentFrame . setValue ( "gl.numCalls" , webGlInfo . render . calls === null ? 0 : webGlInfo . render . calls ) ; this . currentFrame . setValue ( "gl.numPoints" , webGlInfo . render . points === null ? 0 : webGlInfo . render . points ) ; this . currentFrame . setValue ( "gl.numLines" , webGlInfo . render . lines === null ? 0 : webGlInfo . render . lines ) ; this . currentFrame . setValue ( "gl.numTriangles" , webGlInfo . render . triangles === null ? 0 : webGlInfo . render . triangles ) ; } if ( webGlInfo . memory !== undefined ) { this . currentFrame . setValue ( "gl.numGeometries" , webGlInfo . memory . geometries === null ? 0 : webGlInfo . memory . geometries ) ; this . currentFrame . setValue ( "gl.numTextures" , webGlInfo . memory . textures === null ? 0 : webGlInfo . memory . textures ) ; } if ( webGlInfo . programs !== undefined ) { this . currentFrame . setValue ( "gl.numPrograms" , webGlInfo . programs === null ? 0 : webGlInfo . programs . length ) ; } } note addMemoryInfo ( ) { if ( window !== undefined && window . performance !== undefined ) { const memory = ( window . performance as any ) . memory as ChromeMemoryInfo ; if ( memory !== undefined ) { this . currentFrame . setValue ( "memory.totalJSHeapSize" , memory . totalJSHeapSize ) ; this . currentFrame . setValue ( "memory.usedJSHeapSize" , memory . usedJSHeapSize ) ; this . currentFrame . setValue ( "memory.jsHeapSizeLimit" , memory . jsHeapSizeLimit ) ; } } } returns { boolean } memberof storeAndClearFrameInfo ( ) { if ( this . m_frameEvents . length >= this . maxNumFrames ) { return false ; } this . m_frameEvents . addFrame ( this . currentFrame ) ; this . currentFrame . reset ( ) ; return true ; } param header param footer log ( header ? , footer ? ) { logger . log ( header !== undefined ? header : "PerformanceStatistics" ) ; const appResults = this . appResults ; appResults . forEach ( ( value , name ) => { logger . log ( name , value ) ; } ) ; const configs = this . configs ; configs . forEach ( ( value , name ) => { logger . log ( name , value ) ; } ) ; this . m_frameEvents . log ( ) ; if ( footer !== undefined ) { logger . log ( footer ) ; } } /**
     * Convert to a plain object that can be serialized. Required to copy the test results over to
     * nightwatch.
     */ getAsPlainObject ( onlyLastFrame = false ) { const appResults = { } ; const configs = { } ; const frames = { } ; const plainObject = { configs , appResults , frames } ; const appResultValues = this . appResults ; appResultValues . forEach ( ( value , name ) => { appResults [ name ] = value ; } ) ; const configValues = this . configs ; configValues . forEach ( ( value , name ) => { configs [ name ] = value ; } ) ; if ( onlyLastFrame ) { for ( const [ name , buffer ] of this . m_frameEvents . frameEntries ) { frames [ name ] = buffer . bottom ; } } else { for ( const [ name , buffer ] of this . m_frameEvents . frameEntries ) { frames [ name ] = buffer . asArray ( ) ; } } plainObject . messages = this . m_frameEvents . messages . asArray ( ) ; return plainObject ; } /**
     * Convert the last frame values to a plain object that can be serialized. Required to copy the
     * test results over to nightwatch.
     */ getLastFrameStatistics ( ) { return this . getAsPlainObject ( true ) ; } /**
     * Convert to a plain object that can be serialized. Required to copy the test results over to
     * nightwatch.
     */ getAsSimpleFrameStatistics ( onlyLastFrame = false ) { const configs = new Map ( ) ; const appResults = new Map ( ) ; const frames = new Map ( ) ; const simpleStatistics = { configs , appResults , frames , messages : this . m_frameEvents . messages . asArray ( ) } ; const appResultValues = this . appResults ; appResultValues . forEach ( ( value , name ) => { appResults . set ( name , value ) ; } ) ; const configValues = this . configs ; configValues . forEach ( ( value , name ) => { configs . set ( name , value ) ; } ) ; if ( onlyLastFrame ) { for ( const [ name , buffer ] of this . m_frameEvents . frameEntries ) { frames . set ( name , buffer . bottom ) ; } } else { for ( const [ name , buffer ] of this . m_frameEvents . frameEntries ) { frames . set ( name , buffer . asArray ( ) ) ; } } return simpleStatistics ; } }