Add single-benchmark execute, simplify compilation

This commit is contained in:
Sam Doiron
2022-04-13 11:14:23 -03:00
parent 45ddc00710
commit f6d1b56b4e
9 changed files with 281 additions and 65 deletions
+4
View File
@@ -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
View File
@@ -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
+4
View File
@@ -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)
+4
View File
@@ -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
View File
@@ -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
-8
View File
@@ -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 = {}
+2 -5
View File
@@ -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)
+13
View File
@@ -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)
+1 -1
View File
@@ -2,4 +2,4 @@
require_relative '../theme_runner' require_relative '../theme_runner'
Benchmarks.define('theme_runner', ThemeRunner.new) #Benchmarks.define('theme_runner', ThemeRunner.new)