mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-18 10:20:43 -07:00
Add single-benchmark execute, simplify compilation
This commit is contained in:
@@ -18,6 +18,10 @@ group :benchmark, :test do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
group :development do
|
||||||
|
gem 'pry-byebug'
|
||||||
|
end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
gem 'rubocop', '~> 1.4', require: false
|
gem 'rubocop', '~> 1.4', require: false
|
||||||
gem 'rubocop-shopify', '~> 1.0.7', require: false
|
gem 'rubocop-shopify', '~> 1.0.7', require: false
|
||||||
|
|||||||
+191
-50
@@ -3,73 +3,214 @@
|
|||||||
require 'pry-byebug'
|
require 'pry-byebug'
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
module CompileBlockBody
|
class Compiler
|
||||||
def parse(*)
|
def initialize
|
||||||
super
|
@ruby = +""
|
||||||
|
@nodes = {}
|
||||||
|
end
|
||||||
|
|
||||||
ruby = +"->(__context, __output, __nodes) {\n"
|
def <<(line)
|
||||||
compile(ruby)
|
@ruby << line
|
||||||
ruby << "\n}"
|
end
|
||||||
|
|
||||||
nodes = nodelist.each_with_object({}) do |node, hash|
|
def var_name(name)
|
||||||
hash[node.object_id] = node
|
"__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
|
end
|
||||||
|
|
||||||
@instructions = RubyVM::InstructionSequence
|
|
||||||
.compile(ruby)
|
|
||||||
.eval
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_to_output_buffer(context, output)
|
def fallback_evaluate_expr(node)
|
||||||
@instructions.call(context, output, nodes)
|
"__nodes[#{node.object_id}].evaluate(__context)"
|
||||||
output
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def compile(ruby)
|
def to_integer_expr(var_name)
|
||||||
ruby << "__context.resource_limits.increment_render_score(#{nodelist.length})\n"
|
<<~RUBY.strip
|
||||||
ruby << "catch(:__interrupt) do\n"
|
(begin
|
||||||
nodelist.each do |node|
|
if #{var_name}.is_a?(Integer)
|
||||||
if node.instance_of?(String)
|
#{var_name}
|
||||||
ruby << "__output << #{node.inspect}\n"
|
|
||||||
else
|
else
|
||||||
ruby << "begin\n"
|
begin
|
||||||
if node.line_number.is_a?(Integer)
|
Integer(#{var_name}.to_s)
|
||||||
ruby << "__line_number = #{node.line_number}\n"
|
rescue ::ArgumentError
|
||||||
else
|
raise Liquid::ArgumentError, "invalid integer"
|
||||||
ruby << "__line_number = nil\n"
|
|
||||||
end
|
end
|
||||||
ruby << "__is_blank = #{!node.instance_of?(Variable) && node.blank?}\n"
|
end
|
||||||
ruby << "__node = __nodes[#{node.object_id}]\n"
|
end)
|
||||||
if node.respond_to?(:compile)
|
RUBY
|
||||||
node.compile(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
|
else
|
||||||
ruby << "__node.render_to_output_buffer(__context, __output)\n"
|
#{collection_name}.each do |item|
|
||||||
end
|
if #{to_name} && #{to_name} <= index
|
||||||
ruby << <<~RUBY
|
break
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if #{from_name} && #{from_name} <= index
|
||||||
|
segments << item
|
||||||
|
end
|
||||||
|
|
||||||
|
index += 1
|
||||||
end
|
end
|
||||||
|
|
||||||
throw :__interrupt if __context.interrupt?
|
segments
|
||||||
RUBY
|
end
|
||||||
end
|
end
|
||||||
ruby << "__context.resource_limits.increment_write_score(__output)\n"
|
end)
|
||||||
end
|
RUBY
|
||||||
ruby << "end\n"
|
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
class BlockBody
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ module Liquid
|
|||||||
start_int..end_int
|
start_int..end_int
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def compile_expr(compiler)
|
||||||
|
compiler.fallback_evaluate_expr(self)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def to_integer(input)
|
def to_integer(input)
|
||||||
|
|||||||
@@ -93,6 +93,10 @@ module Liquid
|
|||||||
context.apply_global_filter(obj)
|
context.apply_global_filter(obj)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def compile
|
||||||
|
"__output << #{@name}\n"
|
||||||
|
end
|
||||||
|
|
||||||
def render_to_output_buffer(context, output)
|
def render_to_output_buffer(context, output)
|
||||||
obj = render(context)
|
obj = render(context)
|
||||||
|
|
||||||
|
|||||||
+62
-1
@@ -1,7 +1,14 @@
|
|||||||
#!/usr/bin/env ruby
|
#!/usr/bin/env ruby
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
unless ENV.key?("BUNDLE_BIN_PATH")
|
||||||
|
exec("bundle", "exec", "ruby", __FILE__, *ARGV)
|
||||||
|
end
|
||||||
|
|
||||||
require "pry"
|
require "pry"
|
||||||
|
require "liquid"
|
||||||
require "unicode_plot"
|
require "unicode_plot"
|
||||||
require "optparse"
|
require "optparse"
|
||||||
require "open3"
|
require "open3"
|
||||||
@@ -149,11 +156,65 @@ def print_columns(cols)
|
|||||||
end
|
end
|
||||||
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
|
case ARGV.first
|
||||||
when "record", "r"
|
when "record", "r"
|
||||||
record
|
record
|
||||||
when "show", "s"
|
when "show", "s"
|
||||||
show
|
show
|
||||||
|
when "execute", "x"
|
||||||
|
execute
|
||||||
else
|
else
|
||||||
puts "Unknown option: #{ARGV.first}"
|
puts "Invalid command. Expected benchmark [record|show|execute] ..."
|
||||||
|
exit 1
|
||||||
end
|
end
|
||||||
@@ -22,14 +22,6 @@ def get_time_us
|
|||||||
Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_row(charts)
|
|
||||||
charts.map do |chart|
|
|
||||||
io = StringIO.new
|
|
||||||
chart.render(io)
|
|
||||||
puts io
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Benchmarks = Class.new do
|
Benchmarks = Class.new do
|
||||||
def initialize
|
def initialize
|
||||||
@by_name = {}
|
@by_name = {}
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
Benchmarks.define('fizzbuzz_10000', Class.new do
|
Benchmarks.define('simple_loop', Class.new do
|
||||||
TEMPLATE = <<~LIQUID
|
TEMPLATE = "{% for i in (1..1000) %}{{ i }}{% endfor %}"
|
||||||
{% for i in (1..10000) %}{{ i }}
|
|
||||||
{% endfor %}
|
|
||||||
LIQUID
|
|
||||||
|
|
||||||
def compile
|
def compile
|
||||||
@parsed = Liquid::Template.parse(TEMPLATE)
|
@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'
|
require_relative '../theme_runner'
|
||||||
|
|
||||||
Benchmarks.define('theme_runner', ThemeRunner.new)
|
#Benchmarks.define('theme_runner', ThemeRunner.new)
|
||||||
Reference in New Issue
Block a user