-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathanalyze.mts
More file actions
135 lines (130 loc) · 4.79 KB
/
analyze.mts
File metadata and controls
135 lines (130 loc) · 4.79 KB
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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import { AnalysisStateMachine, PromiseState } from './analysis.mts'
let target = process.env.PROMISE_DIAGNOSTICS_HOOK_LOG
if (!target) {
throw new Error('PROMISE_DIAGNOSTICS_HOOK_LOG environment variable must be set to a file path')
}
import fs from 'node:fs'
import { LineProcessor } from './line-processor.mts'
async function* tailFileLines(filePath) {
const bufferSize = 100//1024 * 4
const buffer = new Uint8Array(bufferSize)
const fd = await fs.promises.open(filePath, 'r')
let position = 0
let leftover = new Uint8Array(0)
async function* drain() {
let current_eof = (await fd.stat()).size
while (true) {
// this could be after many reads so we want to sanity check before waiting on a change event
current_eof = (await fd.stat()).size
if (position >= current_eof) {
break
}
const decoder = new TextDecoder()
while (position < current_eof) {
const { bytesRead } = await fd.read(buffer, 0, bufferSize, position)
position += bytesRead
let tmp = new Uint8Array(leftover.byteLength + bytesRead)
tmp.set(leftover, 0)
tmp.set(buffer.subarray(0, bytesRead), leftover.byteLength)
leftover = tmp
let newlineIndex = leftover.indexOf('\n'.charCodeAt(0))
while (newlineIndex !== -1) {
const line = decoder.decode(leftover.subarray(0, newlineIndex))
leftover = leftover.subarray(newlineIndex + 1)
yield line
newlineIndex = leftover.indexOf('\n'.charCodeAt(0))
}
if (bytesRead === 0) {
break // No more data to read?
}
}
}
}
try {
yield* drain()
let watcher = fs.promises.watch(filePath, { persistent: true })
for await (const info of watcher) {
if (info.eventType === 'change') {
yield* drain()
}
}
} finally {
await fd.close()
}
}
let analysis = new AnalysisStateMachine()
const lineProcessor = LineProcessor.createNewDefaultProcessor()
for await (const line of tailFileLines(target)) {
try {
let cmd = JSON.parse(line)
if (cmd[0] === 'end') {
break
}
analysis.insertNextObservation(lineProcessor.decode(cmd[0], cmd.slice(1)))
} catch (e) {
console.error('Error processing line:', line, e)
}
}
//
const unused_allocations: Map<string, PromiseState[]> = new Map()
const overused_allocations_to_unwraps: Map<string, Map<string, PromiseState[]>> = new Map()
for (const msg of analysis.end()) {
let log = false
if (!msg.init.stack) {
continue
}
// ignore promises that are not directly allocated due to user code
if (/^(node:[^\n]*(\n|$))*$/.test(msg.init.stack)) {
continue
}
// some v8 promises are allocated due to user code but are internal and
// often missing linkage, this leads to them being too noisy
if (msg.internal) {
continue
}
if (msg.unwrappedBy.size === 0) {
if (msg.isModuleInitialization) {
continue
}
if (msg.executed) {
if (msg.isAwait) {
continue
}
}
log = true
} else if (msg.unwrappedBy.size > 1) {
log = true
}
if (log) {
if (msg.unwrappedBy.size === 0) {
const shared_allocs = unused_allocations.get(msg.init.stack) ?? []
shared_allocs.push(msg)
unused_allocations.set(msg.init.stack, shared_allocs)
} else {
const shared_allocs = overused_allocations_to_unwraps.get(msg.init.stack) ?? new Map()
overused_allocations_to_unwraps.set(msg.init.stack, shared_allocs)
for (const unwrappedBy of msg.unwrappedBy) {
const unwrapStack = analysis.promises.get(unwrappedBy)!.init.stack
const shared_unwraps = shared_allocs.get(unwrapStack) ?? []
shared_unwraps.push(msg)
shared_allocs.set(unwrapStack, shared_unwraps)
}
}
}
}
analysis = null
for (const [stack, allocs] of unused_allocations) {
console.group(`Unused Promise ${allocs.map(a => a.init.asyncId).join(', ')} (allocated ${allocs.length} times):`)
console.log(stack)
console.groupEnd()
}
for (const [stack, unwraps] of overused_allocations_to_unwraps) {
console.group(`Overused Promise times:`)
console.log(stack)
for (const [unwrapStack, allocs] of unwraps) {
console.group(`Unwrapped ${allocs.length} times (${allocs.map(a => a.init.asyncId).join(', ')}) at:`)
console.log(unwrapStack)
console.groupEnd()
}
console.groupEnd()
}