From 1b1a40c60605df47e91477a76b2c49d7e555dd24 Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Tue, 29 Jul 2025 14:03:19 -0400 Subject: [PATCH 1/6] make theme_runner actually useful outside of the performance benchmarks --- performance/benchmark.rb | 16 +++-- performance/theme_runner.rb | 119 +++++++++++++++++++----------------- 2 files changed, 74 insertions(+), 61 deletions(-) diff --git a/performance/benchmark.rb b/performance/benchmark.rb index b61e9057c..9d68e727d 100644 --- a/performance/benchmark.rb +++ b/performance/benchmark.rb @@ -3,7 +3,13 @@ require 'benchmark/ips' require_relative 'theme_runner' -RubyVM::YJIT.enable if defined?(RubyVM::YJIT) +if defined?(RubyVM::YJIT) + RubyVM::YJIT.enable + puts "* YJIT enabled" +else + puts "* YJIT not enabled" +end + Liquid::Environment.default.error_mode = ARGV.first.to_sym if ARGV.first profiler = ThemeRunner.new @@ -18,8 +24,8 @@ 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" + x.report("tokenize:") { profiler.tokenize_all } if phase == "all" || phase == "tokenize" + x.report("parse:") { profiler.compile_all } if phase == "all" || phase == "parse" + x.report("render:") { profiler.render_all } if phase == "all" || phase == "render" + x.report("parse & render:") { profiler.run_all } if phase == "all" || phase == "run" end diff --git a/performance/theme_runner.rb b/performance/theme_runner.rb index 469503670..aa5b9cb93 100644 --- a/performance/theme_runner.rb +++ b/performance/theme_runner.rb @@ -26,22 +26,38 @@ def read_template_file(template_path) # Initialize a new liquid ThemeRunner instance # Will load all templates into memory, do this now so that we don't profile IO. def initialize - @tests = Dir[__dir__ + '/tests/**/*.liquid'].collect do |test| + @tests = [] + Dir[__dir__ + '/tests/**/*.liquid'].each do |test| next if File.basename(test) == 'theme.liquid' - theme_path = File.dirname(test) + '/theme.liquid' - { + test_name = File.basename(File.dirname(test)) + "/" + File.basename(test) + theme_name = File.basename(File.dirname(test)) + template_name = File.basename(test) + layout_path = File.dirname(test) + '/theme.liquid' + + test = { + test_name: test_name, liquid: File.read(test), - layout: (File.file?(theme_path) ? File.read(theme_path) : nil), - template_name: test, + layout: File.file?(layout_path) ? File.read(layout_path) : nil, + template_name: template_name, + theme_name: theme_name, + theme_path: File.realpath(File.dirname(test)), } - end.compact - compile_all_tests + @tests << test + end end + def find_test(test_name) + @tests.find do |test_hash| + test_hash[:test_name] == test_name + end + end + + attr_accessor :tests + # `compile` will test just the compilation portion of liquid without any templates - def compile + def compile_all @tests.each do |test_hash| Liquid::Template.new.parse(test_hash[:liquid]) Liquid::Template.new.parse(test_hash[:layout]) @@ -49,7 +65,7 @@ def compile end # `tokenize` will just test the tokenizen portion of liquid without any templates - def tokenize + def tokenize_all ss = StringScanner.new("") @tests.each do |test_hash| tokenizer = Liquid::Tokenizer.new( @@ -62,78 +78,69 @@ def tokenize 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| - compile_and_render(liquid, layout, assigns, page_template, template_name) + def run_all + @tests.each do |test| + compile_and_render(test) end end # `render` is called to benchmark just the render portion of liquid - def render + def render_all + @compiled_tests ||= compile_all_tests @compiled_tests.each do |test| - tmpl = test[:tmpl] - assigns = test[:assigns] - layout = test[:layout] - - if layout - assigns['content_for_layout'] = tmpl.render!(assigns) - layout.render!(assigns) - else - tmpl.render!(assigns) - end + render_template(test) end end + def run_one_test(test_name) + test = find_test(test_name) + compile_and_render(test) + end + private - def render_layout(template, layout, assigns) - assigns['content_for_layout'] = template.render!(assigns) - layout&.render!(assigns) + def render_template(compiled_test) + tmpl, assigns, layout = compiled_test.values_at(:tmpl, :assigns, :layout) + if layout + assigns['content_for_layout'] = tmpl.render!(assigns) + layout.render!(assigns) + else + tmpl.render!(assigns) + end end - def compile_and_render(template, layout, assigns, page_template, template_file) - compiled_test = compile_test(template, layout, assigns, page_template, template_file) - render_layout(compiled_test[:tmpl], compiled_test[:layout], compiled_test[:assigns]) + def compile_and_render(test) + compiled_test = compile_test(test[:liquid], test[:layout], test[:template_name], test[:theme_path]) + render_template(compiled_test) end def compile_all_tests @compiled_tests = [] - each_test do |liquid, layout, assigns, page_template, template_name| - @compiled_tests << compile_test(liquid, layout, assigns, page_template, template_name) + @tests.each do |test_hash| + @compiled_tests << compile_test( + test_hash[:liquid], + test_hash[:layout], + test_hash[:template_name], + test_hash[:theme_path], + ) end @compiled_tests end - def compile_test(template, layout, assigns, page_template, template_file) - tmpl = init_template(page_template, template_file) - parsed_template = tmpl.parse(template).dup + def compile_test(template, layout, template_name, theme_path) + tmpl = Liquid::Template.new + tmpl.assigns['page_title'] = 'Page title' + tmpl.assigns['template'] = template_name + tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(theme_path) + + parsed_template = tmpl.parse(template) + assigns = Database.tables.dup if layout - parsed_layout = tmpl.parse(layout) + parsed_layout = tmpl.parse(layout).dup { tmpl: parsed_template, assigns: assigns, layout: parsed_layout } else { tmpl: parsed_template, assigns: assigns } end end - - # utility method with similar functionality needed in `compile_all_tests` and `run` - def each_test - # Dup assigns because will make some changes to them - assigns = Database.tables.dup - - @tests.each do |test_hash| - # Compute page_template outside of profiler run, uninteresting to profiler - page_template = File.basename(test_hash[:template_name], File.extname(test_hash[:template_name])) - yield(test_hash[:liquid], test_hash[:layout], assigns, page_template, test_hash[:template_name]) - end - end - - # set up a new Liquid::Template object for use in `compile_and_render` and `compile_test` - def init_template(page_template, template_file) - tmpl = Liquid::Template.new - tmpl.assigns['page_title'] = 'Page title' - tmpl.assigns['template'] = page_template - tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file)) - tmpl - end end From 41d0d6d51a8145d486ca35aa30b6c62fad6847ad Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Tue, 29 Jul 2025 17:11:18 -0400 Subject: [PATCH 2/6] the performance runner wasn't actually working before --- performance/shopify/database.rb | 25 +++++++-- performance/shopify/vision.database.yml | 68 ++++++++++--------------- performance/theme_runner.rb | 61 ++++++++++++---------- 3 files changed, 81 insertions(+), 73 deletions(-) diff --git a/performance/shopify/database.rb b/performance/shopify/database.rb index 893c4e9d5..922fde344 100644 --- a/performance/shopify/database.rb +++ b/performance/shopify/database.rb @@ -32,19 +32,36 @@ def self.tables end end - # Some standard direct accessors so that the specialized templates - # render correctly - db['collection'] = db['collections'].values.first db['product'] = db['products'].values.first db['blog'] = db['blogs'].values.first db['article'] = db['blog']['articles'].first - db['cart'] = { + # Some standard direct accessors so that the specialized templates + # render correctly + db['collection'] = db['collections'].values.first + db['collection']['tags'] = db['collection']['products'].map { |product| product['tags'] }.flatten.uniq.sort + + db['tags'] = db['collection']['tags'][0..1] + db['all_tags'] = db['products'].values.map { |product| product['tags'] }.flatten.uniq.sort + db['current_tags'] = db['collection']['tags'][0..1] + db['handle'] = db['collection']['handle'] + + db['cart'] = { 'total_price' => db['line_items'].values.inject(0) { |sum, item| sum + item['line_price'] * item['quantity'] }, 'item_count' => db['line_items'].values.inject(0) { |sum, item| sum + item['quantity'] }, 'items' => db['line_items'].values, } + db['linklists'] = db['link_lists'] + + db['shop'] = { + 'name' => 'Snowdevil', + 'currency' => 'USD', + 'money_format' => '${{amount}}', + 'money_with_currency_format' => '${{amount}} USD', + 'money_format_with_currency' => 'USD ${{amount}}', + } + db end end diff --git a/performance/shopify/vision.database.yml b/performance/shopify/vision.database.yml index 9f38c3048..15e206c24 100644 --- a/performance/shopify/vision.database.yml +++ b/performance/shopify/vision.database.yml @@ -345,8 +345,7 @@ products: featured_image: products/arbor_draft.jpg images: - products/arbor_draft.jpg - description: - The Arbor Draft snowboard wouldn't exist if Polynesians hadn't figured out how to surf hundreds of years ago. But the Draft does exist, and it's here to bring your urban and park riding to a new level. The board's freaky Tiki design pays homage to culture that inspired snowboarding. It's designed to spin with ease, land smoothly, lock hook-free onto rails, and take the abuse of a pavement pounding or twelve. The Draft will pop off kickers with authority and carve solidly across the pipe. The Draft features targeted Koa wood die cuts inlayed into the deck that enhance the flex pattern. Now bow down to riding's ancestors. + description: The Arbor Draft snowboard wouldn't exist if Polynesians hadn't figured out how to surf hundreds of years ago. But the Draft does exist, and it's here to bring your urban and park riding to a new level. The board's freaky Tiki design pays homage to culture that inspired snowboarding. It's designed to spin with ease, land smoothly, lock hook-free onto rails, and take the abuse of a pavement pounding or twelve. The Draft will pop off kickers with authority and carve solidly across the pipe. The Draft features targeted Koa wood die cuts inlayed into the deck that enhance the flex pattern. Now bow down to riding's ancestors. variants: - *product-1-var-1 - *product-1-var-2 @@ -377,8 +376,7 @@ products: featured_image: products/element58.jpg images: - products/element58.jpg - description: - The Element is a technically advanced all-mountain board for riders who readily transition from one terrain, snow condition, or riding style to another. Its balanced design provides the versatility needed for the true ride-it-all experience. The Element is exceedingly lively, freely initiates, and holds a tight edge at speed. Its structural real-wood topsheet is made with book-matched Koa. + description: The Element is a technically advanced all-mountain board for riders who readily transition from one terrain, snow condition, or riding style to another. Its balanced design provides the versatility needed for the true ride-it-all experience. The Element is exceedingly lively, freely initiates, and holds a tight edge at speed. Its structural real-wood topsheet is made with book-matched Koa. variants: - *product-2-var-1 @@ -411,8 +409,7 @@ products: - products/technine1.jpg - products/technine2.jpg - products/technine_detail.jpg - description: - 2005 Technine Comic Series Description The Comic series was developed to be the ultimate progressive freestyle board in the Technine line. Dependable edge control and a perfect flex pattern for jumping in the park or out of bounds. Landins and progression will come easy with this board and it will help your riding progress to the next level. Street rails, park jibs, backcountry booters and park jumps, this board will do it all. + description: 2005 Technine Comic Series Description The Comic series was developed to be the ultimate progressive freestyle board in the Technine line. Dependable edge control and a perfect flex pattern for jumping in the park or out of bounds. Landins and progression will come easy with this board and it will help your riding progress to the next level. Street rails, park jibs, backcountry booters and park jumps, this board will do it all. variants: - *product-3-var-1 - *product-3-var-2 @@ -446,8 +443,7 @@ products: images: - products/technine3.jpg - products/technine4.jpg - description: - 2005 Technine Comic Series Description The Comic series was developed to be the ultimate progressive freestyle board in the Technine line. Dependable edge control and a perfect flex pattern for jumping in the park or out of bounds. Landins and progression will come easy with this board and it will help your riding progress to the next level. Street rails, park jibs, backcountry booters and park jumps, this board will do it all. + description: 2005 Technine Comic Series Description The Comic series was developed to be the ultimate progressive freestyle board in the Technine line. Dependable edge control and a perfect flex pattern for jumping in the park or out of bounds. Landins and progression will come easy with this board and it will help your riding progress to the next level. Street rails, park jibs, backcountry booters and park jumps, this board will do it all. variants: - *product-4-var-1 @@ -478,8 +474,7 @@ products: featured_image: products/burton.jpg images: - products/burton.jpg - description: - The Burton boots are particularly well on snowboards. The very best thing about them is that the according picture is cubic. This makes testing in a Vision testing environment very easy. + description: The Burton boots are particularly well on snowboards. The very best thing about them is that the according picture is cubic. This makes testing in a Vision testing environment very easy. variants: - *product-5-var-1 - *product-5-var-2 @@ -516,8 +511,7 @@ products: featured_image: products/ducati.jpg images: - products/ducati.jpg - description: -

