javascript

exercises

exercises.js
/**
 * 19.5 Performance Observer - Exercises
 *
 * Practice implementing performance monitoring patterns
 */

// ============================================
// EXERCISE 1: Performance Dashboard
// ============================================

/**
 * Create a performance dashboard that:
 * - Collects all performance metrics
 * - Provides real-time updates
 * - Generates comprehensive reports
 *
 * Requirements:
 * - Track Web Vitals, resources, and long tasks
 * - Calculate percentiles
 * - Support custom metric tracking
 */

class PerformanceDashboard {
  // Your implementation here
}

/*
// SOLUTION:
class PerformanceDashboard {
    constructor(options = {}) {
        this.options = {
            updateInterval: options.updateInterval || 1000,
            historySize: options.historySize || 100
        };
        
        this.metrics = {
            webVitals: {},
            resources: [],
            longTasks: [],
            customMetrics: [],
            history: []
        };
        
        this.observers = [];
        this.listeners = new Map();
        this.startTime = performance.now();
    }
    
    start() {
        // Web Vitals Observer
        this.setupWebVitalsObserver();
        
        // Resource Observer
        this.setupResourceObserver();
        
        // Long Task Observer
        this.setupLongTaskObserver();
        
        // Periodic updates
        this.updateInterval = setInterval(() => {
            this.takeSnapshot();
            this.emit('update', this.getSnapshot());
        }, this.options.updateInterval);
    }
    
    setupWebVitalsObserver() {
        // LCP
        try {
            const lcpObserver = new PerformanceObserver((list) => {
                const entries = list.getEntries();
                const last = entries[entries.length - 1];
                this.metrics.webVitals.LCP = {
                    value: last.startTime,
                    timestamp: Date.now()
                };
                this.emit('metric', { type: 'LCP', value: last.startTime });
            });
            lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
            this.observers.push(lcpObserver);
        } catch (e) {}
        
        // FID
        try {
            const fidObserver = new PerformanceObserver((list) => {
                const entry = list.getEntries()[0];
                const value = entry.processingStart - entry.startTime;
                this.metrics.webVitals.FID = { value, timestamp: Date.now() };
                this.emit('metric', { type: 'FID', value });
            });
            fidObserver.observe({ type: 'first-input', buffered: true });
            this.observers.push(fidObserver);
        } catch (e) {}
        
        // CLS
        try {
            let clsValue = 0;
            const clsObserver = new PerformanceObserver((list) => {
                for (const entry of list.getEntries()) {
                    if (!entry.hadRecentInput) {
                        clsValue += entry.value;
                    }
                }
                this.metrics.webVitals.CLS = { value: clsValue, timestamp: Date.now() };
                this.emit('metric', { type: 'CLS', value: clsValue });
            });
            clsObserver.observe({ type: 'layout-shift', buffered: true });
            this.observers.push(clsObserver);
        } catch (e) {}
        
        // Paint timing
        try {
            const paintObserver = new PerformanceObserver((list) => {
                for (const entry of list.getEntries()) {
                    this.metrics.webVitals[entry.name] = {
                        value: entry.startTime,
                        timestamp: Date.now()
                    };
                }
            });
            paintObserver.observe({ type: 'paint', buffered: true });
            this.observers.push(paintObserver);
        } catch (e) {}
    }
    
    setupResourceObserver() {
        try {
            const resourceObserver = new PerformanceObserver((list) => {
                for (const entry of list.getEntries()) {
                    this.metrics.resources.push({
                        name: entry.name,
                        type: entry.initiatorType,
                        duration: entry.duration,
                        size: entry.transferSize,
                        timestamp: Date.now()
                    });
                }
            });
            resourceObserver.observe({ type: 'resource', buffered: true });
            this.observers.push(resourceObserver);
        } catch (e) {}
    }
    
    setupLongTaskObserver() {
        try {
            const longTaskObserver = new PerformanceObserver((list) => {
                for (const entry of list.getEntries()) {
                    this.metrics.longTasks.push({
                        duration: entry.duration,
                        startTime: entry.startTime,
                        timestamp: Date.now()
                    });
                    this.emit('longTask', { duration: entry.duration });
                }
            });
            longTaskObserver.observe({ type: 'longtask', buffered: true });
            this.observers.push(longTaskObserver);
        } catch (e) {}
    }
    
    trackCustomMetric(name, value) {
        this.metrics.customMetrics.push({
            name,
            value,
            timestamp: Date.now()
        });
        this.emit('customMetric', { name, value });
    }
    
    takeSnapshot() {
        const snapshot = {
            timestamp: Date.now(),
            uptime: performance.now() - this.startTime,
            webVitals: { ...this.metrics.webVitals },
            resourceCount: this.metrics.resources.length,
            longTaskCount: this.metrics.longTasks.length
        };
        
        this.metrics.history.push(snapshot);
        
        if (this.metrics.history.length > this.options.historySize) {
            this.metrics.history.shift();
        }
    }
    
    getSnapshot() {
        return {
            webVitals: Object.entries(this.metrics.webVitals).map(([name, data]) => ({
                name,
                value: data.value,
                rating: this.getRating(name, data.value)
            })),
            resources: {
                total: this.metrics.resources.length,
                totalSize: this.metrics.resources.reduce((sum, r) => sum + (r.size || 0), 0),
                avgDuration: this.calculateAverage(this.metrics.resources.map(r => r.duration))
            },
            longTasks: {
                count: this.metrics.longTasks.length,
                totalTime: this.metrics.longTasks.reduce((sum, t) => sum + t.duration, 0)
            },
            customMetrics: this.getCustomMetricsSummary()
        };
    }
    
    getCustomMetricsSummary() {
        const byName = {};
        this.metrics.customMetrics.forEach(m => {
            if (!byName[m.name]) byName[m.name] = [];
            byName[m.name].push(m.value);
        });
        
        return Object.entries(byName).map(([name, values]) => ({
            name,
            count: values.length,
            avg: this.calculateAverage(values),
            p50: this.calculatePercentile(values, 50),
            p95: this.calculatePercentile(values, 95)
        }));
    }
    
    calculateAverage(values) {
        if (values.length === 0) return 0;
        return values.reduce((a, b) => a + b, 0) / values.length;
    }
    
    calculatePercentile(values, percentile) {
        if (values.length === 0) return 0;
        const sorted = [...values].sort((a, b) => a - b);
        const index = Math.ceil((percentile / 100) * sorted.length) - 1;
        return sorted[index];
    }
    
    getRating(metric, value) {
        const thresholds = {
            LCP: [2500, 4000],
            FID: [100, 300],
            CLS: [0.1, 0.25],
            'first-contentful-paint': [1800, 3000],
            'first-paint': [1000, 2000]
        };
        
        const [good, poor] = thresholds[metric] || [Infinity, Infinity];
        if (value <= good) return 'good';
        if (value <= poor) return 'needs-improvement';
        return 'poor';
    }
    
    generateReport() {
        const snapshot = this.getSnapshot();
        
        return {
            timestamp: new Date().toISOString(),
            summary: {
                overallScore: this.calculateOverallScore(),
                webVitals: snapshot.webVitals,
                resources: snapshot.resources,
                longTasks: snapshot.longTasks
            },
            customMetrics: snapshot.customMetrics,
            history: this.metrics.history.slice(-10),
            recommendations: this.generateRecommendations()
        };
    }
    
    calculateOverallScore() {
        const vitals = this.metrics.webVitals;
        let score = 100;
        
        if (vitals.LCP?.value > 4000) score -= 30;
        else if (vitals.LCP?.value > 2500) score -= 15;
        
        if (vitals.FID?.value > 300) score -= 20;
        else if (vitals.FID?.value > 100) score -= 10;
        
        if (vitals.CLS?.value > 0.25) score -= 20;
        else if (vitals.CLS?.value > 0.1) score -= 10;
        
        if (this.metrics.longTasks.length > 10) score -= 20;
        else if (this.metrics.longTasks.length > 5) score -= 10;
        
        return Math.max(0, score);
    }
    
    generateRecommendations() {
        const recommendations = [];
        const vitals = this.metrics.webVitals;
        
        if (vitals.LCP?.value > 2500) {
            recommendations.push({
                metric: 'LCP',
                issue: 'Slow Largest Contentful Paint',
                suggestion: 'Optimize images, preload critical resources, improve server response time'
            });
        }
        
        if (vitals.CLS?.value > 0.1) {
            recommendations.push({
                metric: 'CLS',
                issue: 'Layout shifts detected',
                suggestion: 'Set explicit dimensions on images and embeds, avoid inserting content above existing content'
            });
        }
        
        if (this.metrics.longTasks.length > 5) {
            recommendations.push({
                metric: 'Long Tasks',
                issue: 'Multiple long tasks detected',
                suggestion: 'Break up long JavaScript tasks, use web workers, defer non-critical work'
            });
        }
        
        return recommendations;
    }
    
    on(event, callback) {
        if (!this.listeners.has(event)) {
            this.listeners.set(event, []);
        }
        this.listeners.get(event).push(callback);
        return this;
    }
    
    emit(event, data) {
        const callbacks = this.listeners.get(event) || [];
        callbacks.forEach(cb => cb(data));
    }
    
    stop() {
        clearInterval(this.updateInterval);
        this.observers.forEach(obs => obs.disconnect());
    }
}
*/

