From 8a71d29bfb6128106ae9a4cbca0a9fad2edaf6ce Mon Sep 17 00:00:00 2001 From: Sam Doiron Date: Mon, 18 Apr 2022 10:28:40 -0300 Subject: [PATCH] Add + tailor fluid benchmark --- fluid-bench/benchmark.rb | 34 + fluid-bench/database.rb | 57 ++ fluid-bench/money_filter.rb | 19 + fluid-bench/product.liquid | 36 + fluid-bench/shop_filter.rb | 106 +++ fluid-bench/stackprof.dump | Bin 0 -> 135681 bytes fluid-bench/vision.database.yml | 945 +++++++++++++++++++++++ lib/liquid/compile.rb | 220 ++++-- performance/benchmark | 20 +- performance/benchmarks/fizzbuzz_10000.rb | 19 +- 10 files changed, 1392 insertions(+), 64 deletions(-) create mode 100644 fluid-bench/benchmark.rb create mode 100644 fluid-bench/database.rb create mode 100644 fluid-bench/money_filter.rb create mode 100644 fluid-bench/product.liquid create mode 100644 fluid-bench/shop_filter.rb create mode 100644 fluid-bench/stackprof.dump create mode 100644 fluid-bench/vision.database.yml diff --git a/fluid-bench/benchmark.rb b/fluid-bench/benchmark.rb new file mode 100644 index 00000000..ec682b89 --- /dev/null +++ b/fluid-bench/benchmark.rb @@ -0,0 +1,34 @@ +# https://github.com/evanphx/benchmark-ips +require 'liquid' +#require 'liquid/c' +require 'benchmark/ips' + +require_relative '../lib/liquid/compile' + +require_relative 'shop_filter' +require_relative 'money_filter' + +# Each database table is a hash +require_relative 'database' +tables = Database.tables + +Liquid::Template.register_filter(MoneyFilter) +Liquid::Template.register_filter(ShopFilter) +@template = Liquid::Template.parse(File.read("product.liquid")) + +context = Liquid::Context.new([tables, {}], {}, {}, false, Liquid::ResourceLimits.new(Liquid::Template.default_resource_limits)) + +require 'stackprof' + +results = StackProf.run(raw: true, out: 'stackprof.dump') do + 500_000.times do + @template.render(context) + end +end + +Benchmark.ips do |x| + x.report("render") { @template.render(context) } + + # Compare the iterations per second of the various reports + x.compare! +end diff --git a/fluid-bench/database.rb b/fluid-bench/database.rb new file mode 100644 index 00000000..f8f7eb15 --- /dev/null +++ b/fluid-bench/database.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'yaml' +require 'stackprof' + +module Database + DATABASE_FILE_PATH = "#{__dir__}/vision.database.yml" + + # Load the standard vision toolkit database and re-arrage it to be simply exportable + # to liquid as assigns. All this is based on Shopify + def self.tables + @tables ||= begin + db = + if YAML.respond_to?(:unsafe_load_file) # Only Psych 4+ can use unsafe_load_file + # unsafe_load_file is needed for YAML references + YAML.unsafe_load_file(DATABASE_FILE_PATH, symbolize_names: true) + else + YAML.load_file(DATABASE_FILE_PATH, symbolize_names: true) + end + + # From vision source + db[:products].each do |product| + collections = db[:collections].find_all do |collection| + collection[:products].any? { |p| p[:id].to_i == product[:id].to_i } + end + product[:collections] = collections + end + + # key the tables by handles, as this is how liquid expects it. + db = db.each_with_object({}) do |(key, values), assigns| + assigns[key] = values.each_with_object({}) do |v, h| + h[v[:handle]] = v + 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] = { + :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 + end + end +end + +if __FILE__ == $PROGRAM_NAME + p(Database.tables[:collections][:frontpage].keys) + # p Database.tables[:blog][:articles] +end diff --git a/fluid-bench/money_filter.rb b/fluid-bench/money_filter.rb new file mode 100644 index 00000000..b0135e3e --- /dev/null +++ b/fluid-bench/money_filter.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module MoneyFilter + def money_with_currency(money) + return '' if money.nil? + format("$ %.2f USD", money / 100.0) + end + + def money(money) + return '' if money.nil? + format("$ %.2f", money / 100.0) + end + + private + + def currency + ShopDrop.new.currency + end +end diff --git a/fluid-bench/product.liquid b/fluid-bench/product.liquid new file mode 100644 index 00000000..f0d24f2f --- /dev/null +++ b/fluid-bench/product.liquid @@ -0,0 +1,36 @@ +
+ {% for image in product.images %}{% if forloop.first %}
+ {{ product.title | escape }} +
{% else %} +
+ {{ product.title | escape }} +
{% endif %}{% endfor %} +
+
+

