- 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.
479 lines
17 KiB
JavaScript
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}`);
|
|
});
|
|
});
|