Eagerly cache global filters

Including a module can cause Ruby's global constant cache to be busted
if the included module contain constants. So that's something you don't
want to happen at "runtime", otherwise it will severely degrade performance
and if you are using YJIT or MJIT most of the compiled code will be invalidated.

To limit the impact of this, we can pre-include the global filters,
as they're generally registered during boot, that limits the problem
to non-global filters.
This commit is contained in:
Jean Boussier
2022-03-01 13:40:40 +01:00
parent 97f7922457
commit c588337aac
6 changed files with 30 additions and 23 deletions
+1 -1
View File
@@ -59,8 +59,8 @@ require 'liquid/forloop_drop'
require 'liquid/extensions' require 'liquid/extensions'
require 'liquid/errors' require 'liquid/errors'
require 'liquid/interrupts' require 'liquid/interrupts'
require 'liquid/strainer_factory'
require 'liquid/strainer_template' require 'liquid/strainer_template'
require 'liquid/strainer_factory'
require 'liquid/expression' require 'liquid/expression'
require 'liquid/context' require 'liquid/context'
require 'liquid/parser_switching' require 'liquid/parser_switching'
+11 -10
View File
@@ -7,25 +7,26 @@ module Liquid
def add_global_filter(filter) def add_global_filter(filter)
strainer_class_cache.clear strainer_class_cache.clear
global_filters << filter GlobalCache.add_filter(filter)
end end
def create(context, filters = []) def create(context, filters = [])
strainer_from_cache(filters).new(context) strainer_from_cache(filters).new(context)
end end
GlobalCache = Class.new(StrainerTemplate)
private private
def global_filters
@global_filters ||= []
end
def strainer_from_cache(filters) def strainer_from_cache(filters)
strainer_class_cache[filters] ||= begin if filters.empty?
klass = Class.new(StrainerTemplate) GlobalCache
global_filters.each { |f| klass.add_filter(f) } else
filters.each { |f| klass.add_filter(f) } strainer_class_cache[filters] ||= begin
klass klass = Class.new(GlobalCache)
filters.each { |f| klass.add_filter(f) }
klass
end
end end
end end
+5
View File
@@ -31,6 +31,11 @@ module Liquid
filter_methods.include?(method.to_s) filter_methods.include?(method.to_s)
end end
def inherited(subclass)
super
subclass.instance_variable_set(:@filter_methods, @filter_methods.dup)
end
private private
def filter_methods def filter_methods
+10 -10
View File
@@ -72,21 +72,21 @@ module Minitest
end end
def with_global_filter(*globals) def with_global_filter(*globals)
original_global_filters = Liquid::StrainerFactory.instance_variable_get(:@global_filters) original_global_cache = Liquid::StrainerFactory::GlobalCache
Liquid::StrainerFactory.instance_variable_set(:@global_filters, []) Liquid::StrainerFactory.send(:remove_const, :GlobalCache)
globals.each do |global| Liquid::StrainerFactory.const_set(:GlobalCache, Class.new(Liquid::StrainerTemplate))
Liquid::StrainerFactory.add_global_filter(global)
end
Liquid::StrainerFactory.send(:strainer_class_cache).clear
globals.each do |global| globals.each do |global|
Liquid::Template.register_filter(global) Liquid::Template.register_filter(global)
end end
yield
ensure
Liquid::StrainerFactory.send(:strainer_class_cache).clear Liquid::StrainerFactory.send(:strainer_class_cache).clear
Liquid::StrainerFactory.instance_variable_set(:@global_filters, original_global_filters) begin
yield
ensure
Liquid::StrainerFactory.send(:remove_const, :GlobalCache)
Liquid::StrainerFactory.const_set(:GlobalCache, original_global_cache)
Liquid::StrainerFactory.send(:strainer_class_cache).clear
end
end end
def with_error_mode(mode) def with_error_mode(mode)
+2 -1
View File
@@ -52,7 +52,8 @@ class StrainerFactoryUnitTest < Minitest::Test
/\ALiquid error: wrong number of arguments \((1 for 0|given 1, expected 0)\)\z/, /\ALiquid error: wrong number of arguments \((1 for 0|given 1, expected 0)\)\z/,
exception.message exception.message
) )
assert_equal(exception.backtrace[0].split(':')[0], __FILE__) source = AccessScopeFilters.instance_method(:public_filter).source_location
assert_equal(source.map(&:to_s), exception.backtrace[0].split(':')[0..1])
end end
def test_strainer_only_invokes_public_filter_methods def test_strainer_only_invokes_public_filter_methods
+1 -1
View File
@@ -57,8 +57,8 @@ class StrainerTemplateUnitTest < Minitest::Test
end end
def test_add_filter_does_not_raise_when_module_overrides_previously_registered_method def test_add_filter_does_not_raise_when_module_overrides_previously_registered_method
strainer = Context.new.strainer
with_global_filter do with_global_filter do
strainer = Context.new.strainer
strainer.class.add_filter(PublicMethodOverrideFilter) strainer.class.add_filter(PublicMethodOverrideFilter)
assert(strainer.class.send(:filter_methods).include?('public_filter')) assert(strainer.class.send(:filter_methods).include?('public_filter'))
end end