refactored and optimized rendering

Measures:
1) A while loop is faster than iterating with #each.
2) Check string, variable and block tokens first. They are far more
   frequent than interrupt tokens. In their case, checking for an
   interrupt can be avoided.
3) String tokens just map to themselves and don't need the special
   treatment of BlockBody#render_node (except the resource limit
   check).

Benchmark
=========

$ bundle exec rake benchmark:run

Before
------

Run 1)
              parse:     41.630  (± 0.0%) i/s -    420.000  in  10.089309s
             render:     75.962  (± 3.9%) i/s -    763.000  in  10.066823s
     parse & render:     25.497  (± 0.0%) i/s -    256.000  in  10.040862s

Run 2)
              parse:     42.130  (± 0.0%) i/s -    424.000  in  10.064738s
             render:     77.003  (± 1.3%) i/s -    777.000  in  10.093524s
     parse & render:     25.739  (± 0.0%) i/s -    258.000  in  10.024581s

Run 3)
              parse:     41.976  (± 2.4%) i/s -    420.000  in  10.021406s
             render:     76.184  (± 1.3%) i/s -    763.000  in  10.018104s
     parse & render:     25.641  (± 0.0%) i/s -    258.000  in  10.062549s

After
-----

Run 1)
              parse:     42.283  (± 0.0%) i/s -    424.000  in  10.028306s
             render:     83.158  (± 2.4%) i/s -    832.000  in  10.009201s
     parse & render:     26.417  (± 0.0%) i/s -    266.000  in  10.069718s

Run 2)
              parse:     41.159  (± 4.9%) i/s -    412.000  in  10.031297s
             render:     81.591  (± 3.7%) i/s -    816.000  in  10.018225s
     parse & render:     25.924  (± 3.9%) i/s -    260.000  in  10.035653s

Run 3)
              parse:     42.418  (± 2.4%) i/s -    424.000  in  10.003100s
             render:     84.183  (± 2.4%) i/s -    847.000  in  10.069781s
     parse & render:     26.726  (± 0.0%) i/s -    268.000  in  10.029857s
This commit is contained in:
Christopher Aue
2018-04-16 12:48:12 +02:00
parent 4369fe6c85
commit c086017bc9
2 changed files with 37 additions and 33 deletions

View File

@@ -70,33 +70,27 @@ module Liquid
output = []
context.resource_limits.render_score += @nodelist.length
@nodelist.each do |token|
# Break out if we have any unhanded interrupts.
break if context.interrupt?
begin
idx = 0
while node = @nodelist[idx]
case node
when String
check_resources(context, node)
output << node
when Variable
render_node_to_output(node, output, context)
when Block
render_node_to_output(node, output, context, node.blank?)
break if context.interrupt? # might have happened in a for-block
when Continue, Break
# If we get an Interrupt that means the block must stop processing. An
# Interrupt is any command that stops block execution such as {% break %}
# or {% continue %}
if token.is_a?(Continue) || token.is_a?(Break)
context.push_interrupt(token.interrupt)
break
end
node_output = render_node(token, context)
unless token.is_a?(Block) && token.blank?
output << node_output
end
rescue MemoryError => e
raise e
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, token.line_number)
output << nil
rescue ::StandardError => e
line_number = token.is_a?(String) ? nil : token.line_number
output << context.handle_error(e, line_number)
context.push_interrupt(node.interrupt)
break
else # Other non-Block tags
render_node_to_output(node, output, context)
end
idx += 1
end
output.join
@@ -104,15 +98,25 @@ module Liquid
private
def render_node(node, context)
node_output = node.is_a?(String) ? node : node.render(context)
def render_node_to_output(node, output, context, skip_output = false)
node_output = node.render(context)
node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s
check_resources(context, node_output)
output << node_output unless skip_output
rescue MemoryError => e
raise e
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, node.line_number)
output << nil
rescue ::StandardError => e
line_number = node.is_a?(String) ? nil : node.line_number
output << context.handle_error(e, line_number)
end
def check_resources(context, node_output)
context.resource_limits.render_length += node_output.length
if context.resource_limits.reached?
raise MemoryError.new("Memory limits exceeded".freeze)
end
node_output
return unless context.resource_limits.reached?
raise MemoryError.new("Memory limits exceeded".freeze)
end
def create_variable(token, parse_context)

View File

@@ -1,13 +1,13 @@
module Liquid
class BlockBody
def render_node_with_profiling(node, context)
def render_node_with_profiling(node, output, context, skip_output = false)
Profiler.profile_node_render(node) do
render_node_without_profiling(node, context)
render_node_without_profiling(node, output, context, skip_output)
end
end
alias_method :render_node_without_profiling, :render_node
alias_method :render_node, :render_node_with_profiling
alias_method :render_node_without_profiling, :render_node_to_output
alias_method :render_node_to_output, :render_node_with_profiling
end
class Include < Tag