awesome_explain 0.3.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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