Skip to content

Commit 4a970c8

Browse files
committed
Add max concurrency when processing object props
1 parent 03b634f commit 4a970c8

File tree

15 files changed

+213
-77
lines changed

15 files changed

+213
-77
lines changed

integration-tests/debugger/basic.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -641,7 +641,7 @@ describe('Dynamic Instrumentation', function () {
641641
})
642642

643643
describe('condition', function () {
644-
beforeEach(t.triggerBreakpoint)
644+
beforeEach(() => { t.triggerBreakpoint() })
645645

646646
it('should trigger when condition is met', function (done) {
647647
t.agent.on('debugger-input', () => {

integration-tests/debugger/snapshot-global-sample-rate.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ describe('Dynamic Instrumentation', function () {
1111

1212
describe('input messages', function () {
1313
describe('with snapshot', function () {
14-
beforeEach(t.triggerBreakpoint)
14+
beforeEach(() => { t.triggerBreakpoint() })
1515

1616
it('should respect global max snapshot sampling rate', function (_done) {
1717
const MAX_SNAPSHOTS_PER_SECOND_GLOBALLY = 25

integration-tests/debugger/snapshot-pruning.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ describe('Dynamic Instrumentation', function () {
88

99
describe('input messages', function () {
1010
describe('with snapshot', function () {
11-
beforeEach(t.triggerBreakpoint)
11+
beforeEach(() => { t.triggerBreakpoint() })
1212

1313
it('should prune snapshot if payload is too large', function (done) {
1414
t.agent.on('debugger-input', ({ payload: [payload] }) => {

integration-tests/debugger/snapshot-time-budget.spec.js

Lines changed: 70 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,86 @@
33
const assert = require('node:assert')
44
const { setup } = require('./utils')
55

6-
describe('Dynamic Instrumentation', function () {
7-
// Force a very small time budget in ms to trigger partial snapshots
8-
const t = setup({
9-
dependencies: ['fastify'],
10-
env: { DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS: '1' }
11-
})
6+
const tolerance = 5
127

8+
describe('Dynamic Instrumentation', function () {
139
describe('input messages', function () {
1410
describe('with snapshot under tight time budget', function () {
15-
beforeEach(t.triggerBreakpoint)
16-
17-
it('should include partial snapshot marked with notCapturedReason: timeout', function (done) {
18-
t.agent.on('debugger-input', ({ payload: [{ debugger: { snapshot: { captures } } }] }) => {
19-
const { locals } = captures.lines[t.breakpoint.line]
20-
assert.strictEqual(
21-
containsTimeBudget(locals),
22-
true,
23-
'expected at least one field/element to be marked with notCapturedReason: "timeout"'
24-
)
25-
done()
11+
context('1ms time budget', function () {
12+
// Force a very small time budget in ms to trigger partial snapshots
13+
const target = 1
14+
const t = setup({
15+
dependencies: ['fastify'],
16+
env: { DD_DYNAMIC_INSTRUMENTATION_CAPTURE_TIMEOUT_MS: String(target) }
2617
})
2718

28-
t.agent.addRemoteConfig(t.generateRemoteConfig({
29-
captureSnapshot: true,
30-
capture: { maxReferenceDepth: 5 }
31-
}))
19+
it(
20+
'should include partial snapshot marked with notCapturedReason: timeout',
21+
test({ t, maxPausedTime: target + tolerance, breakpointIndex: 0, maxReferenceDepth: 5 })
22+
)
23+
})
24+
25+
context('default time budget', function () {
26+
const target = 10 // default time budget in ms
27+
const t = setup({ dependencies: ['fastify'] })
28+
29+
// TODO: Make this pass
30+
// eslint-disable-next-line mocha/no-pending-tests
31+
it.skip(
32+
'should keep budget when state includes an object with 1 million properties',
33+
test({ t, maxPausedTime: target + tolerance, breakpointIndex: 1, maxReferenceDepth: 1 })
34+
)
35+
36+
// TODO: Make this pass
37+
// eslint-disable-next-line mocha/no-pending-tests
38+
it.skip(
39+
'should keep budget when state includes an array of 1 million primitives',
40+
test({ t, maxPausedTime: target + tolerance, breakpointIndex: 2, maxReferenceDepth: 1 })
41+
)
42+
43+
// TODO: Make this pass
44+
// eslint-disable-next-line mocha/no-pending-tests
45+
it.skip(
46+
'should keep budget when state includes an array of 1 million objects',
47+
test({ t, maxPausedTime: target + tolerance, breakpointIndex: 3, maxReferenceDepth: 1 })
48+
)
3249
})
3350
})
3451
})
3552
})
3653

54+
function test ({ t, maxPausedTime, breakpointIndex, maxReferenceDepth }) {
55+
const breakpoint = t.breakpoints[breakpointIndex]
56+
57+
return async function () {
58+
const payloadReceived = new Promise((/** @type {(value?: unknown) => void} */ resolve) => {
59+
t.agent.on('debugger-input', ({ payload: [{ debugger: { snapshot: { captures } } }] }) => {
60+
const { locals } = captures.lines[breakpoint.line]
61+
assert.strictEqual(
62+
containsTimeBudget(locals),
63+
true,
64+
'expected at least one field/element to be marked with notCapturedReason: "timeout"'
65+
)
66+
resolve()
67+
})
68+
})
69+
70+
t.agent.addRemoteConfig(breakpoint.generateRemoteConfig({
71+
captureSnapshot: true,
72+
capture: { maxReferenceDepth }
73+
}))
74+
75+
const { data } = await breakpoint.triggerBreakpoint()
76+
77+
assert.ok(
78+
data.paused <= maxPausedTime,
79+
`expected thread to be paused <=${maxPausedTime}ms, but was paused for ~${data.paused}ms`
80+
)
81+
82+
await payloadReceived
83+
}
84+
}
85+
3786
function containsTimeBudget (node) {
3887
if (node == null || typeof node !== 'object') return false
3988
if (node.notCapturedReason === 'timeout') return true

integration-tests/debugger/snapshot.spec.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ describe('Dynamic Instrumentation', function () {
88

99
describe('input messages', function () {
1010
describe('with snapshot', function () {
11-
beforeEach(t.triggerBreakpoint)
11+
beforeEach(() => { t.triggerBreakpoint() })
1212

1313
it('should capture a snapshot', function (done) {
1414
t.agent.on('debugger-input', ({ payload: [{ debugger: { snapshot: { captures } } }] }) => {
@@ -209,12 +209,12 @@ describe('Dynamic Instrumentation', function () {
209209
t.agent.on('debugger-input', ({ payload: [{ debugger: { snapshot: { captures } } }] }) => {
210210
const { locals } = captures.lines[t.breakpoint.line]
211211

212-
assert.deepStrictEqual(Object.keys(locals), [
212+
assert.deepStrictEqual(Object.keys(locals).sort(), [
213213
// Up to 3 properties from the closure scope
214214
'fastify', 'getUndefined',
215215
// Up to 3 properties from the local scope
216216
'request', 'nil', 'undef'
217-
])
217+
].sort())
218218

219219
assert.strictEqual(locals.request.type, 'Request')
220220
assert.strictEqual(Object.keys(locals.request.fields).length, maxFieldCount)

integration-tests/debugger/source-map-support.spec.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ describe('Dynamic Instrumentation', function () {
1111
testAppSource: 'target-app/source-map-support/typescript.ts'
1212
})
1313

14-
beforeEach(t.triggerBreakpoint)
14+
beforeEach(() => { t.triggerBreakpoint() })
1515

1616
it('should support source maps', function (done) {
1717
t.agent.on('debugger-input', ({ payload: [{ debugger: { snapshot: { probe: { location } } } }] }) => {
@@ -32,7 +32,7 @@ describe('Dynamic Instrumentation', function () {
3232
testAppSource: 'target-app/source-map-support/minify.js'
3333
})
3434

35-
beforeEach(t.triggerBreakpoint)
35+
beforeEach(() => { t.triggerBreakpoint() })
3636

3737
it('should support source maps', function (done) {
3838
t.agent.on('debugger-input', ({ payload: [{ debugger: { snapshot: { probe: { location } } } }] }) => {
@@ -56,7 +56,7 @@ describe('Dynamic Instrumentation', function () {
5656
dependencies: []
5757
})
5858

59-
beforeEach(t.triggerBreakpoint)
59+
beforeEach(() => { t.triggerBreakpoint() })
6060

6161
it('should support relative source paths in source maps', function (done) {
6262
t.agent.on('debugger-input', ({ payload: [{ debugger: { snapshot: { probe: { location } } } }] }) => {

integration-tests/debugger/target-app/snapshot-time-budget.js

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,35 @@ const Fastify = require('fastify')
66

77
const fastify = Fastify({ logger: { level: 'error' } })
88

9-
fastify.get('/:name', function handler (request) {
9+
fastify.get('/deeply-nested-large-object', function handler () {
1010
// The size of `obj` generated is carefully tuned to never result in a snapshot larger than the 1MB size limit, while
1111
// still being large enough to trigger the time budget limit. However, the size of `fastify` and `request` is not
1212
// stable across Node.js and Fastify versions, so the generated object might need to be adjusted.
13-
const obj = generateObject(5, 6) // eslint-disable-line no-unused-vars
13+
const obj = generateObject(5, 12) // eslint-disable-line no-unused-vars
14+
const start = process.hrtime.bigint()
15+
const diff = process.hrtime.bigint() - start // BREAKPOINT: /deeply-nested-large-object
16+
return { paused: Number(diff) / 1_000_000 }
17+
})
18+
19+
fastify.get('/large-object-of-primitives', function handler () {
20+
const obj = generateObject(0, 1_000_000) // eslint-disable-line no-unused-vars
21+
const start = process.hrtime.bigint()
22+
const diff = process.hrtime.bigint() - start // BREAKPOINT: /large-object-of-primitives
23+
return { paused: Number(diff) / 1_000_000 }
24+
})
25+
26+
fastify.get('/large-array-of-primitives', function handler () {
27+
const arr = Array.from({ length: 1_000_000 }, (_, i) => i) // eslint-disable-line no-unused-vars
28+
const start = process.hrtime.bigint()
29+
const diff = process.hrtime.bigint() - start // BREAKPOINT: /large-array-of-primitives
30+
return { paused: Number(diff) / 1_000_000 }
31+
})
1432

15-
return { hello: request.params.name } // BREAKPOINT: /foo
33+
fastify.get('/large-array-of-objects', function handler () {
34+
const arr = Array.from({ length: 1_000_000 }, (_, i) => ({ i })) // eslint-disable-line no-unused-vars
35+
const start = process.hrtime.bigint()
36+
const diff = process.hrtime.bigint() - start // BREAKPOINT: /large-array-of-objects
37+
return { paused: Number(diff) / 1_000_000 }
1638
})
1739

1840
fastify.listen({ port: process.env.APP_PORT || 0 }, (err) => {

integration-tests/debugger/template.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ describe('Dynamic Instrumentation', function () {
1111
describe('template evaluation', function () {
1212
const t = setup({ dependencies: ['fastify'] })
1313

14-
beforeEach(t.triggerBreakpoint)
14+
beforeEach(() => { t.triggerBreakpoint() })
1515

1616
it('should evaluate template if it requires evaluation', function (done) {
1717
t.agent.on('debugger-input', ({ payload: [payload] }) => {

integration-tests/debugger/utils.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,16 @@ function setup ({ env, testApp, testAppSource, dependencies, silent, stdioHandle
5353
}
5454

5555
// Trigger the breakpoint once probe is successfully installed
56-
function triggerBreakpoint (url) {
56+
async function triggerBreakpoint (url) {
5757
let triggered = false
58-
t.agent.on('debugger-diagnostics', ({ payload }) => {
59-
payload.forEach((event) => {
60-
if (!triggered && event.debugger.diagnostics.status === 'INSTALLED') {
61-
triggered = true
62-
t.axios.get(url)
63-
}
58+
return new Promise((resolve, reject) => {
59+
t.agent.on('debugger-diagnostics', ({ payload }) => {
60+
payload.forEach((event) => {
61+
if (!triggered && event.debugger.diagnostics.status === 'INSTALLED') {
62+
triggered = true
63+
t.axios.get(url).then(resolve).catch(reject)
64+
}
65+
})
6466
})
6567
})
6668
}

packages/dd-trace/src/debugger/devtools_client/index.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,13 @@ session.on('Debugger.paused', async ({ params }) => {
167167
// TODO: Create unique states for each affected probe based on that probes unique `capture` settings (DEBUG-2863)
168168
const processLocalState = numberOfProbesWithSnapshots !== 0 && await getLocalStateForCallFrame(
169169
params.callFrames[0],
170-
start + BigInt(config.dynamicInstrumentation.captureTimeoutMs ?? 10) * 1_000_000n,
171-
{ maxReferenceDepth, maxCollectionSize, maxFieldCount, maxLength }
170+
{
171+
maxReferenceDepth,
172+
maxCollectionSize,
173+
maxFieldCount,
174+
maxLength,
175+
deadlineNs: start + BigInt(config.dynamicInstrumentation.captureTimeoutMs ?? 10) * 1_000_000n
176+
}
172177
)
173178

174179
await session.post('Debugger.resume')

0 commit comments

Comments
 (0)