Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
ricokahler committed Sep 29, 2024
1 parent 2d72c2e commit 60a1017
Show file tree
Hide file tree
Showing 10 changed files with 453 additions and 309 deletions.
48 changes: 48 additions & 0 deletions perf/efps/helpers/aggregateLatencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {type EfpsResult} from '../types'

function calculatePercentile(numbers: number[], percentile: number): number {
// Sort the array in ascending order
const sorted = numbers.slice().sort((a, b) => a - b)

// Calculate the index
const index = percentile * (sorted.length - 1)

// If the index is an integer, return the value at that index
if (Number.isInteger(index)) {
return sorted[index]
}

// Otherwise, interpolate between the two nearest values
const lowerIndex = Math.floor(index)
const upperIndex = Math.ceil(index)
const lowerValue = sorted[lowerIndex]
const upperValue = sorted[upperIndex]

const fraction = index - lowerIndex
return lowerValue + (upperValue - lowerValue) * fraction
}

function calculateError(numbers: number[]) {
const mean = numbers.reduce((sum, num) => sum + num, 0) / numbers.length

// calculate the sum of squared differences from the mean
const squaredDiffs = numbers.map((num) => Math.pow(num - mean, 2))
const sumSquaredDiffs = squaredDiffs.reduce((sum, diff) => sum + diff, 0)

const variance = sumSquaredDiffs / (numbers.length - 1)
const standardDeviation = Math.sqrt(variance)

// We assume normal distribution and multiply the standard deviations by 1.96
// which aims to represent 95% of the population
return 1.96 * standardDeviation
}

export function aggregateLatencies(values: number[]): EfpsResult['latency'] {
return {
median: calculatePercentile(values, 0.5),
error: calculateError(values),
p75: calculatePercentile(values, 0.75),
p90: calculatePercentile(values, 0.9),
p99: calculatePercentile(values, 0.99),
}
}
21 changes: 0 additions & 21 deletions perf/efps/helpers/calculatePercentile.ts

This file was deleted.

47 changes: 47 additions & 0 deletions perf/efps/helpers/measureBlockingTime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {type Page} from 'playwright'

const BLOCKING_TASK_THRESHOLD = 50

export function measureBlockingTime(page: Page): () => Promise<number> {
const idleGapLengthsPromise = page.evaluate(async () => {
const idleGapLengths: number[] = []
const done = false
let last = performance.now()

const handler = () => {
const current = performance.now()
idleGapLengths.push(current - last)
last = current

if (done) return
requestIdleCallback(handler)
}

requestIdleCallback(handler)

await new Promise((resolve) => {
document.addEventListener('__blockingTimeFinish', resolve, {once: true})
})

return idleGapLengths
})

async function getBlockingTime() {
await page.evaluate(() => {
document.dispatchEvent(new CustomEvent('__blockingTimeFinish'))
})

const idleGapLengths = await idleGapLengthsPromise

const blockingTime = idleGapLengths
// only consider the gap lengths that are blocking
.filter((idleGapLength) => idleGapLength > BLOCKING_TASK_THRESHOLD)
// subtract the allowed time so we're only left with blocking time
.map((idleGapLength) => idleGapLength - BLOCKING_TASK_THRESHOLD)
.reduce((sum, next) => sum + next, 0)

return blockingTime
}

return getBlockingTime
}
41 changes: 33 additions & 8 deletions perf/efps/helpers/measureFpsForInput.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
import {type Locator} from 'playwright'
import {type Page} from 'playwright'

import {type EfpsResult} from '../types'
import {calculatePercentile} from './calculatePercentile'
import {aggregateLatencies} from './aggregateLatencies'
import {measureBlockingTime} from './measureBlockingTime'

