text: /**
   * The MIT License (MIT)
   * Copyright (c) 2017 Paul Armstrong
   * https://github.com/paularmstrong/react-component-benchmark
   */
  
  /* global $Values */
  /**
   * @flow
   */
  import * as Timing from './timing'
  import React, { Component } from 'react'
  import { getMean, getMedian, getStdDev } from './math'
  
  import type {
    BenchResultsType,
    FullSampleTimingType,
    SampleTimingType,
  } from './types'
  
  export const BenchmarkType = {
    MOUNT: 'mount',
    UPDATE: 'update',
    UNMOUNT: 'unmount',
  }
  
  const shouldRender = (
    cycle: number,
    type: $Values<typeof BenchmarkType>
  ): boolean => {
    switch (type) {
      // Render every odd iteration (first, third, etc)
      // Mounts and unmounts the component
      case BenchmarkType.MOUNT:
      case BenchmarkType.UNMOUNT:
        return !((cycle + 1) % 2)
      // Render every iteration (updates previously rendered module)
      case BenchmarkType.UPDATE:
        return true
      default:
        return false
    }
  }
  
  const shouldRecord = (
    cycle: number,
    type: $Values<typeof BenchmarkType>
  ): boolean => {
    switch (type) {
      // Record every odd iteration (when mounted: first, third, etc)
      case BenchmarkType.MOUNT:
        return !((cycle + 1) % 2)
      // Record every iteration
      case BenchmarkType.UPDATE:
        return true
      // Record every even iteration (when unmounted)
      case BenchmarkType.UNMOUNT:
        return !(cycle % 2)
      default:
        return false
    }
  }
  
  const isDone = (
    cycle: number,
    sampleCount: number,
    type: $Values<typeof BenchmarkType>
  ): boolean => {
    switch (type) {
      case BenchmarkType.MOUNT:
        return cycle >= sampleCount * 2 - 1
      case BenchmarkType.UPDATE:
        return cycle >= sampleCount - 1
      case BenchmarkType.UNMOUNT:
        return cycle >= sampleCount * 2
      default:
        return true
    }
  }
  
  const sortNumbers = (a: number, b: number): number => a - b
  
  type BenchmarkPropsType = {
    component: typeof React.Component,
    forceLayout?: boolean,
    getComponentProps: Function,
    onComplete: (x: BenchResultsType) => void,
    sampleCount: number,
    timeout: number,
    type: $Values<typeof BenchmarkType>,
  }
  
  type BenchmarkStateType = {
    componentProps: Object,
    cycle: number,
    running: boolean,
  }
  
  /**
   * Benchmark
   * TODO: documentation
   */
  export default class Benchmark extends Component<
    BenchmarkPropsType,
    BenchmarkStateType
  > {
    _raf: ?Function
    _startTime: number
    _samples: Array<SampleTimingType>
  
    static displayName = 'Benchmark'
  
    static defaultProps = {
      sampleCount: 50,
      timeout: 10000, // 10 seconds
      type: BenchmarkType.MOUNT,
    }
  
    static Type = BenchmarkType
  
    constructor(props: BenchmarkPropsType, context?: {}) {
      super(props, context)
      const cycle = 0
      const componentProps = props.getComponentProps({ cycle })
      this.state = {
        componentProps,
        cycle,
        running: false,
      }
      this._startTime = 0
      this._samples = []
    }
  
    componentWillReceiveProps(nextProps: BenchmarkPropsType) {
      if (nextProps) {
        this.setState((state) => ({
          componentProps: nextProps.getComponentProps(state.cycle),
        }))
      }
    }
  
    componentWillUpdate(
      nextProps: BenchmarkPropsType,
      nextState: BenchmarkStateType
    ) {
      if (nextState.running && !this.state.running) {
        this._startTime = Timing.now()
      }
    }
  
    componentDidUpdate() {
      const { forceLayout, sampleCount, timeout, type } = this.props
      const { cycle, running } = this.state
  
      if (running && shouldRecord(cycle, type)) {
        this._samples[cycle].scriptingEnd = Timing.now()
  
        // force style recalc that would otherwise happen before the next frame
        if (forceLayout) {
          this._samples[cycle].layoutStart = Timing.now()
          if (document.body) {
            // eslint-disable-next-line no-unused-expressions
            document.body.offsetWidth
          }
          this._samples[cycle].layoutEnd = Timing.now()
        }
      }
  
      if (running) {
        const now = Timing.now()
        if (
          !isDone(cycle, sampleCount, type) &&
          now - this._startTime < timeout
        ) {
          this._handleCycleComplete()
        } else {
          this._handleComplete(now)
        }
      }
    }
  
    componentWillUnmount() {
      if (this._raf) {
        window.cancelAnimationFrame(this._raf)
      }
    }
  
    render() {
      const { component: Component, type } = this.props
      const { componentProps, cycle, running } = this.state
      if (running && shouldRecord(cycle, type)) {
        this._samples[cycle] = { scriptingStart: Timing.now() }
      }
      return running && shouldRender(cycle, type) ? (
        <Component {...componentProps} />
      ) : null
    }
  
    start() {
      this._samples = []
      this.setState(() => ({ running: true, cycle: 0 }))
    }
  
    _handleCycleComplete() {
      const { getComponentProps, type } = this.props
      const { cycle } = this.state
  
      let componentProps
      if (getComponentProps) {
        // Calculate the component props outside of the time recording (render)
        // so that it doesn't skew results
        componentProps = getComponentProps({ cycle })
        // make sure props always change for update tests
        if (type === BenchmarkType.UPDATE) {
          componentProps['data-test'] = cycle
        }
      }
  
      this._raf = window.requestAnimationFrame(() => {
        this.setState((state: BenchmarkStateType) => ({
          cycle: state.cycle + 1,
          componentProps,
        }))
      })
    }
  
    getSamples(): Array<FullSampleTimingType> {
      return this._samples.reduce(
        (
          memo: Array<FullSampleTimingType>,
          {
            scriptingStart,
            scriptingEnd,
            layoutStart,
            layoutEnd,
          }: SampleTimingType
        ): Array<FullSampleTimingType> => {
          memo.push({
            start: scriptingStart,
            end: layoutEnd || scriptingEnd || 0,
            scriptingStart,
            scriptingEnd: scriptingEnd || 0,
            layoutStart,
            layoutEnd,
          })
          return memo
        },
        []
      )
    }
  
    _handleComplete(endTime: number) {
      const { onComplete } = this.props
      const samples = this.getSamples()
  
      this.setState(() => ({ running: false, cycle: 0 }))
  
      const runTime = endTime - this._startTime
      const sortedElapsedTimes = samples
        .map(({ start, end }) => end - start)
        .sort(sortNumbers)
      const sortedScriptingElapsedTimes = samples
        .map(({ scriptingStart, scriptingEnd }) => scriptingEnd - scriptingStart)
        .sort(sortNumbers)
      const sortedLayoutElapsedTimes = samples
        .map(
          ({ layoutStart, layoutEnd }) => (layoutEnd || 0) - (layoutStart || 0)
        )
        .sort(sortNumbers)
  
      onComplete({
        startTime: this._startTime,
        endTime,
        runTime,
        sampleCount: samples.length,
        samples: samples,
        max: sortedElapsedTimes[sortedElapsedTimes.length - 1],
        min: sortedElapsedTimes[0],
        median: getMedian(sortedElapsedTimes),
        mean: getMean(sortedElapsedTimes),
        stdDev: getStdDev(sortedElapsedTimes),
        meanLayout: getMean(sortedLayoutElapsedTimes),
        meanScripting: getMean(sortedScriptingElapsedTimes),
      })
    }
  }
  