-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathindex.js
More file actions
149 lines (127 loc) · 4.42 KB
/
index.js
File metadata and controls
149 lines (127 loc) · 4.42 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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
'use strict'
const fs = require('fs')
const path = require('path')
const zlib = require('zlib')
const { spawn } = require('child_process')
/**
* Start profiling a Node.js process using the preload script
* @param {string} script - Path to the script to profile
* @param {string[]} args - Arguments to pass to the script
* @param {object} options - Options for profiling
* @param {boolean} options.autoStart - Whether to start profiling immediately (default: false)
* @param {string[]} options.nodeOptions - Node.js CLI options to pass (default: [])
* @param {string} options.delay - Delay before starting profiler ('none', 'until-started', or numeric string for ms, default: 'until-started')
* @param {string|string[]} options.sourcemapDirs - Directory or directories to search for sourcemaps (optional)
* @param {string|string[]} options.nodeModulesSourceMaps - Node modules to load sourcemaps from (optional)
* @returns {Promise<object>} Process information
*/
function startProfiling (script, args = [], options = {}) {
const preloadPath = path.join(__dirname, '..', 'preload.js')
const env = {
...process.env,
...options.env,
FLAME_AUTO_START: options.autoStart ? 'true' : 'false',
FLAME_DELAY: options.delay ?? 'until-started'
}
// Add sourcemap configuration if provided
if (options.sourcemapDirs) {
const dirs = Array.isArray(options.sourcemapDirs)
? options.sourcemapDirs
: [options.sourcemapDirs]
env.FLAME_SOURCEMAP_DIRS = dirs.join(path.delimiter)
}
// Add node_modules sourcemap configuration if provided
if (options.nodeModulesSourceMaps) {
const mods = Array.isArray(options.nodeModulesSourceMaps)
? options.nodeModulesSourceMaps
: [options.nodeModulesSourceMaps]
env.FLAME_NODE_MODULES_SOURCE_MAPS = mods.join(',')
}
// Construct the node command with options
const nodeArgs = [
...(options.nodeOptions || []),
'-r',
preloadPath,
script,
...args
]
const child = spawn('node', nodeArgs, {
stdio: 'inherit',
env,
...options
})
return {
pid: child.pid,
process: child,
toggleProfiler: () => {
if (process.platform !== 'win32') {
process.kill(child.pid, 'SIGUSR2')
} else {
// On Windows, we'll need to use a different approach
console.warn('Direct signal toggle not supported on Windows. Use the CLI toggle command instead.')
}
}
}
}
/**
* Parse a pprof profile file
* @param {string} filePath - Path to the pprof file
* @returns {Promise<object>} Parsed profile data
*/
async function parseProfile (filePath) {
if (!fs.existsSync(filePath)) {
throw new Error(`Profile file not found: ${filePath}`)
}
let data = fs.readFileSync(filePath)
// Check if the file is gzipped
const isGzipped = data[0] === 0x1f && data[1] === 0x8b
if (isGzipped) {
data = zlib.gunzipSync(data)
}
const { Profile } = await import('pprof-format')
return Profile.decode(data)
}
/**
* Generate an HTML flamegraph from a pprof file using @platformatic/react-pprof CLI
* @param {string} pprofPath - Path to the pprof file
* @param {string} outputPath - Path to write the HTML file
* @returns {Promise<object>} CLI result with stdout/stderr
*/
async function generateFlamegraph (pprofPath, outputPath) {
return new Promise((resolve, reject) => {
// Use the react-pprof CLI to generate the flamegraph
const cliPath = require.resolve('react-pprof/cli.js')
// Detect if this is a heap profile and use blue/purple colors
const isHeapProfile = path.basename(pprofPath).includes('heap')
const args = [cliPath, '--output', outputPath]
if (isHeapProfile) {
// Use blue/purple color scheme for heap profiles
args.push('--primary-color', '#4444ff', '--secondary-color', '#cc66ff')
}
args.push(pprofPath)
const child = spawn('node', args, { stdio: 'pipe' })
let stdout = ''
let stderr = ''
child.stdout.on('data', (data) => {
stdout += data.toString()
})
child.stderr.on('data', (data) => {
stderr += data.toString()
})
child.on('close', (code) => {
if (code === 0) {
resolve({ stdout, stderr })
} else {
reject(new Error(`CLI failed with exit code ${code}: ${stderr || stdout}`))
}
})
child.on('error', (error) => {
reject(new Error(`Failed to start CLI: ${error.message}`))
})
})
}
module.exports = {
startProfiling,
parseProfile,
generateFlamegraph
}