‘S’ PERFORMANCE

+ description:

‘S’ PERFORMANCE

Producing 170hp (125kW) and with a dry weight of just 169kg (372.6lb), the new 1198 S now incorporates more World Superbike technology than ever before by taking the 1198 motor and adding top-of-the-range suspension, lightweight chassis components and a true racing-style traction control system designed for road use.

The high performance, fully adjustable 43mm Öhlins forks, which sport low friction titanium nitride-treated fork sliders, respond effortlessly to every imperfection in the tarmac. Beyond their advanced engineering solutions, one of the most important characteristics of Öhlins forks is their ability to communicate the condition and quality of the tyre-to-road contact patch, a feature that puts every rider in superior control. The suspension set-up at the rear is complemented with a fully adjustable Öhlins rear shock equipped with a ride enhancing top-out spring and mounted to a single-sided swingarm for outstanding drive and traction. The front-to-rear Öhlins package is completed with a control-enhancing adjustable steering damper.

variants: @@ -636,7 +630,6 @@ products: - *product-9-var-2 - *product-9-var-3 - # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- # Line Items # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- @@ -644,8 +637,8 @@ products: line_items: - &line_item-1 id: 1 - title: 'Arbor Draft' - subtitle: '151cm' + title: "Arbor Draft" + subtitle: "151cm" price: 29900 line_price: 29900 quantity: 1 @@ -654,8 +647,8 @@ line_items: - &line_item-2 id: 2 - title: 'Comic ~ Orange' - subtitle: '159cm' + title: "Comic ~ Orange" + subtitle: "159cm" price: 19900 line_price: 39800 quantity: 2 @@ -681,7 +674,7 @@ links: - &link-4 id: 4 title: Powered by Shopify - url: 'http://shopify.com' + url: "http://shopify.com" - &link-5 id: 5 title: About Us @@ -715,8 +708,6 @@ links: title: Catalog url: /collections/all - - # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- # Link Lists # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- @@ -724,8 +715,8 @@ links: link_lists: - &link-list-1 id: 1 - title: 'Main Menu' - handle: 'main-menu' + title: "Main Menu" + handle: "main-menu" links: - *link-12 - *link-5 @@ -733,8 +724,8 @@ link_lists: - *link-8 - &link-list-2 id: 1 - title: 'Footer Menu' - handle: 'footer' + title: "Footer Menu" + handle: "footer" links: - *link-5 - *link-6 @@ -768,8 +759,7 @@ collections: title: Snowboards handle: snowboards url: /collections/snowboards - description: -

