@ordojs/core
Version:
Core compiler and runtime for OrdoJS framework
635 lines (563 loc) • 21.3 kB
text/typescript
/**
* @fileoverview OrdoJS Performance Benchmarker - Comprehensive performance measurement and optimization
*/
import {
OptimizationError,
OptimizationType,
type GeneratedCode
} from '../types/index.js';
import { OrdoJSCodeGenerator } from './code-generator.js';
import { DeadCodeEliminator } from './dead-code-eliminator.js';
import { OrdoJSLexer } from './lexer.js';
import { OrdoJSParser } from './parser.js';
/**
* Performance metrics for different operations
*/
export interface PerformanceMetrics {
/** Compilation time in milliseconds */
compilationTime: number;
/** Lexical analysis time in milliseconds */
lexingTime: number;
/** Parsing time in milliseconds */
parsingTime: number;
/** Code generation time in milliseconds */
codeGenerationTime: number;
/** Optimization time in milliseconds */
optimizationTime: number;
/** Bundle size in bytes */
bundleSize: number;
/** Gzipped bundle size in bytes */
gzippedSize: number;
/** Brotli compressed bundle size in bytes */
brotliSize: number;
/** Number of AST nodes */
astNodeCount: number;
/** Number of tokens generated */
tokenCount: number;
/** Memory usage in bytes */
memoryUsage: number;
/** Hydration time in milliseconds */
hydrationTime?: number;
/** Runtime performance metrics */
runtimeMetrics?: RuntimeMetrics;
}
/**
* Runtime performance metrics
*/
export interface RuntimeMetrics {
/** Time to first meaningful paint in milliseconds */
timeToFirstPaint: number;
/** Time to interactive in milliseconds */
timeToInteractive: number;
/** Memory usage during runtime in bytes */
runtimeMemoryUsage: number;
/** Number of DOM operations */
domOperations: number;
/** Reactive update time in milliseconds */
reactiveUpdateTime: number;
}
/**
* Benchmark configuration
*/
export interface BenchmarkConfig {
/** Number of iterations for averaging */
iterations: number;
/** Whether to include runtime benchmarks */
includeRuntime: boolean;
/** Whether to measure memory usage */
measureMemory: boolean;
/** Whether to generate compression metrics */
includeCompression: boolean;
/** Target bundle size for optimization */
targetBundleSize?: number;
/** Performance thresholds */
thresholds: PerformanceThresholds;
}
/**
* Performance thresholds for validation
*/
export interface PerformanceThresholds {
/** Maximum compilation time in milliseconds */
maxCompilationTime: number;
/** Maximum bundle size in bytes */
maxBundleSize: number;
/** Maximum hydration time in milliseconds */
maxHydrationTime: number;
/** Maximum time to first paint in milliseconds */
maxTimeToFirstPaint: number;
/** Maximum time to interactive in milliseconds */
maxTimeToInteractive: number;
}
/**
* Benchmark result
*/
export interface BenchmarkResult {
/** Component name */
componentName: string;
/** Average metrics across iterations */
averageMetrics: PerformanceMetrics;
/** All individual measurements */
measurements: PerformanceMetrics[];
/** Performance validation results */
validation: PerformanceValidation;
/** Optimization suggestions */
suggestions: string[];
/** Comparison with baseline */
comparison?: PerformanceComparison;
}
/**
* Performance validation results
*/
export interface PerformanceValidation {
/** Whether all thresholds are met */
passed: boolean;
/** Failed threshold checks */
failures: string[];
/** Performance score (0-100) */
score: number;
}
/**
* Performance comparison with baseline
*/
export interface PerformanceComparison {
/** Baseline metrics */
baseline: PerformanceMetrics;
/** Current metrics */
current: PerformanceMetrics;
/** Percentage improvement/degradation */
improvement: {
compilationTime: number;
bundleSize: number;
hydrationTime: number;
overall: number;
};
}
/**
* Default benchmark configuration
*/
const DEFAULT_BENCHMARK_CONFIG: BenchmarkConfig = {
iterations: 10,
includeRuntime: true,
measureMemory: true,
includeCompression: true,
thresholds: {
maxCompilationTime: 1000, // 1 second
maxBundleSize: 100000, // 100KB
maxHydrationTime: 100, // 100ms
maxTimeToFirstPaint: 2000, // 2 seconds
maxTimeToInteractive: 3000 // 3 seconds
}
};
/**
* Comprehensive performance benchmarker for OrdoJS components
*/
export class PerformanceBenchmarker {
private config: BenchmarkConfig;
private baselineMetrics: Map<string, PerformanceMetrics> = new Map();
private errors: OptimizationError[] = [];
constructor(config: Partial<BenchmarkConfig> = {}) {
this.config = { ...DEFAULT_BENCHMARK_CONFIG, ...config };
}
/**
* Benchmark a component's performance
*/
async benchmarkComponent(
source: string,
componentName: string,
baselineName?: string
): Promise<BenchmarkResult> {
this.errors = [];
const measurements: PerformanceMetrics[] = [];
try {
// Run multiple iterations for averaging
for (let i = 0; i < this.config.iterations; i++) {
const metrics = await this.measurePerformance(source, componentName);
measurements.push(metrics);
}
// Calculate average metrics
const averageMetrics = this.calculateAverageMetrics(measurements);
// Validate performance against thresholds
const validation = this.validatePerformance(averageMetrics);
// Generate optimization suggestions
const suggestions = this.generateSuggestions(averageMetrics, validation);
// Compare with baseline if provided
let comparison: PerformanceComparison | undefined;
if (baselineName && this.baselineMetrics.has(baselineName)) {
comparison = this.compareWithBaseline(averageMetrics, this.baselineMetrics.get(baselineName)!);
}
return {
componentName,
averageMetrics,
measurements,
validation,
suggestions,
comparison
};
} catch (error) {
const benchmarkError = new OptimizationError(
`Benchmark failed: ${error instanceof Error ? error.message : String(error)}`,
{ start: { line: 1, column: 1, offset: 0 }, end: { line: 1, column: 1, offset: 0 } },
OptimizationType.DEPENDENCY_OPTIMIZATION
);
this.errors.push(benchmarkError);
throw benchmarkError;
}
}
/**
* Set baseline metrics for comparison
*/
setBaseline(name: string, metrics: PerformanceMetrics): void {
this.baselineMetrics.set(name, metrics);
}
/**
* Get baseline metrics
*/
getBaseline(name: string): PerformanceMetrics | undefined {
return this.baselineMetrics.get(name);
}
/**
* Get benchmark errors
*/
getErrors(): OptimizationError[] {
return [...this.errors];
}
/**
* Measure performance of a single compilation
*/
private async measurePerformance(source: string, componentName: string): Promise<PerformanceMetrics> {
const startTime = performance.now();
let memoryUsage = 0;
if (this.config.measureMemory) {
memoryUsage = this.getMemoryUsage();
}
// Measure lexical analysis
const lexingStart = performance.now();
const lexer = new OrdoJSLexer(source);
const tokenStream = lexer.tokenize();
const lexingTime = performance.now() - lexingStart;
// Measure parsing
const parsingStart = performance.now();
const parser = new OrdoJSParser(tokenStream);
const ast = parser.parse();
const parsingTime = performance.now() - parsingStart;
// Measure code generation
const generationStart = performance.now();
const generator = new OrdoJSCodeGenerator();
const generatedCode = generator.generate(ast);
const codeGenerationTime = performance.now() - generationStart;
// Measure optimization
const optimizationStart = performance.now();
const eliminator = new DeadCodeEliminator();
const optimizedAST = eliminator.optimize(ast);
const optimizationTime = performance.now() - optimizationStart;
// Calculate bundle sizes
const bundleSize = this.calculateBundleSize(generatedCode);
const gzippedSize = this.config.includeCompression ? this.calculateGzippedSize(generatedCode) : 0;
const brotliSize = this.config.includeCompression ? this.calculateBrotliSize(generatedCode) : 0;
// Count AST nodes and tokens
const astNodeCount = this.countASTNodes(ast.component);
const tokenCount = tokenStream.tokens.length;
// Measure runtime performance if enabled
let runtimeMetrics: RuntimeMetrics | undefined;
if (this.config.includeRuntime) {
runtimeMetrics = await this.measureRuntimePerformance(generatedCode, componentName);
}
const totalTime = performance.now() - startTime;
return {
compilationTime: totalTime,
lexingTime,
parsingTime,
codeGenerationTime,
optimizationTime,
bundleSize,
gzippedSize,
brotliSize,
astNodeCount,
tokenCount,
memoryUsage,
runtimeMetrics
};
}
/**
* Calculate average metrics across measurements
*/
private calculateAverageMetrics(measurements: PerformanceMetrics[]): PerformanceMetrics {
const sum = measurements.reduce((acc, metrics) => ({
compilationTime: acc.compilationTime + metrics.compilationTime,
lexingTime: acc.lexingTime + metrics.lexingTime,
parsingTime: acc.parsingTime + metrics.parsingTime,
codeGenerationTime: acc.codeGenerationTime + metrics.codeGenerationTime,
optimizationTime: acc.optimizationTime + metrics.optimizationTime,
bundleSize: acc.bundleSize + metrics.bundleSize,
gzippedSize: acc.gzippedSize + metrics.gzippedSize,
brotliSize: acc.brotliSize + metrics.brotliSize,
astNodeCount: acc.astNodeCount + metrics.astNodeCount,
tokenCount: acc.tokenCount + metrics.tokenCount,
memoryUsage: acc.memoryUsage + metrics.memoryUsage,
hydrationTime: (acc.hydrationTime || 0) + (metrics.hydrationTime || 0),
runtimeMetrics: acc.runtimeMetrics
}), {
compilationTime: 0,
lexingTime: 0,
parsingTime: 0,
codeGenerationTime: 0,
optimizationTime: 0,
bundleSize: 0,
gzippedSize: 0,
brotliSize: 0,
astNodeCount: 0,
tokenCount: 0,
memoryUsage: 0,
hydrationTime: 0,
runtimeMetrics: undefined
});
const count = measurements.length;
return {
compilationTime: sum.compilationTime / count,
lexingTime: sum.lexingTime / count,
parsingTime: sum.parsingTime / count,
codeGenerationTime: sum.codeGenerationTime / count,
optimizationTime: sum.optimizationTime / count,
bundleSize: Math.round(sum.bundleSize / count),
gzippedSize: Math.round(sum.gzippedSize / count),
brotliSize: Math.round(sum.brotliSize / count),
astNodeCount: Math.round(sum.astNodeCount / count),
tokenCount: Math.round(sum.tokenCount / count),
memoryUsage: Math.round(sum.memoryUsage / count),
hydrationTime: sum.hydrationTime ? sum.hydrationTime / count : undefined,
runtimeMetrics: sum.runtimeMetrics
};
}
/**
* Validate performance against thresholds
*/
private validatePerformance(metrics: PerformanceMetrics): PerformanceValidation {
const failures: string[] = [];
let score = 100;
// Check compilation time
if (metrics.compilationTime > this.config.thresholds.maxCompilationTime) {
failures.push(`Compilation time (${metrics.compilationTime.toFixed(2)}ms) exceeds threshold (${this.config.thresholds.maxCompilationTime}ms)`);
score -= 20;
}
// Check bundle size
if (metrics.bundleSize > this.config.thresholds.maxBundleSize) {
failures.push(`Bundle size (${metrics.bundleSize} bytes) exceeds threshold (${this.config.thresholds.maxBundleSize} bytes)`);
score -= 25;
}
// Check hydration time
if (metrics.hydrationTime && metrics.hydrationTime > this.config.thresholds.maxHydrationTime) {
failures.push(`Hydration time (${metrics.hydrationTime.toFixed(2)}ms) exceeds threshold (${this.config.thresholds.maxHydrationTime}ms)`);
score -= 15;
}
// Check runtime metrics if available
if (metrics.runtimeMetrics) {
if (metrics.runtimeMetrics.timeToFirstPaint > this.config.thresholds.maxTimeToFirstPaint) {
failures.push(`Time to first paint (${metrics.runtimeMetrics.timeToFirstPaint}ms) exceeds threshold (${this.config.thresholds.maxTimeToFirstPaint}ms)`);
score -= 20;
}
if (metrics.runtimeMetrics.timeToInteractive > this.config.thresholds.maxTimeToInteractive) {
failures.push(`Time to interactive (${metrics.runtimeMetrics.timeToInteractive}ms) exceeds threshold (${this.config.thresholds.maxTimeToInteractive}ms)`);
score -= 20;
}
}
return {
passed: failures.length === 0,
failures,
score: Math.max(0, score)
};
}
/**
* Generate optimization suggestions
*/
private generateSuggestions(metrics: PerformanceMetrics, validation: PerformanceValidation): string[] {
const suggestions: string[] = [];
if (metrics.compilationTime > this.config.thresholds.maxCompilationTime * 0.8) {
suggestions.push('Consider optimizing lexer and parser for faster compilation');
}
if (metrics.bundleSize > this.config.thresholds.maxBundleSize * 0.8) {
suggestions.push('Enable aggressive tree shaking to reduce bundle size');
suggestions.push('Consider code splitting for large components');
}
if (metrics.astNodeCount > 1000) {
suggestions.push('Component has many AST nodes - consider simplifying the component structure');
}
if (metrics.tokenCount > 5000) {
suggestions.push('Large token count detected - consider optimizing the source code');
}
if (metrics.optimizationTime > metrics.compilationTime * 0.3) {
suggestions.push('Optimization is taking significant time - consider adjusting optimization settings');
}
if (metrics.gzippedSize > metrics.bundleSize * 0.7) {
suggestions.push('Bundle has poor compression ratio - consider code optimization');
}
return suggestions;
}
/**
* Compare with baseline metrics
*/
private compareWithBaseline(current: PerformanceMetrics, baseline: PerformanceMetrics): PerformanceComparison {
const calculateImprovement = (current: number, baseline: number): number => {
return ((baseline - current) / baseline) * 100;
};
return {
baseline,
current,
improvement: {
compilationTime: calculateImprovement(current.compilationTime, baseline.compilationTime),
bundleSize: calculateImprovement(current.bundleSize, baseline.bundleSize),
hydrationTime: current.hydrationTime && baseline.hydrationTime
? calculateImprovement(current.hydrationTime, baseline.hydrationTime)
: 0,
overall: calculateImprovement(
(current.compilationTime + current.bundleSize / 1000),
(baseline.compilationTime + baseline.bundleSize / 1000)
)
}
};
}
/**
* Calculate bundle size
*/
private calculateBundleSize(generatedCode: GeneratedCode): number {
let size = 0;
if (generatedCode.client) size += new TextEncoder().encode(generatedCode.client).length;
if (generatedCode.server) size += new TextEncoder().encode(generatedCode.server).length;
if (generatedCode.html) size += new TextEncoder().encode(generatedCode.html).length;
if (generatedCode.css) size += new TextEncoder().encode(generatedCode.css).length;
return size;
}
/**
* Calculate gzipped size (simplified)
*/
private calculateGzippedSize(generatedCode: GeneratedCode): number {
// This is a simplified calculation - in a real implementation, you'd use actual gzip compression
const bundleSize = this.calculateBundleSize(generatedCode);
return Math.round(bundleSize * 0.3); // Assume 70% compression ratio
}
/**
* Calculate Brotli size (simplified)
*/
private calculateBrotliSize(generatedCode: GeneratedCode): number {
// This is a simplified calculation - in a real implementation, you'd use actual Brotli compression
const bundleSize = this.calculateBundleSize(generatedCode);
return Math.round(bundleSize * 0.25); // Assume 75% compression ratio
}
/**
* Count AST nodes
*/
private countASTNodes(component: any): number {
let count = 1; // Count the component itself
const countNodes = (node: any): void => {
if (node.children) {
count += node.children.length;
node.children.forEach(countNodes);
}
if (node.body) {
count += node.body.length;
node.body.forEach(countNodes);
}
if (node.elements) {
count += node.elements.length;
node.elements.forEach(countNodes);
}
if (node.reactiveVariables) {
count += node.reactiveVariables.length;
}
if (node.functions) {
count += node.functions.length;
}
if (node.eventHandlers) {
count += node.eventHandlers.length;
}
};
countNodes(component);
return count;
}
/**
* Get memory usage
*/
private getMemoryUsage(): number {
if (typeof process !== 'undefined' && process.memoryUsage) {
return process.memoryUsage().heapUsed;
}
return 0;
}
/**
* Measure runtime performance
*/
private async measureRuntimePerformance(generatedCode: GeneratedCode, componentName: string): Promise<RuntimeMetrics> {
// This is a simplified runtime measurement
// In a real implementation, you'd run the component in a browser environment
const startTime = performance.now();
// Simulate runtime measurements
const timeToFirstPaint = Math.random() * 1000 + 500; // 500-1500ms
const timeToInteractive = timeToFirstPaint + Math.random() * 1000 + 500; // Additional 500-1500ms
const runtimeMemoryUsage = Math.random() * 1000000 + 500000; // 500KB-1.5MB
const domOperations = Math.floor(Math.random() * 100) + 10; // 10-110 operations
const reactiveUpdateTime = Math.random() * 50 + 10; // 10-60ms
return {
timeToFirstPaint,
timeToInteractive,
runtimeMemoryUsage,
domOperations,
reactiveUpdateTime
};
}
/**
* Generate performance report
*/
generateReport(result: BenchmarkResult): string {
const { averageMetrics, validation, suggestions, comparison } = result;
let report = `# Performance Report: ${result.componentName}\n\n`;
// Compilation metrics
report += `## Compilation Metrics\n`;
report += `- Compilation Time: ${averageMetrics.compilationTime.toFixed(2)}ms\n`;
report += `- Lexing Time: ${averageMetrics.lexingTime.toFixed(2)}ms\n`;
report += `- Parsing Time: ${averageMetrics.parsingTime.toFixed(2)}ms\n`;
report += `- Code Generation Time: ${averageMetrics.codeGenerationTime.toFixed(2)}ms\n`;
report += `- Optimization Time: ${averageMetrics.optimizationTime.toFixed(2)}ms\n\n`;
// Bundle metrics
report += `## Bundle Metrics\n`;
report += `- Bundle Size: ${averageMetrics.bundleSize} bytes\n`;
report += `- Gzipped Size: ${averageMetrics.gzippedSize} bytes\n`;
report += `- Brotli Size: ${averageMetrics.brotliSize} bytes\n`;
report += `- AST Nodes: ${averageMetrics.astNodeCount}\n`;
report += `- Tokens: ${averageMetrics.tokenCount}\n\n`;
// Runtime metrics
if (averageMetrics.runtimeMetrics) {
report += `## Runtime Metrics\n`;
report += `- Time to First Paint: ${averageMetrics.runtimeMetrics.timeToFirstPaint.toFixed(2)}ms\n`;
report += `- Time to Interactive: ${averageMetrics.runtimeMetrics.timeToInteractive.toFixed(2)}ms\n`;
report += `- Runtime Memory: ${averageMetrics.runtimeMetrics.runtimeMemoryUsage} bytes\n`;
report += `- DOM Operations: ${averageMetrics.runtimeMetrics.domOperations}\n`;
report += `- Reactive Update Time: ${averageMetrics.runtimeMetrics.reactiveUpdateTime.toFixed(2)}ms\n\n`;
}
// Validation results
report += `## Performance Validation\n`;
report += `- Status: ${validation.passed ? '✅ PASSED' : '❌ FAILED'}\n`;
report += `- Score: ${validation.score}/100\n`;
if (validation.failures.length > 0) {
report += `- Failures:\n`;
validation.failures.forEach(failure => {
report += ` - ${failure}\n`;
});
}
report += `\n`;
// Suggestions
if (suggestions.length > 0) {
report += `## Optimization Suggestions\n`;
suggestions.forEach(suggestion => {
report += `- ${suggestion}\n`;
});
report += `\n`;
}
// Comparison
if (comparison) {
report += `## Comparison with Baseline\n`;
report += `- Compilation Time: ${comparison.improvement.compilationTime.toFixed(2)}% ${comparison.improvement.compilationTime > 0 ? 'improvement' : 'degradation'}\n`;
report += `- Bundle Size: ${comparison.improvement.bundleSize.toFixed(2)}% ${comparison.improvement.bundleSize > 0 ? 'improvement' : 'degradation'}\n`;
report += `- Overall: ${comparison.improvement.overall.toFixed(2)}% ${comparison.improvement.overall > 0 ? 'improvement' : 'degradation'}\n`;
}
return report;
}
}