diff --git a/BENCHMARKING.md b/BENCHMARKING.md new file mode 100644 index 0000000..0dfdc7e --- /dev/null +++ b/BENCHMARKING.md @@ -0,0 +1,88 @@ +# Memory Allocation Analysis for Zero-Allocation Rendering + +This guide explains how to use the memory profiling tools provided to identify and optimize memory allocation hotspots in the Twig template engine. + +## Running the Memory Analysis + +To run a comprehensive memory analysis and generate reports: + +```bash +./scripts/analyze_memory.sh +``` + +This script will: +1. Run benchmarks with memory allocation tracking +2. Generate heap allocation profiles +3. Analyze the profiles to identify allocation hotspots +4. Create a comprehensive report in the `reports/` directory + +## Interpreting the Results + +The analysis generates several files: + +- `reports/memory_optimization_report.md` - Main report with allocation analysis and recommendations +- `reports/benchmark_results.txt` - Raw benchmark results with memory statistics +- `reports/top_allocations.txt` - Top memory allocation sources from pprof +- `reports/heap.prof` - Heap allocation profile that can be analyzed with `go tool pprof` +- `reports/simple_profile.txt`, `reports/medium_profile.txt`, `reports/complex_profile.txt` - Profile results by template complexity + +## Using Individual Profiling Tools + +### Running Benchmarks with Memory Stats + +```bash +go test -run=^$ -bench=. -benchmem ./memory_profile_test.go +``` + +This command runs all benchmarks and reports allocations per operation. + +### Generating a Heap Profile + +```bash +go test -run=^$ -bench=BenchmarkRenderComplexTemplate -benchmem -memprofile=heap.prof ./memory_profile_test.go +``` + +### Analyzing the Heap Profile + +```bash +go tool pprof -alloc_space heap.prof +``` + +Common pprof commands: +- `top` - Show top allocation sources +- `list FunctionName` - Show line-by-line allocations in a function +- `web` - Open a web visualization of the profile + +### Using the Profiling Tool + +For targeted profiling of specific template complexity: + +```bash +go run cmd/profile/main.go -complexity=3 -iterations=1000 -memprofile=complex.prof +``` + +Options: +- `-complexity` - Template complexity level (1=simple, 2=medium, 3=complex) +- `-iterations` - Number of template renders to perform +- `-memprofile` - Output file for memory profile +- `-cpuprofile` - Output file for CPU profile (optional) + +## Zero-Allocation Implementation Strategy + +Based on the profile results, implement optimizations in this order: + +1. **Object Pooling** - Implement pools for all temporary objects +2. **String Operations** - Optimize string handling to avoid allocations +3. **Context Management** - Improve context creation, cloning, and cleanup +4. **Expression Evaluation** - Minimize allocations in expression execution +5. **Buffer Management** - Reuse output buffers with proper pooling + +## Testing Your Optimizations + +After each optimization, run the memory benchmarks again to verify the reduction in allocations: + +```bash +go test -run=^$ -bench=BenchmarkRender -benchmem ./memory_profile_test.go +``` + +The goal is to see zero (or minimal) allocations per operation in the `allocs/op` column. \ No newline at end of file diff --git a/cmd/profile/main.go b/cmd/profile/main.go new file mode 100644 index 0000000..89d7a92 --- /dev/null +++ b/cmd/profile/main.go @@ -0,0 +1,302 @@ +// Package main provides a simple utility to run memory profiling on the Twig template engine +package main + +import ( + "bytes" + "flag" + "fmt" + "log" + "os" + "runtime" + "runtime/pprof" + "strconv" + "time" + + "github.com/semihalev/twig" +) + +func main() { + // Command-line flags + cpuProfile := flag.String("cpuprofile", "", "write cpu profile to file") + memProfile := flag.String("memprofile", "", "write memory profile to file") + complexityLevel := flag.Int("complexity", 2, "template complexity level (1-3)") + iterations := flag.Int("iterations", 1000, "number of template renders to perform") + flag.Parse() + + // CPU profiling if requested + if *cpuProfile != "" { + f, err := os.Create(*cpuProfile) + if err != nil { + log.Fatal("could not create CPU profile: ", err) + } + defer f.Close() + if err := pprof.StartCPUProfile(f); err != nil { + log.Fatal("could not start CPU profile: ", err) + } + defer pprof.StopCPUProfile() + } + + // Run the appropriate benchmark based on complexity level + var totalTime time.Duration + var totalAllocBytes uint64 + var numGC uint32 + + // Record memory stats before + var memStatsBefore runtime.MemStats + runtime.ReadMemStats(&memStatsBefore) + + switch *complexityLevel { + case 1: + totalTime = runSimpleTemplateBenchmark(*iterations) + case 2: + totalTime = runMediumTemplateBenchmark(*iterations) + case 3: + totalTime = runComplexTemplateBenchmark(*iterations) + default: + log.Fatalf("Invalid complexity level: %d (must be 1-3)", *complexityLevel) + } + + // Record memory stats after + var memStatsAfter runtime.MemStats + runtime.ReadMemStats(&memStatsAfter) + + // Calculate allocation statistics + totalAllocBytes = memStatsAfter.TotalAlloc - memStatsBefore.TotalAlloc + numGC = memStatsAfter.NumGC - memStatsBefore.NumGC + + // Report results + fmt.Printf("=== Twig Memory Profiling Results ===\n") + fmt.Printf("Complexity Level: %d\n", *complexityLevel) + fmt.Printf("Total Iterations: %d\n", *iterations) + fmt.Printf("Total Time: %v\n", totalTime) + fmt.Printf("Time per Iteration: %v\n", totalTime/time.Duration(*iterations)) + fmt.Printf("Total Memory Allocated: %d bytes\n", totalAllocBytes) + fmt.Printf("Memory per Iteration: %d bytes\n", totalAllocBytes/uint64(*iterations)) + fmt.Printf("Number of GCs: %d\n", numGC) + + // Memory profiling if requested + if *memProfile != "" { + f, err := os.Create(*memProfile) + if err != nil { + log.Fatal("could not create memory profile: ", err) + } + defer f.Close() + runtime.GC() // Get up-to-date statistics + if err := pprof.WriteHeapProfile(f); err != nil { + log.Fatal("could not write memory profile: ", err) + } + } +} + +// runSimpleTemplateBenchmark runs a benchmark with a simple template +func runSimpleTemplateBenchmark(iterations int) time.Duration { + engine := twig.New() + err := engine.RegisterString("simple", "Hello, {{ name }}!") + if err != nil { + log.Fatalf("Error registering template: %v", err) + } + + context := map[string]interface{}{ + "name": "World", + } + + startTime := time.Now() + for i := 0; i < iterations; i++ { + var buf bytes.Buffer + template, _ := engine.Load("simple") + err := template.RenderTo(&buf, context) + if err != nil { + log.Fatalf("Error rendering template: %v", err) + } + } + return time.Since(startTime) +} + +// runMediumTemplateBenchmark runs a benchmark with a medium complexity template +func runMediumTemplateBenchmark(iterations int) time.Duration { + engine := twig.New() + templateContent := ` +
+

