From 7cd8df6fa8e69333c846b612fd49b1960547786b Mon Sep 17 00:00:00 2001 From: Chris Pak Date: Sun, 5 Apr 2026 20:53:53 -0700 Subject: [PATCH] Add ByteTables module and bench_quick.rb benchmark harness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ByteTables provides pre-computed 256-entry boolean lookup arrays for byte classification (IDENT_START, IDENT_CONT, WORD, DIGIT, WHITESPACE) and named constants for delimiter bytes (NEWLINE, DASH, DOT, HASH). bench_quick.rb measures parse µs, render µs, and object allocations for the theme benchmark suite. --- lib/liquid.rb | 1 + lib/liquid/byte_tables.rb | 48 +++++++++++++++++++++++++++++ performance/bench_quick.rb | 62 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 lib/liquid/byte_tables.rb create mode 100644 performance/bench_quick.rb diff --git a/lib/liquid.rb b/lib/liquid.rb index 4d0a71a6..b28d828f 100644 --- a/lib/liquid.rb +++ b/lib/liquid.rb @@ -52,6 +52,7 @@ end require "liquid/version" require "liquid/deprecations" require "liquid/const" +require "liquid/byte_tables" require 'liquid/standardfilters' require 'liquid/file_system' require 'liquid/parser_switching' diff --git a/lib/liquid/byte_tables.rb b/lib/liquid/byte_tables.rb new file mode 100644 index 00000000..b3728e75 --- /dev/null +++ b/lib/liquid/byte_tables.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Liquid + # Pre-computed 256-entry boolean lookup tables for byte classification. + # Built once at load time; used as TABLE[byte] — a single array index + # instead of 3-5 comparison operators per check. + module ByteTables + # [a-zA-Z_] — valid first byte of an identifier + IDENT_START = Array.new(256, false).tap do |t| + (97..122).each { |b| t[b] = true } # a-z + (65..90).each { |b| t[b] = true } # A-Z + t[95] = true # _ + end.freeze + + # [a-zA-Z0-9_-] — valid continuation byte of an identifier + IDENT_CONT = Array.new(256, false).tap do |t| + (97..122).each { |b| t[b] = true } # a-z + (65..90).each { |b| t[b] = true } # A-Z + (48..57).each { |b| t[b] = true } # 0-9 + t[95] = true # _ + t[45] = true # - + end.freeze + + # [a-zA-Z0-9_] — \w equivalent (no hyphen), for tag name scanning + WORD = Array.new(256, false).tap do |t| + (97..122).each { |b| t[b] = true } # a-z + (65..90).each { |b| t[b] = true } # A-Z + (48..57).each { |b| t[b] = true } # 0-9 + t[95] = true # _ + end.freeze + + # [0-9] — ASCII digit + DIGIT = Array.new(256, false).tap do |t| + (48..57).each { |b| t[b] = true } + end.freeze + + # Matches bytes removed by Ruby's String#strip: \x00, \t, \n, \v, \f, \r, space + WHITESPACE = Array.new(256, false).tap do |t| + [0, 9, 10, 11, 12, 13, 32].each { |b| t[b] = true } + end.freeze + + # Byte constants for delimiters and punctuation + NEWLINE = 10 + DASH = 45 # '-' + DOT = 46 # '.' + HASH = 35 # '#' + end +end diff --git a/performance/bench_quick.rb b/performance/bench_quick.rb new file mode 100644 index 00000000..a98333b9 --- /dev/null +++ b/performance/bench_quick.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +# Quick benchmark: measures parse µs, render µs, and object allocations +# Outputs machine-readable metrics to stdout + +require_relative 'theme_runner' + +RubyVM::YJIT.enable if defined?(RubyVM::YJIT) + +runner = ThemeRunner.new + +# Warmup — enough iterations for YJIT to fully optimize hot paths +20.times { runner.compile } +20.times { runner.render } + +GC.start +GC.compact if GC.respond_to?(:compact) + +# Measure parse +parse_times = [] +10.times do + GC.disable + t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC) + runner.compile + t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC) + GC.enable + GC.start + parse_times << (t1 - t0) * 1_000_000 # µs +end + +# Measure render +render_times = [] +10.times do + GC.disable + t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC) + runner.render + t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC) + GC.enable + GC.start + render_times << (t1 - t0) * 1_000_000 # µs +end + +# Measure object allocations for one parse+render cycle +require 'objspace' +GC.start +GC.disable +before = ObjectSpace.count_objects.values_at(:TOTAL).first - ObjectSpace.count_objects.values_at(:FREE).first +runner.compile +runner.render +after = ObjectSpace.count_objects.values_at(:TOTAL).first - ObjectSpace.count_objects.values_at(:FREE).first +GC.enable +allocations = after - before + +parse_us = parse_times.min.round(0) +render_us = render_times.min.round(0) +combined_us = parse_us + render_us + +puts "RESULTS" +puts "parse_us=#{parse_us}" +puts "render_us=#{render_us}" +puts "combined_us=#{combined_us}" +puts "allocations=#{allocations}"