Skip to content

Commit

Permalink
⚡️ [RUMF-1113] Notify performance entries by batch (#1255)
Browse files Browse the repository at this point in the history
  • Loading branch information
amortemousque authored Jan 18, 2022
1 parent 0615e30 commit 4d8c293
Show file tree
Hide file tree
Showing 15 changed files with 206 additions and 160 deletions.
13 changes: 13 additions & 0 deletions packages/core/src/tools/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,19 @@ export function find<T, S extends T>(
return undefined
}

export function findLast<T, S extends T>(
array: T[],
predicate: (item: T, index: number, array: T[]) => item is S
): S | undefined {
for (let i = array.length - 1; i >= 0; i -= 1) {
const item = array[i]
if (predicate(item, i, array)) {
return item
}
}
return undefined
}

export function isPercentage(value: unknown) {
return isNumber(value) && value >= 0 && value <= 100
}
Expand Down
16 changes: 9 additions & 7 deletions packages/rum-core/src/boot/startRum.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,12 +222,14 @@ describe('rum events url', () => {
clock.tick(10)
changeLocation('http://foo.com/?bar=qux')

lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, {
entryType: 'longtask',
startTime: relativeNow() - 5,
toJSON: noop,
duration: 5,
} as RumPerformanceEntry)
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [
{
entryType: 'longtask',
startTime: relativeNow() - 5,
toJSON: noop,
duration: 5,
} as RumPerformanceEntry,
])

clock.tick(THROTTLE_VIEW_UPDATE_PERIOD)

Expand Down Expand Up @@ -264,7 +266,7 @@ describe('rum events url', () => {

serverRumEvents.length = 0

lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, FAKE_NAVIGATION_ENTRY)
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [FAKE_NAVIGATION_ENTRY])
clock.tick(THROTTLE_VIEW_UPDATE_PERIOD)

expect(serverRumEvents.length).toEqual(1)
Expand Down
40 changes: 21 additions & 19 deletions packages/rum-core/src/browser/performanceCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,15 @@ export function supportPerformanceEntry() {

export function startPerformanceCollection(lifeCycle: LifeCycle, configuration: RumConfiguration) {
retrieveInitialDocumentResourceTiming((timing) => {
handleRumPerformanceEntry(lifeCycle, configuration, timing)
handleRumPerformanceEntries(lifeCycle, configuration, [timing])
})

if (supportPerformanceObject()) {
handlePerformanceEntries(lifeCycle, configuration, performance.getEntries())
handleRumPerformanceEntries(lifeCycle, configuration, performance.getEntries())
}
if (window.PerformanceObserver) {
const handlePerformanceEntryList = monitor((entries: PerformanceObserverEntryList) =>
handlePerformanceEntries(lifeCycle, configuration, entries.getEntries())
handleRumPerformanceEntries(lifeCycle, configuration, entries.getEntries())
)
const mainEntries = ['resource', 'navigation', 'longtask', 'paint']
const experimentalEntries = ['largest-contentful-paint', 'first-input', 'layout-shift']
Expand All @@ -127,7 +127,8 @@ export function startPerformanceCollection(lifeCycle: LifeCycle, configuration:
observer.observe({ type, buffered: true })
})
} catch (e) {
// Some old browser versions don't support PerformanceObserver without entryTypes option
// Some old browser versions (ex: chrome 67) don't support the PerformanceObserver type and buffered options
// In these cases, fallback to PerformanceObserver with entryTypes
mainEntries.push(...experimentalEntries)
}

Expand All @@ -143,12 +144,12 @@ export function startPerformanceCollection(lifeCycle: LifeCycle, configuration:
}
if (!supportPerformanceTimingEvent('navigation')) {
retrieveNavigationTiming((timing) => {
handleRumPerformanceEntry(lifeCycle, configuration, timing)
handleRumPerformanceEntries(lifeCycle, configuration, [timing])
})
}
if (!supportPerformanceTimingEvent('first-input')) {
retrieveFirstInputTiming((timing) => {
handleRumPerformanceEntry(lifeCycle, configuration, timing)
handleRumPerformanceEntries(lifeCycle, configuration, [timing])
})
}
}
Expand Down Expand Up @@ -283,28 +284,29 @@ function computeRelativePerformanceTiming() {
return result as RelativePerformanceTiming
}

function handlePerformanceEntries(lifeCycle: LifeCycle, configuration: RumConfiguration, entries: PerformanceEntry[]) {
entries.forEach((entry) => {
if (
function handleRumPerformanceEntries(
lifeCycle: LifeCycle,
configuration: RumConfiguration,
entries: Array<PerformanceEntry | RumPerformanceEntry>
) {
const rumPerformanceEntries = entries.filter(
(entry) =>
entry.entryType === 'resource' ||
entry.entryType === 'navigation' ||
entry.entryType === 'paint' ||
entry.entryType === 'longtask' ||
entry.entryType === 'largest-contentful-paint' ||
entry.entryType === 'first-input' ||
entry.entryType === 'layout-shift'
) {
handleRumPerformanceEntry(lifeCycle, configuration, (entry as unknown) as RumPerformanceEntry)
}
})
}
) as RumPerformanceEntry[]

function handleRumPerformanceEntry(lifeCycle: LifeCycle, configuration: RumConfiguration, entry: RumPerformanceEntry) {
if (isIncompleteNavigation(entry) || isForbiddenResource(configuration, entry)) {
return
}
const rumAllowedPerformanceEntries = rumPerformanceEntries.filter(
(entry) => !isIncompleteNavigation(entry) && !isForbiddenResource(configuration, entry)
)

lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, entry)
if (rumAllowedPerformanceEntries.length) {
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, rumAllowedPerformanceEntries)
}
}

