wulf-pulse/lib/services/performance-optimizer.ts
root 6eee14f8af Add comprehensive admin features and multi-system integration
- Add admin dashboard with sync controls and data browser
- Implement RMM, Auvik, and Addigy organization mappings
- Add chunked ticket sync with progress tracking
- Implement entity sync service with rate limiting
- Add analytics engine and performance optimizer
- Create data browser for all PSA entities
- Add navigation components and UI improvements
- Implement background processing and sync services
- Add comprehensive documentation and migration scripts
- Update configuration items with multi-system support
- Enhance contact management and purchase history
- Add issue type assignment and LLM analyzer
- Improve error handling and logging utilities
2025-11-19 14:18:16 -05:00

420 lines
11 KiB
TypeScript

/**
* 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<string, { data: any; timestamp: number; accessCount: number }> = new Map();
private cacheConfig: CacheConfig = {
ttl: 5 * 60 * 1000, // 5 minutes default
maxSize: 1000,
strategy: 'lru',
};
constructor(config?: Partial<CacheConfig>) {
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<string, any>,
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<T, R>(
items: T[],
processor: (batch: T[]) => Promise<R[]>,
batchSize: number = 100,
onProgress?: (processed: number, total: number) => void
): Promise<R[]> {
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<number, TimeEntry[]>();
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<T>(
data: T[],
chunkSize: number = 1000
): AsyncGenerator<T[], void, unknown> {
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',
});