Avoid value_for_database if attribute not updated

After a record is saved, `ActiveModel::Attribute#forgetting_assignment`
is called on each of its attributes.  `forgetting_assignment`, in turn,
calls `ActiveModel::Attribute#value_for_database`.  If an attribute was
not updated, and therefore `value_for_database` was not previously
computed, this will involve an unnecessary serialize (and cast,
depending on type).  And if the attribute was not previously read, it
will also involve an unnecessary deserialize (and cast, depending on
type).

This commit overrides `FromDatabase#forgetting_assignment` to dup the
attribute instead of computing `value_for_database` in the case where
the attribute was not updated.  This can improve the performance for
basic types:

**Benchmark script**

  ```ruby
  # frozen_string_literal: true
  require "benchmark/ips"

  ActiveModel::Attribute.alias_method :baseline_forgetting_assignment, :forgetting_assignment

  {
    big_integer: 123456,
    boolean: 1,
    date: "1999-12-31",
    datetime: "1999-12-31 12:34:56.789011",
    decimal: 123.456,
    float: 123.456,
    immutable_string: "abcdef",
    integer: 123456,
    string: "abcdef",
    time: "1999-12-31 12:34:56.789011",
  }.each do |type_name, value_before_type_cast|
    puts "=== #{type_name} ".ljust(70, "=")

    type = ActiveModel::Type.lookup(type_name)

    Benchmark.ips do |x|
      x.report("before") do
        attribute = ActiveModel::Attribute.from_database("x", value_before_type_cast, type)
        attribute.baseline_forgetting_assignment
      end

      x.report("after") do
        attribute = ActiveModel::Attribute.from_database("x", value_before_type_cast, type)
        attribute.forgetting_assignment
      end

      x.compare!
    end

    Benchmark.ips do |x|
      x.report("before w/ read") do
        attribute = ActiveModel::Attribute.from_database("x", value_before_type_cast, type)
        attribute.value
        attribute.baseline_forgetting_assignment
      end

      x.report("after w/ read") do
        attribute = ActiveModel::Attribute.from_database("x", value_before_type_cast, type)
        attribute.value
        attribute.forgetting_assignment
      end

      x.compare!
    end
  end
  ```

