- 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.
424 lines
13 KiB
JavaScript
424 lines
13 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Performance Benchmark Report Generator
|
|
*
|
|
* Generates HTML and Markdown reports from benchmark-results.json
|
|
*
|
|
* Usage:
|
|
* node tests/e2e/livecomponents/generate-performance-report.js
|
|
* node tests/e2e/livecomponents/generate-performance-report.js --format=html
|
|
* node tests/e2e/livecomponents/generate-performance-report.js --format=markdown
|
|
*/
|
|
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
// Parse command line arguments
|
|
const args = process.argv.slice(2);
|
|
const format = args.find(arg => arg.startsWith('--format='))?.split('=')[1] || 'both';
|
|
|
|
// Load benchmark results
|
|
const resultsPath = path.join(process.cwd(), 'test-results', 'benchmark-results.json');
|
|
|
|
if (!fs.existsSync(resultsPath)) {
|
|
console.error('❌ No benchmark results found. Run benchmarks first:');
|
|
console.error(' npx playwright test performance-benchmarks.spec.js');
|
|
process.exit(1);
|
|
}
|
|
|
|
const data = JSON.parse(fs.readFileSync(resultsPath, 'utf-8'));
|
|
const { timestamp, results, summary } = data;
|
|
|
|
// Group results by scenario
|
|
const scenarios = {};
|
|
results.forEach(result => {
|
|
if (!scenarios[result.scenario]) {
|
|
scenarios[result.scenario] = [];
|
|
}
|
|
scenarios[result.scenario].push(result);
|
|
});
|
|
|
|
/**
|
|
* Generate Markdown Report
|
|
*/
|
|
function generateMarkdownReport() {
|
|
let markdown = `# LiveComponents Performance Benchmark Report
|
|
|
|
**Generated:** ${new Date(timestamp).toLocaleString()}
|
|
|
|
**Summary:**
|
|
- Total Benchmarks: ${summary.total}
|
|
- Passed: ${summary.passed} ✅
|
|
- Failed: ${summary.failed} ${summary.failed > 0 ? '❌' : ''}
|
|
|
|
---
|
|
|
|
## Executive Summary
|
|
|
|
This report compares the performance characteristics of **Fragment-based rendering** vs **Full HTML rendering** in LiveComponents.
|
|
|
|
### Key Findings
|
|
|
|
`;
|
|
|
|
// Calculate overall speedups
|
|
const fragmentVsFull = [];
|
|
Object.values(scenarios).forEach(scenarioResults => {
|
|
const fragmentResult = scenarioResults.find(r => r.metric.includes('Fragment'));
|
|
const fullResult = scenarioResults.find(r => r.metric.includes('Full'));
|
|
|
|
if (fragmentResult && fullResult && fragmentResult.unit === 'ms' && fullResult.unit === 'ms') {
|
|
const speedup = ((fullResult.value - fragmentResult.value) / fullResult.value * 100);
|
|
fragmentVsFull.push({
|
|
scenario: fragmentResult.scenario,
|
|
speedup,
|
|
fragmentTime: fragmentResult.value,
|
|
fullTime: fullResult.value
|
|
});
|
|
}
|
|
});
|
|
|
|
if (fragmentVsFull.length > 0) {
|
|
const avgSpeedup = fragmentVsFull.reduce((sum, item) => sum + item.speedup, 0) / fragmentVsFull.length;
|
|
markdown += `**Average Performance Improvement:** ${avgSpeedup.toFixed(1)}% faster with fragments\n\n`;
|
|
|
|
const best = fragmentVsFull.reduce((best, item) => item.speedup > best.speedup ? item : best);
|
|
markdown += `**Best Case:** ${best.scenario} - ${best.speedup.toFixed(1)}% faster (${best.fragmentTime.toFixed(2)}ms vs ${best.fullTime.toFixed(2)}ms)\n\n`;
|
|
|
|
const worst = fragmentVsFull.reduce((worst, item) => item.speedup < worst.speedup ? item : worst);
|
|
markdown += `**Worst Case:** ${worst.scenario} - ${worst.speedup.toFixed(1)}% faster (${worst.fragmentTime.toFixed(2)}ms vs ${worst.fullTime.toFixed(2)}ms)\n\n`;
|
|
}
|
|
|
|
markdown += `---
|
|
|
|
## Detailed Results
|
|
|
|
`;
|
|
|
|
// Generate detailed results per scenario
|
|
Object.entries(scenarios).forEach(([scenarioName, scenarioResults]) => {
|
|
markdown += `### ${scenarioName}\n\n`;
|
|
|
|
markdown += `| Metric | Value | Threshold | Status |\n`;
|
|
markdown += `|--------|-------|-----------|--------|\n`;
|
|
|
|
scenarioResults.forEach(result => {
|
|
const value = result.unit === 'bytes'
|
|
? `${(result.value / 1024).toFixed(2)} KB`
|
|
: `${result.value.toFixed(2)} ${result.unit}`;
|
|
|
|
const threshold = result.unit === 'bytes'
|
|
? `${(result.threshold / 1024).toFixed(2)} KB`
|
|
: `${result.threshold} ${result.unit}`;
|
|
|
|
const status = result.passed ? '✅ Pass' : '❌ Fail';
|
|
|
|
markdown += `| ${result.metric} | ${value} | ${threshold} | ${status} |\n`;
|
|
});
|
|
|
|
markdown += `\n`;
|
|
});
|
|
|
|
markdown += `---
|
|
|
|
## Recommendations
|
|
|
|
Based on the benchmark results:
|
|
|
|
`;
|
|
|
|
// Generate recommendations
|
|
const recommendations = [];
|
|
|
|
// Check if fragments are faster
|
|
const avgFragmentTime = results
|
|
.filter(r => r.metric.includes('Fragment') && r.unit === 'ms')
|
|
.reduce((sum, r) => sum + r.value, 0) / results.filter(r => r.metric.includes('Fragment') && r.unit === 'ms').length;
|
|
|
|
const avgFullTime = results
|
|
.filter(r => r.metric.includes('Full') && r.unit === 'ms')
|
|
.reduce((sum, r) => sum + r.value, 0) / results.filter(r => r.metric.includes('Full') && r.unit === 'ms').length;
|
|
|
|
if (avgFragmentTime < avgFullTime) {
|
|
const improvement = ((avgFullTime - avgFragmentTime) / avgFullTime * 100).toFixed(1);
|
|
recommendations.push(`✅ **Use fragment rendering** for partial updates - Average ${improvement}% performance improvement`);
|
|
}
|
|
|
|
// Check payload sizes
|
|
const fragmentPayload = results.find(r => r.metric === 'Fragment Payload Size');
|
|
const fullPayload = results.find(r => r.metric === 'Full HTML Payload Size');
|
|
|
|
if (fragmentPayload && fullPayload) {
|
|
const reduction = ((fullPayload.value - fragmentPayload.value) / fullPayload.value * 100).toFixed(1);
|
|
recommendations.push(`✅ **Fragment updates reduce network payload** by ${reduction}% on average`);
|
|
}
|
|
|
|
// Check rapid updates
|
|
const rapidFragment = results.find(r => r.scenario === 'Rapid Updates (10x)' && r.metric.includes('Fragment'));
|
|
const rapidFull = results.find(r => r.scenario === 'Rapid Updates (10x)' && r.metric.includes('Full'));
|
|
|
|
if (rapidFragment && rapidFull) {
|
|
const multiplier = (rapidFull.value / rapidFragment.value).toFixed(1);
|
|
recommendations.push(`✅ **For rapid successive updates**, fragments are ${multiplier}x faster`);
|
|
}
|
|
|
|
// Check memory consumption
|
|
const memFragment = results.find(r => r.metric === 'Fragment Updates Memory Delta');
|
|
const memFull = results.find(r => r.metric === 'Full Renders Memory Delta');
|
|
|
|
if (memFragment && memFull && memFragment.value < memFull.value) {
|
|
const memReduction = ((memFull.value - memFragment.value) / memFull.value * 100).toFixed(1);
|
|
recommendations.push(`✅ **Lower memory consumption** with fragments - ${memReduction}% less memory used`);
|
|
}
|
|
|
|
recommendations.forEach(rec => {
|
|
markdown += `${rec}\n\n`;
|
|
});
|
|
|
|
markdown += `### When to Use Fragments
|
|
|
|
- ✅ **Small, frequent updates** (e.g., counters, notifications, status indicators)
|
|
- ✅ **Partial form updates** (e.g., validation errors, field suggestions)
|
|
- ✅ **List item modifications** (e.g., shopping cart items, task lists)
|
|
- ✅ **Real-time data updates** (e.g., live scores, stock prices)
|
|
- ✅ **Multi-section updates** (e.g., updating header + footer simultaneously)
|
|
|
|
### When to Use Full Render
|
|
|
|
- ⚠️ **Complete layout changes** (e.g., switching views, modal dialogs)
|
|
- ⚠️ **Initial page load** (no fragments to update yet)
|
|
- ⚠️ **Complex interdependent updates** (easier to re-render entire component)
|
|
- ⚠️ **Simple components** (overhead of fragment logic not worth it)
|
|
|
|
---
|
|
|
|
## Performance Metrics Glossary
|
|
|
|
- **Fragment Update Time:** Time to fetch and apply fragment-specific updates
|
|
- **Full Render Time:** Time to fetch and replace entire component HTML
|
|
- **Network Payload:** Size of data transferred from server to client
|
|
- **DOM Update:** Time spent manipulating the Document Object Model
|
|
- **Memory Delta:** Change in JavaScript heap memory usage
|
|
|
|
---
|
|
|
|
*Generated by LiveComponents Performance Benchmark Suite*
|
|
`;
|
|
|
|
return markdown;
|
|
}
|
|
|
|
/**
|
|
* Generate HTML Report
|
|
*/
|
|
function generateHTMLReport() {
|
|
const markdown = generateMarkdownReport();
|
|
|
|
// Simple markdown to HTML conversion
|
|
let html = markdown
|
|
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
|
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
|
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
|
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
.replace(/`(.+?)`/g, '<code>$1</code>')
|
|
.replace(/^- (.+)$/gm, '<li>$1</li>')
|
|
.replace(/\n\n/g, '</p><p>')
|
|
.replace(/^---$/gm, '<hr>');
|
|
|
|
// Wrap in HTML template
|
|
const htmlDoc = `<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>LiveComponents Performance Benchmark Report</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
line-height: 1.6;
|
|
color: #333;
|
|
background: #f5f5f5;
|
|
padding: 2rem;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
background: white;
|
|
padding: 3rem;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
h1 {
|
|
color: #2c3e50;
|
|
border-bottom: 3px solid #3498db;
|
|
padding-bottom: 1rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
h2 {
|
|
color: #34495e;
|
|
margin-top: 3rem;
|
|
margin-bottom: 1rem;
|
|
padding-bottom: 0.5rem;
|
|
border-bottom: 2px solid #ecf0f1;
|
|
}
|
|
|
|
h3 {
|
|
color: #7f8c8d;
|
|
margin-top: 2rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
p {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin: 1.5rem 0;
|
|
background: white;
|
|
}
|
|
|
|
th {
|
|
background: #3498db;
|
|
color: white;
|
|
padding: 1rem;
|
|
text-align: left;
|
|
font-weight: 600;
|
|
}
|
|
|
|
td {
|
|
padding: 0.75rem 1rem;
|
|
border-bottom: 1px solid #ecf0f1;
|
|
}
|
|
|
|
tr:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
ul {
|
|
list-style-position: inside;
|
|
margin: 1rem 0;
|
|
}
|
|
|
|
li {
|
|
margin: 0.5rem 0;
|
|
padding-left: 1rem;
|
|
}
|
|
|
|
code {
|
|
background: #f1f3f4;
|
|
padding: 0.2rem 0.4rem;
|
|
border-radius: 3px;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
strong {
|
|
color: #2c3e50;
|
|
font-weight: 600;
|
|
}
|
|
|
|
hr {
|
|
border: none;
|
|
border-top: 2px solid #ecf0f1;
|
|
margin: 2rem 0;
|
|
}
|
|
|
|
.summary-box {
|
|
background: #e8f4f8;
|
|
border-left: 4px solid #3498db;
|
|
padding: 1.5rem;
|
|
margin: 2rem 0;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.recommendation {
|
|
background: #d4edda;
|
|
border-left: 4px solid #28a745;
|
|
padding: 1rem;
|
|
margin: 1rem 0;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.warning {
|
|
background: #fff3cd;
|
|
border-left: 4px solid #ffc107;
|
|
padding: 1rem;
|
|
margin: 1rem 0;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
@media print {
|
|
body {
|
|
background: white;
|
|
padding: 0;
|
|
}
|
|
|
|
.container {
|
|
box-shadow: none;
|
|
padding: 1rem;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
${html}
|
|
</div>
|
|
</body>
|
|
</html>`;
|
|
|
|
return htmlDoc;
|
|
}
|
|
|
|
/**
|
|
* Main execution
|
|
*/
|
|
function main() {
|
|
console.log('📊 Generating Performance Benchmark Report...\n');
|
|
|
|
const outputDir = path.join(process.cwd(), 'test-results');
|
|
|
|
if (format === 'markdown' || format === 'both') {
|
|
const markdown = generateMarkdownReport();
|
|
const mdPath = path.join(outputDir, 'performance-report.md');
|
|
fs.writeFileSync(mdPath, markdown);
|
|
console.log(`✅ Markdown report: ${mdPath}`);
|
|
}
|
|
|
|
if (format === 'html' || format === 'both') {
|
|
const html = generateHTMLReport();
|
|
const htmlPath = path.join(outputDir, 'performance-report.html');
|
|
fs.writeFileSync(htmlPath, html);
|
|
console.log(`✅ HTML report: ${htmlPath}`);
|
|
}
|
|
|
|
console.log('\n📈 Report Generation Complete!\n');
|
|
console.log('Summary:');
|
|
console.log(` Total Benchmarks: ${summary.total}`);
|
|
console.log(` Passed: ${summary.passed} ✅`);
|
|
console.log(` Failed: ${summary.failed} ${summary.failed > 0 ? '❌' : ''}`);
|
|
|
|
if (format === 'html' || format === 'both') {
|
|
console.log(`\n🌐 Open HTML report in browser:`);
|
|
console.log(` file://${path.join(outputDir, 'performance-report.html')}`);
|
|
}
|
|
}
|
|
|
|
main();
|