{{ user.name }}

+

Age: {{ user.age }}

+ {% if user.bio %} +
{{ user.bio|capitalize }}
+ {% else %} +
No bio available
+ {% endif %} + +
+` + err := engine.RegisterString("medium", templateContent) + if err != nil { + log.Fatalf("Error registering template: %v", err) + } + + context := map[string]interface{}{ + "user": map[string]interface{}{ + "name": "John Doe", + "age": 30, + "bio": "web developer and open source enthusiast", + "skills": []map[string]interface{}{ + {"name": "Go", "level": "Advanced"}, + {"name": "JavaScript", "level": "Intermediate"}, + {"name": "CSS", "level": "Beginner"}, + {"name": "HTML", "level": "Advanced"}, + }, + }, + } + + startTime := time.Now() + for i := 0; i < iterations; i++ { + var buf bytes.Buffer + template, _ := engine.Load("medium") + err := template.RenderTo(&buf, context) + if err != nil { + log.Fatalf("Error rendering template: %v", err) + } + } + return time.Since(startTime) +} + +// runComplexTemplateBenchmark runs a benchmark with a complex template +func runComplexTemplateBenchmark(iterations int) time.Duration { + engine := twig.New() + + // Base template + baseTemplate := ` + + + + {% block title %}Default Title{% endblock %} + {% block styles %} + + {% endblock %} + + +
{% block header %}Default Header{% endblock %}
+
{% block content %}Default Content{% endblock %}
+ + + +` + err := engine.RegisterString("base", baseTemplate) + if err != nil { + log.Fatalf("Error registering template: %v", err) + } + + // Macro template + macroTemplate := ` +{% macro renderProduct(product) %} +
+

