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

Improve performance of Marshaler::Base#find_handler #23

@nashbridges

Description

@nashbridges

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions