awesome_explain 0.3.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/mongodb.yml +53 -0
  3. data/.github/workflows/postgres.yml +56 -0
  4. data/.gitignore +11 -0
  5. data/Appraisals +11 -0
  6. data/Gemfile.lock +209 -49
  7. data/LICENSE.txt +4 -20
  8. data/README.md +155 -7
  9. data/Rakefile +35 -1
  10. data/app/models/awesome_explain/application_record.rb +5 -0
  11. data/app/models/awesome_explain/controller.rb +20 -0
  12. data/app/models/awesome_explain/delayed_job.rb +7 -0
  13. data/app/models/awesome_explain/explain.rb +23 -0
  14. data/app/models/awesome_explain/log.rb +7 -0
  15. data/app/models/awesome_explain/pg_dml_stat.rb +4 -0
  16. data/app/models/awesome_explain/pg_seq_scan.rb +4 -0
  17. data/app/models/awesome_explain/plan_node.rb +52 -0
  18. data/app/models/awesome_explain/plan_tree.rb +66 -0
  19. data/app/models/awesome_explain/sidekiq_worker.rb +7 -0
  20. data/app/models/awesome_explain/sql_explain.rb +14 -0
  21. data/app/models/awesome_explain/sql_plan_node.rb +73 -0
  22. data/app/models/awesome_explain/sql_plan_stats.rb +34 -0
  23. data/app/models/awesome_explain/sql_plan_tree.rb +133 -0
  24. data/app/models/awesome_explain/sql_query.rb +7 -0
  25. data/app/models/awesome_explain/stacktrace.rb +11 -0
  26. data/awesome_explain.gemspec +16 -5
  27. data/bin/rails +14 -0
  28. data/data/mongodb/customers.bson +0 -0
  29. data/data/mongodb/customers.metadata.json +1 -0
  30. data/data/mongodb/line_items.bson +0 -0
  31. data/data/mongodb/line_items.metadata.json +1 -0
  32. data/data/mongodb/orders.bson +0 -0
  33. data/data/mongodb/orders.metadata.json +1 -0
  34. data/data/mongodb/products.bson +0 -0
  35. data/data/mongodb/products.metadata.json +1 -0
  36. data/data/postgresql/dvdrental.tar +0 -0
  37. data/db/migrate/20200507214801_stacktraces.rb +12 -0
  38. data/db/migrate/20200507214949_controllers.rb +16 -0
  39. data/db/migrate/20200507215205_logs.rb +22 -0
  40. data/db/migrate/20200507215243_explains.rb +27 -0
  41. data/gemfiles/rails_4.gemfile +7 -0
  42. data/gemfiles/rails_4.gemfile.lock +208 -0
  43. data/gemfiles/rails_5.gemfile +7 -0
  44. data/gemfiles/rails_5.gemfile.lock +209 -0
  45. data/gemfiles/rails_6.gemfile +7 -0
  46. data/gemfiles/rails_6.gemfile.lock +233 -0
  47. data/images/universe.png +0 -0
  48. data/lib/awesome_explain.rb +79 -2
  49. data/lib/awesome_explain/config.rb +196 -0
  50. data/lib/awesome_explain/engine.rb +5 -0
  51. data/lib/awesome_explain/insights/active_record_insights.rb +137 -0
  52. data/lib/awesome_explain/insights/base.rb +18 -0
  53. data/lib/awesome_explain/insights/mongoid_insights.rb +44 -0
  54. data/lib/awesome_explain/insights/sql_plans_insights.rb +64 -0
  55. data/lib/awesome_explain/kernel.rb +17 -0
  56. data/lib/awesome_explain/mongodb/base.rb +4 -0
  57. data/lib/awesome_explain/mongodb/command_start.rb +84 -0
  58. data/lib/awesome_explain/mongodb/command_success.rb +58 -0
  59. data/lib/awesome_explain/mongodb/formatter.rb +62 -0
  60. data/lib/awesome_explain/mongodb/helpers.rb +71 -0
  61. data/lib/awesome_explain/queue/command.rb +17 -0
  62. data/lib/awesome_explain/queue/simple_queue.rb +88 -0
  63. data/lib/awesome_explain/renderers/active_record.rb +114 -0
  64. data/lib/awesome_explain/renderers/base.rb +2 -0
  65. data/lib/awesome_explain/renderers/mongoid.rb +20 -33
  66. data/lib/awesome_explain/sidekiq_middleware.rb +17 -0
  67. data/lib/awesome_explain/stats/postgresql.rb +54 -0
  68. data/lib/awesome_explain/subscribers/active_record_passive_subscriber.rb +82 -0
  69. data/lib/awesome_explain/subscribers/active_record_subscriber.rb +187 -0
  70. data/lib/awesome_explain/subscribers/base.rb +3 -0
  71. data/lib/awesome_explain/subscribers/command_subscriber.rb +53 -0
  72. data/lib/awesome_explain/tasks/db.rb +325 -0
  73. data/lib/awesome_explain/utils/color.rb +16 -0
  74. data/lib/awesome_explain/version.rb +1 -1
  75. data/lib/tasks/ae.rake +28 -0
  76. data/lib/tasks/awesome_explain_tasks.rake +4 -0
  77. metadata +242 -25
  78. data/.travis.yml +0 -19