// ============================================
// EXERCISE 2: User Timing Helper
// ============================================

/**
 * Create a user timing helper that:
 * - Provides easy-to-use timing API
 * - Supports nested measurements
 * - Generates flame graph data
 *
 * Requirements:
 * - Track parent/child relationships
 * - Calculate exclusive vs inclusive time
 * - Export in standard trace format
 */

class UserTimingHelper {
  // Your implementation here
}

/*
// SOLUTION:
class UserTimingHelper {
    constructor() {
        this.measurements = [];
        this.stack = [];
        this.idCounter = 0;
        
        // Observe our measurements
        this.observer = new PerformanceObserver((list) => {
            for (const entry of list.getEntries()) {
                this.processEntry(entry);
            }
        });
        
        try {
            this.observer.observe({ entryTypes: ['measure'] });
        } catch (e) {}
    }
    
    processEntry(entry) {
        // Find matching measurement and update duration
        const measurement = this.measurements.find(m => 
            m.name === entry.name && m.startTime === entry.startTime
        );
        
        if (measurement) {
            measurement.duration = entry.duration;
        }
    }
    
    start(name, metadata = {}) {
        const id = ++this.idCounter;
        const parentId = this.stack.length > 0 ? this.stack[this.stack.length - 1] : null;
        
        const markName = `${name}-${id}-start`;
        performance.mark(markName);
        
        const measurement = {
            id,
            name,
            parentId,
            startMark: markName,
            startTime: performance.now(),
            duration: 0,
            metadata,
            children: []
        };
        
        this.measurements.push(measurement);
        this.stack.push(id);
        
        // Add to parent's children
        if (parentId) {
            const parent = this.measurements.find(m => m.id === parentId);
            if (parent) parent.children.push(id);
        }
        
        return id;
    }
    
    end(id) {
        const measurement = this.measurements.find(m => m.id === id);
        if (!measurement) return null;
        
        const endMark = `${measurement.name}-${id}-end`;
        performance.mark(endMark);
        
        try {
            performance.measure(measurement.name, measurement.startMark, endMark);
        } catch (e) {}
        
        measurement.endTime = performance.now();
        measurement.duration = measurement.endTime - measurement.startTime;
        
        // Remove from stack
        const stackIndex = this.stack.indexOf(id);
        if (stackIndex > -1) {
            this.stack.splice(stackIndex, 1);
        }
        
        // Clean up marks
        performance.clearMarks(measurement.startMark);
        performance.clearMarks(endMark);
        
        return measurement.duration;
    }
    
    measure(name, callback, metadata = {}) {
        const id = this.start(name, metadata);
        
        try {
            const result = callback();
            
            if (result instanceof Promise) {
                return result.finally(() => this.end(id));
            }
            
            this.end(id);
            return result;
        } catch (error) {
            this.end(id);
            throw error;
        }
    }
    
    async measureAsync(name, asyncCallback, metadata = {}) {
        const id = this.start(name, metadata);
        
        try {
            return await asyncCallback();
        } finally {
            this.end(id);
        }
    }
    
    calculateExclusiveTime(measurement) {
        const childTime = measurement.children.reduce((sum, childId) => {
            const child = this.measurements.find(m => m.id === childId);
            return sum + (child ? child.duration : 0);
        }, 0);
        
        return measurement.duration - childTime;
    }
    
    getFlameGraphData() {
        // Find root measurements (no parent)
        const roots = this.measurements.filter(m => m.parentId === null);
        
        const buildNode = (measurement) => ({
            name: measurement.name,
            value: measurement.duration,
            selfValue: this.calculateExclusiveTime(measurement),
            children: measurement.children
                .map(id => this.measurements.find(m => m.id === id))
                .filter(Boolean)
                .map(buildNode)
        });
        
        return roots.map(buildNode);
    }
    
    getTraceEvents() {
        // Export in Chrome Trace Event format
        return this.measurements.map(m => ({
            name: m.name,
            cat: 'user_timing',
            ph: 'X', // Complete event
            ts: m.startTime * 1000, // Convert to microseconds
            dur: m.duration * 1000,
            pid: 1,
            tid: 1,
            args: m.metadata
        }));
    }
    
    getReport() {
        const byName = {};
        
        this.measurements.forEach(m => {
            if (!byName[m.name]) {
                byName[m.name] = {
                    count: 0,
                    totalDuration: 0,
                    durations: []
                };
            }
            
            byName[m.name].count++;
            byName[m.name].totalDuration += m.duration;
            byName[m.name].durations.push(m.duration);
        });
        
        return Object.entries(byName).map(([name, data]) => ({
            name,
            count: data.count,
            totalDuration: data.totalDuration.toFixed(2) + 'ms',
            avgDuration: (data.totalDuration / data.count).toFixed(2) + 'ms',
            minDuration: Math.min(...data.durations).toFixed(2) + 'ms',
            maxDuration: Math.max(...data.durations).toFixed(2) + 'ms'
        }));
    }
    
    clear() {
        this.measurements = [];
        this.stack = [];
        performance.clearMeasures();
    }
    
    disconnect() {
        this.observer.disconnect();
    }
}
*/

