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
262 lines
7.0 KiB
TypeScript
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,
|
|
}
|
|
}
|
|
}
|