Skip to content
This repository has been archived by the owner on Jun 3, 2023. It is now read-only.

Improve performance of Marshaler::Base#find_handler #23

Open
nashbridges opened this issue Sep 6, 2018 · 2 comments
Open

Improve performance of Marshaler::Base#find_handler #23

nashbridges opened this issue Sep 6, 2018 · 2 comments

Comments

@nashbridges
Copy link

nashbridges commented Sep 6, 2018

In our Rails application we see Marshaler::Base#find_handler 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

We have two bottlenecks:

  1. ActiveSupport modifies inheritance chain, so that core classes become second in the ancestors array. That ads up another cycle in the loop.
[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]
[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]
  1. even in programs without Active Support core_ext included Ruby has to create ancestors array, which is expensive and is not needed for core classes

With AS benchmark

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)

Without AS benchmark

require "bundler/setup"


$LOAD_PATH << File.expand_path("../../lib", __FILE__)


require "transit"


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
   #3.940000   0.010000   3.950000 (  3.951961)
   #3.890000   0.010000   3.900000 (  3.907909)
   #3.880000   0.010000   3.890000 (  3.886866)

#perf
   #2.950000   0.010000   2.960000 (  2.962318)
   #2.900000   0.000000   2.900000 (  2.904893)
   #2.880000   0.010000   2.890000 (  2.895117)
@dchelimsky
Copy link
Contributor

Thanks for the very thorough report! I probably won't have time to look at this for a week or two, but it's on my radar.

@nashbridges
Copy link
Author

@dchelimsky kind reminder :)

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants