Skip to content
  • Konstantin Lazarev's avatar
    cdf8a2b4
    Cache results of computing model type · cdf8a2b4
    Konstantin Lazarev authored
    We faced a significant performance decrease when we started using STI
    without storing full namespaced class name in type column (because of PostgreSQL
    length limit for ENUM types).
    We realized that the cause of it is the slow STI model instantiation. Problematic
    method appears to be `ActiveRecord::Base.compute_type`, which is used to find
    the right class for STI model on every instantiation.
    It builds an array of candidate types and then iterates through it calling
    `safe_constantize` on every type until it finds appropriate constant. So if
    desired type isn't the first element in this array there will be at least one
    unsuccessful call to `safe_constantize`, which is very expensive, since it's
    defined in terms of `begin; rescue; end`.
    
    This commit is an attempt to speed up `compute_type` method simply by caching
    results of previous calls.
    
    ```ruby
    class MyCompany::MyApp::Business::Accounts::Base < ApplicationRecord
      self.table_name = 'accounts'
      self.store_full_sti_class = false
    end
    
    class MyCompany::MyApp::Business::Accounts::Free < Base
    end
    
    class MyCompany::MyApp::Business::Accounts::Standard < Base
      # patch .compute_type there
    end
    
    puts '======================= .compute_type ======================='
    Benchmark.ips do |x|
      x.report("original method") do
        MyCompany::MyApp::Business::Accounts::Free.send :compute_type, 'Free'
      end
      x.report("with types cached") do
        MyCompany::MyApp::Business::Accounts::Standard.send :compute_type, 'Standard'
      end
      x.compare!
    end
    ```
    
    ```
    ======================= .compute_type =======================
      with types cached:  1529019.4 i/s
        original method:     2850.2 i/s - 536.46x  slower
    ```
    
    ```ruby
    5_000.times do |i|
      MyCompany::MyApp::Business::Accounts::Standard.create!(name: "standard_#{i}")
    end
    
    5_000.times do |i|
      MyCompany::MyApp::Business::Accounts::Free.create!(name: "free_#{i}")
    end
    
    puts '====================== .limit(100).to_a ======================='
    Benchmark.ips do |x|
      x.report("without .compute_type patch") do
        MyCompany::MyApp::Business::Accounts::Free.limit(100).to_a
      end
      x.report("with .compute_type patch") do
        MyCompany::MyApp::Business::Accounts::Standard.limit(100).to_a
      end
      x.compare!
    end
    ```
    
    ```
    ====================== .limit(100).to_a =======================
         with .compute_type patch:      360.5 i/s
      without .compute_type patch:       24.7 i/s - 14.59x  slower
    ```
    cdf8a2b4
    Cache results of computing model type
    Konstantin Lazarev authored
    We faced a significant performance decrease when we started using STI
    without storing full namespaced class name in type column (because of PostgreSQL
    length limit for ENUM types).
    We realized that the cause of it is the slow STI model instantiation. Problematic
    method appears to be `ActiveRecord::Base.compute_type`, which is used to find
    the right class for STI model on every instantiation.
    It builds an array of candidate types and then iterates through it calling
    `safe_constantize` on every type until it finds appropriate constant. So if
    desired type isn't the first element in this array there will be at least one
    unsuccessful call to `safe_constantize`, which is very expensive, since it's
    defined in terms of `begin; rescue; end`.
    
    This commit is an attempt to speed up `compute_type` method simply by caching
    results of previous calls.
    
    ```ruby
    class MyCompany::MyApp::Business::Accounts::Base < ApplicationRecord
      self.table_name = 'accounts'
      self.store_full_sti_class = false
    end
    
    class MyCompany::MyApp::Business::Accounts::Free < Base
    end
    
    class MyCompany::MyApp::Business::Accounts::Standard < Base
      # patch .compute_type there
    end
    
    puts '======================= .compute_type ======================='
    Benchmark.ips do |x|
      x.report("original method") do
        MyCompany::MyApp::Business::Accounts::Free.send :compute_type, 'Free'
      end
      x.report("with types cached") do
        MyCompany::MyApp::Business::Accounts::Standard.send :compute_type, 'Standard'
      end
      x.compare!
    end
    ```
    
    ```
    ======================= .compute_type =======================
      with types cached:  1529019.4 i/s
        original method:     2850.2 i/s - 536.46x  slower
    ```
    
    ```ruby
    5_000.times do |i|
      MyCompany::MyApp::Business::Accounts::Standard.create!(name: "standard_#{i}")
    end
    
    5_000.times do |i|
      MyCompany::MyApp::Business::Accounts::Free.create!(name: "free_#{i}")
    end
    
    puts '====================== .limit(100).to_a ======================='
    Benchmark.ips do |x|
      x.report("without .compute_type patch") do
        MyCompany::MyApp::Business::Accounts::Free.limit(100).to_a
      end
      x.report("with .compute_type patch") do
        MyCompany::MyApp::Business::Accounts::Standard.limit(100).to_a
      end
      x.compare!
    end
    ```
    
    ```
    ====================== .limit(100).to_a =======================
         with .compute_type patch:      360.5 i/s
      without .compute_type patch:       24.7 i/s - 14.59x  slower
    ```
Loading