Files
upstream-mirror a774ac673a
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
2026-09-23 16:12:52 +05:00

156 lines
3.8 KiB
TypeScript

import { Page } from '@playwright/test'
export interface FPSMetrics {
averageFps: number
minFps: number
maxFps: number
totalFrames: number
duration: number
samples: number[]
}
export class E2EFpsTracker {
private isTracking = false
private startTime = 0
private frameCount = 0
private fpsSamples: number[] = []
private rafId: number | null = null
private sampleInterval = 100 // Sample FPS every 100ms
constructor(private page: Page) {}
async start(): Promise<void> {
if (this.isTracking) {
throw new Error('FPS tracking is already running')
}
this.isTracking = true
this.startTime = 0
this.frameCount = 0
this.fpsSamples = []
this.rafId = null
// Inject FPS tracking script into the page
await this.page.evaluate(
({ sampleInterval }) => {
;(window as any).__e2eFpsTracker = {
isTracking: true,
startTime: performance.now(),
frameCount: 0,
fpsSamples: [],
lastSampleTime: performance.now(),
sampleInterval,
trackFrame: function () {
if (!this.isTracking) return
this.frameCount++
const now = performance.now()
// Sample FPS at regular intervals
if (now - this.lastSampleTime >= this.sampleInterval) {
const duration = (now - this.lastSampleTime) / 1000
const framesInPeriod = this.frameCount - (this.lastFrameCount || 0)
const currentFps = duration > 0 ? framesInPeriod / duration : 0
this.fpsSamples.push(Math.round(currentFps))
this.lastSampleTime = now
this.lastFrameCount = this.frameCount
}
// Continue tracking
this.rafId = requestAnimationFrame(() => this.trackFrame())
},
}
// Start the tracking loop
const tracker = (window as any).__e2eFpsTracker
tracker.rafId = requestAnimationFrame(() => tracker.trackFrame())
},
{ sampleInterval: this.sampleInterval }
)
}
async stop(): Promise<FPSMetrics> {
if (!this.isTracking) {
throw new Error('FPS tracking is not running')
}
// Stop tracking and get final measurements
const metrics = await this.page.evaluate(() => {
const tracker = (window as any).__e2eFpsTracker
if (!tracker) {
throw new Error('FPS tracker not found on page')
}
tracker.isTracking = false
if (tracker.rafId) {
cancelAnimationFrame(tracker.rafId)
}
const totalDuration = (performance.now() - tracker.startTime) / 1000
const samples = tracker.fpsSamples.filter((fps: number) => fps > 0) // Filter out invalid samples
if (samples.length === 0) {
return {
averageFps: 0,
minFps: 0,
maxFps: 0,
totalFrames: tracker.frameCount,
duration: totalDuration,
samples: [],
}
}
return {
averageFps: Math.round(samples.reduce((a: number, b: number) => a + b, 0) / samples.length),
minFps: Math.min(...samples),
maxFps: Math.max(...samples),
totalFrames: tracker.frameCount,
duration: totalDuration,
samples: samples,
}
})
this.isTracking = false
// Clean up the tracker from the page
await this.page.evaluate(() => {
delete (window as any).__e2eFpsTracker
})
return metrics
}
async measureInteraction<T>(
interactionFn: () => Promise<T>,
options: { warmupMs?: number; measureMs?: number } = {}
): Promise<{ result: T; metrics: FPSMetrics }> {
const { warmupMs = 500, measureMs = 3000 } = options
// Warm up period - let the browser settle
if (warmupMs > 0) {
await this.page.waitForTimeout(warmupMs)
}
await this.start()
// Execute the interaction
const result = await interactionFn()
// Measure for the specified duration
await this.page.waitForTimeout(measureMs)
const metrics = await this.stop()
return { result, metrics }
}
async isTrackingActive(): Promise<boolean> {
return this.page.evaluate(() => {
const tracker = (window as any).__e2eFpsTracker
return tracker ? tracker.isTracking : false
})
}
}