Files

237 lines
5.2 KiB
Go
Raw Permalink Normal View History

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),
}
}