pm2-perfmonitor 2.5.2 → 2.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +59 -59
- package/lib/alert.js +29 -29
- package/lib/app.js +581 -516
- package/lib/defaults.js +123 -123
- package/lib/execa-helper.js +17 -17
- package/lib/job-conf.js +39 -39
- package/lib/message.js +35 -35
- package/lib/perf-sampler.js +241 -241
- package/lib/pm2-extra.js +54 -54
- package/lib/utils.js +77 -62
- package/lib/zombie-check.js +65 -65
- package/package.json +2 -2
package/lib/perf-sampler.js
CHANGED
|
@@ -1,241 +1,241 @@
|
|
|
1
|
-
const path = require('path')
|
|
2
|
-
|
|
3
|
-
const fs = require('fs-extra')
|
|
4
|
-
|
|
5
|
-
const { getExeca } = require('./execa-helper')
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* 执行命令(不通过 shell,直接使用参数数组)
|
|
9
|
-
* @param {string} cmd - 命令名称
|
|
10
|
-
* @param {string[]} args - 参数列表
|
|
11
|
-
* @param {object} options - execa 选项
|
|
12
|
-
* @returns {Promise<boolean>} 是否成功
|
|
13
|
-
*/
|
|
14
|
-
const execCommand = async (cmd, args, options = {}) => {
|
|
15
|
-
try {
|
|
16
|
-
const execa = await getExeca()
|
|
17
|
-
|
|
18
|
-
await execa(cmd, args, options)
|
|
19
|
-
|
|
20
|
-
return true
|
|
21
|
-
} catch (err) {
|
|
22
|
-
console.error(`Command failed: [${cmd} ${args.join(' ')}]`, err.message)
|
|
23
|
-
return false
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* 生成安全的文件时间戳(不依赖区域)
|
|
29
|
-
*/
|
|
30
|
-
const getSafeTimestamp = () => {
|
|
31
|
-
const now = new Date()
|
|
32
|
-
const y = now.getFullYear()
|
|
33
|
-
const m = String(now.getMonth() + 1).padStart(2, '0')
|
|
34
|
-
const d = String(now.getDate()).padStart(2, '0')
|
|
35
|
-
const h = String(now.getHours()).padStart(2, '0')
|
|
36
|
-
const min = String(now.getMinutes()).padStart(2, '0')
|
|
37
|
-
const s = String(now.getSeconds()).padStart(2, '0')
|
|
38
|
-
return `${y}${m}${d}_${h}${min}${s}`
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* 执行 Perf 采样并生成火焰图
|
|
43
|
-
* @param {Object} options - 配置项
|
|
44
|
-
* @param {number} options.pid - 进程 PID
|
|
45
|
-
* @param {string} options.moduleName - 模块名(用于日志前缀)
|
|
46
|
-
* @param {string} options.perfDir - Perf 文件存储目录(当未提供 perfDataFile 时用于生成默认路径)
|
|
47
|
-
* @param {string} options.flamegraphDir - 火焰图工具目录
|
|
48
|
-
* @param {number} [options.sampleDuration=10] - 采样时长(秒)
|
|
49
|
-
* @param {number} [options.sampleFrequency=99] - 采样频率(Hz)
|
|
50
|
-
* @param {string} [options.perfDataFile] - 自定义 perf 数据文件路径(若未提供则自动生成)
|
|
51
|
-
* @param {boolean} [options.keepPerfData=false] - 是否保留原始 perf 数据文件(默认 false,即采样后删除)
|
|
52
|
-
*/
|
|
53
|
-
const performPerfSampling = async ({
|
|
54
|
-
pid,
|
|
55
|
-
moduleName,
|
|
56
|
-
perfDir,
|
|
57
|
-
flamegraphDir,
|
|
58
|
-
sampleDuration = 10,
|
|
59
|
-
sampleFrequency = 99,
|
|
60
|
-
perfDataFile: customPerfDataFile,
|
|
61
|
-
keepPerfData = false,
|
|
62
|
-
}) => {
|
|
63
|
-
const logger = (type, ...args) => {
|
|
64
|
-
console[type](`[${moduleName}]`, ...args)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// --- 参数校验 ---
|
|
68
|
-
if (!perfDir) {
|
|
69
|
-
logger('error', 'perfDir cannot be empty')
|
|
70
|
-
return
|
|
71
|
-
}
|
|
72
|
-
if (!flamegraphDir) {
|
|
73
|
-
logger('error', 'flamegraphDir cannot be empty')
|
|
74
|
-
return
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// PID 必须为数字且为正整数
|
|
78
|
-
const pidNum = Number(pid)
|
|
79
|
-
if (!Number.isInteger(pidNum) || pidNum <= 0) {
|
|
80
|
-
logger('error', `Invalid PID: ${pid} – must be a positive integer`)
|
|
81
|
-
return
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const finalDuration =
|
|
85
|
-
typeof sampleDuration === 'number' && sampleDuration > 0
|
|
86
|
-
? sampleDuration
|
|
87
|
-
: 10
|
|
88
|
-
const finalFrequency =
|
|
89
|
-
typeof sampleFrequency === 'number' && sampleFrequency > 0
|
|
90
|
-
? sampleFrequency
|
|
91
|
-
: 99
|
|
92
|
-
|
|
93
|
-
// 确保 perf 目录存在(用于默认路径,或自定义路径的父目录)
|
|
94
|
-
try {
|
|
95
|
-
await fs.ensureDir(perfDir)
|
|
96
|
-
logger('info', `Perf directory ready: ${perfDir}`)
|
|
97
|
-
} catch (err) {
|
|
98
|
-
logger('error', `Failed to create perf directory: ${err.message}`)
|
|
99
|
-
return
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// 检查 perf 权限
|
|
103
|
-
try {
|
|
104
|
-
const execa = await getExeca()
|
|
105
|
-
|
|
106
|
-
await execa('perf', ['--version'], { timeout: 5000 })
|
|
107
|
-
|
|
108
|
-
logger('info', 'Perf permission check passed')
|
|
109
|
-
} catch (err) {
|
|
110
|
-
logger('error', `Perf permission check failed: ${err.message}`)
|
|
111
|
-
logger(
|
|
112
|
-
'error',
|
|
113
|
-
`Please ensure the perf command is installed and the user has permission to run it.\n
|
|
114
|
-
You can configure the system to allow non-root perf by setting:\n
|
|
115
|
-
"echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid"\n
|
|
116
|
-
"sudo setcap cap_sys_admin+ep $(which perf)"`,
|
|
117
|
-
)
|
|
118
|
-
return // 无权限则直接退出
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// 生成时间戳(仅当需要默认路径时)
|
|
122
|
-
const timestamp = getSafeTimestamp()
|
|
123
|
-
|
|
124
|
-
// 确定 perf 数据文件路径
|
|
125
|
-
let perfDataFile
|
|
126
|
-
if (customPerfDataFile) {
|
|
127
|
-
perfDataFile = customPerfDataFile
|
|
128
|
-
// 确保自定义路径的父目录存在
|
|
129
|
-
const parentDir = path.dirname(perfDataFile)
|
|
130
|
-
try {
|
|
131
|
-
await fs.ensureDir(parentDir)
|
|
132
|
-
} catch (err) {
|
|
133
|
-
logger(
|
|
134
|
-
'error',
|
|
135
|
-
`Failed to create directory for custom perfDataFile: ${err.message}`,
|
|
136
|
-
)
|
|
137
|
-
return
|
|
138
|
-
}
|
|
139
|
-
} else {
|
|
140
|
-
perfDataFile = path.join(perfDir, `perf.${pidNum}.${timestamp}.data`)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// 定义其他文件路径(基于 perfDir 和时间戳,与 perfDataFile 解耦)
|
|
144
|
-
const perfStacksFile = path.join(
|
|
145
|
-
perfDir,
|
|
146
|
-
`perf.${pidNum}.${timestamp}.stacks`,
|
|
147
|
-
)
|
|
148
|
-
const perfFoldedFile = path.join(
|
|
149
|
-
perfDir,
|
|
150
|
-
`perf.${pidNum}.${timestamp}.folded`,
|
|
151
|
-
)
|
|
152
|
-
const perfSvgFile = path.join(perfDir, `perf.${pidNum}.${timestamp}.svg`)
|
|
153
|
-
|
|
154
|
-
try {
|
|
155
|
-
logger(
|
|
156
|
-
'info',
|
|
157
|
-
`PID:${pidNum} Starting perf sampling (${finalDuration}s, ${finalFrequency}Hz)`,
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
// --- Step 1: perf record ---
|
|
161
|
-
const recordOk = await execCommand('perf', [
|
|
162
|
-
'record',
|
|
163
|
-
'-o',
|
|
164
|
-
perfDataFile,
|
|
165
|
-
'-F',
|
|
166
|
-
String(finalFrequency),
|
|
167
|
-
'-p',
|
|
168
|
-
String(pidNum),
|
|
169
|
-
'-g',
|
|
170
|
-
'--',
|
|
171
|
-
'sleep',
|
|
172
|
-
String(finalDuration),
|
|
173
|
-
])
|
|
174
|
-
if (!recordOk) return
|
|
175
|
-
|
|
176
|
-
// --- Step 2: perf script 导出为文本堆栈 ---
|
|
177
|
-
const scriptOk = await execCommand('perf', ['script', '-i', perfDataFile], {
|
|
178
|
-
stdout: {
|
|
179
|
-
file: perfStacksFile,
|
|
180
|
-
},
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
if (!scriptOk) return
|
|
184
|
-
|
|
185
|
-
logger('info', `PID:${pidNum} Perf sampling completed: ${perfStacksFile}`)
|
|
186
|
-
|
|
187
|
-
// 根据 keepPerfData 决定是否删除原始数据文件
|
|
188
|
-
if (!keepPerfData) {
|
|
189
|
-
await fs.remove(perfDataFile).catch(() => {})
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// --- Step 3: 检查火焰图工具 ---
|
|
193
|
-
const stackcollapsePath = path.join(flamegraphDir, 'stackcollapse-perf.pl')
|
|
194
|
-
const flamegraphPath = path.join(flamegraphDir, 'flamegraph.pl')
|
|
195
|
-
|
|
196
|
-
const isStackcollapseValid = await fs
|
|
197
|
-
.access(stackcollapsePath, fs.constants.X_OK)
|
|
198
|
-
.then(() => true)
|
|
199
|
-
.catch(() => false)
|
|
200
|
-
const isFlamegraphValid = await fs
|
|
201
|
-
.access(flamegraphPath, fs.constants.X_OK)
|
|
202
|
-
.then(() => true)
|
|
203
|
-
.catch(() => false)
|
|
204
|
-
|
|
205
|
-
if (isStackcollapseValid && isFlamegraphValid) {
|
|
206
|
-
// --- Step 4: 生成折叠文件 ---
|
|
207
|
-
const collapseOk = await execCommand(
|
|
208
|
-
stackcollapsePath,
|
|
209
|
-
[perfStacksFile],
|
|
210
|
-
{
|
|
211
|
-
stdout: {
|
|
212
|
-
file: perfFoldedFile,
|
|
213
|
-
},
|
|
214
|
-
},
|
|
215
|
-
)
|
|
216
|
-
if (!collapseOk) return
|
|
217
|
-
|
|
218
|
-
// --- Step 5: 生成 SVG 火焰图 ---
|
|
219
|
-
const flameOk = await execCommand(flamegraphPath, [perfFoldedFile], {
|
|
220
|
-
stdout: {
|
|
221
|
-
file: perfSvgFile,
|
|
222
|
-
},
|
|
223
|
-
})
|
|
224
|
-
if (flameOk) {
|
|
225
|
-
logger('info', `PID:${pidNum} Flame graph generated: ${perfSvgFile}`)
|
|
226
|
-
}
|
|
227
|
-
} else {
|
|
228
|
-
const missing = []
|
|
229
|
-
if (!isStackcollapseValid) missing.push('stackcollapse-perf.pl')
|
|
230
|
-
if (!isFlamegraphValid) missing.push('flamegraph.pl')
|
|
231
|
-
logger(
|
|
232
|
-
'info',
|
|
233
|
-
`PID:${pidNum} Skip flame graph – missing/not executable: ${missing.join(', ')}`,
|
|
234
|
-
)
|
|
235
|
-
}
|
|
236
|
-
} catch (err) {
|
|
237
|
-
logger('error', `PID:${pidNum} Perf sampling exception: ${err.message}`)
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
module.exports = { performPerfSampling }
|
|
1
|
+
const path = require('path')
|
|
2
|
+
|
|
3
|
+
const fs = require('fs-extra')
|
|
4
|
+
|
|
5
|
+
const { getExeca } = require('./execa-helper')
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 执行命令(不通过 shell,直接使用参数数组)
|
|
9
|
+
* @param {string} cmd - 命令名称
|
|
10
|
+
* @param {string[]} args - 参数列表
|
|
11
|
+
* @param {object} options - execa 选项
|
|
12
|
+
* @returns {Promise<boolean>} 是否成功
|
|
13
|
+
*/
|
|
14
|
+
const execCommand = async (cmd, args, options = {}) => {
|
|
15
|
+
try {
|
|
16
|
+
const execa = await getExeca()
|
|
17
|
+
|
|
18
|
+
await execa(cmd, args, options)
|
|
19
|
+
|
|
20
|
+
return true
|
|
21
|
+
} catch (err) {
|
|
22
|
+
console.error(`Command failed: [${cmd} ${args.join(' ')}]`, err.message)
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 生成安全的文件时间戳(不依赖区域)
|
|
29
|
+
*/
|
|
30
|
+
const getSafeTimestamp = () => {
|
|
31
|
+
const now = new Date()
|
|
32
|
+
const y = now.getFullYear()
|
|
33
|
+
const m = String(now.getMonth() + 1).padStart(2, '0')
|
|
34
|
+
const d = String(now.getDate()).padStart(2, '0')
|
|
35
|
+
const h = String(now.getHours()).padStart(2, '0')
|
|
36
|
+
const min = String(now.getMinutes()).padStart(2, '0')
|
|
37
|
+
const s = String(now.getSeconds()).padStart(2, '0')
|
|
38
|
+
return `${y}${m}${d}_${h}${min}${s}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 执行 Perf 采样并生成火焰图
|
|
43
|
+
* @param {Object} options - 配置项
|
|
44
|
+
* @param {number} options.pid - 进程 PID
|
|
45
|
+
* @param {string} options.moduleName - 模块名(用于日志前缀)
|
|
46
|
+
* @param {string} options.perfDir - Perf 文件存储目录(当未提供 perfDataFile 时用于生成默认路径)
|
|
47
|
+
* @param {string} options.flamegraphDir - 火焰图工具目录
|
|
48
|
+
* @param {number} [options.sampleDuration=10] - 采样时长(秒)
|
|
49
|
+
* @param {number} [options.sampleFrequency=99] - 采样频率(Hz)
|
|
50
|
+
* @param {string} [options.perfDataFile] - 自定义 perf 数据文件路径(若未提供则自动生成)
|
|
51
|
+
* @param {boolean} [options.keepPerfData=false] - 是否保留原始 perf 数据文件(默认 false,即采样后删除)
|
|
52
|
+
*/
|
|
53
|
+
const performPerfSampling = async ({
|
|
54
|
+
pid,
|
|
55
|
+
moduleName,
|
|
56
|
+
perfDir,
|
|
57
|
+
flamegraphDir,
|
|
58
|
+
sampleDuration = 10,
|
|
59
|
+
sampleFrequency = 99,
|
|
60
|
+
perfDataFile: customPerfDataFile,
|
|
61
|
+
keepPerfData = false,
|
|
62
|
+
}) => {
|
|
63
|
+
const logger = (type, ...args) => {
|
|
64
|
+
console[type](`[${moduleName}]`, ...args)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --- 参数校验 ---
|
|
68
|
+
if (!perfDir) {
|
|
69
|
+
logger('error', 'perfDir cannot be empty')
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
if (!flamegraphDir) {
|
|
73
|
+
logger('error', 'flamegraphDir cannot be empty')
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// PID 必须为数字且为正整数
|
|
78
|
+
const pidNum = Number(pid)
|
|
79
|
+
if (!Number.isInteger(pidNum) || pidNum <= 0) {
|
|
80
|
+
logger('error', `Invalid PID: ${pid} – must be a positive integer`)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const finalDuration =
|
|
85
|
+
typeof sampleDuration === 'number' && sampleDuration > 0
|
|
86
|
+
? sampleDuration
|
|
87
|
+
: 10
|
|
88
|
+
const finalFrequency =
|
|
89
|
+
typeof sampleFrequency === 'number' && sampleFrequency > 0
|
|
90
|
+
? sampleFrequency
|
|
91
|
+
: 99
|
|
92
|
+
|
|
93
|
+
// 确保 perf 目录存在(用于默认路径,或自定义路径的父目录)
|
|
94
|
+
try {
|
|
95
|
+
await fs.ensureDir(perfDir)
|
|
96
|
+
logger('info', `Perf directory ready: ${perfDir}`)
|
|
97
|
+
} catch (err) {
|
|
98
|
+
logger('error', `Failed to create perf directory: ${err.message}`)
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 检查 perf 权限
|
|
103
|
+
try {
|
|
104
|
+
const execa = await getExeca()
|
|
105
|
+
|
|
106
|
+
await execa('perf', ['--version'], { timeout: 5000 })
|
|
107
|
+
|
|
108
|
+
logger('info', 'Perf permission check passed')
|
|
109
|
+
} catch (err) {
|
|
110
|
+
logger('error', `Perf permission check failed: ${err.message}`)
|
|
111
|
+
logger(
|
|
112
|
+
'error',
|
|
113
|
+
`Please ensure the perf command is installed and the user has permission to run it.\n
|
|
114
|
+
You can configure the system to allow non-root perf by setting:\n
|
|
115
|
+
"echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid"\n
|
|
116
|
+
"sudo setcap cap_sys_admin+ep $(which perf)"`,
|
|
117
|
+
)
|
|
118
|
+
return // 无权限则直接退出
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 生成时间戳(仅当需要默认路径时)
|
|
122
|
+
const timestamp = getSafeTimestamp()
|
|
123
|
+
|
|
124
|
+
// 确定 perf 数据文件路径
|
|
125
|
+
let perfDataFile
|
|
126
|
+
if (customPerfDataFile) {
|
|
127
|
+
perfDataFile = customPerfDataFile
|
|
128
|
+
// 确保自定义路径的父目录存在
|
|
129
|
+
const parentDir = path.dirname(perfDataFile)
|
|
130
|
+
try {
|
|
131
|
+
await fs.ensureDir(parentDir)
|
|
132
|
+
} catch (err) {
|
|
133
|
+
logger(
|
|
134
|
+
'error',
|
|
135
|
+
`Failed to create directory for custom perfDataFile: ${err.message}`,
|
|
136
|
+
)
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
perfDataFile = path.join(perfDir, `perf.${pidNum}.${timestamp}.data`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 定义其他文件路径(基于 perfDir 和时间戳,与 perfDataFile 解耦)
|
|
144
|
+
const perfStacksFile = path.join(
|
|
145
|
+
perfDir,
|
|
146
|
+
`perf.${pidNum}.${timestamp}.stacks`,
|
|
147
|
+
)
|
|
148
|
+
const perfFoldedFile = path.join(
|
|
149
|
+
perfDir,
|
|
150
|
+
`perf.${pidNum}.${timestamp}.folded`,
|
|
151
|
+
)
|
|
152
|
+
const perfSvgFile = path.join(perfDir, `perf.${pidNum}.${timestamp}.svg`)
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
logger(
|
|
156
|
+
'info',
|
|
157
|
+
`PID:${pidNum} Starting perf sampling (${finalDuration}s, ${finalFrequency}Hz)`,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
// --- Step 1: perf record ---
|
|
161
|
+
const recordOk = await execCommand('perf', [
|
|
162
|
+
'record',
|
|
163
|
+
'-o',
|
|
164
|
+
perfDataFile,
|
|
165
|
+
'-F',
|
|
166
|
+
String(finalFrequency),
|
|
167
|
+
'-p',
|
|
168
|
+
String(pidNum),
|
|
169
|
+
'-g',
|
|
170
|
+
'--',
|
|
171
|
+
'sleep',
|
|
172
|
+
String(finalDuration),
|
|
173
|
+
])
|
|
174
|
+
if (!recordOk) return
|
|
175
|
+
|
|
176
|
+
// --- Step 2: perf script 导出为文本堆栈 ---
|
|
177
|
+
const scriptOk = await execCommand('perf', ['script', '-i', perfDataFile], {
|
|
178
|
+
stdout: {
|
|
179
|
+
file: perfStacksFile,
|
|
180
|
+
},
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
if (!scriptOk) return
|
|
184
|
+
|
|
185
|
+
logger('info', `PID:${pidNum} Perf sampling completed: ${perfStacksFile}`)
|
|
186
|
+
|
|
187
|
+
// 根据 keepPerfData 决定是否删除原始数据文件
|
|
188
|
+
if (!keepPerfData) {
|
|
189
|
+
await fs.remove(perfDataFile).catch(() => {})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// --- Step 3: 检查火焰图工具 ---
|
|
193
|
+
const stackcollapsePath = path.join(flamegraphDir, 'stackcollapse-perf.pl')
|
|
194
|
+
const flamegraphPath = path.join(flamegraphDir, 'flamegraph.pl')
|
|
195
|
+
|
|
196
|
+
const isStackcollapseValid = await fs
|
|
197
|
+
.access(stackcollapsePath, fs.constants.X_OK)
|
|
198
|
+
.then(() => true)
|
|
199
|
+
.catch(() => false)
|
|
200
|
+
const isFlamegraphValid = await fs
|
|
201
|
+
.access(flamegraphPath, fs.constants.X_OK)
|
|
202
|
+
.then(() => true)
|
|
203
|
+
.catch(() => false)
|
|
204
|
+
|
|
205
|
+
if (isStackcollapseValid && isFlamegraphValid) {
|
|
206
|
+
// --- Step 4: 生成折叠文件 ---
|
|
207
|
+
const collapseOk = await execCommand(
|
|
208
|
+
stackcollapsePath,
|
|
209
|
+
[perfStacksFile],
|
|
210
|
+
{
|
|
211
|
+
stdout: {
|
|
212
|
+
file: perfFoldedFile,
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
)
|
|
216
|
+
if (!collapseOk) return
|
|
217
|
+
|
|
218
|
+
// --- Step 5: 生成 SVG 火焰图 ---
|
|
219
|
+
const flameOk = await execCommand(flamegraphPath, [perfFoldedFile], {
|
|
220
|
+
stdout: {
|
|
221
|
+
file: perfSvgFile,
|
|
222
|
+
},
|
|
223
|
+
})
|
|
224
|
+
if (flameOk) {
|
|
225
|
+
logger('info', `PID:${pidNum} Flame graph generated: ${perfSvgFile}`)
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
const missing = []
|
|
229
|
+
if (!isStackcollapseValid) missing.push('stackcollapse-perf.pl')
|
|
230
|
+
if (!isFlamegraphValid) missing.push('flamegraph.pl')
|
|
231
|
+
logger(
|
|
232
|
+
'info',
|
|
233
|
+
`PID:${pidNum} Skip flame graph – missing/not executable: ${missing.join(', ')}`,
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
} catch (err) {
|
|
237
|
+
logger('error', `PID:${pidNum} Perf sampling exception: ${err.message}`)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
module.exports = { performPerfSampling }
|
package/lib/pm2-extra.js
CHANGED
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
const pm2 = require('pm2')
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @returns { Promise<pm2.ProcessDescription[]> }
|
|
5
|
-
*/
|
|
6
|
-
const listAppsAsync = () => {
|
|
7
|
-
return new Promise((resolve, reject) => {
|
|
8
|
-
pm2.list((err, apps) => {
|
|
9
|
-
if (err) {
|
|
10
|
-
return reject(err)
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
resolve(apps)
|
|
14
|
-
})
|
|
15
|
-
})
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @param { string | number} pm_id
|
|
20
|
-
* @returns { Promise<void> }
|
|
21
|
-
*/
|
|
22
|
-
const stopAppAsync = (pm_id) => {
|
|
23
|
-
return new Promise((resolve, reject) => {
|
|
24
|
-
pm2.stop(pm_id, (err) => {
|
|
25
|
-
if (err) {
|
|
26
|
-
return reject(err)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
resolve()
|
|
30
|
-
})
|
|
31
|
-
})
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* @param { string | number} pm_id
|
|
36
|
-
* @returns { Promise<void> }
|
|
37
|
-
*/
|
|
38
|
-
const restartAppAsync = (pm_id) => {
|
|
39
|
-
return new Promise((resolve, reject) => {
|
|
40
|
-
pm2.restart(pm_id, (err) => {
|
|
41
|
-
if (err) {
|
|
42
|
-
return reject(err)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
resolve()
|
|
46
|
-
})
|
|
47
|
-
})
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
module.exports = {
|
|
51
|
-
listAppsAsync,
|
|
52
|
-
stopAppAsync,
|
|
53
|
-
restartAppAsync,
|
|
54
|
-
}
|
|
1
|
+
const pm2 = require('pm2')
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @returns { Promise<pm2.ProcessDescription[]> }
|
|
5
|
+
*/
|
|
6
|
+
const listAppsAsync = () => {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
pm2.list((err, apps) => {
|
|
9
|
+
if (err) {
|
|
10
|
+
return reject(err)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
resolve(apps)
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param { string | number} pm_id
|
|
20
|
+
* @returns { Promise<void> }
|
|
21
|
+
*/
|
|
22
|
+
const stopAppAsync = (pm_id) => {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
pm2.stop(pm_id, (err) => {
|
|
25
|
+
if (err) {
|
|
26
|
+
return reject(err)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
resolve()
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param { string | number} pm_id
|
|
36
|
+
* @returns { Promise<void> }
|
|
37
|
+
*/
|
|
38
|
+
const restartAppAsync = (pm_id) => {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
pm2.restart(pm_id, (err) => {
|
|
41
|
+
if (err) {
|
|
42
|
+
return reject(err)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
resolve()
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
listAppsAsync,
|
|
52
|
+
stopAppAsync,
|
|
53
|
+
restartAppAsync,
|
|
54
|
+
}
|