active-record-profiler 0.1 → 1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 29a3517f08a6a21ab80d888178ae5371f60f9f05
4
- data.tar.gz: 752933b5e705af4c7698496246f2356df6007dbb
3
+ metadata.gz: 211f71211f9004c489a7de54d5c5e59698d7597d
4
+ data.tar.gz: 956d9fbd435a5eaa6159e221fa4a7182532e6a70
5
5
  SHA512:
6
- metadata.gz: e23f7711b5a0936b5d55e14027f3befd06bf7f6d999d3a4e8b8efcc636d0b41e78cd69cef3f4d11d3c7ba9a673ad50bf2e829a1ea58172e5580ff8a8d09ce74a
7
- data.tar.gz: 3099240d90f3497e8f2f95597e5489c4b487d58e0fc234f29190d9ad4ca3daefe125ffecf05a73b8ac9d8588b1b247eb8c69091eb2fd6683a10c9ce2ad869e13
6
+ metadata.gz: a7b4b502cbd6e098f23aa3666b1567954aedf24d4de83e69168a404216e914ed53c011a815b85ca8bcf5febe33bddf08330969631f28cc87a9f5474a119f3df5
7
+ data.tar.gz: a8e5101c041a79d1383039ac2ab661d832b3a6d054d19be37a3255f5d8eae71b3ce1af69f3445f64ced0dc667248751d0ff108f5c86f987227025e0e8bd906e3
data/README.md CHANGED
@@ -1,147 +1,122 @@
1
1
  ActiveRecordProfiler
2
2
  ====================
3
3
 
