mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-15 08:50:45 -07:00
Add single-benchmark execute, simplify compilation
This commit is contained in:
@@ -18,6 +18,10 @@ group :benchmark, :test do
|
||||
end
|
||||
end
|
||||
|
||||
group :development do
|
||||
gem 'pry-byebug'
|
||||
end
|
||||
|
||||
group :test do
|
||||
gem 'rubocop', '~> 1.4', require: false
|
||||
gem 'rubocop-shopify', '~> 1.0.7', require: false
|
||||
|
||||
+191
-50
@@ -3,73 +3,214 @@
|
||||
require 'pry-byebug'
|
||||
|
||||
module Liquid
|
||||
module CompileBlockBody
|
||||
def parse(*)
|
||||
super
|
||||
class Compiler
|
||||
def initialize
|
||||
@ruby = +""
|
||||
@nodes = {}
|
||||
end
|
||||
|
||||
ruby = +"->(__context, __output, __nodes) {\n"
|
||||
compile(ruby)
|
||||
ruby << "\n}"
|
||||
def <<(line)
|
||||
@ruby << line
|
||||
end
|
||||
|
||||
nodes = nodelist.each_with_object({}) do |node, hash|
|
||||
hash[node.object_id] = node
|
||||
def var_name(name)
|
||||
"__l_#{name}"
|
||||
end
|
||||
|
||||
def to_proc
|
||||
puts @ruby if ENV['SHOW_RUBY'] == '1'
|
||||
RubyVM::InstructionSequence.compile(<<~RUBY).eval.call(@nodes)
|
||||
->(__nodes) {
|
||||
->(__context, __output) {
|
||||
#{@ruby}
|
||||
__output
|
||||
}
|
||||
}
|
||||
RUBY
|
||||
end
|
||||
|
||||
# Compiles any thing that can be returned by Expression/QuotedFragment
|
||||
def compile_expr(node)
|
||||
case node
|
||||
when Liquid::VariableLookup
|
||||
@nodes[node.object_id] = node
|
||||
node.compile_expr(self)
|
||||
when Liquid::RangeLookup
|
||||
@nodes[node.object_id] = node
|
||||
node.compile_expr(self)
|
||||
when Range # returned by RangeLookup when range contains only literals
|
||||
node.inspect
|
||||
when Integer, Float, nil, true, false, ''
|
||||
node.inspect
|
||||
else
|
||||
raise ArgumentError, "cannot compile node #{node.inspect}"
|
||||
end
|
||||
|
||||
@instructions = RubyVM::InstructionSequence
|
||||
.compile(ruby)
|
||||
.eval
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
@instructions.call(context, output, nodes)
|
||||
output
|
||||
def fallback_evaluate_expr(node)
|
||||
"__nodes[#{node.object_id}].evaluate(__context)"
|
||||
end
|
||||
|
||||
def compile(ruby)
|
||||
ruby << "__context.resource_limits.increment_render_score(#{nodelist.length})\n"
|
||||
ruby << "catch(:__interrupt) do\n"
|
||||
nodelist.each do |node|
|
||||
if node.instance_of?(String)
|
||||
ruby << "__output << #{node.inspect}\n"
|
||||
def to_integer_expr(var_name)
|
||||
<<~RUBY.strip
|
||||
(begin
|
||||
if #{var_name}.is_a?(Integer)
|
||||
#{var_name}
|
||||
else
|
||||
ruby << "begin\n"
|
||||
if node.line_number.is_a?(Integer)
|
||||
ruby << "__line_number = #{node.line_number}\n"
|
||||
else
|
||||
ruby << "__line_number = nil\n"
|
||||
begin
|
||||
Integer(#{var_name}.to_s)
|
||||
rescue ::ArgumentError
|
||||
raise Liquid::ArgumentError, "invalid integer"
|
||||
end
|
||||
ruby << "__is_blank = #{!node.instance_of?(Variable) && node.blank?}\n"
|
||||
ruby << "__node = __nodes[#{node.object_id}]\n"
|
||||
if node.respond_to?(:compile)
|
||||
node.compile(ruby)
|
||||
end
|
||||
end)
|
||||
RUBY
|
||||
end
|
||||
|
||||
def slice_collection_expr(collection_name, from_name, to_name)
|
||||
<<~RUBY.strip
|
||||
(begin
|
||||
if (#{from_name} != 0 || !#{to_name}.nil?) && #{collection_name}.respond_to?(:load_slice)
|
||||
#{collection_name}.load_slice(#{from_name}, #{to_name})
|
||||
else
|
||||
segments = []
|
||||
index = 0
|
||||
if #{collection_name}.is_a?(String)
|
||||
#{collection_name}.empty? ? [] : [collection]
|
||||
elsif !#{collection_name}.respond_to?(:each)
|
||||
[]
|
||||
else
|
||||
ruby << "__node.render_to_output_buffer(__context, __output)\n"
|
||||
end
|
||||
ruby << <<~RUBY
|
||||
rescue => __exc
|
||||
case __exc
|
||||
when Liquid::MemoryError
|
||||
raise
|
||||
when Liquid::UndefinedVariable, Liquid::UndefinedDropMethod, Liquid::UndefinedFilter
|
||||
__context.handle_error(__exc, __line_number)
|
||||
else
|
||||
__error_message = __context.handle_error(__exc, __line_number)
|
||||
unless __is_blank
|
||||
__output << __error_message
|
||||
end
|
||||
#{collection_name}.each do |item|
|
||||
if #{to_name} && #{to_name} <= index
|
||||
break
|
||||
end
|
||||
|
||||
if #{from_name} && #{from_name} <= index
|
||||
segments << item
|
||||
end
|
||||
|
||||
index += 1
|
||||
end
|
||||
|
||||
throw :__interrupt if __context.interrupt?
|
||||
RUBY
|
||||
segments
|
||||
end
|
||||
end
|
||||
ruby << "__context.resource_limits.increment_write_score(__output)\n"
|
||||
end
|
||||
ruby << "end\n"
|
||||
end)
|
||||
RUBY
|
||||
end
|
||||
|
||||
def compile(node)
|
||||
if node.instance_of?(String)
|
||||
self << "__output << #{node.inspect}\n"
|
||||
else
|
||||
@nodes[node.object_id] = node
|
||||
if node.respond_to?(:compile)
|
||||
node.compile(self)
|
||||
else
|
||||
line_number = if node.respond_to?(:line_number) && node.line_number.is_a?(Integer)
|
||||
node.line_number
|
||||
else
|
||||
nil
|
||||
end
|
||||
catch_errors(line_number, show_message: !node.blank?) do
|
||||
self << "__nodes[#{node.object_id}].render_to_output_buffer(__context, __output)\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def catch_errors(line_number, show_message: true)
|
||||
self << "begin\n"
|
||||
yield
|
||||
self << <<~RUBY
|
||||
rescue => __exc
|
||||
case __exc
|
||||
when Liquid::MemoryError
|
||||
raise
|
||||
when Liquid::UndefinedVariable, Liquid::UndefinedDropMethod, Liquid::UndefinedFilter
|
||||
__context.handle_error(__exc, #{line_number.inspect})
|
||||
else
|
||||
__error_message = __context.handle_error(__exc, #{line_number.inspect})
|
||||
RUBY
|
||||
self << "__output << __error_message\n" if show_message
|
||||
self << "end\nend\n"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
class BlockBody
|
||||
include CompileBlockBody
|
||||
def render_to_output_buffer(context, output)
|
||||
raise "Tried to render uncompiled block" unless @compiled
|
||||
@compiled.call(context, output)
|
||||
end
|
||||
|
||||
def compile_top_level
|
||||
compiler = Compiler.new
|
||||
compile(compiler)
|
||||
@compiled = compiler.to_proc
|
||||
end
|
||||
|
||||
def compile(compiler)
|
||||
nodelist.each { |node| compiler.compile(node) }
|
||||
end
|
||||
end
|
||||
|
||||
class Document
|
||||
def parse(tokenizer, parse_context)
|
||||
while parse_body(tokenizer)
|
||||
end
|
||||
@body.compile_top_level
|
||||
@body.freeze
|
||||
rescue SyntaxError => e
|
||||
e.line_number ||= parse_context.line_number
|
||||
raise
|
||||
end
|
||||
end
|
||||
|
||||
class VariableLookup
|
||||
def compile_expr(compiler)
|
||||
compiler.var_name(@name)
|
||||
end
|
||||
end
|
||||
|
||||
class Variable
|
||||
def compile(compiler)
|
||||
compiler.catch_errors(@line_number, show_message: true) do
|
||||
compiler << "__output << #{@name.compile_expr(compiler)}.to_s\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class For
|
||||
def compile(compiler)
|
||||
compiler << "collection = #{compiler.compile_expr(@collection_name)}\n"
|
||||
|
||||
unless @from.nil?
|
||||
compiler << <<~RUBY
|
||||
from_value = #{compiler.compile_expr(@from)}
|
||||
from = if from_value.nil?
|
||||
0
|
||||
else
|
||||
#{compiler.to_integer_expr(:from_value)}
|
||||
end
|
||||
|
||||
limit_value = #{compiler.compile_expr(@limit)}
|
||||
to = if limit_value.nil?
|
||||
nil
|
||||
else
|
||||
#{compiler.to_integer_expr(:limit_value)} + from
|
||||
end
|
||||
|
||||
collection = #{compiler.slice_collection_expr(:collection, :from, :to)}
|
||||
#{@reversed ? "segment.reverse!" : "" }
|
||||
RUBY
|
||||
end
|
||||
|
||||
compiler << <<~RUBY
|
||||
collection.each do |#{compiler.var_name(variable_name)}|
|
||||
RUBY
|
||||
compiler.compile(@for_block)
|
||||
compiler << "end\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,6 +25,10 @@ module Liquid
|
||||
start_int..end_int
|
||||
end
|
||||
|
||||
def compile_expr(compiler)
|
||||
compiler.fallback_evaluate_expr(self)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def to_integer(input)
|
||||
|
||||
@@ -93,6 +93,10 @@ module Liquid
|
||||
context.apply_global_filter(obj)
|
||||
end
|
||||
|
||||
def compile
|
||||
"__output << #{@name}\n"
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
obj = render(context)
|
||||
|
||||
|
||||
+62
-1
@@ -1,7 +1,14 @@
|
||||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
|
||||
|
||||
unless ENV.key?("BUNDLE_BIN_PATH")
|
||||
exec("bundle", "exec", "ruby", __FILE__, *ARGV)
|
||||
end
|
||||
|
||||
require "pry"
|
||||
require "liquid"
|
||||
require "unicode_plot"
|
||||
require "optparse"
|
||||
require "open3"
|
||||
@@ -149,11 +156,65 @@ def print_columns(cols)
|
||||
end
|
||||
end
|
||||
|
||||
Benchmarks = Class.new do
|
||||
def define(_name, benchmark)
|
||||
@benchmark = benchmark
|
||||
end
|
||||
|
||||
def run
|
||||
start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
||||
@benchmark.compile
|
||||
parsed = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
||||
|
||||
# warmup
|
||||
1000.times { @benchmark.render}
|
||||
warm = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
||||
|
||||
res = nil
|
||||
1000.times { res = @benchmark.render }
|
||||
ran = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
||||
|
||||
puts res if res.is_a?(String)
|
||||
|
||||
if ENV['SHOW_RUBY'] == '1'
|
||||
STDERR.puts "note: SHOW_RUBY prevents gathering parse metrics"
|
||||
STDERR.puts "render: #{(ran - warm) / 1000}µs"
|
||||
else
|
||||
STDERR.puts "parse: #{parsed - start}µs"
|
||||
STDERR.puts "render: #{(ran - warm) / 1000}µs"
|
||||
STDERR.puts "total: #{(ran - warm) / 1000 + (parsed - start)}µs"
|
||||
end
|
||||
end
|
||||
end.new
|
||||
|
||||
def execute
|
||||
if ARGV.count != 2
|
||||
STDERR.puts "Usage: benchmark.rb record [output_path]"
|
||||
exit(1)
|
||||
end
|
||||
|
||||
case ENV['ENGINE']
|
||||
when 'LIQUID_COMPILE'
|
||||
require_relative '../lib/liquid/compile'
|
||||
when 'LIQUID_C'
|
||||
require 'liquid/c'
|
||||
when 'LIQUID_RUBY'
|
||||
else
|
||||
raise "Invalid ENGINE: #{ENV['ENGINE'].inspect}, expected ENGINE=(LIQUID_RUBY|LIQUID_COMPILE)"
|
||||
end
|
||||
|
||||
require_relative "./benchmarks/#{ARGV[1]}.rb"
|
||||
Benchmarks.run
|
||||
end
|
||||
|
||||
case ARGV.first
|
||||
when "record", "r"
|
||||
record
|
||||
when "show", "s"
|
||||
show
|
||||
when "execute", "x"
|
||||
execute
|
||||
else
|
||||
puts "Unknown option: #{ARGV.first}"
|
||||
puts "Invalid command. Expected benchmark [record|show|execute] ..."
|
||||
exit 1
|
||||
end
|
||||
@@ -22,14 +22,6 @@ def get_time_us
|
||||
Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
||||
end
|
||||
|
||||
def render_row(charts)
|
||||
charts.map do |chart|
|
||||
io = StringIO.new
|
||||
chart.render(io)
|
||||
puts io
|
||||
end
|
||||
end
|
||||
|
||||
Benchmarks = Class.new do
|
||||
def initialize
|
||||
@by_name = {}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
Benchmarks.define('fizzbuzz_10000', Class.new do
|
||||
TEMPLATE = <<~LIQUID
|
||||
{% for i in (1..10000) %}{{ i }}
|
||||
{% endfor %}
|
||||
LIQUID
|
||||
Benchmarks.define('simple_loop', Class.new do
|
||||
TEMPLATE = "{% for i in (1..1000) %}{{ i }}{% endfor %}"
|
||||
|
||||
def compile
|
||||
@parsed = Liquid::Template.parse(TEMPLATE)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
Benchmarks.define('simple_loop', Class.new do
|
||||
TEMPLATE = "{% for i in (1..1000) %}{{ i }}{% endfor %}"
|
||||
|
||||
def compile
|
||||
@parsed = Liquid::Template.parse(TEMPLATE)
|
||||
end
|
||||
|
||||
def render
|
||||
@parsed.render
|
||||
end
|
||||
end.new)
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
require_relative '../theme_runner'
|
||||
|
||||
Benchmarks.define('theme_runner', ThemeRunner.new)
|
||||
#Benchmarks.define('theme_runner', ThemeRunner.new)
|
||||
Reference in New Issue
Block a user