This is a description for my Snowboards collection.

+ description:

This is a description for my Snowboards collection.

products: - *product-1 - *product-2 @@ -787,8 +777,8 @@ collections: - &collection-5 id: 5 title: Paginated Sale - handle: 'paginated-sale' - url: '/collections/paginated-sale' + handle: "paginated-sale" + url: "/collections/paginated-sale" products: - *product-1 - *product-2 @@ -799,8 +789,8 @@ collections: - &collection-6 id: 6 title: All products - handle: 'all' - url: '/collections/all' + handle: "all" + url: "/collections/all" products: - *product-7 - *product-8 @@ -812,7 +802,6 @@ collections: - *product-4 - *product-5 - # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- # Pages and Blogs # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- @@ -823,8 +812,7 @@ pages: handle: contact url: /pages/contact author: Tobi - content: - "

You can contact us via phone under (555) 567-2222.

+ content: "

You can contact us via phone under (555) 567-2222.

Our retail store is located at Rue d'Avignon 32, Avignon (Provence).

Opening Hours:
Monday through Friday: 9am - 6pm
Saturday: 10am - 3pm
Sunday: closed

" created_at: 2005-04-04 12:00 @@ -874,12 +862,12 @@ blogs: url: /blogs/news articles: - id: 3 - title: 'Welcome to the new Foo Shop' + title: "Welcome to the new Foo Shop" author: Daniel content:

