Skip to content

Commit b730634

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 8130067 commit b730634

5 files changed

Lines changed: 173 additions & 8 deletions

File tree

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

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

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

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

1626
// Mock DD_RUM global for context
1727
mockRumGetInternalContext = jasmine.createSpy('getInternalContext').and.returnValue({
@@ -550,6 +560,85 @@ describe('api', () => {
550560
// Should only get 25 calls (global limit)
551561
expect(mockBatchAdd).toHaveBeenCalledTimes(25)
552562
})
563+
564+
it('should respect configured global snapshot rate limit', () => {
565+
initTransport({ maxSnapshotsPerSecondGlobally: 2 })
566+
567+
for (let i = 0; i < 3; i++) {
568+
const probe: Probe = {
569+
id: `configured-global-probe-${i}`,
570+
version: 0,
571+
type: 'LOG_PROBE',
572+
where: { typeName: 'TestClass', methodName: `configuredGlobal${i}` },
573+
template: 'Test',
574+
captureSnapshot: true,
575+
capture: {},
576+
sampling: { snapshotsPerSecond: 5000 },
577+
evaluateAt: 'ENTRY',
578+
}
579+
addProbe(probe)
580+
}
581+
582+
for (let i = 0; i < 3; i++) {
583+
const probes = getProbes(`TestClass;configuredGlobal${i}`)!
584+
onEntry(probes, {}, {})
585+
onReturn(probes, null, {}, {}, {})
586+
}
587+
588+
expect(mockBatchAdd).toHaveBeenCalledTimes(2)
589+
})
590+
})
591+
592+
describe('configured per-second budgets', () => {
593+
it('should respect configured default snapshot per-probe rate limit', () => {
594+
initTransport({ maxSnapshotsPerSecondPerProbe: 0.5 })
595+
596+
const probe: Probe = {
597+
id: 'configured-snapshot-rate-probe',
598+
version: 0,
599+
type: 'LOG_PROBE',
600+
where: { typeName: 'TestClass', methodName: 'configuredSnapshotRate' },
601+
template: 'Test',
602+
captureSnapshot: true,
603+
capture: { maxReferenceDepth: 1 },
604+
sampling: {},
605+
evaluateAt: 'ENTRY',
606+
}
607+
addProbe(probe)
608+
609+
const probes = getProbes('TestClass;configuredSnapshotRate')!
610+
onEntry(probes, {}, {})
611+
onReturn(probes, null, {}, {}, {})
612+
onEntry(probes, {}, {})
613+
onReturn(probes, null, {}, {}, {})
614+
615+
expect(mockBatchAdd).toHaveBeenCalledTimes(1)
616+
})
617+
618+
it('should respect configured default non-snapshot per-probe rate limit', () => {
619+
initTransport({ maxNonSnapshotsPerSecondPerProbe: 1 })
620+
621+
const probe: Probe = {
622+
id: 'configured-non-snapshot-rate-probe',
623+
version: 0,
624+
type: 'LOG_PROBE',
625+
where: { typeName: 'TestClass', methodName: 'configuredNonSnapshotRate' },
626+
template: 'Test',
627+
captureSnapshot: false,
628+
capture: {},
629+
sampling: {},
630+
evaluateAt: 'ENTRY',
631+
}
632+
addProbe(probe)
633+
634+
const probes = getProbes('TestClass;configuredNonSnapshotRate')!
635+
onEntry(probes, {}, {})
636+
onReturn(probes, null, {}, {}, {})
637+
onEntry(probes, {}, {})
638+
onReturn(probes, null, {}, {}, {})
639+
640+
expect(mockBatchAdd).toHaveBeenCalledTimes(1)
641+
})
553642
})
554643

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

packages/debugger/src/domain/api.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ 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 { checkGlobalSnapshotBudget, resetProbeBudgetConfiguration, setProbeBudgetConfiguration } from './probes'
88
import type { ActiveEntry } from './activeEntries'
99
import { active } from './activeEntries'
1010
import { captureStackTrace, parseStackTrace } from './stacktrace'
@@ -32,12 +32,14 @@ let debuggerConfig: DebuggerInitConfiguration | undefined
3232
export function initDebuggerTransport(config: DebuggerInitConfiguration, batch: Batch): void {
3333
debuggerConfig = config
3434
debuggerBatch = batch
35+
setProbeBudgetConfiguration(config)
3536
}
3637

3738
export function resetDebuggerTransport(): void {
3839
debuggerBatch = undefined
3940
debuggerConfig = undefined
4041
active.clear()
42+
resetProbeBudgetConfiguration()
4143
}
4244

4345
/**

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
@@ -60,6 +60,30 @@ export interface DebuggerInitConfiguration {
6060
* @defaultValue 60000
6161
*/
6262
pollInterval?: number
63+
64+
/**
65+
* Maximum number of snapshot events allowed globally per second
66+
*
67+
* @category Data Collection
68+
* @defaultValue 25
69+
*/
70+
maxSnapshotsPerSecondGlobally?: number
71+
72+
/**
73+
* Default maximum number of snapshot events allowed per probe per second
74+
*
75+
* @category Data Collection
76+
* @defaultValue 1
77+
*/
78+
maxSnapshotsPerSecondPerProbe?: number
79+
80+
/**
81+
* Default maximum number of non-snapshot events allowed per probe per second
82+
*
83+
* @category Data Collection
84+
* @defaultValue 5000
85+
*/
86+
maxNonSnapshotsPerSecondPerProbe?: number
6387
}
6488

6589
/**

0 commit comments

Comments
 (0)