// ============================================
// EXERCISE 3: RUM (Real User Monitoring) Collector
// ============================================

/**
 * Create a RUM collector that:
 * - Collects user experience metrics
 * - Batches and sends to analytics
 * - Handles offline scenarios
 *
 * Requirements:
 * - Collect navigation, resource, and interaction timing
 * - Queue data when offline
 * - Send in batches to reduce requests
 */

class RUMCollector {
  // Your implementation here
}

/*
// SOLUTION:
class RUMCollector {
    constructor(options = {}) {
        this.options = {
            endpoint: options.endpoint || '/api/rum',
            batchSize: options.batchSize || 10,
            batchInterval: options.batchInterval || 5000,
            maxQueueSize: options.maxQueueSize || 100
        };
        
        this.queue = [];
        this.sessionId = this.generateSessionId();
        this.pageLoadId = this.generateId();
        this.isOnline = navigator.onLine;
        
        this.init();
    }
    
    init() {
        // Network status
        window.addEventListener('online', () => {
            this.isOnline = true;
            this.flush();
        });
        
        window.addEventListener('offline', () => {
            this.isOnline = false;
        });
        
        // Page visibility
        document.addEventListener('visibilitychange', () => {
            if (document.visibilityState === 'hidden') {
                this.flush(true);
            }
        });
        
        // Before unload
        window.addEventListener('beforeunload', () => {
            this.flush(true);
        });
        
        // Collect initial data
        this.collectNavigationTiming();
        this.setupObservers();
        
        // Batch interval
        this.batchTimer = setInterval(() => {
            this.flush();
        }, this.options.batchInterval);
    }
    
    generateSessionId() {
        const stored = sessionStorage.getItem('rum_session');
        if (stored) return stored;
        
        const id = this.generateId();
        sessionStorage.setItem('rum_session', id);
        return id;
    }
    
    generateId() {
        return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
    }
    
    collectNavigationTiming() {
        // Wait for load event
        const collect = () => {
            const nav = performance.getEntriesByType('navigation')[0];
            if (!nav) return;
            
            this.addEvent({
                type: 'navigation',
                metrics: {
                    dnsTime: nav.domainLookupEnd - nav.domainLookupStart,
                    connectTime: nav.connectEnd - nav.connectStart,
                    ttfb: nav.responseStart - nav.requestStart,
                    responseTime: nav.responseEnd - nav.responseStart,
                    domInteractive: nav.domInteractive,
                    domComplete: nav.domComplete,
                    loadTime: nav.loadEventEnd - nav.startTime,
                    transferSize: nav.transferSize,
                    type: nav.type
                }
            });
        };
        
        if (document.readyState === 'complete') {
            collect();
        } else {
            window.addEventListener('load', () => setTimeout(collect, 0));
        }
    }
    
    setupObservers() {
        // Web Vitals
        try {
            new PerformanceObserver((list) => {
                const entry = list.getEntries()[list.getEntries().length - 1];
                this.addEvent({
                    type: 'web-vital',
                    metric: 'LCP',
                    value: entry.startTime,
                    element: entry.element?.tagName
                });
            }).observe({ type: 'largest-contentful-paint', buffered: true });
        } catch (e) {}
        
        try {
            new PerformanceObserver((list) => {
                const entry = list.getEntries()[0];
                this.addEvent({
                    type: 'web-vital',
                    metric: 'FID',
                    value: entry.processingStart - entry.startTime,
                    eventType: entry.name
                });
            }).observe({ type: 'first-input', buffered: true });
        } catch (e) {}
        
        try {
            let clsValue = 0;
            new PerformanceObserver((list) => {
                for (const entry of list.getEntries()) {
                    if (!entry.hadRecentInput) {
                        clsValue += entry.value;
                    }
                }
                this.updateEvent('cls', {
                    type: 'web-vital',
                    metric: 'CLS',
                    value: clsValue
                });
            }).observe({ type: 'layout-shift', buffered: true });
        } catch (e) {}
        
        // Long tasks
        try {
            new PerformanceObserver((list) => {
                for (const entry of list.getEntries()) {
                    this.addEvent({
                        type: 'long-task',
                        duration: entry.duration,
                        startTime: entry.startTime
                    });
                }
            }).observe({ type: 'longtask', buffered: true });
        } catch (e) {}
    }
    
    addEvent(data) {
        const event = {
            ...data,
            id: this.generateId(),
            sessionId: this.sessionId,
            pageLoadId: this.pageLoadId,
            timestamp: Date.now(),
            url: window.location.href,
            userAgent: navigator.userAgent
        };
        
        this.queue.push(event);
        
        // Prevent queue from growing too large
        if (this.queue.length > this.options.maxQueueSize) {
            this.queue.shift();
        }
        
        // Auto-flush if batch size reached
        if (this.queue.length >= this.options.batchSize) {
            this.flush();
        }
    }
    
    updateEvent(key, data) {
        // Update existing event or add new
        const index = this.queue.findIndex(e => e._key === key);
        if (index > -1) {
            this.queue[index] = { ...this.queue[index], ...data };
        } else {
            this.addEvent({ ...data, _key: key });
        }
    }
    
    trackInteraction(element, eventType) {
        const start = performance.now();
        
        return () => {
            const duration = performance.now() - start;
            this.addEvent({
                type: 'interaction',
                eventType,
                element: element.tagName,
                elementId: element.id,
                duration
            });
        };
    }
    
    trackError(error) {
        this.addEvent({
            type: 'error',
            message: error.message,
            stack: error.stack,
            filename: error.filename,
            lineno: error.lineno
        });
    }
    
    async flush(useBeacon = false) {
        if (this.queue.length === 0) return;
        
        const events = [...this.queue];
        this.queue = [];
        
        if (!this.isOnline) {
            // Store for later
            this.queue = [...events, ...this.queue];
            return;
        }
        
        const payload = JSON.stringify({
            events,
            sentAt: Date.now()
        });
        
        if (useBeacon && navigator.sendBeacon) {
            const success = navigator.sendBeacon(
                this.options.endpoint,
                new Blob([payload], { type: 'application/json' })
            );
            
            if (!success) {
                this.queue = [...events, ...this.queue];
            }
        } else {
            try {
                await fetch(this.options.endpoint, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: payload,
                    keepalive: true
                });
            } catch (error) {
                // Put events back in queue
                this.queue = [...events, ...this.queue];
            }
        }
    }
    
    getQueuedEvents() {
        return [...this.queue];
    }
    
    destroy() {
        clearInterval(this.batchTimer);
        this.flush(true);
    }
}
*/

