Skip to content

Commit e48232c

Browse files
committed
Make Live Debugger per-second budgets configurable
Allow the browser debugger to configure global snapshot and per-probe sampling limits through init options so teams can tune backpressure without changing the runtime.
1 parent b9da4f3 commit e48232c

5 files changed

Lines changed: 174 additions & 8 deletions

File tree

packages/debugger/src/domain/api.spec.ts

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,18 @@ describe('api', () => {
77
let mockBatchAdd: jasmine.Spy
88
let mockRumGetInternalContext: jasmine.Spy
99

10+
function initTransport(overrides: Record<string, unknown> = {}) {
11+
resetDebuggerTransport()
12+
initDebuggerTransport({ service: 'test-service', env: 'test-env', ...overrides } as any, {
13+
add: mockBatchAdd,
14+
} as any)
15+
}
16+
1017
beforeEach(() => {
1118
clearProbes()
1219

1320
mockBatchAdd = jasmine.createSpy('batchAdd')
14-
initDebuggerTransport({ service: 'test-service', env: 'test-env' } as any, { add: mockBatchAdd } as any)
21+
initTransport()
1522

1623
// Mock DD_RUM global for context
1724
mockRumGetInternalContext = jasmine.createSpy('getInternalContext').and.returnValue({
@@ -519,6 +526,85 @@ describe('api', () => {
519526
// Should only get 25 calls (global limit)
520527
expect(mockBatchAdd).toHaveBeenCalledTimes(25)
521528
})
529+
530+
it('should respect configured global snapshot rate limit', () => {
531+
initTransport({ maxSnapshotsPerSecondGlobally: 2 })
532+
533+
for (let i = 0; i < 3; i++) {
534+
const probe: Probe = {
535+
id: `configured-global-probe-${i}`,
536+
version: 0,
537+
type: 'LOG_PROBE',
538+
where: { typeName: 'TestClass', methodName: `configuredGlobal${i}` },
539+
template: 'Test',
540+
captureSnapshot: true,
541+
capture: {},
542+
sampling: { snapshotsPerSecond: 5000 },
543+
evaluateAt: 'ENTRY',
544+
}
545+
addProbe(probe)
546+
}
547+
548+
for (let i = 0; i < 3; i++) {
549+
const probes = getProbes(`TestClass;configuredGlobal${i}`)!
550+
onEntry(probes, {}, {})
551+
onReturn(probes, null, {}, {}, {})
552+
}
553+
554+
expect(mockBatchAdd).toHaveBeenCalledTimes(2)
555+
})
556+
})
557+
558+
describe('configured per-second budgets', () => {
559+
it('should respect configured default snapshot per-probe rate limit', () => {
560+
initTransport({ maxSnapshotsPerSecondPerProbe: 0.5 })
561+
562+
const probe: Probe = {
563+
id: 'configured-snapshot-rate-probe',
564+
version: 0,
565+
type: 'LOG_PROBE',
566+
where: { typeName: 'TestClass', methodName: 'configuredSnapshotRate' },
567+
template: 'Test',
568+
captureSnapshot: true,
569+
capture: { maxReferenceDepth: 1 },
570+
sampling: {},
571+
evaluateAt: 'ENTRY',
572+
}
573+
addProbe(probe)
574+
575+
const probes = getProbes('TestClass;configuredSnapshotRate')!
576+
onEntry(probes, {}, {})
577+
onReturn(probes, null, {}, {}, {})
578+
onEntry(probes, {}, {})
579+
onReturn(probes, null, {}, {}, {})
580+
581+
expect(mockBatchAdd).toHaveBeenCalledTimes(1)
582+
})
583+
584+
it('should respect configured default non-snapshot per-probe rate limit', () => {
585+
initTransport({ maxNonSnapshotsPerSecondPerProbe: 1 })
586+
587+
const probe: Probe = {
588+
id: 'configured-non-snapshot-rate-probe',
589+
version: 0,
590+
type: 'LOG_PROBE',
591+
where: { typeName: 'TestClass', methodName: 'configuredNonSnapshotRate' },
592+
template: 'Test',
593+
captureSnapshot: false,
594+
capture: {},
595+
sampling: {},
596+
evaluateAt: 'ENTRY',
597+
}
598+
addProbe(probe)
599+
600+
const probes = getProbes('TestClass;configuredNonSnapshotRate')!
601+
onEntry(probes, {}, {})
602+
onReturn(probes, null, {}, {}, {})
603+
onEntry(probes, {}, {})
604+
onReturn(probes, null, {}, {}, {})
605+
606+
expect(mockBatchAdd).toHaveBeenCalledTimes(1)
607+
})
522608
})
523609

524610
describe('active entries cleanup', () => {

packages/debugger/src/domain/api.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import { timeStampNow, display, buildTag, generateUUID, getGlobalObject } from '
44
import type { BrowserWindow, DebuggerInitConfiguration } from '../entries/main'
55
import { capture, captureFields } from './capture'
66
import type { InitializedProbe } from './probes'
7-
import { checkGlobalSnapshotBudget } from './probes'
7+
import {
8+
checkGlobalSnapshotBudget,
9+
resetProbeBudgetConfiguration,
10+
setProbeBudgetConfiguration,
11+
} from './probes'
812
import type { ActiveEntry } from './activeEntries'
913
import { active } from './activeEntries'
1014
import { captureStackTrace, parseStackTrace } from './stacktrace'
@@ -27,12 +31,14 @@ let debuggerConfig: DebuggerInitConfiguration | undefined
2731
export function initDebuggerTransport(config: DebuggerInitConfiguration, batch: Batch): void {
2832
debuggerConfig = config
2933
debuggerBatch = batch
34+
setProbeBudgetConfiguration(config)
3035
}
3136

3237
export function resetDebuggerTransport(): void {
3338
debuggerBatch = undefined
3439
debuggerConfig = undefined
3540
active.clear()
41+
resetProbeBudgetConfiguration()
3642
}
3743

3844
/**

packages/debugger/src/domain/probes.spec.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import { display } from '@datadog/browser-core'
22
import { registerCleanupTask } from '@datadog/browser-core/test'
3-
import { initializeProbe, getProbes, addProbe, removeProbe, checkGlobalSnapshotBudget, clearProbes } from './probes'
3+
import {
4+
initializeProbe,
5+
getProbes,
6+
addProbe,
7+
removeProbe,
8+
checkGlobalSnapshotBudget,
9+
clearProbes,
10+
resetProbeBudgetConfiguration,
11+
} from './probes'
412
import type { Probe } from './probes'
513

614
interface TemplateWithCache {
@@ -11,6 +19,7 @@ interface TemplateWithCache {
1119
describe('probes', () => {
1220
beforeEach(() => {
1321
clearProbes()
22+
resetProbeBudgetConfiguration()
1423

1524
registerCleanupTask(() => clearProbes())
1625
})

packages/debugger/src/domain/probes.ts

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import type { TemplateSegment, CompiledTemplate } from './template'
88
import type { CaptureOptions } from './capture'
99

1010
// Sampling rate limits
11-
const MAX_SNAPSHOTS_PER_SECOND_GLOBALLY = 25
12-
const MAX_SNAPSHOTS_PER_SECOND_PER_PROBE = 1
13-
const MAX_NON_SNAPSHOTS_PER_SECOND_PER_PROBE = 5000
11+
const DEFAULT_MAX_SNAPSHOTS_PER_SECOND_GLOBALLY = 25
12+
const DEFAULT_MAX_SNAPSHOTS_PER_SECOND_PER_PROBE = 1
13+
const DEFAULT_MAX_NON_SNAPSHOTS_PER_SECOND_PER_PROBE = 5000
1414

1515
// Global snapshot rate limiting
1616
let globalSnapshotSamplingRateWindowStart = 0
@@ -32,6 +32,12 @@ export interface ProbeSampling {
3232
snapshotsPerSecond?: number
3333
}
3434

35+
export interface ProbeBudgetConfiguration {
36+
maxSnapshotsPerSecondGlobally?: number
37+
maxSnapshotsPerSecondPerProbe?: number
38+
maxNonSnapshotsPerSecondPerProbe?: number
39+
}
40+
3541
export interface Probe {
3642
id: string
3743
version: number
@@ -70,6 +76,32 @@ const probeIdToFunctionId: Record<string, string> = {
7076
// @ts-expect-error - Pre-populate with a placeholder key to help V8 optimize property lookups.
7177
__placeholder__: undefined,
7278
}
79+
let currentProbeBudgetConfiguration: Required<ProbeBudgetConfiguration> = {
80+
maxSnapshotsPerSecondGlobally: DEFAULT_MAX_SNAPSHOTS_PER_SECOND_GLOBALLY,
81+
maxSnapshotsPerSecondPerProbe: DEFAULT_MAX_SNAPSHOTS_PER_SECOND_PER_PROBE,
82+
maxNonSnapshotsPerSecondPerProbe: DEFAULT_MAX_NON_SNAPSHOTS_PER_SECOND_PER_PROBE,
83+
}
84+
85+
export function setProbeBudgetConfiguration(configuration: ProbeBudgetConfiguration = {}): void {
86+
currentProbeBudgetConfiguration = {
87+
maxSnapshotsPerSecondGlobally: normalizeProbeBudgetRate(
88+
configuration.maxSnapshotsPerSecondGlobally,
89+
DEFAULT_MAX_SNAPSHOTS_PER_SECOND_GLOBALLY
90+
),
91+
maxSnapshotsPerSecondPerProbe: normalizeProbeBudgetRate(
92+
configuration.maxSnapshotsPerSecondPerProbe,
93+
DEFAULT_MAX_SNAPSHOTS_PER_SECOND_PER_PROBE
94+
),
95+
maxNonSnapshotsPerSecondPerProbe: normalizeProbeBudgetRate(
96+
configuration.maxNonSnapshotsPerSecondPerProbe,
97+
DEFAULT_MAX_NON_SNAPSHOTS_PER_SECOND_PER_PROBE
98+
),
99+
}
100+
}
101+
102+
export function resetProbeBudgetConfiguration(): void {
103+
setProbeBudgetConfiguration()
104+
}
73105

74106
/**
75107
* Add a probe to the registry
@@ -136,6 +168,9 @@ export function removeProbe(id: string): void {
136168
probe.condition.clearCache()
137169
}
138170
probes.splice(i, 1)
171+
// TODO: Gracefully drain in-flight entries instead of clearing them immediately.
172+
// Deleting a probe can currently race with return/throw handling, whether removal
173+
// comes from delivery updates or budget-based auto-unregistering.
139174
clearActiveEntries(id)
140175
break
141176
}
@@ -199,7 +234,7 @@ export function checkGlobalSnapshotBudget(now: number, captureSnapshot: boolean)
199234
}
200235

201236
// Check if we've exceeded the global limit
202-
if (snapshotsSampledWithinTheLastSecond >= MAX_SNAPSHOTS_PER_SECOND_GLOBALLY) {
237+
if (snapshotsSampledWithinTheLastSecond >= currentProbeBudgetConfiguration.maxSnapshotsPerSecondGlobally) {
203238
return false
204239
}
205240

@@ -266,7 +301,13 @@ export function initializeProbe(probe: Probe): asserts probe is InitializedProbe
266301
// Optimize for fast calculations when probe is hit - calculate sampling budget
267302
const snapshotsPerSecond =
268303
probe.sampling?.snapshotsPerSecond ??
269-
(probe.captureSnapshot ? MAX_SNAPSHOTS_PER_SECOND_PER_PROBE : MAX_NON_SNAPSHOTS_PER_SECOND_PER_PROBE)
304+
(probe.captureSnapshot
305+
? currentProbeBudgetConfiguration.maxSnapshotsPerSecondPerProbe
306+
: currentProbeBudgetConfiguration.maxNonSnapshotsPerSecondPerProbe)
270307
;(probe as InitializedProbe).msBetweenSampling = (1 / snapshotsPerSecond) * 1000 // Convert to milliseconds
271308
;(probe as InitializedProbe).lastCaptureMs = -Infinity // Initialize to -Infinity to allow first call
272309
}
310+
311+
function normalizeProbeBudgetRate(rate: number | undefined, defaultRate: number): number {
312+
return typeof rate === 'number' && Number.isFinite(rate) && rate > 0 ? rate : defaultRate
313+
}

packages/debugger/src/entries/main.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,30 @@ export interface DebuggerInitConfiguration {
6767
* @defaultValue 60000
6868
*/
6969
pollInterval?: number
70+
71+
/**
72+
* Maximum number of snapshot events allowed globally per second
73+
*
74+
* @category Data Collection
75+
* @defaultValue 25
76+
*/
77+
maxSnapshotsPerSecondGlobally?: number
78+
79+
/**
80+
* Default maximum number of snapshot events allowed per probe per second
81+
*
82+
* @category Data Collection
83+
* @defaultValue 1
84+
*/
85+
maxSnapshotsPerSecondPerProbe?: number
86+
87+
/**
88+
* Default maximum number of non-snapshot events allowed per probe per second
89+
*
90+
* @category Data Collection
91+
* @defaultValue 5000
92+
*/
93+
maxNonSnapshotsPerSecondPerProbe?: number
7094
}
7195

7296
/**

0 commit comments

Comments
 (0)