Coverage for fastblocks/adapters/templates/_performance_optimizer.py: 81%
139 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-09 00:47 -0700
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-09 00:47 -0700
1"""Performance optimizer for FastBlocks template rendering."""
3import operator
4import time
5from collections import defaultdict, deque
6from contextlib import suppress
7from dataclasses import dataclass, field
8from typing import Any
9from uuid import UUID
11from acb.depends import depends
14@dataclass
15class PerformanceMetrics:
16 """Performance metrics for template rendering."""
18 render_time: float
19 cache_hit: bool
20 template_size: int
21 context_size: int
22 fragment_count: int = 0
23 memory_usage: int = 0
24 concurrent_renders: int = 1
27@dataclass
28class PerformanceStats:
29 """Aggregated performance statistics."""
31 total_renders: int = 0
32 avg_render_time: float = 0.0
33 cache_hit_ratio: float = 0.0
34 slowest_templates: dict[str, float] = field(default_factory=dict)
35 fastest_templates: dict[str, float] = field(default_factory=dict)
36 memory_peak: int = 0
37 concurrent_peak: int = 0
40class PerformanceOptimizer:
41 """Template rendering performance optimizer."""
43 # Required ACB 0.19.0+ metadata
44 MODULE_ID: UUID = UUID("01937d87-b123-4567-89ab-123456789def")
45 MODULE_STATUS: str = "stable"
47 def __init__(self) -> None:
48 """Initialize performance optimizer."""
49 self.metrics_history: deque[dict[str, Any]] = deque(maxlen=1000)
50 self.template_stats: dict[str, list[float]] = defaultdict(list)
51 self.cache_stats: dict[str, int] = defaultdict(int)
52 self.concurrent_renders: int = 0
53 self.optimization_enabled: bool = True
55 # Register with ACB
56 with suppress(Exception):
57 depends.set(self)
59 def record_render(self, template_name: str, metrics: PerformanceMetrics) -> None:
60 """Record template rendering metrics."""
61 if not self.optimization_enabled:
62 return
64 # Store metrics
65 self.metrics_history.append(
66 {"template": template_name, "timestamp": time.time(), "metrics": metrics}
67 )
69 # Update template-specific stats
70 self.template_stats[template_name].append(metrics.render_time)
72 # Update cache stats
73 cache_key = f"{template_name}_cache"
74 if metrics.cache_hit:
75 self.cache_stats[f"{cache_key}_hits"] += 1
76 else:
77 self.cache_stats[f"{cache_key}_misses"] += 1
79 # Track concurrent renders
80 self.concurrent_renders = max(
81 self.concurrent_renders, metrics.concurrent_renders
82 )
84 def get_performance_stats(self) -> PerformanceStats:
85 """Get aggregated performance statistics."""
86 if not self.metrics_history:
87 return PerformanceStats()
89 total_renders = len(self.metrics_history)
90 total_render_time = sum(
91 entry["metrics"].render_time for entry in self.metrics_history
92 )
93 avg_render_time = total_render_time / total_renders
95 # Cache hit ratio
96 total_hits = sum(
97 count for key, count in self.cache_stats.items() if key.endswith("_hits")
98 )
99 total_requests = sum(self.cache_stats.values())
100 cache_hit_ratio = total_hits / total_requests if total_requests > 0 else 0.0
102 # Template performance analysis
103 slowest_templates: dict[str, Any] = {}
104 fastest_templates: dict[str, Any] = {}
106 for template, times in self.template_stats.items():
107 if times:
108 avg_time = sum(times) / len(times)
109 if len(slowest_templates) < 5:
110 slowest_templates[template] = avg_time
111 elif avg_time > min(slowest_templates.values()):
112 # Replace slowest if this is slower
113 min_key = min(
114 slowest_templates.items(), key=operator.itemgetter(1)
115 )[0]
116 del slowest_templates[min_key]
117 slowest_templates[template] = avg_time
119 if len(fastest_templates) < 5:
120 fastest_templates[template] = avg_time
121 elif avg_time < max(fastest_templates.values()):
122 # Replace fastest if this is faster
123 max_key = max(
124 fastest_templates.items(), key=operator.itemgetter(1)
125 )[0]
126 del fastest_templates[max_key]
127 fastest_templates[template] = avg_time
129 return PerformanceStats(
130 total_renders=total_renders,
131 avg_render_time=avg_render_time,
132 cache_hit_ratio=cache_hit_ratio,
133 slowest_templates=slowest_templates,
134 fastest_templates=fastest_templates,
135 concurrent_peak=self.concurrent_renders,
136 )
138 def get_optimization_recommendations(self) -> list[str]:
139 """Get performance optimization recommendations."""
140 recommendations = []
141 stats = self.get_performance_stats()
143 # Cache optimization
144 if stats.cache_hit_ratio < 0.7:
145 recommendations.append(
146 f"Cache hit ratio is {stats.cache_hit_ratio:.1%}. "
147 "Consider increasing cache TTL or improving cache keys."
148 )
150 # Slow template analysis
151 if stats.avg_render_time > 0.1: # 100ms
152 recommendations.append(
153 f"Average render time is {stats.avg_render_time:.3f}s. "
154 "Consider template optimization or caching strategies."
155 )
157 # Identify problematic templates
158 for template, avg_time in stats.slowest_templates.items():
159 if avg_time > 0.2: # 200ms
160 recommendations.append(
161 f"Template '{template}' averages {avg_time:.3f}s. "
162 "Consider breaking into fragments or optimizing logic."
163 )
165 # Concurrent rendering
166 if stats.concurrent_peak > 50:
167 recommendations.append(
168 f"Peak concurrent renders: {stats.concurrent_peak}. "
169 "Consider implementing render queuing or rate limiting."
170 )
172 return recommendations
174 async def optimize_render_context(
175 self, template_name: str, context: dict[str, Any]
176 ) -> dict[str, Any]:
177 """Optimize render context for better performance."""
178 if not self.optimization_enabled:
179 return context
181 optimized_context = context.copy()
183 # Historical performance analysis
184 if template_name in self.template_stats:
185 avg_time = sum(self.template_stats[template_name]) / len(
186 self.template_stats[template_name]
187 )
189 # For slow templates, optimize context
190 if avg_time > 0.1:
191 # Limit large collections (iterate over copy to avoid RuntimeError)
192 items_to_process = list(optimized_context.items())
193 for key, value in items_to_process:
194 if isinstance(value, list | tuple) and len(value) > 100:
195 optimized_context[f"{key}_paginated"] = True
196 optimized_context[f"{key}_total"] = len(value)
197 optimized_context[key] = value[:50] # Paginate large lists
199 # Convert complex objects to simpler representations
200 elif hasattr(value, "__dict__") and len(value.__dict__) > 20:
201 optimized_context[f"{key}_summary"] = True
203 return optimized_context
205 def should_enable_streaming(self, template_name: str, context_size: int) -> bool:
206 """Determine if streaming should be enabled for this render."""
207 if not self.optimization_enabled:
208 return False
210 # Enable streaming for large contexts
211 if context_size > 50000: # 50KB
212 return True
214 # Historical analysis
215 if template_name in self.template_stats:
216 times = self.template_stats[template_name]
217 if times and max(times) > 0.5: # 500ms
218 return True
220 return False
222 def get_optimal_cache_ttl(self, template_name: str) -> int:
223 """Get optimal cache TTL for a template."""
224 if not self.optimization_enabled:
225 return 300 # Default 5 minutes
227 # Analysis based on template usage patterns
228 if template_name in self.template_stats:
229 times = self.template_stats[template_name]
230 if times:
231 avg_time = sum(times) / len(times)
233 # Slower templates get longer cache TTL
234 if avg_time > 0.2:
235 return 1800 # 30 minutes
236 elif avg_time > 0.1:
237 return 900 # 15 minutes
239 return 300 # 5 minutes
241 return 300
243 def clear_stats(self) -> None:
244 """Clear all performance statistics."""
245 self.metrics_history.clear()
246 self.template_stats.clear()
247 self.cache_stats.clear()
248 self.concurrent_renders = 0
250 def export_metrics(self) -> dict[str, Any]:
251 """Export metrics for external monitoring."""
252 stats = self.get_performance_stats()
254 return {
255 "performance_stats": {
256 "total_renders": stats.total_renders,
257 "avg_render_time": stats.avg_render_time,
258 "cache_hit_ratio": stats.cache_hit_ratio,
259 "concurrent_peak": stats.concurrent_peak,
260 },
261 "template_performance": {
262 "slowest": stats.slowest_templates,
263 "fastest": stats.fastest_templates,
264 },
265 "recommendations": self.get_optimization_recommendations(),
266 "timestamp": time.time(),
267 }
270# Global performance optimizer instance
271_performance_optimizer = None
274def get_performance_optimizer() -> PerformanceOptimizer:
275 """Get global performance optimizer instance."""
276 global _performance_optimizer
277 if _performance_optimizer is None:
278 _performance_optimizer = PerformanceOptimizer()
279 return _performance_optimizer