Welcome to your Shopify store! The jaded Pixel crew is really glad you decided to take Shopify for a spin.

To help you get you started with Shopify, here are a couple of tips regarding what you see on this page.

The text you see here is an article. To edit this article, create new articles or create new pages you can go to the Blogs & Pages tab of the administration menu.

The Shopify t-shirt above is a product and selling products is what Shopify is all about. To edit this product, or create new products you can go to the Products Tab in of the administration menu.

While you're looking around be sure to check out the Collections and Navigations tabs and soon you will be well on your way to populating your site.

And of course don't forget to browse the theme gallery to pick a new look for your shop!

Shopify is in beta
If you would like to make comments or suggestions please visit us in the Shopify Forums or drop us an email.

created_at: 2005-04-04 16:00 - id: 4 - title: 'Breaking News: Restock on all sales products' + title: "Breaking News: Restock on all sales products" author: Tobi content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. created_at: 2005-04-04 12:00 @@ -891,13 +879,12 @@ blogs: url: /blogs/bigcheese-blog articles: - id: 1 - title: 'One thing you probably did not know yet...' + title: "One thing you probably did not know yet..." author: Justin content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. created_at: 2005-04-04 16:00 comments: - - - id: 1 + - id: 1 author: John Smith email: john@smith.com content: Wow...great article man. @@ -905,8 +892,7 @@ blogs: created_at: 2009-01-01 12:00 updated_at: 2009-02-01 12:00 url: "" - - - id: 2 + - id: 2 author: John Jones email: john@jones.com content: I really enjoyed this article. And I love your shop! It's awesome. Shopify rocks! @@ -932,7 +918,7 @@ blogs: url: /blogs/paginated-blog articles: - id: 6 - title: 'One thing you probably did not know yet...' + title: "One thing you probably did not know yet..." author: Justin content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. created_at: 2005-04-04 16:00 diff --git a/performance/theme_runner.rb b/performance/theme_runner.rb index aa5b9cb93..87b110dd3 100644 --- a/performance/theme_runner.rb +++ b/performance/theme_runner.rb @@ -25,15 +25,17 @@ def read_template_file(template_path) # Initialize a new liquid ThemeRunner instance # Will load all templates into memory, do this now so that we don't profile IO. - def initialize + def initialize(strictness: {}) + @strictness = strictness @tests = [] Dir[__dir__ + '/tests/**/*.liquid'].each do |test| next if File.basename(test) == 'theme.liquid' - test_name = File.basename(File.dirname(test)) + "/" + File.basename(test) - theme_name = File.basename(File.dirname(test)) - template_name = File.basename(test) - layout_path = File.dirname(test) + '/theme.liquid' + theme_path = File.realpath(File.dirname(test)) + theme_name = File.basename(theme_path) + test_name = theme_name + "/" + File.basename(test) + template_name = File.basename(test, '.liquid') + layout_path = theme_path + '/theme.liquid' test = { test_name: test_name, @@ -41,7 +43,7 @@ def initialize layout: File.file?(layout_path) ? File.read(layout_path) : nil, template_name: template_name, theme_name: theme_name, - theme_path: File.realpath(File.dirname(test)), + theme_path: theme_path, } @tests << test @@ -100,47 +102,50 @@ def run_one_test(test_name) private def render_template(compiled_test) - tmpl, assigns, layout = compiled_test.values_at(:tmpl, :assigns, :layout) + tmpl, layout, assigns = compiled_test.values_at(:tmpl, :layout, :assigns) if layout - assigns['content_for_layout'] = tmpl.render!(assigns) - layout.render!(assigns) + assigns['content_for_layout'] = tmpl.render!(assigns, @strictness) + layout = layout.render!(assigns, @strictness) + layout else - tmpl.render!(assigns) + tmpl.render!(assigns, @strictness) end end def compile_and_render(test) - compiled_test = compile_test(test[:liquid], test[:layout], test[:template_name], test[:theme_path]) + compiled_test = compile_test(test) render_template(compiled_test) end def compile_all_tests @compiled_tests = [] @tests.each do |test_hash| - @compiled_tests << compile_test( - test_hash[:liquid], - test_hash[:layout], - test_hash[:template_name], - test_hash[:theme_path], - ) + @compiled_tests << compile_test(test_hash) end @compiled_tests end - def compile_test(template, layout, template_name, theme_path) - tmpl = Liquid::Template.new - tmpl.assigns['page_title'] = 'Page title' - tmpl.assigns['template'] = template_name - tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(theme_path) - - parsed_template = tmpl.parse(template) + def compile_test(test_hash) + theme_path, template_name, layout, liquid = test_hash.values_at(:theme_path, :template_name, :layout, :liquid) assigns = Database.tables.dup + assigns.merge!({ + 'title' => 'Page title', + 'page_title' => 'Page title', + 'content_for_header' => '', + 'template' => template_name, + }) + + fs = ThemeRunner::FileSystem.new(theme_path) + + result = {} + result[:assigns] = assigns + result[:tmpl] = Liquid::Template.parse(liquid, registers: { file_system: fs }) + if layout - parsed_layout = tmpl.parse(layout).dup - { tmpl: parsed_template, assigns: assigns, layout: parsed_layout } - else - { tmpl: parsed_template, assigns: assigns } + result[:layout] = Liquid::Template.parse(layout, registers: { file_system: fs }) end + + result end end From baee70ae1f2aa40a046c87b965570a92e6395803 Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Tue, 29 Jul 2025 18:07:31 -0400 Subject: [PATCH 3/6] also fix profiler --- performance/profile.rb | 70 +++++++++++++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/performance/profile.rb b/performance/profile.rb index 70740778d..c1c46e64c 100644 --- a/performance/profile.rb +++ b/performance/profile.rb @@ -1,26 +1,68 @@ # frozen_string_literal: true require 'stackprof' +require 'fileutils' require_relative 'theme_runner' +output_dir = ENV['OUTPUT_DIR'] || "/tmp/liquid-performance" +FileUtils.mkdir_p(output_dir) + Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first profiler = ThemeRunner.new -profiler.run - -[:cpu, :object].each do |profile_type| - puts "Profiling in #{profile_type} mode..." - results = StackProf.run(mode: profile_type) do - 200.times do - profiler.run - end +profiler.run_all # warmup + +puts +puts "writing to #{output_dir}/cpu.profile" +StackProf.run(mode: :cpu, raw: true, out: "#{output_dir}/cpu.profile") do + 100.times do + profiler.run_all + end +end + +puts "writing to #{output_dir}/object.profile" +StackProf.run(mode: :object, raw: true, out: "#{output_dir}/object.profile") do + 100.times do + profiler.run_all end +end - if profile_type == :cpu && (graph_filename = ENV['GRAPH_FILENAME']) - File.open(graph_filename, 'w') do |f| - StackProf::Report.new(results).print_graphviz(nil, f) - end +puts "running cpu profile" +results = StackProf.run(mode: :cpu) do + 100.times do + profiler.run_all end +end + +File.open("#{output_dir}/cpu.graph.dot", 'w+') do |f| + puts("Writing cpu graph to #{File.join(output_dir, "cpu.graph.dot")}") + StackProf::Report.new(results).print_graphviz({}, f) +end + +StackProf::Report.new(results).print_text(false, 20) +puts("Writing cpu profile to #{File.join(output_dir, "cpu.profile")}") +File.write(File.join(output_dir, "cpu.profile"), Marshal.dump(results)) + +puts +puts "Profiling in object mode..." +results = StackProf.run(mode: :object) do + 100.times do + profiler.run_all + end +end + +File.open("#{output_dir}/object.graph.dot", 'w+') do |f| + puts("Writing object graph to #{File.join(output_dir, "object.graph.dot")}") + StackProf::Report.new(results).print_graphviz({}, f) +end - StackProf::Report.new(results).print_text(false, 20) - File.write(ENV['FILENAME'] + "." + profile_type.to_s, Marshal.dump(results)) if ENV['FILENAME'] +StackProf::Report.new(results).print_text(false, 20) +puts("Writing object profile to #{File.join(output_dir, "object.profile")}") +File.write(File.join(output_dir, "object.profile"), Marshal.dump(results)) +puts +puts +puts "files in #{output_dir}:" +Dir.glob("#{output_dir}/*").each do |file| + puts " #{file}" end +puts "Recommended:" +puts "stackprof --d3-flamegraph #{output_dir}/cpu.profile > #{output_dir}/flame.html" From 2420853020c9eccbeca2354cb991b5084e100764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20L=C3=BCtke?= Date: Tue, 29 Jul 2025 18:53:00 -0400 Subject: [PATCH 4/6] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- performance/theme_runner.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/performance/theme_runner.rb b/performance/theme_runner.rb index 87b110dd3..55529d9a6 100644 --- a/performance/theme_runner.rb +++ b/performance/theme_runner.rb @@ -56,7 +56,7 @@ def find_test(test_name) end end - attr_accessor :tests + attr_reader :tests # `compile` will test just the compilation portion of liquid without any templates def compile_all @@ -105,8 +105,8 @@ def render_template(compiled_test) tmpl, layout, assigns = compiled_test.values_at(:tmpl, :layout, :assigns) if layout assigns['content_for_layout'] = tmpl.render!(assigns, @strictness) - layout = layout.render!(assigns, @strictness) - layout + rendered_layout = layout.render!(assigns, @strictness) + rendered_layout else tmpl.render!(assigns, @strictness) end From 14e70cf5baa4db08a3156fb3ebffd51e312417cd Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Tue, 29 Jul 2025 18:55:13 -0400 Subject: [PATCH 5/6] memory-profiler update --- performance/memory_profile.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/performance/memory_profile.rb b/performance/memory_profile.rb index e2934297a..ac2574d24 100644 --- a/performance/memory_profile.rb +++ b/performance/memory_profile.rb @@ -57,7 +57,7 @@ def sanitize(string) runner = ThemeRunner.new Profiler.run do |x| - x.profile('parse') { runner.compile } - x.profile('render') { runner.render } + x.profile('parse') { runner.compile_all } + x.profile('render') { runner.render_all } x.tabulate end From 544b512e7b8a201084887bcd86b6fd5259e76e73 Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Tue, 29 Jul 2025 20:15:22 -0400 Subject: [PATCH 6/6] more profile cleanups --- performance/profile.rb | 60 ++++++++++++------------------------------ 1 file changed, 17 insertions(+), 43 deletions(-) diff --git a/performance/profile.rb b/performance/profile.rb index c1c46e64c..7a00230fa 100644 --- a/performance/profile.rb +++ b/performance/profile.rb @@ -11,58 +11,32 @@ profiler = ThemeRunner.new profiler.run_all # warmup -puts -puts "writing to #{output_dir}/cpu.profile" -StackProf.run(mode: :cpu, raw: true, out: "#{output_dir}/cpu.profile") do - 100.times do - profiler.run_all - end -end - -puts "writing to #{output_dir}/object.profile" -StackProf.run(mode: :object, raw: true, out: "#{output_dir}/object.profile") do - 100.times do - profiler.run_all - end -end - -puts "running cpu profile" -results = StackProf.run(mode: :cpu) do - 100.times do - profiler.run_all +[:cpu, :object].each do |mode| + puts + puts "Profiling in #{mode} mode..." + puts "writing to #{output_dir}/#{mode}.profile:" + puts + StackProf.run(mode: mode, raw: true, out: "#{output_dir}/#{mode}.profile") do + 200.times do + profiler.run_all + end end -end - -File.open("#{output_dir}/cpu.graph.dot", 'w+') do |f| - puts("Writing cpu graph to #{File.join(output_dir, "cpu.graph.dot")}") - StackProf::Report.new(results).print_graphviz({}, f) -end - -StackProf::Report.new(results).print_text(false, 20) -puts("Writing cpu profile to #{File.join(output_dir, "cpu.profile")}") -File.write(File.join(output_dir, "cpu.profile"), Marshal.dump(results)) -puts -puts "Profiling in object mode..." -results = StackProf.run(mode: :object) do - 100.times do - profiler.run_all + result = StackProf.run(mode: mode) do + 100.times do + profiler.run_all + end end -end -File.open("#{output_dir}/object.graph.dot", 'w+') do |f| - puts("Writing object graph to #{File.join(output_dir, "object.graph.dot")}") - StackProf::Report.new(results).print_graphviz({}, f) + StackProf::Report.new(result).print_text(false, 30) end -StackProf::Report.new(results).print_text(false, 20) -puts("Writing object profile to #{File.join(output_dir, "object.profile")}") -File.write(File.join(output_dir, "object.profile"), Marshal.dump(results)) -puts puts puts "files in #{output_dir}:" Dir.glob("#{output_dir}/*").each do |file| puts " #{file}" end puts "Recommended:" -puts "stackprof --d3-flamegraph #{output_dir}/cpu.profile > #{output_dir}/flame.html" +puts " stackprof --d3-flamegraph #{output_dir}/cpu.profile > #{output_dir}/flame.html" +puts " stackprof --method #{output_dir}/cpu.profile" +puts " etc"