/** * Performance Optimizer Service * Implements performance optimizations for large datasets and caching strategies */ import { TimeEntry } from '@/lib/types/database'; import { AnalyticsInsight, AggregateAnalysis } from '@/lib/types/analytics'; export interface CacheConfig { ttl: number; // Time to live in milliseconds maxSize: number; // Maximum number of items in cache strategy: 'lru' | 'fifo' | 'lfu'; } export interface PerformanceMetrics { queryTime: number; cacheHitRate: number; memoryUsage: number; recordsProcessed: number; recordsPerSecond: number; } export class PerformanceOptimizer { private cache: Map = new Map(); private cacheConfig: CacheConfig = { ttl: 5 * 60 * 1000, // 5 minutes default maxSize: 1000, strategy: 'lru', }; constructor(config?: Partial) { if (config) { this.cacheConfig = { ...this.cacheConfig, ...config }; } } /** * Get cached data */ getCachedData(key: string): any | null { const item = this.cache.get(key); if (!item) { return null; } // Check if item is expired if (Date.now() - item.timestamp > this.cacheConfig.ttl) { this.cache.delete(key); return null; } // Update access count for LFU strategy item.accessCount++; return item.data; } /** * Set cached data */ setCachedData(key: string, data: any): void { // Remove oldest items if cache is full if (this.cache.size >= this.cacheConfig.maxSize) { this.evictCache(); } this.cache.set(key, { data, timestamp: Date.now(), accessCount: 1, }); } /** * Clear cache */ clearCache(): void { this.cache.clear(); } /** * Get cache statistics */ getCacheStats(): { size: number; maxSize: number; hitRate: number; memoryUsage: number; } { return { size: this.cache.size, maxSize: this.cacheConfig.maxSize, hitRate: 0, // Would need to track hits/misses for real implementation memoryUsage: this.estimateMemoryUsage(), }; } /** * Optimize time entries query with pagination and filtering */ optimizeTimeEntriesQuery( baseQuery: string, filters: Record, pagination: { limit: number; offset: number } ): { query: string; params: any[] } { const conditions: string[] = []; const params: any[] = []; let paramIndex = 1; // Add filter conditions Object.entries(filters).forEach(([key, value]) => { if (value !== undefined && value !== null) { if (Array.isArray(value)) { conditions.push(`${key} = ANY($${paramIndex})`); params.push(value); } else { conditions.push(`${key} = $${paramIndex}`); params.push(value); } paramIndex++; } }); // Build WHERE clause const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; // Add pagination const query = ` ${baseQuery} ${whereClause} ORDER BY entry_date DESC LIMIT $${paramIndex} OFFSET $${paramIndex + 1} `; params.push(pagination.limit, pagination.offset); return { query, params }; } /** * Batch process large datasets */ async batchProcess( items: T[], processor: (batch: T[]) => Promise, batchSize: number = 100, onProgress?: (processed: number, total: number) => void ): Promise { const results: R[] = []; for (let i = 0; i < items.length; i += batchSize) { const batch = items.slice(i, i + batchSize); const batchResults = await processor(batch); results.push(...batchResults); if (onProgress) { onProgress(Math.min(i + batchSize, items.length), items.length); } // Small delay to prevent overwhelming the system await new Promise(resolve => setTimeout(resolve, 10)); } return results; } /** * Optimize analytics calculations for large datasets */ optimizeAnalyticsCalculation(timeEntries: TimeEntry[]): { summary: { totalEntries: number; totalHours: number; averageHoursPerEntry: number; billableEntries: number; approvedEntries: number; }; scores: { activity: number; content: number; timeliness: number; overall: number; }; } { // Use efficient single-pass calculations let totalHours = 0; let billableEntries = 0; let approvedEntries = 0; let activityScoreSum = 0; let contentScoreSum = 0; let timelinessScoreSum = 0; for (const entry of timeEntries) { totalHours += entry.hours_worked; if (entry.billable) billableEntries++; if (entry.approved) approvedEntries++; // Simplified scoring for performance (would use full analytics engine in real implementation) activityScoreSum += this.calculateQuickActivityScore(entry); contentScoreSum += this.calculateQuickContentScore(entry); timelinessScoreSum += this.calculateQuickTimelinessScore(entry); } const count = timeEntries.length; return { summary: { totalEntries: count, totalHours, averageHoursPerEntry: count > 0 ? totalHours / count : 0, billableEntries, approvedEntries, }, scores: { activity: count > 0 ? activityScoreSum / count : 0, content: count > 0 ? contentScoreSum / count : 0, timeliness: count > 0 ? timelinessScoreSum / count : 0, overall: count > 0 ? (activityScoreSum + contentScoreSum + timelinessScoreSum) / (3 * count) : 0, }, }; } /** * Generate performance metrics */ generateMetrics(startTime: number, recordsProcessed: number): PerformanceMetrics { const queryTime = Date.now() - startTime; return { queryTime, cacheHitRate: this.getCacheStats().hitRate, memoryUsage: this.getCacheStats().memoryUsage, recordsProcessed, recordsPerSecond: recordsProcessed > 0 ? (recordsProcessed / queryTime) * 1000 : 0, }; } /** * Optimize insight generation by grouping and batching */ optimizeInsightGeneration( timeEntries: TimeEntry[], existingInsights: AnalyticsInsight[] = [] ): AnalyticsInsight[] { const insights: AnalyticsInsight[] = [...existingInsights]; // Group by resource for efficiency const resourceGroups = new Map(); for (const entry of timeEntries) { if (!resourceGroups.has(entry.resource_id)) { resourceGroups.set(entry.resource_id, []); } resourceGroups.get(entry.resource_id)!.push(entry); } // Generate insights per resource for (const [resourceId, entries] of resourceGroups) { const totalHours = entries.reduce((sum, entry) => sum + entry.hours_worked, 0); const avgScore = entries.reduce((sum, entry) => sum + this.calculateQuickOverallScore(entry), 0) / entries.length; // Add insight if needed if (avgScore < 0.5) { insights.push({ type: 'warning', category: 'activity', title: `Low Performance: Resource ${resourceId}`, description: `Average score: ${(avgScore * 100).toFixed(1)}%`, recommendation: 'Review time entry quality and provide training', severity: 'medium', actionable: true, }); } if (totalHours > 40) { // More than 40 hours in period insights.push({ type: 'info', category: 'performance', title: `High Activity: Resource ${resourceId}`, description: `${Number(totalHours).toFixed(1)} hours logged`, recommendation: 'Monitor workload and resource allocation', severity: 'low', actionable: true, }); } } return insights; } /** * Memory-efficient data streaming for large exports */ async* streamDataForExport( data: T[], chunkSize: number = 1000 ): AsyncGenerator { for (let i = 0; i < data.length; i += chunkSize) { yield data.slice(i, i + chunkSize); // Allow event loop to process other tasks await new Promise(resolve => setTimeout(resolve, 0)); } } /** * Private helper methods */ private evictCache(): void { switch (this.cacheConfig.strategy) { case 'lru': this.evictLRU(); break; case 'fifo': this.evictFIFO(); break; case 'lfu': this.evictLFU(); break; } } private evictLRU(): void { let oldestKey = ''; let oldestTime = Date.now(); for (const [key, item] of this.cache.entries()) { if (item.timestamp < oldestTime) { oldestTime = item.timestamp; oldestKey = key; } } if (oldestKey) { this.cache.delete(oldestKey); } } private evictFIFO(): void { const firstKey = this.cache.keys().next().value; if (firstKey) { this.cache.delete(firstKey); } } private evictLFU(): void { let leastUsedKey = ''; let leastCount = Infinity; for (const [key, item] of this.cache.entries()) { if (item.accessCount < leastCount) { leastCount = item.accessCount; leastUsedKey = key; } } if (leastUsedKey) { this.cache.delete(leastUsedKey); } } private estimateMemoryUsage(): number { // Rough estimation - in real implementation would use more sophisticated tracking let totalSize = 0; for (const [key, item] of this.cache.entries()) { totalSize += key.length * 2; // String size totalSize += JSON.stringify(item.data).length * 2; // Data size totalSize += 16; // Metadata overhead } return totalSize; } private calculateQuickActivityScore(entry: TimeEntry): number { let score = 0; if (entry.title) score += 0.25; if (entry.notes && entry.notes.length > 10) score += 0.25; if (entry.start_date_time && entry.end_date_time) score += 0.25; if (entry.ticket_id || entry.task_id) score += 0.25; return score; } private calculateQuickContentScore(entry: TimeEntry): number { let score = 0; if (entry.title && entry.title.length > 5) score += 0.3; if (entry.notes && entry.notes.length > 20) score += 0.4; if (entry.internal_notes) score += 0.3; return score; } private calculateQuickTimelinessScore(entry: TimeEntry): number { const entryDate = new Date(entry.entry_date); const createdDate = new Date(entry.created_at); const delayDays = (createdDate.getTime() - entryDate.getTime()) / (1000 * 60 * 60 * 24); if (delayDays <= 1) return 1.0; if (delayDays <= 3) return 0.8; if (delayDays <= 7) return 0.6; return 0.4; } private calculateQuickOverallScore(entry: TimeEntry): number { return ( this.calculateQuickActivityScore(entry) * 0.3 + this.calculateQuickContentScore(entry) * 0.4 + this.calculateQuickTimelinessScore(entry) * 0.3 ); } } // Create singleton instance with optimized configuration export const performanceOptimizer = new PerformanceOptimizer({ ttl: 10 * 60 * 1000, // 10 minutes maxSize: 500, strategy: 'lru', });