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
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
View File
@@ -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
+4
View File
@@ -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)
+4
View File
@@ -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
View File
@@ -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
-8
View File
@@ -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 = {}
+2 -5
View File
@@ -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)
+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'
Benchmarks.define('theme_runner', ThemeRunner.new)
#Benchmarks.define('theme_runner', ThemeRunner.new)