WellNuo/services/performance.ts
Sergei dd5bc7f95a Add performance optimizations for app startup and BLE operations
- Add 2-second timeout to profile fetch in getStoredUser() to ensure
  app startup < 3 seconds even with slow network. Falls back to cached
  user data on timeout.

- Implement early scan termination in BLEManager when devices found.
  Scan now exits after 3 seconds once minimum devices are detected,
  instead of always waiting full 10 seconds.

- Add PerformanceService for tracking app startup time, API response
  times, and BLE operation durations with threshold checking.

- Integrate performance tracking in app/_layout.tsx to measure and
  log startup duration in dev mode.

- Add comprehensive test suite for performance service and BLE
  scan optimizations.

Performance targets:
- App startup: < 3 seconds
- BLE operations: < 10 seconds

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-01 11:45:10 -08:00

252 lines
6.5 KiB
TypeScript

/**
* 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<string, unknown>;
}
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<string, number>();
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<string, unknown>): 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<string, unknown>
): 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<string, unknown>
): 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();