javascript
exercises
exercises.js⚡javascript
/**
* 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,
};
}