// ============================================
// EXERCISE 4: Performance Regression Detector
// ============================================

/**
 * Create a regression detector that:
 * - Compares current vs baseline performance
 * - Detects significant regressions
 * - Alerts on threshold violations
 *
 * Requirements:
 * - Statistical significance testing
 * - Support for multiple metrics
 * - Configurable thresholds
 */

class PerformanceRegressionDetector {
  // Your implementation here
}

/*
// SOLUTION:
class PerformanceRegressionDetector {
    constructor(options = {}) {
        this.options = {
            sampleSize: options.sampleSize || 30,
            significanceLevel: options.significanceLevel || 0.05,
            regressionThreshold: options.regressionThreshold || 0.1 // 10%
        };
        
        this.baselines = new Map();
        this.current = new Map();
        this.listeners = new Map();
    }
    
    setBaseline(metricName, values) {
        this.baselines.set(metricName, {
            values: [...values],
            stats: this.calculateStats(values)
        });
    }
    
    addSample(metricName, value) {
        if (!this.current.has(metricName)) {
            this.current.set(metricName, []);
        }
        
        const samples = this.current.get(metricName);
        samples.push(value);
        
        // Check for regression once we have enough samples
        if (samples.length >= this.options.sampleSize) {
            this.checkRegression(metricName);
        }
    }
    
    checkRegression(metricName) {
        const baseline = this.baselines.get(metricName);
        const current = this.current.get(metricName);
        
        if (!baseline || !current || current.length < 2) {
            return null;
        }
        
        const currentStats = this.calculateStats(current);
        const baselineStats = baseline.stats;
        
        // Calculate percent change
        const percentChange = (currentStats.mean - baselineStats.mean) / baselineStats.mean;
        
        // Perform t-test
        const tTest = this.tTest(current, baseline.values);
        
        const result = {
            metric: metricName,
            baseline: baselineStats,
            current: currentStats,
            percentChange: percentChange * 100,
            pValue: tTest.pValue,
            isSignificant: tTest.pValue < this.options.significanceLevel,
            isRegression: percentChange > this.options.regressionThreshold && 
                          tTest.pValue < this.options.significanceLevel,
            severity: this.calculateSeverity(percentChange)
        };
        
        if (result.isRegression) {
            this.emit('regression', result);
        }
        
        return result;
    }
    
    calculateStats(values) {
        const n = values.length;
        const mean = values.reduce((a, b) => a + b, 0) / n;
        const variance = values.reduce((sum, val) => 
            sum + Math.pow(val - mean, 2), 0
        ) / (n - 1);
        const stdDev = Math.sqrt(variance);
        
        const sorted = [...values].sort((a, b) => a - b);
        const median = n % 2 === 0 ?
            (sorted[n/2 - 1] + sorted[n/2]) / 2 :
            sorted[Math.floor(n/2)];
        
        return {
            n,
            mean,
            median,
            stdDev,
            min: Math.min(...values),
            max: Math.max(...values),
            p95: this.percentile(sorted, 95)
        };
    }
    
    percentile(sortedValues, p) {
        const index = Math.ceil((p / 100) * sortedValues.length) - 1;
        return sortedValues[index];
    }
    
    tTest(sample1, sample2) {
        const n1 = sample1.length;
        const n2 = sample2.length;
        
        const mean1 = sample1.reduce((a, b) => a + b, 0) / n1;
        const mean2 = sample2.reduce((a, b) => a + b, 0) / n2;
        
        const var1 = sample1.reduce((sum, val) => 
            sum + Math.pow(val - mean1, 2), 0
        ) / (n1 - 1);
        const var2 = sample2.reduce((sum, val) => 
            sum + Math.pow(val - mean2, 2), 0
        ) / (n2 - 1);
        
        // Welch's t-test
        const se = Math.sqrt(var1/n1 + var2/n2);
        const t = (mean1 - mean2) / se;
        
        // Degrees of freedom (Welch-Satterthwaite)
        const df = Math.pow(var1/n1 + var2/n2, 2) / (
            Math.pow(var1/n1, 2)/(n1-1) + Math.pow(var2/n2, 2)/(n2-1)
        );
        
        // Approximate p-value (simplified)
        const pValue = this.approximatePValue(Math.abs(t), df);
        
        return { t, df, pValue };
    }
    
    approximatePValue(t, df) {
        // Simplified approximation
        const x = df / (df + t * t);
        return x; // Very rough approximation
    }
    
    calculateSeverity(percentChange) {
        if (percentChange > 0.5) return 'critical';
        if (percentChange > 0.25) return 'high';
        if (percentChange > 0.1) return 'medium';
        return 'low';
    }
    
    getReport() {
        const report = [];
        
        for (const [metric] of this.baselines) {
            const result = this.checkRegression(metric);
            if (result) {
                report.push(result);
            }
        }
        
        return {
            timestamp: new Date().toISOString(),
            metrics: report,
            summary: {
                total: report.length,
                regressions: report.filter(r => r.isRegression).length,
                significant: report.filter(r => r.isSignificant).length
            }
        };
    }
    
    reset(metricName = null) {
        if (metricName) {
            this.current.delete(metricName);
        } else {
            this.current.clear();
        }
    }
    
    on(event, callback) {
        if (!this.listeners.has(event)) {
            this.listeners.set(event, []);
        }
        this.listeners.get(event).push(callback);
        return this;
    }
    
    emit(event, data) {
        const callbacks = this.listeners.get(event) || [];
        callbacks.forEach(cb => cb(data));
    }
}
*/