{{ product.title }}

+ {{ product.description }} + + {% if product.available %} +
+ +
+
+ + +
+ + +
+ {% else %} +

This product is temporarily unavailable

+ {% endif %} + +
+ Continue Shopping
+ Browse more {{ product.type | link_to_type }} or additional {{ product.vendor | link_to_vendor }} products. +
+
diff --git a/fluid-bench/shop_filter.rb b/fluid-bench/shop_filter.rb new file mode 100644 index 00000000..3558c6fc --- /dev/null +++ b/fluid-bench/shop_filter.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +module ShopFilter + def asset_url(input) + "/files/1/[shop_id]/[shop_id]/assets/#{input}" + end + + def global_asset_url(input) + "/global/#{input}" + end + + def shopify_asset_url(input) + "/shopify/#{input}" + end + + def script_tag(url) + %() + end + + def stylesheet_tag(url, media = "all") + %() + end + + def link_to(link, url, title = "") + %(#{link}) + end + + def img_tag(url, alt = "") + %(#{alt}) + end + + def link_to_vendor(vendor) + if vendor + link_to(vendor, url_for_vendor(vendor), vendor) + else + 'Unknown Vendor' + end + end + + def link_to_type(type) + if type + link_to(type, url_for_type(type), type) + else + 'Unknown Vendor' + end + end + + def url_for_vendor(vendor_title) + "/collections/#{to_handle(vendor_title)}" + end + + def url_for_type(type_title) + "/collections/#{to_handle(type_title)}" + end + + def product_img_url(url, style = 'small') + unless url =~ %r{\Aproducts/([\w\-\_]+)\.(\w{2,4})} + raise ArgumentError, 'filter "size" can only be called on product images' + end + + case style + when 'original' + '/files/shops/random_number/' + url + when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon' + "/files/shops/random_number/products/#{Regexp.last_match(1)}_#{style}.#{Regexp.last_match(2)}" + else + raise ArgumentError, 'valid parameters for filter "size" are: original, grande, large, medium, compact, small, thumb and icon ' + end + end + + def default_pagination(paginate) + html = [] + html << %(#{link_to(paginate['previous']['title'], paginate['previous']['url'])}) if paginate['previous'] + + paginate['parts'].each do |part| + html << if part['is_link'] + %(#{link_to(part['title'], part['url'])}) + elsif part['title'].to_i == paginate['current_page'].to_i + %(#{part['title']}) + else + %(#{part['title']}) + end + end + + html << %(#{link_to(paginate['next']['title'], paginate['next']['url'])}) if paginate['next'] + html.join(' ') + end + + # Accepts a number, and two words - one for singular, one for plural + # Returns the singular word if input equals 1, otherwise plural + def pluralize(input, singular, plural) + input == 1 ? singular : plural + end + + private + + def to_handle(str) + result = str.dup + result.downcase! + result.delete!("'\"()[]") + result.gsub!(/\W+/, '-') + result.gsub!(/-+\z/, '') if result[-1] == '-' + result.gsub!(/\A-+/, '') if result[0] == '-' + result + end +end diff --git a/fluid-bench/stackprof.dump b/fluid-bench/stackprof.dump new file mode 100644 index 0000000000000000000000000000000000000000..65a94e1f615d286b4eecd18ccfa23a604273d0a3 GIT binary patch literal 135681 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‘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: + - *product-6-var-1 + + - &product-7 + id: 7 + title: Shopify Shirt + handle: shopify-shirt + type: Shirt + vendor: Shopify + price: 1900 + price_max: 1900 + price_min: 1900 + price_varies: false + available: true + tags: + - shopify + - shirt + - apparel + - tshirt + - clothing + options: + - Color + - Size + compare_at_price: + compare_at_price_max: 0 + compare_at_price_min: 0 + compare_at_price_varies: false + url: /products/shopify-shirt + featured_image: products/shopify_shirt.png + images: + - products/shopify_shirt.png + description: +

High Quality Shopify Shirt. Wear your e-commerce solution with pride and attract attention anywhere you go.

+

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.

+ variants: + - *product-7-var-1 + - *product-7-var-2 + - *product-7-var-3 + - *product-7-var-4 + + - &product-8 + id: 8 + title: Hooded Sweater + handle: hooded-sweater + type: Sweater + vendor: Stormtech + price: 5900 + price_max: 5900 + price_min: 5900 + price_varies: false + available: true + tags: + - sweater + - hooded + - apparel + - clothing + options: + - Color + - Size + compare_at_price: 6900 + compare_at_price_max: 6900 + compare_at_price_min: 6900 + compare_at_price_varies: false + url: /products/hooded-sweater + featured_image: products/hooded-sweater.jpg + images: + - products/hooded-sweater.jpg + - products/hooded-sweater-b.jpg + description: +

Extra comfortable zip up sweater. Durable quality, ideal for any outdoor activities.

+

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.

+ variants: + - *product-8-var-1 + - *product-8-var-2 + - *product-8-var-3 + - *product-8-var-4 + - *product-8-var-5 + - *product-8-var-6 + + - &product-9 + id: 9 + title: D3 Digital SLR Camera + handle: d3 + type: SLR + vendor: Nikon + price: 499995 + price_max: 552500 + price_min: 499995 + price_varies: true + available: true + tags: + - camera + - slr + - nikon + - professional + options: + - Bundle + compare_at_price: + compare_at_price_max: 0 + compare_at_price_min: 0 + compare_at_price_varies: false + url: /products/d3 + featured_image: products/d3.jpg + images: + - products/d3.jpg + - products/d3_2.jpg + - products/d3_3.jpg + description: +

Flagship pro D-SLR with a 12.1-MP FX-format CMOS sensor, blazing 9 fps shooting at full FX resolution and low-noise performance up to 6400 ISO.

+

Nikon's original 12.1-megapixel FX-format (23.9 x 36mm) CMOS sensor: Couple Nikon's exclusive digital image processing system with the 12.1-megapixel FX-format and you'll get breathtakingly rich images while also reducing noise to unprecedented levels with even higher ISOs.

+

Continuous shooting at up to 9 frames per second: At full FX resolution and up to 11fps in the DX crop mode, the D3 offers uncompromised shooting speeds for fast-action and sports photography.

+ variants: + - *product-9-var-1 + - *product-9-var-2 + - *product-9-var-3 + + +# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +# Line Items +# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +line_items: + - &line_item-1 + id: 1 + title: 'Arbor Draft' + subtitle: '151cm' + price: 29900 + line_price: 29900 + quantity: 1 + variant: *product-1-var-1 + product: *product-1 + + - &line_item-2 + id: 2 + title: 'Comic ~ Orange' + subtitle: '159cm' + price: 19900 + line_price: 39800 + quantity: 2 + variant: *product-4-var-1 + product: *product-4 + +# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +# Link Lists +# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +links: + - &link-1 + id: 1 + title: Our Sale + url: /collections/sale + - &link-2 + id: 2 + title: Arbor Stuff + url: /collections/arbor + - &link-3 + id: 3 + title: All our Snowboards + url: /collections/snowboards + - &link-4 + id: 4 + title: Powered by Shopify + url: 'http://shopify.com' + - &link-5 + id: 5 + title: About Us + url: /pages/about-us + - &link-6 + id: 6 + title: Policies + url: /pages/shipping + - &link-7 + id: 7 + title: Contact Us + url: /pages/contact + - &link-8 + id: 8 + title: Our blog + url: /blogs/bigcheese-blog + - &link-9 + id: 9 + title: New Boots + url: /products/burton-boots + - &link-10 + id: 10 + title: Paginated Sale + url: /collections/paginated-sale + - &link-11 + id: 11 + title: Our Paginated blog + url: /blogs/paginated-blog + - &link-12 + id: 12 + title: Catalog + url: /collections/all + + + +# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +# Link Lists +# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +link_lists: + - &link-list-1 + id: 1 + title: 'Main Menu' + handle: 'main-menu' + links: + - *link-12 + - *link-5 + - *link-7 + - *link-8 + - &link-list-2 + id: 1 + title: 'Footer Menu' + handle: 'footer' + links: + - *link-5 + - *link-6 + +# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +# Collections +# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +collections: + - &collection-1 + id: 1 + title: Frontpage + handle: frontpage + url: /collections/frontpage + products: + - *product-7 + - *product-8 + - *product-9 + + - &collection-2 + id: 2 + title: Arbor + handle: arbor + url: /collections/arbor + products: + - *product-1 + - *product-2 + + - &collection-3 + id: 3 + title: Snowboards + handle: snowboards + url: /collections/snowboards + description: +

This is a description for my Snowboards collection.

+ products: + - *product-1 + - *product-2 + - *product-3 + - *product-4 + + - &collection-4 + id: 4 + title: Items On Sale + handle: sale + url: /collections/sale + products: + - *product-1 + + - &collection-5 + id: 5 + title: Paginated Sale + handle: 'paginated-sale' + url: '/collections/paginated-sale' + products: + - *product-1 + - *product-2 + - *product-3 + - *product-4 + products_count: 210 + + - &collection-6 + id: 6 + title: All products + handle: 'all' + url: '/collections/all' + products: + - *product-7 + - *product-8 + - *product-9 + - *product-6 + - *product-1 + - *product-2 + - *product-3 + - *product-4 + - *product-5 + + +# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +# Pages and Blogs +# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +pages: + - &page-2 + id: 1 + title: Contact Us + handle: contact + url: /pages/contact + author: Tobi + 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 + + - &page-3 + id: 2 + title: About Us + handle: about-us + url: /pages/about-us + author: Tobi + content: + "

Our company was founded in 1894 and we are since operating out of Avignon from the beautiful Provence.

+

We offer the highest quality products and are proud to serve our customers to their heart's content.

" + created_at: 2005-04-04 12:00 + + - &page-4 + id: 3 + title: Shopping Cart + handle: shopping-cart + url: /pages/shopping-cart + author: Tobi + content: "
  • Your order is safe with us. Our checkout uses industry standard security to protect your information.
  • Your order will be billed immediately upon checkout.
  • ALL SALES ARE FINAL: Defective or damaged product will be exchanged
  • All orders are processed expediently: usually in under 24 hours.
" + created_at: 2005-04-04 12:00 + + - &page-5 + id: 4 + title: Shipping and Handling + handle: shipping + url: /pages/shipping + 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 + + - &page-6 + id: 5 + title: Frontpage + handle: frontpage + url: /pages/frontpage + 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 + +blogs: + - id: 1 + handle: news + title: News + url: /blogs/news + articles: + - id: 3 + 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' + 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 + articles_count: 2 + + - id: 2 + handle: bigcheese-blog + title: Bigcheese blog + url: /blogs/bigcheese-blog + articles: + - id: 1 + 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 + author: John Smith + email: john@smith.com + content: Wow...great article man. + status: published + created_at: 2009-01-01 12:00 + updated_at: 2009-02-01 12:00 + url: "" + - + 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! + status: published + created_at: 2009-03-01 12:00 + updated_at: 2009-02-01 12:00 + url: "http://somesite.com/" + - id: 2 + title: Fascinating + 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-06 12:00 + comments: + articles_count: 2 + comments_enabled?: true + comment_post_url: "" + comments_count: 2 + moderated?: true + + - id: 3 + handle: paginated-blog + title: Paginated blog + url: /blogs/paginated-blog + articles: + - id: 6 + 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 + + - id: 7 + title: Fascinating + 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-06 12:00 + articles_count: 200 diff --git a/lib/liquid/compile.rb b/lib/liquid/compile.rb index e712e5c5..816488e0 100644 --- a/lib/liquid/compile.rb +++ b/lib/liquid/compile.rb @@ -1,12 +1,14 @@ # frozen_string_literal: true require 'pry-byebug' +require 'set' module Liquid class Compiler def initialize @ruby = +"" @nodes = {} + @declared = Set.new(['__l_product']) end def <<(line) @@ -17,33 +19,51 @@ module Liquid "__l_#{name}" end + def declare(name) + @declared << name + end + + def declared?(name) + @declared.member?(name) + end + def to_proc - puts @ruby if ENV['SHOW_RUBY'] == '1' - RubyVM::InstructionSequence.compile(<<~RUBY).eval.call(@nodes) + show_ruby = ENV["SHOW_RUBY"] && ENV["SHOW_RUBY"].to_i || 0 + STDERR.puts @ruby if show_ruby >= 1 + res = RubyVM::InstructionSequence.compile(<<~RUBY).eval.call(@nodes) ->(__nodes) { - ->(__context, __output) { + ->(__context, __output, __l_product) { + __scope = __context.environments.first #{@ruby} __output } } RUBY + + STDERR.puts RubyVM::InstructionSequence.disasm(res) if show_ruby >= 2 + res end - # Compiles any thing that can be returned by Expression/QuotedFragment def compile_expr(node) - case node - when Liquid::VariableLookup - @nodes[node.object_id] = node + if node.respond_to?(:compile_expr) node.compile_expr(self) - when Liquid::RangeLookup - @nodes[node.object_id] = node - node.compile_expr(self) - when Range # returned by RangeLookup when range contains only literals - node.inspect - when Integer, Float, nil, true, false, '' - node.inspect else - raise ArgumentError, "cannot compile node #{node.inspect}" + case node + when Range # returned by RangeLookup when range contains only literals + node.inspect + when Integer, Float, nil, true, false, String + node.inspect + else + raise ArgumentError, "cannot compile node #{node.inspect}" + end + end + end + + def output(node) + self << if node.is_a?(String) + "__output << #{node.inspect}\n" + else + "__output << #{compile_expr(node)}.to_s\n" end end @@ -67,38 +87,6 @@ module Liquid RUBY end - def slice_collection_expr(collection_name, from_name, to_name) - <<~RUBY.strip - (begin - if (#{from_name} != 0 || !#{to_name}.nil?) && #{collection_name}.respond_to?(:load_slice) - #{collection_name}.load_slice(#{from_name}, #{to_name}) - else - segments = [] - index = 0 - if #{collection_name}.is_a?(String) - #{collection_name}.empty? ? [] : [collection] - elsif !#{collection_name}.respond_to?(:each) - [] - else - #{collection_name}.each do |item| - if #{to_name} && #{to_name} <= index - break - end - - if #{from_name} && #{from_name} <= index - segments << item - end - - index += 1 - end - - segments - end - end - end) - RUBY - end - def compile(node) if node.instance_of?(String) self << "__output << #{node.inspect}\n" @@ -113,7 +101,7 @@ module Liquid nil end catch_errors(line_number, show_message: !node.blank?) do - self << "__nodes[#{node.object_id}].render_to_output_buffer(__context, __output)\n" + self << "__nodes[#{node.object_id}].render_to_output_buffer(__context, __output) # #{node.inspect} \n" end end end @@ -135,13 +123,12 @@ module Liquid self << "__output << __error_message\n" if show_message self << "end\nend\n" end - end class BlockBody def render_to_output_buffer(context, output) raise "Tried to render uncompiled block" unless @compiled - @compiled.call(context, output) + @compiled.call(context, output, context.environments[0][:product]) end def compile_top_level @@ -169,16 +156,88 @@ module Liquid class VariableLookup def compile_expr(compiler) - compiler.var_name(@name) + # HACK + if @name == "forloop" && @lookups == ["first"] + return "forloop_first" + end + + var_name = compiler.var_name(@name) + root = if compiler.declared?(var_name) + var_name + else + "__scope[#{@name.inspect}]" + end + + @lookups.reduce(root) do |prev, lookup| + "#{prev}[:#{lookup}]" + end end end class Variable + FILTERS = { + "modulo" => ->(compiler, expr, args, kwargs) { + "(#{expr} % #{args[0]})" + }, + "product_img_url" => ->(compiler, expr, args, kwargs) { + style = args.fetch(0, 'small') + + rest = case style + when 'original' + "\"/files/shops/random_number/\#{url}\"" + when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon' + "\"/files/shops/random_number/products/\#{$1}_#{style}.\#{$2}\"" + else + "raise ArgumentError, 'valid parameters for filter \"size\" are: original, grande, large, medium, compact, small, thumb and icon '" + end + + <<~RUBY.strip + (begin + if #{expr} =~ %r{\\Aproducts/([\\w\\-\\_]+)\\.(\\w{2,4})} + #{rest} + else + raise ArgumentError, 'filter \"size\" can only be called on product images' + end + end) + RUBY + }, + "escape" => ->(compiler, expr, args, kwargs) { + "(_t = #{expr}; CGI.escape(_t) unless _t)" + }, + "money" => ->(compiler, expr, args, kwargs) { + "(_m = #{expr}; _m.nil? ? '' : \"\#{(_m / 100.0).round(2)}\")" + }, + } + def compile(compiler) - compiler.catch_errors(@line_number, show_message: true) do - compiler << "__output << #{@name.compile_expr(compiler)}.to_s\n" + if const? + compiler.output(name) + else + compiler.catch_errors(@line_number, show_message: true) do + compiler.output(self) + end end end + + def const? + @filters.empty? && !name.respond_to?(:compile_expr) + end + + def compile_expr(compiler) + @filters.reduce(compiler.compile_expr(name)) do |expr, (name, args, kwargs)| + if FILTERS.key?(name) + FILTERS[name].call(compiler, expr, args, kwargs) + else + expr + end + end + end + end + + class Echo + def compile(compiler) + compiler.compile(variable) + end end class For @@ -206,11 +265,64 @@ module Liquid RUBY end + item_var = compiler.var_name(variable_name) compiler << <<~RUBY - collection.each do |#{compiler.var_name(variable_name)}| + forloop_first = true + for #{item_var} in collection RUBY + compiler.declare(item_var) compiler.compile(@for_block) + compiler << <<~RUBY + forloop_first = false + end + RUBY + + end + end + + class If + def compile(compiler) + compiler << "if #{compiler.compile_expr(blocks[0])}\n" + compiler.compile(blocks[0].attachment) + blocks.drop(1).each do |block| + if block.else? + compiler << "else\n" + else + compiler << "elsif #{block.compile_expr(compiler)}\n" + end + compiler.compile(block.attachment) + end compiler << "end\n" end end + + class Comment + def compile(_compiler); end + end + + class Condition + def compile_expr(compiler) + condition = if operator + left_expr = compiler.compile_expr(left) + right_expr = compiler.compile_expr(right) + expr = "((#{left_expr} #{operator} #{right_expr}) #{child_relation}" + else + left_expr = compiler.compile_expr(left) + right_expr = compiler.compile_expr(right) + expr = "#{left_expr}" + end + if child_condition + child_expr = compiler.compile_expr(child_condition) + condition << " #{child_expr}" + end + condition + end + end + + class Assign + def compile(compiler) + compliler.declare(@to) + compiler << "#{compiler.var_name(@to)} = #{compiler.compile_expr(@from)}\n" + end + end end diff --git a/performance/benchmark b/performance/benchmark index 9519db22..70f48771 100755 --- a/performance/benchmark +++ b/performance/benchmark @@ -1,8 +1,6 @@ #!/usr/bin/env ruby # frozen_string_literal: true - - unless ENV.key?("BUNDLE_BIN_PATH") exec("bundle", "exec", "ruby", __FILE__, *ARGV) end @@ -156,6 +154,7 @@ def print_columns(cols) end end +ITERS = 1 Benchmarks = Class.new do def define(_name, benchmark) @benchmark = benchmark @@ -167,27 +166,32 @@ Benchmarks = Class.new do parsed = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond) # warmup - 1000.times { @benchmark.render} + ITERS.times { @benchmark.render} warm = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond) res = nil - 1000.times { res = @benchmark.render } + ITERS.times { res = @benchmark.render } ran = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond) - puts res if res.is_a?(String) + if res.is_a?(String) + puts res + puts "Output digest: #{Digest::SHA2.hexdigest(res)}" + end if ENV['SHOW_RUBY'] == '1' STDERR.puts "note: SHOW_RUBY prevents gathering parse metrics" - STDERR.puts "render: #{(ran - warm) / 1000}µs" + STDERR.puts "render: #{(ran - warm) / ITERS}µs" else STDERR.puts "parse: #{parsed - start}µs" - STDERR.puts "render: #{(ran - warm) / 1000}µs" - STDERR.puts "total: #{(ran - warm) / 1000 + (parsed - start)}µs" + STDERR.puts "render: #{(ran - warm) / ITERS}µs" + STDERR.puts "total: #{(ran - warm) / ITERS + (parsed - start)}µs" end end end.new def execute + require 'digest' + if ARGV.count != 2 STDERR.puts "Usage: benchmark.rb record [output_path]" exit(1) diff --git a/performance/benchmarks/fizzbuzz_10000.rb b/performance/benchmarks/fizzbuzz_10000.rb index 14558a1c..08c302f2 100644 --- a/performance/benchmarks/fizzbuzz_10000.rb +++ b/performance/benchmarks/fizzbuzz_10000.rb @@ -1,7 +1,22 @@ # frozen_string_literal: true -Benchmarks.define('simple_loop', Class.new do - TEMPLATE = "{% for i in (1..1000) %}{{ i }}{% endfor %}" +Benchmarks.define('fizzbuzz_10000', Class.new do + TEMPLATE = <<~LIQUID + {% for i in (1..#{ENV['COUNT'].to_i}) %} + {% liquid + assign rem_3 = i | modulo: 3 + assign rem_5 = i | modulo: 5 + if rem_3 == 0 and rem_5 == 0 + echo "Fizzbuzz" + elsif rem_3 == 0 + echo "Fizz" + elsif rem_5 == 0 + echo "Buzz" + else + echo i + endif + %}{% endfor %} + LIQUID def compile @parsed = Liquid::Template.parse(TEMPLATE)