package templates import ( "bytes" "fmt" "sync" "text/template" "time" ) // TemplateEngine pre-compiles and caches Go templates for fast rendering type TemplateEngine struct { mu sync.RWMutex cache map[string]*CachedTemplate maxSize int compileStats map[string]*CompileStats } // CachedTemplate holds a compiled template with metrics type CachedTemplate struct { Template *template.Template CompiledAt time.Time RenderCount int RenderTime time.Duration } // CompileStats tracks compilation statistics type CompileStats struct { TemplateName string CompileTime time.Duration CompiledAt time.Time RenderCount int TotalRenderTime time.Duration AvgRenderTime time.Duration } // NewTemplateEngine creates a new template engine func NewTemplateEngine(maxSize int) *TemplateEngine { if maxSize <= 0 { maxSize = 100 } return &TemplateEngine{ cache: make(map[string]*CachedTemplate), maxSize: maxSize, compileStats: make(map[string]*CompileStats), } } // Compile compiles and caches a template func (te *TemplateEngine) Compile(name string, templateStr string) (*template.Template, error) { te.mu.Lock() defer te.mu.Unlock() // Check if already cached if cached, exists := te.cache[name]; exists { return cached.Template, nil } // Compile the template startTime := time.Now() tmpl, err := template.New(name).Parse(templateStr) compileTime := time.Since(startTime) if err != nil { return nil, err } // Check size limit if len(te.cache) >= te.maxSize { // Simple FIFO eviction var oldestName string var oldestTime time.Time for n, t := range te.cache { if oldestTime.IsZero() || t.CompiledAt.Before(oldestTime) { oldestName = n oldestTime = t.CompiledAt } } if oldestName != "" { delete(te.cache, oldestName) delete(te.compileStats, oldestName) } } // Cache the compiled template cached := &CachedTemplate{ Template: tmpl, CompiledAt: time.Now(), } te.cache[name] = cached // Track compilation stats te.compileStats[name] = &CompileStats{ TemplateName: name, CompileTime: compileTime, CompiledAt: time.Now(), } return tmpl, nil } // Render renders a cached template with the given data func (te *TemplateEngine) Render(name string, data interface{}) (string, error) { te.mu.RLock() cached, exists := te.cache[name] te.mu.RUnlock() if !exists { return "", fmt.Errorf("template not found: %s", name) } // Render template startTime := time.Now() var buf bytes.Buffer err := cached.Template.Execute(&buf, data) renderTime := time.Since(startTime) if err != nil { return "", err } // Update stats te.mu.Lock() cached.RenderCount++ cached.RenderTime += renderTime if stats, exists := te.compileStats[name]; exists { stats.RenderCount++ stats.TotalRenderTime += renderTime if stats.RenderCount > 0 { stats.AvgRenderTime = stats.TotalRenderTime / time.Duration(stats.RenderCount) } } te.mu.Unlock() return buf.String(), nil } // CompileAndRender compiles (if not cached) and renders a template func (te *TemplateEngine) CompileAndRender(name string, templateStr string, data interface{}) (string, error) { _, err := te.Compile(name, templateStr) if err != nil { return "", err } return te.Render(name, data) } // GetStats returns compilation statistics func (te *TemplateEngine) GetStats(name string) (*CompileStats, bool) { te.mu.RLock() defer te.mu.RUnlock() stats, exists := te.compileStats[name] return stats, exists } // GetAllStats returns all compilation statistics func (te *TemplateEngine) GetAllStats() map[string]*CompileStats { te.mu.RLock() defer te.mu.RUnlock() statsCopy := make(map[string]*CompileStats) for name, stats := range te.compileStats { statsCopy[name] = stats } return statsCopy } // Clear clears all cached templates func (te *TemplateEngine) Clear() { te.mu.Lock() defer te.mu.Unlock() te.cache = make(map[string]*CachedTemplate) te.compileStats = make(map[string]*CompileStats) } // CacheSize returns the current cache size func (te *TemplateEngine) CacheSize() int { te.mu.RLock() defer te.mu.RUnlock() return len(te.cache) } // IsCached checks if a template is cached func (te *TemplateEngine) IsCached(name string) bool { te.mu.RLock() defer te.mu.RUnlock() _, exists := te.cache[name] return exists } // Remove removes a template from cache func (te *TemplateEngine) Remove(name string) { te.mu.Lock() defer te.mu.Unlock() delete(te.cache, name) delete(te.compileStats, name) } // GetCacheStats returns overall cache statistics func (te *TemplateEngine) GetCacheStats() map[string]interface{} { te.mu.RLock() defer te.mu.RUnlock() totalRenders := 0 totalRenderTime := time.Duration(0) for _, stats := range te.compileStats { totalRenders += stats.RenderCount totalRenderTime += stats.TotalRenderTime } avgRenderTime := time.Duration(0) if totalRenders > 0 { avgRenderTime = totalRenderTime / time.Duration(totalRenders) } return map[string]interface{}{ "cache_size": len(te.cache), "max_size": te.maxSize, "total_renders": totalRenders, "total_render_time": totalRenderTime, "avg_render_time": avgRenderTime, "usage_ratio": float64(len(te.cache)) / float64(te.maxSize), } }