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

1"""Performance optimizer for FastBlocks template rendering.""" 

2 

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 

10 

11from acb.depends import depends 

12 

13 

14@dataclass 

15class PerformanceMetrics: 

16 """Performance metrics for template rendering.""" 

17 

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 

25 

26 

27@dataclass 

28class PerformanceStats: 

29 """Aggregated performance statistics.""" 

30 

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 

38 

39 

40class PerformanceOptimizer: 

41 """Template rendering performance optimizer.""" 

42 

43 # Required ACB 0.19.0+ metadata 

44 MODULE_ID: UUID = UUID("01937d87-b123-4567-89ab-123456789def") 

45 MODULE_STATUS: str = "stable" 

46 

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 

54 

55 # Register with ACB 

56 with suppress(Exception): 

57 depends.set(self) 

58 

59 def record_render(self, template_name: str, metrics: PerformanceMetrics) -> None: 

60 """Record template rendering metrics.""" 

61 if not self.optimization_enabled: 

62 return 

63 

64 # Store metrics 

65 self.metrics_history.append( 

66 {"template": template_name, "timestamp": time.time(), "metrics": metrics} 

67 ) 

68 

69 # Update template-specific stats 

70 self.template_stats[template_name].append(metrics.render_time) 

71 

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 

78 

79 # Track concurrent renders 

80 self.concurrent_renders = max( 

81 self.concurrent_renders, metrics.concurrent_renders 

82 ) 

83 

84 def get_performance_stats(self) -> PerformanceStats: 

85 """Get aggregated performance statistics.""" 

86 if not self.metrics_history: 

87 return PerformanceStats() 

88 

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 

94 

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 

101 

102 # Template performance analysis 

103 slowest_templates: dict[str, Any] = {} 

104 fastest_templates: dict[str, Any] = {} 

105 

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 

118 

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 

128 

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 ) 

137 

138 def get_optimization_recommendations(self) -> list[str]: 

139 """Get performance optimization recommendations.""" 

140 recommendations = [] 

141 stats = self.get_performance_stats() 

142 

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 ) 

149 

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 ) 

156 

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 ) 

164 

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 ) 

171 

172 return recommendations 

173 

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 

180 

181 optimized_context = context.copy() 

182 

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 ) 

188 

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 

198 

199 # Convert complex objects to simpler representations 

200 elif hasattr(value, "__dict__") and len(value.__dict__) > 20: 

201 optimized_context[f"{key}_summary"] = True 

202 

203 return optimized_context 

204 

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 

209 

210 # Enable streaming for large contexts 

211 if context_size > 50000: # 50KB 

212 return True 

213 

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 

219 

220 return False 

221 

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 

226 

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) 

232 

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 

238 

239 return 300 # 5 minutes 

240 

241 return 300 

242 

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 

249 

250 def export_metrics(self) -> dict[str, Any]: 

251 """Export metrics for external monitoring.""" 

252 stats = self.get_performance_stats() 

253 

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 } 

268 

269 

270# Global performance optimizer instance 

271_performance_optimizer = None 

272 

273 

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