active-record-profiler 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/CHANGELOG.md +5 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +150 -0
  7. data/Rakefile +14 -0
  8. data/active-record-profiler.gemspec +30 -0
  9. data/lib/active-record-profiler.rb +13 -0
  10. data/lib/active-record-profiler/collector.rb +276 -0
  11. data/lib/active-record-profiler/log_subscriber.rb +30 -0
  12. data/lib/active-record-profiler/logger.rb +67 -0
  13. data/lib/active-record-profiler/profiler_view_helper.rb +200 -0
  14. data/lib/active-record-profiler/railtie.rb +18 -0
  15. data/lib/active-record-profiler/tasks.rake +41 -0
  16. data/lib/active-record-profiler/version.rb +3 -0
  17. data/test/active_record_profiler_test.rb +29 -0
  18. data/test/database.yml +4 -0
  19. data/test/dummy/README.rdoc +28 -0
  20. data/test/dummy/Rakefile +6 -0
  21. data/test/dummy/app/assets/images/.keep +0 -0
  22. data/test/dummy/app/assets/javascripts/application.js +13 -0
  23. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  24. data/test/dummy/app/controllers/application_controller.rb +5 -0
  25. data/test/dummy/app/controllers/concerns/.keep +0 -0
  26. data/test/dummy/app/controllers/profiler_controller.rb +5 -0
  27. data/test/dummy/app/helpers/application_helper.rb +2 -0
  28. data/test/dummy/app/mailers/.keep +0 -0
  29. data/test/dummy/app/models/.keep +0 -0
  30. data/test/dummy/app/models/concerns/.keep +0 -0
  31. data/test/dummy/app/models/widget.rb +2 -0
  32. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  33. data/test/dummy/app/views/profiler/index.html.erb +4 -0
  34. data/test/dummy/bin/bundle +3 -0
  35. data/test/dummy/bin/rails +4 -0
  36. data/test/dummy/bin/rake +4 -0
  37. data/test/dummy/bin/setup +29 -0
  38. data/test/dummy/config.ru +4 -0
  39. data/test/dummy/config/application.rb +25 -0
  40. data/test/dummy/config/boot.rb +5 -0
  41. data/test/dummy/config/database.yml +25 -0
  42. data/test/dummy/config/environment.rb +5 -0
  43. data/test/dummy/config/environments/development.rb +41 -0
  44. data/test/dummy/config/environments/production.rb +79 -0
  45. data/test/dummy/config/environments/test.rb +42 -0
  46. data/test/dummy/config/initializers/active_record_profiler.rb +3 -0
  47. data/test/dummy/config/initializers/assets.rb +11 -0
  48. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  49. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  50. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  51. data/test/dummy/config/initializers/inflections.rb +16 -0
  52. data/test/dummy/config/initializers/mime_types.rb +4 -0
  53. data/test/dummy/config/initializers/session_store.rb +3 -0
  54. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  55. data/test/dummy/config/locales/en.yml +23 -0
  56. data/test/dummy/config/routes.rb +56 -0
  57. data/test/dummy/config/secrets.yml +22 -0
  58. data/test/dummy/db/migrate/20150109175941_create_widgets.rb +8 -0
  59. data/test/dummy/db/schema.rb +21 -0
  60. data/test/dummy/lib/assets/.keep +0 -0
  61. data/test/dummy/public/404.html +67 -0
  62. data/test/dummy/public/422.html +67 -0
  63. data/test/dummy/public/500.html +66 -0
  64. data/test/dummy/public/favicon.ico +0 -0
  65. data/test/dummy/test/fixtures/widgets.yml +9 -0
  66. data/test/dummy/test/models/widget_test.rb +7 -0
  67. data/test/schema.rb +7 -0
  68. data/test/test_helper.rb +24 -0
  69. metadata +237 -0
