Performance testing implemented
CI Pipeline / test (18) (pull_request) Failing after 49s
CI Pipeline / test (20) (pull_request) Failing after 53s
CI Pipeline / e2e (chromium) (pull_request) Failing after 21m9s
CI Pipeline / e2e (firefox) (pull_request) Failing after 25m13s
CI Pipeline / visual-regression (pull_request) Failing after 7m9s
CI Pipeline / performance (pull_request) Failing after 2m1s
CI Pipeline / storybook (pull_request) Failing after 1m32s
CI Pipeline / lint (pull_request) Failing after 44s
CI Pipeline / build (pull_request) Failing after 1m43s
CI Pipeline / e2e (webkit) (pull_request) Failing after 23m14s
CI Pipeline / test (18) (pull_request) Failing after 49s
CI Pipeline / test (20) (pull_request) Failing after 53s
CI Pipeline / e2e (chromium) (pull_request) Failing after 21m9s
CI Pipeline / e2e (firefox) (pull_request) Failing after 25m13s
CI Pipeline / visual-regression (pull_request) Failing after 7m9s
CI Pipeline / performance (pull_request) Failing after 2m1s
CI Pipeline / storybook (pull_request) Failing after 1m32s
CI Pipeline / lint (pull_request) Failing after 44s
CI Pipeline / build (pull_request) Failing after 1m43s
CI Pipeline / e2e (webkit) (pull_request) Failing after 23m14s
This commit is contained in:
@@ -0,0 +1,381 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { PlaywrightPerformanceMonitor } from "../performance/performance-monitor.js";
|
||||
|
||||
// Performance budgets and thresholds
|
||||
const PERFORMANCE_BUDGETS = {
|
||||
// Page load performance
|
||||
page_load_time: 3000, // 3 seconds
|
||||
first_contentful_paint: 2000, // 2 seconds
|
||||
largest_contentful_paint: 2500, // 2.5 seconds
|
||||
first_input_delay: 100, // 100ms
|
||||
|
||||
// Navigation timing
|
||||
dns_lookup: 100, // 100ms
|
||||
tcp_connection: 200, // 200ms
|
||||
ttfb: 600, // 600ms
|
||||
dom_content_loaded: 1500, // 1.5 seconds
|
||||
full_load: 3000, // 3 seconds
|
||||
|
||||
// Component performance
|
||||
component_render_time: 500, // 500ms
|
||||
interaction_time: 100, // 100ms
|
||||
scroll_performance: 50, // 50ms
|
||||
|
||||
// Resource performance
|
||||
network_request_duration: 1000, // 1 second
|
||||
memory_usage_mb: 50, // 50MB
|
||||
};
|
||||
|
||||
// Baseline metrics for regression detection
|
||||
const BASELINE_METRICS = {
|
||||
page_load_time: 2000,
|
||||
first_contentful_paint: 1500,
|
||||
largest_contentful_paint: 2000,
|
||||
first_input_delay: 50,
|
||||
dns_lookup: 50,
|
||||
tcp_connection: 100,
|
||||
ttfb: 400,
|
||||
dom_content_loaded: 1000,
|
||||
full_load: 2000,
|
||||
component_render_time: 300,
|
||||
interaction_time: 50,
|
||||
scroll_performance: 30,
|
||||
network_request_duration: 500,
|
||||
memory_usage_mb: 30,
|
||||
};
|
||||
|
||||
test.describe("Performance Monitoring", () => {
|
||||
let performanceMonitor: PlaywrightPerformanceMonitor;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
performanceMonitor = new PlaywrightPerformanceMonitor(page);
|
||||
performanceMonitor.setThresholds(PERFORMANCE_BUDGETS);
|
||||
performanceMonitor.setBaselines(BASELINE_METRICS);
|
||||
});
|
||||
|
||||
test("homepage load performance", async ({ page }) => {
|
||||
const result = await performanceMonitor.measurePageLoad("/");
|
||||
|
||||
// Assert page load time is within budget
|
||||
expect(result.loadTime).toBeLessThan(PERFORMANCE_BUDGETS.page_load_time);
|
||||
|
||||
// Assert individual metrics
|
||||
expect(result.metrics.ttfb).toBeLessThan(PERFORMANCE_BUDGETS.ttfb);
|
||||
expect(result.metrics.domContentLoaded).toBeLessThan(
|
||||
PERFORMANCE_BUDGETS.dom_content_loaded
|
||||
);
|
||||
expect(result.metrics.load).toBeLessThan(PERFORMANCE_BUDGETS.full_load);
|
||||
|
||||
// Check for performance regressions
|
||||
const summary = performanceMonitor.getSummary();
|
||||
console.log("Performance Summary:", summary);
|
||||
});
|
||||
|
||||
test("core web vitals", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Wait for page to fully load
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Get Core Web Vitals with timeout
|
||||
const coreWebVitals = await page.evaluate(() => {
|
||||
return new Promise((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
observer.disconnect();
|
||||
resolve({ lcp: 0, fid: 0, cls: 0 }); // Default values if timeout
|
||||
}, 10000); // 10 second timeout
|
||||
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
const metrics: any = {};
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.name === "LCP") {
|
||||
metrics.lcp = entry.startTime;
|
||||
} else if (entry.name === "FID") {
|
||||
metrics.fid = entry.processingStart - entry.startTime;
|
||||
} else if (entry.name === "CLS") {
|
||||
metrics.cls = entry.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(metrics).length === 3) {
|
||||
clearTimeout(timeout);
|
||||
observer.disconnect();
|
||||
resolve(metrics);
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe({
|
||||
entryTypes: [
|
||||
"largest-contentful-paint",
|
||||
"first-input",
|
||||
"layout-shift",
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Assert Core Web Vitals are within acceptable ranges
|
||||
expect(coreWebVitals.lcp).toBeLessThan(
|
||||
PERFORMANCE_BUDGETS.largest_contentful_paint
|
||||
);
|
||||
expect(coreWebVitals.fid).toBeLessThan(
|
||||
PERFORMANCE_BUDGETS.first_input_delay
|
||||
);
|
||||
expect(coreWebVitals.cls).toBeLessThan(0.1); // CLS should be less than 0.1
|
||||
});
|
||||
|
||||
test("component render performance", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Measure header render time
|
||||
const headerRenderTime = await performanceMonitor.measureComponentRender(
|
||||
"header"
|
||||
);
|
||||
expect(headerRenderTime).toBeLessThan(
|
||||
PERFORMANCE_BUDGETS.component_render_time
|
||||
);
|
||||
|
||||
// Measure footer render time
|
||||
const footerRenderTime = await performanceMonitor.measureComponentRender(
|
||||
"footer"
|
||||
);
|
||||
expect(footerRenderTime).toBeLessThan(
|
||||
PERFORMANCE_BUDGETS.component_render_time
|
||||
);
|
||||
|
||||
// Measure main content render time
|
||||
const mainRenderTime = await performanceMonitor.measureComponentRender(
|
||||
"main"
|
||||
);
|
||||
expect(mainRenderTime).toBeLessThan(
|
||||
PERFORMANCE_BUDGETS.component_render_time
|
||||
);
|
||||
});
|
||||
|
||||
test("interaction performance", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Wait for page to be ready
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Measure button click performance with better element selection
|
||||
const buttonClickTime = await performanceMonitor.measureInteraction(
|
||||
'button:has-text("Learn how CommunityRule works")',
|
||||
async () => {
|
||||
const button = page
|
||||
.locator('button:has-text("Learn how CommunityRule works")')
|
||||
.first();
|
||||
await button.waitFor({ state: "visible" });
|
||||
await button.click();
|
||||
}
|
||||
);
|
||||
expect(buttonClickTime).toBeLessThan(PERFORMANCE_BUDGETS.interaction_time);
|
||||
|
||||
// Measure link click performance with better element selection
|
||||
const linkClickTime = await performanceMonitor.measureInteraction(
|
||||
'a:has-text("Use Cases")',
|
||||
async () => {
|
||||
const link = page.locator('a:has-text("Use Cases")').first();
|
||||
await link.waitFor({ state: "visible" });
|
||||
await link.click();
|
||||
}
|
||||
);
|
||||
expect(linkClickTime).toBeLessThan(PERFORMANCE_BUDGETS.interaction_time);
|
||||
});
|
||||
|
||||
test("scroll performance", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Measure scroll performance
|
||||
const scrollTime = await performanceMonitor.measureScrollPerformance();
|
||||
expect(scrollTime).toBeLessThan(PERFORMANCE_BUDGETS.scroll_performance);
|
||||
});
|
||||
|
||||
test("memory usage", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Get memory usage
|
||||
const memoryUsage = await performanceMonitor.getMemoryUsage();
|
||||
|
||||
if (memoryUsage) {
|
||||
const usedMemoryMB = memoryUsage.usedJSHeapSize / 1024 / 1024;
|
||||
expect(usedMemoryMB).toBeLessThan(PERFORMANCE_BUDGETS.memory_usage_mb);
|
||||
|
||||
console.log(`Memory Usage: ${usedMemoryMB.toFixed(2)}MB`);
|
||||
}
|
||||
});
|
||||
|
||||
test("network request performance", async ({ page }) => {
|
||||
const requests = await performanceMonitor.monitorNetworkRequests();
|
||||
|
||||
await page.goto("/");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Check that all requests completed within budget
|
||||
const summary = performanceMonitor.getSummary();
|
||||
if (summary.network_request_duration) {
|
||||
expect(summary.network_request_duration.average).toBeLessThan(
|
||||
PERFORMANCE_BUDGETS.network_request_duration
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("responsive performance across breakpoints", async ({ page }) => {
|
||||
const breakpoints = [
|
||||
{ name: "mobile", width: 375, height: 667 },
|
||||
{ name: "tablet", width: 768, height: 1024 },
|
||||
{ name: "desktop", width: 1280, height: 720 },
|
||||
];
|
||||
|
||||
for (const breakpoint of breakpoints) {
|
||||
await page.setViewportSize(breakpoint);
|
||||
|
||||
const result = await performanceMonitor.measurePageLoad("/");
|
||||
|
||||
// Assert performance is maintained across breakpoints
|
||||
expect(result.loadTime).toBeLessThan(PERFORMANCE_BUDGETS.page_load_time);
|
||||
|
||||
console.log(`${breakpoint.name} load time: ${result.loadTime}ms`);
|
||||
}
|
||||
});
|
||||
|
||||
test("performance under load", async ({ page }) => {
|
||||
// Simulate slower network conditions
|
||||
await page.route("**/*", (route) => {
|
||||
route.continue();
|
||||
});
|
||||
|
||||
// Add artificial delay to simulate network latency
|
||||
await page.addInitScript(() => {
|
||||
const originalFetch = window.fetch;
|
||||
window.fetch = async (...args) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100)); // 100ms delay
|
||||
return originalFetch(...args);
|
||||
};
|
||||
});
|
||||
|
||||
const result = await performanceMonitor.measurePageLoad("/");
|
||||
|
||||
// Even under load, page should load within reasonable time
|
||||
expect(result.loadTime).toBeLessThan(
|
||||
PERFORMANCE_BUDGETS.page_load_time * 1.5
|
||||
);
|
||||
});
|
||||
|
||||
test("performance regression detection", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Simulate a performance regression by adding a heavy operation
|
||||
await page.addInitScript(() => {
|
||||
// Add a heavy operation that would cause regression
|
||||
const heavyOperation = () => {
|
||||
let result = 0;
|
||||
for (let i = 0; i < 1000000; i++) {
|
||||
result += Math.random();
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// Execute heavy operation on page load
|
||||
window.addEventListener("load", () => {
|
||||
heavyOperation();
|
||||
});
|
||||
});
|
||||
|
||||
const result = await performanceMonitor.measurePageLoad("/");
|
||||
|
||||
// This should trigger a performance regression warning
|
||||
const summary = performanceMonitor.getSummary();
|
||||
console.log("Performance Summary with Regression:", summary);
|
||||
});
|
||||
|
||||
test("performance metrics export", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Perform various operations to collect metrics
|
||||
await performanceMonitor.measureComponentRender("header");
|
||||
await performanceMonitor.measureScrollPerformance();
|
||||
await performanceMonitor.getMemoryUsage();
|
||||
|
||||
// Export all metrics
|
||||
const exportedData = performanceMonitor.export();
|
||||
|
||||
// Verify exported data structure
|
||||
expect(exportedData.metrics).toBeDefined();
|
||||
expect(exportedData.baselines).toBeDefined();
|
||||
expect(exportedData.thresholds).toBeDefined();
|
||||
expect(exportedData.summary).toBeDefined();
|
||||
|
||||
console.log(
|
||||
"Exported Performance Data:",
|
||||
JSON.stringify(exportedData, null, 2)
|
||||
);
|
||||
});
|
||||
|
||||
test("performance budget compliance", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Collect comprehensive metrics
|
||||
await performanceMonitor.measurePageLoad("/");
|
||||
await performanceMonitor.measureComponentRender("header");
|
||||
await performanceMonitor.measureComponentRender("footer");
|
||||
await performanceMonitor.measureScrollPerformance();
|
||||
await performanceMonitor.getMemoryUsage();
|
||||
|
||||
const summary = performanceMonitor.getSummary();
|
||||
|
||||
// Check all metrics against budgets
|
||||
for (const [metricName, budget] of Object.entries(PERFORMANCE_BUDGETS)) {
|
||||
if (summary[metricName]) {
|
||||
const actualValue =
|
||||
summary[metricName].latest || summary[metricName].average;
|
||||
expect(actualValue).toBeLessThan(budget);
|
||||
console.log(`${metricName}: ${actualValue}ms (budget: ${budget}ms)`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Performance Regression Testing", () => {
|
||||
test("detect performance regressions over time", async ({ page }) => {
|
||||
const performanceMonitor = new PlaywrightPerformanceMonitor(page);
|
||||
|
||||
// Set strict baselines for regression detection
|
||||
const strictBaselines = {
|
||||
page_load_time: 1500,
|
||||
first_contentful_paint: 1000,
|
||||
component_render_time: 200,
|
||||
interaction_time: 30,
|
||||
};
|
||||
|
||||
performanceMonitor.setBaselines(strictBaselines);
|
||||
|
||||
// Run multiple iterations to detect trends
|
||||
const iterations = 3;
|
||||
const results = [];
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const result = await performanceMonitor.measurePageLoad("/");
|
||||
results.push(result.loadTime);
|
||||
|
||||
// Small delay between iterations
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
// Check for consistent performance
|
||||
const averageLoadTime = results.reduce((a, b) => a + b, 0) / results.length;
|
||||
const variance =
|
||||
results.reduce(
|
||||
(acc, val) => acc + Math.pow(val - averageLoadTime, 2),
|
||||
0
|
||||
) / results.length;
|
||||
|
||||
// Performance should be consistent (low variance)
|
||||
expect(variance).toBeLessThan(100000); // Variance should be less than 100ms²
|
||||
|
||||
console.log(`Average load time: ${averageLoadTime}ms`);
|
||||
console.log(`Variance: ${variance}`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,474 @@
|
||||
/**
|
||||
* Performance Monitoring Module
|
||||
*
|
||||
* This module provides comprehensive performance monitoring capabilities
|
||||
* for detecting performance regressions and maintaining performance budgets.
|
||||
*/
|
||||
|
||||
class PerformanceMonitor {
|
||||
constructor() {
|
||||
this.metrics = new Map();
|
||||
this.baselines = new Map();
|
||||
this.thresholds = new Map();
|
||||
this.history = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set performance thresholds for different metrics
|
||||
*/
|
||||
setThresholds(thresholds) {
|
||||
this.thresholds = new Map(Object.entries(thresholds));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set baseline metrics for comparison
|
||||
*/
|
||||
setBaselines(baselines) {
|
||||
this.baselines = new Map(Object.entries(baselines));
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a performance metric
|
||||
*/
|
||||
recordMetric(name, value, context = {}) {
|
||||
const metric = {
|
||||
name,
|
||||
value,
|
||||
timestamp: Date.now(),
|
||||
context,
|
||||
};
|
||||
|
||||
if (!this.metrics.has(name)) {
|
||||
this.metrics.set(name, []);
|
||||
}
|
||||
this.metrics.get(name).push(metric);
|
||||
|
||||
// Check against thresholds
|
||||
this.checkThreshold(name, value);
|
||||
|
||||
// Check against baselines
|
||||
this.checkBaseline(name, value);
|
||||
|
||||
return metric;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a metric exceeds its threshold
|
||||
*/
|
||||
checkThreshold(name, value) {
|
||||
const threshold = this.thresholds.get(name);
|
||||
if (!threshold) return;
|
||||
|
||||
if (value > threshold) {
|
||||
console.warn(
|
||||
`⚠️ Performance threshold exceeded: ${name} = ${value}ms (threshold: ${threshold}ms)`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a metric has regressed from baseline
|
||||
*/
|
||||
checkBaseline(name, value) {
|
||||
const baseline = this.baselines.get(name);
|
||||
if (!baseline) return;
|
||||
|
||||
const regressionThreshold = baseline * 1.2; // 20% regression threshold
|
||||
if (value > regressionThreshold) {
|
||||
console.error(
|
||||
`🚨 Performance regression detected: ${name} = ${value}ms (baseline: ${baseline}ms)`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest metric value
|
||||
*/
|
||||
getLatestMetric(name) {
|
||||
const metrics = this.metrics.get(name);
|
||||
if (!metrics || metrics.length === 0) return null;
|
||||
return metrics[metrics.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get average metric value
|
||||
*/
|
||||
getAverageMetric(name) {
|
||||
const metrics = this.metrics.get(name);
|
||||
if (!metrics || metrics.length === 0) return null;
|
||||
|
||||
const sum = metrics.reduce((acc, metric) => acc + metric.value, 0);
|
||||
return sum / metrics.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get performance summary
|
||||
*/
|
||||
getSummary() {
|
||||
const summary = {};
|
||||
|
||||
for (const [name, metrics] of this.metrics) {
|
||||
const values = metrics.map((m) => m.value);
|
||||
summary[name] = {
|
||||
latest: values[values.length - 1],
|
||||
average: values.reduce((a, b) => a + b, 0) / values.length,
|
||||
min: Math.min(...values),
|
||||
max: Math.max(...values),
|
||||
count: values.length,
|
||||
};
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all metrics
|
||||
*/
|
||||
clear() {
|
||||
this.metrics.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Export metrics for analysis
|
||||
*/
|
||||
export() {
|
||||
return {
|
||||
metrics: Object.fromEntries(this.metrics),
|
||||
baselines: Object.fromEntries(this.baselines),
|
||||
thresholds: Object.fromEntries(this.thresholds),
|
||||
summary: this.getSummary(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Web Performance API wrapper
|
||||
*/
|
||||
class WebPerformanceMonitor extends PerformanceMonitor {
|
||||
constructor() {
|
||||
super();
|
||||
this.performanceObserver = null;
|
||||
this.setupPerformanceObserver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Performance Observer for automatic metric collection
|
||||
*/
|
||||
setupPerformanceObserver() {
|
||||
if (typeof window === "undefined" || !window.PerformanceObserver) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.performanceObserver = new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
this.recordMetric(entry.name, entry.duration, {
|
||||
entryType: entry.entryType,
|
||||
startTime: entry.startTime,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Observe navigation timing
|
||||
this.performanceObserver.observe({ entryTypes: ["navigation"] });
|
||||
|
||||
// Observe resource timing
|
||||
this.performanceObserver.observe({ entryTypes: ["resource"] });
|
||||
|
||||
// Observe paint timing
|
||||
this.performanceObserver.observe({ entryTypes: ["paint"] });
|
||||
|
||||
// Observe layout shifts
|
||||
this.performanceObserver.observe({ entryTypes: ["layout-shift"] });
|
||||
|
||||
// Observe first input delay
|
||||
this.performanceObserver.observe({ entryTypes: ["first-input"] });
|
||||
} catch (error) {
|
||||
console.warn("Performance Observer not supported:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Core Web Vitals metrics
|
||||
*/
|
||||
async getCoreWebVitals() {
|
||||
if (typeof window === "undefined") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
const metrics = {};
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.name === "LCP") {
|
||||
metrics.lcp = entry.startTime;
|
||||
} else if (entry.name === "FID") {
|
||||
metrics.fid = entry.processingStart - entry.startTime;
|
||||
} else if (entry.name === "CLS") {
|
||||
metrics.cls = entry.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(metrics).length === 3) {
|
||||
observer.disconnect();
|
||||
resolve(metrics);
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe({
|
||||
entryTypes: ["largest-contentful-paint", "first-input", "layout-shift"],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get navigation timing metrics
|
||||
*/
|
||||
getNavigationTiming() {
|
||||
if (typeof window === "undefined" || !window.performance) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const navigation = performance.getEntriesByType("navigation")[0];
|
||||
if (!navigation) return null;
|
||||
|
||||
return {
|
||||
dns: navigation.domainLookupEnd - navigation.domainLookupStart,
|
||||
tcp: navigation.connectEnd - navigation.connectStart,
|
||||
ttfb: navigation.responseStart - navigation.requestStart,
|
||||
download: navigation.responseEnd - navigation.responseStart,
|
||||
domContentLoaded:
|
||||
navigation.domContentLoadedEventEnd -
|
||||
navigation.domContentLoadedEventStart,
|
||||
load: navigation.loadEventEnd - navigation.loadEventStart,
|
||||
total: navigation.loadEventEnd - navigation.fetchStart,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get resource timing metrics
|
||||
*/
|
||||
getResourceTiming() {
|
||||
if (typeof window === "undefined" || !window.performance) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resources = performance.getEntriesByType("resource");
|
||||
return resources.map((resource) => ({
|
||||
name: resource.name,
|
||||
duration: resource.duration,
|
||||
size: resource.transferSize,
|
||||
type: resource.initiatorType,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure function execution time
|
||||
*/
|
||||
async measureFunction(name, fn) {
|
||||
const start = performance.now();
|
||||
try {
|
||||
const result = await fn();
|
||||
const duration = performance.now() - start;
|
||||
this.recordMetric(name, duration);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const duration = performance.now() - start;
|
||||
this.recordMetric(`${name}_error`, duration);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure page load performance
|
||||
*/
|
||||
async measurePageLoad(url) {
|
||||
return this.measureFunction("page_load", async () => {
|
||||
const start = performance.now();
|
||||
|
||||
// Simulate page load (in real implementation, this would be actual navigation)
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const navigation = this.getNavigationTiming();
|
||||
const coreWebVitals = await this.getCoreWebVitals();
|
||||
|
||||
return {
|
||||
loadTime: performance.now() - start,
|
||||
navigation,
|
||||
coreWebVitals,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Playwright Performance Monitor
|
||||
*/
|
||||
class PlaywrightPerformanceMonitor extends PerformanceMonitor {
|
||||
constructor(page) {
|
||||
super();
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure page load performance using Playwright
|
||||
*/
|
||||
async measurePageLoad(url) {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Navigate to the page
|
||||
await this.page.goto(url, { waitUntil: "networkidle" });
|
||||
|
||||
const loadTime = Date.now() - startTime;
|
||||
this.recordMetric("page_load_time", loadTime, { url });
|
||||
|
||||
// Get performance metrics from the page
|
||||
const metrics = await this.page.evaluate(() => {
|
||||
const navigation = performance.getEntriesByType("navigation")[0];
|
||||
const paint = performance.getEntriesByType("paint");
|
||||
|
||||
return {
|
||||
dns: navigation?.domainLookupEnd - navigation?.domainLookupStart || 0,
|
||||
tcp: navigation?.connectEnd - navigation?.connectStart || 0,
|
||||
ttfb: navigation?.responseStart - navigation?.requestStart || 0,
|
||||
download: navigation?.responseEnd - navigation?.responseStart || 0,
|
||||
domContentLoaded:
|
||||
navigation?.domContentLoadedEventEnd -
|
||||
navigation?.domContentLoadedEventStart || 0,
|
||||
load: navigation?.loadEventEnd - navigation?.loadEventStart || 0,
|
||||
firstPaint: paint.find((p) => p.name === "first-paint")?.startTime || 0,
|
||||
firstContentfulPaint:
|
||||
paint.find((p) => p.name === "first-contentful-paint")?.startTime ||
|
||||
0,
|
||||
};
|
||||
});
|
||||
|
||||
// Record individual metrics
|
||||
for (const [name, value] of Object.entries(metrics)) {
|
||||
this.recordMetric(name, value, { url });
|
||||
}
|
||||
|
||||
return {
|
||||
loadTime,
|
||||
metrics,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure component render performance
|
||||
*/
|
||||
async measureComponentRender(selector) {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Wait for the component to be visible
|
||||
await this.page.waitForSelector(selector, { state: "visible" });
|
||||
|
||||
const renderTime = Date.now() - startTime;
|
||||
this.recordMetric("component_render_time", renderTime, { selector });
|
||||
|
||||
return renderTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure interaction performance
|
||||
*/
|
||||
async measureInteraction(selector, action) {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Perform the action
|
||||
await action();
|
||||
|
||||
const interactionTime = Date.now() - startTime;
|
||||
this.recordMetric("interaction_time", interactionTime, {
|
||||
selector,
|
||||
action: action.name,
|
||||
});
|
||||
|
||||
return interactionTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure scroll performance
|
||||
*/
|
||||
async measureScrollPerformance() {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Scroll to bottom
|
||||
await this.page.evaluate(() => {
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
});
|
||||
|
||||
const scrollTime = Date.now() - startTime;
|
||||
this.recordMetric("scroll_performance", scrollTime);
|
||||
|
||||
return scrollTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get memory usage
|
||||
*/
|
||||
async getMemoryUsage() {
|
||||
const memory = await this.page.evaluate(() => {
|
||||
if (performance.memory) {
|
||||
return {
|
||||
usedJSHeapSize: performance.memory.usedJSHeapSize,
|
||||
totalJSHeapSize: performance.memory.totalJSHeapSize,
|
||||
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (memory) {
|
||||
this.recordMetric("memory_usage_mb", memory.usedJSHeapSize / 1024 / 1024);
|
||||
}
|
||||
|
||||
return memory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitor network requests
|
||||
*/
|
||||
async monitorNetworkRequests() {
|
||||
const requests = [];
|
||||
|
||||
this.page.on("request", (request) => {
|
||||
requests.push({
|
||||
url: request.url(),
|
||||
method: request.method(),
|
||||
resourceType: request.resourceType(),
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
});
|
||||
|
||||
this.page.on("response", (response) => {
|
||||
const request = requests.find((r) => r.url === response.url());
|
||||
if (request) {
|
||||
request.status = response.status();
|
||||
request.size = response.headers()["content-length"] || 0;
|
||||
request.duration = Date.now() - request.timestamp;
|
||||
|
||||
this.recordMetric("network_request_duration", request.duration, {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
status: request.status,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return requests;
|
||||
}
|
||||
}
|
||||
|
||||
// Export the performance monitors
|
||||
module.exports = {
|
||||
PerformanceMonitor,
|
||||
WebPerformanceMonitor,
|
||||
PlaywrightPerformanceMonitor,
|
||||
};
|
||||
Reference in New Issue
Block a user