use StringScanner to improve Expression Parsing and Tokenizer

This commit is contained in:
Michael Go
2025-01-07 14:32:06 -04:00
parent b4667adadf
commit 8ecb703d4d
12 changed files with 520 additions and 15 deletions
+8 -5
View File
@@ -9,14 +9,17 @@ Liquid::Environment.default.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new
Benchmark.ips do |x|
x.time = 10
x.warmup = 5
x.time = 20
x.warmup = 10
puts
puts "Running benchmark for #{x.time} seconds (with #{x.warmup} seconds warmup)."
puts
x.report("parse:") { profiler.compile }
x.report("render:") { profiler.render }
x.report("parse & render:") { profiler.run }
phase = ENV["PHASE"] || "all"
x.report("tokenize:") { profiler.tokenize } if phase == "all" || phase == "tokenize"
x.report("parse:") { profiler.compile } if phase == "all" || phase == "parse"
x.report("render:") { profiler.render } if phase == "all" || phase == "render"
x.report("parse & render:") { profiler.run } if phase == "all" || phase == "run"
end
+8
View File
@@ -48,6 +48,14 @@ class ThemeRunner
end
end
# `tokenize` will just test the tokenizen portion of liquid without any templates
def tokenize
@tests.each do |test_hash|
tokenizer = Liquid::Tokenizer.new(test_hash[:liquid], true)
while tokenizer.shift; end
end
end
# `run` is called to benchmark rendering and compiling at the same time
def run
each_test do |liquid, layout, assigns, page_template, template_name|
+183
View File
@@ -0,0 +1,183 @@
# frozen_string_literal: true
require "benchmark/ips"
# benchmark liquid lexing
require 'liquid'
RubyVM::YJIT.enable
STRING_MARKUPS = [
"\"foo\"",
"\"fooooooooooo\"",
"\"foooooooooooooooooooooooooooooo\"",
"'foo'",
"'fooooooooooo'",
"'foooooooooooooooooooooooooooooo'",
]
VARIABLE_MARKUPS = [
"article",
"article.title",
"article.title.size",
"very_long_variable_name_2024_11_05",
"very_long_variable_name_2024_11_05.size",
]
NUMBER_MARKUPS = [
"0",
"35",
"1241891024912849",
"3.5",
"3.51214128409128",
"12381902839.123819283910283",
"123.456.789",
"-123",
"-12.33",
"-405.231",
"-0",
"0",
"0.0",
"0.0000000000000000000000",
"0.00000000001",
]
RANGE_MARKUPS = [
"(1..30)",
"(1...30)",
"(1..30..5)",
"(1.0...30.0)",
"(1.........30)",
"(1..foo)",
"(foo..30)",
"(foo..bar)",
"(foo...bar...100)",
"(foo...bar...100.0)",
]
LITERAL_MARKUPS = [
nil,
'nil',
'null',
'',
'true',
'false',
'blank',
'empty',
]
MARKUPS = {
"string" => STRING_MARKUPS,
"literal" => LITERAL_MARKUPS,
"variable" => VARIABLE_MARKUPS,
"number" => NUMBER_MARKUPS,
"range" => RANGE_MARKUPS,
}
def compare_objects(object_1, object_2)
if object_1.is_a?(Liquid::VariableLookup) && object_2.is_a?(Liquid::VariableLookup)
return false if object_1.name != object_2.name
elsif object_1 != object_2
return false
end
true
end
def compare_range_lookup(expression_1_result, expression_2_result)
return false unless expression_1_result.is_a?(Liquid::RangeLookup) && expression_2_result.is_a?(Liquid::RangeLookup)
start_obj_1 = expression_1_result.start_obj
start_obj_2 = expression_2_result.start_obj
return false unless compare_objects(start_obj_1, start_obj_2)
end_obj_1 = expression_1_result.end_obj
end_obj_2 = expression_2_result.end_obj
return false unless compare_objects(end_obj_1, end_obj_2)
true
end
MARKUPS.values.flatten.each do |markup|
expression_1_result = Liquid::Expression1.parse(markup)
expression_2_result = Liquid::Expression2.parse(markup)
next if expression_1_result == expression_2_result
if expression_1_result.is_a?(Liquid::RangeLookup) && expression_2_result.is_a?(Liquid::RangeLookup)
next if compare_range_lookup(expression_1_result, expression_2_result)
end
warn "Expression1 and Expression2 results are different for markup: #{markup}"
warn "expected: #{expression_1_result}"
warn "got: #{expression_2_result}"
abort
end
warmed_up = false
MARKUPS.each do |type, markups|
Benchmark.ips do |x|
if warmed_up
x.config(time: 10, warmup: 5)
warmed_up = true
else
x.config(time: 10)
end
x.report("Liquid::Expression1#parse: #{type}") do
if Liquid::Expression != Liquid::Expression1
Liquid.send(:remove_const, :Expression)
Liquid.const_set(:Expression, Liquid::Expression1)
end
markups.each do |markup|
Liquid::Expression1.parse(markup)
end
end
x.report("Liquid::Expression2#parse: #{type}") do
if Liquid::Expression != Liquid::Expression2
Liquid.send(:remove_const, :Expression)
Liquid.const_set(:Expression, Liquid::Expression2)
end
markups.each do |markup|
Liquid::Expression2.parse(markup)
end
end
x.compare!
end
end
Benchmark.ips do |x|
x.config(time: 10)
x.report("Liquid::Expression1#parse: all") do
if Liquid::Expression != Liquid::Expression1
Liquid.send(:remove_const, :Expression)
Liquid.const_set(:Expression, Liquid::Expression1)
end
MARKUPS.values.flatten.each do |markup|
Liquid::Expression1.parse(markup)
end
end
x.report("Liquid::Expression2#parse: all") do
if Liquid::Expression != Liquid::Expression2
Liquid.send(:remove_const, :Expression)
Liquid.const_set(:Expression, Liquid::Expression2)
end
MARKUPS.values.flatten.each do |markup|
Liquid::Expression2.parse(markup)
end
end
x.compare!
end