Add a class cache to avoid runtime extend calls

* Strainer has a class cache that creates Strainer subclasses for each filter
   set that is used on .create calls.
 * Context now creates a list of filters and passes this to Strainer.create to
   utilize the class cache in almost all use cases.
 * If add_filter was called after a render, then the method cache may still be
   invalidated.

Conflicts:

	lib/liquid/strainer.rb
This commit is contained in:
James Tucker
2013-08-31 18:56:35 +00:00
parent e8b41c8856
commit 9b2d5b7dd3
3 changed files with 36 additions and 7 deletions
+13 -3
View File
@@ -25,6 +25,7 @@ module Liquid
squash_instance_assigns_with_environments squash_instance_assigns_with_environments
@interrupts = [] @interrupts = []
@filters = []
end end
def resource_limits_reached? def resource_limits_reached?
@@ -34,7 +35,7 @@ module Liquid
end end
def strainer def strainer
@strainer ||= Strainer.create(self) @strainer ||= Strainer.create(self, @filters)
end end
# Adds filters to this context. # Adds filters to this context.
@@ -43,11 +44,20 @@ module Liquid
# for that # for that
def add_filters(filters) def add_filters(filters)
filters = [filters].flatten.compact filters = [filters].flatten.compact
filters.each do |f| filters.each do |f|
raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module) raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module)
Strainer.add_known_filter(f) Strainer.add_known_filter(f)
strainer.extend(f) end
# If strainer is already setup then there's no choice but to use a runtime
# extend call. If strainer is not yet created, we can utilize strainers
# cached class based API, which avoids busting the method cache.
if @strainer
filters.each do |f|
strainer.extend(f)
end
else
@filters.concat filters
end end
end end
+12 -4
View File
@@ -11,6 +11,11 @@ module Liquid
@@filters = [] @@filters = []
@@known_filters = Set.new @@known_filters = Set.new
@@known_methods = Set.new @@known_methods = Set.new
@@strainer_class_cache = Hash.new do |hash, filters|
hash[filters] = Class.new(Strainer) do
filters.each { |f| include f }
end
end
def initialize(context) def initialize(context)
@context = context @context = context
@@ -32,10 +37,13 @@ module Liquid
end end
end end
def self.create(context) def self.strainer_class_cache
strainer = Strainer.new(context) @@strainer_class_cache
@@filters.each { |m| strainer.extend(m) } end
strainer
def self.create(context, filters = nil)
filters = @@filters.values + (filters || [])
strainer_class_cache[filters].new(context)
end end
def invoke(method, *args) def invoke(method, *args)
+11
View File
@@ -49,4 +49,15 @@ class StrainerTest < Test::Unit::TestCase
assert_equal "has_method?", strainer.invoke("invoke", "has_method?", "invoke") assert_equal "has_method?", strainer.invoke("invoke", "has_method?", "invoke")
end end
def test_strainer_uses_a_class_cache_to_avoid_method_cache_invalidation
a, b = Module.new, Module.new
strainer = Strainer.create(nil, [a,b])
assert_kind_of Strainer, strainer
assert_kind_of a, strainer
assert_kind_of b, strainer
Strainer.class_variable_get(:@@filters).values.each do |m|
assert_kind_of m, strainer
end
end
end # StrainerTest end # StrainerTest