Skip to content

Commit 38436ff

Browse files
authored
Add Liquid IL benchmark (#525)
https://github.com/tobi/liquid-il compiles Liquid code to Ruby code. Let's see how well YJIT and ZJIT can optimize that Ruby code.
1 parent 52889cd commit 38436ff

23 files changed

Lines changed: 4030 additions & 0 deletions

‎benchmarks.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ liquid-c:
1919
liquid-compile:
2020
desc: compiles a chosen-for-profiling Liquid theme repeatedly.
2121
category: headline
22+
liquid-il:
23+
desc: renders LiquidIL-generated Ruby for a set of Liquid templates repeatedly (Liquid compiled to standalone Ruby).
24+
category: headline
2225
liquid-render:
2326
desc: renders a chosen-for-profiling Liquid theme repeatedly.
2427
category: headline

‎benchmarks/liquid-il/Gemfile‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# frozen_string_literal: true
2+
3+
source "https://rubygems.org"
4+
git_source(:github) do |repo_name|
5+
"https://github.com/#{repo_name}.git"
6+
end
7+
8+
# LiquidIL compiles Liquid to Ruby; this benchmark renders its generated Ruby.
9+
# See extract.rb for how generated/ is produced from templates.yml.
10+
gem "liquid-il", github: "tobi/liquid-il", ref: "1b07d162068f1ed2becec1bafd29f36f52dbbd5b"
11+
12+
gem "base64"
13+
gem "bigdecimal"

‎benchmarks/liquid-il/Gemfile.lock‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
GIT
2+
remote: https://github.com/tobi/liquid-il.git
3+
revision: 1b07d162068f1ed2becec1bafd29f36f52dbbd5b
4+
ref: 1b07d162068f1ed2becec1bafd29f36f52dbbd5b
5+
specs:
6+
liquid-il (0.1.0)
7+
8+
GEM
9+
remote: https://rubygems.org/
10+
specs:
11+
base64 (0.3.0)
12+
bigdecimal (4.1.2)
13+
14+
PLATFORMS
15+
ruby
16+
x86_64-linux
17+
18+
DEPENDENCIES
19+
base64
20+
bigdecimal
21+
liquid-il!
22+
23+
CHECKSUMS
24+
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
25+
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
26+
bundler (4.0.16) sha256=d6ca5dd440c24f9abce9844cf44cc8e18c6a553de65a47efb4544137af92c47d
27+
liquid-il (0.1.0)
28+
29+
BUNDLED WITH
30+
4.0.16

‎benchmarks/liquid-il/benchmark.rb‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
require_relative '../../harness/loader'
2+
3+
Dir.chdir __dir__
4+
use_gemfile
5+
6+
require 'liquid_il'
7+
require 'json'
8+
9+
# LiquidIL compiles Liquid templates to standalone Ruby (see extract.rb). We
10+
# render that committed generated Ruby here, so this benchmark exercises the
11+
# machine-generated code a JIT actually compiles: heavy runtime-helper dispatch,
12+
# partial lambdas, string-buffer building, hash lookups, and loops. The exact
13+
# Ruby lives in generated/ and does not drift with the LiquidIL compiler.
14+
manifest = JSON.parse(File.read(File.join(__dir__, 'manifest.json')))
15+
CASES = manifest.map do |m|
16+
require File.join(__dir__, 'generated', "#{m['spec']}.rb")
17+
mod = Object.const_get(m['module'])
18+
assigns = JSON.parse(File.read(File.join(__dir__, 'fixtures', "#{m['spec']}.json")))
19+
[mod, assigns]
20+
end
21+
22+
# Sanity: every generated module must produce output, or the timing is empty.
23+
CASES.each { |mod, assigns| raise "empty render for #{mod}" if mod.render(assigns).to_s.empty? }
24+
25+
run_benchmark(150) do
26+
# Each render is quick; render the whole template set several times per
27+
# iteration to reduce time-measurement noise (mirrors liquid-render).
28+
1000.times do
29+
CASES.each { |mod, assigns| mod.render(assigns) }
30+
end
31+
end

‎benchmarks/liquid-il/extract.rb‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# frozen_string_literal: true
2+
3+
# Regenerate the committed LiquidIL-generated Ruby sample from templates.yml.
4+
#
5+
# LiquidIL (https://github.com/tobi/liquid-il) compiles a Liquid template to
6+
# standalone Ruby (Template#to_ruby), inlining static partials so the result
7+
# renders with no file_system. This script compiles each template in
8+
# templates.yml to a module under generated/, writes its render assigns to
9+
# fixtures/, and validates the standalone module reproduces the template's
10+
# expected output. benchmark.rb then renders those committed modules in a hot
11+
# loop, so the exact Ruby the JIT compiles lives in the repo and does not drift
12+
# with the compiler.
13+
#
14+
# BUNDLE_GEMFILE=$PWD/Gemfile bundle exec ruby extract.rb
15+
#
16+
# The generated/ and fixtures/ output is committed; you only need to re-run
17+
# this when templates.yml or the pinned LiquidIL version changes.
18+
19+
require "json"
20+
require "yaml"
21+
require "liquid_il"
22+
23+
HERE = __dir__
24+
GEN_DIR = File.join(HERE, "generated")
25+
FIX_DIR = File.join(HERE, "fixtures")
26+
27+
# Compile-time file system for the templates' inline partials. Static partials
28+
# are inlined into the caller, so the generated Ruby holds no reference to it.
29+
class SpecFileSystem
30+
def initialize(files) = @files = files || {}
31+
def read_template_file(name, _context = nil)
32+
@files.fetch(name.to_s) { raise LiquidIL::FileSystemError, "no such partial: #{name}" }
33+
end
34+
end
35+
36+
def module_name_for(name)
37+
"LiquidILBench" + name.split(/[^a-zA-Z0-9]/).reject(&:empty?).map(&:capitalize).join
38+
end
39+
40+
def deep_dup(obj)
41+
case obj
42+
when Hash then obj.each_with_object({}) { |(k, v), h| h[k] = deep_dup(v) }
43+
when Array then obj.map { |v| deep_dup(v) }
44+
else obj
45+
end
46+
end
47+
48+
Dir.mkdir(GEN_DIR) unless Dir.exist?(GEN_DIR)
49+
Dir.mkdir(FIX_DIR) unless Dir.exist?(FIX_DIR)
50+
51+
specs = YAML.safe_load(File.read(File.join(HERE, "templates.yml")), aliases: true).fetch("specs")
52+
manifest = []
53+
54+
specs.each do |spec|
55+
name = spec.fetch("name")
56+
mod = module_name_for(name)
57+
58+
ctx = LiquidIL::Context.new(file_system: SpecFileSystem.new(spec["filesystem"]))
59+
template = ctx.parse(spec.fetch("template"))
60+
ruby = template.to_ruby(mod)
61+
62+
# Validate the standalone module in an isolated namespace, no file_system.
63+
probe = Module.new
64+
probe.module_eval(ruby, "generated/#{name}.rb")
65+
produced = probe.const_get(mod).render(deep_dup(spec["environment"] || {}))
66+
expected = spec["expected"]
67+
if expected && produced != expected
68+
raise "#{name}: generated Ruby output does not match expected"
69+
end
70+
71+
File.write(File.join(GEN_DIR, "#{name}.rb"), ruby)
72+
File.write(File.join(FIX_DIR, "#{name}.json"), JSON.pretty_generate(spec["environment"] || {}) + "\n")
73+
manifest << { "spec" => name, "module" => mod, "ruby_bytes" => ruby.bytesize }
74+
printf(" ok %-32s %6d B\n", name, ruby.bytesize)
75+
end
76+
77+
File.write(File.join(HERE, "manifest.json"), JSON.pretty_generate(manifest) + "\n")
78+
puts "Extracted #{manifest.size} module(s); #{manifest.sum { |m| m["ruby_bytes"] }} B generated Ruby."
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{
2+
"product": {
3+
"name": "Premium Widget",
4+
"price": 99.99,
5+
"description": "The finest widget money can buy",
6+
"sku": "WDG-001",
7+
"in_stock": true,
8+
"features": [
9+
"Durable",
10+
"Lightweight",
11+
"Eco-friendly"
12+
]
13+
},
14+
"breadcrumbs": [
15+
{
16+
"label": "Home",
17+
"url": "/"
18+
},
19+
{
20+
"label": "Widgets",
21+
"url": "/widgets"
22+
},
23+
{
24+
"label": "Premium Widget",
25+
"url": null
26+
}
27+
],
28+
"reviews": [
29+
{
30+
"author": "Alice",
31+
"rating": 5,
32+
"text": "Amazing!"
33+
},
34+
{
35+
"author": "Bob",
36+
"rating": 4,
37+
"text": "Very good"
38+
},
39+
{
40+
"author": "Carol",
41+
"rating": 5,
42+
"text": "Love it"
43+
}
44+
],
45+
"related": [
46+
{
47+
"name": "Basic Widget",
48+
"price": 29.99
49+
},
50+
{
51+
"name": "Super Widget",
52+
"price": 149.99
53+
},
54+
{
55+
"name": "Widget Pro",
56+
"price": 199.99
57+
}
58+
]
59+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
{
2+
"notifications": [
3+
{
4+
"type": "order",
5+
"order_id": "ORD-001",
6+
"status": "confirmed",
7+
"total": 149.99,
8+
"items": 3,
9+
"date": "2024-01-15"
10+
},
11+
{
12+
"type": "shipping",
13+
"order_id": "ORD-002",
14+
"carrier": "FedEx",
15+
"tracking": "1234567890",
16+
"eta": "Jan 18"
17+
},
18+
{
19+
"type": "review",
20+
"product": "Leather Jacket",
21+
"customer": "Alice",
22+
"rating": 5,
23+
"snippet": "Amazing quality!"
24+
},
25+
{
26+
"type": "promo",
27+
"code": "SAVE20",
28+
"discount": "20%",
29+
"expires": "Jan 31",
30+
"min_purchase": 50
31+
},
32+
{
33+
"type": "stock",
34+
"product": "Blue Widget",
35+
"sku": "BW-001",
36+
"status": "back_in_stock",
37+
"quantity": 25
38+
},
39+
{
40+
"type": "order",
41+
"order_id": "ORD-003",
42+
"status": "processing",
43+
"total": 89.5,
44+
"items": 2,
45+
"date": "2024-01-16"
46+
},
47+
{
48+
"type": "shipping",
49+
"order_id": "ORD-001",
50+
"carrier": "UPS",
51+
"tracking": "9876543210",
52+
"eta": "Jan 17"
53+
},
54+
{
55+
"type": "review",
56+
"product": "Cotton T-Shirt",
57+
"customer": "Bob",
58+
"rating": 4,
59+
"snippet": "Great fit"
60+
},
61+
{
62+
"type": "promo",
63+
"code": "FREESHIP",
64+
"discount": "Free Shipping",
65+
"expires": "Feb 1",
66+
"min_purchase": 75
67+
},
68+
{
69+
"type": "stock",
70+
"product": "Red Scarf",
71+
"sku": "RS-002",
72+
"status": "low_stock",
73+
"quantity": 3
74+
},
75+
{
76+
"type": "order",
77+
"order_id": "ORD-004",
78+
"status": "shipped",
79+
"total": 299.0,
80+
"items": 1,
81+
"date": "2024-01-14"
82+
},
83+
{
84+
"type": "shipping",
85+
"order_id": "ORD-004",
86+
"carrier": "USPS",
87+
"tracking": "5555555555",
88+
"eta": "Jan 19"
89+
},
90+
{
91+
"type": "review",
92+
"product": "Wool Sweater",
93+
"customer": "Carol",
94+
"rating": 5,
95+
"snippet": "Perfect for winter"
96+
},
97+
{
98+
"type": "promo",
99+
"code": "VIP25",
100+
"discount": "25%",
101+
"expires": "Jan 20",
102+
"min_purchase": 100
103+
},
104+
{
105+
"type": "stock",
106+
"product": "Green Hat",
107+
"sku": "GH-003",
108+
"status": "back_in_stock",
109+
"quantity": 50
110+
}
111+
]
112+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"items": [
3+
1,
4+
2,
5+
3,
6+
4,
7+
5,
8+
6,
9+
7,
10+
8,
11+
9,
12+
10,
13+
11,
14+
12,
15+
13,
16+
14,
17+
15,
18+
16,
19+
17,
20+
18,
21+
19,
22+
20
23+
]
24+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"products": [
3+
{
4+
"name": "Widget",
5+
"price": 9.99,
6+
"description": "A useful widget"
7+
},
8+
{
9+
"name": "Gadget",
10+
"price": 19.99,
11+
"description": "An amazing gadget"
12+
},
13+
{
14+
"name": "Gizmo",
15+
"price": 29.99,
16+
"description": "A fantastic gizmo"
17+
},
18+
{
19+
"name": "Doohickey",
20+
"price": 39.99,
21+
"description": "A wonderful doohickey"
22+
},
23+
{
24+
"name": "Thingamajig",
25+
"price": 49.99,
26+
"description": "A marvelous thingamajig"
27+
}
28+
]
29+
}

0 commit comments

Comments
 (0)