@platformatic/wattpm-pprof-capture 3.8.0 → 3.10.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 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
- let isCapturing = false
7
- let latestProfile = null
8
- let captureInterval = null
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 rotateProfile () {
23
- // `true` immediately restarts profiling after stopping
24
- latestProfile = time.stop(true)
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
- if (isCapturing) {
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
- time.start(options)
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
- if (!isCapturing) {
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
- latestProfile = time.stop()
53
- return latestProfile.encode()
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
- if (latestProfile == null) {
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.8.0",
3
+ "version": "3.10.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/service": "3.8.0",
26
- "@platformatic/foundation": "3.8.0"
25
+ "@platformatic/foundation": "3.10.0",
26
+ "@platformatic/service": "3.10.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
+ })