@platformatic/wattpm-pprof-capture 3.24.0 → 3.25.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
@@ -31,6 +31,7 @@ const profilingState = {
31
31
  cpu: {
32
32
  isCapturing: false,
33
33
  latestProfile: null,
34
+ latestProfileTimestamp: null,
34
35
  captureInterval: null,
35
36
  durationMillis: null,
36
37
  eluThreshold: null,
@@ -42,6 +43,7 @@ const profilingState = {
42
43
  heap: {
43
44
  isCapturing: false,
44
45
  latestProfile: null,
46
+ latestProfileTimestamp: null,
45
47
  captureInterval: null,
46
48
  durationMillis: null,
47
49
  eluThreshold: null,
@@ -76,6 +78,7 @@ function scheduleLastProfileCleanup (state) {
76
78
  if (state.options?.durationMillis) {
77
79
  state.clearProfileTimeout = setTimeout(() => {
78
80
  state.latestProfile = undefined
81
+ state.latestProfileTimestamp = null
79
82
  state.clearProfileTimeout = null
80
83
  }, state.options.durationMillis)
81
84
  state.clearProfileTimeout.unref()
@@ -148,6 +151,7 @@ function stopProfiler (type, state) {
148
151
  scheduleLastProfileCleanup(state)
149
152
  unscheduleProfileRotation(state)
150
153
  state.profilerStarted = false
154
+ state.latestProfileTimestamp = Date.now()
151
155
 
152
156
  if (type === 'heap') {
153
157
  // Get the profile before stopping
@@ -330,6 +334,7 @@ export function getLastProfile (options = {}) {
330
334
  const profiler = getProfiler(type)
331
335
  // Get heap profile with sourceMapper if enabled and available
332
336
  state.latestProfile = (state.sourceMapsEnabled && sourceMapper) ? profiler.profile(undefined, sourceMapper) : profiler.profile()
337
+ state.latestProfileTimestamp = Date.now()
333
338
  }
334
339
 
335
340
  // Check if we have a profile
@@ -357,7 +362,8 @@ export function getProfilingState (options = {}) {
357
362
  isProfilerRunning: state.profilerStarted,
358
363
  isPausedBelowThreshold: state.eluThreshold != null && !state.profilerStarted,
359
364
  lastELU: lastELU?.utilization,
360
- eluThreshold: state.eluThreshold
365
+ eluThreshold: state.eluThreshold,
366
+ latestProfileTimestamp: state.latestProfileTimestamp
361
367
  }
362
368
  }
363
369
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/wattpm-pprof-capture",
3
- "version": "3.24.0",
3
+ "version": "3.25.0",
4
4
  "description": "pprof profiling capture for wattpm",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -26,8 +26,8 @@
26
26
  "neostandard": "^0.12.0",
27
27
  "pprof-format": "^2.1.0",
28
28
  "typescript": "^5.0.0",
29
- "@platformatic/foundation": "3.24.0",
30
- "@platformatic/service": "3.24.0"
29
+ "@platformatic/service": "3.25.0",
30
+ "@platformatic/foundation": "3.25.0"
31
31
  },
32
32
  "engines": {
33
33
  "node": ">=22.19.0"
@@ -647,3 +647,124 @@ test('profiling with eluThreshold should continue rotating while above threshold
647
647
  await request(`${url}/cpu-intensive/stop`, { method: 'POST' })
648
648
  await app.sendCommandToApplication('service', 'stopProfiling')
649
649
  })
650
+
651
+ test('latestProfileTimestamp should be set after profile rotation', async t => {
652
+ const { app } = await createApp(t)
653
+
654
+ // Check initial state - timestamp should be null
655
+ const initialState = await app.sendCommandToApplication('service', 'getProfilingState')
656
+ assert.strictEqual(initialState.latestProfileTimestamp, null, 'Timestamp should be null before profiling')
657
+
658
+ // Start profiling with rotation
659
+ await app.sendCommandToApplication('service', 'startProfiling', { durationMillis: 200 })
660
+
661
+ // Wait for first rotation
662
+ await new Promise(resolve => setTimeout(resolve, 250))
663
+
664
+ // Get state and verify timestamp is set
665
+ const stateAfterRotation = await app.sendCommandToApplication('service', 'getProfilingState')
666
+ assert.ok(stateAfterRotation.latestProfileTimestamp != null, 'Timestamp should be set after rotation')
667
+ assert.ok(typeof stateAfterRotation.latestProfileTimestamp === 'number', 'Timestamp should be a number')
668
+ assert.ok(stateAfterRotation.latestProfileTimestamp <= Date.now(), 'Timestamp should not be in the future')
669
+ assert.ok(stateAfterRotation.latestProfileTimestamp > Date.now() - 5000, 'Timestamp should be recent')
670
+
671
+ await app.sendCommandToApplication('service', 'stopProfiling')
672
+ })
673
+
674
+ test('latestProfileTimestamp should be set after stopProfiling', async t => {
675
+ const { app } = await createApp(t)
676
+
677
+ // Start profiling without rotation
678
+ await app.sendCommandToApplication('service', 'startProfiling', {})
679
+
680
+ // Get state before stop - timestamp should be null (no rotation occurred)
681
+ const stateBeforeStop = await app.sendCommandToApplication('service', 'getProfilingState')
682
+ assert.strictEqual(stateBeforeStop.latestProfileTimestamp, null, 'Timestamp should be null before stop')
683
+
684
+ // Stop profiling
685
+ const beforeStopTime = Date.now()
686
+ await app.sendCommandToApplication('service', 'stopProfiling')
687
+ const afterStopTime = Date.now()
688
+
689
+ // Get state after stop - timestamp should be set
690
+ const stateAfterStop = await app.sendCommandToApplication('service', 'getProfilingState')
691
+ assert.ok(stateAfterStop.latestProfileTimestamp != null, 'Timestamp should be set after stopProfiling')
692
+ assert.ok(stateAfterStop.latestProfileTimestamp >= beforeStopTime, 'Timestamp should be >= time before stop')
693
+ assert.ok(stateAfterStop.latestProfileTimestamp <= afterStopTime, 'Timestamp should be <= time after stop')
694
+ })
695
+
696
+ test('latestProfileTimestamp should be cleared after profile cleanup timeout', async t => {
697
+ const { app } = await createApp(t)
698
+
699
+ // Start profiling with short rotation interval
700
+ await app.sendCommandToApplication('service', 'startProfiling', { durationMillis: 200 })
701
+
702
+ // Wait for first rotation
703
+ await new Promise(resolve => setTimeout(resolve, 250))
704
+
705
+ // Verify timestamp is set
706
+ const stateWithProfile = await app.sendCommandToApplication('service', 'getProfilingState')
707
+ assert.ok(stateWithProfile.latestProfileTimestamp != null, 'Timestamp should be set after rotation')
708
+ assert.ok(stateWithProfile.hasProfile, 'Should have profile')
709
+
710
+ // Stop profiling - this schedules cleanup after durationMillis (200ms)
711
+ await app.sendCommandToApplication('service', 'stopProfiling')
712
+
713
+ // Wait for cleanup timeout (durationMillis after stop)
714
+ await new Promise(resolve => setTimeout(resolve, 300))
715
+
716
+ // Verify timestamp is cleared after cleanup
717
+ const stateAfterCleanup = await app.sendCommandToApplication('service', 'getProfilingState')
718
+ assert.strictEqual(stateAfterCleanup.latestProfileTimestamp, null, 'Timestamp should be cleared after cleanup')
719
+ assert.ok(!stateAfterCleanup.hasProfile, 'Profile should be cleared')
720
+ })
721
+
722
+ test('latestProfileTimestamp should be set for heap profiling', async t => {
723
+ const { app } = await createApp(t)
724
+
725
+ // Check initial state for heap - timestamp should be null
726
+ const initialState = await app.sendCommandToApplication('service', 'getProfilingState', { type: 'heap' })
727
+ assert.strictEqual(initialState.latestProfileTimestamp, null, 'Heap timestamp should be null before profiling')
728
+
729
+ // Start heap profiling
730
+ await app.sendCommandToApplication('service', 'startProfiling', { type: 'heap' })
731
+
732
+ // Wait a bit
733
+ await new Promise(resolve => setTimeout(resolve, 100))
734
+
735
+ // Get last profile (for heap, this captures a snapshot)
736
+ const beforeGetProfile = Date.now()
737
+ await app.sendCommandToApplication('service', 'getLastProfile', { type: 'heap' })
738
+ const afterGetProfile = Date.now()
739
+
740
+ // Verify timestamp is set after getLastProfile for heap
741
+ const stateAfterGet = await app.sendCommandToApplication('service', 'getProfilingState', { type: 'heap' })
742
+ assert.ok(stateAfterGet.latestProfileTimestamp != null, 'Heap timestamp should be set after getLastProfile')
743
+ assert.ok(stateAfterGet.latestProfileTimestamp >= beforeGetProfile, 'Timestamp should be >= time before get')
744
+ assert.ok(stateAfterGet.latestProfileTimestamp <= afterGetProfile, 'Timestamp should be <= time after get')
745
+
746
+ await app.sendCommandToApplication('service', 'stopProfiling', { type: 'heap' })
747
+ })
748
+
749
+ test('latestProfileTimestamp should update with each rotation', async t => {
750
+ const { app } = await createApp(t)
751
+
752
+ // Start profiling with short rotation interval
753
+ await app.sendCommandToApplication('service', 'startProfiling', { durationMillis: 200 })
754
+
755
+ // Wait for first rotation
756
+ await new Promise(resolve => setTimeout(resolve, 250))
757
+ const stateAfterFirst = await app.sendCommandToApplication('service', 'getProfilingState')
758
+ const firstTimestamp = stateAfterFirst.latestProfileTimestamp
759
+ assert.ok(firstTimestamp != null, 'Timestamp should be set after first rotation')
760
+
761
+ // Wait for second rotation
762
+ await new Promise(resolve => setTimeout(resolve, 250))
763
+ const stateAfterSecond = await app.sendCommandToApplication('service', 'getProfilingState')
764
+ const secondTimestamp = stateAfterSecond.latestProfileTimestamp
765
+
766
+ // Second timestamp should be greater than first
767
+ assert.ok(secondTimestamp > firstTimestamp, 'Timestamp should update with each rotation')
768
+
769
+ await app.sendCommandToApplication('service', 'stopProfiling')
770
+ })