Skip to content

Commit

Permalink
show prettier timing
Browse files Browse the repository at this point in the history
  • Loading branch information
pauldambra committed Nov 14, 2023
1 parent 8a4757d commit 15b08c1
Show file tree
Hide file tree
Showing 4 changed files with 376 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Fragment, useState } from 'react'
import { CodeSnippet, Language } from 'lib/components/CodeSnippet'
import { FlaggedFeature } from 'lib/components/FlaggedFeature'
import { FEATURE_FLAGS } from 'lib/constants'
import { NetworkRequestTiming } from 'scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming'

const friendlyHttpStatus = {
'0': 'Request not sent',
Expand Down Expand Up @@ -179,6 +180,10 @@ export function ItemPerformanceEvent({
return acc
}

if (key.includes('time') || key.includes('end') || key.includes('start')) {
return acc
}

return {
...acc,
[key]: typeof value === 'number' ? Math.round(value) : value,
Expand Down Expand Up @@ -334,7 +339,6 @@ export function ItemPerformanceEvent({
</p>
</>
)}

<LemonDivider dashed />
{['fetch', 'xmlhttprequest'].includes(item.initiator_type || '') ? (
<>
Expand All @@ -346,7 +350,13 @@ export function ItemPerformanceEvent({
{
key: 'timings',
label: 'timings',
content: <SimpleKeyValueList item={sanitizedProps} />,
content: (
<>
<SimpleKeyValueList item={sanitizedProps} />
<LemonDivider dashed />
<NetworkRequestTiming performanceEvent={item} />
</>
),
},
{
key: 'headers',
Expand Down Expand Up @@ -387,10 +397,16 @@ export function ItemPerformanceEvent({
</FlaggedFeature>
<FlaggedFeature flag={FEATURE_FLAGS.NETWORK_PAYLOAD_CAPTURE} match={false}>
<SimpleKeyValueList item={sanitizedProps} />
<LemonDivider dashed />
<NetworkRequestTiming performanceEvent={item} />
</FlaggedFeature>
</>
) : (
<SimpleKeyValueList item={sanitizedProps} />
<>
<SimpleKeyValueList item={sanitizedProps} />
<LemonDivider dashed />
<NetworkRequestTiming performanceEvent={item} />
</>
)}
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { mswDecorator } from '~/mocks/browser'
import { Meta } from '@storybook/react'
import { PerformanceEvent } from '~/types'
import { NetworkRequestTiming } from 'scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming'

const meta: Meta<typeof NetworkRequestTiming> = {
title: 'Components/NetworkRequestTiming',
component: NetworkRequestTiming,
decorators: [
mswDecorator({
get: {},
}),
],
}
export default meta

export function Basic(): JSX.Element {
return (
<NetworkRequestTiming
performanceEvent={
{
connect_end: 9525.599999964237,
connect_start: 9525.599999964237,
decoded_body_size: 18260,
domain_lookup_end: 9525.599999964237,
domain_lookup_start: 9525.599999964237,
duration: 935.5,
encoded_body_size: 18260,
entry_type: 'resource',
fetch_start: 9525.599999964237,
initiator_type: 'fetch',
name: 'http://localhost:8000/api/organizations/@current/plugins/repository/',
next_hop_protocol: 'http/1.1',
redirect_end: 0,
redirect_start: 0,
render_blocking_status: 'non-blocking',
request_start: 9803.099999964237,
response_end: 10461.099999964237,
response_start: 10428.399999976158,
response_status: 200,
secure_connection_start: 0,
start_time: 9525.599999964237,
time_origin: '1699990397357',
timestamp: 1699990406882,
transfer_size: 18560,
window_id: '018bcf51-b1f0-7fe0-ac05-10543621f4f2',
worker_start: 0,
uuid: '12345',
distinct_id: '23456',
session_id: 'abcde',
pageview_id: 'fghij',
current_url: 'http://localhost:8000/insights',
} satisfies PerformanceEvent
}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
import { PerformanceEvent } from '~/types'
import { getSeriesColor } from 'lib/colors'
import { humanFriendlyMilliseconds } from 'lib/utils'
import { Tooltip } from 'lib/lemon-ui/Tooltip'

function colorForEntry(entryType: string | undefined): string {
switch (entryType) {
case 'domComplete':
return getSeriesColor(1)
case 'domInteractive':
return getSeriesColor(2)
case 'pageLoaded':
return getSeriesColor(3)
case 'first-contentful-paint':
return getSeriesColor(4)
case 'css':
return getSeriesColor(6)
case 'xmlhttprequest':
return getSeriesColor(7)
case 'fetch':
return getSeriesColor(8)
case 'other':
return getSeriesColor(9)
case 'script':
return getSeriesColor(10)
case 'link':
return getSeriesColor(11)
case 'first-paint':
return getSeriesColor(11)
default:
return getSeriesColor(13)
}
}

export interface EventPerformanceMeasure {
start: number
end: number
color: string
reducedHeight?: boolean
}

const perfSections = [
'redirect',
'app cache',
'dns lookup',
'connection time',
'tls time',
'waiting for first byte (TTFB)',
'receiving response',
'document processing',
] as const

const perfDescriptions: Record<(typeof perfSections)[number], string> = {
redirect:
'The time it took to fetch any previous resources that redirected to this one. If either redirect_start or redirect_end timestamp is 0, there were no redirects, or one of the redirects wasn’t from the same origin as this resource.',
'app cache': 'The time taken to check the application cache or fetch the resource from the application cache.',
'dns lookup': 'The time taken to complete any DNS lookup for the resource.',
'connection time': 'The time taken to establish a connection to the server to retrieve the resource.',
'tls time': 'The time taken for the SSL/TLS handshake.',
'waiting for first byte (TTFB)': 'The time taken waiting for the server to start returning a response.',
'receiving response': 'The time taken to receive the response from the server.',
'document processing':
'The time taken to process the document after the response from the server has been received.',
}

function colorForSection(section: (typeof perfSections)[number]): string {
switch (section) {
case 'redirect':
return getSeriesColor(1)
case 'app cache':
return getSeriesColor(2)
case 'dns lookup':
return getSeriesColor(3)
case 'connection time':
return getSeriesColor(4)
case 'tls time':
return getSeriesColor(6)
case 'waiting for first byte (TTFB)':
return getSeriesColor(7)
case 'receiving response':
return getSeriesColor(8)
case 'document processing':
return getSeriesColor(9)
default:
return getSeriesColor(10)
}
}

/**
* There are defined sections to performance measurement. We may have data for some or all of them
*
* 1) Redirect
* - from startTime which would also be redirectStart
* - until redirect_end
*
* 2) App Cache
* - from fetch_start
* - until domain_lookup_start
*
* 3) DNS
* - from domain_lookup_start
* - until domain_lookup_end
*
* 4) TCP
* - from connect_start
* - until connect_end
*
* this contains any time to negotiate SSL/TLS
* - from secure_connection_start
* - until connect_end
*
* 5) Request
* - from request_start
* - until response_start
*
* 6) Response
* - from response_start
* - until response_end
*
* 7) Document Processing
* - from response_end
* - until load_event_end
*
* see https://nicj.net/resourcetiming-in-practice/
*
* @param perfEntry
* @param maxTime
*/
function calculatePerformanceParts(perfEntry: PerformanceEvent): Record<string, EventPerformanceMeasure> {
const performanceParts: Record<string, EventPerformanceMeasure> = {}

if (perfEntry.redirect_start && perfEntry.redirect_end) {
performanceParts['redirect'] = {
start: perfEntry.redirect_start,
end: perfEntry.redirect_end,
color: colorForEntry(perfEntry.initiator_type),
}
}

if (perfEntry.fetch_start && perfEntry.domain_lookup_start) {
performanceParts['app cache'] = {
start: perfEntry.fetch_start,
end: perfEntry.domain_lookup_start,
color: colorForEntry(perfEntry.initiator_type),
}
}

if (perfEntry.domain_lookup_end && perfEntry.domain_lookup_start) {
performanceParts['dns lookup'] = {
start: perfEntry.domain_lookup_start,
end: perfEntry.domain_lookup_end,
color: colorForEntry(perfEntry.initiator_type),
}
}

if (perfEntry.connect_end && perfEntry.connect_start) {
performanceParts['connection time'] = {
start: perfEntry.connect_start,
end: perfEntry.connect_end,
color: colorForEntry(perfEntry.initiator_type),
}

if (perfEntry.secure_connection_start) {
performanceParts['tls time'] = {
start: perfEntry.secure_connection_start,
end: perfEntry.connect_end,
color: colorForEntry(perfEntry.initiator_type),
reducedHeight: true,
}
}
}

if (perfEntry.response_start && perfEntry.request_start) {
performanceParts['waiting for first byte (TTFB)'] = {
start: perfEntry.request_start,
end: perfEntry.response_start,
color: colorForEntry(perfEntry.initiator_type),
}
}

if (perfEntry.response_start && perfEntry.response_end) {
performanceParts['receiving response'] = {
start: perfEntry.response_start,
end: perfEntry.response_end,
color: colorForEntry(perfEntry.initiator_type),
}
}

if (perfEntry.response_end && perfEntry.load_event_end) {
performanceParts['document processing'] = {
start: perfEntry.response_end,
end: perfEntry.load_event_end,
color: colorForEntry(perfEntry.initiator_type),
}
}

return performanceParts
}

function percentagesWithinEventRange({
partStart,
partEnd,
rangeEnd,
rangeStart,
}: {
partStart: number
partEnd: number
rangeStart: number
rangeEnd: number
}): { startPercentage: string; widthPercentage: string } {
const totalDuration = rangeEnd - rangeStart
const partStartRelativeToTimeline = partStart - rangeStart
const partDuration = partEnd - partStart

const partPercentage = (partDuration / totalDuration) * 100
const partStartPercentage = (partStartRelativeToTimeline / totalDuration) * 100
return { startPercentage: `${partStartPercentage}%`, widthPercentage: `${partPercentage}%` }
}

export const NetworkRequestTiming = ({
performanceEvent,
}: {
performanceEvent: PerformanceEvent
}): JSX.Element | null => {
const rangeStart = performanceEvent.start_time
const rangeEnd = performanceEvent.response_end
if (typeof rangeStart === 'number' && typeof rangeEnd === 'number') {
const performanceParts = calculatePerformanceParts(performanceEvent)
return (
<div className={'font-semibold text-xs'}>
{perfSections.map((section) => {
const matchedSection = performanceParts[section]
const start = matchedSection?.start
const end = matchedSection?.end
const partDuration = end - start
let formattedDuration: string | undefined
let startPercentage = null
let widthPercentage = null

if (isNaN(partDuration) || partDuration === 0) {
formattedDuration = ''
} else {
formattedDuration = humanFriendlyMilliseconds(partDuration)
const percentages = percentagesWithinEventRange({
rangeStart,
rangeEnd,
partStart: start,
partEnd: end,
})
startPercentage = percentages.startPercentage
widthPercentage = percentages.widthPercentage
}

return (
<>
<div key={section} className={'flex flex-row px-2 py-1'}>
<div className={'w-2/5'}>
<Tooltip title={perfDescriptions[section]}>{section}</Tooltip>
</div>
<div className={'flex-1 grow relative'}>
<div
className={'relative h-full'}
/* eslint-disable-next-line react/forbid-dom-props */
style={{
backgroundColor: colorForSection(section),
width: widthPercentage ?? '0%',
left: startPercentage ?? '0%',
}}
/>
</div>
<div className={'w-1/6 text-right'}>{formattedDuration || ''}</div>
</div>
</>
)
})}
</div>
)
}
return null
}
Loading

0 comments on commit 15b08c1

Please sign in to comment.