@platformatic/wattpm-pprof-capture 3.8.0 → 3.9.0
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/index.js +83 -23
- package/package.json +3 -3
- package/test/watt-pprof-capture.test.js +116 -0
package/index.js
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
|
-
import { time } from '@datadog/pprof'
|
|
1
|
+
import { time, heap } from '@datadog/pprof'
|
|
2
2
|
import { NoProfileAvailableError, ProfilingAlreadyStartedError, ProfilingNotStartedError } from './lib/errors.js'
|
|
3
3
|
|
|
4
4
|
const kITC = Symbol.for('plt.runtime.itc')
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
// Track profiling state separately for each type
|
|
7
|
+
const profilingState = {
|
|
8
|
+
cpu: {
|
|
9
|
+
isCapturing: false,
|
|
10
|
+
latestProfile: null,
|
|
11
|
+
captureInterval: null
|
|
12
|
+
},
|
|
13
|
+
heap: {
|
|
14
|
+
isCapturing: false,
|
|
15
|
+
latestProfile: null,
|
|
16
|
+
captureInterval: null
|
|
17
|
+
}
|
|
18
|
+
}
|
|
9
19
|
|
|
10
20
|
// Keep trying until ITC is available. This is needed because preloads run
|
|
11
21
|
// before the app thread initialization, so globalThis.platformatic.messaging
|
|
@@ -19,51 +29,101 @@ const registerInterval = setInterval(() => {
|
|
|
19
29
|
}
|
|
20
30
|
}, 10)
|
|
21
31
|
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
function getProfiler (type) {
|
|
33
|
+
return type === 'heap' ? heap : time
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function rotateProfile (type) {
|
|
37
|
+
const profiler = getProfiler(type)
|
|
38
|
+
const state = profilingState[type]
|
|
39
|
+
|
|
40
|
+
if (type === 'heap') {
|
|
41
|
+
// Heap profiler needs to call profile() to get the current profile
|
|
42
|
+
state.latestProfile = profiler.profile()
|
|
43
|
+
} else {
|
|
44
|
+
// CPU time profiler: `true` immediately restarts profiling after stopping
|
|
45
|
+
state.latestProfile = profiler.stop(true)
|
|
46
|
+
}
|
|
25
47
|
}
|
|
26
48
|
|
|
27
49
|
export function startProfiling (options = {}) {
|
|
28
|
-
|
|
50
|
+
const type = options.type || 'cpu'
|
|
51
|
+
const state = profilingState[type]
|
|
52
|
+
|
|
53
|
+
if (state.isCapturing) {
|
|
29
54
|
throw new ProfilingAlreadyStartedError()
|
|
30
55
|
}
|
|
31
|
-
isCapturing = true
|
|
56
|
+
state.isCapturing = true
|
|
32
57
|
|
|
33
|
-
|
|
58
|
+
const profiler = getProfiler(type)
|
|
59
|
+
|
|
60
|
+
// Heap profiler has different API than time profiler
|
|
61
|
+
if (type === 'heap') {
|
|
62
|
+
// Heap profiler takes intervalBytes and stackDepth as positional arguments
|
|
63
|
+
// Default: 512KB interval, 64 stack depth
|
|
64
|
+
const intervalBytes = options.intervalBytes || 512 * 1024
|
|
65
|
+
const stackDepth = options.stackDepth || 64
|
|
66
|
+
profiler.start(intervalBytes, stackDepth)
|
|
67
|
+
} else {
|
|
68
|
+
// CPU time profiler takes options object
|
|
69
|
+
profiler.start(options)
|
|
70
|
+
}
|
|
34
71
|
|
|
35
72
|
// Set up profile window rotation if durationMillis is provided
|
|
36
73
|
const timeout = options.durationMillis
|
|
37
74
|
if (timeout) {
|
|
38
|
-
captureInterval = setInterval(rotateProfile, timeout)
|
|
39
|
-
captureInterval.unref()
|
|
75
|
+
state.captureInterval = setInterval(() => rotateProfile(type), timeout)
|
|
76
|
+
state.captureInterval.unref()
|
|
40
77
|
}
|
|
41
78
|
}
|
|
42
79
|
|
|
43
|
-
export function stopProfiling () {
|
|
44
|
-
|
|
80
|
+
export function stopProfiling (options = {}) {
|
|
81
|
+
const type = options.type || 'cpu'
|
|
82
|
+
const state = profilingState[type]
|
|
83
|
+
|
|
84
|
+
if (!state.isCapturing) {
|
|
45
85
|
throw new ProfilingNotStartedError()
|
|
46
86
|
}
|
|
47
|
-
isCapturing = false
|
|
87
|
+
state.isCapturing = false
|
|
48
88
|
|
|
49
|
-
clearInterval(captureInterval)
|
|
50
|
-
captureInterval = null
|
|
89
|
+
clearInterval(state.captureInterval)
|
|
90
|
+
state.captureInterval = null
|
|
51
91
|
|
|
52
|
-
|
|
53
|
-
|
|
92
|
+
const profiler = getProfiler(type)
|
|
93
|
+
|
|
94
|
+
// Heap and CPU profilers have different stop APIs
|
|
95
|
+
if (type === 'heap') {
|
|
96
|
+
// Get the profile before stopping
|
|
97
|
+
state.latestProfile = profiler.profile()
|
|
98
|
+
profiler.stop()
|
|
99
|
+
} else {
|
|
100
|
+
// CPU time profiler returns the profile when stopping
|
|
101
|
+
state.latestProfile = profiler.stop()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return state.latestProfile.encode()
|
|
54
105
|
}
|
|
55
106
|
|
|
56
|
-
export function getLastProfile () {
|
|
107
|
+
export function getLastProfile (options = {}) {
|
|
108
|
+
const type = options.type || 'cpu'
|
|
109
|
+
const state = profilingState[type]
|
|
110
|
+
|
|
57
111
|
// TODO: Should it be allowed to get last profile after stopping?
|
|
58
|
-
if (!isCapturing) {
|
|
112
|
+
if (!state.isCapturing) {
|
|
59
113
|
throw new ProfilingNotStartedError()
|
|
60
114
|
}
|
|
61
115
|
|
|
62
|
-
|
|
116
|
+
const profiler = getProfiler(type)
|
|
117
|
+
|
|
118
|
+
// For heap profiler, always get the current profile
|
|
119
|
+
// For CPU profiler, use the cached profile if available
|
|
120
|
+
if (type === 'heap') {
|
|
121
|
+
state.latestProfile = profiler.profile()
|
|
122
|
+
} else if (state.latestProfile == null) {
|
|
63
123
|
throw new NoProfileAvailableError()
|
|
64
124
|
}
|
|
65
125
|
|
|
66
|
-
return latestProfile.encode()
|
|
126
|
+
return state.latestProfile.encode()
|
|
67
127
|
}
|
|
68
128
|
|
|
69
129
|
export * as errors from './lib/errors.js'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/wattpm-pprof-capture",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.0",
|
|
4
4
|
"description": "pprof profiling capture for wattpm",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"eslint": "9",
|
|
24
24
|
"neostandard": "^0.12.0",
|
|
25
|
-
"@platformatic/
|
|
26
|
-
"@platformatic/
|
|
25
|
+
"@platformatic/foundation": "3.9.0",
|
|
26
|
+
"@platformatic/service": "3.9.0"
|
|
27
27
|
},
|
|
28
28
|
"engines": {
|
|
29
29
|
"node": ">=22.19.0"
|
|
@@ -200,3 +200,119 @@ test('getLastProfile should return same profile until next rotation', async t =>
|
|
|
200
200
|
|
|
201
201
|
await app.sendCommandToApplication('service', 'stopProfiling')
|
|
202
202
|
})
|
|
203
|
+
|
|
204
|
+
test('heap profiling should work', async t => {
|
|
205
|
+
const { app } = await createApp(t)
|
|
206
|
+
|
|
207
|
+
// Start heap profiling
|
|
208
|
+
await app.sendCommandToApplication('service', 'startProfiling', { type: 'heap' })
|
|
209
|
+
|
|
210
|
+
// Wait a bit for some allocations
|
|
211
|
+
await new Promise(resolve => setTimeout(resolve, 200))
|
|
212
|
+
|
|
213
|
+
// Stop heap profiling and get profile
|
|
214
|
+
const profile = await app.sendCommandToApplication('service', 'stopProfiling', { type: 'heap' })
|
|
215
|
+
assert.ok(profile instanceof Uint8Array, 'Heap profile should be Uint8Array')
|
|
216
|
+
assert.ok(profile.length > 0, 'Heap profile should have content')
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
test('heap profiling should throw error when not started', async t => {
|
|
220
|
+
const { app } = await createApp(t)
|
|
221
|
+
|
|
222
|
+
// Try to stop heap profiling without starting
|
|
223
|
+
await assert.rejects(
|
|
224
|
+
() => app.sendCommandToApplication('service', 'stopProfiling', { type: 'heap' }),
|
|
225
|
+
{ code: 'PLT_PPROF_PROFILING_NOT_STARTED' },
|
|
226
|
+
'Should throw ProfilingNotStartedError for heap profiling'
|
|
227
|
+
)
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
test('heap profiling multiple start attempts should throw error', async t => {
|
|
231
|
+
const { app } = await createApp(t)
|
|
232
|
+
|
|
233
|
+
// Start heap profiling
|
|
234
|
+
await app.sendCommandToApplication('service', 'startProfiling', { type: 'heap' })
|
|
235
|
+
|
|
236
|
+
// Try to start again - should throw ProfilingAlreadyStartedError
|
|
237
|
+
await assert.rejects(
|
|
238
|
+
() => app.sendCommandToApplication('service', 'startProfiling', { type: 'heap' }),
|
|
239
|
+
{ code: 'PLT_PPROF_PROFILING_ALREADY_STARTED' },
|
|
240
|
+
'Should throw ProfilingAlreadyStartedError for heap profiling'
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
await app.sendCommandToApplication('service', 'stopProfiling', { type: 'heap' })
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
test('concurrent CPU and heap profiling should work', async t => {
|
|
247
|
+
const { app } = await createApp(t)
|
|
248
|
+
|
|
249
|
+
// Start both CPU and heap profiling
|
|
250
|
+
await app.sendCommandToApplication('service', 'startProfiling', { type: 'cpu' })
|
|
251
|
+
await app.sendCommandToApplication('service', 'startProfiling', { type: 'heap' })
|
|
252
|
+
|
|
253
|
+
// Wait a bit for data
|
|
254
|
+
await new Promise(resolve => setTimeout(resolve, 200))
|
|
255
|
+
|
|
256
|
+
// Stop both and get profiles
|
|
257
|
+
const cpuProfile = await app.sendCommandToApplication('service', 'stopProfiling', { type: 'cpu' })
|
|
258
|
+
const heapProfile = await app.sendCommandToApplication('service', 'stopProfiling', { type: 'heap' })
|
|
259
|
+
|
|
260
|
+
assert.ok(cpuProfile instanceof Uint8Array, 'CPU profile should be Uint8Array')
|
|
261
|
+
assert.ok(cpuProfile.length > 0, 'CPU profile should have content')
|
|
262
|
+
assert.ok(heapProfile instanceof Uint8Array, 'Heap profile should be Uint8Array')
|
|
263
|
+
assert.ok(heapProfile.length > 0, 'Heap profile should have content')
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
test('heap profiling getLastProfile should return current heap snapshot', async t => {
|
|
267
|
+
const { app } = await createApp(t)
|
|
268
|
+
|
|
269
|
+
// Start heap profiling
|
|
270
|
+
await app.sendCommandToApplication('service', 'startProfiling', { type: 'heap' })
|
|
271
|
+
|
|
272
|
+
// Wait a bit for allocations
|
|
273
|
+
await new Promise(resolve => setTimeout(resolve, 200))
|
|
274
|
+
|
|
275
|
+
// Get last profile should work for heap (returns current snapshot)
|
|
276
|
+
const profile1 = await app.sendCommandToApplication('service', 'getLastProfile', { type: 'heap' })
|
|
277
|
+
assert.ok(profile1 instanceof Uint8Array, 'Heap profile should be Uint8Array')
|
|
278
|
+
assert.ok(profile1.length > 0, 'Heap profile should have content')
|
|
279
|
+
|
|
280
|
+
// Get another snapshot
|
|
281
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
282
|
+
const profile2 = await app.sendCommandToApplication('service', 'getLastProfile', { type: 'heap' })
|
|
283
|
+
assert.ok(profile2 instanceof Uint8Array, 'Heap profile should be Uint8Array')
|
|
284
|
+
|
|
285
|
+
await app.sendCommandToApplication('service', 'stopProfiling', { type: 'heap' })
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test('CPU and heap profiling are independent', async t => {
|
|
289
|
+
const { app } = await createApp(t)
|
|
290
|
+
|
|
291
|
+
// Start only CPU profiling
|
|
292
|
+
await app.sendCommandToApplication('service', 'startProfiling', { type: 'cpu' })
|
|
293
|
+
|
|
294
|
+
// Heap profiling should not be started
|
|
295
|
+
await assert.rejects(
|
|
296
|
+
() => app.sendCommandToApplication('service', 'stopProfiling', { type: 'heap' }),
|
|
297
|
+
{ code: 'PLT_PPROF_PROFILING_NOT_STARTED' },
|
|
298
|
+
'Heap profiling should not be started'
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
// CPU profiling should work
|
|
302
|
+
const cpuProfile = await app.sendCommandToApplication('service', 'stopProfiling', { type: 'cpu' })
|
|
303
|
+
assert.ok(cpuProfile instanceof Uint8Array, 'CPU profile should work')
|
|
304
|
+
|
|
305
|
+
// Now start heap profiling
|
|
306
|
+
await app.sendCommandToApplication('service', 'startProfiling', { type: 'heap' })
|
|
307
|
+
|
|
308
|
+
// CPU profiling should not be started anymore
|
|
309
|
+
await assert.rejects(
|
|
310
|
+
() => app.sendCommandToApplication('service', 'stopProfiling', { type: 'cpu' }),
|
|
311
|
+
{ code: 'PLT_PPROF_PROFILING_NOT_STARTED' },
|
|
312
|
+
'CPU profiling should not be started'
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
// Heap profiling should work
|
|
316
|
+
const heapProfile = await app.sendCommandToApplication('service', 'stopProfiling', { type: 'heap' })
|
|
317
|
+
assert.ok(heapProfile instanceof Uint8Array, 'Heap profile should work')
|
|
318
|
+
})
|