{{ product.name }}

+

{{ product.description|capitalize }}

+
{{ product.price|format("$%.2f") }}
+ {% if product.tags %} +
+ {% for tag in product.tags %} + {{ tag }} + {% endfor %} +
+ {% endif %} +
+{% endmacro %} +` + err = engine.RegisterString("macros", macroTemplate) + if err != nil { + log.Fatalf("Error registering template: %v", err) + } + + // Page template + pageTemplate := ` +{% extends "base" %} +{% import "macros" as components %} + +{% block title %}{{ page.title }} - {{ parent() }}{% endblock %} + +{% block styles %} + {{ parent() }} + +{% endblock %} + +{% block header %} +

{{ page.title }}

+ +{% endblock %} + +{% block content %} +
+

Product List ({{ products|length }} items)

+ + {% for product in products %} + {{ components.renderProduct(product) }} + {% endfor %} + + {% set total = 0 %} + {% for product in products %} + {% set total = total + product.price %} + {% endfor %} + +
+

Total products: {{ products|length }}

+

Average price: {{ (total / products|length)|format("$%.2f") }}

+

Price range: {{ products|map(p => p.price)|sort|first|format("$%.2f") }} - {{ products|map(p => p.price)|sort|last|format("$%.2f") }}

+
+
+{% endblock %} +` + err = engine.RegisterString("page", pageTemplate) + if err != nil { + log.Fatalf("Error registering template: %v", err) + } + + // Create a complex context with various data types + products := make([]map[string]interface{}, 20) + for i := 0; i < 20; i++ { + products[i] = map[string]interface{}{ + "id": i + 1, + "name": "Product " + strconv.Itoa(i+1), + "description": "This is product " + strconv.Itoa(i+1) + " with detailed information.", + "price": 15.0 + float64(i)*1.5, + "tags": []string{"tag1", "tag2", "tag3"}[0:1+(i%3)], + } + } + + context := map[string]interface{}{ + "page": map[string]interface{}{ + "title": "Product Catalog", + }, + "navigation": []map[string]interface{}{ + {"url": "/", "text": "Home"}, + {"url": "/products", "text": "Products"}, + {"url": "/about", "text": "About"}, + {"url": "/contact", "text": "Contact"}, + }, + "products": products, + } + + startTime := time.Now() + for i := 0; i < iterations; i++ { + var buf bytes.Buffer + template, _ := engine.Load("page") + err := template.RenderTo(&buf, context) + if err != nil { + log.Fatalf("Error rendering template: %v", err) + } + } + return time.Since(startTime) +} \ No newline at end of file diff --git a/memory_analysis.go b/memory_analysis.go new file mode 100644 index 0000000..8195da5 --- /dev/null +++ b/memory_analysis.go @@ -0,0 +1,138 @@ +// This file contains a utility function to generate a memory allocation report +package twig + +import ( + "fmt" + "os" + "runtime/pprof" + "sort" + "strings" +) + +// RunMemoryAnalysis runs a comprehensive memory analysis on the template engine +// and generates a report of allocation hotspots. +func RunMemoryAnalysis() error { + // Create a memory profile output file + f, err := os.Create("twig_memory.pprof") + if err != nil { + return fmt.Errorf("failed to create memory profile: %v", err) + } + defer f.Close() + + // Write a memory profile with detailed allocation info + if err := pprof.WriteHeapProfile(f); err != nil { + return fmt.Errorf("failed to write memory profile: %v", err) + } + + // Generate a report based on the memory profile + reportFile, err := os.Create("twig_memory_report.txt") + if err != nil { + return fmt.Errorf("failed to create report file: %v", err) + } + defer reportFile.Close() + + // Header information + fmt.Fprintf(reportFile, "# TWIG MEMORY ALLOCATION REPORT\n\n") + fmt.Fprintf(reportFile, "This report shows memory allocation hotspots in the Twig template engine.\n") + fmt.Fprintf(reportFile, "Optimizing these areas will help achieve a zero-allocation rendering path.\n\n") + + // Instructions for viewing the profile + fmt.Fprintf(reportFile, "## How to View the Profile\n\n") + fmt.Fprintf(reportFile, "Run this command to analyze the memory profile:\n") + fmt.Fprintf(reportFile, "```\ngo tool pprof -alloc_space twig_memory.pprof\n```\n\n") + fmt.Fprintf(reportFile, "Common commands in the pprof interface:\n") + fmt.Fprintf(reportFile, "- `top`: Shows the top allocation sources\n") + fmt.Fprintf(reportFile, "- `list FunctionName`: Shows line-by-line allocations in a function\n") + fmt.Fprintf(reportFile, "- `web`: Opens a web browser with a visualization of the profile\n\n") + + // Benchmarking instructions + fmt.Fprintf(reportFile, "## Benchmark Results\n\n") + fmt.Fprintf(reportFile, "Run this command to see allocation statistics:\n") + fmt.Fprintf(reportFile, "```\ngo test -run=^$ -bench=. -benchmem ./memory_profile_test.go\n```\n\n") + + // Generate tables for common allocation sources + generateAllocationTables(reportFile) + + // Recommendation section + fmt.Fprintf(reportFile, "## Optimization Recommendations\n\n") + fmt.Fprintf(reportFile, "Based on common patterns in template engines, consider these areas for optimization:\n\n") + + recommendations := []string{ + "**Context Creation**: Pool and reuse RenderContext objects", + "**String Concatenation**: Replace with direct WriteString to output buffers", + "**Expression Evaluation**: Eliminate intermediate allocations during evaluation", + "**Filter Chain Evaluation**: Reuse filter result objects", + "**Map Creation**: Pre-size maps and reuse map objects where possible", + "**String Conversions**: Use allocation-free ToString implementations for common types", + "**Buffer Management**: Pool and reuse output buffers", + "**Node Creation**: Extend the node pool to cover all node types", + "**Slice Allocations**: Pre-allocate slices with expected capacity", + } + + for _, rec := range recommendations { + fmt.Fprintf(reportFile, "- %s\n", rec) + } + + // Implementation strategy + fmt.Fprintf(reportFile, "\n## Implementation Strategy\n\n") + fmt.Fprintf(reportFile, "1. **Start with high-impact areas**: Focus on the top allocation sources first\n") + fmt.Fprintf(reportFile, "2. **Implement pools for all temporary objects**: Especially RenderContext objects\n") + fmt.Fprintf(reportFile, "3. **Optimize string operations**: String handling is often a major source of allocations\n") + fmt.Fprintf(reportFile, "4. **Review all map/slice creations**: Pre-size collections where possible\n") + fmt.Fprintf(reportFile, "5. **Incremental testing**: Benchmark after each optimization to measure impact\n\n") + + fmt.Fprintf(reportFile, "## Final Notes\n\n") + fmt.Fprintf(reportFile, "Remember that some allocations are unavoidable, especially for dynamic templates.\n") + fmt.Fprintf(reportFile, "The goal is to eliminate allocations in the core rendering path, prioritizing the\n") + fmt.Fprintf(reportFile, "most frequent operations for maximum performance impact.\n") + + return nil +} + +// Helper function to generate allocation tables for common sources +func generateAllocationTables(w *os.File) { + // String Handling Table + fmt.Fprintf(w, "### String Operations\n\n") + fmt.Fprintf(w, "| Operation | Allocation Issue | Optimization |\n") + fmt.Fprintf(w, "|-----------|-----------------|-------------|\n") + stringOps := [][]string{ + {"String Concatenation", "Creates new strings", "Use WriteString to buffer"}, + {"Substring Operations", "Creates new strings", "Reuse byte slices when possible"}, + {"String Conversion", "Boxing/unboxing", "Specialized ToString methods"}, + {"Format/Replace", "Creates intermediate strings", "Write directly to output buffer"}, + } + for _, op := range stringOps { + fmt.Fprintf(w, "| %s | %s | %s |\n", op[0], op[1], op[2]) + } + fmt.Fprintf(w, "\n") + + // Context Operations Table + fmt.Fprintf(w, "### Context Operations\n\n") + fmt.Fprintf(w, "| Operation | Allocation Issue | Optimization |\n") + fmt.Fprintf(w, "|-----------|-----------------|-------------|\n") + contextOps := [][]string{ + {"Context Creation", "New map allocations", "Pool and reuse context objects"}, + {"Context Cloning", "Copying maps", "Selective copying or copy-on-write"}, + {"Variable Lookup", "Interface conversions", "Type-specialized getters"}, + {"Context Merging", "Map copies for scope", "Prototype or linked contexts"}, + } + for _, op := range contextOps { + fmt.Fprintf(w, "| %s | %s | %s |\n", op[0], op[1], op[2]) + } + fmt.Fprintf(w, "\n") + + // Node Operations Table + fmt.Fprintf(w, "### Node Operations\n\n") + fmt.Fprintf(w, "| Node Type | Allocation Issue | Optimization |\n") + fmt.Fprintf(w, "|-----------|-----------------|-------------|\n") + nodeOps := [][]string{ + {"Expression Nodes", "New node for each evaluation", "Pool and reuse node objects"}, + {"Filter Nodes", "Chained filter allocations", "Intermediate result pooling"}, + {"Loop Nodes", "Iterator allocations", "Reuse loop context and iterators"}, + {"Block Nodes", "Block context allocations", "Pool block contexts"}, + } + for _, op := range nodeOps { + fmt.Fprintf(w, "| %s | %s | %s |\n", op[0], op[1], op[2]) + } + fmt.Fprintf(w, "\n") +} \ No newline at end of file diff --git a/memory_profile_test.go b/memory_profile_test.go new file mode 100644 index 0000000..a8d0736 --- /dev/null +++ b/memory_profile_test.go @@ -0,0 +1,368 @@ +package twig + +import ( + "bytes" + "testing" +) + +// BenchmarkRenderSimpleTemplate benchmarks rendering a simple template with just variables +func BenchmarkRenderSimpleTemplate(b *testing.B) { + engine := New() + err := engine.RegisterString("simple", "Hello, {{ name }}!") + if err != nil { + b.Fatalf("Error registering template: %v", err) + } + + context := map[string]interface{}{ + "name": "World", + } + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + var buf bytes.Buffer + template, _ := engine.Load("simple") + _ = template.RenderTo(&buf, context) + } +} + +// BenchmarkRenderComplexTemplate benchmarks rendering a template with conditionals, loops, and filters +func BenchmarkRenderComplexTemplate(b *testing.B) { + engine := New() + templateContent := ` +
+

