/
detect-async-leaks.ts
93 lines (80 loc) · 2.39 KB
/
detect-async-leaks.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
import asyncHooks from 'node:async_hooks'
import { promisify } from 'node:util'
import { relative } from 'pathe'
import { rpc } from '../../rpc'
import { VitestTestRunner } from '../test'
export interface HangingOps {
error: Error
taskId?: string
}
const asyncSleep = promisify(setTimeout)
export class WithAsyncLeaksDetecter extends VitestTestRunner {
private hangingOps: Map<number, HangingOps> = new Map()
private asyncHook: asyncHooks.AsyncHook = asyncHooks.createHook({
init: (asyncId, type, triggerAsyncId) => {
// Ignore some async resources
if (
[
'PROMISE',
'TIMERWRAP',
'ELDHISTOGRAM',
'PerformanceObserver',
'RANDOMBYTESREQUEST',
'DNSCHANNEL',
'ZLIB',
'SIGNREQUEST',
'TLSWRAP',
'TCPWRAP',
].includes(type)
)
return
const task = this.workerState.current
const filepath = task?.file?.filepath || this.workerState.filepath
if (!filepath)
return
const { stackTraceLimit } = Error
Error.stackTraceLimit = Math.max(100, stackTraceLimit)
const error = new Error(type)
let fromUser = error.stack?.includes(filepath)
let directlyTriggered = true
if (!fromUser) {
// Check if the async resource is indirectly triggered by user code
const trigger = this.hangingOps.get(triggerAsyncId)
if (trigger) {
fromUser = true
directlyTriggered = false
error.stack = trigger.error.stack
}
}
if (fromUser) {
const relativePath = relative(this.config.root, filepath)
if (directlyTriggered) {
error.stack = error.stack
?.split(/\n\s+/)
.findLast(s => s.includes(filepath))
?.replace(filepath, relativePath)
}
this.hangingOps.set(asyncId, {
error,
taskId: task?.id || relativePath,
})
}
},
destroy: (asyncId) => {
this.hangingOps.delete(asyncId)
},
})
onBeforeRunFiles() {
super.onBeforeRunFiles()
this.asyncHook.enable()
}
async onAfterRunFiles() {
// Wait for async resources to be destroyed
await asyncSleep(0)
if (this.hangingOps.size > 0)
await asyncSleep(0)
rpc().detectAsyncLeaks(Array.from(this.hangingOps.values()))
this.asyncHook.disable()
super.onAfterRunFiles()
}
}