Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance Tests: Separate page setup from test #53808

Merged
merged 18 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions packages/e2e-test-utils-playwright/src/lighthouse/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@
import type { Page } from '@playwright/test';
import * as lighthouse from 'lighthouse/core/index.cjs';

type LighthouseConstructorProps = {
page: Page;
port: number;
};

export class Lighthouse {
constructor(
public readonly page: Page,
public readonly port: number
) {
page: Page;
port: number;

constructor( { page, port }: LighthouseConstructorProps ) {
this.page = page;
this.port = port;
}
Expand Down
194 changes: 180 additions & 14 deletions packages/e2e-test-utils-playwright/src/metrics/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,55 @@
/**
* External dependencies
*/
import type { Page } from '@playwright/test';
import type { Page, Browser } from '@playwright/test';

type EventType =
| 'click'
| 'focus'
| 'focusin'
| 'keydown'
| 'keypress'
| 'keyup'
| 'mouseout'
| 'mouseover';

interface TraceEvent {
cat: string;
name: string;
dur?: number;
args: {
data?: {
type: EventType;
};
};
}

interface Trace {
traceEvents: TraceEvent[];
}

interface LoadingDurations {
serverResponse: number;
firstPaint: number;
domContentLoaded: number;
loaded: number;
firstContentfulPaint: number;
timeSinceResponseEnd: number;
}

type MetricsConstructorProps = {
page: Page;
};

export class Metrics {
constructor( public readonly page: Page ) {
browser: Browser;
page: Page;
trace: Trace;

constructor( { page }: MetricsConstructorProps ) {
this.page = page;
this.browser = page.context().browser()!;
this.trace = { traceEvents: [] };
}

/**
Expand Down Expand Up @@ -37,11 +81,9 @@ export class Metrics {
* Returns time to first byte (TTFB) using the Navigation Timing API.
*
* @see https://web.dev/ttfb/#measure-ttfb-in-javascript
*
* @return {Promise<number>} TTFB value.
*/
async getTimeToFirstByte() {
return this.page.evaluate< number >( () => {
async getTimeToFirstByte(): Promise< number > {
return await this.page.evaluate< number >( () => {
const { responseStart, startTime } = (
performance.getEntriesByType(
'navigation'
Expand All @@ -56,11 +98,9 @@ export class Metrics {
*
* @see https://w3c.github.io/largest-contentful-paint/
* @see https://web.dev/lcp/#measure-lcp-in-javascript
*
* @return {Promise<number>} LCP value.
*/
async getLargestContentfulPaint() {
return this.page.evaluate< number >(
async getLargestContentfulPaint(): Promise< number > {
return await this.page.evaluate< number >(
() =>
new Promise( ( resolve ) => {
new PerformanceObserver( ( entryList ) => {
Expand All @@ -82,11 +122,9 @@ export class Metrics {
*
* @see https://github.com/WICG/layout-instability
* @see https://web.dev/cls/#measure-layout-shifts-in-javascript
*
* @return {Promise<number>} CLS value.
*/
async getCumulativeLayoutShift() {
return this.page.evaluate< number >(
async getCumulativeLayoutShift(): Promise< number > {
return await this.page.evaluate< number >(
() =>
new Promise( ( resolve ) => {
let CLS = 0;
Expand All @@ -108,4 +146,132 @@ export class Metrics {
} )
);
}

/**
* Returns the loading durations using the Navigation Timing API. All the
* durations exclude the server response time.
*/
async getLoadingDurations(): Promise< LoadingDurations > {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious to know whether playwright automatically infers the return type of evaluate here so that we don't have to explicitly cast it as it might be out-of-sync. 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. Looks like there were more places where we cast unnecessarily. Addressed in 33a78eb

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed a little bit too much, reverted some lines in 71a1d58 😅

return await this.page.evaluate( () => {
const [
{
requestStart,
responseStart,
responseEnd,
domContentLoadedEventEnd,
loadEventEnd,
},
] = performance.getEntriesByType(
'navigation'
) as PerformanceNavigationTiming[];
const paintTimings = performance.getEntriesByType(
'paint'
) as PerformancePaintTiming[];

const firstPaintStartTime = paintTimings.find(
( { name } ) => name === 'first-paint'
)?.startTime as number;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Instead of casting as number, how about we just use !?

Suggested change
)?.startTime as number;
)!.startTime;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in feb23d5


const firstContentfulPaintStartTime = paintTimings.find(
( { name } ) => name === 'first-contentful-paint'
)?.startTime as number;

return {
// Server side metric.
serverResponse: responseStart - requestStart,
// For client side metrics, consider the end of the response (the
// browser receives the HTML) as the start time (0).
firstPaint: firstPaintStartTime - responseEnd,
domContentLoaded: domContentLoadedEventEnd - responseEnd,
loaded: loadEventEnd - responseEnd,
firstContentfulPaint:
firstContentfulPaintStartTime - responseEnd,
timeSinceResponseEnd: performance.now() - responseEnd,
};
} );
}

/**
* Starts Chromium tracing with predefined options for performance testing.
*
* @param options Options to pass to `browser.startTracing()`.
*/
async startTracing( options = {} ): Promise< void > {
return await this.browser.startTracing( this.page, {
screenshots: false,
categories: [ 'devtools.timeline' ],
...options,
} );
}

/**
* Stops Chromium tracing and saves the trace.
*/
async stopTracing(): Promise< void > {
const traceBuffer = await this.browser.stopTracing();
const traceJSON = JSON.parse( traceBuffer.toString() );

this.trace = traceJSON;
}

/**
* Returns the durations of all typing events.
*/
getTypingEventDurations(): number[][] {
return [
this.getEventDurations( 'keydown' ),
this.getEventDurations( 'keypress' ),
this.getEventDurations( 'keyup' ),
];
}

/**
* Returns the durations of all selection events.
*/
getSelectionEventDurations(): number[][] {
return [
this.getEventDurations( 'focus' ),
this.getEventDurations( 'focusin' ),
];
}

/**
* Returns the durations of all click events.
*/
getClickEventDurations(): number[][] {
return [ this.getEventDurations( 'click' ) ];
}

/**
* Returns the durations of all hover events.
*/
getHoverEventDurations(): number[][] {
return [
this.getEventDurations( 'mouseover' ),
this.getEventDurations( 'mouseout' ),
];
}

/**
* Returns the durations of all events of a given type.
*
* @param eventType The type of event to filter.
*/
getEventDurations( eventType: EventType ): number[] {
if ( this.trace.traceEvents.length === 0 ) {
throw new Error(
'No trace events found. Did you forget to call stopTracing()?'
);
}

return this.trace.traceEvents
.filter(
( item: TraceEvent ): boolean =>
item.cat === 'devtools.timeline' &&
item.name === 'EventDispatch' &&
item?.args?.data?.type === eventType &&
!! item.dur
)
.map( ( item ) => ( item.dur ? item.dur / 1000 : 0 ) );
}
}
4 changes: 2 additions & 2 deletions packages/e2e-test-utils-playwright/src/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,10 @@ const test = base.extend<
{ scope: 'worker' },
],
lighthouse: async ( { page, lighthousePort }, use ) => {
await use( new Lighthouse( page, lighthousePort ) );
await use( new Lighthouse( { page, port: lighthousePort } ) );
},
metrics: async ( { page }, use ) => {
await use( new Metrics( page ) );
await use( new Metrics( { page } ) );
},
} );

Expand Down
1 change: 1 addition & 0 deletions test/performance/fixtures/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { PerfUtils } from './perf-utils';
Loading
Loading