{{ title|upper }}

+ {% if showHeader %} +
Welcome, {{ user.name }}!
+ {% endif %} + + {% set total = 0 %} + {% for item in items %} + {% set total = total + item.price %} + {% endfor %} +
Total: {{ total|format("$%.2f") }}
+
+` + err := engine.RegisterString("complex", templateContent) + if err != nil { + b.Fatalf("Error registering template: %v", err) + } + + context := map[string]interface{}{ + "title": "Product List", + "showHeader": true, + "user": map[string]interface{}{ + "name": "John", + }, + "items": []map[string]interface{}{ + {"name": "Item 1", "price": 10.5}, + {"name": "Item 2", "price": 15.0}, + {"name": "Item 3", "price": 8.75}, + {"name": "Item 4", "price": 12.25}, + {"name": "Item 5", "price": 9.99}, + }, + } + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + var buf bytes.Buffer + template, _ := engine.Load("complex") + _ = template.RenderTo(&buf, context) + } +} + +// BenchmarkRenderMacros benchmarks rendering a template with macro definitions and calls +func BenchmarkRenderMacros(b *testing.B) { + engine := New() + templateContent := ` +{% macro input(name, value='', type='text') %} + +{% endmacro %} + +{% macro form(action, method='post') %} +
+ {{ _self.input('username', user.username) }} + {{ _self.input('password', '', 'password') }} + {{ _self.input('submit', 'Login', 'submit') }} +
+{% endmacro %} + +{{ _self.form('/login') }} +` + err := engine.RegisterString("macros", templateContent) + if err != nil { + b.Fatalf("Error registering template: %v", err) + } + + context := map[string]interface{}{ + "user": map[string]interface{}{ + "username": "johndoe", + }, + } + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + var buf bytes.Buffer + template, _ := engine.Load("macros") + _ = template.RenderTo(&buf, context) + } +} + +// BenchmarkRenderInheritance benchmarks rendering a template with inheritance +func BenchmarkRenderInheritance(b *testing.B) { + engine := New() + + // Base template + baseTemplate := ` + + + + {% block title %}Default Title{% endblock %} + + +
{% block header %}Default Header{% endblock %}
+
{% block content %}Default Content{% endblock %}
+ + + +` + err := engine.RegisterString("base", baseTemplate) + if err != nil { + b.Fatalf("Error registering template: %v", err) + } + + // Child template + childTemplate := ` +{% extends "base" %} + +{% block title %}{{ pageTitle }} - {{ parent() }}{% endblock %} + +{% block header %} +

