Hello guys!
In our Rails application we see Marshaler::Base#find_template as a hotspot.
The problem is that the method looks up all the inheritance chain for an encoded object:
def find_handler(obj)
obj.class.ancestors.each do |a|
if handler = @handlers[a]
return handler
end
end
nil
end
Here's Hash `ancestors` in Rails
[4] pry(main)> {}.class.ancestors
=> [ActiveSupport::ToJsonWithActiveSupportEncoder,
Hash,
JSON::Ext::Generator::GeneratorMethods::Hash,
Enumerable,
ActiveSupport::ToJsonWithActiveSupportEncoder,
Object,
PP::ObjectMixin,
JSON::Ext::Generator::GeneratorMethods::Object,
ActiveSupport::Tryable,
ActiveSupport::Dependencies::Loadable,
Kernel,
BasicObject]
Here's String `ancestors`:
[5] pry(main)> "".class.ancestors
=> [ActiveSupport::ToJsonWithActiveSupportEncoder,
String,
JSON::Ext::Generator::GeneratorMethods::String,
Comparable,
ActiveSupport::ToJsonWithActiveSupportEncoder,
Object,
PP::ObjectMixin,
JSON::Ext::Generator::GeneratorMethods::Object,
ActiveSupport::Tryable,
ActiveSupport::Dependencies::Loadable,
Kernel,
BasicObject]
Basically all core objects are prepended with ToJsonWithActiveSupportEncoder.
That makes Ruby to spend two loop iterations on each encoded key or value to get a handler for Array, Hash or String.
Performance implications are quite severe if encoded object is big enough:
# benchmark.rb
require "bundler/setup"
$LOAD_PATH << File.expand_path("../../lib", __FILE__)
require "transit"
require "active_support"
require "active_support/core_ext"
require "benchmark"
example = Array.new(1_000) do
{
ids: [1, 2, 3, 4],
translations: {
can: {
be: {
nested: "value"
}
}
}
}
end
module Transit
module Marshaler
class OrigJson < Json
def find_handler(obj)
obj.class.ancestors.each do |a|
if handler = @handlers[a]
return handler
end
end
nil
end
end
class PerfJson < Json
def find_handler(obj)
handler = @handlers[obj.class]
return handler if handler
obj.class.ancestors.each do |a|
if handler = @handlers[a]
return handler
end
end
nil
end
end
end
class OrigWriter < Writer
def initialize(format, io, opts = {})
@marshaler = Marshaler::OrigJson.new(io, {:handlers => {},
:oj_opts => {:indent => -1}}.merge(opts))
end
end
class PerfWriter < Writer
def initialize(format, io, opts = {})
@marshaler = Marshaler::PerfJson.new(io, {:handlers => {},
:oj_opts => {:indent => -1}}.merge(opts))
end
end
end
original_writer = Transit::OrigWriter.new(:json, StringIO.new)
perf_writer = Transit::PerfWriter.new(:json, StringIO.new)
n = 100
Benchmark.benchmark do |bm|
puts "original"
3.times do
bm.report do
n.times do
original_writer.write(example)
end
end
end
puts
puts "perf"
3.times do
bm.report do
n.times do
perf_writer.write(example)
end
end
end
end
#original
#4.490000 0.000000 4.490000 ( 4.505609)
#4.510000 0.010000 4.520000 ( 4.522835)
#4.500000 0.010000 4.510000 ( 4.515551)
#perf
#2.950000 0.010000 2.960000 ( 2.955664)
#2.920000 0.000000 2.920000 ( 2.934824)
#2.940000 0.010000 2.950000 ( 2.951365)
Please, review and accept the attached patch.