**Results**

  ```
  === big_integer ======================================================
  Warming up --------------------------------------
                before    35.830k i/100ms
                 after    99.520k i/100ms
  Calculating -------------------------------------
                before    356.010k (± 1.0%) i/s -      1.792M in   5.032655s
                 after    989.674k (± 1.2%) i/s -      4.976M in   5.028638s

  Comparison:
                 after:   989674.2 i/s
                before:   356010.4 i/s - 2.78x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read    35.030k i/100ms
         after w/ read    56.005k i/100ms
  Calculating -------------------------------------
        before w/ read    349.509k (± 1.3%) i/s -      1.752M in   5.012188s
         after w/ read    558.953k (± 1.2%) i/s -      2.800M in   5.010511s

  Comparison:
         after w/ read:   558953.4 i/s
        before w/ read:   349508.6 i/s - 1.60x  (± 0.00) slower

  === boolean ==========================================================
  Warming up --------------------------------------
                before    31.989k i/100ms
                 after    99.698k i/100ms
  Calculating -------------------------------------
                before    319.969k (± 1.0%) i/s -      1.631M in   5.099207s
                 after    994.759k (± 1.1%) i/s -      4.985M in   5.011775s

  Comparison:
                 after:   994758.6 i/s
                before:   319969.3 i/s - 3.11x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read    31.090k i/100ms
         after w/ read    33.306k i/100ms
  Calculating -------------------------------------
        before w/ read    308.859k (± 1.1%) i/s -      1.554M in   5.033639s
         after w/ read    332.726k (± 1.1%) i/s -      1.665M in   5.005669s

  Comparison:
         after w/ read:   332726.2 i/s
        before w/ read:   308858.6 i/s - 1.08x  (± 0.00) slower

  === date =============================================================
  Warming up --------------------------------------
                before    14.466k i/100ms
                 after    98.451k i/100ms
  Calculating -------------------------------------
                before    145.594k (± 1.4%) i/s -    737.766k in   5.068354s
                 after    990.526k (± 1.1%) i/s -      5.021M in   5.069638s

  Comparison:
                 after:   990526.4 i/s
                before:   145593.9 i/s - 6.80x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read    14.130k i/100ms
         after w/ read    14.702k i/100ms
  Calculating -------------------------------------
        before w/ read    141.887k (± 1.3%) i/s -    720.630k in   5.079719s
         after w/ read    148.244k (± 1.0%) i/s -    749.802k in   5.058433s

  Comparison:
         after w/ read:   148244.1 i/s
        before w/ read:   141886.8 i/s - 1.04x  (± 0.00) slower

  === datetime =========================================================
  Warming up --------------------------------------
                before     9.484k i/100ms
                 after    97.889k i/100ms
  Calculating -------------------------------------
                before     95.640k (± 1.3%) i/s -    483.684k in   5.058190s
                 after    988.827k (± 1.1%) i/s -      4.992M in   5.049376s

  Comparison:
                 after:   988827.4 i/s
                before:    95639.7 i/s - 10.34x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read     9.440k i/100ms
         after w/ read    10.479k i/100ms
  Calculating -------------------------------------
        before w/ read     94.420k (± 1.1%) i/s -    481.440k in   5.099530s
         after w/ read    105.935k (± 1.0%) i/s -    534.429k in   5.045377s

  Comparison:
         after w/ read:   105935.2 i/s
        before w/ read:    94419.6 i/s - 1.12x  (± 0.00) slower

  === decimal ==========================================================
  Warming up --------------------------------------
                before    12.877k i/100ms
                 after    98.081k i/100ms
  Calculating -------------------------------------
                before    127.627k (± 1.4%) i/s -    643.850k in   5.045749s
                 after    990.178k (± 0.9%) i/s -      5.002M in   5.052175s

  Comparison:
                 after:   990178.0 i/s
                before:   127627.5 i/s - 7.76x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read    12.640k i/100ms
         after w/ read    19.739k i/100ms
  Calculating -------------------------------------
        before w/ read    124.933k (± 1.4%) i/s -    632.000k in   5.059694s
         after w/ read    200.110k (± 1.0%) i/s -      1.007M in   5.031169s

  Comparison:
         after w/ read:   200110.3 i/s
        before w/ read:   124932.8 i/s - 1.60x  (± 0.00) slower

  === float ============================================================
  Warming up --------------------------------------
                before    45.424k i/100ms
                 after    99.512k i/100ms
  Calculating -------------------------------------
                before    449.861k (± 1.0%) i/s -      2.271M in   5.049231s
                 after    991.130k (± 1.1%) i/s -      4.976M in   5.020705s

  Comparison:
                 after:   991130.0 i/s
                before:   449861.3 i/s - 2.20x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read    43.283k i/100ms
         after w/ read    42.888k i/100ms
  Calculating -------------------------------------
        before w/ read    431.630k (± 1.0%) i/s -      2.164M in   5.014423s
         after w/ read    429.631k (± 1.0%) i/s -      2.187M in   5.091609s

  Comparison:
        before w/ read:   431630.0 i/s
         after w/ read:   429631.0 i/s - same-ish: difference falls within error

  === immutable_string =================================================
  Warming up --------------------------------------
                before    36.820k i/100ms
                 after    97.812k i/100ms
  Calculating -------------------------------------
                before    369.412k (± 1.1%) i/s -      1.878M in   5.083860s
                 after    985.345k (± 1.1%) i/s -      4.988M in   5.063224s

  Comparison:
                 after:   985345.0 i/s
                before:   369411.8 i/s - 2.67x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read    36.082k i/100ms
         after w/ read    37.250k i/100ms
  Calculating -------------------------------------
        before w/ read    361.134k (± 1.0%) i/s -      1.840M in   5.096085s
         after w/ read    369.471k (± 0.8%) i/s -      1.862M in   5.041349s

  Comparison:
         after w/ read:   369471.0 i/s
        before w/ read:   361133.6 i/s - 1.02x  (± 0.00) slower

  === integer ==========================================================
  Warming up --------------------------------------
                before    35.763k i/100ms
                 after    98.444k i/100ms
  Calculating -------------------------------------
                before    358.171k (± 1.1%) i/s -      1.824M in   5.092870s
                 after    995.138k (± 1.0%) i/s -      5.021M in   5.045729s

  Comparison:
                 after:   995137.6 i/s
                before:   358171.4 i/s - 2.78x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read    34.534k i/100ms
         after w/ read    53.971k i/100ms
  Calculating -------------------------------------
        before w/ read    345.029k (± 1.0%) i/s -      1.727M in   5.005030s
         after w/ read    537.373k (± 0.9%) i/s -      2.699M in   5.022178s

  Comparison:
         after w/ read:   537373.4 i/s
        before w/ read:   345029.3 i/s - 1.56x  (± 0.00) slower

  === string ===========================================================
  Warming up --------------------------------------
                before    33.882k i/100ms
                 after    97.193k i/100ms
  Calculating -------------------------------------
                before    339.174k (± 1.1%) i/s -      1.728M in   5.095272s
                 after    984.544k (± 0.9%) i/s -      4.957M in   5.035057s

  Comparison:
                 after:   984543.5 i/s
                before:   339174.0 i/s - 2.90x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read    32.897k i/100ms
         after w/ read    31.767k i/100ms
  Calculating -------------------------------------
        before w/ read    329.911k (± 0.8%) i/s -      1.678M in   5.085756s
         after w/ read    320.010k (± 0.6%) i/s -      1.620M in   5.062867s

  Comparison:
        before w/ read:   329910.6 i/s
         after w/ read:   320010.1 i/s - 1.03x  (± 0.00) slower

  === time =============================================================
  Warming up --------------------------------------
                before     7.461k i/100ms
                 after    97.467k i/100ms
  Calculating -------------------------------------
                before     74.854k (± 1.5%) i/s -    380.511k in   5.084475s
                 after    986.169k (± 0.9%) i/s -      4.971M in   5.040923s

  Comparison:
                 after:   986168.8 i/s
                before:    74854.5 i/s - 13.17x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read     7.438k i/100ms
         after w/ read     8.046k i/100ms
  Calculating -------------------------------------
        before w/ read     73.436k (± 1.5%) i/s -    371.900k in   5.065491s
         after w/ read     79.752k (± 1.5%) i/s -    402.300k in   5.045612s

  Comparison:
         after w/ read:    79751.7 i/s
        before w/ read:    73435.8 i/s - 1.09x  (± 0.00) slower
  ```

