mirror of
https://github.com/semihalev/twig.git
synced 2026-03-14 13:55:46 +01:00
- 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>
310 lines
8.4 KiB
Go
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
|
|
}
|