- 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>
252 lines
6.5 KiB
TypeScript
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();
|