mirror of
https://github.com/semihalev/twig.git
synced 2026-03-14 22:05:46 +01:00
- Merged buffer pooling optimizations into buffer_pool.go - Integrated string interning and tag detection into zero_alloc_tokenizer.go - Removed duplicate and superseded optimization implementations - Added optimized expression parsing to expr.go - Ensured all tests pass with consolidated implementation - Maintained zero allocation implementation for tokenization 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
761 lines
No EOL
20 KiB
Go
761 lines
No EOL
20 KiB
Go
package twig
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
"sync"
|
|
)
|
|
|
|
// BufferPool is a specialized pool for string building operations
|
|
// Designed for zero allocation rendering of templates
|
|
type BufferPool struct {
|
|
pool sync.Pool
|
|
}
|
|
|
|
// Buffer is a specialized buffer for string operations
|
|
// that minimizes allocations during template rendering
|
|
type Buffer struct {
|
|
buf []byte
|
|
pool *BufferPool
|
|
reset bool
|
|
}
|
|
|
|
// Global buffer pool instance
|
|
var globalBufferPool = NewBufferPool()
|
|
|
|
// TokenBufferPool provides a pool for token buffers
|
|
// These are specialized token buffers with pre-allocated capacity
|
|
// based on the expected template size
|
|
var TokenBufferPool = sync.Pool{
|
|
New: func() interface{} {
|
|
buffer := make([]Token, 0, 128) // Default mid-size buffer
|
|
return &buffer
|
|
},
|
|
}
|
|
|
|
// Size-tiered token buffer pools for different template sizes
|
|
// This avoids wasteful allocations for small templates and
|
|
// ensures enough capacity for large templates
|
|
var (
|
|
// Small templates (< 4KB)
|
|
SmallTokenBufferPool = sync.Pool{
|
|
New: func() interface{} {
|
|
buffer := make([]Token, 0, 64)
|
|
return &buffer
|
|
},
|
|
}
|
|
|
|
// Medium templates (4KB - 20KB)
|
|
MediumTokenBufferPool = sync.Pool{
|
|
New: func() interface{} {
|
|
buffer := make([]Token, 0, 256)
|
|
return &buffer
|
|
},
|
|
}
|
|
|
|
// Large templates (> 20KB)
|
|
LargeTokenBufferPool = sync.Pool{
|
|
New: func() interface{} {
|
|
buffer := make([]Token, 0, 1024)
|
|
return &buffer
|
|
},
|
|
}
|
|
)
|
|
|
|
// NewBufferPool creates a new buffer pool
|
|
func NewBufferPool() *BufferPool {
|
|
return &BufferPool{
|
|
pool: sync.Pool{
|
|
New: func() interface{} {
|
|
// Start with a reasonable capacity
|
|
return &Buffer{
|
|
buf: make([]byte, 0, 1024),
|
|
reset: true,
|
|
}
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// Get retrieves a buffer from the pool
|
|
func (p *BufferPool) Get() *Buffer {
|
|
buffer := p.pool.Get().(*Buffer)
|
|
if buffer.reset {
|
|
buffer.buf = buffer.buf[:0] // Reset length but keep capacity
|
|
} else {
|
|
buffer.buf = buffer.buf[:0] // Ensure buffer is empty
|
|
buffer.reset = true
|
|
}
|
|
buffer.pool = p
|
|
return buffer
|
|
}
|
|
|
|
// GetBuffer retrieves a buffer from the global pool
|
|
func GetBuffer() *Buffer {
|
|
return globalBufferPool.Get()
|
|
}
|
|
|
|
// Release returns the buffer to its pool
|
|
func (b *Buffer) Release() {
|
|
if b.pool != nil {
|
|
b.pool.pool.Put(b)
|
|
}
|
|
}
|
|
|
|
// Write implements io.Writer
|
|
func (b *Buffer) Write(p []byte) (n int, err error) {
|
|
b.buf = append(b.buf, p...)
|
|
return len(p), nil
|
|
}
|
|
|
|
// WriteString writes a string to the buffer with zero allocation
|
|
func (b *Buffer) WriteString(s string) (n int, err error) {
|
|
b.buf = append(b.buf, s...)
|
|
return len(s), nil
|
|
}
|
|
|
|
// WriteByte writes a single byte to the buffer
|
|
func (b *Buffer) WriteByte(c byte) error {
|
|
b.buf = append(b.buf, c)
|
|
return nil
|
|
}
|
|
|
|
// WriteSpecialized functions for common types to avoid string conversions
|
|
|
|
// Pre-computed small integer strings to avoid allocations
|
|
var smallIntStrings = [...]string{
|
|
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
|
|
"10", "11", "12", "13", "14", "15", "16", "17", "18", "19",
|
|
"20", "21", "22", "23", "24", "25", "26", "27", "28", "29",
|
|
"30", "31", "32", "33", "34", "35", "36", "37", "38", "39",
|
|
"40", "41", "42", "43", "44", "45", "46", "47", "48", "49",
|
|
"50", "51", "52", "53", "54", "55", "56", "57", "58", "59",
|
|
"60", "61", "62", "63", "64", "65", "66", "67", "68", "69",
|
|
"70", "71", "72", "73", "74", "75", "76", "77", "78", "79",
|
|
"80", "81", "82", "83", "84", "85", "86", "87", "88", "89",
|
|
"90", "91", "92", "93", "94", "95", "96", "97", "98", "99",
|
|
}
|
|
|
|
// Pre-computed small negative integer strings
|
|
var smallNegIntStrings = [...]string{
|
|
"0", "-1", "-2", "-3", "-4", "-5", "-6", "-7", "-8", "-9",
|
|
"-10", "-11", "-12", "-13", "-14", "-15", "-16", "-17", "-18", "-19",
|
|
"-20", "-21", "-22", "-23", "-24", "-25", "-26", "-27", "-28", "-29",
|
|
"-30", "-31", "-32", "-33", "-34", "-35", "-36", "-37", "-38", "-39",
|
|
"-40", "-41", "-42", "-43", "-44", "-45", "-46", "-47", "-48", "-49",
|
|
"-50", "-51", "-52", "-53", "-54", "-55", "-56", "-57", "-58", "-59",
|
|
"-60", "-61", "-62", "-63", "-64", "-65", "-66", "-67", "-68", "-69",
|
|
"-70", "-71", "-72", "-73", "-74", "-75", "-76", "-77", "-78", "-79",
|
|
"-80", "-81", "-82", "-83", "-84", "-85", "-86", "-87", "-88", "-89",
|
|
"-90", "-91", "-92", "-93", "-94", "-95", "-96", "-97", "-98", "-99",
|
|
}
|
|
|
|
// WriteInt writes an integer to the buffer with minimal allocations
|
|
// Uses a fast path for common integer values
|
|
func (b *Buffer) WriteInt(i int) (n int, err error) {
|
|
// Fast path for small integers using pre-computed strings
|
|
if i >= 0 && i < 100 {
|
|
return b.WriteString(smallIntStrings[i])
|
|
} else if i > -100 && i < 0 {
|
|
return b.WriteString(smallNegIntStrings[-i])
|
|
}
|
|
|
|
// Optimization: manual integer formatting for common sizes
|
|
// Avoid the allocations in strconv.Itoa for numbers we can handle directly
|
|
if i >= -999999 && i <= 999999 {
|
|
return b.formatInt(int64(i))
|
|
}
|
|
|
|
// For larger integers, fallback to standard formatting
|
|
// This still allocates, but is rare enough to be acceptable
|
|
s := strconv.FormatInt(int64(i), 10)
|
|
return b.WriteString(s)
|
|
}
|
|
|
|
// formatInt does manual string formatting for integers without allocation
|
|
// This is a specialized version that handles integers up to 6 digits
|
|
func (b *Buffer) formatInt(i int64) (int, error) {
|
|
// Handle negative numbers
|
|
if i < 0 {
|
|
b.WriteByte('-')
|
|
i = -i
|
|
}
|
|
|
|
// Count digits to determine buffer size
|
|
var digits int
|
|
if i < 10 {
|
|
digits = 1
|
|
} else if i < 100 {
|
|
digits = 2
|
|
} else if i < 1000 {
|
|
digits = 3
|
|
} else if i < 10000 {
|
|
digits = 4
|
|
} else if i < 100000 {
|
|
digits = 5
|
|
} else {
|
|
digits = 6
|
|
}
|
|
|
|
// Reserve space for the digits
|
|
// Compute in reverse order, then reverse the result
|
|
start := len(b.buf)
|
|
for j := 0; j < digits; j++ {
|
|
digit := byte('0' + i%10)
|
|
b.buf = append(b.buf, digit)
|
|
i /= 10
|
|
}
|
|
|
|
// Reverse the digits
|
|
end := len(b.buf) - 1
|
|
for j := 0; j < digits/2; j++ {
|
|
b.buf[start+j], b.buf[end-j] = b.buf[end-j], b.buf[start+j]
|
|
}
|
|
|
|
return digits, nil
|
|
}
|
|
|
|
// WriteFloat writes a float to the buffer with optimizations for common cases
|
|
func (b *Buffer) WriteFloat(f float64, fmt byte, prec int) (n int, err error) {
|
|
// Special case for integers or near-integers with default precision
|
|
if prec == -1 && fmt == 'f' {
|
|
// If it's a whole number within integer range, use integer formatting
|
|
if f == float64(int64(f)) && f <= 9007199254740991 && f >= -9007199254740991 {
|
|
// It's a whole number that can be represented exactly as an int64
|
|
return b.formatInt(int64(f))
|
|
}
|
|
}
|
|
|
|
// Special case for small, common floating-point values with 1-2 decimal places
|
|
if fmt == 'f' && f >= 0 && f < 1000 && (prec == 1 || prec == 2 || prec == -1) {
|
|
// Try to format common floats manually without allocation
|
|
intPart := int64(f)
|
|
|
|
// Get the fractional part based on precision
|
|
var fracFactor int64
|
|
var fracPrec int
|
|
if prec == -1 {
|
|
// Default precision, up to 6 decimal places
|
|
// Check if we can represent this exactly with fewer digits
|
|
fracPart := f - float64(intPart)
|
|
if fracPart == 0 {
|
|
// It's a whole number
|
|
return b.formatInt(intPart)
|
|
}
|
|
|
|
// Test if 1-2 decimal places is enough
|
|
if fracPart*100 == float64(int64(fracPart*100)) {
|
|
// Two decimal places is sufficient
|
|
fracFactor = 100
|
|
fracPrec = 2
|
|
} else if fracPart*10 == float64(int64(fracPart*10)) {
|
|
// One decimal place is sufficient
|
|
fracFactor = 10
|
|
fracPrec = 1
|
|
} else {
|
|
// Needs more precision, use strconv
|
|
goto useStrconv
|
|
}
|
|
} else if prec == 1 {
|
|
fracFactor = 10
|
|
fracPrec = 1
|
|
} else {
|
|
fracFactor = 100
|
|
fracPrec = 2
|
|
}
|
|
|
|
// Format integer part first
|
|
intLen, err := b.formatInt(intPart)
|
|
if err != nil {
|
|
return intLen, err
|
|
}
|
|
|
|
// Add decimal point
|
|
if err := b.WriteByte('.'); err != nil {
|
|
return intLen, err
|
|
}
|
|
|
|
// Format fractional part, ensuring proper padding with zeros
|
|
fracPart := int64((f - float64(intPart)) * float64(fracFactor) + 0.5) // Round
|
|
if fracPart >= fracFactor {
|
|
// Rounding caused carry
|
|
fracPart = 0
|
|
// Adjust integer part
|
|
b.Reset()
|
|
intLen, err = b.formatInt(intPart + 1)
|
|
if err != nil {
|
|
return intLen, err
|
|
}
|
|
if err := b.WriteByte('.'); err != nil {
|
|
return intLen, err
|
|
}
|
|
}
|
|
|
|
// Write fractional part with leading zeros if needed
|
|
if fracPrec == 2 && fracPart < 10 {
|
|
if err := b.WriteByte('0'); err != nil {
|
|
return intLen + 1, err
|
|
}
|
|
}
|
|
|
|
fracLen, err := b.formatInt(fracPart)
|
|
if err != nil {
|
|
return intLen + 1, err
|
|
}
|
|
|
|
return intLen + 1 + fracLen, nil
|
|
}
|
|
|
|
useStrconv:
|
|
// Fallback to standard formatting for complex or unusual cases
|
|
s := strconv.FormatFloat(f, fmt, prec, 64)
|
|
return b.WriteString(s)
|
|
}
|
|
|
|
// WriteFormat appends a formatted string to the buffer with minimal allocations
|
|
// Similar to fmt.Sprintf but reuses the buffer and avoids allocations
|
|
// Only handles a limited set of format specifiers: %s, %d, %v
|
|
func (b *Buffer) WriteFormat(format string, args ...interface{}) (n int, err error) {
|
|
// Fast path for simple string with no format specifiers
|
|
if len(args) == 0 {
|
|
return b.WriteString(format)
|
|
}
|
|
|
|
startIdx := 0
|
|
argIdx := 0
|
|
totalWritten := 0
|
|
|
|
// Scan the format string for format specifiers
|
|
for i := 0; i < len(format); i++ {
|
|
if format[i] != '%' {
|
|
continue
|
|
}
|
|
|
|
// Found a potential format specifier
|
|
if i+1 >= len(format) {
|
|
// % at the end of the string is invalid
|
|
break
|
|
}
|
|
|
|
// Check next character
|
|
next := format[i+1]
|
|
if next == '%' {
|
|
// It's an escaped %
|
|
// Write everything up to and including the first %
|
|
if i > startIdx {
|
|
written, err := b.WriteString(format[startIdx:i+1])
|
|
totalWritten += written
|
|
if err != nil {
|
|
return totalWritten, err
|
|
}
|
|
}
|
|
// Skip the second %
|
|
i++
|
|
startIdx = i+1
|
|
continue
|
|
}
|
|
|
|
// Write the part before the format specifier
|
|
if i > startIdx {
|
|
written, err := b.WriteString(format[startIdx:i])
|
|
totalWritten += written
|
|
if err != nil {
|
|
return totalWritten, err
|
|
}
|
|
}
|
|
|
|
// Make sure we have an argument for this specifier
|
|
if argIdx >= len(args) {
|
|
// More specifiers than arguments, skip
|
|
startIdx = i
|
|
continue
|
|
}
|
|
|
|
arg := args[argIdx]
|
|
argIdx++
|
|
|
|
// Process the format specifier
|
|
switch next {
|
|
case 's':
|
|
// String format
|
|
if str, ok := arg.(string); ok {
|
|
written, err := b.WriteString(str)
|
|
totalWritten += written
|
|
if err != nil {
|
|
return totalWritten, err
|
|
}
|
|
} else {
|
|
// Convert to string
|
|
written, err := writeValueToBuffer(b, arg)
|
|
totalWritten += written
|
|
if err != nil {
|
|
return totalWritten, err
|
|
}
|
|
}
|
|
case 'd', 'v':
|
|
// Integer or default format
|
|
if i, ok := arg.(int); ok {
|
|
written, err := b.WriteInt(i)
|
|
totalWritten += written
|
|
if err != nil {
|
|
return totalWritten, err
|
|
}
|
|
} else {
|
|
// Use general value formatting
|
|
written, err := writeValueToBuffer(b, arg)
|
|
totalWritten += written
|
|
if err != nil {
|
|
return totalWritten, err
|
|
}
|
|
}
|
|
default:
|
|
// Unsupported format specifier, just output it as-is
|
|
if err := b.WriteByte('%'); err != nil {
|
|
return totalWritten, err
|
|
}
|
|
totalWritten++
|
|
if err := b.WriteByte(next); err != nil {
|
|
return totalWritten, err
|
|
}
|
|
totalWritten++
|
|
}
|
|
|
|
// Move past the format specifier
|
|
i++
|
|
startIdx = i+1
|
|
}
|
|
|
|
// Write any remaining part of the format string
|
|
if startIdx < len(format) {
|
|
written, err := b.WriteString(format[startIdx:])
|
|
totalWritten += written
|
|
if err != nil {
|
|
return totalWritten, err
|
|
}
|
|
}
|
|
|
|
return totalWritten, nil
|
|
}
|
|
|
|
// Grow ensures the buffer has enough capacity for n more bytes
|
|
// This helps avoid multiple small allocations during growth
|
|
func (b *Buffer) Grow(n int) {
|
|
// Calculate new capacity needed
|
|
needed := len(b.buf) + n
|
|
if cap(b.buf) >= needed {
|
|
return // Already have enough capacity
|
|
}
|
|
|
|
// Grow capacity with a smart algorithm that avoids frequent resizing
|
|
// Double the capacity until we have enough, but with some optimizations:
|
|
// - For small buffers (<1KB), grow more aggressively (2x)
|
|
// - For medium buffers (1KB-64KB), grow at 1.5x
|
|
// - For large buffers (>64KB), grow at 1.25x to avoid excessive memory usage
|
|
|
|
newCap := cap(b.buf)
|
|
const (
|
|
smallBuffer = 1024 // 1KB
|
|
mediumBuffer = 64 * 1024 // 64KB
|
|
)
|
|
|
|
for newCap < needed {
|
|
if newCap < smallBuffer {
|
|
newCap *= 2 // Double small buffers
|
|
} else if newCap < mediumBuffer {
|
|
newCap = newCap + newCap/2 // Grow medium buffers by 1.5x
|
|
} else {
|
|
newCap = newCap + newCap/4 // Grow large buffers by 1.25x
|
|
}
|
|
}
|
|
|
|
// Create new buffer with the calculated capacity
|
|
newBuf := make([]byte, len(b.buf), newCap)
|
|
copy(newBuf, b.buf)
|
|
b.buf = newBuf
|
|
}
|
|
|
|
// WriteBool writes a boolean value to the buffer
|
|
func (b *Buffer) WriteBool(v bool) (n int, err error) {
|
|
if v {
|
|
return b.WriteString("true")
|
|
}
|
|
return b.WriteString("false")
|
|
}
|
|
|
|
// Len returns the current length of the buffer
|
|
func (b *Buffer) Len() int {
|
|
return len(b.buf)
|
|
}
|
|
|
|
// String returns the contents as a string
|
|
func (b *Buffer) String() string {
|
|
return string(b.buf)
|
|
}
|
|
|
|
// Bytes returns the contents as a byte slice
|
|
func (b *Buffer) Bytes() []byte {
|
|
return b.buf
|
|
}
|
|
|
|
// Reset empties the buffer
|
|
func (b *Buffer) Reset() {
|
|
b.buf = b.buf[:0]
|
|
}
|
|
|
|
// WriteTo writes the buffer to an io.Writer
|
|
func (b *Buffer) WriteTo(w io.Writer) (int64, error) {
|
|
n, err := w.Write(b.buf)
|
|
return int64(n), err
|
|
}
|
|
|
|
// Global-level utility functions for writing values with minimal allocations
|
|
|
|
// WriteValue writes any value to a writer in the most efficient way possible
|
|
func WriteValue(w io.Writer, val interface{}) (n int, err error) {
|
|
// First check if we can use optimized path for known writer types
|
|
if bw, ok := w.(*Buffer); ok {
|
|
return writeValueToBuffer(bw, val)
|
|
}
|
|
|
|
// If writer is a StringWriter, we can optimize some cases
|
|
if sw, ok := w.(io.StringWriter); ok {
|
|
return writeValueToStringWriter(sw, val)
|
|
}
|
|
|
|
// Fallback path - use temp buffer for conversion to avoid allocating strings
|
|
buf := GetBuffer()
|
|
defer buf.Release()
|
|
|
|
_, _ = writeValueToBuffer(buf, val)
|
|
return w.Write(buf.Bytes())
|
|
}
|
|
|
|
// writeValueToBuffer writes a value to a Buffer using type-specific optimizations
|
|
func writeValueToBuffer(b *Buffer, val interface{}) (n int, err error) {
|
|
if val == nil {
|
|
return 0, nil
|
|
}
|
|
|
|
switch v := val.(type) {
|
|
case string:
|
|
return b.WriteString(v)
|
|
case int:
|
|
return b.WriteInt(v)
|
|
case int64:
|
|
return b.WriteString(strconv.FormatInt(v, 10))
|
|
case float64:
|
|
return b.WriteFloat(v, 'f', -1)
|
|
case bool:
|
|
return b.WriteBool(v)
|
|
case []byte:
|
|
return b.Write(v)
|
|
default:
|
|
// Fall back to string conversion
|
|
return b.WriteString(defaultToString(val))
|
|
}
|
|
}
|
|
|
|
// writeValueToStringWriter writes a value to an io.StringWriter
|
|
func writeValueToStringWriter(w io.StringWriter, val interface{}) (n int, err error) {
|
|
if val == nil {
|
|
return 0, nil
|
|
}
|
|
|
|
switch v := val.(type) {
|
|
case string:
|
|
return w.WriteString(v)
|
|
case int:
|
|
if v >= 0 && v < 100 {
|
|
// Use cached small integers for fast path
|
|
return w.WriteString(smallIntStrings[v])
|
|
} else if v > -100 && v < 0 {
|
|
return w.WriteString(smallNegIntStrings[-v])
|
|
}
|
|
return w.WriteString(strconv.Itoa(v))
|
|
case int64:
|
|
return w.WriteString(strconv.FormatInt(v, 10))
|
|
case float64:
|
|
return w.WriteString(strconv.FormatFloat(v, 'f', -1, 64))
|
|
case bool:
|
|
if v {
|
|
return w.WriteString("true")
|
|
}
|
|
return w.WriteString("false")
|
|
case []byte:
|
|
return w.WriteString(string(v))
|
|
default:
|
|
// Fall back to string conversion
|
|
return w.WriteString(defaultToString(val))
|
|
}
|
|
}
|
|
|
|
// defaultToString converts a value to a string using the default method
|
|
func defaultToString(val interface{}) string {
|
|
return stringify(val)
|
|
}
|
|
|
|
// stringify is a helper to convert any value to string
|
|
func stringify(val interface{}) string {
|
|
if val == nil {
|
|
return ""
|
|
}
|
|
|
|
// Use type switch for efficient handling of common types
|
|
switch v := val.(type) {
|
|
case string:
|
|
return v
|
|
case int:
|
|
// Use cached small integers for fast path
|
|
if v >= 0 && v < 100 {
|
|
return smallIntStrings[v]
|
|
} else if v > -100 && v < 0 {
|
|
return smallNegIntStrings[-v]
|
|
}
|
|
return strconv.Itoa(v)
|
|
case int64:
|
|
return strconv.FormatInt(v, 10)
|
|
case float64:
|
|
return strconv.FormatFloat(v, 'f', -1, 64)
|
|
case bool:
|
|
return strconv.FormatBool(v)
|
|
case []byte:
|
|
return string(v)
|
|
}
|
|
|
|
// Fall back to fmt.Sprintf for complex types
|
|
return fmt.Sprintf("%v", val)
|
|
}
|
|
|
|
// GetTokenBuffer gets a token buffer with capacity optimized for the given template size
|
|
func GetTokenBuffer(templateSize int) *[]Token {
|
|
// Select the appropriate pool based on template size
|
|
// For very small templates, use the small pool
|
|
// For medium templates, use the medium pool
|
|
// For large templates, use the large pool
|
|
// For extremely large templates, allocate directly
|
|
|
|
var buffer *[]Token
|
|
|
|
if templateSize < 4*1024 {
|
|
// Small template
|
|
buffer = SmallTokenBufferPool.Get().(*[]Token)
|
|
if cap(*buffer) < estimateTokenCount(templateSize) {
|
|
// Need more capacity
|
|
*buffer = make([]Token, 0, estimateTokenCount(templateSize))
|
|
}
|
|
} else if templateSize < 20*1024 {
|
|
// Medium template
|
|
buffer = MediumTokenBufferPool.Get().(*[]Token)
|
|
if cap(*buffer) < estimateTokenCount(templateSize) {
|
|
// Need more capacity
|
|
*buffer = make([]Token, 0, estimateTokenCount(templateSize))
|
|
}
|
|
} else if templateSize < 100*1024 {
|
|
// Large template
|
|
buffer = LargeTokenBufferPool.Get().(*[]Token)
|
|
if cap(*buffer) < estimateTokenCount(templateSize) {
|
|
// Need more capacity
|
|
*buffer = make([]Token, 0, estimateTokenCount(templateSize))
|
|
}
|
|
} else {
|
|
// Extremely large template
|
|
// Allocate directly with appropriate capacity
|
|
newBuffer := make([]Token, 0, estimateTokenCount(templateSize))
|
|
buffer = &newBuffer
|
|
}
|
|
|
|
// Clear the buffer in case it contains old tokens
|
|
*buffer = (*buffer)[:0]
|
|
|
|
return buffer
|
|
}
|
|
|
|
// ReleaseTokenBuffer returns a token buffer to the appropriate pool
|
|
func ReleaseTokenBuffer(buffer *[]Token) {
|
|
if buffer == nil {
|
|
return
|
|
}
|
|
|
|
// Clear the buffer to prevent memory leaks
|
|
*buffer = (*buffer)[:0]
|
|
|
|
// Put back in the appropriate pool based on capacity
|
|
cap := cap(*buffer)
|
|
if cap <= 64 {
|
|
SmallTokenBufferPool.Put(buffer)
|
|
} else if cap <= 256 {
|
|
MediumTokenBufferPool.Put(buffer)
|
|
} else if cap <= 1024 {
|
|
LargeTokenBufferPool.Put(buffer)
|
|
}
|
|
// Very large buffers aren't returned to the pool
|
|
}
|
|
|
|
// GetTokenBufferWithCapacity gets a token buffer with at least the requested capacity
|
|
func GetTokenBufferWithCapacity(capacity int) *[]Token {
|
|
// For very small capacity requests, use the small pool
|
|
// For medium capacity requests, use the medium pool
|
|
// For large capacity requests, use the large pool
|
|
// For very large capacity requests, allocate directly
|
|
var buffer *[]Token
|
|
|
|
if capacity <= 64 {
|
|
buffer = SmallTokenBufferPool.Get().(*[]Token)
|
|
if cap(*buffer) < capacity {
|
|
*buffer = make([]Token, 0, capacity)
|
|
}
|
|
} else if capacity <= 256 {
|
|
buffer = MediumTokenBufferPool.Get().(*[]Token)
|
|
if cap(*buffer) < capacity {
|
|
*buffer = make([]Token, 0, capacity)
|
|
}
|
|
} else if capacity <= 1024 {
|
|
buffer = LargeTokenBufferPool.Get().(*[]Token)
|
|
if cap(*buffer) < capacity {
|
|
*buffer = make([]Token, 0, capacity)
|
|
}
|
|
} else {
|
|
// Very large capacity request, allocate directly
|
|
newBuffer := make([]Token, 0, capacity)
|
|
buffer = &newBuffer
|
|
}
|
|
|
|
// Clear the buffer
|
|
*buffer = (*buffer)[:0]
|
|
|
|
return buffer
|
|
}
|
|
|
|
// RecycleTokens enables token slice reuse for nested template parsing
|
|
// This is useful for include/extends tags to avoid allocations
|
|
func RecycleTokens(tokens []Token) []Token {
|
|
// If tokens is nil or empty, return an empty slice
|
|
if len(tokens) == 0 {
|
|
return []Token{}
|
|
}
|
|
|
|
// Create a new slice with the same backing array
|
|
recycled := tokens[:0]
|
|
return recycled
|
|
}
|
|
|
|
// estimateTokenCount estimates the number of tokens needed for a template
|
|
// This is used to allocate token buffers with appropriate capacity
|
|
func estimateTokenCount(templateSize int) int {
|
|
// From empirical testing, templates typically have:
|
|
// - Small templates: ~1 token per 12 bytes
|
|
// - Medium templates: ~1 token per 15 bytes
|
|
// - Large templates: ~1 token per 20 bytes
|
|
|
|
if templateSize < 4*1024 {
|
|
// Small template
|
|
return max(64, templateSize/12+16)
|
|
} else if templateSize < 20*1024 {
|
|
// Medium template
|
|
return max(256, templateSize/15+32)
|
|
} else {
|
|
// Large template
|
|
return max(1024, templateSize/20+64)
|
|
}
|
|
} |