snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
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
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
This commit is contained in:
645
apps/examples/e2e/fixtures/perf-utils.ts
Normal file
645
apps/examples/e2e/fixtures/perf-utils.ts
Normal file
@@ -0,0 +1,645 @@
|
||||
import { Page, PlaywrightTestArgs } from '@playwright/test'
|
||||
import { Editor } from 'tldraw'
|
||||
import { BaselineManager, ComparisonResult, Environment } from './baseline-manager'
|
||||
import { E2EFpsTracker, FPSMetrics } from './fps-tracker'
|
||||
import { HeavyBoardGenerator } from './heavy-board-generator'
|
||||
|
||||
export interface PerformanceTestResult {
|
||||
interaction: string
|
||||
metrics: FPSMetrics
|
||||
comparison: ComparisonResult
|
||||
environment: Environment
|
||||
}
|
||||
|
||||
export interface InteractionTestOptions {
|
||||
warmupMs?: number
|
||||
measureMs?: number
|
||||
expectMinFps?: number
|
||||
}
|
||||
|
||||
export class PerformanceTestSuite {
|
||||
private fpsTracker: E2EFpsTracker
|
||||
private baselineManager: BaselineManager
|
||||
private boardGenerator: HeavyBoardGenerator
|
||||
private environment: Environment
|
||||
|
||||
constructor(page: Page, browserName: string) {
|
||||
this.fpsTracker = new E2EFpsTracker(page)
|
||||
this.baselineManager = new BaselineManager()
|
||||
this.boardGenerator = new HeavyBoardGenerator(page)
|
||||
|
||||
// Detect environment
|
||||
this.environment = {
|
||||
platform: process.platform,
|
||||
viewport: '1920x1080', // Will be updated based on actual viewport
|
||||
browser: browserName,
|
||||
}
|
||||
}
|
||||
|
||||
async initialize(page: Page): Promise<void> {
|
||||
// Update viewport information
|
||||
const viewport = page.viewportSize()
|
||||
if (viewport) {
|
||||
this.environment.viewport = `${viewport.width}x${viewport.height}`
|
||||
}
|
||||
|
||||
// Set up the page for performance testing
|
||||
await page.goto('http://localhost:5420/end-to-end')
|
||||
await page.waitForSelector('.tl-canvas')
|
||||
|
||||
// Disable animations for consistent testing
|
||||
await page.evaluate(() => {
|
||||
const editor: Editor = (window as any).editor
|
||||
if (editor) {
|
||||
editor.user.updateUserPreferences({ animationSpeed: 0 })
|
||||
}
|
||||
})
|
||||
|
||||
// Let the editor settle
|
||||
await page.waitForTimeout(500)
|
||||
}
|
||||
|
||||
async setupHeavyBoard(shapeCount = 200): Promise<void> {
|
||||
await this.boardGenerator.setupStressTestBoard(shapeCount)
|
||||
|
||||
// Ensure the board is fully rendered
|
||||
await this.waitForStableFramerate()
|
||||
}
|
||||
|
||||
private async waitForStableFramerate(maxWaitMs = 3000): Promise<void> {
|
||||
// Wait for the framerate to stabilize after board generation
|
||||
const startTime = Date.now()
|
||||
while (Date.now() - startTime < maxWaitMs) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
// Check if we're getting consistent frame times
|
||||
const isStable = await this.checkFramerateStability()
|
||||
if (isStable) break
|
||||
}
|
||||
}
|
||||
|
||||
private async checkFramerateStability(): Promise<boolean> {
|
||||
// Simple heuristic: check if the page has settled
|
||||
return true // For now, just return true. Could be enhanced with actual stability detection
|
||||
}
|
||||
|
||||
async testShapeRotation(options: InteractionTestOptions = {}): Promise<PerformanceTestResult> {
|
||||
const { warmupMs = 500, measureMs = 10000, expectMinFps } = options // 10 seconds for lots of rotation
|
||||
|
||||
const { metrics } = await this.fpsTracker.measureInteraction(
|
||||
async () => {
|
||||
const page = this.getPage()
|
||||
|
||||
// Select all shapes for rotation
|
||||
await page.keyboard.press('Control+a')
|
||||
|
||||
// Wait a moment for selection
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// Perform continuous rotations using the editor API
|
||||
await page.evaluate(() => {
|
||||
const editor: Editor = (window as any).editor
|
||||
if (!editor) return
|
||||
|
||||
// Rotate shapes continuously during measurement period
|
||||
// 10 seconds at 60fps with 5° steps = ~8.7 rotations total!
|
||||
let totalRotations = 0
|
||||
const rotateStep = Math.PI / 36 // 5 degrees per step
|
||||
const degreesPerStep = 5
|
||||
|
||||
const rotateInterval = setInterval(() => {
|
||||
const selectedShapes = editor.getSelectedShapes()
|
||||
if (selectedShapes.length > 0) {
|
||||
editor.rotateShapesBy(selectedShapes, rotateStep)
|
||||
totalRotations += degreesPerStep / 360 // Track full rotations
|
||||
;(window as any).__totalRotations = totalRotations
|
||||
}
|
||||
}, 16) // ~60fps rotation updates
|
||||
|
||||
// Store interval ID and stats so we can clean it up
|
||||
;(window as any).__rotationInterval = rotateInterval
|
||||
})
|
||||
},
|
||||
{ warmupMs, measureMs }
|
||||
)
|
||||
|
||||
// Clean up and clear selection
|
||||
const page = this.getPage()
|
||||
|
||||
// Stop the rotation interval and get stats
|
||||
const rotationStats = await page.evaluate(() => {
|
||||
const interval = (window as any).__rotationInterval
|
||||
const totalRotations = (window as any).__totalRotations || 0
|
||||
|
||||
if (interval) {
|
||||
clearInterval(interval)
|
||||
delete (window as any).__rotationInterval
|
||||
delete (window as any).__totalRotations
|
||||
}
|
||||
|
||||
return { totalRotations: Math.round(totalRotations * 10) / 10 } // Round to 1 decimal
|
||||
})
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`🔄 Completed ${rotationStats.totalRotations} rotations during measurement`)
|
||||
|
||||
// Clear selection
|
||||
await page.keyboard.press('Escape')
|
||||
|
||||
return this.finalizeTestResult('rotate_shapes', metrics, expectMinFps)
|
||||
}
|
||||
|
||||
async testShapeDragging(options: InteractionTestOptions = {}): Promise<PerformanceTestResult> {
|
||||
const { warmupMs = 500, measureMs = 3000, expectMinFps } = options
|
||||
|
||||
// Select shapes for dragging
|
||||
await this.boardGenerator.selectRandomShapes(5)
|
||||
|
||||
const { metrics } = await this.fpsTracker.measureInteraction(
|
||||
async () => {
|
||||
const page = this.getPage()
|
||||
|
||||
// Select all shapes for rotation
|
||||
await page.keyboard.press('Control+a')
|
||||
|
||||
// Drag shapes around the canvas
|
||||
const startX = 400
|
||||
const startY = 400
|
||||
|
||||
await page.mouse.move(startX, startY)
|
||||
await page.mouse.down()
|
||||
|
||||
// Drag in various directions
|
||||
const movements = [
|
||||
[600, 300],
|
||||
[800, 500],
|
||||
[500, 700],
|
||||
[300, 600],
|
||||
[400, 400],
|
||||
[500, 500],
|
||||
[700, 700],
|
||||
[900, 900],
|
||||
[1100, 1100],
|
||||
[1300, 1300],
|
||||
[200, 200],
|
||||
[324, 324],
|
||||
[448, 448],
|
||||
[572, 572],
|
||||
[696, 696],
|
||||
[820, 820],
|
||||
]
|
||||
|
||||
for (const [x, y] of movements) {
|
||||
await page.mouse.move(x, y, { steps: 10 })
|
||||
await page.waitForTimeout(16) // ~60fps
|
||||
}
|
||||
|
||||
await page.mouse.up()
|
||||
},
|
||||
{ warmupMs, measureMs }
|
||||
)
|
||||
|
||||
// Clear selection
|
||||
const page = this.getPage()
|
||||
await page.keyboard.press('Escape')
|
||||
|
||||
return this.finalizeTestResult('drag_shapes', metrics, expectMinFps)
|
||||
}
|
||||
|
||||
async testShapeResizing(options: InteractionTestOptions = {}): Promise<PerformanceTestResult> {
|
||||
const { warmupMs = 500, measureMs = 3000, expectMinFps } = options
|
||||
|
||||
const { metrics } = await this.fpsTracker.measureInteraction(
|
||||
async () => {
|
||||
const page = this.getPage()
|
||||
|
||||
// Select all shapes for resizing
|
||||
await page.keyboard.press('Control+a')
|
||||
|
||||
// Use editor API to programmatically resize selected shapes
|
||||
await page.evaluate(() => {
|
||||
const editor: Editor = (window as any).editor
|
||||
if (!editor) return
|
||||
|
||||
// Simple seeded random number generator for consistent testing
|
||||
let seed = 12345
|
||||
function seededRandom() {
|
||||
seed = (seed * 9301 + 49297) % 233280
|
||||
return seed / 233280
|
||||
}
|
||||
|
||||
let resizeCycle = 0
|
||||
const resizeInterval = setInterval(() => {
|
||||
const selectedShapes = editor.getSelectedShapes()
|
||||
if (selectedShapes.length === 0) return
|
||||
|
||||
// Generate seeded random scale factors
|
||||
let scaleFactor: number
|
||||
if (resizeCycle % 2 === 0) {
|
||||
// Scale up: random between 1.1 and 5
|
||||
scaleFactor = 1.1 + seededRandom() * (2.0 - 1.1)
|
||||
} else {
|
||||
// Scale down: random between 0.2 and 0.9
|
||||
scaleFactor = 0.5 + seededRandom() * (0.9 - 0.5)
|
||||
}
|
||||
|
||||
// Get current bounds for the scale origin
|
||||
const bounds = editor.getSelectionPageBounds()
|
||||
if (bounds) {
|
||||
// Resize each selected shape individually
|
||||
selectedShapes.forEach((shape: any) => {
|
||||
editor.resizeShape(
|
||||
shape.id,
|
||||
{ x: scaleFactor, y: scaleFactor },
|
||||
{
|
||||
scaleOrigin: bounds.center,
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
resizeCycle++
|
||||
}, 16) // Resize every 16ms for ~60fps
|
||||
|
||||
;(window as any).__resizeInterval = resizeInterval
|
||||
})
|
||||
},
|
||||
{ warmupMs, measureMs }
|
||||
)
|
||||
|
||||
// Stop the resize interval
|
||||
const page = this.getPage()
|
||||
await page.evaluate(() => {
|
||||
const interval = (window as any).__resizeInterval
|
||||
if (interval) {
|
||||
clearInterval(interval)
|
||||
delete (window as any).__resizeInterval
|
||||
}
|
||||
})
|
||||
|
||||
// Clear selection
|
||||
await page.keyboard.press('Escape')
|
||||
|
||||
return this.finalizeTestResult('resize_shapes', metrics, expectMinFps)
|
||||
}
|
||||
|
||||
async testCanvasPanning(options: InteractionTestOptions = {}): Promise<PerformanceTestResult> {
|
||||
const { warmupMs = 500, measureMs = 3000, expectMinFps } = options
|
||||
|
||||
const { metrics } = await this.fpsTracker.measureInteraction(
|
||||
async () => {
|
||||
const page = this.getPage()
|
||||
|
||||
// Pan around the canvas using Editor camera API
|
||||
await page.evaluate(() => {
|
||||
const editor: Editor = (window as any).editor
|
||||
if (!editor) return
|
||||
|
||||
// Get initial camera position
|
||||
const initialCamera = editor.getCamera()
|
||||
let panCycle = 0
|
||||
|
||||
// Define pan movements as offsets from initial position
|
||||
const panOffsets = [
|
||||
{ x: -1800, y: -200 },
|
||||
{ x: 400, y: -1800 },
|
||||
{ x: 800, y: 1200 },
|
||||
{ x: -900, y: 800 },
|
||||
{ x: 0, y: 0 },
|
||||
{ x: -3600, y: -3600 },
|
||||
{ x: -900, y: -1900 },
|
||||
{ x: -900, y: 1200 },
|
||||
{ x: 300, y: 800 },
|
||||
{ x: -1800, y: -800 },
|
||||
{ x: 400, y: -1800 },
|
||||
{ x: 800, y: 1200 },
|
||||
{ x: -900, y: 800 },
|
||||
{ x: 0, y: 0 },
|
||||
{ x: -3600, y: -3600 },
|
||||
{ x: -900, y: -1900 },
|
||||
{ x: -900, y: 1200 },
|
||||
{ x: 300, y: 800 },
|
||||
]
|
||||
|
||||
const panInterval = setInterval(() => {
|
||||
const offsetIndex = panCycle % panOffsets.length
|
||||
const offset = panOffsets[offsetIndex]
|
||||
|
||||
// Set camera position to initial position plus offset
|
||||
editor.setCamera({
|
||||
x: initialCamera.x + offset.x,
|
||||
y: initialCamera.y + offset.y,
|
||||
z: initialCamera.z,
|
||||
})
|
||||
|
||||
panCycle++
|
||||
}, 16) // Update every 16ms for ~60fps
|
||||
|
||||
;(window as any).__panInterval = panInterval
|
||||
})
|
||||
},
|
||||
{ warmupMs, measureMs }
|
||||
)
|
||||
|
||||
// Stop the pan interval
|
||||
const page = this.getPage()
|
||||
await page.evaluate(() => {
|
||||
const interval = (window as any).__panInterval
|
||||
if (interval) {
|
||||
clearInterval(interval)
|
||||
delete (window as any).__panInterval
|
||||
}
|
||||
})
|
||||
|
||||
return this.finalizeTestResult('canvas_panning', metrics, expectMinFps)
|
||||
}
|
||||
|
||||
async testCanvasZooming(options: InteractionTestOptions = {}): Promise<PerformanceTestResult> {
|
||||
const { warmupMs = 500, measureMs = 10000, expectMinFps } = options // Shorter measure time for zooming
|
||||
|
||||
const { metrics } = await this.fpsTracker.measureInteraction(
|
||||
async () => {
|
||||
const page = this.getPage()
|
||||
|
||||
// Use editor API to zoom in and out continuously
|
||||
await page.evaluate(() => {
|
||||
const editor: Editor = (window as any).editor
|
||||
if (!editor) return
|
||||
|
||||
let zoomCycle = 0
|
||||
const zoomInterval = setInterval(() => {
|
||||
const cycle = Math.floor(zoomCycle / 10) % 2 // Switch every 10 steps
|
||||
if (cycle === 0) {
|
||||
editor.zoomIn() // Zoom in phase
|
||||
} else {
|
||||
editor.zoomOut() // Zoom out phase
|
||||
}
|
||||
zoomCycle++
|
||||
}, 16) // Zoom every 16ms
|
||||
|
||||
;(window as any).__zoomInterval = zoomInterval
|
||||
})
|
||||
},
|
||||
{ warmupMs, measureMs }
|
||||
)
|
||||
|
||||
// Stop the zoom interval
|
||||
const page = this.getPage()
|
||||
await page.evaluate(() => {
|
||||
const interval = (window as any).__zoomInterval
|
||||
if (interval) {
|
||||
clearInterval(interval)
|
||||
delete (window as any).__zoomInterval
|
||||
}
|
||||
})
|
||||
|
||||
return this.finalizeTestResult('canvas_zooming', metrics, expectMinFps)
|
||||
}
|
||||
|
||||
async testTextShapeCreation(
|
||||
options: InteractionTestOptions = {}
|
||||
): Promise<PerformanceTestResult> {
|
||||
const { warmupMs = 500, measureMs = 10000, expectMinFps } = options
|
||||
|
||||
const { metrics } = await this.fpsTracker.measureInteraction(
|
||||
async () => {
|
||||
const page = this.getPage()
|
||||
|
||||
// Repeatedly create text shapes and frames during the measurement
|
||||
// window. Each cycle deletes the previous batch so text layouts
|
||||
// can't be cached, forcing TextManager.measureText /
|
||||
// measureTextSpans to run every time. The samples mix ASCII with
|
||||
// emoji ZWJ sequences, flags, and accents so the grapheme
|
||||
// segmenter path is exercised. See
|
||||
// https://github.com/tldraw/tldraw/issues/9112.
|
||||
await page.evaluate(() => {
|
||||
const editor = (window as any).editor
|
||||
const tldrawApi = (window as any).tldrawApi
|
||||
if (!editor || !tldrawApi) return
|
||||
|
||||
const textSamples = [
|
||||
'Performance text measurement',
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing',
|
||||
'Hello 👨👩👧 family',
|
||||
'Flag 🇫🇷 with accents café résumé',
|
||||
'Multi-line\ntext content\nwith line breaks',
|
||||
'Emoji 👍🏽 skin tone modifier 🎉 party',
|
||||
]
|
||||
|
||||
// Seeded RNG for reproducible layouts.
|
||||
let seed = 9112
|
||||
const next = () => {
|
||||
seed = (seed * 9301 + 49297) % 233280
|
||||
return seed / 233280
|
||||
}
|
||||
const range = (min: number, max: number) => Math.floor(next() * (max - min + 1)) + min
|
||||
const choice = <T>(arr: T[]): T => arr[range(0, arr.length - 1)]
|
||||
const sizes = ['s', 'm', 'l', 'xl']
|
||||
|
||||
let batch = 0
|
||||
const createInterval = setInterval(() => {
|
||||
const prev = editor.getCurrentPageShapes()
|
||||
if (prev.length) {
|
||||
editor.deleteShapes(prev.map((s: any) => s.id))
|
||||
}
|
||||
|
||||
const shapes: any[] = []
|
||||
for (let i = 0; i < 25; i++) {
|
||||
const sample = `${choice(textSamples)} ${i}`
|
||||
shapes.push({
|
||||
id: `shape:text-${batch}-${i}`,
|
||||
type: 'text',
|
||||
x: range(0, 1600),
|
||||
y: range(0, 1000),
|
||||
props: {
|
||||
richText: tldrawApi.toRichText(sample),
|
||||
size: choice(sizes),
|
||||
font: 'draw',
|
||||
},
|
||||
})
|
||||
}
|
||||
// Frames whose names end in emoji/accents exercise the
|
||||
// title measurement path from the original bug report.
|
||||
for (let i = 0; i < 5; i++) {
|
||||
shapes.push({
|
||||
id: `shape:frame-${batch}-${i}`,
|
||||
type: 'frame',
|
||||
x: range(0, 1600),
|
||||
y: range(0, 1000),
|
||||
props: { w: 320, h: 220, name: `Frame ${choice(textSamples)}` },
|
||||
})
|
||||
}
|
||||
|
||||
editor.createShapes(shapes)
|
||||
batch++
|
||||
}, 100)
|
||||
|
||||
;(window as any).__textCreateInterval = createInterval
|
||||
})
|
||||
},
|
||||
{ warmupMs, measureMs }
|
||||
)
|
||||
|
||||
// Stop the creation interval and clean up.
|
||||
const page = this.getPage()
|
||||
await page.evaluate(() => {
|
||||
const interval = (window as any).__textCreateInterval
|
||||
if (interval) {
|
||||
clearInterval(interval)
|
||||
delete (window as any).__textCreateInterval
|
||||
}
|
||||
})
|
||||
await page.keyboard.press('Escape')
|
||||
|
||||
return this.finalizeTestResult('create_text_shapes', metrics, expectMinFps)
|
||||
}
|
||||
|
||||
async runFullPerformanceTest(): Promise<PerformanceTestResult[]> {
|
||||
const results: PerformanceTestResult[] = []
|
||||
|
||||
// Run all performance tests
|
||||
await this.setupHeavyBoard()
|
||||
results.push(await this.testShapeRotation())
|
||||
await this.setupHeavyBoard()
|
||||
results.push(await this.testShapeDragging())
|
||||
await this.setupHeavyBoard(50)
|
||||
results.push(await this.testShapeResizing())
|
||||
await this.setupHeavyBoard(1000)
|
||||
results.push(await this.testCanvasPanning())
|
||||
await this.setupHeavyBoard()
|
||||
results.push(await this.testCanvasZooming())
|
||||
results.push(await this.testTextShapeCreation())
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
updateBaseline(interaction: string, metrics: FPSMetrics, force = false): void {
|
||||
this.baselineManager.updateBaseline(interaction, metrics, this.environment, force)
|
||||
}
|
||||
|
||||
getBaseline(interaction: string) {
|
||||
return this.baselineManager.getBaseline(interaction, this.environment)
|
||||
}
|
||||
|
||||
setThresholds(regressionThreshold: number, warningThreshold: number): void {
|
||||
this.baselineManager.setThresholds(regressionThreshold, warningThreshold)
|
||||
}
|
||||
|
||||
// Helper method to control baseline auto-creation
|
||||
async runTestWithBaseline(
|
||||
testFn: () => Promise<{ metrics: FPSMetrics }>,
|
||||
interaction: string,
|
||||
autoCreate = true
|
||||
): Promise<PerformanceTestResult> {
|
||||
const { metrics } = await testFn()
|
||||
const comparison = this.baselineManager.compareWithBaseline(
|
||||
interaction,
|
||||
metrics,
|
||||
this.environment,
|
||||
autoCreate
|
||||
)
|
||||
|
||||
return {
|
||||
interaction,
|
||||
metrics,
|
||||
comparison,
|
||||
environment: this.environment,
|
||||
}
|
||||
}
|
||||
|
||||
// Common method for finalizing performance test results
|
||||
private finalizeTestResult(
|
||||
interaction: string,
|
||||
metrics: FPSMetrics,
|
||||
expectMinFps?: number
|
||||
): PerformanceTestResult {
|
||||
const comparison = this.baselineManager.compareWithBaseline(
|
||||
interaction,
|
||||
metrics,
|
||||
this.environment
|
||||
)
|
||||
|
||||
// Validate minimum FPS if specified
|
||||
if (expectMinFps && metrics.minFps < expectMinFps) {
|
||||
comparison.status = 'fail'
|
||||
comparison.message += ` Minimum FPS ${metrics.minFps} below expected ${expectMinFps}.`
|
||||
}
|
||||
|
||||
return {
|
||||
interaction,
|
||||
metrics,
|
||||
comparison,
|
||||
environment: this.environment,
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to get page reference from fps tracker
|
||||
private getPage(): Page {
|
||||
return (this.fpsTracker as any).page as Page
|
||||
}
|
||||
|
||||
// Method to manually create/update baselines
|
||||
async establishBaselines(): Promise<void> {
|
||||
await this.setupHeavyBoard()
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('🔄 Establishing performance baselines...')
|
||||
|
||||
const interactions = [
|
||||
{ name: 'rotate_shapes', testFn: () => this.testShapeRotation() },
|
||||
{ name: 'drag_shapes', testFn: () => this.testShapeDragging() },
|
||||
{ name: 'resize_shapes', testFn: () => this.testShapeResizing() },
|
||||
{ name: 'canvas_panning', testFn: () => this.testCanvasPanning() },
|
||||
{ name: 'canvas_zooming', testFn: () => this.testCanvasZooming() },
|
||||
{ name: 'create_text_shapes', testFn: () => this.testTextShapeCreation() },
|
||||
]
|
||||
|
||||
for (const { name, testFn } of interactions) {
|
||||
const result = await testFn()
|
||||
this.updateBaseline(name, result.metrics, true)
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`✅ Baseline set for ${name}: ${result.metrics.averageFps} FPS`)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('🎉 All baselines established!')
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to set up page for performance testing
|
||||
export async function setupPerformanceTest(
|
||||
{ page }: PlaywrightTestArgs,
|
||||
browserName: string
|
||||
): Promise<PerformanceTestSuite> {
|
||||
const perfSuite = new PerformanceTestSuite(page, browserName)
|
||||
await perfSuite.initialize(page)
|
||||
return perfSuite
|
||||
}
|
||||
|
||||
// Helper function to format performance results for reporting
|
||||
export function formatPerformanceResults(results: PerformanceTestResult[]): string {
|
||||
let report = '\n=== Performance Test Results ===\n'
|
||||
|
||||
for (const result of results) {
|
||||
const { interaction, metrics, comparison } = result
|
||||
const status = comparison.status.toUpperCase()
|
||||
const statusEmoji =
|
||||
comparison.status === 'pass' ? '✅' : comparison.status === 'warning' ? '⚠️' : '❌'
|
||||
|
||||
report += `\n${statusEmoji} ${interaction.replace('_', ' ').toUpperCase()} [${status}]\n`
|
||||
report += ` Average FPS: ${metrics.averageFps} (${comparison.avgFpsChange > 0 ? '+' : ''}${comparison.avgFpsChange.toFixed(1)}%)\n`
|
||||
report += ` Min FPS: ${metrics.minFps}, Max FPS: ${metrics.maxFps}\n`
|
||||
report += ` Samples: ${metrics.samples.length}, Duration: ${metrics.duration.toFixed(1)}s\n`
|
||||
report += ` ${comparison.message}\n`
|
||||
}
|
||||
|
||||
const failCount = results.filter((r) => r.comparison.status === 'fail').length
|
||||
const warningCount = results.filter((r) => r.comparison.status === 'warning').length
|
||||
const passCount = results.filter((r) => r.comparison.status === 'pass').length
|
||||
|
||||
report += `\n=== Summary ===\n`
|
||||
report += ` ✅ Passed: ${passCount}\n`
|
||||
report += ` ⚠️ Warnings: ${warningCount}\n`
|
||||
report += ` ❌ Failed: ${failCount}\n`
|
||||
|
||||
return report
|
||||
}
|
||||
Reference in New Issue
Block a user