Class: RailsVitals::Analyzers::AssociationMapper
- Inherits:
-
Object
- Object
- RailsVitals::Analyzers::AssociationMapper
- Defined in:
- lib/rails_vitals/analyzers/association_mapper.rb
Defined Under Namespace
Classes: AssociationEdge, ModelNode
Class Method Summary collapse
-
.assign_positions(nodes) ⇒ Object
Assign x/y positions by depth layer.
-
.association_depth(model, all_models) ⇒ Object
Depth = how many belongs_to hops from root.
- .build(store) ⇒ Object
- .build_edges(model, n1_tables) ⇒ Object
- .discover_models ⇒ Object
- .queries_for_model(model, records) ⇒ Object
Class Method Details
.assign_positions(nodes) ⇒ Object
Assign x/y positions by depth layer
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/rails_vitals/analyzers/association_mapper.rb', line 101 def self.assign_positions(nodes) by_depth = nodes.group_by(&:depth) canvas_w = 900 canvas_h = 120 + (by_depth.keys.max || 0) * 160 by_depth.each do |depth, layer_nodes| count = layer_nodes.size x_step = canvas_w / (count + 1) layer_nodes.each_with_index do |node, i| node.position = { x: x_step * (i + 1), y: 60 + depth * 160 } end end [ nodes, canvas_h ] end |
.association_depth(model, all_models) ⇒ Object
Depth = how many belongs_to hops from root
53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/rails_vitals/analyzers/association_mapper.rb', line 53 def self.association_depth(model, all_models) belongs_to_targets = model.reflect_on_all_associations(:belongs_to) .map { |r| r.klass rescue nil } .compact return 0 if belongs_to_targets.empty? belongs_to_targets.map { |target| target == model ? 0 : association_depth(target, all_models) + 1 }.min rescue 0 end |
.build(store) ⇒ Object
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/rails_vitals/analyzers/association_mapper.rb', line 17 def self.build(store) records = store.all models = discover_models n1_data = NPlusOneAggregator.aggregate(records) n1_tables = n1_data.map { |p| p[:table] }.compact.uniq nodes = models.map do |model| queries = queries_for_model(model, records) avg_time = queries.empty? ? 0 : (queries.sum { |q| q[:duration_ms] } / queries.size).round(2) ModelNode.new( name: model.name, table: model.table_name, depth: association_depth(model, models), position: nil, associations: build_edges(model, n1_tables), query_count: queries.size, avg_query_time_ms: avg_time, has_n1: n1_tables.include?(model.table_name), n1_patterns: n1_data.select { |p| p[:table] == model.table_name } ) end nodes, canvas_h = assign_positions(nodes) [ nodes, canvas_h ] end |
.build_edges(model, n1_tables) ⇒ Object
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/rails_vitals/analyzers/association_mapper.rb', line 67 def self.build_edges(model, n1_tables) model.reflect_on_all_associations.map do |assoc| target = assoc.klass rescue next fk = assoc.foreign_key.to_s table = assoc.macro == :belongs_to ? model.table_name : target.table_name indexed = begin ActiveRecord::Base.connection .indexes(table) .any? { |i| i.columns.first == fk } rescue false end AssociationEdge.new( from_model: model.name, to_model: target.name, macro: assoc.macro, foreign_key: fk, indexed: indexed, has_n1: n1_tables.include?(target.table_name) ) end.compact end |
.discover_models ⇒ Object
44 45 46 47 48 49 50 |
# File 'lib/rails_vitals/analyzers/association_mapper.rb', line 44 def self.discover_models ActiveRecord::Base.descendants .reject(&:abstract_class?) .reject { |m| m.name&.start_with?("RailsVitals") } .select { |m| m.table_exists? rescue false } .sort_by(&:name) end |
.queries_for_model(model, records) ⇒ Object
92 93 94 95 96 97 98 |
# File 'lib/rails_vitals/analyzers/association_mapper.rb', line 92 def self.queries_for_model(model, records) records.flat_map { |r| r.queries } .select { |q| q[:sql].match?(/FROM\s+"?#{model.table_name}"?/i) || q[:sql].match?(/UPDATE\s+"?#{model.table_name}"?/i) } end |