dd-trace 3.38.1 → 3.40.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/LICENSE-3rdparty.csv +2 -2
- package/README.md +3 -3
- package/ext/kinds.d.ts +1 -0
- package/ext/kinds.js +2 -1
- package/ext/tags.d.ts +2 -1
- package/ext/tags.js +6 -1
- package/index.d.ts +9 -1
- package/package.json +8 -8
- package/packages/datadog-core/src/storage/async_resource.js +1 -1
- package/packages/datadog-esbuild/index.js +1 -20
- package/packages/datadog-instrumentations/src/cucumber.js +5 -0
- package/packages/datadog-instrumentations/src/helpers/bundler-register.js +1 -2
- package/packages/datadog-instrumentations/src/helpers/instrument.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/register.js +1 -1
- package/packages/datadog-instrumentations/src/jest.js +39 -10
- package/packages/datadog-instrumentations/src/knex.js +24 -17
- package/packages/datadog-instrumentations/src/mocha.js +16 -1
- package/packages/datadog-instrumentations/src/next.js +58 -23
- package/packages/datadog-instrumentations/src/playwright.js +11 -6
- package/packages/datadog-instrumentations/src/restify.js +14 -1
- package/packages/datadog-plugin-http/src/client.js +2 -0
- package/packages/datadog-plugin-jest/src/index.js +11 -3
- package/packages/datadog-plugin-kafkajs/src/consumer.js +8 -6
- package/packages/datadog-plugin-kafkajs/src/producer.js +9 -6
- package/packages/datadog-plugin-mocha/src/index.js +7 -1
- package/packages/datadog-plugin-next/src/index.js +4 -3
- package/packages/datadog-plugin-playwright/src/index.js +4 -1
- package/packages/dd-trace/src/appsec/channels.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +1 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secret-analyzer.js +60 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js +269 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/hsts-header-missing-analyzer.js +5 -2
- package/packages/dd-trace/src/appsec/iast/analyzers/missing-header-analyzer.js +22 -4
- package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +9 -2
- package/packages/dd-trace/src/appsec/iast/analyzers/xcontenttype-header-missing-analyzer.js +2 -2
- package/packages/dd-trace/src/appsec/iast/iast-log.js +9 -4
- package/packages/dd-trace/src/appsec/iast/iast-plugin.js +1 -1
- package/packages/dd-trace/src/appsec/iast/index.js +1 -1
- package/packages/dd-trace/src/appsec/iast/path-line.js +7 -2
- package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +13 -2
- package/packages/dd-trace/src/appsec/iast/telemetry/index.js +1 -14
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +19 -0
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js +2 -1
- package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +1 -0
- package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +5 -1
- package/packages/dd-trace/src/appsec/recommended.json +272 -48
- package/packages/dd-trace/src/appsec/reporter.js +31 -34
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-itr-configuration.js +16 -4
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +2 -0
- package/packages/dd-trace/src/config.js +35 -17
- package/packages/dd-trace/src/datastreams/processor.js +60 -15
- package/packages/dd-trace/src/format.js +6 -1
- package/packages/dd-trace/src/git_properties.js +16 -15
- package/packages/dd-trace/src/iitm.js +1 -1
- package/packages/dd-trace/src/log/channels.js +1 -1
- package/packages/dd-trace/src/opentelemetry/span.js +95 -2
- package/packages/dd-trace/src/opentelemetry/tracer.js +9 -10
- package/packages/dd-trace/src/opentracing/span.js +4 -0
- package/packages/dd-trace/src/opentracing/span_context.js +5 -2
- package/packages/dd-trace/src/plugin_manager.js +1 -1
- package/packages/dd-trace/src/plugins/database.js +1 -1
- package/packages/dd-trace/src/plugins/plugin.js +1 -1
- package/packages/dd-trace/src/plugins/util/ci.js +6 -19
- package/packages/dd-trace/src/plugins/util/git.js +2 -1
- package/packages/dd-trace/src/plugins/util/ip_extractor.js +7 -6
- package/packages/dd-trace/src/plugins/util/test.js +29 -1
- package/packages/dd-trace/src/plugins/util/url.js +26 -0
- package/packages/dd-trace/src/plugins/util/user-provided-git.js +1 -14
- package/packages/dd-trace/src/profiling/config.js +23 -20
- package/packages/dd-trace/src/profiling/profilers/events.js +161 -0
- package/packages/dd-trace/src/profiling/profilers/shared.js +9 -0
- package/packages/dd-trace/src/profiling/profilers/wall.js +84 -47
- package/packages/dd-trace/src/ritm.js +1 -1
- package/packages/dd-trace/src/span_processor.js +4 -0
- package/packages/dd-trace/src/telemetry/dependencies.js +1 -1
- package/packages/dd-trace/src/telemetry/index.js +5 -1
- package/packages/dd-trace/src/telemetry/logs/index.js +65 -0
- package/packages/dd-trace/src/{appsec/iast/telemetry/log → telemetry/logs}/log-collector.js +9 -22
- package/packages/dd-trace/src/tracer.js +4 -2
- package/packages/dd-trace/src/appsec/iast/telemetry/log/index.js +0 -87
- package/packages/diagnostics_channel/index.js +0 -3
- package/packages/diagnostics_channel/src/index.js +0 -121
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
const URL = require('url').URL
|
|
2
|
-
|
|
3
1
|
const {
|
|
4
2
|
GIT_BRANCH,
|
|
5
3
|
GIT_COMMIT_SHA,
|
|
@@ -24,6 +22,7 @@ const {
|
|
|
24
22
|
CI_NODE_LABELS,
|
|
25
23
|
CI_NODE_NAME
|
|
26
24
|
} = require('./tags')
|
|
25
|
+
const { filterSensitiveInfoFromRepository } = require('./url')
|
|
27
26
|
|
|
28
27
|
// Receives a string with the form 'John Doe <john.doe@gmail.com>'
|
|
29
28
|
// and returns { name: 'John Doe', email: 'john.doe@gmail.com' }
|
|
@@ -67,20 +66,6 @@ function normalizeRef (ref) {
|
|
|
67
66
|
return ref.replace(/origin\/|refs\/heads\/|tags\//gm, '')
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
function filterSensitiveInfoFromRepository (repositoryUrl) {
|
|
71
|
-
if (repositoryUrl.startsWith('git@')) {
|
|
72
|
-
return repositoryUrl
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
const { protocol, hostname, pathname } = new URL(repositoryUrl)
|
|
77
|
-
|
|
78
|
-
return `${protocol}//${hostname}${pathname}`
|
|
79
|
-
} catch (e) {
|
|
80
|
-
return ''
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
69
|
function resolveTilde (filePath) {
|
|
85
70
|
if (!filePath || typeof filePath !== 'string') {
|
|
86
71
|
return ''
|
|
@@ -271,20 +256,22 @@ module.exports = {
|
|
|
271
256
|
const ref = GITHUB_HEAD_REF || GITHUB_REF || ''
|
|
272
257
|
const refKey = ref.includes('tags/') ? GIT_TAG : GIT_BRANCH
|
|
273
258
|
|
|
259
|
+
// Both pipeline URL and job URL include GITHUB_SERVER_URL, which can include user credentials,
|
|
260
|
+
// so we pass them through `filterSensitiveInfoFromRepository`.
|
|
274
261
|
tags = {
|
|
275
262
|
[CI_PIPELINE_ID]: GITHUB_RUN_ID,
|
|
276
263
|
[CI_PIPELINE_NAME]: GITHUB_WORKFLOW,
|
|
277
264
|
[CI_PIPELINE_NUMBER]: GITHUB_RUN_NUMBER,
|
|
278
|
-
[CI_PIPELINE_URL]: pipelineURL,
|
|
265
|
+
[CI_PIPELINE_URL]: filterSensitiveInfoFromRepository(pipelineURL),
|
|
279
266
|
[CI_PROVIDER_NAME]: 'github',
|
|
280
267
|
[GIT_COMMIT_SHA]: GITHUB_SHA,
|
|
281
268
|
[GIT_REPOSITORY_URL]: repositoryURL,
|
|
282
|
-
[CI_JOB_URL]: jobUrl,
|
|
269
|
+
[CI_JOB_URL]: filterSensitiveInfoFromRepository(jobUrl),
|
|
283
270
|
[CI_JOB_NAME]: GITHUB_JOB,
|
|
284
271
|
[CI_WORKSPACE_PATH]: GITHUB_WORKSPACE,
|
|
285
272
|
[refKey]: ref,
|
|
286
273
|
[CI_ENV_VARS]: JSON.stringify({
|
|
287
|
-
GITHUB_SERVER_URL,
|
|
274
|
+
GITHUB_SERVER_URL: filterSensitiveInfoFromRepository(GITHUB_SERVER_URL),
|
|
288
275
|
GITHUB_REPOSITORY,
|
|
289
276
|
GITHUB_RUN_ID,
|
|
290
277
|
GITHUB_RUN_ATTEMPT
|
|
@@ -19,6 +19,7 @@ const {
|
|
|
19
19
|
GIT_COMMIT_AUTHOR_NAME,
|
|
20
20
|
CI_WORKSPACE_PATH
|
|
21
21
|
} = require('./tags')
|
|
22
|
+
const { filterSensitiveInfoFromRepository } = require('./url')
|
|
22
23
|
|
|
23
24
|
const GIT_REV_LIST_MAX_BUFFER = 8 * 1024 * 1024 // 8MB
|
|
24
25
|
|
|
@@ -214,7 +215,7 @@ function getGitMetadata (ciMetadata) {
|
|
|
214
215
|
|
|
215
216
|
return {
|
|
216
217
|
[GIT_REPOSITORY_URL]:
|
|
217
|
-
repositoryUrl || sanitizedExec('git', ['ls-remote', '--get-url']),
|
|
218
|
+
filterSensitiveInfoFromRepository(repositoryUrl || sanitizedExec('git', ['ls-remote', '--get-url'])),
|
|
218
219
|
[GIT_COMMIT_MESSAGE]:
|
|
219
220
|
commitMessage || sanitizedExec('git', ['show', '-s', '--format=%s']),
|
|
220
221
|
[GIT_COMMIT_AUTHOR_DATE]: authorDate,
|
|
@@ -48,8 +48,8 @@ function extractIp (config, req) {
|
|
|
48
48
|
|
|
49
49
|
let firstPrivateIp
|
|
50
50
|
if (headers) {
|
|
51
|
-
for (
|
|
52
|
-
const firstIp = findFirstIp(headers[
|
|
51
|
+
for (const ipHeaderName of ipHeaderList) {
|
|
52
|
+
const firstIp = findFirstIp(headers[ipHeaderName])
|
|
53
53
|
|
|
54
54
|
if (firstIp.public) {
|
|
55
55
|
return firstIp.public
|
|
@@ -59,7 +59,7 @@ function extractIp (config, req) {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
return firstPrivateIp ||
|
|
62
|
+
return firstPrivateIp || req.socket?.remoteAddress
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
function findFirstIp (str) {
|
|
@@ -68,8 +68,8 @@ function findFirstIp (str) {
|
|
|
68
68
|
|
|
69
69
|
const splitted = str.split(',')
|
|
70
70
|
|
|
71
|
-
for (
|
|
72
|
-
const chunk =
|
|
71
|
+
for (const part of splitted) {
|
|
72
|
+
const chunk = part.trim()
|
|
73
73
|
|
|
74
74
|
// TODO: strip port and interface data ?
|
|
75
75
|
|
|
@@ -90,5 +90,6 @@ function findFirstIp (str) {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
module.exports = {
|
|
93
|
-
extractIp
|
|
93
|
+
extractIp,
|
|
94
|
+
ipHeaderList
|
|
94
95
|
}
|
|
@@ -118,7 +118,8 @@ module.exports = {
|
|
|
118
118
|
fromCoverageMapToCoverage,
|
|
119
119
|
getTestLineStart,
|
|
120
120
|
getCallSites,
|
|
121
|
-
removeInvalidMetadata
|
|
121
|
+
removeInvalidMetadata,
|
|
122
|
+
parseAnnotations
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
// Returns pkg manager and its version, separated by '-', e.g. npm-8.15.0 or yarn-1.22.19
|
|
@@ -492,3 +493,30 @@ function getCallSites () {
|
|
|
492
493
|
|
|
493
494
|
return v8StackTrace
|
|
494
495
|
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Gets an object of test tags from an Playwright annotations array.
|
|
499
|
+
* @param {Object[]} annotations - Annotations from a Playwright test.
|
|
500
|
+
* @param {string} annotations[].type - Type of annotation. A string of the shape DD_TAGS[$tag_name].
|
|
501
|
+
* @param {string} annotations[].description - Value of the tag.
|
|
502
|
+
*/
|
|
503
|
+
function parseAnnotations (annotations) {
|
|
504
|
+
return annotations.reduce((tags, annotation) => {
|
|
505
|
+
if (!annotation?.type) {
|
|
506
|
+
return tags
|
|
507
|
+
}
|
|
508
|
+
const { type, description } = annotation
|
|
509
|
+
if (type.startsWith('DD_TAGS')) {
|
|
510
|
+
const regex = /\[(.*?)\]/
|
|
511
|
+
const match = regex.exec(type)
|
|
512
|
+
let tagValue = ''
|
|
513
|
+
if (match) {
|
|
514
|
+
tagValue = match[1]
|
|
515
|
+
}
|
|
516
|
+
if (tagValue) {
|
|
517
|
+
tags[tagValue] = description
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return tags
|
|
521
|
+
}, {})
|
|
522
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const { URL } = require('url')
|
|
2
|
+
|
|
3
|
+
function filterSensitiveInfoFromRepository (repositoryUrl) {
|
|
4
|
+
if (!repositoryUrl) {
|
|
5
|
+
return ''
|
|
6
|
+
}
|
|
7
|
+
if (repositoryUrl.startsWith('git@')) {
|
|
8
|
+
return repositoryUrl
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Remove the username from ssh URLs
|
|
12
|
+
if (repositoryUrl.startsWith('ssh://')) {
|
|
13
|
+
const sshRegex = /^(ssh:\/\/)[^@/]*@/
|
|
14
|
+
return repositoryUrl.replace(sshRegex, '$1')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const { protocol, host, pathname } = new URL(repositoryUrl)
|
|
19
|
+
|
|
20
|
+
return `${protocol}//${host}${pathname === '/' ? '' : pathname}`
|
|
21
|
+
} catch (e) {
|
|
22
|
+
return ''
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { filterSensitiveInfoFromRepository }
|
|
@@ -13,7 +13,7 @@ const {
|
|
|
13
13
|
} = require('./tags')
|
|
14
14
|
|
|
15
15
|
const { normalizeRef } = require('./ci')
|
|
16
|
-
const {
|
|
16
|
+
const { filterSensitiveInfoFromRepository } = require('./url')
|
|
17
17
|
|
|
18
18
|
function removeEmptyValues (tags) {
|
|
19
19
|
return Object.keys(tags).reduce((filteredTags, tag) => {
|
|
@@ -27,19 +27,6 @@ function removeEmptyValues (tags) {
|
|
|
27
27
|
}, {})
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function filterSensitiveInfoFromRepository (repositoryUrl) {
|
|
31
|
-
try {
|
|
32
|
-
if (repositoryUrl.startsWith('git@')) {
|
|
33
|
-
return repositoryUrl
|
|
34
|
-
}
|
|
35
|
-
const { protocol, hostname, pathname } = new URL(repositoryUrl)
|
|
36
|
-
|
|
37
|
-
return `${protocol}//${hostname}${pathname}`
|
|
38
|
-
} catch (e) {
|
|
39
|
-
return repositoryUrl
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
30
|
// The regex is extracted from
|
|
44
31
|
// https://github.com/jonschlinkert/is-git-url/blob/396965ffabf2f46656c8af4c47bef1d69f09292e/index.js#L9C15-L9C87
|
|
45
32
|
function validateGitRepositoryUrl (repoUrl) {
|
|
@@ -9,6 +9,7 @@ const { FileExporter } = require('./exporters/file')
|
|
|
9
9
|
const { ConsoleLogger } = require('./loggers/console')
|
|
10
10
|
const WallProfiler = require('./profilers/wall')
|
|
11
11
|
const SpaceProfiler = require('./profilers/space')
|
|
12
|
+
const EventsProfiler = require('./profilers/events')
|
|
12
13
|
const { oomExportStrategies, snapshotKinds } = require('./constants')
|
|
13
14
|
const { tagger } = require('./tagger')
|
|
14
15
|
const { isFalse, isTrue } = require('../util')
|
|
@@ -37,6 +38,7 @@ class Config {
|
|
|
37
38
|
DD_PROFILING_EXPERIMENTAL_OOM_HEAP_LIMIT_EXTENSION_SIZE,
|
|
38
39
|
DD_PROFILING_EXPERIMENTAL_OOM_MAX_HEAP_EXTENSION_COUNT,
|
|
39
40
|
DD_PROFILING_EXPERIMENTAL_OOM_EXPORT_STRATEGIES,
|
|
41
|
+
DD_PROFILING_EXPERIMENTAL_TIMELINE_ENABLED,
|
|
40
42
|
DD_PROFILING_CODEHOTSPOTS_ENABLED,
|
|
41
43
|
DD_PROFILING_ENDPOINT_COLLECTION_ENABLED,
|
|
42
44
|
DD_PROFILING_EXPERIMENTAL_CODEHOTSPOTS_ENABLED,
|
|
@@ -126,26 +128,17 @@ class Config {
|
|
|
126
128
|
|
|
127
129
|
const profilers = options.profilers
|
|
128
130
|
? options.profilers
|
|
129
|
-
: getProfilers({
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
this.codeHotspotsEnabled = isTrue(
|
|
131
|
+
: getProfilers({
|
|
132
|
+
DD_PROFILING_HEAP_ENABLED,
|
|
133
|
+
DD_PROFILING_WALLTIME_ENABLED,
|
|
134
|
+
DD_PROFILING_EXPERIMENTAL_TIMELINE_ENABLED,
|
|
135
|
+
DD_PROFILING_PROFILERS
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
this.codeHotspotsEnabled = isTrue(coalesce(options.codeHotspotsEnabled,
|
|
139
|
+
DD_PROFILING_CODEHOTSPOTS_ENABLED,
|
|
140
|
+
DD_PROFILING_EXPERIMENTAL_CODEHOTSPOTS_ENABLED, false))
|
|
137
141
|
logExperimentalVarDeprecation('CODEHOTSPOTS_ENABLED')
|
|
138
|
-
if (this.endpointCollectionEnabled && !this.codeHotspotsEnabled) {
|
|
139
|
-
if (getCodeHotspotsOptionsOr(undefined) !== undefined) {
|
|
140
|
-
this.logger.warn(
|
|
141
|
-
'Endpoint collection is enabled, but Code Hotspots are disabled. ' +
|
|
142
|
-
'Enable Code Hotspots too for endpoint collection to work.')
|
|
143
|
-
this.endpointCollectionEnabled = false
|
|
144
|
-
} else {
|
|
145
|
-
this.logger.info('Code Hotspots are implicitly enabled by endpoint collection.')
|
|
146
|
-
this.codeHotspotsEnabled = true
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
142
|
|
|
150
143
|
this.profilers = ensureProfilers(profilers, this)
|
|
151
144
|
}
|
|
@@ -153,7 +146,10 @@ class Config {
|
|
|
153
146
|
|
|
154
147
|
module.exports = { Config }
|
|
155
148
|
|
|
156
|
-
function getProfilers ({
|
|
149
|
+
function getProfilers ({
|
|
150
|
+
DD_PROFILING_HEAP_ENABLED, DD_PROFILING_WALLTIME_ENABLED,
|
|
151
|
+
DD_PROFILING_PROFILERS, DD_PROFILING_EXPERIMENTAL_TIMELINE_ENABLED
|
|
152
|
+
}) {
|
|
157
153
|
// First consider "legacy" DD_PROFILING_PROFILERS env variable, defaulting to wall + space
|
|
158
154
|
// Use a Set to avoid duplicates
|
|
159
155
|
const profilers = new Set(coalesce(DD_PROFILING_PROFILERS, 'wall,space').split(','))
|
|
@@ -176,6 +172,11 @@ function getProfilers ({ DD_PROFILING_HEAP_ENABLED, DD_PROFILING_WALLTIME_ENABLE
|
|
|
176
172
|
}
|
|
177
173
|
}
|
|
178
174
|
|
|
175
|
+
// Events profiler is a profiler for timeline events that goes with the wall
|
|
176
|
+
// profiler
|
|
177
|
+
if (profilers.has('wall') && DD_PROFILING_EXPERIMENTAL_TIMELINE_ENABLED) {
|
|
178
|
+
profilers.add('events')
|
|
179
|
+
}
|
|
179
180
|
return [...profilers]
|
|
180
181
|
}
|
|
181
182
|
|
|
@@ -237,6 +238,8 @@ function getProfiler (name, options) {
|
|
|
237
238
|
return new WallProfiler(options)
|
|
238
239
|
case 'space':
|
|
239
240
|
return new SpaceProfiler(options)
|
|
241
|
+
case 'events':
|
|
242
|
+
return new EventsProfiler(options)
|
|
240
243
|
default:
|
|
241
244
|
options.logger.error(`Unknown profiler "${name}"`)
|
|
242
245
|
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
const { performance, constants, PerformanceObserver } = require('node:perf_hooks')
|
|
2
|
+
const { END_TIMESTAMP, THREAD_NAME, threadNamePrefix } = require('./shared')
|
|
3
|
+
const semver = require('semver')
|
|
4
|
+
const { Function, Label, Line, Location, Profile, Sample, StringTable, ValueType } = require('pprof-format')
|
|
5
|
+
const pprof = require('@datadog/pprof/')
|
|
6
|
+
|
|
7
|
+
// Format of perf_hooks events changed with Node 16, we need to be mindful of it.
|
|
8
|
+
const node16 = semver.gte(process.version, '16.0.0')
|
|
9
|
+
|
|
10
|
+
// perf_hooks uses millis, with fractional part representing nanos. We emit nanos into the pprof file.
|
|
11
|
+
const MS_TO_NS = 1000000
|
|
12
|
+
|
|
13
|
+
// While this is an "events profiler", meaning it emits a pprof file based on events observed as
|
|
14
|
+
// perf_hooks events, the emitted pprof file uses the type "timeline".
|
|
15
|
+
const pprofValueType = 'timeline'
|
|
16
|
+
const pprofValueUnit = 'nanoseconds'
|
|
17
|
+
const threadName = `${threadNamePrefix} GC`
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* This class generates pprof files with timeline events sourced from Node.js
|
|
21
|
+
* performance measurement APIs.
|
|
22
|
+
*/
|
|
23
|
+
class EventsProfiler {
|
|
24
|
+
constructor (options = {}) {
|
|
25
|
+
this.type = 'events'
|
|
26
|
+
this._flushIntervalNanos = (options.flushInterval || 60000) * 1e6 // 60 sec
|
|
27
|
+
this._observer = undefined
|
|
28
|
+
this.entries = []
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
start () {
|
|
32
|
+
function add (items) {
|
|
33
|
+
this.entries.push(...items.getEntries())
|
|
34
|
+
}
|
|
35
|
+
if (!this._observer) {
|
|
36
|
+
this._observer = new PerformanceObserver(add.bind(this))
|
|
37
|
+
}
|
|
38
|
+
// Currently only support GC
|
|
39
|
+
this._observer.observe({ type: 'gc' })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
stop () {
|
|
43
|
+
if (this._observer) {
|
|
44
|
+
this._observer.disconnect()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
profile () {
|
|
49
|
+
const stringTable = new StringTable()
|
|
50
|
+
const timestampLabelKey = stringTable.dedup(END_TIMESTAMP)
|
|
51
|
+
const kindLabelKey = stringTable.dedup('gc type')
|
|
52
|
+
const reasonLabelKey = stringTable.dedup('gc reason')
|
|
53
|
+
const kindLabels = []
|
|
54
|
+
const reasonLabels = []
|
|
55
|
+
const locations = []
|
|
56
|
+
const functions = []
|
|
57
|
+
const locationsPerKind = []
|
|
58
|
+
const flagObj = {}
|
|
59
|
+
|
|
60
|
+
function labelFromStr (key, valStr) {
|
|
61
|
+
return new Label({ key, str: stringTable.dedup(valStr) })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function labelFromStrStr (keyStr, valStr) {
|
|
65
|
+
return labelFromStr(stringTable.dedup(keyStr), valStr)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Create labels for all GC performance flags and kinds of GC
|
|
69
|
+
for (const [key, value] of Object.entries(constants)) {
|
|
70
|
+
if (key.startsWith('NODE_PERFORMANCE_GC_FLAGS_')) {
|
|
71
|
+
flagObj[key.substring(26).toLowerCase()] = value
|
|
72
|
+
} else if (key.startsWith('NODE_PERFORMANCE_GC_')) {
|
|
73
|
+
// It's a constant for a kind of GC
|
|
74
|
+
const kind = key.substring(20).toLowerCase()
|
|
75
|
+
kindLabels[value] = labelFromStr(kindLabelKey, kind)
|
|
76
|
+
// Construct a single-frame "location" too
|
|
77
|
+
const fn = new Function({ id: functions.length + 1, name: stringTable.dedup(`${kind} GC`) })
|
|
78
|
+
functions.push(fn)
|
|
79
|
+
const line = new Line({ functionId: fn.id })
|
|
80
|
+
const location = new Location({ id: locations.length + 1, line: [line] })
|
|
81
|
+
locations.push(location)
|
|
82
|
+
locationsPerKind[value] = [location.id]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const gcEventLabel = labelFromStrStr('event', 'gc')
|
|
87
|
+
const threadLabel = labelFromStrStr(THREAD_NAME, threadName)
|
|
88
|
+
|
|
89
|
+
function getReasonLabel (flags) {
|
|
90
|
+
if (flags === 0) {
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
let reasonLabel = reasonLabels[flags]
|
|
94
|
+
if (!reasonLabel) {
|
|
95
|
+
const reasons = []
|
|
96
|
+
for (const [key, value] of Object.entries(flagObj)) {
|
|
97
|
+
if (value & flags) {
|
|
98
|
+
reasons.push(key)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const reasonStr = reasons.join(',')
|
|
102
|
+
reasonLabel = labelFromStr(reasonLabelKey, reasonStr)
|
|
103
|
+
reasonLabels[flags] = reasonLabel
|
|
104
|
+
}
|
|
105
|
+
return reasonLabel
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let durationFrom = Number.POSITIVE_INFINITY
|
|
109
|
+
let durationTo = 0
|
|
110
|
+
const dateOffset = BigInt(Math.round(performance.timeOrigin * MS_TO_NS))
|
|
111
|
+
|
|
112
|
+
const samples = this.entries.map((item) => {
|
|
113
|
+
const { startTime, duration } = item
|
|
114
|
+
const { kind, flags } = node16 ? item.detail : item
|
|
115
|
+
const endTime = startTime + duration
|
|
116
|
+
if (durationFrom > startTime) durationFrom = startTime
|
|
117
|
+
if (durationTo < endTime) durationTo = endTime
|
|
118
|
+
const labels = [
|
|
119
|
+
gcEventLabel,
|
|
120
|
+
threadLabel,
|
|
121
|
+
new Label({ key: timestampLabelKey, num: dateOffset + BigInt(Math.round(endTime * MS_TO_NS)) }),
|
|
122
|
+
kindLabels[kind]
|
|
123
|
+
]
|
|
124
|
+
const reasonLabel = getReasonLabel(flags)
|
|
125
|
+
if (reasonLabel) {
|
|
126
|
+
labels.push(reasonLabel)
|
|
127
|
+
}
|
|
128
|
+
const sample = new Sample({
|
|
129
|
+
value: [Math.round(duration * MS_TO_NS)],
|
|
130
|
+
label: labels,
|
|
131
|
+
locationId: locationsPerKind[kind]
|
|
132
|
+
})
|
|
133
|
+
return sample
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
this.entries = []
|
|
137
|
+
|
|
138
|
+
const timeValueType = new ValueType({
|
|
139
|
+
type: stringTable.dedup(pprofValueType),
|
|
140
|
+
unit: stringTable.dedup(pprofValueUnit)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
return new Profile({
|
|
144
|
+
sampleType: [timeValueType],
|
|
145
|
+
timeNanos: dateOffset + BigInt(Math.round(durationFrom * MS_TO_NS)),
|
|
146
|
+
periodType: timeValueType,
|
|
147
|
+
period: this._flushIntervalNanos,
|
|
148
|
+
durationNanos: Math.max(0, Math.round((durationTo - durationFrom) * MS_TO_NS)),
|
|
149
|
+
sample: samples,
|
|
150
|
+
location: locations,
|
|
151
|
+
function: functions,
|
|
152
|
+
stringTable: stringTable
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
encode (profile) {
|
|
157
|
+
return pprof.encode(profile)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = EventsProfiler
|