OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-booking / node_modules / bare-performance / index.js

482 lines · javascript ref:
const { observerType } = require('./lib/constants')
const { InvalidModificationError } = require('./lib/errors')
const binding = require('./binding')

const { TIME_ORIGIN } = binding

exports.now = function now() {
  return binding.now() / 1e6 - TIME_ORIGIN
}

exports.eventLoopUtilization = function eventLoopUtilization(prevUtil, secUtil) {
  if (secUtil) {
    const idle = prevUtil.idle - secUtil.idle
    const active = prevUtil.active - secUtil.active
    return { idle, active, utilization: active / (idle + active) }
  }

  let idle = exports.idleTime()
  if (idle === 0) return { idle: 0, active: 0, utilization: 0 }

  let active = exports.now() - idle
  if (!prevUtil) return { idle, active, utilization: active / (idle + active) }

  idle = idle - prevUtil.idle
  active = active - prevUtil.active

  return { idle, active, utilization: active / (idle + active) }
}

exports.idleTime = function idleTime() {
  return binding.idleTime()
}

exports.metricsInfo = function metricsInfo() {
  return binding.metricsInfo()
}

exports.timeOrigin = TIME_ORIGIN

// For Node.js compatibility
exports.performance = exports

// For Node.js compatibility
class PerformanceNodeTiming {
  get idleTime() {
    return exports.idleTime()
  }

  get uvMetricsInfo() {
    return exports.metricsInfo()
  }
}

// For Node.js compatibility
exports.nodeTiming = new PerformanceNodeTiming()

// Performance Timeline globals
const globalObservers = new Set()
const globalPendingObservers = new Set()
let globalBuffer = []
let pending = false

class PerformanceEntry {
  constructor(name, type, start, duration) {
    this._name = name
    this._type = type
    this._start = start
    this._duration = duration
  }

  get name() {
    return this._name
  }

  get entryType() {
    return this._type
  }

  get startTime() {
    return this._start
  }

  get duration() {
    return this._duration
  }
}

exports.PerformanceEntry = PerformanceEntry

class PerformanceMark extends PerformanceEntry {
  constructor(name, opts = {}) {
    const { startTime = exports.now(), detail = null } = opts

    super(name, 'mark', startTime, 0)

    this._detail = detail
  }

  get detail() {
    return this._detail
  }
}

exports.PerformanceMark = PerformanceMark

class PerformanceMeasure extends PerformanceEntry {
  constructor(name, startTime, duration, opts = {}) {
    const { detail = null } = opts

    super(name, 'measure', startTime, duration)

    this._detail = detail
  }

  get detail() {
    return this._detail
  }
}

exports.PerformanceMeasure = PerformanceMeasure

class PerformanceObserverEntryList {
  constructor(entryList) {
    this._list = entryList
  }

  getEntries() {
    return this._list
  }

  getEntriesByType(type) {
    return this._list.filter((entry) => entry.entryType === type)
  }

  getEntriesByName(name) {
    return this._list.filter((entry) => entry.name === name)
  }
}

exports.PerformanceObserverEntryList = PerformanceObserverEntryList

class PerformanceObserver {
  constructor(cb) {
    this._entryTypes = new Set()
    this._type = observerType.UNDEFINED
    this._buffer = []
    this._cb = cb
  }

  static get supportedEntryTypes() {
    return ['mark', 'measure']
  }

  observe(opts = {}) {
    if ((!opts.entryTypes && !opts.type) || (opts.entryTypes && opts.type)) {
      throw new TypeError('opts.entryTypes OR opts.type must be specified')
    }

    if (
      (this._type === observerType.MULTIPLE && opts.type) ||
      (this._type === observerType.SINGLE && opts.entryTypes)
    ) {
      throw new InvalidModificationError('Cannot change the PerformanceObserver type')
    }

    if (this._type === observerType.UNDEFINED) {
      this._type = opts.entryTypes ? observerType.MULTIPLE : observerType.SINGLE
    }

    this._entryTypes.clear()

    if (this._type === observerType.MULTIPLE) {
      for (const entryType of opts.entryTypes) {
        if (PerformanceObserver.supportedEntryTypes.includes(entryType)) {
          this._entryTypes.add(entryType)
        }
      }
    } else {
      if (PerformanceObserver.supportedEntryTypes.includes(opts.type)) {
        this._entryTypes.add(opts.type)

        if (opts.buffered === true) {
          const bufferedEntries = globalBuffer.filter((entry) => entry.entryType === opts.type)

          if (bufferedEntries.length > 0) {
            this._buffer.push(...bufferedEntries)

            globalPendingObservers.add(this)
            processPendingObservers()
          }
        }
      }
    }

    if (this._entryTypes.size > 0) {
      globalObservers.add(this)
    } else {
      this.disconnect()
    }
  }

  takeRecords() {
    const buf = this._buffer
    this._buffer = []
    return buf
  }

  disconnect() {
    globalObservers.delete(this)
    this._entryTypes.clear()
    this._type = observerType.UNDEFINED
  }
}

exports.PerformanceObserver = PerformanceObserver

exports.mark = function mark(name, opts) {
  const mark = new PerformanceMark(name, opts)

  processEntry(mark)

  globalBuffer.push(mark)

  return mark
}

exports.clearMarks = function clearMarks(name) {
  if (name) {
    globalBuffer = globalBuffer.filter((entry) => entry.name !== name && entry.entryType !== 'mark')
  } else {
    globalBuffer = globalBuffer.filter((entry) => entry.entryType !== 'mark')
  }
}