function isIncompleteNavigation(entry: RumPerformanceEntry) {
Expand Down
8 changes: 4 additions & 4 deletions packages/rum-core/src/domain/lifeCycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { AutoAction, AutoActionCreatedEvent } from './rumEventsCollection/a
import type { ViewEvent, ViewCreatedEvent, ViewEndedEvent } from './rumEventsCollection/view/trackViews'

export enum LifeCycleEventType {
PERFORMANCE_ENTRY_COLLECTED,
PERFORMANCE_ENTRIES_COLLECTED,
AUTO_ACTION_CREATED,
AUTO_ACTION_COMPLETED,
AUTO_ACTION_DISCARDED,
Expand Down Expand Up @@ -40,7 +40,7 @@ export enum LifeCycleEventType {
export class LifeCycle {
private callbacks: { [key in LifeCycleEventType]?: Array<(data: any) => void> } = {}

notify(eventType: LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, data: RumPerformanceEntry): void
notify(eventType: LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, data: RumPerformanceEntry[]): void
notify(eventType: LifeCycleEventType.REQUEST_STARTED, data: RequestStartEvent): void
notify(eventType: LifeCycleEventType.REQUEST_COMPLETED, data: RequestCompleteEvent): void
notify(eventType: LifeCycleEventType.AUTO_ACTION_COMPLETED, data: AutoAction): void
Expand Down Expand Up @@ -69,8 +69,8 @@ export class LifeCycle {
}

subscribe(
eventType: LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED,
callback: (data: RumPerformanceEntry) => void
eventType: LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED,
callback: (data: RumPerformanceEntry[]) => void
): Subscription
subscribe(eventType: LifeCycleEventType.REQUEST_STARTED, callback: (data: RequestStartEvent) => void): Subscription
subscribe(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,32 +35,31 @@ describe('long task collection', () => {

it('should only listen to long task performance entry', () => {
const { lifeCycle, rawRumEvents } = setupBuilder.build()
;[
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [
LONG_TASK,
{ duration: 100 as Duration, entryType: 'navigation', startTime: 1234 },
{ duration: 100 as Duration, entryType: 'resource', startTime: 1234 },
{ duration: 100 as Duration, entryType: 'paint', startTime: 1234 },
].forEach((entry) => {
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, entry as RumPerformanceEntry)
})
] as RumPerformanceEntry[])

expect(rawRumEvents.length).toBe(1)
})

it('should only collect when session has a replay plan', () => {
const { lifeCycle, rawRumEvents } = setupBuilder.build()

sessionManager.setReplayPlan()
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, LONG_TASK)
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [LONG_TASK])
expect(rawRumEvents.length).toBe(1)

sessionManager.setLitePlan()
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, LONG_TASK)
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [LONG_TASK])
expect(rawRumEvents.length).toBe(1)
})

it('should create raw rum event from performance entry', () => {
const { lifeCycle, rawRumEvents } = setupBuilder.build()
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, LONG_TASK)
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [LONG_TASK])

expect(rawRumEvents[0].startTime).toBe(1234 as RelativeTime)
expect(rawRumEvents[0].rawRumEvent).toEqual({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,29 @@ import { LifeCycleEventType } from '../../lifeCycle'
import type { RumSessionManager } from '../../rumSessionManager'

export function startLongTaskCollection(lifeCycle: LifeCycle, sessionManager: RumSessionManager) {
lifeCycle.subscribe(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, (entry) => {
if (entry.entryType !== 'longtask') {
return
lifeCycle.subscribe(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, (entries) => {
for (const entry of entries) {
if (entry.entryType !== 'longtask') {
break
}
const session = sessionManager.findTrackedSession(entry.startTime)
if (!session || session.hasLitePlan) {
break
}
const startClocks = relativeToClocks(entry.startTime)
const rawRumEvent: RawRumLongTaskEvent = {
date: startClocks.timeStamp,
long_task: {
id: generateUUID(),
duration: toServerDuration(entry.duration),
},
type: RumEventType.LONG_TASK,
}
lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, {
rawRumEvent,
startTime: startClocks.relative,
domainContext: { performanceEntry: entry.toJSON() },
})
}
const session = sessionManager.findTrackedSession(entry.startTime)
if (!session || session.hasLitePlan) {
return
}
const startClocks = relativeToClocks(entry.startTime)
const rawRumEvent: RawRumLongTaskEvent = {
date: startClocks.timeStamp,
long_task: {
id: generateUUID(),
duration: toServerDuration(entry.duration),
},
type: RumEventType.LONG_TASK,
}
lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, {
rawRumEvent,
startTime: startClocks.relative,
domainContext: { performanceEntry: entry.toJSON() },
})
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe('resourceCollection', () => {
name: 'https://resource.com/valid',
startTime: 1234 as RelativeTime,
})
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, performanceEntry)
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [performanceEntry])

expect(rawRumEvents[0].startTime).toBe(1234 as RelativeTime)
expect(rawRumEvents[0].rawRumEvent).toEqual({
Expand Down Expand Up @@ -147,12 +147,11 @@ describe('resourceCollection', () => {
describe('tracing info', () => {
it('should be processed from traced initial document', () => {
const { lifeCycle, rawRumEvents } = setupBuilder.build()
lifeCycle.notify(
LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED,
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [
createResourceEntry({
traceId: '1234',
})
)
}),
])
const traceInfo = (rawRumEvents[0].rawRumEvent as RawRumResourceEvent)._dd!
expect(traceInfo).toBeDefined()
expect(traceInfo.trace_id).toBe('1234')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ export function startResourceCollection(lifeCycle: LifeCycle) {
lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, processRequest(request))
})

lifeCycle.subscribe(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, (entry) => {
if (entry.entryType === 'resource' && !isRequestKind(entry)) {
lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, processResourceEntry(entry))
lifeCycle.subscribe(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, (entries) => {
for (const entry of entries) {
if (entry.entryType === 'resource' && !isRequestKind(entry)) {
lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, processResourceEntry(entry))
}
}
})
}
Expand Down
Loading

0 comments on commit 4d8c293

Please sign in to comment.