Skip to content

Commit c0b41ae

Browse files
committed
feat(profiling): tag profiles with operation's start vital's id
1 parent 79a4d8e commit c0b41ae

9 files changed

Lines changed: 276 additions & 26 deletions

File tree

packages/core/src/tools/valueHistory.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface ValueHistory<Value> {
2626

2727
closeActive: (endTime: RelativeTime) => void
2828
findAll: (startTime?: RelativeTime, duration?: Duration) => Value[]
29+
findAllEntries: (startTime?: RelativeTime, duration?: Duration) => Array<ValueHistoryEntry<Value>>
2930
getEntries: (startTime: RelativeTime) => Array<ValueHistoryEntry<Value>>
3031
reset: () => void
3132
stop: () => void
@@ -130,6 +131,19 @@ export function createValueHistory<Value>({
130131
.map((entry) => entry.value)
131132
}
132133

134+
/**
135+
* Return all entries with an active period overlapping with the duration,
136+
* or all entries that were active during `startTime` if no duration is provided,
137+
* or all currently active entries if no `startTime` is provided.
138+
*/
139+
function findAllEntries(
140+
startTime: RelativeTime = END_OF_TIMES,
141+
duration = 0 as Duration
142+
): Array<ValueHistoryEntry<Value>> {
143+
const endTime = addDuration(startTime, duration)
144+
return entries.filter((entry) => entry.startTime <= endTime && startTime <= entry.endTime)
145+
}
146+
133147
/**
134148
* Return all the entries whose start time is equal to the given startTime.
135149
*/
@@ -155,7 +169,7 @@ export function createValueHistory<Value>({
155169
}
156170
}
157171

158-
return { add, find, closeActive, findAll, getEntries, reset, stop }
172+
return { add, find, closeActive, findAll, findAllEntries, getEntries, reset, stop }
159173
}
160174

161175
/**

packages/rum-core/src/domain/lifeCycle.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export const enum LifeCycleEventType {
4646
RUM_EVENT_COLLECTED,
4747
RAW_ERROR_COLLECTED,
4848
ACTION_STARTED,
49-
VITAL_STARTED,
49+
DURATION_VITAL_STARTED,
5050
}
5151

5252
// This is a workaround for an issue occurring when the Browser SDK is included in a TypeScript
@@ -79,7 +79,7 @@ declare const LifeCycleEventTypeAsConst: {
7979
RAW_RUM_EVENT_COLLECTED: LifeCycleEventType.RAW_RUM_EVENT_COLLECTED
8080
RUM_EVENT_COLLECTED: LifeCycleEventType.RUM_EVENT_COLLECTED
8181
RAW_ERROR_COLLECTED: LifeCycleEventType.RAW_ERROR_COLLECTED
82-
VITAL_STARTED: LifeCycleEventType.VITAL_STARTED
82+
DURATION_VITAL_STARTED: LifeCycleEventType.DURATION_VITAL_STARTED
8383
}
8484

8585
// Note: this interface needs to be exported even if it is not used outside of this module, else TS
@@ -104,7 +104,7 @@ export interface LifeCycleEventMap {
104104
error: RawError
105105
customerContext?: Context
106106
}
107-
[LifeCycleEventTypeAsConst.VITAL_STARTED]: DurationVitalStart
107+
[LifeCycleEventTypeAsConst.DURATION_VITAL_STARTED]: DurationVitalStart
108108
}
109109

110110
export interface RawRumEventCollectedData<E extends RawRumEvent = RawRumEvent> {

packages/rum-core/src/domain/vital/vitalCollection.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ describe('vitalCollection', () => {
211211

212212
it('should notify lifecycle with vital started event when starting a duration vital', () => {
213213
const subscriberSpy = jasmine.createSpy()
214-
lifeCycle.subscribe(LifeCycleEventType.VITAL_STARTED, subscriberSpy)
214+
lifeCycle.subscribe(LifeCycleEventType.DURATION_VITAL_STARTED, subscriberSpy)
215215

216216
vitalCollection.startDurationVital('foo')
217217

packages/rum-core/src/domain/vital/vitalCollection.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export function startVitalCollection(lifeCycle: LifeCycle, pageStateHistory: Pag
142142
name,
143143
...options,
144144
})
145-
lifeCycle.notify(LifeCycleEventType.VITAL_STARTED, { id, name, startClocks, ...options })
145+
lifeCycle.notify(LifeCycleEventType.DURATION_VITAL_STARTED, { id, name, startClocks, ...options })
146146
}
147147

148148
function stopDurationVital(name: string, options: DurationVitalOptions = {}, stopClocks = clocksNow()) {

packages/rum-core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export type {
2323
RumVitalEventDomainContext,
2424
} from './domainContext.types'
2525
export type { ReplayStats, RawRumActionEvent, RawRumEvent } from './rawRumEvent.types'
26-
export { ActionType, RumEventType, FrustrationType, RumLongTaskEntryType } from './rawRumEvent.types'
26+
export { ActionType, RumEventType, FrustrationType, RumLongTaskEntryType, VitalType } from './rawRumEvent.types'
2727
export { startRum } from './boot/startRum'
2828
export type { RawRumEventCollectedData } from './domain/lifeCycle'
2929
export { LifeCycle, LifeCycleEventType } from './domain/lifeCycle'

packages/rum/src/domain/profiling/datadogProfiler.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,12 @@ export function createRumProfiler(
229229
const duration = elapsed(startClocks.relative, endClocks.relative)
230230
const longTasks = longTaskHistory.findAll(startClocks.relative, duration)
231231
const actions = actionHistory.findAll(startClocks.relative, duration)
232-
const vitals = vitalHistory.findAll(startClocks.relative, duration)
232+
const vitals = vitalHistory.findAll(startClocks.relative, duration).map((vital) => ({
233+
id: vital.id,
234+
label: vital.label,
235+
startClocks: vital.startClocks,
236+
duration: vital.duration,
237+
}))
233238
const isBelowDurationThreshold = duration < profilerConfiguration.minProfileDurationMs
234239

235240
if (longTasks.length === 0 && isBelowDurationThreshold) {

packages/rum/src/domain/profiling/profiler.spec.ts

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import type { ViewHistoryEntry } from '@datadog/browser-rum-core'
2-
import { LifeCycle, LifeCycleEventType, RumPerformanceEntryType, createHooks } from '@datadog/browser-rum-core'
2+
import {
3+
LifeCycle,
4+
LifeCycleEventType,
5+
RumPerformanceEntryType,
6+
VitalType,
7+
createHooks,
8+
} from '@datadog/browser-rum-core'
39
import type { Duration, SessionRenewalEvent } from '@datadog/browser-core'
410
import {
511
addDuration,
@@ -8,6 +14,7 @@ import {
814
createIdentityEncoder,
915
createValueHistory,
1016
deepClone,
17+
elapsed,
1118
ONE_DAY,
1219
relativeNow,
1320
timeStampNow,
@@ -141,6 +148,18 @@ describe('profiler', () => {
141148
addVital: (vital: VitalContext) => {
142149
vitalHistory.add(vital, relativeNow()).close(addDuration(relativeNow(), vital.duration ?? (0 as Duration)))
143150
},
151+
startOperationStep: (id: string, label: string, operationKey?: string) => {
152+
const startClocks = clocksNow()
153+
const entry = vitalHistory.add(
154+
{ id, type: VitalType.OPERATION_STEP, label, operationKey, startClocks, duration: undefined },
155+
startClocks.relative
156+
)
157+
return () => {
158+
const endTime = relativeNow()
159+
entry.value.duration = elapsed(entry.startTime, endTime)
160+
entry.close(endTime)
161+
}
162+
},
144163
}
145164
}
146165

@@ -415,6 +434,7 @@ describe('profiler', () => {
415434
expect(profilingContextManager.get()?.status).toBe('running')
416435
addVital({
417436
id: 'vital-id-1',
437+
type: VitalType.DURATION,
418438
label: 'vital-label-1',
419439
startClocks: clocksNow(),
420440
duration: 50 as Duration,
@@ -423,6 +443,7 @@ describe('profiler', () => {
423443

424444
addVital({
425445
id: 'vital-id-2',
446+
type: VitalType.DURATION,
426447
label: 'vital-label-2',
427448
startClocks: clocksNow(),
428449
duration: 100 as Duration,
@@ -442,6 +463,7 @@ describe('profiler', () => {
442463

443464
addVital({
444465
id: 'vital-id-3',
466+
type: VitalType.DURATION,
445467
label: 'vital-label-3',
446468
startClocks: clocksNow(),
447469
duration: 100 as Duration,
@@ -495,6 +517,79 @@ describe('profiler', () => {
495517
])
496518
})
497519

520+
it('should collect all ongoing operations during a profiling session', async () => {
521+
const clock = mockClock()
522+
const { profiler, startOperationStep } = setupProfiler()
523+
524+
// Profile 1: start all three operations, end op1
525+
profiler.start()
526+
await waitForBoolean(() => profiler.isRunning())
527+
528+
const endOp1 = startOperationStep('op-id-1', 'op-label-1')
529+
clock.tick(10)
530+
const endOp2 = startOperationStep('op-id-2', 'op-label-2')
531+
clock.tick(10)
532+
const endOp3 = startOperationStep('op-id-3', 'op-label-3')
533+
clock.tick(10)
534+
endOp1() // op1 ends during profile 1
535+
536+
clock.tick(70)
537+
profiler.stop()
538+
await waitNextMicrotask()
539+
540+
// Profile 2: end op2
541+
profiler.start()
542+
await waitForBoolean(() => profiler.isRunning())
543+
544+
clock.tick(50)
545+
endOp2() // op2 ends during profile 2
546+
547+
clock.tick(50)
548+
profiler.stop()
549+
await waitNextMicrotask()
550+
551+
// Profile 3: end op3
552+
profiler.start()
553+
await waitForBoolean(() => profiler.isRunning())
554+
555+
clock.tick(50)
556+
endOp3() // op3 ends during profile 3
557+
558+
clock.tick(50)
559+
profiler.stop()
560+
await waitNextMicrotask()
561+
await waitNextMicrotask()
562+
563+
expect(interceptor.requests.length).toBe(3)
564+
565+
const req1 = await readFormDataRequest<ProfileEventPayload>(interceptor.requests[0])
566+
const req2 = await readFormDataRequest<ProfileEventPayload>(interceptor.requests[1])
567+
const req3 = await readFormDataRequest<ProfileEventPayload>(interceptor.requests[2])
568+
569+
const vitals1 = req1['wall-time.json'].vitals
570+
const vitals2 = req2['wall-time.json'].vitals
571+
const vitals3 = req3['wall-time.json'].vitals
572+
573+
// Profile 1: all three operations present, only op1 has a duration
574+
expect(vitals1?.map((v) => v.id)).toEqual(jasmine.arrayContaining(['op-id-1', 'op-id-2', 'op-id-3']))
575+
expect(vitals1?.find((v) => v.id === 'op-id-1')?.duration).toBe(30 as Duration)
576+
expect(vitals1?.find((v) => v.id === 'op-id-2')?.duration).toBeUndefined()
577+
expect(vitals1?.find((v) => v.id === 'op-id-3')?.duration).toBeUndefined()
578+
579+
// Profile 2: op1 is gone (ended before profile 2 started), op2 and op3 present, only op2 has a duration
580+
expect(vitals2?.map((v) => v.id)).not.toContain('op-id-1')
581+
expect(vitals2?.map((v) => v.id)).toEqual(jasmine.arrayContaining(['op-id-2', 'op-id-3']))
582+
expect(vitals2?.find((v) => v.id === 'op-id-2')?.duration).toBe(140 as Duration)
583+
expect(vitals2?.find((v) => v.id === 'op-id-3')?.duration).toBeUndefined()
584+
585+
// Profile 3: only op3 remains, with a duration
586+
expect(vitals3?.map((v) => v.id)).not.toContain('op-id-1')
587+
expect(vitals3?.map((v) => v.id)).not.toContain('op-id-2')
588+
expect(vitals3?.length).toBe(1)
589+
expect(vitals3?.[0].id).toBe('op-id-3')
590+
expect(vitals3?.[0].duration).toBe(230 as Duration)
591+
})
592+
498593
it('should collect views and set default view name in the Profile', async () => {
499594
const initialViewEntry = {
500595
id: 'view-user',

0 commit comments

Comments
 (0)