And, notably, this avoids potentially expensive serialize and
deserialize calls for non-basic types, such as with encrypted
attributes, when the attribute has not been read or assigned:

**Before**

  ```irb
  irb> Post.encrypts :body, encryptor: MyLoggingEncryptor.new

  irb> post = Post.create!(title: "untitled", body: "The Body")
  ! encrypting "The Body"
    TRANSACTION (0.1ms)  begin transaction
    Post Create (0.6ms)  INSERT INTO "posts" ...
  ! decrypted "The Body"
    TRANSACTION (120.2ms)  commit transaction

  irb> post.update!(title: "The Title")
    TRANSACTION (0.1ms)  begin transaction
    Post Update (0.7ms)  UPDATE "posts" ...
  ! decrypted "The Body"
  ! encrypting "The Body"
    TRANSACTION (92.1ms)  commit transaction
  ```

**After**

  ```irb
  irb> Post.encrypts :body, encryptor: MyLoggingEncryptor.new

  irb> post = Post.create!(title: "untitled", body: "The Body")
  ! encrypting "The Body"
    TRANSACTION (0.1ms)  begin transaction
    Post Create (0.7ms)  INSERT INTO "posts" ...
  ! decrypted "The Body"
    TRANSACTION (103.7ms)  commit transaction

  irb> post.update!(title: "The Title")
    TRANSACTION (0.1ms)  begin transaction
    Post Update (0.7ms)  UPDATE "posts" ...
    TRANSACTION (91.2ms)  commit transaction
  ```
This commit is contained in:
Jonathan Hefner 2022-10-19 22:48:02 -05:00
parent 6676989957
commit 98aa59a7f6
1 changed files with 13 additions and 0 deletions

View File

@ -175,6 +175,19 @@ module ActiveModel
type.deserialize(value)
end
def forgetting_assignment
# If this attribute was not persisted (with a `value_for_database`
# that might differ from `value_before_type_cast`) and `value` has not
# changed in place, we can simply dup this attribute to avoid
# deserialize / cast / serialize calls from computing the new
# attribute's `value_before_type_cast`.
if !defined?(@value_for_database) && !changed_in_place?
dup
else
super
end
end
private
def _original_value_for_database
value_before_type_cast