@platformatic/watt-extra 1.6.3-alpha.3 → 1.6.3-alpha.4

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.
@@ -2,7 +2,10 @@
2
2
  "permissions": {
3
3
  "allow": [
4
4
  "Bash(node --test-only:*)",
5
- "Bash(node --test:*)"
5
+ "Bash(node --test:*)",
6
+ "Bash(for i in {1..3})",
7
+ "Bash(do echo \"=== Run $i ===\")",
8
+ "Bash(done)"
6
9
  ],
7
10
  "deny": [],
8
11
  "ask": []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/watt-extra",
3
- "version": "1.6.3-alpha.3",
3
+ "version": "1.6.3-alpha.4",
4
4
  "description": "The Platformatic runtime manager",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -56,6 +56,7 @@ export class Profiler {
56
56
  }
57
57
 
58
58
  async requestProfile (request = {}) {
59
+ process._rawDebug('--------REQUEST--------', request)
59
60
  request.timestamp ??= Date.now()
60
61
  this.#requests.push(request)
61
62
  this.#unscheduleStopProfiling()
@@ -139,9 +140,14 @@ export class Profiler {
139
140
  this.#isProfiling = false
140
141
  this.#log.info('Stopping profiling')
141
142
 
142
- await this.#runtime.sendCommandToApplication(
143
- this.#workerId, 'stopProfiling', this.#profileOptions
144
- )
143
+ try {
144
+ await this.#runtime.sendCommandToApplication(
145
+ this.#workerId, 'stopProfiling', this.#profileOptions
146
+ )
147
+ } catch (err) {
148
+ // Ignore errors if the app is already closing
149
+ this.#log.debug({ err }, 'Failed to stop profiling')
150
+ }
145
151
  }
146
152
 
147
153
  async #getProfile () {
@@ -265,6 +271,8 @@ async function flamegraphs (app, _opts) {
265
271
  }
266
272
  }
267
273
 
