import hrtime from "browser-process-hrtime" ; import { EventEmitter } from "events" ; import * as mathjs from "mathjs" ; import { FOOTER , HEADER } from "./etc" ; import { JestReporter , KarmaReporter , MochaReporter } from "./reporters" ; export { JestReporter , KarmaReporter , MochaReporter } ; export declare interface BenchmarkEventEmitter { emit ( event , description , measurement ) : boolean ; on ( event , listener ) : this ; once ( event , listener ) : this ; } export class BenchmarkEventEmitter extends EventEmitter { } /**
 * Base error for benchmark failures, such as a function taking too long
 * to execute.
 */ export class PerformanceError extends Error { constructor ( message ? ) { super ( message ) ; Object . setPrototypeOf ( this , new . target . prototype ) ; } } /**
 * Performance measurement result from running a benchmark.
 */ export class Measurement { /**
     * Optional name of the measurement, for use in reporting.
     */ description : Array < string > = [ ] ; param durations constructor ( public durations ) { if ( durations . length === 0 ) { throw new Error ( "The list of durations must not be empty" ) ; } } /**
     * Mean of all durations measured, in milliseconds.
     */ get mean ( ) : number { return mathjs . mean ( this . durations ) ; } /**
     * Minimum duration measured, in milliseconds.
     */ get min ( ) : number { return mathjs . min ( this . durations ) ; } /**
     * Maximum duration measured, in milliseconds.
     */ get max ( ) : number { return mathjs . max ( this . durations ) ; } /**
     * Standard deviation of all durations measured, in milliseconds.
     */ get standardDeviation ( ) : number { return mathjs . std ( this . durations ) ; } /**
     * Margin of error at 95% confidence level, in milliseconds.
     */ get marginOfError ( ) : number { return mathjs . sqrt ( mathjs . variance ( this . durations ) / this . durations . length ) * 1.96 ; } } /**
 * Options for Benchmark.measure().
 */ export interface MeasureOptions { default iterations : number ; default serial : boolean ; /**
     * If the mean measured duration exceeds this many milliseconds,
     * throw a [[PerformanceError]].
     */ meanUnder ? : number ; /**
     * If the minimum measured duration exceeds this many milliseconds,
     * throw a [[PerformanceError]].
     */ minUnder ? : number ; /**
     * If the maximum measured duration exceeds this many milliseconds,
     * throw a [[PerformanceError]].
     */ maxUnder ? : number ; /**
     * If the margin of error at 95% confidence level exceeds this many milliseconds,
     * throw a [[PerformanceError]].
     */ marginOfErrorUnder ? : number ; /**
     * If the standard deviation of all durations measured exceeds this many milliseconds,
     * throw a [[PerformanceError]].
     */ standardDeviationUnder ? : number ; /**
     * Callback to invoke before each iteration.
     */ beforeEach ? : ( ) => any ; /**
     * Callback to invoke after each iteration.
     */ afterEach ? : ( ) => any ; default verify : boolean ; } /**
 * Default options for Benchmark.measure().
 */ const defaultMeasureOptions = { iterations : 100 , serial : true , verify : true , } ; /**
 * Raw data collected from [[Benchmark.record]].
 */ export interface BenchmarkData { /**
     * Description passed to [[Benchmark.record]].
     */ [ description ] : { /**
         * Durations of all measured iterations, in milliseconds.
         */ durations : Array < number > ; /**
         * Nested test data, such as when passing `["A", "B"]` as the
         * description to [[Benchmark.record]].
         */ children : BenchmarkData ; } ; } async function maybePromise ( fn ) { const ret = fn ( ) ; if ( ret instanceof Promise ) { await ret ; } } function round ( value , places = 5 ) { return mathjs . round ( value , places ) as number ; } param fn param options export async function measure ( fn , options = { } ) { const mergedOptions = { ... defaultMeasureOptions , ... options } ; const durations = [ ] ; let calls = [ ] ; for ( let i = 0 ; i < mergedOptions . iterations ; i ++ ) { calls . push ( async ( ) => { if ( mergedOptions . beforeEach !== undefined ) { await maybePromise ( mergedOptions . beforeEach ) ; } const startTime = hrtime ( ) ; await maybePromise ( fn ) ; const [ durationSec , durationNano ] = hrtime ( startTime ) ; durations . push ( durationSec * 1e3 + durationNano / 1e6 ) ; if ( mergedOptions . afterEach !== undefined ) { await maybePromise ( mergedOptions . afterEach ) ; } } ) ; } if ( mergedOptions . serial ) { for ( const call of calls ) { await call ( ) ; } } else { await Promise . all ( calls . map ( x => x ( ) ) ) ; } const measurement = new Measurement ( durations ) ; verifyMeasurement ( measurement , mergedOptions ) ; return measurement ; } function verifyMeasurement ( measurement , options ) { if ( ! options . verify ) { return ; } if ( options . meanUnder !== undefined ) { if ( measurement . mean > options . meanUnder ) { throw new PerformanceError ( `Mean time of ${ measurement . mean } ms exceeded threshold of ${ options . meanUnder } ms` ) ; } } if ( options . minUnder !== undefined ) { if ( measurement . min > options . minUnder ) { throw new PerformanceError ( `Minimum time of ${ measurement . min } ms exceeded threshold of ${ options . minUnder } ms` ) ; } } if ( options . maxUnder !== undefined ) { if ( measurement . max > options . maxUnder ) { throw new PerformanceError ( `Maximum time of ${ measurement . max } ms exceeded threshold of ${ options . maxUnder } ms` ) ; } } if ( options . marginOfErrorUnder !== undefined ) { if ( measurement . marginOfError > options . marginOfErrorUnder ) { throw new PerformanceError ( `Margin of error time of ${ measurement . marginOfError } ms exceeded threshold of ${ options . marginOfErrorUnder } ms` ) ; } } if ( options . standardDeviationUnder !== undefined ) { if ( measurement . standardDeviation > options . standardDeviationUnder ) { throw new PerformanceError ( `Standard deviation time of ${ measurement . standardDeviation } ms exceeded threshold of ${ options . standardDeviationUnder } ms` ) ; } } } /**
 * Used for filtering benchmark data.
 */ export enum Criteria { Fastest , Slowest } /**
 * Aggregator for performance results of various tests.
 */ export class Benchmark { /**
     * Raw data collected from [[Benchmark.record]].
     */ data : BenchmarkData = { } ; /**
     * Event emitter.
     *
     * * `record` is emitted after [[Benchmark.record]] finishes all iterations.
     *
     * Refer to [[BenchmarkEventEmitter.on]] for the event callback signatures.
     */ events : BenchmarkEventEmitter = new BenchmarkEventEmitter ( ) ; param fn param options async record ( fn , options ? ) ; param description param fn param options async record ( description , fn , options ? ) ; async record ( a , b , c ? ) { let description ; let descriptionSpecified = false ; let fn ; let options ; if ( typeof a === "function" ) { description = [ ] ; fn = a ; options = b || { } ; } else { description = a ; descriptionSpecified = true ; fn = b ; options = c || { } ; } const mergedOptions = { ... defaultMeasureOptions , ... options } ; if ( ( descriptionSpecified && description . length === 0 ) ) { throw new Error ( "The description must not be empty" ) ; } if ( typeof description === "string" ) { description = [ description ] ; } const measurement = await measure ( fn , { ... mergedOptions , verify : false } ) ; if ( description . length > 0 ) { this . incorporate ( description , measurement ) ; } this . events . emit ( "record" , description , measurement ) ; verifyMeasurement ( measurement , { ... mergedOptions , verify : true } ) ; return measurement ; } param description param measurement incorporate ( description , measurement ) { if ( ( description . length === 0 ) ) { throw new Error ( "The description must not be empty" ) ; } this . addBenchmarkDurations ( this . data , description , measurement . durations ) ; } private addBenchmarkDurations ( data , categories , durations ) { if ( ! ( categories [ 0 ] in data ) ) { data [ categories [ 0 ] ] = { durations : [ ] , children : { } } ; } if ( categories . length === 1 ) { data [ categories [ 0 ] ] . durations = data [ categories [ 0 ] ] . durations . concat ( durations ) ; } else { this . addBenchmarkDurations ( data [ categories [ 0 ] ] . children , categories . slice ( 1 ) , durations ) ; } } private reportLevel ( level , depth ) { let lines = [ ] ; for ( const [ description , info ] of Object . entries ( level ) ) { const showMeasurement = info . durations . length > 0 ; const showChildren = Object . keys ( info . children ) . length > 0 ; lines . push ( `${ "  " . repeat ( depth ) }${ description }:` ) ; if ( showMeasurement ) { const measurement = new Measurement ( info . durations ) ; const mean = round ( measurement . mean ) ; const moe = round ( measurement . marginOfError ) ; const iterations = measurement . durations . length ; lines . push ( `${ "  " . repeat ( depth + 1 ) }${ mean } ms (+/- ${ moe } ms) from ${ iterations } iterations` ) ; } if ( showMeasurement && showChildren ) { lines . push ( "" ) ; } if ( showChildren ) { lines = lines . concat ( this . reportLevel ( info . children , depth + 1 ) ) ; } } return lines ; } /**
     * Create a report of all the benchmark results.
     */ report ( ) { const lines = this . reportLevel ( this . data , 0 ) ; if ( lines . length === 0 ) { return "" ; } else { return [ HEADER , ... this . reportLevel ( this . data , 0 ) , FOOTER ] . join ( "\n" ) ; } } private getMeasurementsAtLevel ( level , descriptions ) { let measurements = [ ] ; for ( const [ description , info ] of Object . entries ( level ) ) { const localDescriptions = [ ... descriptions , description ] ; if ( info . durations . length > 0 ) { const measurement = new Measurement ( info . durations ) ; measurement . description = localDescriptions ; measurements . push ( measurement ) ; } measurements = measurements . concat ( this . getMeasurementsAtLevel ( info . children , localDescriptions ) ) ; } return measurements ; } /**
     * Get a list of [[Measurement]] based on [[Benchmark.data]].
     */ get measurements ( ) : Array < Measurement > { return this . getMeasurementsAtLevel ( this . data , [ ] ) ; } param criteria param value returns find ( criteria , value = m => m . mean + m . marginOfError ) { let candidate = null ; for ( const measurement of this . measurements ) { if ( candidate === null ) { candidate = measurement ; } else if ( criteria === Criteria . Fastest && value ( measurement ) < value ( candidate ) ) { candidate = measurement ; } else if ( criteria === Criteria . Slowest && value ( measurement ) > value ( candidate ) ) { candidate = measurement ; } } return candidate ; } } /**
 * Default [[Benchmark]] instance for shared usage throughout your tests.
 * Each instance stores its own state from measurement results, so if you
 * want to avoid global state, you can create additional instances as well.
 */ export const benchmark = new Benchmark ( ) ;