export async function measureFpsForInput(input: Locator): Promise<EfpsResult> {
interface MeasureFpsForInputOptions {
label?: string
page: Page
fieldName: string
}

export async function measureFpsForInput({
label,
fieldName,
page,
}: MeasureFpsForInputOptions): Promise<EfpsResult> {
const start = Date.now()

const input = page
.locator(
`[data-testid="field-${fieldName}"] input[type="text"], ` +
`[data-testid="field-${fieldName}"] textarea`,
)
.first()
await input.waitFor({state: 'visible'})
const characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'

Expand Down Expand Up @@ -49,6 +68,8 @@ export async function measureFpsForInput(input: Locator): Promise<EfpsResult> {
await input.pressSequentially(startingMarker)
await new Promise((resolve) => setTimeout(resolve, 500))

const getBlockingTime = measureBlockingTime(page)

for (const character of characters) {
inputEvents.push({character, timestamp: Date.now()})
await input.press(character)
Expand All @@ -57,6 +78,9 @@ export async function measureFpsForInput(input: Locator): Promise<EfpsResult> {

await input.blur()

await page.evaluate(() => window.document.dispatchEvent(new CustomEvent('__finish')))

const blockingTime = await getBlockingTime()
const renderEvents = await rendersPromise

await new Promise((resolve) => setTimeout(resolve, 500))
Expand All @@ -74,9 +98,10 @@ export async function measureFpsForInput(input: Locator): Promise<EfpsResult> {
return matchingEvent.timestamp - inputEvent.timestamp
})

const p50 = Math.min(1000 / calculatePercentile(latencies, 0.5), 100)
const p75 = Math.min(1000 / calculatePercentile(latencies, 0.75), 100)
const p90 = Math.min(1000 / calculatePercentile(latencies, 0.9), 100)

return {p50, p75, p90, latencies}
return {
latency: aggregateLatencies(latencies),
blockingTime,
label: label || fieldName,
runDuration: Date.now() - start,
}
}
38 changes: 27 additions & 11 deletions perf/efps/helpers/measureFpsForPte.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
import {type Locator} from 'playwright'
import {type Page} from 'playwright'

import {type EfpsResult} from '../types'
import {calculatePercentile} from './calculatePercentile'
import {aggregateLatencies} from './aggregateLatencies'
import {measureBlockingTime} from './measureBlockingTime'

export async function measureFpsForPte(pteField: Locator): Promise<EfpsResult> {
interface MeasureFpsForPteOptions {
fieldName: string
label?: string
page: Page
}

export async function measureFpsForPte({
fieldName,
page,
label,
}: MeasureFpsForPteOptions): Promise<EfpsResult> {
const start = Date.now()
const pteField = page.locator(`[data-testid="field-${fieldName}"]`)
const characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'

await pteField.waitFor({state: 'visible'})
Expand All @@ -24,14 +37,14 @@ export async function measureFpsForPte(pteField: Locator): Promise<EfpsResult> {
}[] = []

const mutationObserver = new MutationObserver(() => {
const start = performance.now()
const textStart = performance.now()
const textContent = el.textContent || ''
const end = performance.now()
const textEnd = performance.now()

updates.push({
value: textContent,
timestamp: Date.now(),
textContentProcessingTime: end - start,
textContentProcessingTime: textEnd - textStart,
})
})

Expand Down Expand Up @@ -63,6 +76,7 @@ export async function measureFpsForPte(pteField: Locator): Promise<EfpsResult> {
await contentEditable.pressSequentially(startingMarker)
await new Promise((resolve) => setTimeout(resolve, 500))

const getBlockingTime = measureBlockingTime(page)
for (const character of characters) {
inputEvents.push({character, timestamp: Date.now()})
await contentEditable.press(character)
Expand All @@ -71,6 +85,7 @@ export async function measureFpsForPte(pteField: Locator): Promise<EfpsResult> {

await contentEditable.blur()

const blockingTime = await getBlockingTime()
const renderEvents = await rendersPromise

const latencies = inputEvents.map((inputEvent) => {
Expand All @@ -86,9 +101,10 @@ export async function measureFpsForPte(pteField: Locator): Promise<EfpsResult> {
return matchingEvent.timestamp - inputEvent.timestamp - matchingEvent.textContentProcessingTime
})

const p50 = Math.min(1000 / calculatePercentile(latencies, 0.5), 100)
const p75 = Math.min(1000 / calculatePercentile(latencies, 0.75), 100)
const p90 = Math.min(1000 / calculatePercentile(latencies, 0.9), 100)

return {p50, p75, p90, latencies}
return {
latency: aggregateLatencies(latencies),
blockingTime,
label: label || fieldName,
runDuration: Date.now() - start,
}
}
Loading

0 comments on commit 60a1017

Please sign in to comment.