@@ -0,0 +1,88 @@
1
+ # module AwesomeExplain::Queue
2
+ # class SimpleQueue
3
+ # def initialize
4
+ # @elems = []
5
+ # @mutex = Mutex.new
6
+ # @cond_var = ConditionVariable.new
7
+ # end
8
+
9
+ # def <<(elem)
10
+ # @mutex.synchronize do
11
+ # @elems << elem
12
+ # @cond_var.signal
13
+ # end
14
+ # end
15
+
16
+ # def pop(blocking = true, timeout = nil)
17
+ # @mutex.synchronize do
18
+ # if blocking
19
+ # if timeout.nil?
20
+ # while @elems.empty?
21
+ # @cond_var.wait(@mutex)
22
+ # end
23
+ # else
24
+ # timeout_time = Time.now.to_f + timeout
25
+ # while @elems.empty? && (remaining_time = timeout_time - Time.now.to_f) > 0
26
+ # @cond_var.wait(@mutex, remaining_time)
27
+ # end
28
+ # end
29
+ # end
30
+ # raise ThreadError, 'queue empty' if @elems.empty?
31
+ # sleep 1
32
+ # @elems.shift
33
+ # end
34
+ # end
35
+ # end
36
+ # end
37
+
38
+ module AwesomeExplain::Queue
39
+ class SimpleQueue
40
+ include Singleton
41
+
42
+ def initialize
43
+ @queue = Queue.new
44
+ # Thread.new do
45
+ # puts 'while true ==============================='
46
+ # command = @queue.pop(false)
47
+ # # command.run if command
48
+ # end
49
+ # @read_io, @write_io = IO.pipe
50
+ end
51
+
52
+ def <<(o)
53
+ # pop(false) until @queue.size < 2
54
+ if @queue.size >= 2
55
+ items = []
56
+ while @queue.size >= 2
57
+ items << @queue.pop
58
+ end
59
+ Thread.new { sleep 2; puts "Poped #{items}"; puts items.inspect }
60
+ end
61
+ puts "Adding to queue @@@@@@@@@@@@@"
62
+ @queue << o
63
+ # @write_io << '.'
64
+ self
65
+ end
66
+
67
+ def pop(nonblock=false)
68
+ # return unless @queue.size >= 5
69
+ # @queue.size.times.each do
70
+
71
+ # end
72
+ puts Thread.current.inspect
73
+ o = @queue.pop(nonblock)
74
+ # @read_io.read(1)
75
+ puts "<------ Element poped ------>"
76
+ puts o
77
+ o
78
+ end
79
+
80
+ def size
81
+ @queue.size
82
+ end
83
+
84
+ def to_io
85
+ @read_io
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,114 @@
1
+ module AwesomeExplain
2
+ module Renderers
3
+ class ActiveRecord
4
+ attr_reader :result, :query, :sql_explain
5
+
6
+ def initialize(query, result = nil)
7
+ @query = query
8
+ @result = result || explain_query
9
+ end
10
+
11
+ def explain_query
12
+ explain = AwesomeExplain::Config.instance.connection.raw_connection.exec(
13
+ "EXPLAIN (ANALYZE true, COSTS true, FORMAT json) #{query.to_sql}"
14
+ )
15
+ explain = explain.map { |h| h.values.first }.join("\n")
16
+
17
+ @sql_explain = SqlExplain.new(explain_output: explain)
18
+ end
19
+
20
+ def print
21
+ table = Terminal::Table.new do |t|
22
+ general_stats_section t
23
+ table_stats_section t
24
+ node_types_section t
25
+ index_stats_section t
26
+ end
27
+ puts table
28
+ end
29
+
30
+ def plan_stats
31
+ @plan_stats ||= @sql_explain.tree.plan_stats
32
+ end
33
+
34
+ def table_stats
35
+ @table_stats ||= plan_stats.table_stats
36
+ end
37
+
38
+ def node_type_stats
39
+ @node_type_stats ||= plan_stats.node_type_stats
40
+ end
41
+
42
+ def index_stats
43
+ @index_stats ||= plan_stats.index_stats
44
+ end
45
+
46
+ def seq_scans_row
47
+ color = plan_stats.seq_scans.positive? ? :cyan : :green
48
+
49
+ seq_scans_label = AwesomeExplain::Utils::Color.fg_color(
50
+ color,
51
+ 'Seq Scans'
52
+ )
53
+
54
+ seq_scans_val = AwesomeExplain::Utils::Color.fg_color(
55
+ color,
56
+ plan_stats.seq_scans.to_s
57
+ )
58
+
59
+ [seq_scans_label, seq_scans_val]
60
+ end
61
+
62
+ def general_stats_section(t)
63
+ title = AwesomeExplain::Utils::Color.fg_color :yellow, 'General Stats'
64
+ t << [{ value: title, alignment: :center, colspan: 2}]
65
+ t << :separator
66
+ t << ['Table', 'Count']
67
+ t << :separator
68
+ t << ['Total Rows Planned', plan_stats.total_rows_planned]
69
+ t << ['Total Rows', plan_stats.total_rows]
70
+ t << ['Total Loops', plan_stats.total_loops]
71
+ t << seq_scans_row
72
+ t << ['Indexes Used', plan_stats.index_stats.size]
73
+ end
74
+
75
+ def table_stats_section(t)
76
+ title = AwesomeExplain::Utils::Color.fg_color :yellow, 'Table Stats'
77
+ t << :separator
78
+ t << [{ value: title, alignment: :center, colspan: 2}]
79
+ t << :separator
80
+ t << ['Table', 'Count']
81
+ t << :separator
82
+ table_stats.each do |table_name, stats|
83
+ t << [table_name, stats.dig(:count)]
84
+ end
85
+ end
86
+
87
+ def node_types_section(t)
88
+ title = AwesomeExplain::Utils::Color.fg_color :yellow, 'Node Type Stats'
89
+ t << :separator
90
+ t << [{ value: title, alignment: :center, colspan: 2}]
91
+ t << :separator
92
+ t << ['Node Type', 'Count']
93
+ t << :separator
94
+ node_type_stats.each do |node_type, stats|
95
+ t << [node_type, stats.dig(:count)]
96
+ end
97
+ end
98
+
99
+ def index_stats_section(t)
100
+ if index_stats.size.positive?
101
+ title = AwesomeExplain::Utils::Color.fg_color :yellow, 'Index Stats'
102
+ t << :separator
103
+ t << [{ value: title, alignment: :center, colspan: 2}]
104
+ t << :separator
105
+ t << ['Index Name', 'Count']
106
+ t << :separator
107
+ index_stats.each do |index, stats|
108
+ t << [index, stats.dig(:count)]
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,2 @@
1
+ require 'awesome_explain/renderers/mongoid'
2
+ require 'awesome_explain/renderers/active_record'
@@ -3,41 +3,25 @@ module AwesomeExplain
3
3
  class Mongoid