// ============================================
// EXERCISE 5: Synthetic Performance Monitor
// ============================================

/**
 * Create a synthetic monitor that:
 * - Periodically measures operations
 * - Tracks trends over time
 * - Generates alerts on anomalies
 *
 * Requirements:
 * - Run measurements at intervals
 * - Detect anomalies using statistics
 * - Provide trend analysis
 */

class SyntheticPerformanceMonitor {
  // Your implementation here
}

/*
// SOLUTION:
class SyntheticPerformanceMonitor {
    constructor(options = {}) {
        this.options = {
            interval: options.interval || 30000,
            historySize: options.historySize || 100,
            anomalyThreshold: options.anomalyThreshold || 2 // standard deviations
        };
        
        this.tests = new Map();
        this.results = new Map();
        this.listeners = new Map();
        this.intervalId = null;
    }
    
    registerTest(name, testFn, options = {}) {
        this.tests.set(name, {
            fn: testFn,
            timeout: options.timeout || 5000,
            weight: options.weight || 1
        });
        
        this.results.set(name, {
            history: [],
            anomalies: [],
            stats: null
        });
    }
    
    async runTest(name) {
        const test = this.tests.get(name);
        if (!test) return null;
        
        const start = performance.now();
        let success = true;
        let error = null;
        
        try {
            await Promise.race([
                test.fn(),
                new Promise((_, reject) => 
                    setTimeout(() => reject(new Error('Timeout')), test.timeout)
                )
            ]);
        } catch (e) {
            success = false;
            error = e.message;
        }
        
        const duration = performance.now() - start;
        
        const result = {
            name,
            duration,
            success,
            error,
            timestamp: Date.now()
        };
        
        this.recordResult(name, result);
        
        return result;
    }
    
    recordResult(name, result) {
        const data = this.results.get(name);
        if (!data) return;
        
        data.history.push(result);
        
        // Trim history
        if (data.history.length > this.options.historySize) {
            data.history.shift();
        }
        
        // Update stats
        const durations = data.history
            .filter(r => r.success)
            .map(r => r.duration);
        
        if (durations.length > 5) {
            data.stats = this.calculateStats(durations);
            
            // Check for anomaly
            if (result.success) {
                const zscore = (result.duration - data.stats.mean) / data.stats.stdDev;
                
                if (Math.abs(zscore) > this.options.anomalyThreshold) {
                    const anomaly = {
                        ...result,
                        zscore,
                        type: zscore > 0 ? 'slow' : 'fast'
                    };
                    
                    data.anomalies.push(anomaly);
                    this.emit('anomaly', anomaly);
                }
            }
        }
        
        this.emit('result', result);
    }
    
    calculateStats(values) {
        const n = values.length;
        const mean = values.reduce((a, b) => a + b, 0) / n;
        const variance = values.reduce((sum, val) => 
            sum + Math.pow(val - mean, 2), 0
        ) / n;
        const stdDev = Math.sqrt(variance);
        
        // Calculate trend
        let trend = 0;
        if (n >= 10) {
            const recentAvg = values.slice(-5).reduce((a, b) => a + b, 0) / 5;
            const olderAvg = values.slice(-10, -5).reduce((a, b) => a + b, 0) / 5;
            trend = ((recentAvg - olderAvg) / olderAvg) * 100;
        }
        
        return { mean, stdDev, n, trend };
    }
    
    async runAllTests() {
        const results = [];
        
        for (const [name] of this.tests) {
            const result = await this.runTest(name);
            results.push(result);
        }
        
        return results;
    }
    
    start() {
        this.runAllTests();
        
        this.intervalId = setInterval(() => {
            this.runAllTests();
        }, this.options.interval);
    }
    
    stop() {
        if (this.intervalId) {
            clearInterval(this.intervalId);
            this.intervalId = null;
        }
    }
    
    getTestStats(name) {
        const data = this.results.get(name);
        if (!data) return null;
        
        const successCount = data.history.filter(r => r.success).length;
        
        return {
            name,
            sampleCount: data.history.length,
            successRate: (successCount / data.history.length * 100).toFixed(1) + '%',
            stats: data.stats,
            anomalyCount: data.anomalies.length,
            trend: data.stats?.trend?.toFixed(1) + '%' || 'N/A',
            lastResult: data.history[data.history.length - 1]
        };
    }
    
    getReport() {
        const testStats = [];
        
        for (const [name] of this.tests) {
            const stats = this.getTestStats(name);
            if (stats) testStats.push(stats);
        }
        
        return {
            timestamp: new Date().toISOString(),
            interval: this.options.interval,
            tests: testStats,
            summary: {
                totalTests: testStats.length,
                healthyTests: testStats.filter(t => 
                    parseFloat(t.successRate) >= 99 && 
                    Math.abs(parseFloat(t.trend) || 0) < 10
                ).length
            }
        };
    }
    
    getTrends() {
        const trends = [];
        
        for (const [name, data] of this.results) {
            if (data.stats && data.history.length >= 10) {
                trends.push({
                    name,
                    trend: data.stats.trend,
                    direction: data.stats.trend > 5 ? 'degrading' :
                               data.stats.trend < -5 ? 'improving' : 'stable'
                });
            }
        }
        
        return trends;
    }
    
    on(event, callback) {
        if (!this.listeners.has(event)) {
            this.listeners.set(event, []);
        }
        this.listeners.get(event).push(callback);
        return this;
    }
    
    emit(event, data) {
        const callbacks = this.listeners.get(event) || [];
        callbacks.forEach(cb => cb(data));
    }
}
*/

// ============================================
// TEST UTILITIES
// ============================================

console.log('=== PerformanceObserver Exercises ===');
console.log('');
console.log('Exercises:');
console.log('1. PerformanceDashboard - Comprehensive metrics collection');
console.log('2. UserTimingHelper - Custom timing with nesting');
console.log('3. RUMCollector - Real User Monitoring');
console.log(
  '4. PerformanceRegressionDetector - Statistical regression detection'
);
console.log('5. SyntheticPerformanceMonitor - Periodic performance testing');
console.log('');
console.log('These exercises require a browser environment.');
console.log('Uncomment solutions to see implementations.');

// Export for browser use
if (typeof module !== 'undefined' && module.exports) {
  module.exports = {
    PerformanceDashboard,
    UserTimingHelper,
    RUMCollector,
    PerformanceRegressionDetector,
    SyntheticPerformanceMonitor,
  };
}
Exercises - JavaScript Tutorial | DeepML