liminal-sessions / src /utils /performanceOptimizer.ts
Severian's picture
Upload 32 files
6a03d7f verified
// Performance Optimization System for Web Audio Visualizer
// Optimized for Hugging Face Spaces free tier deployment
interface PerformanceMetrics {
fps: number
frameTime: number
memoryUsage: number
cpuUsage: number
gpuMemory: number
particleCount: number
lastUpdate: number
}
interface OptimizationSettings {
targetFPS: number
minFPS: number
maxParticles: number
minParticles: number
adaptiveQualityStep: number
memoryThreshold: number
cpuThreshold: number
}
interface CacheEntry {
data: any
timestamp: number
accessCount: number
size: number
}
export class PerformanceOptimizer {
private metrics: PerformanceMetrics
private settings: OptimizationSettings
private frameHistory: number[] = []
private lastFrameTime: number = 0
private adaptiveLevel: number = 1.0 // 1.0 = max quality, 0.1 = min quality
// Caching system
private shaderCache: Map<string, any> = new Map()
private geometryCache: Map<string, any> = new Map()
private materialCache: Map<string, any> = new Map()
private calculationCache: Map<string, CacheEntry> = new Map()
// Object pools for reuse
private bufferPool: Map<string, Float32Array[]> = new Map()
// Performance monitoring
private performanceObserver: PerformanceObserver | null = null
private memoryInfo: any = null
constructor(settings: Partial<OptimizationSettings> = {}) {
this.settings = {
targetFPS: 60,
minFPS: 30,
maxParticles: 15000,
minParticles: 3000,
adaptiveQualityStep: 0.05,
memoryThreshold: 100 * 1024 * 1024, // 100MB
cpuThreshold: 80, // 80% CPU usage
...settings
}
this.metrics = {
fps: 60,
frameTime: 16.67,
memoryUsage: 0,
cpuUsage: 0,
gpuMemory: 0,
particleCount: this.settings.maxParticles,
lastUpdate: performance.now()
}
this.initializePerformanceMonitoring()
}
private initializePerformanceMonitoring() {
// Modern performance monitoring
if ('PerformanceObserver' in window) {
this.performanceObserver = new PerformanceObserver((_list) => {
// Performance entries are processed in updatePerformance method
})
try {
this.performanceObserver.observe({ entryTypes: ['measure'] })
} catch (e) {
console.warn('Performance monitoring not fully supported')
}
}
// Memory monitoring for supported browsers
if ('memory' in performance) {
this.memoryInfo = (performance as any).memory
}
}
// Adaptive quality management
updatePerformance(currentTime: number): void {
const deltaTime = currentTime - this.lastFrameTime
this.lastFrameTime = currentTime
if (deltaTime > 0) {
const currentFPS = 1000 / deltaTime
this.frameHistory.push(currentFPS)
// Keep only last 60 frames for rolling average
if (this.frameHistory.length > 60) {
this.frameHistory.shift()
}
// Calculate average FPS
const avgFPS = this.frameHistory.reduce((a, b) => a + b, 0) / this.frameHistory.length
this.metrics.fps = Math.round(avgFPS)
this.metrics.frameTime = deltaTime
// Update memory usage if available
if (this.memoryInfo) {
this.metrics.memoryUsage = this.memoryInfo.usedJSHeapSize
this.metrics.gpuMemory = this.memoryInfo.totalJSHeapSize
}
// Adaptive quality adjustment
this.adjustQualityLevel()
}
}
private adjustQualityLevel(): void {
const { fps } = this.metrics
const { targetFPS, minFPS, adaptiveQualityStep } = this.settings
if (fps < minFPS) {
// Performance is bad, reduce quality aggressively
this.adaptiveLevel = Math.max(0.1, this.adaptiveLevel - adaptiveQualityStep * 2)
console.warn(`🚨 Performance low (${fps} FPS), reducing quality to ${(this.adaptiveLevel * 100).toFixed(0)}%`)
} else if (fps < targetFPS) {
// Performance is below target, reduce quality gradually
this.adaptiveLevel = Math.max(0.1, this.adaptiveLevel - adaptiveQualityStep)
} else if (fps > targetFPS + 10 && this.adaptiveLevel < 1.0) {
// Performance is good, increase quality gradually
this.adaptiveLevel = Math.min(1.0, this.adaptiveLevel + adaptiveQualityStep * 0.5)
}
// Update particle count based on quality level
const targetParticles = Math.floor(
this.settings.minParticles +
(this.settings.maxParticles - this.settings.minParticles) * this.adaptiveLevel
)
this.metrics.particleCount = targetParticles
}
// Smart caching system
getCachedCalculation(key: string, calculator: () => any, ttl: number = 1000): any {
const cached = this.calculationCache.get(key)
const now = performance.now()
if (cached && (now - cached.timestamp) < ttl) {
cached.accessCount++
return cached.data
}
// Calculate new value
const data = calculator()
this.calculationCache.set(key, {
data,
timestamp: now,
accessCount: 1,
size: this.estimateSize(data)
})
// Clean cache if it gets too large
this.cleanCache()
return data
}
// Shader caching for Three.js materials
getCachedShader(vertexShader: string, fragmentShader: string, uniforms: any): any {
const key = this.hashShader(vertexShader, fragmentShader)
if (this.shaderCache.has(key)) {
const cached = this.shaderCache.get(key)
// Update uniforms on cached shader
Object.assign(cached.uniforms, uniforms)
return cached
}
// Create new shader (this would be done in the component)
return null // Component will create and cache
}
cacheShader(vertexShader: string, fragmentShader: string, material: any): void {
const key = this.hashShader(vertexShader, fragmentShader)
this.shaderCache.set(key, material)
}
// Geometry and buffer pooling
getPooledBuffer(type: string, size: number): Float32Array {
const poolKey = `${type}_${size}`
if (!this.bufferPool.has(poolKey)) {
this.bufferPool.set(poolKey, [])
}
const pool = this.bufferPool.get(poolKey)!
if (pool.length > 0) {
const buffer = pool.pop()!
buffer.fill(0) // Reset buffer
return buffer
}
// Create new buffer if pool is empty
return new Float32Array(size)
}
returnPooledBuffer(type: string, size: number, buffer: Float32Array): void {
const poolKey = `${type}_${size}`
if (!this.bufferPool.has(poolKey)) {
this.bufferPool.set(poolKey, [])
}
const pool = this.bufferPool.get(poolKey)!
// Limit pool size to prevent memory bloat
if (pool.length < 5) {
pool.push(buffer)
}
}
// Level of Detail (LOD) calculations
getLODSettings(distance: number, importance: number = 1.0): {
particleSize: number
updateFrequency: number
detailLevel: number
} {
const distanceFactor = Math.max(0.1, Math.min(1.0, 5.0 / distance))
const qualityFactor = this.adaptiveLevel * importance
return {
particleSize: distanceFactor * qualityFactor,
updateFrequency: Math.max(1, Math.floor(10 / (distanceFactor * qualityFactor))),
detailLevel: distanceFactor * qualityFactor
}
}
// Frame skipping for heavy operations
shouldSkipFrame(_operation: string, frequency: number): boolean {
const frameCount = Math.floor(performance.now() / 16.67) // Approximate frame count
const skipPattern = Math.max(1, Math.floor(frequency / this.adaptiveLevel))
return frameCount % skipPattern !== 0
}
// Memory management
private cleanCache(): void {
const now = performance.now()
const maxCacheAge = 30000 // 30 seconds
const maxCacheSize = 50 * 1024 * 1024 // 50MB
let totalSize = 0
const entries = Array.from(this.calculationCache.entries())
// Calculate total cache size
entries.forEach(([_, entry]) => {
totalSize += entry.size
})
// Clean old or least used entries if cache is too large
if (totalSize > maxCacheSize) {
entries
.sort((a, b) => {
const scoreA = a[1].accessCount / (now - a[1].timestamp)
const scoreB = b[1].accessCount / (now - b[1].timestamp)
return scoreA - scoreB // Lower score = less valuable
})
.slice(0, Math.floor(entries.length * 0.3)) // Remove 30% least valuable
.forEach(([key, _]) => {
this.calculationCache.delete(key)
})
}
// Remove old entries
entries.forEach(([key, entry]) => {
if (now - entry.timestamp > maxCacheAge) {
this.calculationCache.delete(key)
}
})
}
// Utility functions
private hashShader(vertex: string, fragment: string): string {
// Simple hash function for shader caching
let hash = 0
const str = vertex + fragment
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i)
hash = ((hash << 5) - hash) + char
hash = hash & hash // Convert to 32-bit integer
}
return hash.toString()
}
private estimateSize(obj: any): number {
// Rough size estimation for caching
const str = JSON.stringify(obj)
return str.length * 2 // Approximate bytes (UTF-16)
}
// Public getters
getMetrics(): PerformanceMetrics {
return { ...this.metrics }
}
getAdaptiveLevel(): number {
return this.adaptiveLevel
}
getOptimalParticleCount(baseCount: number): number {
return Math.floor(baseCount * this.adaptiveLevel)
}
getOptimalUpdateFrequency(baseFrequency: number): number {
return Math.max(1, Math.floor(baseFrequency / this.adaptiveLevel))
}
// Cleanup
dispose(): void {
if (this.performanceObserver) {
this.performanceObserver.disconnect()
}
this.shaderCache.clear()
this.geometryCache.clear()
this.materialCache.clear()
this.calculationCache.clear()
this.bufferPool.clear()
}
}
// Singleton instance for global use
export const performanceOptimizer = new PerformanceOptimizer({
targetFPS: 60,
minFPS: 25, // Lower threshold for Hugging Face Spaces
maxParticles: 8000, // Reduced for CPU deployment
minParticles: 2000,
adaptiveQualityStep: 0.08,
memoryThreshold: 80 * 1024 * 1024, // 80MB for free tier
cpuThreshold: 75
})