{{ pageTitle }}

+ {{ parent() }} +{% endblock %} + +{% block content %} + {% for item in items %} +
{{ item }}
+ {% endfor %} +{% endblock %} +` + err = engine.RegisterString("child", childTemplate) + if err != nil { + b.Fatalf("Error registering template: %v", err) + } + + context := map[string]interface{}{ + "pageTitle": "Products Page", + "items": []string{ + "Product 1", + "Product 2", + "Product 3", + }, + } + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + var buf bytes.Buffer + template, _ := engine.Load("child") + _ = template.RenderTo(&buf, context) + } +} + +// BenchmarkRenderFilters benchmarks heavy use of filter chains +func BenchmarkRenderFilters(b *testing.B) { + engine := New() + templateContent := ` +{% set text = "Hello, this is some example text for filtering!" %} + +

{{ text|upper|trim }}

+

{{ text|lower|replace("example", "sample")|capitalize }}

+

{{ text|split(" ")|join("-")|upper }}

+

{{ text|length }}

+

{{ '2023-05-15'|date("Y-m-d") }}

+

{{ 123.456|number_format(2, ".", ",") }}

+

{{ ['a', 'b', 'c']|join(", ")|upper }}

+

{{ text|slice(7, 10)|capitalize }}

+

{{ text|replace({"example": "great", "text": "content"}) }}