4
4
  attr_reader :result, :query
5
5
 
6
- COLOR_ESCAPES = {
7
- none: 0, bright: 1, black: 30,
8
- red: 31, green: 32, yellow: 33,
9
- blue: 34, magenta: 35, cyan: 36,
10
- white: 37, default: 39
11
- }
12
-
13
- def initialize(query)
6
+ def initialize(query, result = nil)
14
7
  @query = query
8
+ @result = result || query.explain
15
9
  end
16
10
 
17
11
  def print
18
- @result = query.explain
19
-
20
- print_general_info
21
- end
22
-
23
- # Text foreground color
24
- def fg_color(clr, text = nil)
25
- "\x1B[" + (COLOR_ESCAPES[clr] || 0).to_s + 'm' + (text ? text + "\x1B[0m" : '')
26
- end
27
-
28
- # Text background color
29
- def bg_color(clr, text = nil)
30
- "\x1B[" + ((COLOR_ESCAPES[clr] || 0) + 10).to_s + 'm' + (text ? text + "\x1B[0m" : '')
12
+ ap result, indent: -2
13
+ puts
14
+ puts explain_summary
15
+ puts
31
16
  end
32
17
 
33
- def print_general_info
34
- ap result, indent: -2
18
+ def explain_summary
35
19
  table = Terminal::Table.new do |t|