4
- ActiveRecordProfiler monkey-patches
5
- ActiveRecord::ConnectionAdapters::AbstractAdapter both to improve the
6
- standard SQL logging and to provide profiler-like tracking of SQL
7
- statements generated by application code.
8
-
9
- Each SQL log entry generated by ActiveRecord will have appended the
10
- filename, line number, and function name of the nearest calling
11
- application code. The information is determined by walking up the call
12
- stack until a filename within the /app/ directory is found. If no such
13
- filename can be found, the SQL will be logged with a location of
14
- 'Non-application code'.
15
-
16
- Additionally, the profiler will keep track of the total time spent by all
17
- SQL calls coming from each calling location, as well as the number of time
18
- that location accessed the database. Certain SQL calls not under the
19
- direct control of the application are not counted in these statistics,
20
- such as "SHOW FIELDS", "SET NAMES", "BEGIN", and "COMMIT" statements which
21
- tend to skew the timing statistics and provide less useful information
22
- about slow SQL queries.
23
-
24
- Periodically, the profiler will dump its statistics out to a file and
25
- restart all of its counters/timers. The output file is named for the time
26
- and PID from which it was written, so that multiple processes can safely
27
- write their output simultaneously.
4
+ The `active-record-profiler` gem adds a log formatter to improve the standard SQL logging and a log subscriber to provide profiler-like tracking of SQL statements generated by application code. The log formatter makes the SQL statements in your logs look like this (imagine it's all in one log line):
5
+
6
+ Person Load (0.3ms) SELECT `people`.* FROM `people` WHERE `people`.`id` = ? LIMIT 1 [["id", 1]]
7
+ CALLED BY 'app/controllers/day_controller.rb:282:in `find_person''
8
+
9
+ If you're using log colorizing, then it's even fancier:
10
+
11
+ ![Enhanced Log](./screenshots/enhanced-log.png?raw=true)
12
+
13
+ Each SQL log entry generated by ActiveRecord will have appended the filename, line number, and function name of the nearest calling application code. The information is determined by walking up the call stack until a filename within the `/app/` directory is found. If no such filename can be found, the SQL will be logged with a location of 'Non-application code'.
14
+
15
+ Additionally, the profiler will keep track of the total time spent by all SQL calls coming from each calling location, as well as the number of time that location accessed the database. Certain SQL calls not under the direct control of the application are not counted in these statistics, such as `SHOW FIELDS`, `SET NAMES`, `BEGIN`, and `COMMIT` statements which tend to skew the timing statistics and provide less useful information about slow SQL queries.
16
+
17
+ Periodically, the profiler will dump its statistics out to a file and restart all of its counters/timers. The output file is named for the time, process id, and thread id from which it was written, so that multiple threads/processes can safely write their output simultaneously.
28
18
 
29
19
  Installation
30
20
  ============
31
- Add it to your Gemfile, do `bundle install`, and then add a new initializer, `config/initializers/active_record_profiler.rb`:
21
+ Add it to your Gemfile, like so:
22
+
23
+ gem 'active-record-profiler'
24
+
25
+ Then do `bundle install`, and then add a new initializer, `config/initializers/active_record_profiler.rb`:
32
26
 
33
27
  ActiveRecord::Base.logger =
34
28
  ActiveRecordProfiler::Logger.new(ActiveRecord::Base.logger)
35
29
  ActiveRecordProfiler::LogSubscriber.attach_to :active_record unless Rails.env.test?
36
30
 
37
-
38
31
  The first line adds call-site information to ActiveRecord logging, and the second line enables profiling (except in the test environment, where it would mess up your profiling data).
39
32
 
40
33
  Configuration
41
34
  =============
35
+
36
+ ## stats_flush_period ##
42
37
  Control the (approximate) frequency of statistics flushes (default: `1.hour`)
43
38
 
44
- ActiveRecordProfiler::Collector.stats_flush_period = 1.hour
39
+ ActiveRecordProfiler.stats_flush_period = 1.hour
45
40
 
46
41
  Note that only flushed data is available for use in the rake reports (described below). If you are running a multithreaded or multiprocess server (which covers most common rails server types), your data will be incomplete until all those threads/processes/servers have flushed their data. This limitation exists primarily to avoid the overhead of coordinating/locking during the process of serving your application's web requests.
47
42
 
48
- Directory where profile data is recorded (default: `Rails.root,join('log', 'profiler_data'`)
43
+ ## profile_dir ##
44
+ Directory where profile data is recorded. (default: `Rails.root,join('log', 'profiler_data'`)
45
+
46
+ ActiveRecordProfiler.profile_dir = Rails.root.join('log', 'profiler_data'
47
+
48
+ ## sql_ignore_pattern ##
49
+ Any SQL statements matching this pattern will not be tracked by the profiler output, though it will still appear in the enhanced SQL logging. (default: `/^(SHOW FIELDS |SET SQL_AUTO_IS_NULL|SET NAMES |EXPLAIN |BEGIN|COMMIT|PRAGMA )/`)
50
+
51
+ ActiveRecordProfiler.sql_ignore_pattern = /^SET /x
49
52
 
50
- ActiveRecordProfiler::Collector.profile_dir = Rails.root.join('log', 'profiler_data'
53
+ ## app_path_pattern ##
54
+ Any calling location that matches this pattern will be tracked individually. Any locations that do __not__ match will be grouped under a single `Non-application code` label. The default only includes the `app`, `lib`, and `vendor` directories, but that's usually suitable and avoids noise. (default: `Regexp.new(Regexp.quote("#{Rails.root.expand_path}/") + "(:?app|lib|vendor)/")`)
51
55
 
52
- Any SQL statements matching this pattern will not be tracked by the
53
- profiler output, though it will still appear in the enhanced SQL logging
54
- (default: `/^(SHOW FIELDS |SET SQL_AUTO_IS_NULL|SET NAMES |EXPLAIN |BEGIN|COMMIT|PRAGMA )/`)
56
+ ActiveRecordProfiler.app_path_pattern = Regexp.new(Regexp.quote("#{Rails.root.expand_path}/"))
55
57
 
56
- ActiveRecordProfiler::Collector.sql_ignore_pattern = /^SET /x
58
+ ## trim_root_path ##
59
+ This prefix will be removed from the calling site filepath for brevity. (default: `"#{Rails.root.expand_path}/"`)
57
60
 
61
+ ActiveRecordProfiler.trim_root_path = "#{Rails.root.parent.expand_path}/app"
58
62
 
59
- Reports
60
- =======
61
- To see a top-100 list of what SQL statements your application is spending its
62
- time in, run the following rake task:
63
+ ## profile_self ##
64
+ This setting controls whether the profiler records information about how much time was spent in the profiling code itself. If you want to know what kind of overhead the profiler is adding, set this to true. (default: `false`)
65
+
66
+ ActiveRecordProfiler.profile_self = true
67
+
68
+
69
+ ## link_location ##
70
+ Whether or not to make the locations in the profiler reports into source code links (only works with editors that can be launched via URL scheme) (default: `false`):
71
+
72
+ ActiveRecordProfiler.link_location = true
73
+
74
+
75
+ Command Line Reports
76
+ ====================
77
+ To see a top-100 list of what SQL statements your application is spending its time in, run the following rake task:
63
78
 
64
79
  rake profiler:aggregate max_lines=100 show_sql=true
65
80
 
66
- This will return a list of the SQL which is taking the most time in your
67
- application in this format:
81
+ This will return a list of the SQL which is taking the most time in your application in this format:
68
82
 
69
83
  <file path>:<line number>:in <method name>: <total duration>, <call count>, <max single call duration>
70
84
 
71
- This will aggregate all of the profiler data you have accumulated; in order
72
- to limit the timeframe of the data, use the `prefix` option to specify a
73
- partial date/time:
85
+ This will aggregate all of the profiler data you have accumulated; in order to limit the timeframe of the data, use the `prefix` option to specify a partial date/time:
74
86
 
75
87
  rake profiler:aggregate max_lines=100 show_sql=true prefix=2010-06-20-10 # data from June 20 during the 10am hour (roughly)
76
88
 
77
- Each thread running the profiler flushes its stats periodically, and there
78
- is a rake task to combine multiple profiler data files together in order to
79
- keep the number of data files down to a manageable number. A good way to
80
- manage the data files on a server is to set up a cron task to run the
81
- following command once per hour or once per day:
89
+ Each thread running the profiler flushes its stats periodically, and there is a rake task to combine multiple profiler data files together in order to keep the number of data files down to a manageable number. A good way to manage the data files on a server is to set up a cron task to run the following command once per hour or once per day:
82
90
 
83
91
  rake profiler:aggregate compact=<'hour' or 'date'> RAILS_ENV=qa
84
92
 
85
- Compacting by hour will result in a single file for each hour any process
86
- dumped its stats. Compacting by day will result in a single file for each
87
- day. When using the `prefix` option to generate a profiler report, you
88
- cannot specify an hour if you have compacted your data by date instead of
89
- hour (the prefix matching operates on the file names, which will not have
90
- hours if they have been compacted by date).
93
+ Compacting by hour will result in a single file for each hour any process dumped its stats. Compacting by day will result in a single file for each day. When using the `prefix` option to generate a profiler report, you cannot specify an hour if you have compacted your data by date instead of hour (the prefix matching operates on the file names, which will not have hours if they have been compacted by date).
91
94
 
92
95
  You can clear out all profiler data using the following command:
93
96
 
94
97
  rake profiler:clear_data
95
98
 
96
- If you want programmatic access to the profiler data, check out the source
97
- code for the rake tasks in `lib/active-record-profiler/tasks.rake`.
99
+ If you want programmatic access to the profiler data, check out the source code for the rake tasks in `lib/active-record-profiler/tasks.rake`.
98
100
 
99
101
 
100
- =======
101
- HTML Reports
102
- ============
102
+ Web Reports
103
+ ===========
103
104
 
104
- The profiler includes some view helpers to make it easy for your application
105
- to generate a sortable HTML table of profiler information. The core helper
106
- method generates a table based on an ActiveRecordProfiler::Collector object.
107
- In its simplest form, it can be called from a view like this:
105
+ The `active-record-profiler` gem also includes support for producing reports in your browser, which look something like this:
108
106
 
109
- <div id="#profiler">
110
- <%= profiler_report(params) %>
111
- </div>
112
-
113
- The full set of parameters available looks like this:
107
+ ![Web Report UI](./screenshots/web-reports.png?raw=true)
108
+
109
+ In order to make it available, you'll want to mount the engine within your application's `routes.rb`. Since you don't want the public to have access to your profiler reports, you'll want to limit access to it. You could only mount the profiler's web interface in the development environment:
114
110
 
115
- profiler_report(page_parameters, date_prefix, sort_id, max_rows, profiler_collector, css_options)
116
-
117
- parameters:
118
- page_parameters: this is generally the request parameters, used to build the report-sorting links (HashWithIndifferentAccess)
119
- options: hash containing optional settings for the report; supported keys:
120
-
121
- :date : year, year-month, year-month-day, year-month-day-hour used to filter the profiler data; defaults to Today (String)
122
- :sort : ActiveRecordProfiler::(DURATION|COUNT|LONGEST|AVG_DURATION) specifying which field to sort the report by; defaults to DURATION (Constant/Integer)
123
- :max_rows : Maximum number of table rows to output; in other words, report on the top max_rows SQL statements; defaults to 100 (Integer)
124
- :collector : object representing the profile data to use in building the report; defaults to an empty collector using the configured profile data directory (ActiveRecordProfiler::Collector )
125
- :table : css class applied to the report <table> element; defaults to nil
126
- :header_row : css class applied to the report's header row; defaults to nil
127
- :row : css class applied to the report's data rows; defaults to nil
128
- :link_locations : true/false value indicating whether to build textmate links to the source code whence a given piece of SQL came; defaults to false
129
-
130
- An easy way to support filtering of report data by month/date/hour is to
131
- use a view like this:
111
+ mount ActiveRecordProfiler::Engine => "/profiler" if Rails.env.development?
132
112
 
133
- <%= profiler_date_filter_form(params) %>
134
- <%= profiler_report(params) %>
135
-
136
- And if you use TextMate, then you may want to throw in some extra goodies
137
- to generate links to the actual source code files and lines where the SQL
138
- was triggered (Note: the current javascript requires jQuery):
113
+ Or, if you are using [Devise](https://github.com/plataformatec/devise) and have an admin flag on a `User` model, for example:
139
114
 
140
- <%= profiler_date_filter_form(params) %>
141
- <%= profiler_report_local_path_form %>
142
- <%= profile_report_local_path_javascript %>
143
- <%= profiler_report(params, {:link_locations => true}) %>
115
+ authenticated :user, -> user { user.admin? } do
116
+ mount ActiveRecordProfiler::Engine => "/profiler"
117
+ end
144
118
 
119
+ When you visit the path where you've mounted the web interface, you will by default see a report for the current day. If you just installed `active-record-profiler`, there probably won't be anything there yet, in which case you should go play with your app for a while and come back later. :) Also note that if the web server you are using has multiple processes or threads, you won't see data from those other processes/threads until they dump their data.
145
120
 
146
121
  Miscellaneous
147
122
  =============
@@ -22,6 +22,7 @@ EOF
22
22
  spec.require_paths = ["lib"]
23
23
 
24
24
  spec.add_dependency "rails", "~> 4.0"
25
+ spec.add_dependency "jquery-rails", "~> 3.1"
25
26
 
26
27
  spec.add_development_dependency "bundler", "~> 1.3"
27
28
  spec.add_development_dependency "rake", "~> 10.3"
@@ -0,0 +1,14 @@
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 jquery
14
+ //= require_tree .
@@ -0,0 +1,104 @@
1
+ var ActiveRecordProfiler = (function (my_module) {
2
+ my_module = my_module || {};
3
+
4
+ var SOURCE_ROOT_KEY = "db_prof_source_root";
5
+ var SOURCE_EDITOR_KEY = "db_prof_source_editor";
6
+ var profilerSourceRootField, sourceEditorSelect, railsRoot;
7
+
8
+ function supportsLocalStorage() {
9
+ try {
10
+ return (('localStorage' in window) && (window['localStorage'] !== null));
11
+ } catch (e) {
12
+ return false;
13
+ }
14
+ }
15
+
16
+ function setDBProfSourceRoot(value) {
17
+ if (supportsLocalStorage()) {
18
+ if (!!value) {
19
+ window.localStorage[SOURCE_ROOT_KEY] = value;
20
+ } else {
21
+ window.localStorage.removeItem(SOURCE_ROOT_KEY);
22
+ }
23
+ }
24
+ }
25
+
26
+ function getDBProfSourceRoot() {
27
+ var root = railsRoot;
28
+
29
+ if (supportsLocalStorage()) {
30
+ var localRoot = window.localStorage[SOURCE_ROOT_KEY];
31
+ if (!!localRoot) {
32
+ root = localRoot;
33
+ }
34
+ }
35
+ return root;
36
+ }
37
+
38
+ function setSourceEditor(value) {
39
+ if (supportsLocalStorage()) {
40
+ if (!!value) {
41
+ window.localStorage[SOURCE_EDITOR_KEY] = value;
42
+ } else {
43
+ window.localStorage.removeItem(SOURCE_EDITOR_KEY);
44
+ }
45
+ }
46
+ }
47
+
48
+ function getSourceEditor() {
49
+ if (supportsLocalStorage()) {
50
+ return window.localStorage[SOURCE_EDITOR_KEY];
51
+ } else {
52
+ return undefined;
53
+ }
54
+ }
55
+
56
+ my_module.formatLink = function (file, line, editor) {
57
+ var link;
58
+
59
+ switch (editor) {
60
+ case "subl":
61
+ case "txmt":
62
+ link = editor + "://open/?url=file://" + file + "&line=" + line;
63
+ break;
64
+ default:
65
+ // do nothing, return undefined link
66
+ }
67
+
68
+ return link;
69
+ };
70
+
71
+ my_module.showSourceFile = function (file, line) {
72
+ var root = profilerSourceRootField.val();
73
+ var editor = sourceEditorSelect.val();
74
+ if (root == "") { root = railsRoot; }
75
+ // window.location = "txmt://open/?url=file://" + root + "/" + file + "&line=" + line;
76
+ var link = my_module.formatLink(root + "/" + file, line, editor);
77
+ if (link) {
78
+ window.location = link;
79
+ }
80
+ };
81
+
82
+ $(function () {
83
+ profilerSourceRootField = $('#source_root');
84
+ railsRoot = profilerSourceRootField.val();
85
+
86
+ profilerSourceRootField.val(getDBProfSourceRoot());
87
+ profilerSourceRootField.change(function (e) {
88
+ setDBProfSourceRoot($(this).val());
89
+ });
90
+
91
+ sourceEditorSelect = $('#source_editor');
92
+ sourceEditorSelect.val(getSourceEditor());
93
+ sourceEditorSelect.change(function (e) {
94
+ setSourceEditor($(this).val());
95
+ })
96
+
97
+ $('.profiler-report').on('click', '.source-link', function (event) {
98
+ var link = $(this);
99
+ my_module.showSourceFile(link.data('file'), link.data('line'));
100
+ });
101
+ });
102
+
103
+ return my_module;
104
+ }(ActiveRecordProfiler));
@@ -0,0 +1,61 @@
1
+ .profiler-report {
2
+ border: solid 1px gray;
3
+ border-collapse: collapse;
4
+ margin-top: 5px;
5
+ }
6
+
7
+ .profiler-report th, .profiler-report td {
8
+ vertical-align: top;
9
+ padding: 3px;
10
+ }
11
+
12
+ .profiler-report thead {
13
+ border: solid 1px gray;
14
+ background: #c9e6ec;
15
+ }
16
+
17
+ .profiler-report th {
18
+ vertical-align: middle;
19
+ font-weight: bold;
20
+ }
21
+
22
+ .profiler-report th.location {
23
+ width: 19%;
24
+ }
25
+
26
+ .profiler-report th.total-duration {
27
+ width: 6%;
28
+ }
29
+ .profiler-report th.count {
30
+ width: 6%;
31
+ }
32
+
33
+ .profiler-report th.average-duration {
34
+ width: 6%;
35
+ }
36
+
37
+ .profiler-report th.max-duration {
38
+ width: 6%;
39
+ }
40
+
41
+ .profiler-report th.max-duration-sql {
42
+ width: 57%;
43
+ }
44
+
45
+ .profiler-report tbody tr:nth-child(even) {
46
+ background-color: #eee;
47
+ }
48
+
49
+ .profiler-report .rank-bar {
50
+ background-color: red;
51
+ height: 1.2em;
52
+ }
53
+
54
+ .profiler-report td.numeric {
55
+ text-align: right;
56
+ }
57
+
58
+ .profiler-report td.sql {
59
+ font-family: monospace;
60
+ padding-left: 10px;
61
+ }
@@ -0,0 +1,4 @@
1
+ module ActiveRecordProfiler
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,30 @@
1
+ module ActiveRecordProfiler
2
+ class ReportsController < ActiveRecordProfiler::ApplicationController
3
+ include ReportsHelper
4
+
5
+ def index
6
+ @report_options = report_options(params)
7
+ collector = ActiveRecordProfiler::Collector.new
8
+ @totals = collector.aggregate(:prefix => @report_options[:date])
9
+ @top_locations = collector.sorted_locations(@report_options[:sort],
10
+ @report_options[:max_rows])
11
+ top_item = @totals[@top_locations.first]
12
+ @max_bar_value = bar_value(@report_options[:sort], top_item)
13
+ end
14
+
15
+ private
16
+ def report_options(page_params)
17
+ options = page_params.reverse_merge(
18
+ :date => Time.now.strftime(Collector::DATE_FORMAT),
19
+ :sort => Collector::DURATION,
20
+ :max_rows => 100
21
+ )
22
+
23
+ options[:sort] = options[:sort].to_i
24
+ options[:max_rows] = options[:max_rows].to_i
25
+ options[:link_location] = ActiveRecordProfiler.link_location
26
+
27
+ return options
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,27 @@
1
+ module ActiveRecordProfiler
2
+ module ReportsHelper
3
+ def report_column_header_link(column_id, sort_by, label, page_params)
4
+ link_to_unless(sort_by == column_id,
5
+ label,
6
+ page_params.merge(:sort => column_id)
7
+ )
8
+ end
9
+
10
+ def bar_value(sort, item)
11
+ return 0 unless item && sort
12
+ if sort == ActiveRecordProfiler::Collector::AVG_DURATION
13
+ item[Collector::DURATION] / item[Collector::COUNT]
14
+ else
15
+ item[sort]
16
+ end
17
+ end
18
+
19
+ def breakable_path(path)
20
+ h(path).gsub('/', "&#8203;/")
21
+ end
22
+
23
+ def location_description(path, line_number)
24
+ "#{breakable_path(path)}: #{h(line_number)}".html_safe
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,6 @@
1
+ <form>
2
+ <%= hidden_field_tag :sort, @report_options[:sort] %>
3
+ <label for="profiler_date">Filter by date-hour prefix (yyyy-mm-dd-hh):</label>
4
+ <%= text_field_tag :date, @report_options[:date], {:size =>10, :id => :profiler_date} %>
5
+ <input name="commit" type="submit" value="Go">
6
+ </form>
@@ -0,0 +1,11 @@
1
+ <form>
2
+ <label for="source_root">Local source path (for location links):</label>
3
+ <input id="source_root" name="source_root" size="50" type="text" value="<%= Rails.root %>">
4
+ <br />
5
+ <label for="source_root">Editor (for location links):</label>
6
+ <select id="source_editor">
7
+ <option value="">None</option>
8
+ <option value="subl">Sublime Text</option>
9
+ <option value="txmt">TextMate</option>
10
+ </select>
11
+ </form>
@@ -0,0 +1,51 @@
1
+ <table class="profiler-report">
2
+ <thead>
3
+ <tr>
4
+ <th class="location">Location</th>
5
+ <th class="duration">
6
+ <%= report_column_header_link(ActiveRecordProfiler::Collector::DURATION, @report_options[:sort], 'Total Duration (s)', params) %>
7
+ </th>
8
+ <th class="count">
9
+ <%= report_column_header_link(ActiveRecordProfiler::Collector::COUNT, @report_options[:sort], 'Count', params) %>
10
+ </th>
11
+ <th class="average-duration">
12
+ <%= report_column_header_link(ActiveRecordProfiler::Collector::AVG_DURATION, @report_options[:sort], 'Avg. Duration (ms)', params) %>
13
+ </th>
14
+ <th class="max-duration">
15
+ <%= report_column_header_link(ActiveRecordProfiler::Collector::LONGEST, @report_options[:sort], 'Max. Duration (ms)', params) %>
16
+ </th>
17
+ <th class="max-duration-sql">SQL for Max Duration</th>
18
+ </tr>
19
+ </thead>
20
+ <tbody>
21
+ <% @top_locations.each do |location| -%>
22
+ <tr>
23
+ <%
24
+ item = @totals[location]
25
+ path, line_number, method = location.split(':')
26
+ this_bar = bar_value(@report_options[:sort], item)
27
+ bar_width = 100 * this_bar / @max_bar_value
28
+ -%>
29
+ <td title="<%= location %>">
30
+ <%= link_to_if(@report_options[:link_location], location_description(path, line_number), "#", :class => 'source-link', :data => {file: path, line: line_number}) %>
31
+ <div class="rank-bar" style="width:<%= bar_width %>%"></div>
32
+ </td>
33
+ <td class="numeric">
34
+ <%= number_with_precision(item[ActiveRecordProfiler::Collector::DURATION], :precision => 3) %>
35
+ </td>
36
+ <td class="numeric">
37
+ <%= number_with_delimiter(item[ActiveRecordProfiler::Collector::COUNT]) %>
38
+ </td>
39
+ <td class="numeric">
40
+ <%= number_with_precision(item[ActiveRecordProfiler::Collector::DURATION] / item[ActiveRecordProfiler::Collector::COUNT] * 1000, :precision => 3) %>
41
+ </td>
42
+ <td class="numeric">
43
+ <%= number_with_precision(item[ActiveRecordProfiler::Collector::LONGEST] * 1000, :precision => 3) %>
44
+ </td>
45
+ <td class="sql">
46
+ <%= item[ActiveRecordProfiler::Collector::LONGEST_SQL] %>
47
+ </td>
48
+ </tr>
49
+ <% end -%>
50
+ </tbody>
51
+ </table>
@@ -0,0 +1,7 @@
1
+ <h1>Active Record Profiler Report</h1>
2
+
3
+ <div id="profiler">
4
+ <%= render "date_filter_form" %>
5
+ <%= render "local_path_form" %>
6
+ <%= render "report_table" %>
7
+ </div>
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>ActiveRecordProfiler</title>
5
+ <%= stylesheet_link_tag "active_record_profiler/application", media: "all" %>
6
+ <%= javascript_include_tag "active_record_profiler/application" %>
7
+ <%= csrf_meta_tags %>
8
+ </head>
9
+ <body>
10
+
11
+ <%= yield %>
12
+
13
+ </body>
14
+ </html>
@@ -0,0 +1,8 @@
1
+ ActiveRecordProfiler.stats_flush_period = 1.hour
2
+ ActiveRecordProfiler.profile_dir = Rails.root.join("log", "profiler_data")
3
+ ActiveRecordProfiler.sql_ignore_pattern =
4
+ /^(SHOW (:?FULL )?FIELDS |SET SQL_AUTO_IS_NULL|SET NAMES |EXPLAIN |BEGIN|COMMIT|PRAGMA )/i
5
+ ActiveRecordProfiler.app_path_pattern = Regexp.new(Regexp.quote("#{Rails.root.expand_path}/") + "(:?app|lib|vendor)/")
6
+ ActiveRecordProfiler.trim_root_path = "#{Rails.root.expand_path}/"
7
+ ActiveRecordProfiler.profile_self = false
8
+ ActiveRecordProfiler.link_location = false
data/config/routes.rb ADDED
@@ -0,0 +1,3 @@
1
+ ActiveRecordProfiler::Engine.routes.draw do
2
+ get "/" => "reports#index"
3
+ end
@@ -27,7 +27,6 @@ module ActiveRecordProfiler
27
27
  # You can control the (approximate) frequency of statistics flushes by setting
28
28
  # ActiveRecordProfiler::Collector.stats_flush_period = 1.hour
29
29
  cattr_accessor :stats_flush_period
30
- self.stats_flush_period = 1.hour
31
30
 
32
31
  # Directory where profile data is recorded
33
32
  cattr_accessor :profile_dir
@@ -35,19 +34,23 @@ module ActiveRecordProfiler
35
34
  # Any SQL statements matching this pattern will not be tracked by the profiler output
36
35
  # (though it will still appear in the enhanced SQL logging).
37
36
  cattr_accessor :sql_ignore_pattern
38
- self.sql_ignore_pattern = /^(SHOW FIELDS |SET SQL_AUTO_IS_NULL|SET NAMES |EXPLAIN |BEGIN|COMMIT|PRAGMA )/i
39
37
 
38
+ # Any locations matching this pattern will be considered part of the
39
+ # application. Any locations NOT matching will be lumped under a single
40
+ # 'Non-application code' entry.
40
41
  cattr_accessor :app_path_pattern
41
- self.app_path_pattern = Regexp.new(Regexp.quote("/app/"))
42
42
 
43
+ # This prefix will be removed from location names, for brevity.
43
44
  cattr_accessor :trim_root_path
44
45
 
46
+ # Controls whether or not to record profiling data for time spent in the
47
+ # profiler code itself.
48
+ cattr_accessor :profile_self
49
+
50
+ # Obsolete, but still referenced
45
51
  cattr_accessor :storage_backend
46
52
  self.storage_backend = :json
47
53
 
48
- cattr_accessor :profile_self
49
- self.profile_self = false
50
-
51
54
  attr_accessor :last_stats_flush
52
55
  attr_accessor :query_sites
53
56
  attr_accessor :profile_data_directory
@@ -0,0 +1,19 @@
1
+ module ActiveRecordProfiler
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace ActiveRecordProfiler
4
+
5
+ rake_tasks do
6
+ load 'active-record-profiler/tasks.rake'
7
+ end
8
+
9
+ initializer "active_record_profiler.apply_default" do
10
+ # Copy module settings to collector class
11
+ [:stats_flush_period, :profile_dir, :sql_ignore_pattern,
12
+ :app_path_pattern, :trim_root_path, :profile_self
13
+ ].each do |config_item|
14
+ ActiveRecordProfiler::Collector.send("#{config_item}=".to_sym,
15
+ ActiveRecordProfiler.send(config_item))
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,3 +1,3 @@
1
1
  module ActiveRecordProfiler
2
- VERSION = "0.1"
2
+ VERSION = "1.0"
3
3
  end
@@ -2,12 +2,13 @@ require 'active-record-profiler/version'
2
2
  require 'active-record-profiler/collector'
3
3
  require 'active-record-profiler/log_subscriber'
4
4
  require 'active-record-profiler/logger'
5
- require 'active-record-profiler/profiler_view_helper'
6
-
7
- require 'active-record-profiler/railtie' if defined?(Rails)
5
+ require "active-record-profiler/engine" if defined?(Rails)
8
6
 
9
7
  module ActiveRecordProfiler
10
8
  require 'fileutils'
11
9
  require 'json'
10
+
11
+ mattr_accessor :link_location, :stats_flush_period, :profile_dir,
12
+ :sql_ignore_pattern, :app_path_pattern, :trim_root_path, :profile_self
12
13
  end
13
14
 
Binary file
Binary file
@@ -1,56 +1,3 @@
1
1
  Rails.application.routes.draw do
2
- # The priority is based upon order of creation: first created -> highest priority.
3
- # See how all your routes lay out with "rake routes".
4
-
5
- # You can have the root of your site routed with "root"
6
2
  root 'profiler#index'
7
-
8
- # Example of regular route:
9
- # get 'products/:id' => 'catalog#view'
10
-
11
- # Example of named route that can be invoked with purchase_url(id: product.id)
12
- # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase
13
-
14
- # Example resource route (maps HTTP verbs to controller actions automatically):
15
- # resources :products
16
-
17
- # Example resource route with options:
18
- # resources :products do
19
- # member do
20
- # get 'short'
21
- # post 'toggle'
22
- # end
23
- #
24
- # collection do
25
- # get 'sold'
26
- # end
27
- # end
28
-
29
- # Example resource route with sub-resources:
30
- # resources :products do
31
- # resources :comments, :sales
32
- # resource :seller
33
- # end
34
-
35
- # Example resource route with more complex sub-resources:
36
- # resources :products do
37
- # resources :comments
38
- # resources :sales do
39
- # get 'recent', on: :collection
40
- # end
41
- # end
42
-
43
- # Example resource route with concerns:
44
- # concern :toggleable do
45
- # post 'toggle'
46
- # end
47
- # resources :posts, concerns: :toggleable
48
- # resources :photos, concerns: :toggleable
49
-
50
- # Example resource route within a namespace:
51
- # namespace :admin do
52
- # # Directs /admin/products/* to Admin::ProductsController
53
- # # (app/controllers/admin/products_controller.rb)
54
- # resources :products
55
- # end
56
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active-record-profiler
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.1'
4
+ version: '1.0'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Turner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-01-22 00:00:00.000000000 Z
11
+ date: 2015-01-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '4.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: jquery-rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.1'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: bundler
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -96,14 +110,28 @@ files:
96
110
  - README.md
97
111
  - Rakefile
98
112
  - active-record-profiler.gemspec
113
+ - app/assets/javascripts/active_record_profiler/application.js
114
+ - app/assets/javascripts/active_record_profiler/profiler.js
115
+ - app/assets/stylesheets/active_record_profiler/application.css
116
+ - app/controllers/active_record_profiler/application_controller.rb
117
+ - app/controllers/active_record_profiler/reports_controller.rb
118
+ - app/helpers/active_record_profiler/reports_helper.rb
119
+ - app/views/active_record_profiler/reports/_date_filter_form.html.erb
120
+ - app/views/active_record_profiler/reports/_local_path_form.html.erb
121
+ - app/views/active_record_profiler/reports/_report_table.html.erb
122
+ - app/views/active_record_profiler/reports/index.html.erb
123
+ - app/views/layouts/active_record_profiler/application.html.erb
124
+ - config/initializers/default_config.rb
125
+ - config/routes.rb
99
126
  - lib/active-record-profiler.rb
100
127
  - lib/active-record-profiler/collector.rb
128
+ - lib/active-record-profiler/engine.rb
101
129
  - lib/active-record-profiler/log_subscriber.rb
102
130
  - lib/active-record-profiler/logger.rb
103
- - lib/active-record-profiler/profiler_view_helper.rb
104
- - lib/active-record-profiler/railtie.rb
105
131
  - lib/active-record-profiler/tasks.rake
106
132
  - lib/active-record-profiler/version.rb
133
+ - screenshots/enhanced-log.png
134
+ - screenshots/web-reports.png
107
135
  - test/active_record_profiler_test.rb
108
136
  - test/database.yml
109
137
  - test/dummy/README.rdoc
@@ -1,200 +0,0 @@
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
@@ -1,18 +0,0 @@
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