Files
upstream-tldraw/apps/examples/e2e/fixtures/baseline-manager.ts
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

262 lines
7.0 KiB
TypeScript

import fs from 'fs'
import path from 'path'
import { FPSMetrics } from './fps-tracker'
export interface PerformanceBaseline {
avgFps: number
minFps: number
maxFps: number
timestamp: string
environment: {
platform: string
viewport: string
browser: string
}
}
export interface BaselineData {
baselines: {
[platform: string]: {
[interaction: string]: PerformanceBaseline
}
}
metadata: {
lastUpdated: string
version: string
}
}
export interface ComparisonResult {
interaction: string
current: FPSMetrics
baseline: PerformanceBaseline
avgFpsChange: number // Percentage change
minFpsChange: number
isRegression: boolean
isImprovement: boolean
status: 'pass' | 'warning' | 'fail'
message: string
}
export interface Environment {
platform: string
viewport: string
browser: string
}
const __dirname = path.dirname(new URL(import.meta.url).pathname)
export class BaselineManager {
private baselineFilePath: string
private regressionThreshold = 15 // Percentage
private warningThreshold = 10 // Percentage
constructor(baselineDir = path.join(__dirname, '..', 'baselines')) {
// Ensure baseline directory exists
if (!fs.existsSync(baselineDir)) {
fs.mkdirSync(baselineDir, { recursive: true })
}
this.baselineFilePath = path.join(baselineDir, 'fps-baselines.json')
}
private loadBaselines(): BaselineData {
if (!fs.existsSync(this.baselineFilePath)) {
return {
baselines: {},
metadata: {
lastUpdated: new Date().toISOString(),
version: '1.0.0',
},
}
}
try {
const content = fs.readFileSync(this.baselineFilePath, 'utf-8')
return JSON.parse(content)
} catch (error) {
console.warn('Failed to load baseline file, creating new one:', error)
return {
baselines: {},
metadata: {
lastUpdated: new Date().toISOString(),
version: '1.0.0',
},
}
}
}
private saveBaselines(data: BaselineData): void {
try {
const content = JSON.stringify(data, null, 2)
fs.writeFileSync(this.baselineFilePath, content, 'utf-8')
} catch (error) {
throw new Error(`Failed to save baselines: ${error}`)
}
}
private getPlatformKey(env: Environment): string {
return `${env.platform}-${env.viewport}`
}
private calculatePercentageChange(current: number, baseline: number): number {
if (baseline === 0) return 0
return ((current - baseline) / baseline) * 100
}
updateBaseline(
interaction: string,
metrics: FPSMetrics,
environment: Environment,
force = false
): void {
const data = this.loadBaselines()
const platformKey = this.getPlatformKey(environment)
if (!data.baselines[platformKey]) {
data.baselines[platformKey] = {}
}
const existingBaseline = data.baselines[platformKey][interaction]
const newBaseline: PerformanceBaseline = {
avgFps: metrics.averageFps,
minFps: metrics.minFps,
maxFps: metrics.maxFps,
timestamp: new Date().toISOString(),
environment,
}
// Only update if it's an improvement, forced, or no existing baseline
if (!existingBaseline || force || metrics.averageFps > existingBaseline.avgFps) {
data.baselines[platformKey][interaction] = newBaseline
data.metadata.lastUpdated = new Date().toISOString()
this.saveBaselines(data)
// eslint-disable-next-line no-console
console.log(`Updated baseline for ${interaction} on ${platformKey}:`, newBaseline)
}
}
compareWithBaseline(
interaction: string,
metrics: FPSMetrics,
environment: Environment,
autoCreateBaseline = true
): ComparisonResult {
const data = this.loadBaselines()
const platformKey = this.getPlatformKey(environment)
const baseline = data.baselines[platformKey]?.[interaction]
if (!baseline) {
if (autoCreateBaseline) {
// Automatically create baseline on first run
this.updateBaseline(interaction, metrics, environment, true)
return {
interaction,
current: metrics,
baseline: {
avgFps: metrics.averageFps,
minFps: metrics.minFps,
maxFps: metrics.maxFps,
timestamp: new Date().toISOString(),
environment,
},
avgFpsChange: 0,
minFpsChange: 0,
isRegression: false,
isImprovement: false,
status: 'pass',
message: `New baseline created for ${interaction} on ${platformKey}. Average FPS: ${metrics.averageFps}`,
}
} else {
return {
interaction,
current: metrics,
baseline: {
avgFps: 0,
minFps: 0,
maxFps: 0,
timestamp: '',
environment,
},
avgFpsChange: 0,
minFpsChange: 0,
isRegression: false,
isImprovement: false,
status: 'warning',
message: `No baseline found for ${interaction} on ${platformKey}. Consider setting a baseline first.`,
}
}
}
const avgFpsChange = this.calculatePercentageChange(metrics.averageFps, baseline.avgFps)
const minFpsChange = this.calculatePercentageChange(metrics.minFps, baseline.minFps)
const isRegression = avgFpsChange < -this.regressionThreshold
const isImprovement = avgFpsChange > this.warningThreshold
let status: 'pass' | 'warning' | 'fail' = 'pass'
let message = `Performance stable. Average FPS: ${metrics.averageFps} (${avgFpsChange.toFixed(1)}% change)`
if (isRegression) {
status = 'fail'
message = `Performance regression detected! Average FPS dropped by ${Math.abs(avgFpsChange).toFixed(1)}% (${baseline.avgFps} → ${metrics.averageFps})`
} else if (isImprovement) {
status = 'warning' // Warning to indicate a significant change that might warrant baseline update
message = `Performance improvement detected! Average FPS improved by ${avgFpsChange.toFixed(1)}% (${baseline.avgFps} → ${metrics.averageFps}). Consider updating baseline.`
}
return {
interaction,
current: metrics,
baseline,
avgFpsChange,
minFpsChange,
isRegression,
isImprovement,
status,
message,
}
}
getAllBaselines(): BaselineData {
return this.loadBaselines()
}
getBaseline(interaction: string, environment: Environment): PerformanceBaseline | null {
const data = this.loadBaselines()
const platformKey = this.getPlatformKey(environment)
return data.baselines[platformKey]?.[interaction] || null
}
deleteBaseline(interaction: string, environment: Environment): boolean {
const data = this.loadBaselines()
const platformKey = this.getPlatformKey(environment)
if (data.baselines[platformKey]?.[interaction]) {
delete data.baselines[platformKey][interaction]
data.metadata.lastUpdated = new Date().toISOString()
this.saveBaselines(data)
return true
}
return false
}
setThresholds(regressionThreshold: number, warningThreshold: number): void {
if (regressionThreshold <= 0 || warningThreshold <= 0) {
throw new Error('Thresholds must be positive numbers')
}
if (warningThreshold > regressionThreshold) {
throw new Error('Warning threshold must be less than or equal to regression threshold')
}
this.regressionThreshold = regressionThreshold
this.warningThreshold = warningThreshold
}
getThresholds(): { regressionThreshold: number; warningThreshold: number } {
return {
regressionThreshold: this.regressionThreshold,
warningThreshold: this.warningThreshold,
}
}
}