Skip to content

Commit 103c8c3

Browse files
committed
chore(exit): log any un-ended timings
It will be helpful to us when debugging the "exit handler never called" bugs to know which timings were started but not ended. Tests moved to use real npm. PR-URL: #3479 Credit: @wraithgar Close: #3479 Reviewed-by: @ruyadorno
1 parent feeb8e4 commit 103c8c3

File tree

5 files changed

+59
-57
lines changed

5 files changed

+59
-57
lines changed

lib/base-command.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ class BaseCommand {
7575
}
7676

7777
async setWorkspaces (filters) {
78-
// TODO npm guards workspaces/global mode so we should use this.npm.prefix?
7978
const ws = await getWorkspaces(filters, { path: this.npm.localPrefix })
8079
this.workspaces = ws
8180
this.workspaceNames = [...ws.keys()]

lib/npm.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const npm = module.exports = new class extends EventEmitter {
5050
constructor () {
5151
super()
5252
// TODO make this only ever load once (or unload) in tests
53-
require('./utils/perf.js')
53+
this.timers = require('./utils/perf.js').timers
5454
this.started = Date.now()
5555
this.command = null
5656
this.commands = proxyCmds

lib/utils/exit-handler.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,14 @@ process.on('timing', (name, value) => {
3232
})
3333

3434
process.on('exit', code => {
35+
// process.emit is synchronous, so the timeEnd handler will run before the
36+
// unfinished timer check below
3537
process.emit('timeEnd', 'npm')
3638
log.disableProgress()
37-
if (npm.config && npm.config.loaded && npm.config.get('timing')) {
39+
for (const [name, timers] of npm.timers)
40+
log.verbose('unfinished npm timer', name, timers)
41+
42+
if (npm.config.loaded && npm.config.get('timing')) {
3843
try {
3944
const file = path.resolve(npm.config.get('cache'), '_timing.json')
4045
const dir = path.dirname(npm.config.get('cache'))
@@ -69,7 +74,7 @@ process.on('exit', code => {
6974
}
7075
}
7176
// In timing mode we always write the log file
72-
if (npm.config && npm.config.loaded && npm.config.get('timing') && !wroteLogFile)
77+
if (npm.config.loaded && npm.config.get('timing') && !wroteLogFile)
7378
writeLogFile()
7479
if (wroteLogFile) {
7580
// just a line break
@@ -113,7 +118,7 @@ const exit = (code, noLog) => {
113118

114119
const exitHandler = (err) => {
115120
log.disableProgress()
116-
if (!npm.config || !npm.config.loaded) {
121+
if (!npm.config.loaded) {
117122
// logging won't work unless we pretend that it's ready
118123
err = err || new Error('Exit prior to config file resolving.')
119124
console.error(err.stack || err.message)
@@ -180,7 +185,7 @@ const exitHandler = (err) => {
180185
for (const errline of [...msg.summary, ...msg.detail])
181186
log.error(...errline)
182187

183-
if (npm.config && npm.config.get('json')) {
188+
if (npm.config.loaded && npm.config.get('json')) {
184189
const error = {
185190
error: {
186191
code: err.code,

lib/utils/perf.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
const log = require('npmlog')
2-
const timings = new Map()
2+
const timers = new Map()
33

44
process.on('time', (name) => {
5-
timings.set(name, Date.now())
5+
timers.set(name, Date.now())
66
})
77

88
process.on('timeEnd', (name) => {
9-
if (timings.has(name)) {
10-
const ms = Date.now() - timings.get(name)
9+
if (timers.has(name)) {
10+
const ms = Date.now() - timers.get(name)
1111
process.emit('timing', name, ms)
1212
log.timing(name, `Completed in ${ms}ms`)
13-
timings.delete(name)
13+
timers.delete(name)
1414
} else
1515
log.silly('timing', "Tried to end timer that doesn't exist:", name)
1616
})
1717

18+
exports.timers = timers
1819
// for tests
1920
/* istanbul ignore next */
2021
exports.reset = () => {

test/lib/utils/exit-handler.js

Lines changed: 43 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
/* eslint-disable no-extend-native */
22
/* eslint-disable no-global-assign */
3+
const t = require('tap')
34
const EventEmitter = require('events')
45
const writeFileAtomic = require('write-file-atomic')
5-
const t = require('tap')
6+
7+
const { real: mockNpm } = require('../../fixtures/mock-npm')
68

79
// NOTE: Although these unit tests may look like the rest on the surface,
810
// they are in fact very special due to the amount of things hooking directly
@@ -25,23 +27,14 @@ t.cleanSnapshot = (str) => redactCwd(str)
2527

2628
// internal modules mocks
2729
const cacheFolder = t.testdir({})
28-
const config = {
29-
values: {
30-
cache: cacheFolder,
31-
timing: true,
32-
},
33-
loaded: true,
34-
updateNotification: null,
35-
get (key) {
36-
return this.values[key]
37-
},
38-
}
3930

40-
const npm = {
41-
version: '1.0.0',
42-
config,
43-
shelloutCommands: ['exec', 'run-script'],
44-
}
31+
const { npm } = mockNpm(t)
32+
33+
t.before(async () => {
34+
npm.version = '1.0.0'
35+
await npm.load()
36+
npm.config.set('cache', cacheFolder)
37+
})
4538

4639
const npmlog = {
4740
disableProgress: () => null,
@@ -190,9 +183,15 @@ t.test('handles unknown error', (t) => {
190183
t.test('npm.config not ready', (t) => {
191184
t.plan(1)
192185

193-
config.loaded = false
194-
186+
const { npm: unloaded } = mockNpm(t)
195187
const _error = console.error
188+
189+
t.teardown(() => {
190+
console.error = _error
191+
exitHandler.setNpm(npm)
192+
})
193+
194+
exitHandler.setNpm(unloaded)
196195
console.error = (msg) => {
197196
t.match(
198197
msg,
@@ -202,25 +201,20 @@ t.test('npm.config not ready', (t) => {
202201
}
203202

204203
exitHandler()
205-
206-
t.teardown(() => {
207-
console.error = _error
208-
config.loaded = true
209-
})
210204
})
211205

212206
t.test('fail to write logfile', (t) => {
213207
t.plan(1)
214208

209+
t.teardown(() => {
210+
npm.config.set('cache', cacheFolder)
211+
})
212+
215213
const badDir = t.testdir({
216214
_logs: 'is a file',
217215
})
218216

219-
config.values.cache = badDir
220-
221-
t.teardown(() => {
222-
config.values.cache = cacheFolder
223-
})
217+
npm.config.set('cache', badDir)
224218

225219
t.doesNotThrow(
226220
() => exitHandler(err),
@@ -231,7 +225,11 @@ t.test('fail to write logfile', (t) => {
231225
t.test('console.log output using --json', (t) => {
232226
t.plan(1)
233227

234-
config.values.json = true
228+
npm.config.set('json', true)
229+
t.teardown(() => {
230+
console.error = _error
231+
npm.config.set('json', false)
232+
})
235233

236234
const _error = console.error
237235
console.error = (jsonOutput) => {
@@ -249,11 +247,6 @@ t.test('console.log output using --json', (t) => {
249247
}
250248

251249
exitHandler(new Error('Error: EBADTHING Something happened'))
252-
253-
t.teardown(() => {
254-
console.error = _error
255-
delete config.values.json
256-
})
257250
})
258251

259252
t.test('throw a non-error obj', (t) => {
@@ -354,7 +347,12 @@ t.test('on exit handler', (t) => {
354347
t.test('it worked', (t) => {
355348
t.plan(2)
356349

357-
config.values.timing = false
350+
npm.config.set('timing', false)
351+
352+
t.teardown(() => {
353+
process.exit = _exit
354+
npm.config.set('timing', true)
355+
})
358356

359357
const _exit = process.exit
360358
process.exit = (code) => {
@@ -370,11 +368,6 @@ t.test('it worked', (t) => {
370368
process.emit('exit', 0)
371369
}
372370

373-
t.teardown(() => {
374-
process.exit = _exit
375-
config.values.timing = true
376-
})
377-
378371
exitHandler()
379372
})
380373

@@ -466,6 +459,14 @@ t.test('exit handler called twice', (t) => {
466459
t.test('defaults to log error msg if stack is missing', (t) => {
467460
t.plan(1)
468461

462+
const { npm: unloaded } = mockNpm(t)
463+
const _error = console.error
464+
465+
t.teardown(() => {
466+
console.error = _error
467+
exitHandler.setNpm(npm)
468+
})
469+
469470
const noStackErr = Object.assign(
470471
new Error('Error with no stack'),
471472
{
@@ -475,15 +476,11 @@ t.test('defaults to log error msg if stack is missing', (t) => {
475476
)
476477
delete noStackErr.stack
477478

478-
npm.config.loaded = false
479-
480-
const _error = console.error
481479
console.error = (msg) => {
482-
console.error = _error
483-
npm.config.loaded = true
484480
t.equal(msg, 'Error with no stack', 'should use error msg')
485481
}
486482

483+
exitHandler.setNpm(unloaded)
487484
exitHandler(noStackErr)
488485
})
489486

0 commit comments

Comments
 (0)