mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-16 09:20:42 -07:00
Major optimizations to reduce allocations and improve execution speed: 1. For loops: Replace catch/throw with while + break flag - Uses while loop with index instead of .each with catch/throw - Break implemented with flag variable, continue with next - Result: 18% fewer allocations, 85% faster for simple loops 2. Forloop property inlining - Inline forloop.index as (__idx__ + 1), forloop.first as (__idx__ == 0), etc. - Completely eliminates forloop hash allocation when all properties inlinable - Result: Loop with forloop went from +46% MORE to -16% FEWER allocations 3. LR.to_array helper with EMPTY_ARRAY constant - Centralized array conversion with frozen empty array for nil - Avoids allocations for empty collections 4. Inline LR.truthy? calls - Replace LR.truthy?(x) with (x != nil && x != false) - Eliminates method call overhead in conditions 5. Keep Time methods available in sandbox for date filter Overall results: - Allocations: 3.5% MORE -> 24% FEWER (27% improvement) - Time: 64% faster -> 89% faster (25% improvement) Also adds: - compile_profiler.rb for measuring allocations/performance - compile_acceptance_test.rb for output equivalence testing - OPTIMIZATION.md documenting optimization status
212 lines
6.8 KiB
Ruby
212 lines
6.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'test_helper'
|
|
require 'yaml'
|
|
|
|
# Load Shopify-style tags and filters for performance templates
|
|
require_relative '../../performance/shopify/comment_form'
|
|
require_relative '../../performance/shopify/paginate'
|
|
require_relative '../../performance/shopify/json_filter'
|
|
require_relative '../../performance/shopify/money_filter'
|
|
require_relative '../../performance/shopify/shop_filter'
|
|
require_relative '../../performance/shopify/tag_filter'
|
|
require_relative '../../performance/shopify/weight_filter'
|
|
|
|
# Acceptance tests for compiled templates
|
|
#
|
|
# These tests run every performance benchmark template through both
|
|
# the interpreted Liquid renderer and the compiled Ruby renderer,
|
|
# verifying that outputs match exactly.
|
|
#
|
|
# Run with: RUBY_BOX=1 ruby -W:no-experimental -Ilib:test test/unit/compile_acceptance_test.rb
|
|
class CompileAcceptanceTest < Minitest::Test
|
|
include Liquid
|
|
|
|
PERFORMANCE_DIR = File.expand_path('../../performance', __dir__)
|
|
TESTS_DIR = File.join(PERFORMANCE_DIR, 'tests')
|
|
DATABASE_FILE = File.join(PERFORMANCE_DIR, 'shopify/vision.database.yml')
|
|
|
|
class << self
|
|
def database
|
|
@database ||= load_database
|
|
end
|
|
|
|
def load_database
|
|
db = if YAML.respond_to?(:unsafe_load_file)
|
|
YAML.unsafe_load_file(DATABASE_FILE)
|
|
else
|
|
YAML.load_file(DATABASE_FILE)
|
|
end
|
|
|
|
# From vision source - link products to collections
|
|
db['products'].each do |product|
|
|
collections = db['collections'].find_all do |collection|
|
|
collection['products'].any? { |p| p['id'].to_i == product['id'].to_i }
|
|
end
|
|
product['collections'] = collections
|
|
end
|
|
|
|
# Key tables by handles
|
|
db = db.each_with_object({}) do |(key, values), assigns|
|
|
assigns[key] = values.each_with_object({}) do |v, h|
|
|
h[v['handle']] = v
|
|
end
|
|
end
|
|
|
|
# Standard direct accessors
|
|
db['collection'] = db['collections'].values.first
|
|
db['product'] = db['products'].values.first
|
|
db['blog'] = db['blogs'].values.first
|
|
db['article'] = db['blog']['articles'].first
|
|
db['cart'] = {
|
|
'total_price' => db['line_items'].values.inject(0) { |sum, item| sum + item['line_price'] * item['quantity'] },
|
|
'item_count' => db['line_items'].values.inject(0) { |sum, item| sum + item['quantity'] },
|
|
'items' => db['line_items'].values,
|
|
}
|
|
|
|
db
|
|
end
|
|
|
|
def register_shopify_extensions!
|
|
return if @extensions_registered
|
|
|
|
env = Liquid::Environment.default
|
|
env.register_tag('paginate', Paginate)
|
|
env.register_tag('form', CommentForm)
|
|
env.register_filter(JsonFilter)
|
|
env.register_filter(MoneyFilter)
|
|
env.register_filter(WeightFilter)
|
|
env.register_filter(ShopFilter)
|
|
env.register_filter(TagFilter)
|
|
|
|
@extensions_registered = true
|
|
end
|
|
end
|
|
|
|
# File system for {% render %} and {% include %} tags
|
|
class TestFileSystem
|
|
def initialize(path)
|
|
@path = path
|
|
end
|
|
|
|
def read_template_file(template_path)
|
|
File.read(File.join(@path, "#{template_path}.liquid"))
|
|
end
|
|
end
|
|
|
|
def setup
|
|
self.class.register_shopify_extensions!
|
|
@database = self.class.database
|
|
end
|
|
|
|
# Find all test templates and generate a test method for each
|
|
Dir.glob(File.join(TESTS_DIR, '**/*.liquid')).each do |template_path|
|
|
# Skip theme.liquid files - they're layouts, not standalone templates
|
|
next if File.basename(template_path) == 'theme.liquid'
|
|
|
|
# Extract theme name and template name for test method name
|
|
relative_path = template_path.sub("#{TESTS_DIR}/", '')
|
|
theme_name = File.dirname(relative_path)
|
|
template_name = File.basename(relative_path, '.liquid')
|
|
|
|
test_method_name = "test_#{theme_name}_#{template_name}".gsub(/[^a-zA-Z0-9_]/, '_')
|
|
|
|
define_method(test_method_name) do
|
|
run_acceptance_test(template_path, theme_name, template_name)
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def run_acceptance_test(template_path, theme_name, template_name)
|
|
# Read the template
|
|
template_source = File.read(template_path)
|
|
|
|
# Check for a theme layout
|
|
theme_path = File.join(File.dirname(template_path), 'theme.liquid')
|
|
layout_source = File.exist?(theme_path) ? File.read(theme_path) : nil
|
|
|
|
# Set up assigns
|
|
assigns = @database.dup
|
|
assigns['page_title'] = 'Test Page'
|
|
assigns['template'] = template_name
|
|
|
|
# Set up file system for partials
|
|
file_system = TestFileSystem.new(File.dirname(template_path))
|
|
|
|
# Render with interpreted Liquid
|
|
interpreted_output = render_interpreted(template_source, layout_source, assigns, file_system)
|
|
|
|
# Render with compiled Ruby
|
|
compiled_output = render_compiled(template_source, layout_source, assigns, file_system)
|
|
|
|
# Compare outputs
|
|
assert_equal(
|
|
interpreted_output,
|
|
compiled_output,
|
|
"Output mismatch for #{theme_name}/#{template_name}.liquid\n" \
|
|
"Interpreted length: #{interpreted_output.length}\n" \
|
|
"Compiled length: #{compiled_output.length}\n" \
|
|
"First difference at: #{find_first_diff(interpreted_output, compiled_output)}"
|
|
)
|
|
end
|
|
|
|
def render_interpreted(template_source, layout_source, assigns, file_system)
|
|
template = Template.parse(template_source)
|
|
template.registers[:file_system] = file_system
|
|
|
|
content = template.render!(assigns.dup)
|
|
|
|
if layout_source
|
|
layout = Template.parse(layout_source)
|
|
layout.registers[:file_system] = file_system
|
|
layout_assigns = assigns.dup
|
|
layout_assigns['content_for_layout'] = content
|
|
layout.render!(layout_assigns)
|
|
else
|
|
content
|
|
end
|
|
end
|
|
|
|
def render_compiled(template_source, layout_source, assigns, file_system)
|
|
template = Template.parse(template_source)
|
|
compiled = template.compile_to_ruby
|
|
|
|
# Set up filter handler with Shopify filters
|
|
filter_handler = Class.new do
|
|
include JsonFilter
|
|
include MoneyFilter
|
|
include WeightFilter
|
|
include ShopFilter
|
|
include TagFilter
|
|
end.new
|
|
|
|
compiled.filter_handler = filter_handler
|
|
|
|
content = compiled.call(assigns.dup, registers: { file_system: file_system })
|
|
|
|
if layout_source
|
|
layout = Template.parse(layout_source)
|
|
layout_compiled = layout.compile_to_ruby
|
|
layout_compiled.filter_handler = filter_handler
|
|
layout_assigns = assigns.dup
|
|
layout_assigns['content_for_layout'] = content
|
|
layout_compiled.call(layout_assigns, registers: { file_system: file_system })
|
|
else
|
|
content
|
|
end
|
|
end
|
|
|
|
def find_first_diff(str1, str2)
|
|
min_len = [str1.length, str2.length].min
|
|
diff_pos = (0...min_len).find { |i| str1[i] != str2[i] } || min_len
|
|
|
|
context_start = [0, diff_pos - 20].max
|
|
context_end = [str1.length, str2.length, diff_pos + 30].min
|
|
|
|
"position #{diff_pos}:\n" \
|
|
" Interpreted: #{str1[context_start...context_end].inspect}\n" \
|
|
" Compiled: #{str2[context_start...context_end].inspect}"
|
|
end
|
|
end
|