Skip to content

Commit afd86e3

Browse files
committed
[DI] Add support for sampling
1 parent 87a98ad commit afd86e3

File tree

4 files changed

+85
-5
lines changed

4 files changed

+85
-5
lines changed

integration-tests/debugger/basic.spec.js

+39
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,45 @@ describe('Dynamic Instrumentation', function () {
338338
})
339339
})
340340

341+
describe('sampling', function () {
342+
it('should respect sampling rate for single probe', function (done) {
343+
let start, timer
344+
let payloadsReceived = 0
345+
const rcConfig = t.generateRemoteConfig({ sampling: { snapshotsPerSecond: 1 } })
346+
347+
function triggerBreakpointContinuously () {
348+
t.axios.get(t.breakpoint.url).catch(done)
349+
timer = setTimeout(triggerBreakpointContinuously, 10)
350+
}
351+
352+
t.agent.on('debugger-diagnostics', ({ payload }) => {
353+
if (payload.debugger.diagnostics.status === 'INSTALLED') triggerBreakpointContinuously()
354+
})
355+
356+
t.agent.on('debugger-input', () => {
357+
payloadsReceived++
358+
if (payloadsReceived === 1) {
359+
start = Date.now()
360+
} else if (payloadsReceived === 2) {
361+
const duration = Date.now() - start
362+
clearTimeout(timer)
363+
364+
// Allow for a variance of -5/+50ms (time will tell if this is enough)
365+
assert.isAbove(duration, 995)
366+
assert.isBelow(duration, 1050)
367+
368+
// Wait at least a full sampling period, to see if we get any more payloads
369+
timer = setTimeout(done, 1250)
370+
} else {
371+
clearTimeout(timer)
372+
done(new Error('Too many payloads received!'))
373+
}
374+
})
375+
376+
t.agent.addRemoteConfig(rcConfig)
377+
})
378+
})
379+
341380
describe('race conditions', function () {
342381
it('should remove the last breakpoint completely before trying to add a new one', function (done) {
343382
const rcConfig2 = t.generateRemoteConfig()

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

+8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict'
22

33
const session = require('./session')
4+
const { MAX_SNAPSHOTS_PER_SECOND_PER_PROBE, MAX_NON_SNAPSHOTS_PER_SECOND_PER_PROBE } = require('./defaults')
45
const { findScriptFromPartialPath, probes, breakpoints } = require('./state')
56
const log = require('../../log')
67

@@ -21,6 +22,13 @@ async function addBreakpoint (probe) {
2122
probe.location = { file, lines: [String(line)] }
2223
delete probe.where
2324

25+
// Optimize for fast calculations when probe is hit
26+
const snapshotsPerSecond = probe.sampling.snapshotsPerSecond ?? (probe.captureSnapshot
27+
? MAX_SNAPSHOTS_PER_SECOND_PER_PROBE
28+
: MAX_NON_SNAPSHOTS_PER_SECOND_PER_PROBE)
29+
probe.sampling.nsBetweenSampling = BigInt(1 / snapshotsPerSecond * 1e9)
30+
probe.lastCaptureNs = 0n
31+
2432
// TODO: Inbetween `await session.post('Debugger.enable')` and here, the scripts are parsed and cached.
2533
// Maybe there's a race condition here or maybe we're guraenteed that `await session.post('Debugger.enable')` will
2634
// not continue untill all scripts have been parsed?
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
'use strict'
2+
3+
module.exports = {
4+
MAX_SNAPSHOTS_PER_SECOND_PER_PROBE: 1,
5+
MAX_NON_SNAPSHOTS_PER_SECOND_PER_PROBE: 5_000
6+
}

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

+32-5
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,47 @@ require('./remote_config')
1818
const threadId = parentThreadId === 0 ? `pid:${process.pid}` : `pid:${process.pid};tid:${parentThreadId}`
1919
const threadName = parentThreadId === 0 ? 'MainThread' : `WorkerThread:${parentThreadId}`
2020

21+
// WARNING: The code above the line `await session.post('Debugger.resume')` is highly optimized. Please edit with care!
2122
session.on('Debugger.paused', async ({ params }) => {
2223
const start = process.hrtime.bigint()
23-
const timestamp = Date.now()
2424

2525
let captureSnapshotForProbe = null
2626
let maxReferenceDepth, maxCollectionSize, maxFieldCount, maxLength
27-
const probes = params.hitBreakpoints.map((id) => {
27+
28+
// V8 doesn't allow seting more than one breakpoint at a specific location, however, it's possible to set two
29+
// breakpoints just next to eachother that will "snap" to the same logical location, which in turn will be hit at the
30+
// same time. E.g. index.js:1:1 and index.js:1:2.
31+
// TODO: Investigate if it will improve performance to create a fast-path for when there's only a single breakpoint
32+
let sampled = false
33+
const length = params.hitBreakpoints.length
34+
let probes = new Array(length)
35+
for (let i = 0; i < length; i++) {
36+
const id = params.hitBreakpoints[i]
2837
const probe = breakpoints.get(id)
29-
if (probe.captureSnapshot) {
38+
39+
if (start - probe.lastCaptureNs < probe.sampling.nsBetweenSampling) {
40+
continue
41+
}
42+
43+
sampled = true
44+
probe.lastCaptureNs = start
45+
46+
if (probe.captureSnapshot === true) {
3047
captureSnapshotForProbe = probe
3148
maxReferenceDepth = highestOrUndefined(probe.capture.maxReferenceDepth, maxReferenceDepth)
3249
maxCollectionSize = highestOrUndefined(probe.capture.maxCollectionSize, maxCollectionSize)
3350
maxFieldCount = highestOrUndefined(probe.capture.maxFieldCount, maxFieldCount)
3451
maxLength = highestOrUndefined(probe.capture.maxLength, maxLength)
3552
}
36-
return probe
37-
})
53+
54+
probes[i] = probe
55+
}
56+
57+
if (sampled === false) {
58+
return session.post('Debugger.resume')
59+
}
60+
61+
const timestamp = Date.now()
3862

3963
let processLocalState
4064
if (captureSnapshotForProbe !== null) {
@@ -56,6 +80,9 @@ session.on('Debugger.paused', async ({ params }) => {
5680

5781
log.debug(`Finished processing breakpoints - main thread paused for: ${Number(diff) / 1000000} ms`)
5882

83+
// Due to the highly optimized algorithm above, the `probes` array might have gaps
84+
probes = probes.filter((probe) => !!probe)
85+
5986
const logger = {
6087
// We can safely use `location.file` from the first probe in the array, since all probes hit by `hitBreakpoints`
6188
// must exist in the same file since the debugger can only pause the main thread in one location.

0 commit comments

Comments
 (0)