@@ -0,0 +1,30 @@
1
+ require 'active_record/log_subscriber'
2
+
3
+ module ActiveRecordProfiler
4
+ class LogSubscriber < ActiveRecord::LogSubscriber
5
+ def sql(event)
6
+ start_time = Time.now.to_f
7
+ payload = event.payload
8
+
9
+ duration = event.duration
10
+ sql_string = payload[:sql]
11
+
12
+ begin
13
+ collector = ActiveRecordProfiler::Collector.instance
14
+ loc = collector.call_location_name
15
+ collector.record_caller_info(loc, duration, sql_string.strip)
16
+
17
+ collector.record_self_info((Time.now.to_f - start_time), 'updating profiler stats') if ActiveRecordProfiler::Collector.profile_self?
18
+
19
+ start_time = Time.now.to_f
20
+ if collector.should_flush_stats?
21
+ collector.flush_query_sites_statistics
22
+ collector.record_self_info((Time.now.to_f - start_time), 'flushing profiler stats') if ActiveRecordProfiler::Collector.profile_self?
23
+ end
24
+ rescue Exception => e
25
+ Rails.logger.error("Caught exception in #{self.class}: #{e} at #{e.backtrace.first}")
26
+ end
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,67 @@
1
+ require 'logger'
2
+
3
+ module ActiveRecordProfiler
4
+ class Logger < SimpleDelegator
5
+
6
+ def add(severity, message = nil, progname = nil, &block)
7
+ return true if (severity || ::Logger::Severity::UNKNOWN) < self.level
8
+
9
+ if message.nil?
10
+ if block_given?
11
+ message = yield
12
+ else
13
+ message = progname
14
+ progname = self.progname
15
+ end
16
+ end
17
+
18
+ message = add_call_site_to_message(message)
19
+
20
+ super(severity, message, progname, &block)
21
+ end
22
+
23
+ # Define all of the basic logging methods so that they invoke our add()
24
+ # method rather than delegating to the delagatee's methods, which will then
25
+ # invoke the delegatee's add() method that does not add call-site
26
+ # information.
27
+ [:debug, :info, :warn, :error, :fatal, :unknown].each do |level|
28
+ define_method(level) do |progname = nil, &block|
29
+ add(
30
+ ::Logger::Severity.const_get(level.to_s.upcase),
31
+ nil,
32
+ progname,
33
+ &block
34
+ )
35
+ end
36
+ end
37
+
38
+ protected
39
+ def add_call_site_to_message(msg)
40
+ message = msg
41
+
42
+ if String === msg
43
+ match = /^\s*(?:\e\[\d+m)*(:?[^(]*)\(([0-9.]+)\s*([a-z]s(:?econds)?)\)(?:\e\[\d+m)*\s*(.*)/.match(msg)
44
+
45
+ if match
46
+ loc = collector.call_location_name
47
+ message = "#{msg} CALLED BY '#{formatted_location(loc)}'"
48
+ end
49
+ end
50
+
51
+ return message
52
+ end
53
+
54
+ def collector
55
+ ActiveRecordProfiler::Collector.instance
56
+ end
57
+
58
+ def formatted_location(loc)
59
+ if Rails.configuration.colorize_logging
60
+ "\e[4;32;1m#{loc}\e[0m"
61
+ else
62
+ loc
63
+ end
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,200 @@
1
+ module ActiveRecordProfiler
2
+ module ProfilerViewHelper
3
+ def profiler_date_filter_form(page_params, options = {})
4
+ page_params ||= {}
5
+ options ||= {}
6
+ date = options[:date] || page_params[:date] || Time.now.strftime(ActiveRecordProfiler::Collector::DATE_FORMAT)
7
+ sort_by = options[:sort] || page_params[:sort] || ActiveRecordProfiler::Collector::DURATION
8
+
9
+ content_tag(:form) do
10
+ [
11
+ hidden_field_tag(:sort, sort_by),
12
+ label_tag(:profiler_date, 'Filter by date-hour prefix (yyyy-mm-dd-hh):'),
13
+ text_field_tag(:date, date, {:size =>10, :id => :profiler_date}),
14
+ submit_tag('Go')
15
+ ].join.html_safe
16
+ end
17
+ end
18
+
19
+ def profiler_report(page_params, options = {})
20
+ date = options[:date] || page_params[:date] || Time.now.strftime(ActiveRecordProfiler::Collector::DATE_FORMAT)
21
+ sort_by = (options[:sort] || page_params[:sort] || ActiveRecordProfiler::Collector::DURATION).to_i
22
+ max_rows = (options[:max_rows] || page_params[:max_rows] || 100).to_i
23
+ collector = options[:collector] || ActiveRecordProfiler::Collector.new
24
+
25
+ options = options.reverse_merge(:table => 'profiler', :link_locations => false)
26
+
27
+ totals = collector.aggregate(:prefix => date)
28
+ top_locs = collector.sorted_locations(sort_by.to_i, max_rows)
29
+
30
+ rows = []
31
+
32
+ if top_locs.present?
33
+ top_item = totals[top_locs.first]
34
+ max_bar = sort_by == ActiveRecordProfiler::Collector::AVG_DURATION ? (top_item[0]/top_item[1]) : top_item[sort_by]
35
+
36
+ top_locs.each do |location|
37
+ rows << content_tag(
38
+ :tr,
39
+ profiler_report_cols(location, totals[location], sort_by, max_bar, options),
40
+ {:class => "#{cycle('oddRow', 'evenRow')} #{options[:row]}"}
41
+ )
42
+ end
43
+ end
44
+
45
+ content_tag(:table, {:class => options[:table]}) do
46
+ [
47
+ content_tag(:thead,
48
+ profiler_report_header(page_params, sort_by, max_rows, options)
49
+ ),
50
+ content_tag(:tbody) do
51
+ rows.join.html_safe
52
+ end
53
+ ].join.html_safe
54
+ end
55
+ end
56
+
57
+ def profiler_column_header_link(column_id, sort_by, page_params)
58
+ labels = {
59
+ ActiveRecordProfiler::Collector::DURATION => '<b>Total Duration (s)</b>',
60
+ ActiveRecordProfiler::Collector::COUNT => '<b>Count</b>',
61
+ ActiveRecordProfiler::Collector::AVG_DURATION => '<b>Avg. Duration (ms)</b>',
62
+ ActiveRecordProfiler::Collector::LONGEST => '<b>Max. Duration (ms)</b>',
63
+ }
64
+
65
+ link_to_unless(sort_by == column_id,
66
+ labels[column_id].html_safe,
67
+ page_params.merge(:sort => column_id)
68
+ )
69
+ end
70
+
71
+ def profiler_report_header(page_params, sort_by, max_rows, options)
72
+ column_styles = options[:column_styles] || {
73
+ :location => 'width:19%',
74
+ :total_duration => 'width:6%',
75
+ :count => 'width:6%',
76
+ :average_duration => 'width:6%',
77
+ :max_duration => 'width:6%',
78
+ :longest_sql => 'width:57%'
79
+ }
80
+
81
+ headers = []
82
+
83
+ headers << content_tag(:th, 'Location', {:style => column_styles[:location]})
84
+
85
+ headers << content_tag(
86
+ :th,
87
+ profiler_column_header_link(ActiveRecordProfiler::Collector::DURATION, sort_by, page_params),
88
+ {:style => column_styles[:total_duration]}
89
+ )
90
+
91
+ headers << content_tag(
92
+ :th,
93
+ profiler_column_header_link(ActiveRecordProfiler::Collector::COUNT, sort_by, page_params),
94
+ {:style => column_styles[:count]}
95
+ )
96
+
97
+ headers << content_tag(
98
+ :th,
99
+ profiler_column_header_link(ActiveRecordProfiler::Collector::AVG_DURATION, sort_by, page_params),
100
+ {:style => column_styles[:average_duration]}
101
+ )
102
+
103
+ headers << content_tag(
104
+ :th,
105
+ profiler_column_header_link(ActiveRecordProfiler::Collector::LONGEST, sort_by, page_params),
106
+ {:style => column_styles[:max_duration]}
107
+ )
108
+
109
+ headers << content_tag(:th, 'SQL for Max Duration', {:style => column_styles[:longest_sql]})
110
+
111
+ content_tag(:tr, {:class => options[:header]}) do
112
+ headers.join.html_safe
113
+ end
114
+ end
115
+
116
+ def profiler_report_cols(location, row_data, sort_by, max_bar, options)
117
+ columns = []
118
+
119
+ loc_parts = location.split(':')
120
+ breakble_loc = loc_parts[0].gsub('/', "&#8203;/")
121
+ this_bar = sort_by == ActiveRecordProfiler::Collector::AVG_DURATION ? (row_data[0]/row_data[1]) : row_data[sort_by]
122
+
123
+ columns << content_tag(:td, {:title=>loc_parts[2]}) do
124
+ [
125
+ link_to_if(
126
+ options[:link_locations],
127
+ "#{breakble_loc}: #{loc_parts[1]}".html_safe,
128
+ "javascript:showSourceFile('#{loc_parts[0]}', '#{loc_parts[1]}');"
129
+ ),
130
+ content_tag(:div, '', {:style=>"background-color:red; height:1.2em; width:#{100*this_bar/max_bar}%"})
131
+ ].join.html_safe
132
+ end
133
+
134
+ columns << content_tag(:td, number_with_precision(row_data[0], :precision => 3), {:class => "numeric"})
135
+ columns << content_tag(:td, number_with_delimiter(row_data[1]), {:class => "numeric"})
136
+ columns << content_tag(:td, number_with_precision(row_data[0]/row_data[1] * 1000, :precision => 3), {:class => "numeric"})
137
+ columns << content_tag(:td, number_with_precision(row_data[2] * 1000, :precision => 3), {:class => "numeric"})
138
+ columns << content_tag(:td, h(row_data[3]), {:class => "sql"})
139
+
140
+ columns.join('').html_safe
141
+ end
142
+
143
+ def profiler_report_local_path_form
144
+ content_tag(:form,
145
+ [
146
+ label_tag(:source_root, 'Local gistweb source path (for location links):'),
147
+ text_field_tag(:source_root, nil, {:size => 50, :id => :source_root})
148
+ ]
149
+ )
150
+ end
151
+
152
+ def profiler_local_path_link_formatters
153
+ @@profiler_local_path_link_formatters ||= {
154
+ :textmate => '"txmt://open/?url=file://" + root + "/" + file + "&line=" + line',
155
+ }
156
+ end
157
+
158
+ def profile_report_local_path_javascript(link_format = :textmate)
159
+ formatter = profiler_local_path_link_formatters[link_format]
160
+
161
+ javascript_tag(%Q[
162
+ var profiler_source_root = $('#source_root');
163
+
164
+ function setDBProfSourceRoot(value) {
165
+ var exdate = new Date();
166
+ var expireDays = 356;
167
+ exdate.setDate(exdate.getDate() + expireDays);
168
+ document.cookie = "db_prof_source_root=" + escape(value) + ";expires=" + exdate.toGMTString();
169
+ }
170
+
171
+ function getDBProfSourceRoot() {
172
+ if (document.cookie.length>0) {
173
+ var c_name = "db_prof_source_root";
174
+ c_start = document.cookie.indexOf(c_name + "=");
175
+ if (c_start != -1) {
176
+ c_start = c_start + c_name.length + 1;
177
+ c_end = document.cookie.indexOf(";", c_start);
178
+ if (c_end == -1) { c_end = document.cookie.length; }
179
+ var root = document.cookie.substring(c_start, c_end);
180
+ if (root != "") {
181
+ return unescape(root);
182
+ }
183
+ }
184
+ }
185
+ return "#{Rails.root}";
186
+ }
187
+ function showSourceFile(file, line){
188
+ var root = profiler_source_root.val();
189
+ if (root == "") { root = "#{Rails.root}"}
190
+ window.location = #{formatter};
191
+ }
192
+
193
+ profiler_source_root.val(getDBProfSourceRoot());
194
+ profiler_source_root.change(function(e){
195
+ setDBProfSourceRoot($(this).val());
196
+ });
197
+ ])
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,18 @@
1
+ require 'active-record-profiler'
2
+ require 'rails'
3
+
4
+ module ActiveRecordProfiler
5
+ class Railtie < Rails::Railtie
6
+ initializer "active_record_profiler.add_to_abstract_adapter" do |app|
7
+ # ActiveRecordProfiler::LogSubscriber.attach_to :active_record
8
+ ActionView::Base.send :include, ProfilerViewHelper
9
+
10
+ Collector.trim_root_path = "#{Rails.root.expand_path}/"
11
+ Collector.profile_dir = Rails.root.join("log", "profiler_data")
12
+ end
13
+
14
+ rake_tasks do
15
+ load 'active-record-profiler/tasks.rake'
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,41 @@
1
+ namespace "profiler" do
2
+
3
+ desc 'Aggregate profile data and display locations with the longest total durations'
4
+ task :aggregate => :environment do
5
+ top_n = ENV['max_lines'].present? ? ENV['max_lines'].to_i : 50
6
+ show_longest_sql = ENV['show_sql'] == 'true' ? true : false
7
+ prefix = ENV['prefix'].present? ? ENV['prefix'] : nil
8
+ compact = case ENV['compact']
9
+ when 'date'
10
+ :date
11
+ when 'hour'
12
+ :hour
13
+ else
14
+ nil
15
+ end
16
+
17
+ if compact && prefix.nil?
18
+ case compact
19
+ when :date
20
+ prefix = 1.day.ago.strftime(ActiveRecordProfiler::Collector::DATE_FORMAT)
21
+ when :hour
22
+ prefix = 1.hour.ago.strftime(ActiveRecordProfiler::Collector::DATE_FORMAT + ActiveRecordProfiler::HOUR_FORMAT)
23
+ end
24
+ end
25
+
26
+ collector = ActiveRecordProfiler::Collector.new
27
+ totals = collector.aggregate(:prefix => prefix, :compact => compact)
28
+ top_locs = collector.sorted_locations(ActiveRecordProfiler::Collector::DURATION, top_n)
29
+
30
+ top_locs.each do |loc|
31
+ data = show_longest_sql ? totals[loc] : totals[loc][0..-2]
32
+ puts "#{loc}: #{data.join(', ')}"
33
+ end
34
+ end
35
+
36
+ desc 'Clear out the profiler data diretory'
37
+ task :clear_data => :environment do
38
+ ActiveRecordProfiler::Collector.clear_data
39
+ end
40
+
41
+ end
@@ -0,0 +1,3 @@
1
+ module ActiveRecordProfiler
2
+ VERSION = "0.1"
3
+ end
@@ -0,0 +1,29 @@
1
+ require 'test_helper.rb'
2
+
3
+ class ActiveRecordProfilerTest < ActiveSupport::TestCase
4
+ def setup
5
+ @collector = ActiveRecordProfiler::Collector.instance
6
+ @test_log = StringIO.new
7
+ ActiveRecord::Base.logger = ActiveRecordProfiler::Logger.new(
8
+ ActiveSupport::Logger.new(@test_log))
9
+ end
10
+
11
+ def test_caller_location_appears_in_log
12
+ sql = 'SELECT 1 FROM widgets'
13
+ ActiveRecord::Base.connection.select_value(sql)
14
+ @test_log.rewind
15
+ log_data = @test_log.read
16
+ assert_match Regexp.new(Regexp.quote(sql) + '.*' + Regexp.quote('active_record_profiler_test.rb')), log_data
17
+ end
18
+
19
+ def test_profiler_records_query_site
20
+ assert @collector
21
+ @collector.flush_query_sites_statistics
22
+ assert @collector.query_sites.blank?
23
+ sql = 'SELECT 1 FROM widgets'
24
+ ActiveRecord::Base.connection.select_value(sql)
25
+ @test_log.rewind
26
+ assert @collector.query_sites.present?
27
+ end
28
+
29
+ end
data/test/database.yml ADDED
@@ -0,0 +1,4 @@
1
+ test:
2
+ adapter: sqlite3
3
+ database: db/dummy.sqlite3
4
+ pool: 5
@@ -0,0 +1,28 @@
1
+ == README
2
+
3
+ This README would normally document whatever steps are necessary to get the
4
+ application up and running.
5
+
6
+ Things you may want to cover:
7
+
8
+ * Ruby version
9
+
10
+ * System dependencies
11
+
12
+ * Configuration
13
+
14
+ * Database creation
15
+
16
+ * Database initialization
17
+
18
+ * How to run the test suite
19
+
20
+ * Services (job queues, cache servers, search engines, etc.)
21
+
22
+ * Deployment instructions
23
+
24
+ * ...
25
+
26
+
27
+ Please feel free to use a different markup language if you do not plan to run
28
+ <tt>rake doc:app</tt>.
@@ -0,0 +1,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require File.expand_path('../config/application', __FILE__)
5
+
6
+ Rails.application.load_tasks
File without changes
@@ -0,0 +1,13 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file.
9
+ //
10
+ // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .