bullet icon indicating copy to clipboard operation
bullet copied to clipboard

Big performance issues with auto-crafted active record model

Open anykeyh opened this issue 7 years ago • 0 comments

TL;DR When bullet is activated, my request take ~4 seconds to load. When it's not, it take only 0.05s

The problem

I've a big performance issue in the case of crafted active record model.

Before showing the issue, I need to explain first what we do:

We need to handle parameterable views in our project, to simplify our code and avoid N+1 request. The fact is the views we are trying to render are too complex for the standard eager_loading / includes or active record. We then prefer to craft our views through SQL and use it as readonly model through active record.

Here is an example of our views:

class View::Parametrable < ActiveRecord::Base
  self.abstract_class = true

  def readonly?; true; end

  def self.view
    raise NotImplementedError, "Parametrable view must implement a class method view to perform the SQL!"
  end

  def self.columns_definition
    raise NotImplementedError, "You must setup the columns"
  end

  def self.[](*args)
    Class.new(self) do
      default_scope -> { include_from(*args) }
    end
  end

  def self.load_schema!
    @columns_hash = Hash[
      self.columns_definition.map do |name, value|
      [name.to_s,
        ActiveRecord::ConnectionAdapters::Column.new(
          name.to_s,
          nil,
          ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(sql_type: value)
        )
      ]
    end
  ]
  end

private

  def self.include_from *args
    @computed_view ||= sanitize_sql([self.view, *args])
    code = "(#{@computed_view}) AS #{self.table_name}"
    from(code)
  end

end

And here an example of implementation:

class View::UserHistory < View::Parametrable
  def self.columns_definition
    {
      id: 'integer',
      user_id: 'integer',
      project_id: 'integer',
      batch_id: 'integer',
      project_name: 'text',
      state: 'text',
      starts_at: 'date',
      ends_at: 'date',
      status: 'string',
      task_id: 'integer',
      total_time: 'integer',
      end_reason: 'string',
      total_pause_time: 'integer'
    }
  end

  def self.view
    <<-SQL
    SELECT
      user_time_ranges.id as id,
      user_time_ranges.user_id as user_id,
      projects.id as project_id,
      project_tasks.batch_id as batch_id,
      projects.name as project_name,
      project_task_states.state as state,
      user_time_ranges.starts_at as starts_at,
      user_time_ranges.ends_at as ends_at,
      (
        CASE
          WHEN project_task_states.ends_at IS NULL THEN 'done'
          ELSE 'in_progress'
        END
      ) as status,
      project_tasks.id as task_id,
      user_time_ranges.total_in_seconds as total_time,
      user_time_ranges.end_reason as end_reason,
      (
        SELECT
          EXTRACT(
            EPOCH FROM utr2.starts_at - user_time_ranges.ends_at
          )::integer
        FROM user_time_ranges utr2
        WHERE
          utr2.starts_at > user_time_ranges.ends_at AND
          utr2.user_id = :user_id
        ORDER BY utr2.starts_at ASC
        LIMIT 1
      ) as total_pause_time
    FROM user_time_ranges
    INNER JOIN project_tasks ON project_tasks.id = user_time_ranges.task_id
    INNER JOIN project_task_states ON project_task_states.id = user_time_ranges.state_id
    INNER JOIN projects ON projects.id = project_tasks.project_id
    WHERE
      user_time_ranges.starts_at >= :starts_at AND
      user_time_ranges.ends_at <= :ends_at AND
      user_time_ranges.user_id = :user_id
    ORDER BY
      user_time_ranges.starts_at ASC
    SQL
  end

  belongs_to :project
  belongs_to :task, class_name: "Project::Task"
  belongs_to :user
  belongs_to :time_range, class_name: "User::TimeRange"
end

Then we access to the view like this:

View::UserHistory[starts_at: '2017-02-15 00:00:00', ends_at: '2017-02-15 23:59:59', user_id: 53]

It works perfectly fine with Active Record, is quite fast and on our refactoring we improve the speed of our request by a factor of x10 or more.

However, in development target, bullet is starting to get stuck with theses kind of models.