274
+ process._rawDebug('--------ALERT IDS--------', alertIds)
275
+
268
276
  try {
269
277
  const alertId = alertIds.shift()
270
278
  const flamegraph = await sendServiceFlamegraph(
@@ -294,7 +302,7 @@ async function flamegraphs (app, _opts) {
294
302
  async function sendServiceFlamegraph (scalerUrl, serviceId, profile, profileType, alertId) {
295
303
  const podId = app.instanceId
296
304
  const url = `${scalerUrl}/pods/${podId}/services/${serviceId}/flamegraph`
297
- app.log.info({ serviceId, podId, profileType }, 'Sending flamegraph')
305
+ app.log.info({ serviceId, podId, profileType, alertId }, 'Sending flamegraph')
298
306
 
299
307
  const query = { profileType }
300
308
  if (alertId) {
@@ -318,8 +326,14 @@ async function flamegraphs (app, _opts) {
318
326
  throw new Error(`Failed to send flamegraph: ${error}`)
319
327
  }
320
328
 
321
- const response = await body.json()
322
- return response
329
+ const flamegraph = await body.json()
330
+
331
+ app.log.info(
332
+ { serviceId, podId, profileType, flamegraph },
333
+ 'Flamegraph successfully stored'
334
+ )
335
+
336
+ return flamegraph
323
337
  }
324
338
 
325
339
  // Function that supports ICC that doesn't have attach flamegraph API
@@ -0,0 +1,443 @@
1
+ import assert from 'node:assert'
2
+ import { test } from 'node:test'
3
+ import { randomUUID } from 'node:crypto'
4
+ import { join, dirname } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+ import { setTimeout as sleep } from 'node:timers/promises'
7
+ import { Profile } from 'pprof-format'
8
+ import { setUpEnvironment, startICC } from './helper.js'
9
+ import { start } from '../index.js'
10
+ import { Profiler } from '../plugins/flamegraphs.js'
11
+
12
+ const __filename = fileURLToPath(import.meta.url)
13
+ const __dirname = dirname(__filename)
14
+
15
+ async function setupApp (t) {
16
+ const applicationName = 'test-app'
17
+ const applicationId = randomUUID()
18
+ const applicationPath = join(__dirname, 'fixtures', 'service-1')
19
+
20
+ const icc = await startICC(t, {
21
+ applicationId,
22
+ applicationName
23
+ })
24
+
25
+ setUpEnvironment({
26
+ PLT_APP_NAME: applicationName,
27
+ PLT_APP_DIR: applicationPath,
28
+ PLT_ICC_URL: 'http://127.0.0.1:3000'
29
+ })
30
+
31
+ const app = await start()
32
+
33
+ t.after(async () => {
34
+ await app.close()
35
+ await icc.close()
36
+ })
37
+
38
+ return app
39
+ }
40
+
41
+ test('Profiler should start profiling and generate profile on first request', async (t) => {
42
+ const app = await setupApp(t)
43
+
44
+ let profileReceived = false
45
+ const profiler = new Profiler({
46
+ app,
47
+ workerId: 'main:0',
48
+ type: 'cpu',
49
+ duration: 1000,
50
+ sourceMaps: false,
51
+ onProfile: (err, profile, requests) => {
52
+ assert.strictEqual(err, null)
53
+ assert.ok(profile)
54
+ assert.ok(profile.data)
55
+ assert.ok(profile.timestamp)
56
+ assert.strictEqual(requests.length, 1)
57
+ assert.strictEqual(requests[0].alertId, 'test-alert')
58
+ profileReceived = true
59
+ }
60
+ })
61
+
62
+ t.after(async () => {
63
+ await profiler.stop()
64
+ })
65
+
66
+ // Request a profile
67
+ await profiler.requestProfile({ alertId: 'test-alert' })
68
+
69
+ // Wait for profile to be generated (duration is 1 second)
70
+ await sleep(1500)
71
+
72
+ assert.ok(profileReceived, 'Profile should have been generated')
73
+ })
74
+
75
+ test('Profiler should queue multiple requests and associate them with the profile', async (t) => {
76
+ const app = await setupApp(t)
77
+
78
+ let profileReceived = false
79
+ const profiler = new Profiler({
80
+ app,
81
+ workerId: 'main:0',
82
+ type: 'cpu',
83
+ duration: 1000,
84
+ sourceMaps: false,
85
+ onProfile: (err, profile, requests) => {
86
+ assert.strictEqual(err, null)
87
+ assert.ok(profile)
88
+ assert.strictEqual(requests.length, 3)
89
+ assert.strictEqual(requests[0].alertId, 'alert-1')
90
+ assert.strictEqual(requests[1].alertId, 'alert-2')
91
+ assert.strictEqual(requests[2].alertId, 'alert-3')
92
+ profileReceived = true
93
+ }
94
+ })
95
+
96
+ t.after(async () => {
97
+ await profiler.stop()
98
+ })
99
+
100
+ // Queue multiple requests
101
+ await profiler.requestProfile({ alertId: 'alert-1' })
102
+ await profiler.requestProfile({ alertId: 'alert-2' })
103
+ await profiler.requestProfile({ alertId: 'alert-3' })
104
+
105
+ // Wait for profile to be generated
106
+ await sleep(1500)
107
+
108
+ assert.ok(profileReceived, 'Profile should have been generated with all requests')
109
+ })
110
+
111
+ test('Profiler should filter requests by timestamp', async (t) => {
112
+ const app = await setupApp(t)
113
+
114
+ const profilesReceived = []
115
+ const profiler = new Profiler({
116
+ app,
117
+ workerId: 'main:0',
118
+ type: 'cpu',
119
+ duration: 1000,
120
+ sourceMaps: false,
121
+ onProfile: (err, profile, requests) => {
122
+ if (err) return
123
+ profilesReceived.push({ profile, requests })
124
+ }
125
+ })
126
+
127
+ t.after(async () => {
128
+ await profiler.stop()
129
+ })
130
+
131
+ // Request first profile
132
+ await profiler.requestProfile({ alertId: 'alert-1' })
133
+
134
+ // Wait for first profile
135
+ await sleep(1500)
136
+
137
+ // Request second profile after some delay
138
+ await profiler.requestProfile({ alertId: 'alert-2' })
139
+
140
+ // Wait for second profile
141
+ await sleep(1500)
142
+
143
+ assert.strictEqual(profilesReceived.length, 2)
144
+ assert.strictEqual(profilesReceived[0].requests.length, 1)
145
+ assert.strictEqual(profilesReceived[0].requests[0].alertId, 'alert-1')
146
+ assert.strictEqual(profilesReceived[1].requests.length, 1)
147
+ assert.strictEqual(profilesReceived[1].requests[0].alertId, 'alert-2')
148
+ })
149
+
150
+ test('Profiler should auto-stop after idle period', async (t) => {
151
+ const app = await setupApp(t)
152
+
153
+ let stopCalled = false
154
+ const originalSendCommand = app.watt.runtime.sendCommandToApplication
155
+ app.watt.runtime.sendCommandToApplication = async (workerId, command, options) => {
156
+ if (command === 'stopProfiling') {
157
+ stopCalled = true
158
+ }
159
+ return originalSendCommand.call(app.watt.runtime, workerId, command, options)
160
+ }
161
+
162
+ const profiler = new Profiler({
163
+ app,
164
+ workerId: 'main:0',
165
+ type: 'cpu',
166
+ duration: 1000,
167
+ sourceMaps: false,
168
+ onProfile: () => {}
169
+ })
170
+
171
+ t.after(async () => {
172
+ await profiler.stop()
173
+ })
174
+
175
+ // Request a profile
176
+ await profiler.requestProfile({ alertId: 'test-alert' })
177
+
178
+ // Wait for profile generation + idle timeout (duration + duration/2)
179
+ await sleep(2000)
180
+
181
+ assert.ok(stopCalled, 'Profiler should have stopped after idle period')
182
+ })
183
+
184
+ test('Profiler should not stop if new requests arrive', async (t) => {
185
+ const app = await setupApp(t)
186
+
187
+ let stopCalled = false
188
+ const originalSendCommand = app.watt.runtime.sendCommandToApplication
189
+ app.watt.runtime.sendCommandToApplication = async (workerId, command, options) => {
190
+ if (command === 'stopProfiling') {
191
+ stopCalled = true
192
+ }
193
+ return originalSendCommand.call(app.watt.runtime, workerId, command, options)
194
+ }
195
+
196
+ const profilesReceived = []
197
+ const profiler = new Profiler({
198
+ app,
199
+ workerId: 'main:0',
200
+ type: 'cpu',
201
+ duration: 1000,
202
+ sourceMaps: false,
203
+ onProfile: (err, profile, requests) => {
204
+ if (err) return
205
+ profilesReceived.push({ profile, requests })
206
+ }
207
+ })
208
+
209
+ t.after(async () => {
210
+ await profiler.stop()
211
+ })
212
+
213
+ // Request first profile
214
+ await profiler.requestProfile({ alertId: 'alert-1' })
215
+
216
+ // Wait for first profile
217
+ await sleep(1200)
218
+
219
+ // Request second profile before idle timeout
220
+ await profiler.requestProfile({ alertId: 'alert-2' })
221
+
222
+ // Wait for second profile
223
+ await sleep(1200)
224
+
225
+ assert.strictEqual(profilesReceived.length, 2)
226
+ assert.strictEqual(stopCalled, false, 'Profiler should not have stopped yet')
227
+ })
228
+
229
+ test('Profiler should handle errors when starting profiling', async (t) => {
230
+ const app = await setupApp(t)
231
+
232
+ // Mock sendCommandToApplication to throw error on startProfiling
233
+ const originalSendCommand = app.watt.runtime.sendCommandToApplication
234
+ app.watt.runtime.sendCommandToApplication = async (workerId, command, options) => {
235
+ if (command === 'startProfiling') {
236
+ throw new Error('Failed to start profiling')
237
+ }
238
+ return originalSendCommand.call(app.watt.runtime, workerId, command, options)
239
+ }
240
+
241
+ let errorReceived = false
242
+ const profiler = new Profiler({
243
+ app,
244
+ workerId: 'main:0',
245
+ type: 'cpu',
246
+ duration: 1000,
247
+ sourceMaps: false,
248
+ onProfile: (err, profile, requests) => {
249
+ assert.ok(err)
250
+ assert.strictEqual(err.message, 'Failed to start profiling')
251
+ assert.strictEqual(profile, null)
252
+ assert.strictEqual(requests.length, 1)
253
+ errorReceived = true
254
+ }
255
+ })
256
+
257
+ t.after(async () => {
258
+ await profiler.stop()
259
+ })
260
+
261
+ // Request a profile
262
+ await profiler.requestProfile({ alertId: 'test-alert' })
263
+
264
+ // Wait for error callback
265
+ await sleep(200)
266
+
267
+ assert.ok(errorReceived, 'Error should have been handled')
268
+ })
269
+
270
+ test('Profiler should handle errors when getting profile', async (t) => {
271
+ const app = await setupApp(t)
272
+
273
+ // Mock sendCommandToApplication to throw error on getLastProfile
274
+ const originalSendCommand = app.watt.runtime.sendCommandToApplication
275
+ app.watt.runtime.sendCommandToApplication = async (workerId, command, options) => {
276
+ if (command === 'getLastProfile') {
277
+ throw new Error('Failed to get profile')
278
+ }
279
+ return originalSendCommand.call(app.watt.runtime, workerId, command, options)
280
+ }
281
+
282
+ let errorReceived = false
283
+ const profiler = new Profiler({
284
+ app,
285
+ workerId: 'main:0',
286
+ type: 'cpu',
287
+ duration: 1000,
288
+ sourceMaps: false,
289
+ onProfile: (err, profile, requests) => {
290
+ assert.ok(err)
291
+ assert.strictEqual(err.message, 'Failed to get profile')
292
+ assert.strictEqual(profile, null)
293
+ assert.strictEqual(requests.length, 1)
294
+ errorReceived = true
295
+ }
296
+ })
297
+
298
+ t.after(async () => {
299
+ await profiler.stop()
300
+ })
301
+
302
+ // Request a profile
303
+ await profiler.requestProfile({ alertId: 'test-alert' })
304
+
305
+ // Wait for profile generation attempt
306
+ await sleep(1500)
307
+
308
+ assert.ok(errorReceived, 'Error should have been handled')
309
+ })
310
+
311
+ test('Profiler should pass sourceMaps option correctly', async (t) => {
312
+ const app = await setupApp(t)
313
+
314
+ let sourceMapsOptionReceived = null
315
+ const originalSendCommand = app.watt.runtime.sendCommandToApplication
316
+ app.watt.runtime.sendCommandToApplication = async (workerId, command, options) => {
317
+ if (command === 'startProfiling') {
318
+ sourceMapsOptionReceived = options.sourceMaps
319
+ }
320
+ return originalSendCommand.call(app.watt.runtime, workerId, command, options)
321
+ }
322
+
323
+ const profiler = new Profiler({
324
+ app,
325
+ workerId: 'main:0',
326
+ type: 'cpu',
327
+ duration: 1000,
328
+ sourceMaps: true,
329
+ onProfile: () => {}
330
+ })
331
+
332
+ t.after(async () => {
333
+ await profiler.stop()
334
+ })
335
+
336
+ // Request a profile
337
+ await profiler.requestProfile({ alertId: 'test-alert' })
338
+
339
+ // Wait for profiling to start
340
+ await sleep(200)
341
+
342
+ assert.strictEqual(sourceMapsOptionReceived, true, 'sourceMaps option should be passed correctly')
343
+ })
344
+
345
+ test('Profiler should manually stop profiling when stop() is called', async (t) => {
346
+ const app = await setupApp(t)
347
+
348
+ let stopCalled = false
349
+ const originalSendCommand = app.watt.runtime.sendCommandToApplication
350
+ app.watt.runtime.sendCommandToApplication = async (workerId, command, options) => {
351
+ if (command === 'stopProfiling') {
352
+ stopCalled = true
353
+ }
354
+ return originalSendCommand.call(app.watt.runtime, workerId, command, options)
355
+ }
356
+
357
+ const profiler = new Profiler({
358
+ app,
359
+ workerId: 'main:0',
360
+ type: 'cpu',
361
+ duration: 1000,
362
+ sourceMaps: false,
363
+ onProfile: () => {}
364
+ })
365
+
366
+ // Request a profile
367
+ await profiler.requestProfile({ alertId: 'test-alert' })
368
+
369
+ // Wait a bit
370
+ await sleep(200)
371
+
372
+ // Manually stop
373
+ await profiler.stop()
374
+
375
+ assert.ok(stopCalled, 'stopProfiling should have been called')
376
+ })
377
+
378
+ test('Profiler should handle requests with custom timestamps', async (t) => {
379
+ const app = await setupApp(t)
380
+
381
+ let profileReceived = false
382
+ const profiler = new Profiler({
383
+ app,
384
+ workerId: 'main:0',
385
+ type: 'cpu',
386
+ duration: 1000,
387
+ sourceMaps: false,
388
+ onProfile: (err, profile, requests) => {
389
+ assert.strictEqual(err, null)
390
+ assert.ok(profile)
391
+ assert.strictEqual(requests.length, 1)
392
+ assert.strictEqual(requests[0].timestamp, 123456)
393
+ profileReceived = true
394
+ }
395
+ })
396
+
397
+ t.after(async () => {
398
+ await profiler.stop()
399
+ })
400
+
401
+ // Request with custom timestamp
402
+ await profiler.requestProfile({ alertId: 'test-alert', timestamp: 123456 })
403
+
404
+ // Wait for profile to be generated
405
+ await sleep(1500)
406
+
407
+ assert.ok(profileReceived, 'Profile should have been generated with custom timestamp')
408
+ })
409
+
410
+ test('Profiler should generate valid pprof profile', async (t) => {
411
+ const app = await setupApp(t)
412
+
413
+ let profileData = null
414
+ const profiler = new Profiler({
415
+ app,
416
+ workerId: 'main:0',
417
+ type: 'cpu',
418
+ duration: 1000,
419
+ sourceMaps: false,
420
+ onProfile: (err, profile, requests) => {
421
+ if (!err && profile) {
422
+ profileData = profile.data
423
+ }
424
+ }
425
+ })
426
+
427
+ t.after(async () => {
428
+ await profiler.stop()
429
+ })
430
+
431
+ // Request a profile
432
+ await profiler.requestProfile({ alertId: 'test-alert' })
433
+
434
+ // Wait for profile to be generated
435
+ await sleep(1500)
436
+
437
+ assert.ok(profileData, 'Profile data should exist')
438
+
439
+ // Verify it's a valid pprof format
440
+ const profile = Profile.decode(profileData)
441
+ assert.ok(profile, 'Profile should be decodable')
442
+ assert.ok(profile.sample, 'Profile should have samples')
443
+ })