Skip to content

Commit

Permalink
fix(rum-core): capture tbt after all task entries are observed (#803)
Browse files Browse the repository at this point in the history
* fix(rum-core): capture tbt after all task entries are observed

* chore: add test for different dispatch
  • Loading branch information
vigneshshanmugam authored Jun 15, 2020
1 parent 18ee0bf commit 5ec41ba
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 42 deletions.
52 changes: 26 additions & 26 deletions packages/rum-core/src/performance-monitoring/metrics.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,14 @@ import { noop, PERF } from '../common/utils'
import Span from './span'

export const metrics = {
fcp: 0
fcp: 0,
tbt: {
start: Infinity,
duration: 0
}
}

const LONG_TASK_THRESHOLD = 50
/**
* Create Spans for the long task entries
* Spec - https://w3c.github.io/longtasks/
Expand Down Expand Up @@ -109,49 +114,52 @@ export function createFirstInputDelaySpan(fidEntries) {
}
}

export function createTotalBlockingTimeSpan(tbtObject) {
const { start, duration } = tbtObject
const tbtSpan = new Span('Total Blocking Time', LONG_TASK, {
startTime: start
})
tbtSpan.end(start + duration)
return tbtSpan
}

/**
* Calculate Total Blocking Time (TBT) from long tasks
*/
export function calculateTotalBlockingTime(longtaskEntries) {
const threshold = 50
const totalBlockingTime = {
start: Infinity,
duration: 0
}
for (let i = 0; i < longtaskEntries.length; i++) {
const { name, startTime, duration } = longtaskEntries[i]
longtaskEntries.forEach(entry => {
const { name, startTime, duration } = entry
/**
* FCP is picked as the lower bound because there is little risk of user input happening
* before FCP and it will not harm user experience.
*/
if (startTime < metrics.fcp) {
continue
return
}
/**
* Account for long task originated only from the current browsing context
* or from the same origin.
* https://w3c.github.io/longtasks/#performancelongtasktiming
*/
if (name !== 'self' && name.indexOf('same-origin') === -1) {
continue
return
}
/**
* Calcualte the start time of the first long task so we can add it
* as span start
*/
totalBlockingTime.start = Math.min(totalBlockingTime.start, startTime)
metrics.tbt.start = Math.min(metrics.tbt.start, startTime)

const blockingTime = duration - threshold
/**
* Theoretically blocking time would always be greater than 0 as Long tasks are
* tasks that exceeds 50ms, but this would be configurable in the future so
* the > 0 check acts as an extra guard
*/
const blockingTime = duration - LONG_TASK_THRESHOLD
if (blockingTime > 0) {
totalBlockingTime.duration += blockingTime
metrics.tbt.duration += blockingTime
}
}
return totalBlockingTime
})
}

/**
Expand Down Expand Up @@ -219,24 +227,16 @@ export function captureObserverEntries(list, { capturePaint }) {
}

/**
* Capture TBT as Span only for page load navigation
* Capture First Input Delay (FID) as span
*/
const { duration, start } = calculateTotalBlockingTime(longtaskEntries)

if (duration > 0) {
const tbtSpan = new Span('Total Blocking Time', LONG_TASK, {
startTime: start
})
tbtSpan.end(start + duration)
result.spans.push(tbtSpan)
}

const fidEntries = list.getEntriesByType(FIRST_INPUT)
const fidSpan = createFirstInputDelaySpan(fidEntries)
if (fidSpan) {
result.spans.push(fidSpan)
}

calculateTotalBlockingTime(longtaskEntries)

return result
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@

import { Promise } from '../common/polyfills'
import Transaction from './transaction'
import { PerfEntryRecorder, captureObserverEntries } from './metrics'
import {
PerfEntryRecorder,
captureObserverEntries,
metrics,
createTotalBlockingTimeSpan
} from './metrics'
import { extend, getEarliestSpan, getLatestNonXHRSpan } from '../common/utils'
import { captureNavigation } from './capture-navigation'
import {
Expand Down Expand Up @@ -278,6 +283,13 @@ class TransactionService {
if (name === NAME_UNKNOWN && pageLoadTransactionName) {
tr.name = pageLoadTransactionName
}
/**
* Capture the TBT as span after observing for all long task entries
* and once performance observer is disconnected
*/
if (tr.captureTimings && metrics.tbt.duration > 0) {
tr.spans.push(createTotalBlockingTimeSpan(metrics.tbt))
}
}
captureNavigation(tr)

Expand Down
49 changes: 34 additions & 15 deletions packages/rum-core/test/performance-monitoring/metrics.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
captureObserverEntries,
calculateTotalBlockingTime,
createFirstInputDelaySpan,
createTotalBlockingTimeSpan,
PerfEntryRecorder,
metrics
} from '../../src/performance-monitoring/metrics'
Expand All @@ -46,6 +47,7 @@ describe('Metrics', () => {
beforeEach(() => {
list.getEntriesByType.and.returnValue([])
list.getEntriesByName.and.returnValue([])
metrics.tbt = { start: Infinity, duration: 0 }
})

it('should not create long tasks spans if entries are not present', () => {
Expand Down Expand Up @@ -154,40 +156,57 @@ describe('Metrics', () => {

describe('Total Blocking Time', () => {
it('should create total blocking time as span', () => {
list.getEntriesByType.and.callFake(mockObserverEntryTypes)
const { spans } = captureObserverEntries(list, {
capturePaint: true
})
const tbtSpans = spans.filter(
span => span.name === 'Total Blocking Time'
)
expect(tbtSpans[0]).toEqual(
calculateTotalBlockingTime(longtaskEntries)
const tbtSpan = createTotalBlockingTimeSpan(metrics.tbt)
expect(tbtSpan).toEqual(
jasmine.objectContaining({
name: 'Total Blocking Time',
type: LONG_TASK
type: LONG_TASK,
_start: 745.4100000031758,
_end: 995.3399999591056
})
)
})

it('should calculate total blocking time from long tasks', () => {
const tbt = calculateTotalBlockingTime(longtaskEntries)
expect(tbt).toEqual({
calculateTotalBlockingTime(longtaskEntries)
expect(metrics.tbt).toEqual({
start: 745.4100000031758,
duration: 249.92999995592982
})
})

it('should update tbt when longtasks are dispatched at different times', () => {
list.getEntriesByType.and.callFake(mockObserverEntryTypes)
captureObserverEntries(list, {
capturePaint: true
})
expect(metrics.tbt).toEqual({
start: 745.4100000031758,
duration: 249.92999995592982
})

// simulating second longtask entry list
captureObserverEntries(list, {
capturePaint: true
})
expect(metrics.tbt).toEqual({
start: 745.4100000031758,
duration: 499.85999991185963
})
})

it('should calculate total blocking time based on FCP', () => {
metrics.fcp = 1000
const tbt = calculateTotalBlockingTime(longtaskEntries)
expect(tbt).toEqual({
calculateTotalBlockingTime(longtaskEntries)
expect(metrics.tbt).toEqual({
start: 1023.40999995591,
duration: 245.35999997751787
})
})

it('should return tbt as 0 when entries are not self/same-origin', () => {
const tbt = calculateTotalBlockingTime([
calculateTotalBlockingTime([
{
name: 'unknown',
startTime: 10
Expand All @@ -197,7 +216,7 @@ describe('Metrics', () => {
startTime: 20
}
])
expect(tbt).toEqual({
expect(metrics.tbt).toEqual({
start: Infinity,
duration: 0
})
Expand Down

0 comments on commit 5ec41ba

Please sign in to comment.