Here is the profiling stack trace. As seen, the thread love to take time into Object#bullet_key.

As far as I know, our view generation strategy is a bit tricky, we fake the active record columns reflection and the model table doesn't really exists. Yeah, we forge the fake table in the FROM clause of ActiveRecord...

Is there anything we can do to improve our system and make it works with bullet without performance leaking ?

  %total   %self      total       self       wait      child            calls     name
--------------------------------------------------------------------------------
 100.00%   0.00%      4.611      0.000      0.000      4.611                1     ActionView::CompiledTemplates#_app_views_tasks__daily_user_table_html_haml___2212557634625081580_70280950334840
                      4.600      0.000      0.000      4.600              1/1     ActiveRecord::Relation#to_a
                      0.011      0.000      0.000      0.011              1/2     ActiveRecord::Scoping::Named::ClassMethods#all
                      0.000      0.000      0.000      0.000              1/1     Array#count
--------------------------------------------------------------------------------
                      4.600      0.000      0.000      4.600              1/1     ActionView::CompiledTemplates#_app_views_tasks__daily_user_table_html_haml___2212557634625081580_70280950334840
  99.77%   0.00%      4.600      0.000      0.000      4.600                1     ActiveRecord::Relation#to_a
                      4.600      0.000      0.000      4.600              1/2     ActiveRecord::Relation#records
                      0.000      0.000      0.000      0.000             1/21     Kernel#dup
--------------------------------------------------------------------------------
                      2.623      0.000      0.000      2.623              1/2     ActiveRecord::Relation#records
                      4.600      0.000      0.000      4.600              1/2     ActiveRecord::Relation#to_a
  99.77%   0.00%      4.600      0.000      0.000      4.600                2    *ActiveRecord::Relation#records
                      2.623      0.000      0.000      2.623              1/2     ActiveRecord::Relation#records
                      2.623      0.000      0.000      2.623              1/1     ActiveRecord::Relation#load
                      1.027      0.000      0.000      1.027              1/2     <Class::Bullet::Detector::NPlusOneQuery>#add_possible_objects
                      0.949      0.000      0.000      0.949              1/2     <Class::Bullet::Detector::CounterCache>#add_possible_objects
                      0.001      0.001      0.000      0.000             1/14     Module#name
                      0.000      0.000      0.000      0.000              1/1     Kernel#!~
                      0.000      0.000      0.000      0.000              1/6     <Module::Bullet>#start?
                      0.000      0.000      0.000      0.000          1/38827     Kernel#class
                      0.000      0.000      0.000      0.000              1/4     Array#first
--------------------------------------------------------------------------------
                      2.126      0.067      0.000      2.059        2156/4312     Array#each
                      2.331      0.075      0.000      2.256        2156/4312     Array#map
  96.68%   3.09%      4.458      0.143      0.000      4.315             4312     Object#bullet_key
                      4.120      4.120      0.000      0.000        4312/4312     Module#to_s
                      0.172      0.022      0.000      0.151        4312/6468     Object#primary_key_value
                      0.022      0.013      0.000      0.009        4312/4313     ActiveSupport::NumericWithFormat#to_s
                      0.001      0.001      0.000      0.000       4312/38827     Kernel#class
--------------------------------------------------------------------------------
                      4.120      4.120      0.000      0.000        4312/4312     Object#bullet_key
  89.36%  89.36%      4.120      4.120      0.000      0.000             4312     Module#to_s
--------------------------------------------------------------------------------
                      2.623      0.000      0.000      2.623              1/1     ActiveRecord::Relation#records
  56.89%   0.00%      2.623      0.000      0.000      2.623                1     ActiveRecord::Relation#load
                      2.623      0.000      0.000      2.623              1/1     ActiveRecord::Relation#exec_queries
--------------------------------------------------------------------------------
                      2.623      0.000      0.000      2.623              1/1     ActiveRecord::Relation#load
  56.89%   0.00%      2.623      0.000      0.000      2.623                1     ActiveRecord::Relation#exec_queries
                      2.623      0.000      0.000      2.623              1/1     <Class::ActiveRecord::Base>#find_by_sql
                      0.000      0.000      0.000      0.000              1/2     ActiveRecord::QueryMethods#arel
                      0.000      0.000      0.000      0.000              1/1     ActiveRecord::QueryMethods#bound_attributes
                      0.000      0.000      0.000      0.000              2/2     ActiveRecord::Relation#eager_loading?
                      0.000      0.000      0.000      0.000              1/1     ActiveRecord::Relation#build_preloader
                      0.000      0.000      0.000      0.000              1/5     Kernel#freeze
                      0.000      0.000      0.000      0.000              1/3     ActiveRecord::QueryMethods#preload_values
                      0.000      0.000      0.000      0.000              1/1     ActiveRecord::QueryMethods#readonly_value
                      0.000      0.000      0.000      0.000          1/12961     Array#each
                      0.000      0.000      0.000      0.000              1/5     ActiveRecord::QueryMethods#includes_values
--------------------------------------------------------------------------------
                      2.623      0.000      0.000      2.623              1/1     ActiveRecord::Relation#exec_queries
  56.89%   0.00%      2.623      0.000      0.000      2.623                1     <Class::ActiveRecord::Base>#find_by_sql
                      1.342      0.000      0.000      1.342              1/2     <Class::Bullet::Detector::CounterCache>#add_possible_objects
                      1.236      0.000      0.000      1.236              1/2     <Class::Bullet::Detector::NPlusOneQuery>#add_possible_objects
                      0.046      0.000      0.000      0.046              1/1     ActiveRecord::Querying#find_by_sql
                      0.000      0.000      0.000      0.000              1/6     <Module::Bullet>#start?
                      0.000      0.000      0.000      0.000          1/11336     Kernel#is_a?
--------------------------------------------------------------------------------
                      0.000      0.000      0.000      0.000             1/12     ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#exec_no_cache
                      0.000      0.000      0.000      0.000             1/12     Arel::SelectManager#project
                      0.004      0.004      0.000      0.000             2/12     ActiveRecord::Result#hash_rows
                      1.142      0.002      0.000      1.140             4/12     <Class::Bullet::Detector::NPlusOneQuery>#add_possible_objects
                      1.251      0.002      0.000      1.248             4/12     <Class::Bullet::Detector::CounterCache>#add_possible_objects
  51.97%   0.18%      2.396      0.008      0.000      2.388               12     Array#map
                      2.331      0.075      0.000      2.256        2156/4312     Object#bullet_key
                      0.057      0.005      0.000      0.051        2156/6468     Object#primary_key_value
                      0.000      0.000      0.000      0.000            13/21     Kernel#dup
                      0.000      0.000      0.000      0.000            13/13     String#freeze
                      0.000      0.000      0.000      0.000             1/12     Array#include?
                      0.000      0.000      0.000      0.000          1/38827     Kernel#class
--------------------------------------------------------------------------------
                      0.949      0.000      0.000      0.949              1/2     ActiveRecord::Relation#records
                      1.342      0.000      0.000      1.342              1/2     <Class::ActiveRecord::Base>#find_by_sql
  49.68%   0.00%      2.290      0.000      0.000      2.290                2     <Class::Bullet::Detector::CounterCache>#add_possible_objects
                      1.251      0.002      0.000      1.248             4/12     Array#map
                      1.039      0.002      0.000      1.037          2/12961     Array#each
                      0.000      0.000      0.000      0.000              2/4     Array#join
                      0.000      0.000      0.000      0.000              2/4     <Module::Bullet>#debug
                      0.000      0.000      0.000      0.000              2/6     <Module::Bullet>#start?
                      0.000      0.000      0.000      0.000              2/4     Array#compact
                      0.000      0.000      0.000      0.000              2/2     <Module::Bullet>#counter_cache_enable?
                      0.000      0.000      0.000      0.000              2/4     Kernel#Array
--------------------------------------------------------------------------------
                      1.027      0.000      0.000      1.027              1/2     ActiveRecord::Relation#records
                      1.236      0.000      0.000      1.236              1/2     <Class::ActiveRecord::Base>#find_by_sql
  49.08%   0.00%      2.263      0.000      0.000      2.263                2     <Class::Bullet::Detector::NPlusOneQuery>#add_possible_objects
                      1.142      0.002      0.000      1.140             4/12     Array#map
                      1.121      0.002      0.000      1.118          2/12961     Array#each
                      0.000      0.000      0.000      0.000              2/4     Array#join
                      0.000      0.000      0.000      0.000              2/4     <Module::Bullet>#debug
                      0.000      0.000      0.000      0.000              2/4     Array#compact
                      0.000      0.000      0.000      0.000              2/6     <Module::Bullet>#start?
                      0.000      0.000      0.000      0.000              2/2     <Module::Bullet>#n_plus_one_query_enable?
                      0.000      0.000      0.000      0.000              2/4     Kernel#Array
--------------------------------------------------------------------------------
                      0.000      0.000      0.000      0.000          1/12961     ActiveModel::AttributeMethods::ClassMethods#define_attribute_methods
                      0.000      0.000      0.000      0.000          2/12961     Enumerable#grep
                      0.000      0.000      0.000      0.000          2/12961     Enumerable#each_entry
                      0.000      0.000      0.000      0.000          2/12961     ActiveRecord::QueryMethods#validate_order_args
                      0.000      0.000      0.000      0.000          1/12961     ActiveRecord::Relation#exec_queries
                      0.000      0.000      0.000      0.000          2/12961     ActiveRecord::Relation::Merger#merge
                      0.000      0.000      0.000      0.000          2/12961     ActiveSupport::Notifications::Fanout#start
                      0.000      0.000      0.000      0.000          2/12961     Enumerable#each_with_index
                      0.000      0.000      0.000      0.000          2/12961     ActiveRecord::Relation::Merger#merge_clauses
                      0.001      0.000      0.000      0.001          2/12961     ActiveSupport::Notifications::Fanout#finish
                      0.009      0.000      0.000      0.008          2/12961     Enumerable#inject
                      0.023      0.001      0.000      0.022          1/12961     ActiveRecord::Result#each
                      0.064      0.025      0.000      0.039      12936/12961     Enumerable#find
                      1.039      0.002      0.000      1.037          2/12961     <Class::Bullet::Detector::CounterCache>#add_possible_objects
                      1.121      0.002      0.000      1.118          2/12961     <Class::Bullet::Detector::NPlusOneQuery>#add_possible_objects
  48.45%   0.46%      2.234      0.021      0.000      2.213            12961    *Array#each
                      2.126      0.067      0.000      2.059        2156/4312     Object#bullet_key
                      0.025      0.025      0.000      0.000      25872/25872     Symbol#=~
                      0.025      0.004      0.000      0.021        2156/2156     Bullet::Registry::Object#add
                      0.022      0.001      0.000      0.021          539/539     ActiveRecord::Persistence::ClassMethods#instantiate
                      0.014      0.014      0.000      0.000      25872/25872     <Class::ActiveRecord::DynamicMatchers::Method>#pattern
                      0.008      0.000      0.000      0.008              1/1     BasicObject#instance_exec
                      0.002      0.001      0.000      0.001        1078/1078     <Class::Bullet::Detector::CounterCache>#possible_objects
                      0.002      0.001      0.000      0.001        1078/1078     <Class::Bullet::Detector::Association>#possible_objects
                      0.001      0.000      0.000      0.001              2/2     ActiveSupport::Notifications::Fanout::Subscribers::Evented#finish
                      0.000      0.000      0.000      0.000              1/1     ActiveRecord::SpawnMethods#merge
                      0.000      0.000      0.000      0.000              4/4     ActiveRecord::Relation::WhereClause#merge
                      0.000      0.000      0.000      0.000            13/13     ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#get_oid_type
                      0.000      0.000      0.000      0.000              1/1     Arel::Visitors::ToSql#visit_Arel_Nodes_SelectCore
                      0.000      0.000      0.000      0.000              2/2     ActiveSupport::Notifications::Fanout::Subscribers::Evented#start
                      0.000      0.000      0.000      0.000              1/4     Arel::Visitors::Reduce#visit
                      0.000      0.000      0.000      0.000              1/1     ActiveSupport::Notifications::Fanout::Subscribers::Timed#finish
                      0.000      0.000      0.000      0.000              4/6     ActiveRecord::QueryMethods#where_clause
                      0.000      0.000      0.000      0.000              1/1     ActiveSupport::Notifications::Fanout::Subscribers::Timed#start
                      0.000      0.000      0.000      0.000              4/6     ActiveRecord::QueryMethods#having_clause
                      0.000      0.000      0.000      0.000              2/2     ActiveRecord::QueryMethods#having_clause=
                      0.000      0.000      0.000      0.000              2/2     ActiveRecord::QueryMethods#where_clause=
                      0.000      0.000      0.000      0.000            24/34     NilClass#nil?
                      0.000      0.000      0.000      0.000              2/5     ActiveRecord::QueryMethods#from_clause=
                      0.000      0.000      0.000      0.000            13/13     PG::Result#ftype
                      0.000      0.000      0.000      0.000              2/2     ActiveRecord::Relation::FromClause#merge
                      0.000      0.000      0.000      0.000            13/13     PG::Result#fmod
                      0.000      0.000      0.000      0.000             4/12     ActiveRecord::QueryMethods#from_clause
                      0.000      0.000      0.000      0.000          1/12952     Kernel#respond_to?
