go-twig/compiled.go
semihalev f1add2d820 Implement efficient LRU eviction strategy for attribute cache
- Added tracking of last access time and access count to cache entries
- Implemented eviction policy to remove least recently/frequently used entries
- Cache now removes 10% of entries when the cache is full, prioritizing by usage
- Added benchmarks and tests to verify eviction strategy
- Fixed the previously ineffective eviction strategy
- Improved cache efficiency for applications with many diverse object types

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-11 14:20:47 +03:00

310 lines
8.4 KiB
Go

package twig
import (
"bytes"
"encoding/binary"
"encoding/gob"
"fmt"
"io"
"sync"
"time"
)
// bytesBufferPool is used to reuse byte buffers during template serialization
var bytesBufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
// getBuffer gets a bytes.Buffer from the pool
func getBuffer() *bytes.Buffer {
buf := bytesBufferPool.Get().(*bytes.Buffer)
buf.Reset()
return buf
}
// putBuffer returns a bytes.Buffer to the pool
func putBuffer(buf *bytes.Buffer) {
bytesBufferPool.Put(buf)
}
func init() {
// Register all node types for serialization
// This is only needed for the AST serialization which still uses gob
gob.Register(&RootNode{})
gob.Register(&TextNode{})
gob.Register(&PrintNode{})
gob.Register(&BlockNode{})
gob.Register(&ForNode{})
gob.Register(&IfNode{})
gob.Register(&SetNode{})
gob.Register(&IncludeNode{})
gob.Register(&ExtendsNode{})
gob.Register(&MacroNode{})
gob.Register(&ImportNode{})
gob.Register(&FromImportNode{})
gob.Register(&FunctionNode{})
gob.Register(&CommentNode{})
// Expression nodes
gob.Register(&ExpressionNode{})
gob.Register(&VariableNode{})
gob.Register(&LiteralNode{})
gob.Register(&UnaryNode{})
gob.Register(&BinaryNode{})
}
// CompiledTemplate represents a compiled Twig template
type CompiledTemplate struct {
Name string // Template name
Source string // Original template source
LastModified int64 // Last modification timestamp
CompileTime int64 // Time when compilation occurred
AST []byte // Serialized AST data
}
// Size returns the approximate memory size of the compiled template in bytes
func (c *CompiledTemplate) Size() int {
if c == nil {
return 0
}
// Calculate approximate size
size := 0
size += len(c.Name)
size += len(c.Source)
size += len(c.AST)
size += 16 // Size of int64 fields
return size
}
// CompileTemplate compiles a parsed template into a compiled format
func CompileTemplate(tmpl *Template) (*CompiledTemplate, error) {
if tmpl == nil {
return nil, fmt.Errorf("%w: cannot compile nil template", ErrCompilation)
}
// Serialize the AST (Node tree)
var astBuf bytes.Buffer
enc := gob.NewEncoder(&astBuf)
// Encode the AST
if err := enc.Encode(tmpl.nodes); err != nil {
// If serialization fails, we'll still create the template but without AST
// We continue silently to allow template creation
}
// Store the template source, metadata, and AST
compiled := &CompiledTemplate{
Name: tmpl.name,
Source: tmpl.source,
LastModified: tmpl.lastModified,
CompileTime: time.Now().Unix(),
AST: astBuf.Bytes(),
}
return compiled, nil
}
// LoadFromCompiled loads a template from its compiled representation
func LoadFromCompiled(compiled *CompiledTemplate, env *Environment, engine *Engine) (*Template, error) {
if compiled == nil {
return nil, fmt.Errorf("%w: cannot load from nil compiled template", ErrCompilation)
}
var nodes Node
// Try to use the cached AST if available
if len(compiled.AST) > 0 {
// Attempt to deserialize the AST
dec := gob.NewDecoder(bytes.NewReader(compiled.AST))
// Try to decode the AST
err := dec.Decode(&nodes)
if err != nil {
// Fall back to parsing if AST deserialization fails
// We continue silently and fall back to parsing
nodes = nil
}
}
// If AST deserialization failed or AST is not available, parse the source
if nodes == nil {
parser := &Parser{}
var err error
nodes, err = parser.Parse(compiled.Source)
if err != nil {
return nil, fmt.Errorf("failed to parse compiled template: %w", err)
}
}
// Create the template from the nodes (either from AST or freshly parsed)
tmpl := &Template{
name: compiled.Name,
source: compiled.Source,
nodes: nodes,
env: env,
engine: engine,
lastModified: compiled.LastModified,
}
return tmpl, nil
}
// writeString writes a string to a buffer with length prefix
func writeString(w io.Writer, s string) error {
// Write the string length as uint32
if err := binary.Write(w, binary.LittleEndian, uint32(len(s))); err != nil {
return err
}
// Write the string data
_, err := w.Write([]byte(s))
return err
}
// readString reads a length-prefixed string from a reader
func readString(r io.Reader) (string, error) {
// Read string length
var length uint32
if err := binary.Read(r, binary.LittleEndian, &length); err != nil {
return "", err
}
// Read string data
data := make([]byte, length)
if _, err := io.ReadFull(r, data); err != nil {
return "", err
}
return string(data), nil
}
// SerializeCompiledTemplate serializes a compiled template to a byte array
func SerializeCompiledTemplate(compiled *CompiledTemplate) ([]byte, error) {
// Get a buffer from the pool
buf := getBuffer()
defer putBuffer(buf)
// Use binary encoding for metadata (more efficient than gob)
// Write format version (for future compatibility)
if err := binary.Write(buf, binary.LittleEndian, uint8(1)); err != nil {
return nil, fmt.Errorf("failed to serialize version: %w", err)
}
// Write Name as length-prefixed string
if err := writeString(buf, compiled.Name); err != nil {
return nil, fmt.Errorf("failed to serialize name: %w", err)
}
// Write Source as length-prefixed string
if err := writeString(buf, compiled.Source); err != nil {
return nil, fmt.Errorf("failed to serialize source: %w", err)
}
// Write timestamps
if err := binary.Write(buf, binary.LittleEndian, compiled.LastModified); err != nil {
return nil, fmt.Errorf("failed to serialize LastModified: %w", err)
}
if err := binary.Write(buf, binary.LittleEndian, compiled.CompileTime); err != nil {
return nil, fmt.Errorf("failed to serialize CompileTime: %w", err)
}
// Write AST data length followed by data
if err := binary.Write(buf, binary.LittleEndian, uint32(len(compiled.AST))); err != nil {
return nil, fmt.Errorf("failed to serialize AST length: %w", err)
}
if _, err := buf.Write(compiled.AST); err != nil {
return nil, fmt.Errorf("failed to serialize AST data: %w", err)
}
// Return a copy of the buffer data
return bytes.Clone(buf.Bytes()), nil
}
// DeserializeCompiledTemplate deserializes a compiled template from a byte array
func DeserializeCompiledTemplate(data []byte) (*CompiledTemplate, error) {
if len(data) == 0 {
return nil, fmt.Errorf("empty data cannot be deserialized")
}
// Try the new binary format first
compiled, err := deserializeBinaryFormat(data)
if err == nil {
return compiled, nil
}
// Fall back to the old gob format if binary deserialization fails
// This ensures backward compatibility with previously compiled templates
return deserializeGobFormat(data)
}
// deserializeBinaryFormat deserializes using the new binary format
func deserializeBinaryFormat(data []byte) (*CompiledTemplate, error) {
// Create a reader for the data
r := bytes.NewReader(data)
// Read and verify format version
var version uint8
if err := binary.Read(r, binary.LittleEndian, &version); err != nil {
return nil, fmt.Errorf("failed to read format version: %w", err)
}
if version != 1 {
return nil, fmt.Errorf("unsupported format version: %d", version)
}
// Create a new compiled template
compiled := new(CompiledTemplate)
// Read Name
var err error
compiled.Name, err = readString(r)
if err != nil {
return nil, fmt.Errorf("failed to read name: %w", err)
}
// Read Source
compiled.Source, err = readString(r)
if err != nil {
return nil, fmt.Errorf("failed to read source: %w", err)
}
// Read timestamps
if err := binary.Read(r, binary.LittleEndian, &compiled.LastModified); err != nil {
return nil, fmt.Errorf("failed to read LastModified: %w", err)
}
if err := binary.Read(r, binary.LittleEndian, &compiled.CompileTime); err != nil {
return nil, fmt.Errorf("failed to read CompileTime: %w", err)
}
// Read AST data length and data
var astLength uint32
if err := binary.Read(r, binary.LittleEndian, &astLength); err != nil {
return nil, fmt.Errorf("failed to read AST length: %w", err)
}
compiled.AST = make([]byte, astLength)
if _, err := io.ReadFull(r, compiled.AST); err != nil {
return nil, fmt.Errorf("failed to read AST data: %w", err)
}
return compiled, nil
}
// deserializeGobFormat deserializes using the old gob format
func deserializeGobFormat(data []byte) (*CompiledTemplate, error) {
dec := gob.NewDecoder(bytes.NewReader(data))
var compiled CompiledTemplate
if err := dec.Decode(&compiled); err != nil {
return nil, fmt.Errorf("failed to deserialize compiled template: %w", err)
}
return &compiled, nil
}