/** * Performance Metrics Service * * Tracks app startup time, API response times, and BLE operation durations * to ensure performance targets are met: * - App startup: < 3 seconds * - BLE operations: < 10 seconds */ export interface PerformanceMetric { name: string; duration: number; timestamp: number; success: boolean; metadata?: Record; } export interface PerformanceThresholds { appStartup: number; // Target: 3000ms bleOperation: number; // Target: 10000ms apiCall: number; // Target: 2000ms } export const PERFORMANCE_THRESHOLDS: PerformanceThresholds = { appStartup: 3000, // 3 seconds bleOperation: 10000, // 10 seconds apiCall: 2000, // 2 seconds }; class PerformanceService { private metrics: PerformanceMetric[] = []; private timers = new Map(); private appStartTime: number | null = null; private appReadyTime: number | null = null; /** * Mark app startup begin (should be called as early as possible) */ markAppStart(): void { this.appStartTime = Date.now(); } /** * Mark app ready (should be called when first screen is interactive) * Returns duration in milliseconds */ markAppReady(): number { this.appReadyTime = Date.now(); const duration = this.appStartTime ? this.appReadyTime - this.appStartTime : 0; this.addMetric({ name: 'app_startup', duration, timestamp: this.appReadyTime, success: duration <= PERFORMANCE_THRESHOLDS.appStartup, metadata: { threshold: PERFORMANCE_THRESHOLDS.appStartup, withinTarget: duration <= PERFORMANCE_THRESHOLDS.appStartup, }, }); if (duration > PERFORMANCE_THRESHOLDS.appStartup) { console.warn(`[Performance] App startup exceeded threshold: ${duration}ms (target: ${PERFORMANCE_THRESHOLDS.appStartup}ms)`); } return duration; } /** * Get app startup duration (if app is ready) */ getAppStartupDuration(): number | null { if (!this.appStartTime || !this.appReadyTime) return null; return this.appReadyTime - this.appStartTime; } /** * Start a timer for an operation */ startTimer(name: string): void { this.timers.set(name, Date.now()); } /** * End a timer and record the metric * Returns duration in milliseconds */ endTimer(name: string, success = true, metadata?: Record): number { const startTime = this.timers.get(name); if (!startTime) { console.warn(`[Performance] Timer "${name}" was not started`); return 0; } const endTime = Date.now(); const duration = endTime - startTime; this.timers.delete(name); this.addMetric({ name, duration, timestamp: endTime, success, metadata, }); return duration; } /** * Track a BLE operation */ trackBleOperation( operation: 'scan' | 'connect' | 'command' | 'wifi_config' | 'bulk_wifi', duration: number, success: boolean, metadata?: Record ): void { const withinTarget = duration <= PERFORMANCE_THRESHOLDS.bleOperation; this.addMetric({ name: `ble_${operation}`, duration, timestamp: Date.now(), success, metadata: { ...metadata, threshold: PERFORMANCE_THRESHOLDS.bleOperation, withinTarget, }, }); if (!withinTarget) { console.warn(`[Performance] BLE ${operation} exceeded threshold: ${duration}ms (target: ${PERFORMANCE_THRESHOLDS.bleOperation}ms)`); } } /** * Track an API call */ trackApiCall( endpoint: string, duration: number, success: boolean, metadata?: Record ): void { const withinTarget = duration <= PERFORMANCE_THRESHOLDS.apiCall; this.addMetric({ name: `api_${endpoint}`, duration, timestamp: Date.now(), success, metadata: { ...metadata, threshold: PERFORMANCE_THRESHOLDS.apiCall, withinTarget, }, }); if (!withinTarget && success) { console.warn(`[Performance] API ${endpoint} exceeded threshold: ${duration}ms (target: ${PERFORMANCE_THRESHOLDS.apiCall}ms)`); } } /** * Add a metric to the collection */ private addMetric(metric: PerformanceMetric): void { this.metrics.push(metric); // Keep only last 100 metrics to avoid memory issues if (this.metrics.length > 100) { this.metrics = this.metrics.slice(-100); } } /** * Get all metrics */ getMetrics(): PerformanceMetric[] { return [...this.metrics]; } /** * Get metrics by name pattern */ getMetricsByName(pattern: string | RegExp): PerformanceMetric[] { return this.metrics.filter((m) => typeof pattern === 'string' ? m.name.includes(pattern) : pattern.test(m.name) ); } /** * Get average duration for a metric type */ getAverageDuration(pattern: string | RegExp): number | null { const matching = this.getMetricsByName(pattern); if (matching.length === 0) return null; const total = matching.reduce((sum, m) => sum + m.duration, 0); return total / matching.length; } /** * Get performance summary */ getSummary(): { appStartup: { duration: number | null; withinTarget: boolean }; bleOperations: { count: number; avgDuration: number | null; successRate: number }; apiCalls: { count: number; avgDuration: number | null; successRate: number }; } { const bleMetrics = this.getMetricsByName(/^ble_/); const apiMetrics = this.getMetricsByName(/^api_/); const bleSuccessCount = bleMetrics.filter((m) => m.success).length; const apiSuccessCount = apiMetrics.filter((m) => m.success).length; const appDuration = this.getAppStartupDuration(); return { appStartup: { duration: appDuration, withinTarget: appDuration !== null && appDuration <= PERFORMANCE_THRESHOLDS.appStartup, }, bleOperations: { count: bleMetrics.length, avgDuration: this.getAverageDuration(/^ble_/), successRate: bleMetrics.length > 0 ? bleSuccessCount / bleMetrics.length : 1, }, apiCalls: { count: apiMetrics.length, avgDuration: this.getAverageDuration(/^api_/), successRate: apiMetrics.length > 0 ? apiSuccessCount / apiMetrics.length : 1, }, }; } /** * Clear all metrics */ clear(): void { this.metrics = []; this.timers.clear(); this.appStartTime = null; this.appReadyTime = null; } } // Singleton instance export const performanceService = new PerformanceService();