Skip to content
  • Jonathan Hefner's avatar
    8e383fda
    Avoid double type cast when serializing attributes · 8e383fda
    Jonathan Hefner authored
    Most model attribute types try to cast a given value before serializing
    it.  This allows uncast values to be passed to finder methods and still
    be serialized appropriately.  However, when persisting a model, this
    cast is unnecessary because the value will already have been cast by
    `ActiveModel::Attribute#value`.
    
    To eliminate the overhead of a 2nd cast, this commit introduces a
    `ActiveModel::Type::SerializeCastValue` module.  Types can include this
    module, and their `serialize_cast_value` method will be called instead
    of `serialize` when serializing an already-cast value.
    
    To preserve existing behavior of any user types that subclass Rails'
    types, `serialize_after_cast` will only be called if the type itself
    (not a superclass) includes `ActiveModel::Type::SerializeCastValue`.
    This also applies to type decorators implemented via `DelegateClass`.
    
    Benchmark script:
    
      ```ruby
      require "active_model"
      require "benchmark/ips"
    
      class ActiveModel::Attribute
        alias baseline_value_for_database value_for_database
      end
    
      VALUES = {
        my_big_integer: "123456",
        my_boolean: "true",
        my_date: "1999-12-31",
        my_datetime: "1999-12-31 12:34:56 UTC",
        my_decimal: "123.456",
        my_float: "123.456",
        my_immutable_string: "abcdef",
        my_integer: "123456",
        my_string: "abcdef",
        my_time: "1999-12-31T12:34:56.789-10:00",
      }
    
      TYPES = VALUES.to_h { |name, value| [name, name.to_s.delete_prefix("my_").to_sym] }
    
      class MyModel
        include ActiveModel::API
        include ActiveModel::Attributes
    
        TYPES.each do |name, type|
          attribute name, type
        end
      end
    
      TYPES.each do |name, type|
        $attribute_set ||= MyModel.new(VALUES).instance_variable_get(:@attributes)
        attribute = $attribute_set[name.to_s]
    
        puts "=" * 72
        Benchmark.ips do |x|
          x.report("#{type} before") { attribute.baseline_value_for_database }
          x.report("#{type} after") { attribute.value_for_database }
          x.compare!
        end
      end
      ```
    
    Benchmark results:
    
      ```
      ========================================================================
      Warming up --------------------------------------
        big_integer before   100.417k i/100ms
         big_integer after   260.375k i/100ms
      Calculating -------------------------------------
        big_integer before      1.005M (± 1.0%) i/s -      5.121M in   5.096498s
         big_integer after      2.630M (± 1.0%) i/s -     13.279M in   5.050387s
    
      Comparison:
         big_integer after:  2629583.6 i/s
        big_integer before:  1004961.2 i/s - 2.62x  (± 0.00) slower
    
      ========================================================================
      Warming up --------------------------------------
            boolean before   230.663k i/100ms
             boolean after   299.262k i/100ms
      Calculating -------------------------------------
            boolean before      2.313M (± 0.7%) i/s -     11.764M in   5.085925s
             boolean after      3.037M (± 0.6%) i/s -     15.262M in   5.026280s
    
      Comparison:
             boolean after:  3036640.8 i/s
            boolean before:  2313127.8 i/s - 1.31x  (± 0.00) slower
    
      ========================================================================
      Warming up --------------------------------------
               date before   148.821k i/100ms
                date after   298.939k i/100ms
      Calculating -------------------------------------
               date before      1.486M (± 0.6%) i/s -      7.441M in   5.006091s
                date after      2.963M (± 0.8%) i/s -     14.947M in   5.045651s
    
      Comparison:
                date after:  2962535.3 i/s
               date before:  1486459.4 i/s - 1.99x  (± 0.00) slower
    
      ========================================================================
      Warming up --------------------------------------
           datetime before    92.818k i/100ms
            datetime after   136.710k i/100ms
      Calculating -------------------------------------
           datetime before    920.236k (± 0.6%) i/s -      4.641M in   5.043355s
            datetime after      1.366M (± 0.8%) i/s -      6.836M in   5.003307s
    
      Comparison:
            datetime after:  1366294.1 i/s
           datetime before:   920236.1 i/s - 1.48x  (± 0.00) slower
    
      ========================================================================
      Warming up --------------------------------------
            decimal before    50.194k i/100ms
             decimal after   298.674k i/100ms
      Calculating -------------------------------------
            decimal before    494.141k (± 1.4%) i/s -      2.510M in   5.079995s
             decimal after      3.015M (± 1.0%) i/s -     15.232M in   5.052929s
    
      Comparison:
             decimal after:  3014901.3 i/s
            decimal before:   494141.2 i/s - 6.10x  (± 0.00) slower
    
      ========================================================================
      Warming up --------------------------------------
              float before   217.547k i/100ms
               float after   298.106k i/100ms
      Calculating -------------------------------------
              float before      2.157M (± 0.8%) i/s -     10.877M in   5.043292s
               float after      2.991M (± 0.6%) i/s -     15.203M in   5.082806s
    
      Comparison:
               float after:  2991262.8 i/s
              float before:  2156940.2 i/s - 1.39x  (± 0.00) slower
    
      ========================================================================
      Warming up --------------------------------------
      immutable_string before
                             163.287k i/100ms
      immutable_string after
                             298.245k i/100ms
      Calculating -------------------------------------
      immutable_string before
                                1.652M (± 0.7%) i/s -      8.328M in   5.040855s
      immutable_string after
                                3.022M (± 0.9%) i/s -     15.210M in   5.033151s
    
      Comparison:
      immutable_string after:  3022313.3 i/s
      immutable_string before:  1652121.7 i/s - 1.83x  (± 0.00) slower
    
      ========================================================================
      Warming up --------------------------------------
            integer before   115.383k i/100ms
             integer after   159.702k i/100ms
      Calculating -------------------------------------
            integer before      1.132M (± 0.8%) i/s -      5.769M in   5.095041s
             integer after      1.641M (± 0.5%) i/s -      8.305M in   5.061893s
    
      Comparison:
             integer after:  1640635.8 i/s
            integer before:  1132381.5 i/s - 1.45x  (± 0.00) slower
    
      ========================================================================
      Warming up --------------------------------------
             string before   163.061k i/100ms
              string after   299.885k i/100ms
      Calculating -------------------------------------
             string before      1.659M (± 0.7%) i/s -      8.316M in   5.012609s
              string after      2.999M (± 0.6%) i/s -     15.294M in   5.100008s
    
      Comparison:
              string after:  2998956.0 i/s
             string before:  1659115.6 i/s - 1.81x  (± 0.00) slower
    
      ========================================================================
      Warming up --------------------------------------
               time before    98.250k i/100ms
                time after   133.463k i/100ms
      Calculating -------------------------------------
               time before    987.771k (± 0.7%) i/s -      5.011M in   5.073023s
                time after      1.330M (± 0.5%) i/s -      6.673M in   5.016573s
    
      Comparison:
                time after:  1330253.9 i/s
               time before:   987771.0 i/s - 1.35x  (± 0.00) slower
      ```
    8e383fda
    Avoid double type cast when serializing attributes
    Jonathan Hefner authored
    Most model attribute types try to cast a given value before serializing
    it.  This allows uncast values to be passed to finder methods and still
    be serialized appropriately.  However, when persisting a model, this
    cast is unnecessary because the value will already have been cast by
    `ActiveModel::Attribute#value`.
    
    To eliminate the overhead of a 2nd cast, this commit introduces a
    `ActiveModel::Type::SerializeCastValue` module.  Types can include this
    module, and their `serialize_cast_value` method will be called instead
    of `serialize` when serializing an already-cast value.
    
    To preserve existing behavior of any user types that subclass Rails'
    types, `serialize_after_cast` will only be called if the type itself
    (not a superclass) includes `ActiveModel::Type::SerializeCastValue`.
    This also applies to type decorators implemented via `DelegateClass`.
    
    Benchmark script:
    
      ```ruby
      require "active_model"
      require "benchmark/ips"
    
      class ActiveModel::Attribute
        alias baseline_value_for_database value_for_database
      end
    
      VALUES = {
        my_big_integer: "123456",
        my_boolean: "true",
        my_date: "1999-12-31",
        my_datetime: "1999-12-31 12:34:56 UTC",
        my_decimal: "123.456",
        my_float: "123.456",
        my_immutable_string: "abcdef",
        my_integer: "123456",
        my_string: "abcdef",
        my_time: "1999-12-31T12:34:56.789-10:00",
      }
    
      TYPES = VALUES.to_h { |name, value| [name, name.to_s.delete_prefix("my_").to_sym] }
    
      class MyModel
        include ActiveModel::API
        include ActiveModel::Attributes
    
        TYPES.each do |name, type|
          attribute name, type
        end
      end
    
      TYPES.each do |name, type|
        $attribute_set ||= MyModel.new(VALUES).instance_variable_get(:@attributes)
        attribute = $attribute_set[name.to_s]
    
        puts "=" * 72
        Benchmark.ips do |x|
          x.report("#{type} before") { attribute.baseline_value_for_database }
          x.report("#{type} after") { attribute.value_for_database }
          x.compare!
        end
      end
      ```
    
    Benchmark results:
    
      ```
      ========================================================================
      Warming up --------------------------------------
        big_integer before   100.417k i/100ms
         big_integer after   260.375k i/100ms
      Calculating -------------------------------------
        big_integer before      1.005M (± 1.0%) i/s -      5.121M in   5.096498s
         big_integer after      2.630M (± 1.0%) i/s -     13.279M in   5.050387s
    
      Comparison:
         big_integer after:  2629583.6 i/s
        big_integer before:  1004961.2 i/s - 2.62x  (± 0.00) slower
    
      ========================================================================
      Warming up --------------------------------------
            boolean before   230.663k i/100ms
             boolean after   299.262k i/100ms
      Calculating -------------------------------------
            boolean before      2.313M (± 0.7%) i/s -     11.764M in   5.085925s
             boolean after      3.037M (± 0.6%) i/s -     15.262M in   5.026280s
    
      Comparison:
             boolean after:  3036640.8 i/s
            boolean before:  2313127.8 i/s - 1.31x  (± 0.00) slower
    
      ========================================================================
      Warming up --------------------------------------
               date before   148.821k i/100ms
                date after   298.939k i/100ms
      Calculating -------------------------------------
               date before      1.486M (± 0.6%) i/s -      7.441M in   5.006091s
                date after      2.963M (± 0.8%) i/s -     14.947M in   5.045651s
    
      Comparison:
                date after:  2962535.3 i/s
               date before:  1486459.4 i/s - 1.99x  (± 0.00) slower
    
      ========================================================================
      Warming up --------------------------------------
           datetime before    92.818k i/100ms
            datetime after   136.710k i/100ms
      Calculating -------------------------------------
           datetime before    920.236k (± 0.6%) i/s -      4.641M in   5.043355s
            datetime after      1.366M (± 0.8%) i/s -      6.836M in   5.003307s
    
      Comparison:
            datetime after:  1366294.1 i/s
           datetime before:   920236.1 i/s - 1.48x  (± 0.00) slower
    
      ========================================================================
      Warming up --------------------------------------
            decimal before    50.194k i/100ms
             decimal after   298.674k i/100ms
      Calculating -------------------------------------
            decimal before    494.141k (± 1.4%) i/s -      2.510M in   5.079995s
             decimal after      3.015M (± 1.0%) i/s -     15.232M in   5.052929s
    
      Comparison:
             decimal after:  3014901.3 i/s
            decimal before:   494141.2 i/s - 6.10x  (± 0.00) slower
    
      ========================================================================
      Warming up --------------------------------------
              float before   217.547k i/100ms
               float after   298.106k i/100ms
      Calculating -------------------------------------
              float before      2.157M (± 0.8%) i/s -     10.877M in   5.043292s
               float after      2.991M (± 0.6%) i/s -     15.203M in   5.082806s
    
      Comparison:
               float after:  2991262.8 i/s
              float before:  2156940.2 i/s - 1.39x  (± 0.00) slower
    
      ========================================================================
      Warming up --------------------------------------
      immutable_string before
                             163.287k i/100ms
      immutable_string after
                             298.245k i/100ms
      Calculating -------------------------------------
      immutable_string before
                                1.652M (± 0.7%) i/s -      8.328M in   5.040855s
      immutable_string after
                                3.022M (± 0.9%) i/s -     15.210M in   5.033151s
    
      Comparison:
      immutable_string after:  3022313.3 i/s
      immutable_string before:  1652121.7 i/s - 1.83x  (± 0.00) slower
    
      ========================================================================
      Warming up --------------------------------------
            integer before   115.383k i/100ms
             integer after   159.702k i/100ms
      Calculating -------------------------------------
            integer before      1.132M (± 0.8%) i/s -      5.769M in   5.095041s
             integer after      1.641M (± 0.5%) i/s -      8.305M in   5.061893s
    
      Comparison:
             integer after:  1640635.8 i/s
            integer before:  1132381.5 i/s - 1.45x  (± 0.00) slower
    
      ========================================================================
      Warming up --------------------------------------
             string before   163.061k i/100ms
              string after   299.885k i/100ms
      Calculating -------------------------------------
             string before      1.659M (± 0.7%) i/s -      8.316M in   5.012609s
              string after      2.999M (± 0.6%) i/s -     15.294M in   5.100008s
    
      Comparison:
              string after:  2998956.0 i/s
             string before:  1659115.6 i/s - 1.81x  (± 0.00) slower
    
      ========================================================================
      Warming up --------------------------------------
               time before    98.250k i/100ms
                time after   133.463k i/100ms
      Calculating -------------------------------------
               time before    987.771k (± 0.7%) i/s -      5.011M in   5.073023s
                time after      1.330M (± 0.5%) i/s -      6.673M in   5.016573s
    
      Comparison:
                time after:  1330253.9 i/s
               time before:   987771.0 i/s - 1.35x  (± 0.00) slower
      ```
Loading