36
20
  winning_plan_label = 'Winning Plan'
37
21
  plan_data = winning_plan_data
38
22
  winning_plan_str = plan_data[0]
39
23
  used_indexes = plan_data[1]
40
- winning_plan_label = fg_color :red, winning_plan_label if winning_plan_str =~ /COLLSCAN/
24
+ winning_plan_label = AwesomeExplain::Utils::Color.fg_color :red, winning_plan_label if winning_plan_str =~ /COLLSCAN/
41
25
  t << [winning_plan_label, winning_plan_str]
42
26
  t << :separator
43
27
  t << ['Used Indexes', used_indexes.join(', ')]
@@ -59,8 +43,8 @@ module AwesomeExplain
59
43
  exec_label_ms = 'Execution time(ms)'
60
44
 
61
45
  if exec_time > 10
62
- exec_label = fg_color :red, exec_label
63
- exec_label_ms = fg_color :red, exec_label_ms
46
+ exec_label = AwesomeExplain::Utils::Color.fg_color :red, exec_label
47
+ exec_label_ms = AwesomeExplain::Utils::Color.fg_color :red, exec_label_ms
64
48
  end
65
49
  t << [exec_label_ms, exec_time_ms]
66
50
  t << :separator
@@ -68,16 +52,18 @@ module AwesomeExplain
68
52
  end
69
53
  end
70
54
 
71
- puts
72
- puts table
73
- puts
55
+ table
56
+ end
57
+
58
+ def parsed_query
59
+ root.dig('parsedQuery')
74
60
  end
75
61
 
76
62
  def winning_plan_data
77
63
  used_indexes = []
78
64
  plan = winning_plan
79
65
  plan_str = stage_label_and_stats(plan)
80
- plan_str = dig_input_stages(plan.dig('inputStage'), plan_str, used_indexes) if plan['inputStage']
66
+ plan_str = dig_input_stages(plan.dig('inputStage'), plan_str, used_indexes) if plan&.dig('inputStage')
81
67
 
82
68
  [plan_str, used_indexes]
83
69
  end
@@ -87,15 +73,15 @@ module AwesomeExplain
87
73
  end
88
74
 
89
75
  def winning_plan
90
- root.dig('executionStats', 'executionStages') || root.dig('queryPlanner', 'winningPlan')
76
+ root.dig('executionStats', 'executionStages') || root.dig('queryPlanner', 'winningPlan') || root['stages'].first['$cursor'].dig('queryPlanner', 'winningPlan') || {}
91
77
  end
