Files
michaelschiemer/tests/e2e/livecomponents/performance-benchmarks.spec.js
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

479 lines
17 KiB
JavaScript

/**
* LiveComponents Performance Benchmarks
*
* Measures and compares performance characteristics of:
* - Fragment-based rendering vs Full HTML rendering
* - Single vs Multiple fragment updates
* - Small vs Large component updates
* - Network payload sizes
* - DOM update times
*
* Run with: npx playwright test performance-benchmarks.spec.js
*
* Generate report: node tests/e2e/livecomponents/generate-performance-report.js
*/
import { test, expect } from '@playwright/test';
// Performance thresholds
const THRESHOLDS = {
fragmentRender: {
small: 50, // ms for single small fragment
medium: 100, // ms for 5-10 fragments
large: 200 // ms for complex component
},
fullRender: {
small: 150, // ms for full render (small component)
medium: 300, // ms for full render (medium component)
large: 500 // ms for full render (large component)
},
networkPayload: {
fragmentMax: 5000, // bytes for fragment response
fullMax: 50000 // bytes for full HTML response
}
};
// Store benchmark results for report generation
const benchmarkResults = [];
/**
* Helper: Measure action execution time
*/
async function measureActionTime(page, componentId, action, params = {}, options = {}) {
const startTime = await page.evaluate(() => performance.now());
await page.evaluate(({ id, actionName, actionParams, opts }) => {
const component = window.LiveComponents.get(id);
return component.call(actionName, actionParams, opts);
}, {
id: componentId,
actionName: action,
actionParams: params,
opts: options
});
// Wait for update to complete
await page.waitForTimeout(50);
const endTime = await page.evaluate(() => performance.now());
return endTime - startTime;
}
/**
* Helper: Measure network payload size
*/
async function measurePayloadSize(page, action) {
let payloadSize = 0;
const responsePromise = page.waitForResponse(
response => response.url().includes('/live-component/'),
{ timeout: 5000 }
);
await action();
const response = await responsePromise;
const body = await response.text();
payloadSize = new TextEncoder().encode(body).length;
return payloadSize;
}
/**
* Helper: Store benchmark result
*/
function storeBenchmarkResult(scenario, metric, value, threshold, unit = 'ms') {
const passed = value <= threshold;
benchmarkResults.push({
scenario,
metric,
value,
threshold,
unit,
passed,
timestamp: new Date().toISOString()
});
}
test.describe('Performance Benchmarks: Fragment vs Full Render', () => {
test.beforeEach(async ({ page }) => {
await page.goto('https://localhost/livecomponents/test/performance');
await page.waitForFunction(() => window.LiveComponents !== undefined);
});
test('Benchmark: Single small fragment update', async ({ page }) => {
// Measure fragment update
const fragmentTime = await measureActionTime(
page,
'counter:benchmark',
'increment',
{},
{ fragments: ['#counter-value'] }
);
// Measure full render
const fullTime = await measureActionTime(
page,
'counter:benchmark',
'increment',
{}
);
// Store results
storeBenchmarkResult('Single Small Fragment', 'Fragment Update Time', fragmentTime, THRESHOLDS.fragmentRender.small);
storeBenchmarkResult('Single Small Fragment', 'Full Render Time', fullTime, THRESHOLDS.fullRender.small);
// Assertions
expect(fragmentTime).toBeLessThan(THRESHOLDS.fragmentRender.small);
expect(fragmentTime).toBeLessThan(fullTime); // Fragment should be faster
const speedup = ((fullTime - fragmentTime) / fullTime * 100).toFixed(1);
console.log(`Fragment speedup: ${speedup}% faster than full render`);
});
test('Benchmark: Multiple fragment updates (5 fragments)', async ({ page }) => {
const fragments = [
'#item-1',
'#item-2',
'#item-3',
'#item-4',
'#item-5'
];
// Measure fragment update
const fragmentTime = await measureActionTime(
page,
'list:benchmark',
'updateItems',
{ count: 5 },
{ fragments }
);
// Measure full render
const fullTime = await measureActionTime(
page,
'list:benchmark',
'updateItems',
{ count: 5 }
);
// Store results
storeBenchmarkResult('Multiple Fragments (5)', 'Fragment Update Time', fragmentTime, THRESHOLDS.fragmentRender.medium);
storeBenchmarkResult('Multiple Fragments (5)', 'Full Render Time', fullTime, THRESHOLDS.fullRender.medium);
// Assertions
expect(fragmentTime).toBeLessThan(THRESHOLDS.fragmentRender.medium);
expect(fragmentTime).toBeLessThan(fullTime);
const speedup = ((fullTime - fragmentTime) / fullTime * 100).toFixed(1);
console.log(`5-fragment speedup: ${speedup}% faster than full render`);
});
test('Benchmark: Large component update (100 items)', async ({ page }) => {
const fragments = ['#item-list'];
// Measure fragment update
const fragmentTime = await measureActionTime(
page,
'product-list:benchmark',
'loadItems',
{ count: 100 },
{ fragments }
);
// Measure full render
const fullTime = await measureActionTime(
page,
'product-list:benchmark',
'loadItems',
{ count: 100 }
);
// Store results
storeBenchmarkResult('Large Component (100 items)', 'Fragment Update Time', fragmentTime, THRESHOLDS.fragmentRender.large);
storeBenchmarkResult('Large Component (100 items)', 'Full Render Time', fullTime, THRESHOLDS.fullRender.large);
// Assertions
expect(fragmentTime).toBeLessThan(THRESHOLDS.fragmentRender.large);
expect(fragmentTime).toBeLessThan(fullTime);
const speedup = ((fullTime - fragmentTime) / fullTime * 100).toFixed(1);
console.log(`100-item speedup: ${speedup}% faster than full render`);
});
test('Benchmark: Network payload size comparison', async ({ page }) => {
// Measure fragment payload
const fragmentPayload = await measurePayloadSize(page, async () => {
await page.evaluate(() => {
window.LiveComponents.get('counter:benchmark').call('increment', {}, {
fragments: ['#counter-value']
});
});
});
// Reset state
await page.evaluate(() => {
window.LiveComponents.get('counter:benchmark').call('reset');
});
await page.waitForTimeout(100);
// Measure full HTML payload
const fullPayload = await measurePayloadSize(page, async () => {
await page.evaluate(() => {
window.LiveComponents.get('counter:benchmark').call('increment');
});
});
// Store results
storeBenchmarkResult('Network Payload', 'Fragment Payload Size', fragmentPayload, THRESHOLDS.networkPayload.fragmentMax, 'bytes');
storeBenchmarkResult('Network Payload', 'Full HTML Payload Size', fullPayload, THRESHOLDS.networkPayload.fullMax, 'bytes');
// Assertions
expect(fragmentPayload).toBeLessThan(THRESHOLDS.networkPayload.fragmentMax);
expect(fullPayload).toBeLessThan(THRESHOLDS.networkPayload.fullMax);
expect(fragmentPayload).toBeLessThan(fullPayload);
const reduction = ((fullPayload - fragmentPayload) / fullPayload * 100).toFixed(1);
console.log(`Fragment payload reduction: ${reduction}% smaller than full HTML`);
console.log(`Fragment: ${fragmentPayload} bytes, Full: ${fullPayload} bytes`);
});
test('Benchmark: Rapid successive updates (10 updates)', async ({ page }) => {
const updateCount = 10;
// Measure fragment updates
const fragmentStartTime = await page.evaluate(() => performance.now());
for (let i = 0; i < updateCount; i++) {
await page.evaluate(() => {
window.LiveComponents.get('counter:benchmark').call('increment', {}, {
fragments: ['#counter-value']
});
});
await page.waitForTimeout(10); // Small delay between updates
}
const fragmentEndTime = await page.evaluate(() => performance.now());
const fragmentTotalTime = fragmentEndTime - fragmentStartTime;
// Reset
await page.evaluate(() => {
window.LiveComponents.get('counter:benchmark').call('reset');
});
await page.waitForTimeout(100);
// Measure full renders
const fullStartTime = await page.evaluate(() => performance.now());
for (let i = 0; i < updateCount; i++) {
await page.evaluate(() => {
window.LiveComponents.get('counter:benchmark').call('increment');
});
await page.waitForTimeout(10);
}
const fullEndTime = await page.evaluate(() => performance.now());
const fullTotalTime = fullEndTime - fullStartTime;
// Store results
storeBenchmarkResult('Rapid Updates (10x)', 'Fragment Total Time', fragmentTotalTime, 500);
storeBenchmarkResult('Rapid Updates (10x)', 'Full Render Total Time', fullTotalTime, 1500);
// Assertions
expect(fragmentTotalTime).toBeLessThan(fullTotalTime);
const avgFragment = fragmentTotalTime / updateCount;
const avgFull = fullTotalTime / updateCount;
console.log(`Average fragment update: ${avgFragment.toFixed(2)}ms`);
console.log(`Average full render: ${avgFull.toFixed(2)}ms`);
console.log(`Fragment ${((fullTotalTime / fragmentTotalTime).toFixed(1))}x faster for rapid updates`);
});
test('Benchmark: DOM manipulation overhead', async ({ page }) => {
// Measure pure DOM update time (client-side only)
const domUpdateTime = await page.evaluate(() => {
const container = document.querySelector('[data-component-id="counter:benchmark"]');
const fragment = container.querySelector('[data-lc-fragment="counter-value"]');
const startTime = performance.now();
// Simulate fragment update
fragment.textContent = 'Updated Value';
const endTime = performance.now();
return endTime - startTime;
});
// Measure network + server time for fragment update
const startTime = await page.evaluate(() => performance.now());
await page.evaluate(() => {
window.LiveComponents.get('counter:benchmark').call('increment', {}, {
fragments: ['#counter-value']
});
});
await page.waitForTimeout(50);
const endTime = await page.evaluate(() => performance.now());
const totalTime = endTime - startTime;
const networkServerTime = totalTime - domUpdateTime;
// Store results
storeBenchmarkResult('DOM Overhead', 'Pure DOM Update', domUpdateTime, 5);
storeBenchmarkResult('DOM Overhead', 'Network + Server Time', networkServerTime, 100);
storeBenchmarkResult('DOM Overhead', 'Total Fragment Update', totalTime, 150);
console.log(`DOM update: ${domUpdateTime.toFixed(2)}ms`);
console.log(`Network + Server: ${networkServerTime.toFixed(2)}ms`);
console.log(`Total: ${totalTime.toFixed(2)}ms`);
expect(domUpdateTime).toBeLessThan(5); // DOM should be very fast
});
test('Benchmark: Memory consumption comparison', async ({ page }) => {
// Measure memory before
const memoryBefore = await page.evaluate(() => {
if (performance.memory) {
return performance.memory.usedJSHeapSize;
}
return 0;
});
// Perform 50 fragment updates
for (let i = 0; i < 50; i++) {
await page.evaluate(() => {
window.LiveComponents.get('counter:benchmark').call('increment', {}, {
fragments: ['#counter-value']
});
});
await page.waitForTimeout(10);
}
// Measure memory after
const memoryAfterFragments = await page.evaluate(() => {
if (performance.memory) {
return performance.memory.usedJSHeapSize;
}
return 0;
});
// Reset
await page.reload();
await page.waitForFunction(() => window.LiveComponents !== undefined);
const memoryBeforeFull = await page.evaluate(() => {
if (performance.memory) {
return performance.memory.usedJSHeapSize;
}
return 0;
});
// Perform 50 full renders
for (let i = 0; i < 50; i++) {
await page.evaluate(() => {
window.LiveComponents.get('counter:benchmark').call('increment');
});
await page.waitForTimeout(10);
}
const memoryAfterFull = await page.evaluate(() => {
if (performance.memory) {
return performance.memory.usedJSHeapSize;
}
return 0;
});
if (memoryBefore > 0) {
const fragmentMemoryDelta = memoryAfterFragments - memoryBefore;
const fullMemoryDelta = memoryAfterFull - memoryBeforeFull;
storeBenchmarkResult('Memory Consumption (50 updates)', 'Fragment Updates Memory Delta', fragmentMemoryDelta, 1000000, 'bytes');
storeBenchmarkResult('Memory Consumption (50 updates)', 'Full Renders Memory Delta', fullMemoryDelta, 2000000, 'bytes');
console.log(`Fragment memory delta: ${(fragmentMemoryDelta / 1024).toFixed(2)} KB`);
console.log(`Full render memory delta: ${(fullMemoryDelta / 1024).toFixed(2)} KB`);
} else {
console.log('Memory API not available (Firefox/Safari)');
}
});
test('Benchmark: Cache effectiveness', async ({ page }) => {
// First update (no cache)
const firstUpdateTime = await measureActionTime(
page,
'counter:benchmark',
'increment',
{},
{ fragments: ['#counter-value'] }
);
// Second update (potentially cached)
const secondUpdateTime = await measureActionTime(
page,
'counter:benchmark',
'increment',
{},
{ fragments: ['#counter-value'] }
);
// Third update (cached)
const thirdUpdateTime = await measureActionTime(
page,
'counter:benchmark',
'increment',
{},
{ fragments: ['#counter-value'] }
);
const avgCachedTime = (secondUpdateTime + thirdUpdateTime) / 2;
storeBenchmarkResult('Cache Effectiveness', 'First Update (Cold)', firstUpdateTime, 100);
storeBenchmarkResult('Cache Effectiveness', 'Average Cached Update', avgCachedTime, 80);
console.log(`First update (cold): ${firstUpdateTime.toFixed(2)}ms`);
console.log(`Average cached: ${avgCachedTime.toFixed(2)}ms`);
if (avgCachedTime < firstUpdateTime) {
const improvement = ((firstUpdateTime - avgCachedTime) / firstUpdateTime * 100).toFixed(1);
console.log(`Cache improves performance by ${improvement}%`);
}
});
// After all tests, write results to file for report generation
test.afterAll(async () => {
const fs = await import('fs');
const path = await import('path');
const resultsFile = path.default.join(
process.cwd(),
'test-results',
'benchmark-results.json'
);
// Ensure directory exists
const dir = path.default.dirname(resultsFile);
if (!fs.default.existsSync(dir)) {
fs.default.mkdirSync(dir, { recursive: true });
}
fs.default.writeFileSync(
resultsFile,
JSON.stringify({
timestamp: new Date().toISOString(),
results: benchmarkResults,
summary: {
total: benchmarkResults.length,
passed: benchmarkResults.filter(r => r.passed).length,
failed: benchmarkResults.filter(r => !r.passed).length
}
}, null, 2)
);
console.log(`\nBenchmark results written to: ${resultsFile}`);
console.log(`Total benchmarks: ${benchmarkResults.length}`);
console.log(`Passed: ${benchmarkResults.filter(r => r.passed).length}`);
console.log(`Failed: ${benchmarkResults.filter(r => !r.passed).length}`);
});
});