• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1const EE = require('events')
2const fs = require('fs')
3const log = require('./log-shim')
4
5// This is an event emiiter but on/off
6// only listen on a single internal event that gets
7// emitted whenever a timer ends
8class Timers extends EE {
9  file = null
10
11  #unfinished = new Map()
12  #finished = {}
13  #onTimeEnd = Symbol('onTimeEnd')
14  #initialListener = null
15  #initialTimer = null
16
17  constructor ({ listener = null, start = 'npm' } = {}) {
18    super()
19    this.#initialListener = listener
20    this.#initialTimer = start
21    this.#init()
22  }
23
24  get unfinished () {
25    return this.#unfinished
26  }
27
28  get finished () {
29    return this.#finished
30  }
31
32  #init () {
33    this.on()
34    if (this.#initialListener) {
35      this.on(this.#initialListener)
36    }
37    process.emit('time', this.#initialTimer)
38    this.started = this.#unfinished.get(this.#initialTimer)
39  }
40
41  on (listener) {
42    if (listener) {
43      super.on(this.#onTimeEnd, listener)
44    } else {
45      process.on('time', this.#timeListener)
46      process.on('timeEnd', this.#timeEndListener)
47    }
48  }
49
50  off (listener) {
51    if (listener) {
52      super.off(this.#onTimeEnd, listener)
53    } else {
54      this.removeAllListeners(this.#onTimeEnd)
55      process.off('time', this.#timeListener)
56      process.off('timeEnd', this.#timeEndListener)
57    }
58  }
59
60  time (name, fn) {
61    process.emit('time', name)
62    const end = () => process.emit('timeEnd', name)
63    if (typeof fn === 'function') {
64      const res = fn()
65      return res && res.finally ? res.finally(end) : (end(), res)
66    }
67    return end
68  }
69
70  load ({ path } = {}) {
71    if (path) {
72      this.file = `${path}timing.json`
73    }
74  }
75
76  writeFile (metadata) {
77    if (!this.file) {
78      return
79    }
80
81    try {
82      const globalStart = this.started
83      const globalEnd = this.#finished.npm || Date.now()
84      const content = {
85        metadata,
86        timers: this.#finished,
87        // add any unfinished timers with their relative start/end
88        unfinishedTimers: [...this.#unfinished.entries()].reduce((acc, [name, start]) => {
89          acc[name] = [start - globalStart, globalEnd - globalStart]
90          return acc
91        }, {}),
92      }
93      fs.writeFileSync(this.file, JSON.stringify(content) + '\n')
94    } catch (e) {
95      this.file = null
96      log.warn('timing', `could not write timing file: ${e}`)
97    }
98  }
99
100  #timeListener = (name) => {
101    this.#unfinished.set(name, Date.now())
102  }
103
104  #timeEndListener = (name) => {
105    if (this.#unfinished.has(name)) {
106      const ms = Date.now() - this.#unfinished.get(name)
107      this.#finished[name] = ms
108      this.#unfinished.delete(name)
109      this.emit(this.#onTimeEnd, name, ms)
110    } else {
111      log.silly('timing', "Tried to end timer that doesn't exist:", name)
112    }
113  }
114}
115
116module.exports = Timers
117