exports.measure = function measure(name, start, end) {
  let opts = {}

  if (typeof start === 'object' && start !== null && Object.keys(start).length > 0) {
    opts = start

    if (!opts.start && !opts.end) {
      throw new TypeError('opts.start or opts.end must be specified')
    }

    if (opts.name && opts.end && opts.duration) {
      throw new TypeError('One of opts.name, opts.end or opts.duration must not be specified')
    }

    start = opts.start
    end = opts.end
  }

  let endTime

  if (end) {
    endTime = toTimestamp(end)
  } else if (start && opts.duration) {
    endTime = toTimestamp(start) + toTimestamp(duration)
  } else {
    endTime = exports.now()
  }

  let startTime

  if (start) {
    startTime = toTimestamp(start)
  } else if (end && opts.duration) {
    startTime = toTimestamp(end) - toTimestamp(duration)
  } else {
    startTime = 0
  }

  const duration = endTime - startTime

  const measure = new PerformanceMeasure(name, startTime, duration, opts)

  processEntry(measure)

  globalBuffer.push(measure)

  return measure
}

exports.clearMeasures = function clearMeasures(name) {
  if (name) {
    globalBuffer = globalBuffer.filter(
      (entry) => entry.name !== name && entry.entryType !== 'measure'
    )
  } else {
    globalBuffer = globalBuffer.filter((entry) => entry.entryType !== 'measure')
  }
}

exports.getEntries = function getEntries() {
  return new PerformanceObserverEntryList(globalBuffer).getEntries()
}

exports.getEntriesByName = function getEntriesByName(name) {
  return new PerformanceObserverEntryList(globalBuffer).getEntriesByName(name)
}

exports.getEntriesByType = function getEntriesByType(type) {
  return new PerformanceObserverEntryList(globalBuffer).getEntriesByType(type)
}

// https://w3c.github.io/performance-timeline/#queue-a-performanceentry
function processEntry(entry) {
  for (const observer of globalObservers) {
    if (observer._entryTypes.has(entry.entryType)) {
      observer._buffer.push(entry)

      globalPendingObservers.add(observer)
      processPendingObservers()
    }
  }
}

// https://w3c.github.io/performance-timeline/#queue-the-performanceobserver-task
function processPendingObservers() {
  if (pending) return

  pending = true
  setImmediate(() => {
    pending = false

    const observers = Array.from(globalPendingObservers.values())
    globalPendingObservers.clear()

    for (const observer of observers) {
      observer._cb(new exports.PerformanceObserverEntryList(observer.takeRecords()), observer)
    }
  })
}

// https://w3c.github.io/user-timing/#convert-a-mark-to-a-timestamp
function toTimestamp(mark) {
  if (typeof mark === 'string') {
    const performanceMark = globalBuffer.findLast(
      (entry) => entry.name === mark && entry.entryType === 'mark'
    )

    if (performanceMark === undefined) {
      throw new SyntaxError(`Mark ${mark} not found`)
    }

    return performanceMark.startTime
  } else if (typeof mark === 'number') {
    return mark
  }
}

class Histogram {
  constructor(opts = {}) {
    const { lowest = 1, highest = Number.MAX_SAFE_INTEGER, figures = 3 } = opts

    this._handle = binding.histogramInit(this, lowest, highest, figures)

    this._count = 0
    this._exceeds = 0
  }

  get max() {
    return binding.histogramMax(this._handle)
  }

  get min() {
    return binding.histogramMin(this._handle)
  }

  get mean() {
    return binding.histogramMean(this._handle)
  }

  get percentiles() {
    return new Map(binding.histogramPercentiles(this._handle))
  }

  get stddev() {
    return binding.histogramStddev(this._handle)
  }

  get count() {
    return this._count
  }

  get exceeds() {
    return this._exceeds
  }

  percentile(percentile) {
    return binding.histogramPercentile(this._handle, percentile)
  }

  reset() {
    this._count = 0
    this._exceeds = 0

    binding.histogramReset(this._handle)
  }
}

class RecordableHistogram extends Histogram {
  add(other) {
    this._count += other._count
    this._exceeds += other._exceeds

    binding.histogramAdd(this._handle, other._handle)
  }

  record(value) {
    if (binding.histogramRecord(this._handle, value)) this._count++
    else this._exceeds++
  }
}

exports.createHistogram = function createHistogram(opts) {
  return new RecordableHistogram(opts)
}

class IntervalHistogram extends Histogram {
  constructor(opts = {}) {
    const { resolution = 10 } = opts

    super({
      lowest: 1,
      highest: 3_600_000_000_000 // One hour in nanoseconds
    })

    this._resolution = resolution
    this._enabled = false

    this._timerId = -1
    this._timerStartTime = -1
  }

  enable() {
    if (this._enabled === true) return false

    this._timerStartTime = binding.now()
    this._timerId = setInterval(this._oninterval.bind(this), this._resolution)

    this._enabled = true

    return true
  }

  disable() {
    if (this._enabled === false) return false

    clearInterval(this._timerId)

    this._enabled = false

    return true
  }

  _oninterval() {
    const now = binding.now()
    const actual = now - this._timerStartTime
    const expected = this._resolution * 1e6

    const hasDelay = actual > expected

    if (hasDelay) {
      const delay = actual - expected

      if (binding.histogramRecord(this._handle, delay)) this._count++
      else this._exceeds++
    }

    this._timerStartTime = now
  }

  [Symbol.dispose]() {
    this.disable()
  }
}

exports.monitorEventLoopDelay = function monitorEventLoopDelay(opts) {
  return new IntervalHistogram(opts)
}