92
78
 
93
79
  def rejected_plans
94
- root.dig('queryPlanner', 'rejectedPlans')
80
+ root.dig('queryPlanner', 'rejectedPlans') || root['stages']&.first['$cursor'].dig('queryPlanner', 'rejectedPlans') || {}
95
81
  end
96
82
 
97
83
  def execution_stats
98
- root.dig('executionStats')
84
+ root.dig('executionStats') || root['stages']&.first&.dig('$cursor')&.dig('executionStats') || {}
99
85
  end
100
86
 
101
87
  def dig_input_stages(stage, str, used_indexes, input_stages = false)
@@ -123,6 +109,7 @@ module AwesomeExplain
123
109
  end
124
110
 
125
111
  def stage_label_and_stats(stage)
112
+ return unless stage.present?
126
113
  str = "#{stage.dig('stage')} ("
127
114
  str += "#{stage.dig('docsExamined')} / " if stage.dig('docsExamined').present?
128
115
  str += stage.dig('nReturned').to_s if stage.dig('nReturned').present?
@@ -0,0 +1,17 @@
1
+ module AwesomeExplain
2
+ class SidekiqMiddleware
3
+ # def call(worker_class, job, queue, redis_pool)
4
+ def call(worker_class, job, queue)
5
+ begin
6
+ Thread.current[:sidekiq_worker_class] = worker_class.class.name
7
+ Thread.current[:sidekiq_job] = job
8
+ Thread.current[:sidekiq_queue] = queue
9
+ Thread.current['ae_source'] = 'sidekiq'
10
+ rescue => exception
11
+ # Do nothing
12
+ end
13
+
14
+ yield
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,54 @@
1
+ module AwesomeExplain
2
+ module Stats
3
+ class PostgreSQL
4
+ def self.upsert!
5
+ upsert_seq_scans!
6
+ upsert_dml_stats!
7
+ end
8
+
9
+ def self.upsert_seq_scans!
10
+ result = ActiveRecord::Base.connection.execute(seq_scans_sql).to_a
11
+ result.each do |row|
12
+ row = OpenStruct.new(row)
13
+ pg_seq_scan = AwesomeExplain::PgSeqScan.find_or_create_by({
14
+ schema_name: row.schema_name,
15
+ table_name: row.table_name
16
+ })
17
+
18
+ pg_seq_scan.update({
19
+ seq_scan: row.seq_scan,
20
+ seq_tup_read: row.seq_tup_read,
21
+ idx_scan: row.idx_scan,
22
+ idx_tup_fetch: row.idx_tup_fetch,
23
+ size_bytes: row.size_bytes
24
+ })
25
+ end
26
+ end
27
+
28
+ def self.upsert_dml_stats!
29
+ result = ActiveRecord::Base.connection.execute(dml_stats_sql).to_a
30
+ result.each do |row|
31
+ row = OpenStruct.new(row)
32
+ pg_dml_stat = AwesomeExplain::PgDmlStat.find_or_create_by({
33
+ schema_name: row.schema_name,
34
+ table_name: row.table_name
35
+ })
36
+
37
+ pg_dml_stat.update({
38
+ total_inserts: row.n_tup_ins,
39
+ total_updates: row.n_tup_upd,
40
+ total_deletes: row.n_tup_del,
41
+ })
42
+ end
43
+ end
44
+
45
+ def self.seq_scans_sql
46
+ "select schemaname as schema_name, relname as table_name, seq_scan, seq_tup_read, idx_scan, idx_tup_fetch, pg_relation_size(schemaname::text || '.'::text || relname::text) as size_bytes from pg_stat_user_tables"
47
+ end
48
+
49
+ def self.dml_stats_sql
50
+ "SELECT schemaname as schema_name, relname as table_name, n_tup_ins, n_tup_upd, n_tup_del FROM pg_stat_user_tables"
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,82 @@
1
+ module AwesomeExplain::Subscribers
2
+ class ActiveRecordPassiveSubscriber < ActiveSupport::LogSubscriber
3
+ def sql(event)
4
+ if track_sql(event)
5
+ sql = event.payload[:sql]
6
+ begin
7
+ table_name_and_schema = extract_table_name_and_schema(sql)
8
+ table_name = table_name_and_schema.first
9
+ schema_name = table_name_and_schema.last
10
+ request_id = event.payload[:connection_id]
11
+ binds = event.payload[:binds]
12
+ cached = event.payload[:name] == 'CACHE'
13
+ operation = extract_sql_operation(sql)
14
+ name = event.payload[:name]
15
+
16
+ connection_id = event.payload[:connection_id]
17
+ connection = ::AwesomeExplain::Config.instance.connection
18
+ explain_uuid = SecureRandom.uuid
19
+ explain = connection.raw_connection.exec("EXPLAIN (ANALYZE true, COSTS true, FORMAT json) #{sql}")
20
+ explain = explain.map { |h| h.values.first }.join("\n")
21
+ explain = ::AwesomeExplain::SqlExplain.new(explain_output: explain)
22
+ AwesomeExplain::Insights::SqlPlansInsights.add explain.tree.plan_stats
23
+ AwesomeExplain::Insights::SqlPlansInsights.add_query sql
24
+ rescue => exception
25
+ logger.warn sql
26
+ logger.warn exception.to_s
27
+ logger.warn exception.backtrace[0..5]
28
+ end
29
+ end
30
+ end
31
+
32
+ def track_sql(event)
33
+ return false unless Thread.current['ae_analyze']
34
+ return false if event.payload[:connection].class.name == 'ActiveRecord::ConnectionAdapters::SQLite3Adapter'
35
+ sql = event.payload[:sql]
36
+ !sql.match(/EXPLAIN|SAVEPOINT|nextval|CREATE|BEGIN|COMMIT|ROLLBACK|begin|commit|rollback|ar_|sql_|pg_|explain|logs|controllers|stacktraces|schema_migrations|delayed_jobs/) &&
37
+ sql.strip == sql &&
38
+ event.payload[:name] != 'SCHEMA'
39
+ end
40
+
41
+ def ddm_query?(sql)
42
+ matched = sql.match(/INSERT|DELETE|UPDATE/)
43
+ matched.present? && matched[0].present?
44
+ end
45
+
46
+ def resolve_source_name
47
+ Thread.current['ae_source'] || DEFAULT_SOURCE_NAME
48
+ end
49
+
50
+ def db_logging_enbled?
51
+ # return true if Thread.current['ae_analyze']
52
+ return false if Rails.const_defined?('Console')
53
+ true
54
+ end
55
+
56
+ def extract_sql_operation(sql)
57
+ sql.match(/SELECT|INSERT|DELETE|UPDATE/)[0]
58
+ end
59
+
60
+ def extract_table_name_and_schema(sql)
61
+ matched = sql.match(/FROM\s+(\"\w+\")\.?(\"\w+\")?/)
62
+ return reduce_table_and_schema(matched) if matched && matched[1].present?
63
+
64
+ matched = sql.match(/INSERT INTO\s+(\"\w+\")\.?(\"\w+\")?/)
65
+ return reduce_table_and_schema(matched) if matched && matched[1].present?
66
+
67
+ matched = sql.match(/UPDATE\s+(\"\w+\")\.?(\"\w+\")?/)
68
+ return reduce_table_and_schema(matched) if matched && matched[1].present?
69
+ end
70
+
71
+ def reduce_table_and_schema(matched)
72
+ if matched[1].present?
73
+ table_name = matched[2].nil? ? matched[1] : matched[2]
74
+ table_name = table_name.gsub(/\"/, '')
75
+ schema_name = matched[2].nil? ? 'public' : matched[1]
76
+ schema_name = schema_name.gsub(/\"/, '')
77
+
78
+ return [table_name, schema_name]
79
+ end
80
+ end
81
+ end
82
+ end