Skip to content

Commit 0664b41

Browse files
authored
Merge pull request #7 from zendesk/next
feat: expose tti and ttr PerformanceMeasures and add debugging metadata to them
2 parents 95a1c5b + a948ff4 commit 0664b41

6 files changed

+85
-37
lines changed

src/ActionLog.ts

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,22 @@ export class ActionLog<CustomMetadata extends Record<string, unknown>> {
8585
return this.actions
8686
}
8787

88+
/**
89+
* Clear performance marks that were added by this ActionLog instance.
90+
*/
91+
private clearPerformanceMarks(): void {
92+
this.actions.forEach((action) => {
93+
if (!action.entry.name) return
94+
try {
95+
if (action.entry instanceof PerformanceMeasure) {
96+
performance.clearMeasures(action.entry.name)
97+
}
98+
} catch {
99+
// ignore
100+
}
101+
})
102+
}
103+
88104
/**
89105
* Clear parts of the internal state, so it's ready for the next measurement.
90106
*/
@@ -99,6 +115,7 @@ export class ActionLog<CustomMetadata extends Record<string, unknown>> {
99115
this.debouncedTrigger.cancel()
100116
}
101117
this.stopObserving()
118+
this.clearPerformanceMarks()
102119
this.actions = []
103120
this.lastStage = INFORMATIVE_STAGES.INITIAL
104121
this.lastStageUpdatedAt = performance.now()
@@ -627,6 +644,27 @@ export class ActionLog<CustomMetadata extends Record<string, unknown>> {
627644

628645
const { lastRenderAction } = this
629646

647+
const metadataValues = [...this.customMetadataBySource.values()]
648+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
649+
const metadata: CustomMetadata = Object.assign({}, ...metadataValues)
650+
651+
const detail = {
652+
metadata,
653+
timingId: this.id,
654+
isFirstLoad: !this.hasReportedAtLeastOnce,
655+
maximumActiveBeaconsCount:
656+
highestNumberOfActiveBeaconsCountAtAnyGivenTime,
657+
minimumExpectedSimultaneousBeacons:
658+
this.minimumExpectedSimultaneousBeacons,
659+
flushReason:
660+
typeof flushReason === 'symbol'
661+
? flushReason.description ?? 'manual'
662+
: flushReason,
663+
}
664+
665+
let tti: PerformanceMeasure | undefined
666+
let ttr: PerformanceMeasure | undefined
667+
630668
if (timedOut) {
631669
this.addStageChange(
632670
{
@@ -637,41 +675,30 @@ export class ActionLog<CustomMetadata extends Record<string, unknown>> {
637675
)
638676
} else if (lastRenderAction) {
639677
// add a measure so we can use it in Lighthouse runs
640-
performanceMeasure(
678+
tti = performanceMeasure(
641679
`${this.id}/tti`,
642680
firstAction.entry.startMark ?? firstAction.entry,
643681
lastAction.entry.endMark ?? lastAction.entry,
682+
detail,
644683
)
645-
performanceMeasure(
684+
ttr = performanceMeasure(
646685
`${this.id}/ttr`,
647686
firstAction.entry.startMark ?? firstAction.entry,
648687
lastRenderAction.entry.endMark ?? lastRenderAction.entry,
688+
detail,
649689
)
650690
}
651691

652-
const metadataValues = [...this.customMetadataBySource.values()]
653-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
654-
const metadata: CustomMetadata = Object.assign({}, ...metadataValues)
655-
656692
const reportArgs: ReportArguments<CustomMetadata> = {
693+
...detail,
657694
actions: this.actions,
658-
metadata,
659695
loadingStages: this.loadingStages,
660696
finalStages: this.finalStages,
661697
immediateSendReportStages:
662698
this.immediateSendReportStages.length > 0
663699
? [...ERROR_STAGES, ...this.immediateSendReportStages]
664700
: ERROR_STAGES,
665-
timingId: this.id,
666-
isFirstLoad: !this.hasReportedAtLeastOnce,
667-
maximumActiveBeaconsCount:
668-
highestNumberOfActiveBeaconsCountAtAnyGivenTime,
669-
minimumExpectedSimultaneousBeacons:
670-
this.minimumExpectedSimultaneousBeacons,
671-
flushReason:
672-
typeof flushReason === 'symbol'
673-
? flushReason.description ?? 'manual'
674-
: flushReason,
701+
measures: { tti, ttr },
675702
}
676703

677704
if (this.reportFn === noop) {

src/TimingDisplay.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ function getPoints<A extends Action>(
174174
}
175175

176176
function getChartPoints(actions: ActionWithStateMetadata[]) {
177-
const report = generateReport({ actions })
177+
const report = generateReport({ actions, measures: {} })
178178
const sources = Object.keys(report.counts)
179179
const sourceToColor = Object.fromEntries(
180180
sources.map((source, index) => [

src/generateReport.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ describe('generateReport', () => {
170170
flushReason: 'test',
171171
maximumActiveBeaconsCount: 1,
172172
minimumExpectedSimultaneousBeacons: 1,
173+
measures: {},
173174
})
174175

175176
expect(report).toStrictEqual(expectedReport)

src/performanceMark.ts

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,24 @@ const getTimingMarkName = (name: string) => `useTiming: ${name}`
1010
export const performanceMark = (
1111
name: string,
1212
markOptions?: PerformanceMarkOptions,
13+
realMark = false,
1314
): PerformanceMark => {
14-
// We want to use performance.mark, instead of performance.now or Date.now,
15-
// because those named metrics will then show up in the profiler and in Lighthouse audits
16-
// see: https://web.dev/user-timings/
17-
// incidentally, this also makes testing waaay easier, because we don't have to deal with timestamps
18-
19-
// Since old browsers (like >1yr old Firefox/Gecko) unfortunately behaves differently to other browsers,
20-
// in that it doesn't immediately return the instance of PerformanceMark object
21-
// so we sort-of polyfill it cheaply below.
22-
// see: https://bugzilla.mozilla.org/show_bug.cgi?id=1724645
2315
const markName = getTimingMarkName(name)
2416

25-
try {
26-
const mark = performance.mark(markName, markOptions)
27-
if (mark) return mark
28-
} catch {
29-
// do nothing, polyfill below
17+
// default to a "fake performance.mark", to improve UX in the profiler
18+
// (otherwise we have thousands of little marks sprinkled everywhere)
19+
if (realMark) {
20+
// Since old browsers (like >1yr old Firefox/Gecko) unfortunately behaves differently to other browsers,
21+
// in that it doesn't immediately return the instance of PerformanceMark object
22+
// so we sort-of polyfill it cheaply below.
23+
// see: https://bugzilla.mozilla.org/show_bug.cgi?id=1724645
24+
25+
try {
26+
const mark = performance.mark(markName, markOptions)
27+
if (mark) return mark
28+
} catch {
29+
// do nothing, polyfill below
30+
}
3031
}
3132

3233
// polyfill:
@@ -36,25 +37,32 @@ export const performanceMark = (
3637
startTime: markOptions?.startTime ?? performance.now(),
3738
entryType: 'mark',
3839
toJSON: () => ({}),
39-
detail: null,
40+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
41+
detail: markOptions?.detail ?? null,
4042
}
4143
}
4244

4345
export const performanceMeasure = (
4446
name: string,
4547
startMark: PerformanceEntry,
4648
endMark?: PerformanceEntry,
49+
detail?: unknown,
4750
): PerformanceMeasure => {
48-
// same story as above
51+
// We want to use performance.mark, instead of performance.now or Date.now,
52+
// because those named metrics will then show up in the profiler and in Lighthouse audits
53+
// see: https://web.dev/user-timings/
54+
// incidentally, this also makes testing waaay easier, because we don't have to deal with timestamps
55+
4956
const measureName = getTimingMarkName(name)
5057
const end = endMark ? endMark.startTime + endMark.duration : performance.now()
5158

5259
// some old browsers might not like performance.measure / performance.mark
5360
// we don't want to crash due to reporting, so we'll polyfill instead
5461
try {
5562
const measure = performance.measure(measureName, {
56-
start: startMark.startTime,
63+
start: startMark.startTime + startMark.duration,
5764
end,
65+
detail,
5866
})
5967

6068
if (measure) return measure
@@ -68,6 +76,6 @@ export const performanceMeasure = (
6876
startTime: startMark.startTime,
6977
entryType: 'measure',
7078
toJSON: () => ({}),
71-
detail: null,
79+
detail: detail ?? null,
7280
}
7381
}

src/stories/MeasureTiming.stories.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ const RenderImmediately = ({
4949
return <div>Rendering immediately</div>
5050
}
5151

52+
RenderImmediately.displayName = 'RenderImmediately'
53+
5254
const TakesAWhileB = ({
5355
setStage,
5456
}: {
@@ -73,6 +75,8 @@ const TakesAWhileB = ({
7375
return <div>Something else that loads for a while... {progress}%</div>
7476
}
7577

78+
TakesAWhileB.displayName = 'TakesAWhileB'
79+
7680
const TakesAWhile = ({
7781
reportFn,
7882
isActive,
@@ -110,6 +114,8 @@ const TakesAWhile = ({
110114
)
111115
}
112116

117+
TakesAWhile.displayName = 'TakesAWhile'
118+
113119
const VisualizerExample = ({ mounted, ...props }: IArgs) => {
114120
const { content, visualizer } = props
115121

@@ -128,6 +134,8 @@ const VisualizerExample = ({ mounted, ...props }: IArgs) => {
128134
)
129135
}
130136

137+
VisualizerExample.displayName = 'VisualizerExample'
138+
131139
export const MeasureTimingStory: StoryObj<IArgs> = {
132140
render: (props) => <VisualizerExample {...props} />,
133141
args: {
@@ -168,7 +176,7 @@ export const MeasureTimingStory: StoryObj<IArgs> = {
168176
},
169177
}
170178

171-
const Component: React.FunctionComponent<{}> = () => <>'Hello world'</>
179+
const Component: React.FunctionComponent<{}> = () => <>Hello world</>
172180

173181
const meta: Meta<{}> = {
174182
// title: 'Packages/MeasureTiming',

src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ export interface ReportArguments<CustomMetadata extends Record<string, unknown>>
151151
readonly minimumExpectedSimultaneousBeacons?: number
152152
readonly flushReason?: string
153153
readonly metadata?: CustomMetadata
154+
readonly measures: {
155+
tti?: PerformanceMeasure
156+
ttr?: PerformanceMeasure
157+
}
154158
}
155159

156160
export interface DynamicActionLogOptions<

0 commit comments

Comments
 (0)