@aidra/pmx-io 0.0.0-stage → 9.13.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/README.md +69 -2
- package/bin/pmx-io-node.js +143 -0
- package/index.d.ts +381 -0
- package/index.js +27 -0
- package/index.mjs +47 -0
- package/lib/actions.js +120 -0
- package/lib/activate.js +402 -0
- package/lib/application.js +106 -0
- package/lib/bun.js +155 -0
- package/lib/config.js +67 -0
- package/lib/constants.js +17 -0
- package/lib/debug.js +48 -0
- package/lib/esm-hooks.mjs +111 -0
- package/lib/exit.js +79 -0
- package/lib/features/metrics.js +409 -0
- package/lib/features/notify.js +358 -0
- package/lib/features/profiling.js +208 -0
- package/lib/hooks.js +151 -0
- package/lib/inbound.js +174 -0
- package/lib/integrations.js +372 -0
- package/lib/itg/amqplib.js +169 -0
- package/lib/itg/bullmq.js +141 -0
- package/lib/itg/common.js +657 -0
- package/lib/itg/dbcomment.js +114 -0
- package/lib/itg/index.js +103 -0
- package/lib/itg/ioredis.js +212 -0
- package/lib/itg/kafkajs.js +205 -0
- package/lib/itg/mongodb.js +322 -0
- package/lib/itg/mysql2.js +205 -0
- package/lib/itg/nats.js +531 -0
- package/lib/itg/noderedis.js +236 -0
- package/lib/itg/pg.js +253 -0
- package/lib/itg/pgqueue.js +305 -0
- package/lib/itg/postgres.js +252 -0
- package/lib/itg/redis-common.js +265 -0
- package/lib/itg/sql.js +302 -0
- package/lib/metrics/service.js +286 -0
- package/lib/metrics/types.js +342 -0
- package/lib/modules.js +260 -0
- package/lib/node-options.js +62 -0
- package/lib/otel/bridge.js +797 -0
- package/lib/otel/env.js +114 -0
- package/lib/outbound.js +362 -0
- package/lib/pmx.js +757 -0
- package/lib/relay/hub.js +279 -0
- package/lib/relay/transport.js +253 -0
- package/lib/standalone/activate.js +307 -0
- package/lib/standalone/capture.js +55 -0
- package/lib/standalone/container.js +909 -0
- package/lib/standalone/transport.js +1023 -0
- package/lib/tracing/context.js +90 -0
- package/lib/tracing/control.js +124 -0
- package/lib/tracing/exporter.js +115 -0
- package/lib/tracing/index.js +242 -0
- package/lib/tracing/logs.js +176 -0
- package/lib/tracing/options.js +105 -0
- package/lib/tracing/paths.js +34 -0
- package/lib/tracing/tracer.js +850 -0
- package/lib/tracing/w3c.js +415 -0
- package/lib/transport/ipc.js +97 -0
- package/lib/transport/ws.js +511 -0
- package/lib/util.js +109 -0
- package/package.json +48 -4
- package/preload.cjs +26 -0
- package/preload.mjs +25 -0
- package/standalone.cjs +16 -0
- package/standalone.mjs +17 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
// Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// Errors: notifyError, framework handlers, uncaught capture (80 A.5.1–A.5.4, NIO-070..072).
|
|
5
|
+
|
|
6
|
+
const fs = require('fs')
|
|
7
|
+
const path = require('path')
|
|
8
|
+
const { fileURLToPath } = require('url')
|
|
9
|
+
const { PKG_DIR, isPlainObject } = require('../util')
|
|
10
|
+
const debug = require('../debug')('axm:features:notify')
|
|
11
|
+
|
|
12
|
+
const MAX_MESSAGE = 1000
|
|
13
|
+
const CACHE_TTL_MS = 30 * 60 * 1000
|
|
14
|
+
const REPORTED = Symbol.for('@pmx/io:reported')
|
|
15
|
+
const REJECTION_PREFIX = 'You have triggered an unhandledRejection, you may have forgotten to catch a Promise rejection:'
|
|
16
|
+
|
|
17
|
+
// ---- non-Error conversion (A.5.1) ----
|
|
18
|
+
|
|
19
|
+
function toError (value) {
|
|
20
|
+
if (value instanceof Error) return value
|
|
21
|
+
if (value && typeof value === 'object' && typeof value.message === 'string' && typeof value.stack === 'string') return value
|
|
22
|
+
let message
|
|
23
|
+
try {
|
|
24
|
+
message = 'Non-error value: ' + JSON.stringify(value)
|
|
25
|
+
} catch (err) {
|
|
26
|
+
try {
|
|
27
|
+
message = 'Unserializable non-error value: ' + String(err)
|
|
28
|
+
} catch (_) {
|
|
29
|
+
message = 'Unserializable non-error value that cannot be converted to a string'
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (message.length > MAX_MESSAGE) message = message.slice(0, MAX_MESSAGE) + '...'
|
|
33
|
+
const e = new Error(message)
|
|
34
|
+
return e
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---- source excerpt (A.5.4) ----
|
|
38
|
+
|
|
39
|
+
const fileCache = new Map() // path -> { at, lines }
|
|
40
|
+
|
|
41
|
+
function readLines (file) {
|
|
42
|
+
const now = Date.now()
|
|
43
|
+
const hit = fileCache.get(file)
|
|
44
|
+
if (hit && now - hit.at < CACHE_TTL_MS) return hit.lines
|
|
45
|
+
const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/)
|
|
46
|
+
fileCache.set(file, { at: now, lines })
|
|
47
|
+
if (fileCache.size > 100) fileCache.delete(fileCache.keys().next().value)
|
|
48
|
+
return lines
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const FRAME_RES = [
|
|
52
|
+
/\(([^()]*):(\d+):\d+\)$/, // at fn (/path/file.js:1:2)
|
|
53
|
+
/^at (.*):(\d+):\d+$/, // at /path/file.js:1:2
|
|
54
|
+
/^(\/.*|file:\/\/.*):(\d+)$/ // header line of a fatal exception: /path/file.js:3
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
function frames (stack) {
|
|
58
|
+
const out = []
|
|
59
|
+
for (const raw of String(stack).split('\n')) {
|
|
60
|
+
const line = raw.trim()
|
|
61
|
+
let m = null
|
|
62
|
+
for (const re of FRAME_RES) {
|
|
63
|
+
m = re.exec(line)
|
|
64
|
+
if (m) break
|
|
65
|
+
}
|
|
66
|
+
if (!m) continue
|
|
67
|
+
let file = m[1]
|
|
68
|
+
if (file.startsWith('async ')) file = file.slice(6)
|
|
69
|
+
if (file.startsWith('file://')) {
|
|
70
|
+
try {
|
|
71
|
+
file = fileURLToPath(file)
|
|
72
|
+
} catch (_) {
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
out.push({ file, line: Number(m[2]) })
|
|
77
|
+
}
|
|
78
|
+
return out
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const LIB_DIR = path.join(PKG_DIR, 'lib') + path.sep
|
|
82
|
+
const LIB_ENTRIES = ['index.js', 'index.mjs', 'preload.cjs', 'preload.mjs'].map((f) => path.join(PKG_DIR, f))
|
|
83
|
+
|
|
84
|
+
function isLibraryFile (abs) {
|
|
85
|
+
return abs.startsWith(LIB_DIR) || LIB_ENTRIES.includes(abs)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function sourceExcerpt (err) {
|
|
89
|
+
if (!err || typeof err.stack !== 'string') return null
|
|
90
|
+
for (const f of frames(err.stack)) {
|
|
91
|
+
if (!(path.isAbsolute(f.file) || f.file.startsWith('.'))) continue
|
|
92
|
+
if (f.file.includes('node_modules')) continue
|
|
93
|
+
const abs = path.resolve(f.file)
|
|
94
|
+
if (isLibraryFile(abs)) continue
|
|
95
|
+
let lines
|
|
96
|
+
try {
|
|
97
|
+
lines = readLines(abs)
|
|
98
|
+
} catch (_) {
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
const idx = f.line - 1
|
|
102
|
+
if (idx < 0 || idx >= lines.length) return null
|
|
103
|
+
const before = lines.slice(Math.max(0, idx - 5), idx)
|
|
104
|
+
const current = lines[idx].replace(/\t/g, ' ').replace(' ', '>>')
|
|
105
|
+
const afterL = lines.slice(idx + 1, idx + 6)
|
|
106
|
+
return { callsite: `${f.file}:${f.line}`, context: before.concat([current], afterL).join('\n') }
|
|
107
|
+
}
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function addExcerpt (payload, err) {
|
|
112
|
+
try {
|
|
113
|
+
const ex = sourceExcerpt(err)
|
|
114
|
+
if (ex) {
|
|
115
|
+
payload.callsite = ex.callsite
|
|
116
|
+
payload.context = ex.context
|
|
117
|
+
}
|
|
118
|
+
} catch (e) {
|
|
119
|
+
debug('excerpt failed: %s', e && e.message)
|
|
120
|
+
}
|
|
121
|
+
return payload
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function markReported (err) {
|
|
125
|
+
if (err && (typeof err === 'object' || typeof err === 'function')) {
|
|
126
|
+
try {
|
|
127
|
+
Object.defineProperty(err, REPORTED, { value: true, enumerable: false, configurable: true })
|
|
128
|
+
} catch (_) {}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** NIO-259: the current span's ids, or null. */
|
|
133
|
+
function currentTraceIds () {
|
|
134
|
+
const r = globalThis[Symbol.for('@pmx/io:context')]
|
|
135
|
+
if (!r || !r.als) return null
|
|
136
|
+
const store = r.als.getStore()
|
|
137
|
+
const span = store && store.span
|
|
138
|
+
if (!span || typeof span.traceId !== 'string' || typeof span.spanId !== 'string') return null
|
|
139
|
+
return { trace_id: span.traceId, span_id: span.spanId }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function addTraceIds (payload) {
|
|
143
|
+
const ids = currentTraceIds()
|
|
144
|
+
if (!ids) return payload
|
|
145
|
+
const meta = payload.metadata && typeof payload.metadata === 'object' ? payload.metadata : {}
|
|
146
|
+
if (meta.trace_id === undefined) meta.trace_id = ids.trace_id
|
|
147
|
+
if (meta.span_id === undefined) meta.span_id = ids.span_id
|
|
148
|
+
payload.metadata = meta
|
|
149
|
+
return payload
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** notifyError payload (A.5.1). */
|
|
153
|
+
function notifyPayload (value, context) {
|
|
154
|
+
const err = toError(value)
|
|
155
|
+
const payload = {
|
|
156
|
+
message: err.message,
|
|
157
|
+
stack: err.stack,
|
|
158
|
+
name: err.name,
|
|
159
|
+
metadata: isPlainObject(context) ? context : {}
|
|
160
|
+
}
|
|
161
|
+
return addExcerpt(payload, err)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Uncaught payload (NIO-070): all JSON-serialisable own properties plus name/message/stack. */
|
|
165
|
+
function uncaughtPayload (value) {
|
|
166
|
+
const err = toError(value)
|
|
167
|
+
const payload = {}
|
|
168
|
+
for (const key of Object.getOwnPropertyNames(err)) {
|
|
169
|
+
let v
|
|
170
|
+
try {
|
|
171
|
+
v = err[key]
|
|
172
|
+
if (typeof v === 'function' || typeof v === 'symbol' || v === undefined) continue
|
|
173
|
+
if (typeof v === 'bigint') continue
|
|
174
|
+
JSON.stringify(v)
|
|
175
|
+
} catch (_) {
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
payload[key] = v
|
|
179
|
+
}
|
|
180
|
+
payload.name = err.name
|
|
181
|
+
payload.message = err.message
|
|
182
|
+
payload.stack = err.stack
|
|
183
|
+
return addExcerpt(payload, err)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
class NotifyFeature {
|
|
187
|
+
/**
|
|
188
|
+
* @param {object} transport
|
|
189
|
+
* @param {boolean} capture install uncaught/unhandled handlers
|
|
190
|
+
*/
|
|
191
|
+
constructor (transport) {
|
|
192
|
+
this._transport = transport
|
|
193
|
+
this._onUncaught = null
|
|
194
|
+
this._onRejection = null
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
setTracing (on) {
|
|
198
|
+
this._tracing = on === true
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
notifyError (err, context) {
|
|
202
|
+
const payload = notifyPayload(err, context)
|
|
203
|
+
if (this._tracing) addTraceIds(payload)
|
|
204
|
+
markReported(err)
|
|
205
|
+
this._transport.send('process:exception', payload)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Worker threads (NIO-293): reported through `uncaughtExceptionMonitor`, which does not change
|
|
210
|
+
* Node's default termination of the worker (no 'uncaughtException' listener is added).
|
|
211
|
+
*/
|
|
212
|
+
enableThreadCapture () {
|
|
213
|
+
this._onMonitor = (err) => {
|
|
214
|
+
let payload
|
|
215
|
+
try {
|
|
216
|
+
payload = uncaughtPayload(err)
|
|
217
|
+
} catch (_) {
|
|
218
|
+
payload = { message: String(err && err.message) }
|
|
219
|
+
}
|
|
220
|
+
if (this._tracing) addTraceIds(payload)
|
|
221
|
+
try {
|
|
222
|
+
this._transport.send('process:exception', payload)
|
|
223
|
+
} catch (_) {}
|
|
224
|
+
}
|
|
225
|
+
process.on('uncaughtExceptionMonitor', this._onMonitor)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
enableCapture () {
|
|
229
|
+
this._onUncaught = (err) => {
|
|
230
|
+
try {
|
|
231
|
+
console.error(err)
|
|
232
|
+
} catch (_) {}
|
|
233
|
+
const onlyListener = process.listeners('uncaughtException').length <= 1
|
|
234
|
+
let exited = false
|
|
235
|
+
const exit = () => {
|
|
236
|
+
if (exited) return
|
|
237
|
+
exited = true
|
|
238
|
+
if (onlyListener) process.exit(1)
|
|
239
|
+
}
|
|
240
|
+
let payload
|
|
241
|
+
try {
|
|
242
|
+
payload = uncaughtPayload(err)
|
|
243
|
+
} catch (_) {
|
|
244
|
+
payload = { message: String(err && err.message) }
|
|
245
|
+
}
|
|
246
|
+
if (this._tracing) addTraceIds(payload)
|
|
247
|
+
try {
|
|
248
|
+
const g = globalThis[Symbol.for('@pmx/io:standalone')]
|
|
249
|
+
if (g && typeof g.onUncaught === 'function') g.onUncaught(err)
|
|
250
|
+
} catch (_) {}
|
|
251
|
+
try {
|
|
252
|
+
this._transport.send('process:exception', payload, exit)
|
|
253
|
+
} catch (_) {}
|
|
254
|
+
if (onlyListener) {
|
|
255
|
+
// Exit after the send callback or 100 ms, whichever comes first (NIO-070).
|
|
256
|
+
setTimeout(exit, 100)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
this._onRejection = (reason) => {
|
|
260
|
+
// NIO-070 uses the cluster wrapper's behaviour (40 §4.1) in both modes: a falsy reason
|
|
261
|
+
// (e.g. `Promise.reject()`) still prints the warning, followed by the value itself, and is
|
|
262
|
+
// reported as "No error but unhandledRejection was caught!" (70 T-INT-022/024, Q-NIO-030).
|
|
263
|
+
try {
|
|
264
|
+
console.error(REJECTION_PREFIX)
|
|
265
|
+
console.error(reason)
|
|
266
|
+
} catch (_) {}
|
|
267
|
+
let payload
|
|
268
|
+
if (!reason) {
|
|
269
|
+
payload = { message: 'No error but unhandledRejection was caught!' }
|
|
270
|
+
} else {
|
|
271
|
+
try {
|
|
272
|
+
payload = uncaughtPayload(reason)
|
|
273
|
+
} catch (_) {
|
|
274
|
+
payload = { message: String(reason && reason.message) }
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (this._tracing && payload) addTraceIds(payload)
|
|
278
|
+
try {
|
|
279
|
+
this._transport.send('process:exception', payload)
|
|
280
|
+
} catch (_) {}
|
|
281
|
+
}
|
|
282
|
+
process.on('uncaughtException', this._onUncaught)
|
|
283
|
+
process.on('unhandledRejection', this._onRejection)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
expressErrorHandler () {
|
|
287
|
+
this._transport.send('axm:option:configuration', { error: true })
|
|
288
|
+
return (err, req, res, next) => {
|
|
289
|
+
try {
|
|
290
|
+
const e = toError(err)
|
|
291
|
+
const payload = {
|
|
292
|
+
message: e.message,
|
|
293
|
+
stack: e.stack,
|
|
294
|
+
name: e.name,
|
|
295
|
+
metadata: {
|
|
296
|
+
http: {
|
|
297
|
+
url: req.url,
|
|
298
|
+
params: req.params,
|
|
299
|
+
method: req.method,
|
|
300
|
+
query: req.query,
|
|
301
|
+
body: req.body,
|
|
302
|
+
path: req.path,
|
|
303
|
+
route: req.route && req.route.path !== undefined ? req.route.path : undefined
|
|
304
|
+
},
|
|
305
|
+
custom: { user: req.user && typeof req.user === 'object' ? req.user.id : undefined }
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
markReported(err)
|
|
309
|
+
this._transport.send('process:exception', payload)
|
|
310
|
+
} catch (_) {}
|
|
311
|
+
return next(err)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
koaErrorHandler () {
|
|
316
|
+
this._transport.send('axm:option:configuration', { error: true })
|
|
317
|
+
return async (ctx, next) => {
|
|
318
|
+
try {
|
|
319
|
+
await next()
|
|
320
|
+
} catch (err) {
|
|
321
|
+
try {
|
|
322
|
+
const e = toError(err)
|
|
323
|
+
const request = ctx.request || {}
|
|
324
|
+
markReported(err)
|
|
325
|
+
this._transport.send('process:exception', {
|
|
326
|
+
message: e.message,
|
|
327
|
+
stack: e.stack,
|
|
328
|
+
name: e.name,
|
|
329
|
+
metadata: {
|
|
330
|
+
http: {
|
|
331
|
+
url: request.url,
|
|
332
|
+
params: ctx.params,
|
|
333
|
+
method: request.method,
|
|
334
|
+
query: request.query,
|
|
335
|
+
body: request.body,
|
|
336
|
+
path: request.path,
|
|
337
|
+
route: ctx._matchedRoute
|
|
338
|
+
},
|
|
339
|
+
custom: { user: ctx.user && typeof ctx.user === 'object' ? ctx.user.id : undefined }
|
|
340
|
+
}
|
|
341
|
+
})
|
|
342
|
+
} catch (_) {}
|
|
343
|
+
throw err
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
destroy () {
|
|
349
|
+
if (this._onUncaught) process.removeListener('uncaughtException', this._onUncaught)
|
|
350
|
+
if (this._onRejection) process.removeListener('unhandledRejection', this._onRejection)
|
|
351
|
+
if (this._onMonitor) process.removeListener('uncaughtExceptionMonitor', this._onMonitor)
|
|
352
|
+
this._onMonitor = null
|
|
353
|
+
this._onUncaught = null
|
|
354
|
+
this._onRejection = null
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
module.exports = { NotifyFeature, toError, sourceExcerpt, notifyPayload, uncaughtPayload, REJECTION_PREFIX, REPORTED, markReported }
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// Remote-triggered profiling through the built-in inspector (80 A.7, NIO-077, 02 D-06).
|
|
5
|
+
|
|
6
|
+
const { shortUuid } = require('../util')
|
|
7
|
+
const debug = require('../debug')('axm:features:profiling')
|
|
8
|
+
|
|
9
|
+
const DISABLED_FLAGS = {
|
|
10
|
+
heapdump: false,
|
|
11
|
+
'feature.profiler.heap_snapshot': false,
|
|
12
|
+
'feature.profiler.heap_sampling': false,
|
|
13
|
+
'feature.profiler.cpu_js': false
|
|
14
|
+
}
|
|
15
|
+
const ENABLED_FLAGS = {
|
|
16
|
+
heapdump: true,
|
|
17
|
+
'feature.profiler.heapsnapshot': false,
|
|
18
|
+
'feature.profiler.heapsampling': true,
|
|
19
|
+
'feature.profiler.cpu_js': true
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function canUseInspector () {
|
|
23
|
+
if (process.versions.bun) return false
|
|
24
|
+
// process.features.inspector tells without loading the module (keeps startup cheap).
|
|
25
|
+
if (process.features && typeof process.features.inspector === 'boolean') return process.features.inspector
|
|
26
|
+
try {
|
|
27
|
+
const inspector = require('inspector')
|
|
28
|
+
return typeof inspector.Session === 'function'
|
|
29
|
+
} catch (_) {
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function initiatedOf (opts) {
|
|
35
|
+
return opts && typeof opts.initiated === 'string' ? opts.initiated : 'manual'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function timeoutOf (opts) {
|
|
39
|
+
if (!opts) return null
|
|
40
|
+
const t = typeof opts.timeout === 'string' ? Number(opts.timeout) : opts.timeout
|
|
41
|
+
return typeof t === 'number' && Number.isFinite(t) && t > 0 ? t : null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class ProfilingFeature {
|
|
45
|
+
constructor (transport, actions) {
|
|
46
|
+
this._transport = transport
|
|
47
|
+
this._actions = actions
|
|
48
|
+
this._session = null
|
|
49
|
+
this._available = false
|
|
50
|
+
this._running = null // { kind: 'cpu'|'heap', uuid, at, initiated, timer, started }
|
|
51
|
+
this._enabling = null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* IO-005 step 2: decide whether the inspector is usable. The in-process session itself is
|
|
56
|
+
* connected on first use, so that an instrumented app that is never profiled pays nothing.
|
|
57
|
+
*/
|
|
58
|
+
connect () {
|
|
59
|
+
this._available = canUseInspector()
|
|
60
|
+
return this._available
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_ensureSession () {
|
|
64
|
+
if (this._session) return true
|
|
65
|
+
if (!this._available) return false
|
|
66
|
+
try {
|
|
67
|
+
const inspector = require('inspector')
|
|
68
|
+
const session = new inspector.Session()
|
|
69
|
+
session.connect()
|
|
70
|
+
this._session = session
|
|
71
|
+
return true
|
|
72
|
+
} catch (err) {
|
|
73
|
+
debug('inspector unusable: %s', err && err.message)
|
|
74
|
+
this._available = false
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Enable the Profiler and HeapProfiler domains on first use (keeps init cheap). */
|
|
80
|
+
_enable () {
|
|
81
|
+
if (!this._enabling) {
|
|
82
|
+
this._enabling = Promise.all([this._post('Profiler.enable'), this._post('HeapProfiler.enable')])
|
|
83
|
+
}
|
|
84
|
+
return this._enabling
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
_post (method, params) {
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
if (!this._session) return reject(new Error('Profiler not available'))
|
|
90
|
+
this._session.post(method, params || {}, (err, res) => (err ? reject(err) : resolve(res)))
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Register the five actions (after sending the profiler flags). */
|
|
95
|
+
init () {
|
|
96
|
+
if (!this._available) {
|
|
97
|
+
if (!process.versions.bun) console.error('Failed to require the profiler via inspector, disabling profiling ...')
|
|
98
|
+
this._transport.send('axm:option:configuration', DISABLED_FLAGS)
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
this._transport.send('axm:option:configuration', ENABLED_FLAGS)
|
|
102
|
+
const a = this._actions
|
|
103
|
+
a.register('km:heapdump', undefined, (opts, reply) => this._heapdump(opts, reply))
|
|
104
|
+
a.register('km:cpu:profiling:start', undefined, (opts, reply) => this._start('cpu', opts, reply))
|
|
105
|
+
a.register('km:cpu:profiling:stop', undefined, (reply) => this._stop('cpu', reply))
|
|
106
|
+
a.register('km:heap:sampling:start', undefined, (opts, reply) => this._start('heap', opts, reply))
|
|
107
|
+
a.register('km:heap:sampling:stop', undefined, (reply) => this._stop('heap', reply))
|
|
108
|
+
return true
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
_start (kind, opts, reply) {
|
|
112
|
+
if (!this._ensureSession()) return reply({ err: 'Profiler not available', success: false })
|
|
113
|
+
if (this._running) return reply({ err: 'A profiling is already running', success: false })
|
|
114
|
+
const uuid = shortUuid()
|
|
115
|
+
const run = { kind, uuid, at: Date.now(), initiated: initiatedOf(opts), timer: null }
|
|
116
|
+
this._running = run
|
|
117
|
+
reply({ success: true, uuid })
|
|
118
|
+
const samplingInterval = Number(opts && opts.samplingInterval) > 0 ? Number(opts.samplingInterval) : 16384
|
|
119
|
+
const started = this._enable().then(() => kind === 'cpu'
|
|
120
|
+
? this._post('Profiler.start')
|
|
121
|
+
: this._post('HeapProfiler.startSampling', { samplingInterval }))
|
|
122
|
+
started.catch((err) => {
|
|
123
|
+
debug('profiler start failed: %s', err && err.message)
|
|
124
|
+
if (this._running === run) this._running = null
|
|
125
|
+
})
|
|
126
|
+
const timeout = timeoutOf(opts)
|
|
127
|
+
if (timeout) {
|
|
128
|
+
run.timer = setTimeout(() => {
|
|
129
|
+
if (this._running === run) this._finish(run).catch(() => {})
|
|
130
|
+
}, timeout)
|
|
131
|
+
if (run.timer.unref) run.timer.unref()
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
_stop (kind, reply) {
|
|
136
|
+
const run = this._running
|
|
137
|
+
if (!run || run.kind !== kind) return reply({ err: 'No profiling are already running', success: false })
|
|
138
|
+
reply({ success: true, uuid: run.uuid })
|
|
139
|
+
this._finish(run).catch((err) => debug('profiler stop failed: %s', err && err.message))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async _finish (run) {
|
|
143
|
+
if (run.timer) clearTimeout(run.timer)
|
|
144
|
+
this._running = null
|
|
145
|
+
let profile
|
|
146
|
+
if (run.kind === 'cpu') {
|
|
147
|
+
profile = (await this._post('Profiler.stop')).profile
|
|
148
|
+
} else {
|
|
149
|
+
profile = (await this._post('HeapProfiler.stopSampling')).profile
|
|
150
|
+
}
|
|
151
|
+
const type = run.kind === 'cpu' ? 'cpuprofile' : 'heapprofile'
|
|
152
|
+
const payload = {
|
|
153
|
+
uuid: run.uuid,
|
|
154
|
+
duration: Date.now() - run.at,
|
|
155
|
+
at: run.at,
|
|
156
|
+
data: JSON.stringify(profile),
|
|
157
|
+
success: true,
|
|
158
|
+
initiated: run.initiated,
|
|
159
|
+
type
|
|
160
|
+
}
|
|
161
|
+
payload[type] = true
|
|
162
|
+
this._transport.send('profilings', payload)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
_heapdump (opts, reply) {
|
|
166
|
+
if (!this._ensureSession()) return reply({ err: 'Profiler not available', success: false })
|
|
167
|
+
reply({ success: true })
|
|
168
|
+
const at = Date.now()
|
|
169
|
+
const initiated = initiatedOf(opts)
|
|
170
|
+
const t = setTimeout(() => {
|
|
171
|
+
const chunks = []
|
|
172
|
+
const onChunk = (m) => chunks.push(m.params.chunk)
|
|
173
|
+
this._session.on('HeapProfiler.addHeapSnapshotChunk', onChunk)
|
|
174
|
+
this._enable().then(() => this._post('HeapProfiler.takeHeapSnapshot', { reportProgress: false }))
|
|
175
|
+
.then(() => {
|
|
176
|
+
this._session.removeListener('HeapProfiler.addHeapSnapshotChunk', onChunk)
|
|
177
|
+
this._transport.send('profilings', {
|
|
178
|
+
data: chunks.join(''),
|
|
179
|
+
at,
|
|
180
|
+
initiated,
|
|
181
|
+
duration: Date.now() - at,
|
|
182
|
+
type: 'heapdump'
|
|
183
|
+
})
|
|
184
|
+
}, (err) => {
|
|
185
|
+
this._session.removeListener('HeapProfiler.addHeapSnapshotChunk', onChunk)
|
|
186
|
+
reply({ success: err && err.message, err })
|
|
187
|
+
})
|
|
188
|
+
}, 200)
|
|
189
|
+
if (t.unref) t.unref()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
destroy () {
|
|
193
|
+
if (this._running) {
|
|
194
|
+
if (this._running.timer) clearTimeout(this._running.timer)
|
|
195
|
+
const kind = this._running.kind
|
|
196
|
+
this._running = null
|
|
197
|
+
this._post(kind === 'cpu' ? 'Profiler.stop' : 'HeapProfiler.stopSampling').catch(() => {})
|
|
198
|
+
}
|
|
199
|
+
if (this._session) {
|
|
200
|
+
try {
|
|
201
|
+
this._session.disconnect()
|
|
202
|
+
} catch (_) {}
|
|
203
|
+
}
|
|
204
|
+
this._session = null
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = { ProfilingFeature, canUseInspector, DISABLED_FLAGS, ENABLED_FLAGS }
|