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