+

{{ text|default("No text provided")|upper }}

+` + err := engine.RegisterString("filters", templateContent) + if err != nil { + b.Fatalf("Error registering template: %v", err) + } + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + var buf bytes.Buffer + template, _ := engine.Load("filters") + _ = template.RenderTo(&buf, nil) + } +} + +// BenchmarkRenderWithLargeContext benchmarks rendering with a large context +func BenchmarkRenderWithLargeContext(b *testing.B) { + engine := New() + templateContent := ` + +` + err := engine.RegisterString("large_context", templateContent) + if err != nil { + b.Fatalf("Error registering template: %v", err) + } + + // Create a large context with 100 users + users := make([]map[string]interface{}, 100) + for i := 0; i < 100; i++ { + users[i] = map[string]interface{}{ + "id": i + 1, + "name": "User " + string(rune(65+i%26)), + "email": "user" + string(rune(65+i%26)) + "@example.com", + "roles": []string{"User", "Editor", "Admin", "Viewer"}[0:1+(i%4)], + } + } + + context := map[string]interface{}{ + "users": users, + } + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + var buf bytes.Buffer + template, _ := engine.Load("large_context") + _ = template.RenderTo(&buf, context) + } +} + +// BenchmarkContextCloning benchmarks the RenderContext cloning operation +func BenchmarkContextCloning(b *testing.B) { + engine := New() + + // Create a base context with some data + baseContext := NewRenderContext(engine.environment, map[string]interface{}{ + "user": map[string]interface{}{ + "id": 123, + "name": "John Doe", + "roles": []string{ + "admin", "editor", "user", + }, + }, + "settings": map[string]interface{}{ + "theme": "dark", + "notifications": true, + "language": "en", + }, + "items": []map[string]interface{}{ + {"id": 1, "name": "Item 1"}, + {"id": 2, "name": "Item 2"}, + {"id": 3, "name": "Item 3"}, + }, + }, engine) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + // Create a clone of the context + clonedCtx := baseContext.Clone() + clonedCtx.Release() // Return to pool after use + } + + baseContext.Release() // Clean up +} + +// BenchmarkExpressionEvaluation benchmarks various expression evaluations +func BenchmarkExpressionEvaluation(b *testing.B) { + engine := New() + + // Register a simple template with different expression types + templateContent := ` +{{ 1 + 2 * 3 }} +{{ "Hello " ~ name ~ "!" }} +{{ items[0] }} +{{ user.name }} +{{ items|length > 3 ? "Many items" : "Few items" }} +{{ range(1, 10)|join(", ") }} +` + err := engine.RegisterString("expressions", templateContent) + if err != nil { + b.Fatalf("Error registering template: %v", err) + } + + context := map[string]interface{}{ + "name": "World", + "user": map[string]interface{}{ + "name": "John", + "age": 30, + }, + "items": []string{"a", "b", "c", "d", "e"}, + } + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + var buf bytes.Buffer + template, _ := engine.Load("expressions") + _ = template.RenderTo(&buf, context) + } +} + +// BenchmarkStringOperations benchmarks string manipulation operations +func BenchmarkStringOperations(b *testing.B) { + engine := New() + + // Register a template with various string operations + templateContent := ` +{{ " Hello, World! "|trim }} +{{ text|replace("o", "0") }} +{{ text|upper }} +{{ text|lower }} +{{ text|capitalize }} +{{ text|slice(7, 5) }} +{{ text|split(", ")|join("-") }} +{{ "%s, %s!"|format("Hello", "World") }} +` + err := engine.RegisterString("string_ops", templateContent) + if err != nil { + b.Fatalf("Error registering template: %v", err) + } + + context := map[string]interface{}{ + "text": "Hello, World!", + } + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + var buf bytes.Buffer + template, _ := engine.Load("string_ops") + _ = template.RenderTo(&buf, context) + } +} \ No newline at end of file diff --git a/scripts/analyze_memory.sh b/scripts/analyze_memory.sh new file mode 100755 index 0000000..d024f20 --- /dev/null +++ b/scripts/analyze_memory.sh @@ -0,0 +1,160 @@ +#!/bin/bash + +# Create output directory +mkdir -p reports + +echo "=== Running Memory Allocation Analysis for Twig Template Engine ===" +echo "" + +# Run standard benchmarks with memory statistics +echo "Step 1: Running benchmarks with memory allocation reporting..." +go test -run=^$ -bench=BenchmarkRender -benchmem ./memory_profile_test.go | tee reports/benchmark_results.txt + +# Generate allocation profile +echo "" +echo "Step 2: Generating heap allocation profile..." +go test -run=^$ -bench=BenchmarkRenderComplexTemplate -benchmem -memprofile=reports/heap.prof ./memory_profile_test.go + +# Run heap profile analysis and save top allocations +echo "" +echo "Step 3: Analyzing allocation profile..." +go tool pprof -text -alloc_space reports/heap.prof > reports/top_allocations.txt + +echo "" +echo "Step 4: Generating detailed memory profile report..." +# Run with different template complexities +echo " - Profiling simple templates..." +go run cmd/profile/main.go -complexity=1 -iterations=1000 -memprofile=reports/simple.prof > reports/simple_profile.txt + +echo " - Profiling medium templates..." +go run cmd/profile/main.go -complexity=2 -iterations=1000 -memprofile=reports/medium.prof > reports/medium_profile.txt + +echo " - Profiling complex templates..." +go run cmd/profile/main.go -complexity=3 -iterations=1000 -memprofile=reports/complex.prof > reports/complex_profile.txt + +# Generate flamegraph (requires go-torch if available) +if command -v go-torch &> /dev/null +then + echo "" + echo "Step 5: Generating flamegraph visualization..." + go-torch -alloc_space reports/heap.prof -file reports/allocations_flamegraph.svg +fi + +# Compile the comprehensive report +echo "" +echo "Step 6: Compiling final report..." + +cat > reports/memory_optimization_report.md << 'EOF' +# Twig Template Engine Memory Optimization Report + +## Summary + +This report analyzes memory allocation patterns in the Twig template engine to identify areas for implementing a zero-allocation rendering path. + +## Benchmark Results + +``` +EOF + +cat reports/benchmark_results.txt >> reports/memory_optimization_report.md + +cat >> reports/memory_optimization_report.md << 'EOF' +``` + +## Top Allocation Sources + +The following are the top functions allocating memory during template rendering: + +``` +EOF + +head -20 reports/top_allocations.txt >> reports/memory_optimization_report.md + +cat >> reports/memory_optimization_report.md << 'EOF' +``` + +## Memory Profile by Template Complexity + +### Simple Templates + +EOF + +grep -A 10 "Memory" reports/simple_profile.txt >> reports/memory_optimization_report.md + +cat >> reports/memory_optimization_report.md << 'EOF' + +### Medium Templates + +EOF + +grep -A 10 "Memory" reports/medium_profile.txt >> reports/memory_optimization_report.md + +cat >> reports/memory_optimization_report.md << 'EOF' + +### Complex Templates + +EOF + +grep -A 10 "Memory" reports/complex_profile.txt >> reports/memory_optimization_report.md + +cat >> reports/memory_optimization_report.md << 'EOF' + +## Key Allocation Hotspots + +Based on the profiling data, these areas should be prioritized for optimization: + +1. **String Operations** - String concatenation, substring operations, and conversions +2. **Context Creation** - Creating and copying RenderContext objects +3. **Map Allocations** - Temporary maps created during rendering +4. **Slice Allocations** - Dynamic arrays for node collections +5. **Expression Evaluation** - Temporary objects created during expression processing +6. **Buffer Management** - Output buffer allocations +7. **Function/Filter Calls** - Parameter passing and result handling + +## Optimization Strategies + +### String Operations + +- Replace string concatenation with direct writes to io.Writer +- Use pooled byte buffers instead of creating new strings +- Implement specialized ToString methods to avoid allocations for common types + +### Context Handling + +- Implement pooling for RenderContext objects +- Create a linked-context mechanism instead of copying values +- Preallocate and reuse context maps + +### Map and Slice Allocations + +- Preallocate maps and slices with known capacities +- Reuse map and slice objects from pools +- Avoid unnecessary copying of collections + +### Expression Evaluation + +- Pool expression evaluation result objects +- Optimize common expression patterns with specialized handlers +- Reduce intermediate allocations in expression trees + +### Implementation Plan + +1. Start with the highest allocation areas first +2. Implement object pooling for all major components +3. Create specialized non-allocating paths for common operations +4. Revise string handling to minimize allocations +5. Optimize hot spots in critical rendering code paths + +## Next Steps + +1. Implement object pools for all identified allocation sources +2. Create benchmarks to validate each optimization +3. Develop specialized string handling utilities +4. Optimize context handling and cloning +5. Enhance expression evaluation to minimize allocations + +EOF + +echo "" +echo "Report generation complete. See reports/memory_optimization_report.md for results." +echo "" \ No newline at end of file