From 0df3f1c37201b233f469bc5884333677f6608473 Mon Sep 17 00:00:00 2001 From: Jason Hiltz-Laforge Date: Sat, 19 Jul 2014 15:16:40 +0000 Subject: [PATCH] Cache parsed markup parts to avoid repeated calls during template render --- History.md | 1 + lib/liquid/context.rb | 27 +++++++++++++++++++-------- test/unit/context_unit_test.rb | 10 ++++++++++ 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/History.md b/History.md index 881e8853..9e18adec 100644 --- a/History.md +++ b/History.md @@ -3,6 +3,7 @@ ## 3.0.0 / not yet released / branch "master" * ... +* Optimize variable parsing to avoid repeated regex evaluation during template rendering #383 [Jason Hiltz-Laforge, jasonhl] * Optimize checking for block interrupts to reduce object allocation #380 [Jason Hiltz-Laforge, jasonhl] * Properly set context rethrow_errors on render! #349 [Thierry Joyal, tjoyal] * Fix broken rendering of variables which are equal to false, see #345 [Florian Weingarten, fw42] diff --git a/lib/liquid/context.rb b/lib/liquid/context.rb index 36ff7a84..d3f6af93 100644 --- a/lib/liquid/context.rb +++ b/lib/liquid/context.rb @@ -17,6 +17,8 @@ module Liquid attr_accessor :rethrow_errors + SQUARE_BRACKETED = /\A\[(.*)\]\z/m + def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = {}) @environments = [environments].flatten @scopes = [(outer_scope || {})] @@ -28,6 +30,7 @@ module Liquid @interrupts = [] @filters = [] + @parsed_variables = Hash.new{ |cache, markup| cache[markup] = variable_parse(markup) } end def increment_used_resources(key, obj) @@ -213,6 +216,16 @@ module Liquid return variable end + def variable_parse(markup) + parts = markup.scan(VariableParser) + needs_resolution = false + if parts.first =~ SQUARE_BRACKETED + needs_resolution = true + parts[0] = $1 + end + {:first => parts.shift, :needs_resolution => needs_resolution, :rest => parts} + end + # Resolves namespaced queries gracefully. # # Example @@ -220,19 +233,17 @@ module Liquid # assert_equal 'tobi', @context['hash.name'] # assert_equal 'tobi', @context['hash["name"]'] def variable(markup) - parts = markup.scan(VariableParser) - square_bracketed = /\A\[(.*)\]\z/m + parts = @parsed_variables[markup] - first_part = parts.shift - - if first_part =~ square_bracketed - first_part = resolve($1) + first_part = parts[:first] + if parts[:needs_resolution] + first_part = resolve(parts[:first]) end if object = find_variable(first_part) - parts.each do |part| - part = resolve($1) if part_resolved = (part =~ square_bracketed) + parts[:rest].each do |part| + part = resolve($1) if part_resolved = (part =~ SQUARE_BRACKETED) # If object is a hash- or array-like object we look for the # presence of the key and if its available we return it diff --git a/test/unit/context_unit_test.rb b/test/unit/context_unit_test.rb index bb5bf02c..0b2d0a4c 100644 --- a/test/unit/context_unit_test.rb +++ b/test/unit/context_unit_test.rb @@ -473,4 +473,14 @@ class ContextUnitTest < Test::Unit::TestCase assert mock_empty.has_been_called? end + def test_variable_lookup_caches_markup + mock_scan = Spy.on_instance_method(String, :scan).and_return(["string"]) + + @context['string'] = 'string' + @context['string'] + @context['string'] + + assert_equal 1, mock_scan.calls.size + end + end # ContextTest