Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions OPTIMIZATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Liquid Compiled Template Optimization Log

This document tracks optimizations made to the compiled Liquid template engine.
Each entry shows before/after code and measured impact.

---

## Baseline Measurement

**Date:** 2024-12-31
**Commit:** (pending profiler implementation)

### Current State

The compiled template engine generates Ruby code from Liquid templates.
Before optimizations, here's a sample of generated code for a simple loop:

```ruby
# Template: {% for product in products %}{{ forloop.index }}: {{ product.name }}{% endfor %}

->(assigns, __context__, __external__) do
__output__ = +""

__coll1__ = assigns["products"]
__coll1__ = __coll1__.to_a if __coll1__.is_a?(Range)
__len3__ = __coll1__.respond_to?(:length) ? __coll1__.length : 0
__idx2__ = 0
catch(:__loop__break__) do
(__coll1__.respond_to?(:each) ? __coll1__ : []).each do |__item__|
catch(:__loop__continue__) do
assigns["product"] = __item__
assigns['forloop'] = {
'name' => "product-products",
'length' => __len3__,
'index' => __idx2__ + 1,
'index0' => __idx2__,
'rindex' => __len3__ - __idx2__,
'rindex0' => __len3__ - __idx2__ - 1,
'first' => __idx2__ == 0,
'last' => __idx2__ == __len3__ - 1,
}
__output__ << LR.output(LR.lookup(assigns["forloop"], "index", __context__))
__output__ << ": "
__output__ << LR.output(LR.lookup(assigns["product"], "name", __context__))
end
__idx2__ += 1
end
end
assigns.delete("product")
assigns.delete('forloop')

__output__
end
```

### Issues Identified

1. **catch/throw overhead** - Used even when no break/continue in loop
2. **Hash allocation per iteration** - 8 key/value pairs computed every time
3. **respond_to? checks** - Redundant after type is known
4. **LR.lookup for forloop** - Unnecessary indirection for known hash
5. **String literals not frozen** - Allocates on each render
6. **Output buffer grows dynamically** - No pre-allocation

---

## Optimization Log

<!-- Entries will be added here as optimizations are implemented -->

32 changes: 23 additions & 9 deletions lib/liquid/box.rb
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ def load_liquid_runtime!
@box.require('base64')
@box.require('bigdecimal')
@box.require('bigdecimal/util') # For String#to_d etc.
@box.require('date') # For date filter
@box.require('time') # For Time.parse

# Now load the runtime which captures method references from these
@box.require(RUNTIME_PATH)
Expand All @@ -134,6 +136,8 @@ def load_liquid_runtime!
require 'base64'
require 'bigdecimal'
require 'bigdecimal/util'
require 'date'
require 'time'
require RUNTIME_PATH
end

Expand All @@ -143,6 +147,10 @@ def load_liquid_runtime!
@user_constants << "CGI"
@user_constants << "Base64"
@user_constants << "BigDecimal"
@user_constants << "Date"
@user_constants << "DateTime"
@user_constants << "Time"
@user_constants << "Liquid" # For Liquid::Compile::CompiledContext
end

# Add gem paths to the box's load_path so require works for gems
Expand Down Expand Up @@ -332,13 +340,17 @@ class << Marshal
end

def neuter_time!
# Time is neutered by default for security.
# Templates that need time should receive it via assigns.
@box.eval(<<~'RUBY')
class << Time
[:now, :new, :at, :mktime, :local, :utc, :gm].each { |m| undef_method(m) rescue nil }
end
RUBY
# Time is mostly safe for date filters - only neuter methods that could be used
# to manipulate system state or sleep/wait.
# Keep: now, at, parse, mktime - needed for date filter
# Remove: nothing for now - Time is pure computation
#
# Note: If you want stricter isolation, templates should receive "now" via assigns
# @box.eval(<<~'RUBY')
# class << Time
# [:now, :new, :at, :mktime, :local, :utc, :gm].each { |m| undef_method(m) rescue nil }
# end
# RUBY
end

