mirror of
https://github.com/Shopify/liquid.git
synced 2026-10-03 00:55:11 -07:00
Speed up lexing
This commit is contained in:
@@ -6,3 +6,4 @@ pkg
|
|||||||
.rvmrc
|
.rvmrc
|
||||||
.bundle
|
.bundle
|
||||||
.byebug_history
|
.byebug_history
|
||||||
|
Gemfile.lock
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
3.3.0
|
3.3.4
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
GIT
|
|
||||||
remote: https://github.com/Shopify/liquid-c.git
|
|
||||||
revision: 5a786af7284df55e013ea20551c4b688d02e8326
|
|
||||||
ref: main
|
|
||||||
specs:
|
|
||||||
liquid-c (4.2.0)
|
|
||||||
liquid (>= 5.0.1)
|
|
||||||
|
|
||||||
PATH
|
|
||||||
remote: .
|
|
||||||
specs:
|
|
||||||
liquid (5.6.0.alpha)
|
|
||||||
|
|
||||||
GEM
|
|
||||||
remote: https://rubygems.org/
|
|
||||||
specs:
|
|
||||||
ast (2.4.2)
|
|
||||||
base64 (0.2.0)
|
|
||||||
benchmark-ips (2.13.0)
|
|
||||||
json (2.7.2)
|
|
||||||
language_server-protocol (3.17.0.3)
|
|
||||||
memory_profiler (1.0.1)
|
|
||||||
minitest (5.22.3)
|
|
||||||
parallel (1.24.0)
|
|
||||||
parser (3.3.0.5)
|
|
||||||
ast (~> 2.4.1)
|
|
||||||
racc
|
|
||||||
racc (1.7.3)
|
|
||||||
rainbow (3.1.1)
|
|
||||||
rake (13.2.1)
|
|
||||||
regexp_parser (2.9.0)
|
|
||||||
rexml (3.2.6)
|
|
||||||
rubocop (1.61.0)
|
|
||||||
json (~> 2.3)
|
|
||||||
language_server-protocol (>= 3.17.0)
|
|
||||||
parallel (~> 1.10)
|
|
||||||
parser (>= 3.3.0.2)
|
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
|
||||||
regexp_parser (>= 1.8, < 3.0)
|
|
||||||
rexml (>= 3.2.5, < 4.0)
|
|
||||||
rubocop-ast (>= 1.30.0, < 2.0)
|
|
||||||
ruby-progressbar (~> 1.7)
|
|
||||||
unicode-display_width (>= 2.4.0, < 3.0)
|
|
||||||
rubocop-ast (1.31.2)
|
|
||||||
parser (>= 3.3.0.4)
|
|
||||||
rubocop-performance (1.19.1)
|
|
||||||
rubocop (>= 1.7.0, < 2.0)
|
|
||||||
rubocop-ast (>= 0.4.0)
|
|
||||||
rubocop-shopify (2.12.0)
|
|
||||||
rubocop (~> 1.44)
|
|
||||||
ruby-progressbar (1.13.0)
|
|
||||||
stackprof (0.2.26)
|
|
||||||
terminal-table (3.0.2)
|
|
||||||
unicode-display_width (>= 1.1.1, < 3)
|
|
||||||
unicode-display_width (2.5.0)
|
|
||||||
|
|
||||||
PLATFORMS
|
|
||||||
ruby
|
|
||||||
|
|
||||||
DEPENDENCIES
|
|
||||||
base64
|
|
||||||
benchmark-ips
|
|
||||||
liquid!
|
|
||||||
liquid-c!
|
|
||||||
memory_profiler
|
|
||||||
minitest
|
|
||||||
rake (~> 13.0)
|
|
||||||
rubocop (~> 1.61.0)
|
|
||||||
rubocop-performance
|
|
||||||
rubocop-shopify (~> 2.12.0)
|
|
||||||
stackprof
|
|
||||||
terminal-table
|
|
||||||
|
|
||||||
BUNDLED WITH
|
|
||||||
2.5.7
|
|
||||||
@@ -81,6 +81,14 @@ namespace :benchmark do
|
|||||||
task :strict do
|
task :strict do
|
||||||
ruby "./performance/benchmark.rb strict"
|
ruby "./performance/benchmark.rb strict"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
desc "Run unit benchmarks"
|
||||||
|
task :unit do
|
||||||
|
Dir["./performance/unit/*_benchmark.rb"].each do |file|
|
||||||
|
puts "🧪 Running #{file}"
|
||||||
|
ruby file
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
namespace :profile do
|
namespace :profile do
|
||||||
|
|||||||
+139
-1
@@ -1,8 +1,9 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require "strscan"
|
require "strscan"
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
class Lexer
|
class Lexer1
|
||||||
SPECIALS = {
|
SPECIALS = {
|
||||||
'|' => :pipe,
|
'|' => :pipe,
|
||||||
'.' => :dot,
|
'.' => :dot,
|
||||||
@@ -58,4 +59,141 @@ module Liquid
|
|||||||
@output << [:end_of_string]
|
@output << [:end_of_string]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class Lexer2
|
||||||
|
CLOSE_ROUND = [:close_round, ")"].freeze
|
||||||
|
CLOSE_SQUARE = [:close_square, "]"].freeze
|
||||||
|
COLON = [:colon, ":"].freeze
|
||||||
|
COMMA = [:comma, ","].freeze
|
||||||
|
COMPARISION_NOT_EQUAL = [:comparison, "!="].freeze
|
||||||
|
COMPARISON_CONTAINS = [:comparison, "contains"].freeze
|
||||||
|
COMPARISON_EQUAL = [:comparison, "=="].freeze
|
||||||
|
COMPARISON_GREATER_THAN = [:comparison, ">"].freeze
|
||||||
|
COMPARISON_GREATER_THAN_OR_EQUAL = [:comparison, ">="].freeze
|
||||||
|
COMPARISON_LESS_THAN = [:comparison, "<"].freeze
|
||||||
|
COMPARISON_LESS_THAN_OR_EQUAL = [:comparison, "<="].freeze
|
||||||
|
COMPARISON_NOT_EQUAL_ALT = [:comparison, "<>"].freeze
|
||||||
|
CONTAINS = /contains(?=\s)/
|
||||||
|
DASH = [:dash, "-"].freeze
|
||||||
|
DOT = [:dot, "."].freeze
|
||||||
|
DOTDOT = [:dotdot, ".."].freeze
|
||||||
|
DOT_ORD = ".".ord
|
||||||
|
DOUBLE_STRING_LITERAL = /"[^\"]*"/
|
||||||
|
EOS = [:end_of_string].freeze
|
||||||
|
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
|
||||||
|
NUMBER_LITERAL = /-?\d+(\.\d+)?/
|
||||||
|
OPEN_ROUND = [:open_round, "("].freeze
|
||||||
|
OPEN_SQUARE = [:open_square, "["].freeze
|
||||||
|
PIPE = [:pipe, "|"].freeze
|
||||||
|
QUESTION = [:question, "?"].freeze
|
||||||
|
RUBY_WHITESPACE = [" ", "\t", "\r", "\n", "\f"].freeze
|
||||||
|
SINGLE_STRING_LITERAL = /'[^\']*'/
|
||||||
|
WHITESPACE_OR_NOTHING = /\s*/
|
||||||
|
|
||||||
|
COMPARISON_JUMP_TABLE = [].tap do |table|
|
||||||
|
table["=".ord] = [].tap do |sub_table|
|
||||||
|
sub_table["=".ord] = COMPARISON_EQUAL
|
||||||
|
sub_table.freeze
|
||||||
|
end
|
||||||
|
table["!".ord] = [].tap do |sub_table|
|
||||||
|
sub_table["=".ord] = COMPARISION_NOT_EQUAL
|
||||||
|
sub_table.freeze
|
||||||
|
end
|
||||||
|
table["<".ord] = [].tap do |sub_table|
|
||||||
|
sub_table["=".ord] = COMPARISON_LESS_THAN_OR_EQUAL
|
||||||
|
sub_table[">".ord] = COMPARISON_NOT_EQUAL_ALT
|
||||||
|
RUBY_WHITESPACE.each { |c| sub_table[c.ord] = COMPARISON_LESS_THAN }
|
||||||
|
sub_table.freeze
|
||||||
|
end
|
||||||
|
table[">".ord] = [].tap do |sub_table|
|
||||||
|
sub_table["=".ord] = COMPARISON_GREATER_THAN_OR_EQUAL
|
||||||
|
RUBY_WHITESPACE.each { |c| sub_table[c.ord] = COMPARISON_GREATER_THAN }
|
||||||
|
sub_table.freeze
|
||||||
|
end
|
||||||
|
table.freeze
|
||||||
|
end
|
||||||
|
|
||||||
|
NEXT_MATCHER_JUMP_TABLE = [].tap do |table|
|
||||||
|
"a".upto("z") do |c|
|
||||||
|
table[c.ord] = [:id, IDENTIFIER].freeze
|
||||||
|
table[c.upcase.ord] = [:id, IDENTIFIER].freeze
|
||||||
|
end
|
||||||
|
table["_".ord] = [:id, IDENTIFIER].freeze
|
||||||
|
|
||||||
|
"0".upto("9") do |c|
|
||||||
|
table[c.ord] = [:number, NUMBER_LITERAL].freeze
|
||||||
|
end
|
||||||
|
table["-".ord] = [:number, NUMBER_LITERAL].freeze
|
||||||
|
|
||||||
|
table["'".ord] = [:string, SINGLE_STRING_LITERAL].freeze
|
||||||
|
table["\"".ord] = [:string, DOUBLE_STRING_LITERAL].freeze
|
||||||
|
table.freeze
|
||||||
|
end
|
||||||
|
|
||||||
|
SPECIAL_TABLE = [].tap do |table|
|
||||||
|
table["|".ord] = PIPE
|
||||||
|
table[".".ord] = DOT
|
||||||
|
table[":".ord] = COLON
|
||||||
|
table[",".ord] = COMMA
|
||||||
|
table["[".ord] = OPEN_SQUARE
|
||||||
|
table["]".ord] = CLOSE_SQUARE
|
||||||
|
table["(".ord] = OPEN_ROUND
|
||||||
|
table[")".ord] = CLOSE_ROUND
|
||||||
|
table["?".ord] = QUESTION
|
||||||
|
table["-".ord] = DASH
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(input)
|
||||||
|
@ss = StringScanner.new(input)
|
||||||
|
end
|
||||||
|
|
||||||
|
def tokenize
|
||||||
|
@output = []
|
||||||
|
|
||||||
|
until @ss.eos?
|
||||||
|
@ss.skip(WHITESPACE_OR_NOTHING)
|
||||||
|
|
||||||
|
break if @ss.eos?
|
||||||
|
|
||||||
|
peeked = @ss.peek_byte
|
||||||
|
|
||||||
|
if (special = SPECIAL_TABLE[peeked])
|
||||||
|
@ss.scan_byte
|
||||||
|
if special == DOT && @ss.peek_byte == DOT_ORD
|
||||||
|
@ss.scan_byte
|
||||||
|
@output << DOTDOT
|
||||||
|
else
|
||||||
|
@output << special
|
||||||
|
end
|
||||||
|
elsif (sub_table = COMPARISON_JUMP_TABLE[peeked])
|
||||||
|
@ss.scan_byte
|
||||||
|
next_peeked = @ss.peek_byte
|
||||||
|
if (found = sub_table[next_peeked])
|
||||||
|
@output << found
|
||||||
|
@ss.scan_byte
|
||||||
|
else
|
||||||
|
raise SyntaxError, "Unexpected character #{peeked.chr}"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
type, pattern = NEXT_MATCHER_JUMP_TABLE[peeked]
|
||||||
|
|
||||||
|
if type && (t = @ss.scan(pattern))
|
||||||
|
# rubocop:disable Metrics/BlockNesting
|
||||||
|
@output << if type == :id && t == "contains"
|
||||||
|
COMPARISON_CONTAINS
|
||||||
|
else
|
||||||
|
[type, t]
|
||||||
|
end
|
||||||
|
# rubocop:enable Metrics/BlockNesting
|
||||||
|
else
|
||||||
|
raise SyntaxError, "Unexpected character #{peeked.chr}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@output << EOS
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Lexer = StringScanner.instance_methods.include?(:scan_byte) ? Lexer2 : Lexer1
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ module Liquid
|
|||||||
str = consume
|
str = consume
|
||||||
str << variable_lookups
|
str << variable_lookups
|
||||||
when :open_square
|
when :open_square
|
||||||
str = consume
|
str = consume.dup
|
||||||
str << expression
|
str << expression
|
||||||
str << consume(:close_square)
|
str << consume(:close_square)
|
||||||
str << variable_lookups
|
str << variable_lookups
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ Gem::Specification.new do |s|
|
|||||||
|
|
||||||
s.require_path = "lib"
|
s.require_path = "lib"
|
||||||
|
|
||||||
|
s.add_dependency("strscan")
|
||||||
|
s.add_dependency("bigdecimal")
|
||||||
|
|
||||||
s.add_development_dependency('rake', '~> 13.0')
|
s.add_development_dependency('rake', '~> 13.0')
|
||||||
s.add_development_dependency('minitest')
|
s.add_development_dependency('minitest')
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
require 'benchmark/ips'
|
require 'benchmark/ips'
|
||||||
require_relative 'theme_runner'
|
require_relative 'theme_runner'
|
||||||
|
|
||||||
|
RubyVM::YJIT.enable if defined?(RubyVM::YJIT)
|
||||||
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||||
profiler = ThemeRunner.new
|
profiler = ThemeRunner.new
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "benchmark/ips"
|
||||||
|
|
||||||
|
# benchmark liquid lexing
|
||||||
|
|
||||||
|
require 'liquid'
|
||||||
|
|
||||||
|
RubyVM::YJIT.enable
|
||||||
|
|
||||||
|
EXPRESSIONS = [
|
||||||
|
"foo[1..2].baz",
|
||||||
|
"12.0",
|
||||||
|
"foo.bar.based",
|
||||||
|
"21 - 62",
|
||||||
|
"foo.bar.baz",
|
||||||
|
"foo > 12",
|
||||||
|
"foo < 12",
|
||||||
|
"foo <= 12",
|
||||||
|
"foo >= 12",
|
||||||
|
"foo <> 12",
|
||||||
|
"foo == 12",
|
||||||
|
"foo != 12",
|
||||||
|
"foo contains 12",
|
||||||
|
"foo contains 'bar'",
|
||||||
|
"foo != 'bar'",
|
||||||
|
"'foo' contains 'bar'",
|
||||||
|
'234089',
|
||||||
|
]
|
||||||
|
|
||||||
|
Benchmark.ips do |x|
|
||||||
|
x.config(time: 10, warmup: 5)
|
||||||
|
|
||||||
|
x.report("Liquid::Lexer1#tokenize") do
|
||||||
|
EXPRESSIONS.each do |expr|
|
||||||
|
l = Liquid::Lexer1.new(expr)
|
||||||
|
l.tokenize
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
x.report("Liquid::Lexer2#tokenize") do
|
||||||
|
EXPRESSIONS.each do |expr|
|
||||||
|
l = Liquid::Lexer2.new(expr)
|
||||||
|
l.tokenize
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
x.compare!
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user