--------------------------------------------------------------------------------
                      0.057      0.005      0.000      0.051        2156/6468     Array#map
                      0.172      0.022      0.000      0.151        4312/6468     Object#bullet_key
   4.97%   0.59%      0.229      0.027      0.000      0.202             6468     Object#primary_key_value
                      0.115      0.021      0.000      0.093      12936/12936     ActiveRecord::DynamicMatchers#respond_to?
                      0.071      0.012      0.000      0.059        6468/6468     ActiveRecord::AttributeMethods::PrimaryKey#id
                      0.009      0.009      0.000      0.000      12936/19406     ActiveRecord::AttributeMethods::PrimaryKey::ClassMethods#primary_key
                      0.007      0.007      0.000      0.000      25872/38827     Kernel#class
--------------------------------------------------------------------------------
                      0.115      0.021      0.000      0.093      12936/12936     Object#primary_key_value
   2.49%   0.46%      0.115      0.021      0.000      0.093            12936     ActiveRecord::DynamicMatchers#respond_to?
                      0.089      0.013      0.000      0.076      12936/12936     <Class::ActiveRecord::DynamicMatchers::Method>#match
                      0.005      0.005      0.000      0.000      12936/12952     Kernel#respond_to?
--------------------------------------------------------------------------------
                      0.089      0.013      0.000      0.076      12936/12936     ActiveRecord::DynamicMatchers#respond_to?
   1.92%   0.28%      0.089      0.013      0.000      0.076            12936     <Class::ActiveRecord::DynamicMatchers::Method>#match
                      0.076      0.012      0.000      0.064      12936/12936     Enumerable#find
--------------------------------------------------------------------------------
                      0.076      0.012      0.000      0.064      12936/12936     <Class::ActiveRecord::DynamicMatchers::Method>#match
   1.64%   0.25%      0.076      0.012      0.000      0.064            12936     Enumerable#find
                      0.064      0.025      0.000      0.039      12936/12961     Array#each
--------------------------------------------------------------------------------
                      0.071      0.012      0.000      0.059        6468/6468     Object#primary_key_value
   1.53%   0.26%      0.071      0.012      0.000      0.059             6468     ActiveRecord::AttributeMethods::PrimaryKey#id
                      0.044      0.008      0.000      0.036        6468/6468     ActiveRecord::AttributeMethods::Read#_read_attribute
                      0.009      0.006      0.000      0.003        6468/6468     ActiveRecord::Transactions#sync_with_transaction_state
                      0.004      0.004      0.000      0.000       6468/19406     ActiveRecord::AttributeMethods::PrimaryKey::ClassMethods#primary_key
                      0.002      0.002      0.000      0.000       6468/38827     Kernel#class
--------------------------------------------------------------------------------

anykeyh avatar Apr 05 '17 06:04 anykeyh