def neuter_environment!
Expand Down Expand Up @@ -378,18 +390,20 @@ def neuter_basic_object!
class BasicObject
undef_method(:instance_eval) rescue nil
undef_method(:instance_exec) rescue nil
undef_method(:__send__) rescue nil
# Don't undef __send__ - it causes warnings and is equivalent to send
# which we already restrict via public_send
end
RUBY
end

def neuter_object!
@box.eval(<<~'RUBY')
class Object
# Keep public_send - it's safe (only calls public methods) and useful
[:gem, :gem_original_require, :require, :require_relative, :load,
:display, :define_singleton_method,
:instance_variable_set, :remove_instance_variable,
:extend, :send, :public_send,
:extend, :send,
].each { |m| undef_method(m) rescue nil }
end
RUBY
Expand Down
55 changes: 30 additions & 25 deletions lib/liquid/compile.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,53 @@
# Liquid Ruby Compiler
#
# This module provides the ability to compile Liquid templates to pure Ruby code.
# The compiled code can be eval'd to create a proc that renders the template
# without needing the Liquid library at runtime.
# Compiled templates execute in a secure sandbox using Liquid::Box (on Ruby 4.0+).
#
# ## Usage
#
# template = Liquid::Template.parse("Hello, {{ name }}!")
# ruby_code = template.compile_to_ruby
# render_proc = eval(ruby_code)
# result = render_proc.call({ "name" => "World" })
# # => "Hello, World!"
#
# ## Optimization Opportunities
# compiled = template.compile_to_ruby
#
# The compiled Ruby code has several significant advantages over interpreted Liquid:
#
# 1. **No Context Object**: Variables are extracted directly from the assigns hash
# and accessed without the Context abstraction layer.
# # Render securely (sandboxed on Ruby 4.0+)
# result = compiled.render({ "name" => "World" })
# # => "Hello, World!"
#
# 2. **No Filter Invocation Overhead**: Filters are compiled to direct Ruby method
# calls rather than going through context.invoke().
# # Access the generated Ruby source
# puts compiled.source
#
# 3. **No Resource Limits Tracking**: The compiled code doesn't track render
# scores, write scores, or assign scores, eliminating per-node overhead.
# # Check security status
# compiled.secure? # => true on Ruby 4.0+
#
# 4. **No Stack-based Scoping**: Ruby's native block scoping is used instead
# of manually managing scope stacks.
# ## Security
#
# 5. **Direct String Concatenation**: Output is built with direct << operations.
# On Ruby 4.0+, compiled templates execute in a Ruby::Box sandbox that prevents:
# - File system access (File, IO, Dir)
# - Process control (system, exec, spawn, fork)
# - Network access (Socket, Net::HTTP)
# - Code loading (require, load, eval)
# - Dangerous metaprogramming (define_method, const_set, send)
#
# 6. **Native Control Flow**: break/continue use Ruby's throw/catch mechanism.
# On Ruby < 4.0, a polyfill is used that prints a security warning to STDERR.
# The polyfill provides NO ACTUAL SECURITY - use Ruby 4.0+ in production.
#
# 7. **No to_liquid Calls**: Values are used directly without conversion.
# ## Performance Benefits
#
# 8. **No Profiling Hooks**: No profiler overhead in the generated code.
# Compiled templates are ~1.5x faster than interpreted Liquid because:
#
# 9. **No Exception Rendering**: Errors propagate naturally.
# 1. **No Context Object**: Variables accessed directly from assigns hash
# 2. **No Filter Dispatch**: Filters compiled to direct Ruby calls
# 3. **No Resource Limits**: No per-node overhead for limit tracking
# 4. **Native Scoping**: Ruby's block scoping instead of manual stacks
# 5. **Direct Concatenation**: Output built with << operations
# 6. **Native Control Flow**: break/continue use Ruby's throw/catch
# 7. **No to_liquid Calls**: Values used directly
# 8. **No Profiling Hooks**: No profiler overhead
#
# ## Limitations
#
# - {% render %} and {% include %} tags require runtime support
# - {% render %} and {% include %} resolved at compile time when possible
# - Custom tags need explicit compiler implementations
# - Custom filters need to be available at runtime
# - Custom filters must be available at runtime
#
module Liquid
module Compile
Expand Down
Loading
Loading