action_cost 0.0.1
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.
- data/.document +5 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +33 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +27 -0
- data/Rakefile +45 -0
- data/VERSION +1 -0
- data/action_cost.gemspec +72 -0
- data/app/controllers/action_cost/dashboards_controller.rb +32 -0
- data/app/views/action_cost/dashboards/ca.html.erb +31 -0
- data/app/views/action_cost/dashboards/index.html.erb +25 -0
- data/app/views/layouts/action_cost.html.erb +27 -0
- data/config/routes.rb +9 -0
- data/lib/action_cost.rb +2 -0
- data/lib/action_cost/engine.rb +27 -0
- data/lib/action_cost/extensions/postgresql_adapter.rb +24 -0
- data/lib/action_cost/middleware.rb +63 -0
- data/lib/action_cost/record_cache/index_hook.rb +22 -0
- data/lib/action_cost/record_cache_parser.rb +27 -0
- data/lib/action_cost/request_stats.rb +90 -0
- data/lib/action_cost/sql_parser.rb +88 -0
- data/lib/action_cost/stats_collector.rb +54 -0
- data/test/helper.rb +18 -0
- data/test/test_action_cost.rb +7 -0
- metadata +139 -0
data/.document
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
# Add dependencies required to use your gem here.
|
3
|
+
# Example:
|
4
|
+
# gem "activesupport", ">= 2.3.5"
|
5
|
+
|
6
|
+
# Add dependencies to develop your gem here.
|
7
|
+
# Include everything needed to run rake, tests, features, etc.
|
8
|
+
group :development do
|
9
|
+
gem "shoulda", ">= 0"
|
10
|
+
gem "rdoc", "~> 3.12"
|
11
|
+
gem "bundler", "~> 1.0"
|
12
|
+
gem "jeweler", "~> 1.8.4"
|
13
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
activesupport (3.2.9)
|
5
|
+
i18n (~> 0.6)
|
6
|
+
multi_json (~> 1.0)
|
7
|
+
git (1.2.5)
|
8
|
+
i18n (0.6.1)
|
9
|
+
jeweler (1.8.4)
|
10
|
+
bundler (~> 1.0)
|
11
|
+
git (>= 1.2.5)
|
12
|
+
rake
|
13
|
+
rdoc
|
14
|
+
json (1.7.6)
|
15
|
+
multi_json (1.3.7)
|
16
|
+
rake (10.0.3)
|
17
|
+
rdoc (3.12.1)
|
18
|
+
json (~> 1.4)
|
19
|
+
shoulda (3.3.2)
|
20
|
+
shoulda-context (~> 1.0.1)
|
21
|
+
shoulda-matchers (~> 1.4.1)
|
22
|
+
shoulda-context (1.0.1)
|
23
|
+
shoulda-matchers (1.4.1)
|
24
|
+
activesupport (>= 3.0.0)
|
25
|
+
|
26
|
+
PLATFORMS
|
27
|
+
ruby
|
28
|
+
|
29
|
+
DEPENDENCIES
|
30
|
+
bundler (~> 1.0)
|
31
|
+
jeweler (~> 1.8.4)
|
32
|
+
rdoc (~> 3.12)
|
33
|
+
shoulda
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2013 Philippe Le Rohellec
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
= ActionCost
|
2
|
+
|
3
|
+
ActionCost is a Rails 3 engine implemented as a gem.
|
4
|
+
It hooks into ActiveRecord (and RecordCache if used) and counts the number of SQL queries
|
5
|
+
per controller action and per table.
|
6
|
+
|
7
|
+
== Setup
|
8
|
+
Simply include action_cost into your Rails 3 application Gemfile.
|
9
|
+
gem 'action_cost', '0.0.1'
|
10
|
+
|
11
|
+
In some applications it's also necessary to add routes in #{Rails.root}/config/routes.rb for the ActionCost dashboard pages:
|
12
|
+
namespace :action_cost do
|
13
|
+
get '/' => 'dashboards#index'
|
14
|
+
match '/ca/:ca' => 'dashboards#ca', :as => 'ca', :constraints => { :ca => /.*/ }
|
15
|
+
end
|
16
|
+
|
17
|
+
== Where is the data stored?
|
18
|
+
In the Rails process memory. The data goes away when the process dies.
|
19
|
+
|
20
|
+
== Where is the data visible?
|
21
|
+
1. Go to http://yourapp/action_cost, it will display a page with a list of controller action and the average number of queries made in each one of them. Click on a controller action to find which tables were queried.
|
22
|
+
|
23
|
+
2. After each controller action completes, an action_cost summary is #{Rails.root}/log/action_cost.log
|
24
|
+
|
25
|
+
|
26
|
+
|
27
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
17
|
+
gem.name = "action_cost"
|
18
|
+
gem.homepage = "http://github.com/plerohellec/action_cost"
|
19
|
+
gem.license = "MIT"
|
20
|
+
gem.summary = %Q{ActionCost measures the cost of controller actions}
|
21
|
+
gem.description = %Q{ActionCost measures the performance of a Rails 3 app controller actions in terms of number of calls to the database and to RecordCache.}
|
22
|
+
gem.email = "philippe@lerohellec.com"
|
23
|
+
gem.authors = ["Philippe Le Rohellec"]
|
24
|
+
# dependencies defined in Gemfile
|
25
|
+
end
|
26
|
+
Jeweler::RubygemsDotOrgTasks.new
|
27
|
+
|
28
|
+
require 'rake/testtask'
|
29
|
+
Rake::TestTask.new(:test) do |test|
|
30
|
+
test.libs << 'lib' << 'test'
|
31
|
+
test.pattern = 'test/**/test_*.rb'
|
32
|
+
test.verbose = true
|
33
|
+
end
|
34
|
+
|
35
|
+
task :default => :test
|
36
|
+
|
37
|
+
require 'rdoc/task'
|
38
|
+
Rake::RDocTask.new do |rdoc|
|
39
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
40
|
+
|
41
|
+
rdoc.rdoc_dir = 'rdoc'
|
42
|
+
rdoc.title = "action_cost #{version}"
|
43
|
+
rdoc.rdoc_files.include('README*')
|
44
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
45
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/action_cost.gemspec
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "action_cost"
|
8
|
+
s.version = "0.0.1"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Philippe Le Rohellec"]
|
12
|
+
s.date = "2013-03-25"
|
13
|
+
s.description = "ActionCost measures the performance of a Rails 3 app controller actions in terms of number of calls to the database and to RecordCache."
|
14
|
+
s.email = "philippe@lerohellec.com"
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE.txt",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
"Gemfile",
|
22
|
+
"Gemfile.lock",
|
23
|
+
"LICENSE.txt",
|
24
|
+
"README.rdoc",
|
25
|
+
"Rakefile",
|
26
|
+
"VERSION",
|
27
|
+
"action_cost.gemspec",
|
28
|
+
"app/controllers/action_cost/dashboards_controller.rb",
|
29
|
+
"app/views/action_cost/dashboards/ca.html.erb",
|
30
|
+
"app/views/action_cost/dashboards/index.html.erb",
|
31
|
+
"app/views/layouts/action_cost.html.erb",
|
32
|
+
"config/routes.rb",
|
33
|
+
"lib/action_cost.rb",
|
34
|
+
"lib/action_cost/engine.rb",
|
35
|
+
"lib/action_cost/extensions/postgresql_adapter.rb",
|
36
|
+
"lib/action_cost/middleware.rb",
|
37
|
+
"lib/action_cost/record_cache/index_hook.rb",
|
38
|
+
"lib/action_cost/record_cache_parser.rb",
|
39
|
+
"lib/action_cost/request_stats.rb",
|
40
|
+
"lib/action_cost/sql_parser.rb",
|
41
|
+
"lib/action_cost/stats_collector.rb",
|
42
|
+
"test/helper.rb",
|
43
|
+
"test/test_action_cost.rb"
|
44
|
+
]
|
45
|
+
s.homepage = "http://github.com/plerohellec/action_cost"
|
46
|
+
s.licenses = ["MIT"]
|
47
|
+
s.require_paths = ["lib"]
|
48
|
+
s.rubygems_version = "1.8.22"
|
49
|
+
s.summary = "ActionCost measures the cost of controller actions"
|
50
|
+
|
51
|
+
if s.respond_to? :specification_version then
|
52
|
+
s.specification_version = 3
|
53
|
+
|
54
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
55
|
+
s.add_development_dependency(%q<shoulda>, [">= 0"])
|
56
|
+
s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
|
57
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0"])
|
58
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.8.4"])
|
59
|
+
else
|
60
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
61
|
+
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
62
|
+
s.add_dependency(%q<bundler>, ["~> 1.0"])
|
63
|
+
s.add_dependency(%q<jeweler>, ["~> 1.8.4"])
|
64
|
+
end
|
65
|
+
else
|
66
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
67
|
+
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
68
|
+
s.add_dependency(%q<bundler>, ["~> 1.0"])
|
69
|
+
s.add_dependency(%q<jeweler>, ["~> 1.8.4"])
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class ActionCost::DashboardsController < ApplicationController
|
2
|
+
|
3
|
+
layout 'action_cost'
|
4
|
+
|
5
|
+
def index
|
6
|
+
respond_to do |format|
|
7
|
+
format.json do
|
8
|
+
render :json => ActionCost::Middleware.accumulated_stats.to_json
|
9
|
+
end
|
10
|
+
format.html do
|
11
|
+
@data = ActionCost::Middleware.accumulated_stats
|
12
|
+
ca_sums = {}
|
13
|
+
@data.each_pair do |key, val|
|
14
|
+
#val[:operations]['select'] = 0 unless val[:operations]['select']
|
15
|
+
ca_sums[key] = val[:operations][:rc]['select'] + val[:operations][:sql]['select']
|
16
|
+
end
|
17
|
+
@sorted_cas = ca_sums.sort_by { |k,v| v/@data[k][:num] }.reverse.map { |v| v[0] }
|
18
|
+
render
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
def ca
|
25
|
+
ca = params[:ca]
|
26
|
+
@data = ActionCost::Middleware.accumulated_stats
|
27
|
+
@num = @data[ca][:num]
|
28
|
+
@tables = @data[ca][:tables]
|
29
|
+
@rc_sorted_table_names = @tables[:rc].sort_by { |k,v| v/@data[ca][:num] }.reverse.map { |v| v[0] }
|
30
|
+
@sql_sorted_table_names = @tables[:sql].sort_by { |k,v| v/@data[ca][:num] }.reverse.map { |v| v[0] }
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
<h1>RecordCache queries</h1>
|
2
|
+
<table width=400px">
|
3
|
+
<tr>
|
4
|
+
<th width=200px>Table</th>
|
5
|
+
<th width=200px>Query count</th>
|
6
|
+
</tr>
|
7
|
+
<% @rc_sorted_table_names.each do |table| %>
|
8
|
+
<tr>
|
9
|
+
<td><%= table %></td>
|
10
|
+
<td><%= @tables[:rc][table]/@num %></td>
|
11
|
+
</tr>
|
12
|
+
<% end %>
|
13
|
+
</table>
|
14
|
+
<br/>
|
15
|
+
|
16
|
+
<h1>SQL queries</h1>
|
17
|
+
<table width="400px">
|
18
|
+
<tr>
|
19
|
+
<th width=200px>Table</th>
|
20
|
+
<th width=200px>Query count</th>
|
21
|
+
</tr>
|
22
|
+
<% @sql_sorted_table_names.each do |table| %>
|
23
|
+
<tr>
|
24
|
+
<td><%= table %></td>
|
25
|
+
<td><%= @tables[:sql][table]/@num %></td>
|
26
|
+
</tr>
|
27
|
+
<% end %>
|
28
|
+
</table>
|
29
|
+
|
30
|
+
<br/>
|
31
|
+
<%= link_to 'All controller actions', action_cost_path %>
|
@@ -0,0 +1,25 @@
|
|
1
|
+
<table width="100%">
|
2
|
+
<tr>
|
3
|
+
<th>Controller Action</th>
|
4
|
+
<th>RecordCache Select</th>
|
5
|
+
<th>SQL Select</th>
|
6
|
+
<th>SQL Update</th>
|
7
|
+
<th>SQL Insert</th>
|
8
|
+
<th>SQL Delete</th>
|
9
|
+
<th>Calls</th>
|
10
|
+
</tr>
|
11
|
+
<% @sorted_cas.each do |ca| %>
|
12
|
+
<% num = @data[ca][:num] %>
|
13
|
+
<tr>
|
14
|
+
<td><%= link_to ca, action_cost_ca_path(URI::escape(ca)) %></td>
|
15
|
+
<td><%= @data[ca][:operations][:rc]['select'] / num %></td>
|
16
|
+
<td><%= @data[ca][:operations][:sql]['select'] / num %></td>
|
17
|
+
<td><%= @data[ca][:operations][:sql]['update'] / num %></td>
|
18
|
+
<td><%= @data[ca][:operations][:sql]['insert'] / num %></td>
|
19
|
+
<td><%= @data[ca][:operations][:sql]['delete'] / num %></td>
|
20
|
+
<td><%= num %></td>
|
21
|
+
</tr>
|
22
|
+
<% end %>
|
23
|
+
</table>
|
24
|
+
|
25
|
+
<br/>
|
@@ -0,0 +1,27 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>ActionCost Dashboard</title>
|
5
|
+
<%= javascript_include_tag :defaults %>
|
6
|
+
<%= csrf_meta_tag %>
|
7
|
+
|
8
|
+
<style>
|
9
|
+
body, td, th {
|
10
|
+
font-family: Verdana, Arial, sans-serif;
|
11
|
+
font-size: 14px }
|
12
|
+
th {
|
13
|
+
text-align: right;
|
14
|
+
font: bold
|
15
|
+
}
|
16
|
+
tr { border-top: 1px solid #444;
|
17
|
+
text-align: right
|
18
|
+
}
|
19
|
+
td { text-align: right }
|
20
|
+
</style>
|
21
|
+
</head>
|
22
|
+
<body>
|
23
|
+
|
24
|
+
<%= yield %>
|
25
|
+
|
26
|
+
</body>
|
27
|
+
</html>
|
data/config/routes.rb
ADDED
data/lib/action_cost.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module ActionCost
|
2
|
+
class Engine < Rails::Engine
|
3
|
+
|
4
|
+
engine_base_dir = File.expand_path("../../..", __FILE__)
|
5
|
+
app_base_dir = File.expand_path("../../../app", __FILE__)
|
6
|
+
lib_base_dir = File.expand_path("../../../lib", __FILE__)
|
7
|
+
|
8
|
+
config.autoload_paths << lib_base_dir
|
9
|
+
|
10
|
+
initializer 'action_cost:record_cache_hook' do
|
11
|
+
if defined?(::RecordCache::Index)=='constant' && ::RecordCache::Index.class==Class
|
12
|
+
require "#{lib_base_dir}/action_cost/record_cache/index_hook"
|
13
|
+
::RecordCache::Index.send(:include, ActionCost::RecordCache::IndexHook)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
initializer "action_cost:instrument_postgresql_adapter" do |app|
|
18
|
+
require "#{lib_base_dir}/action_cost/extensions/postgresql_adapter"
|
19
|
+
end
|
20
|
+
|
21
|
+
initializer "action_cost.add_middleware" do |app|
|
22
|
+
app.middleware.use ActionCost::Middleware
|
23
|
+
$action_cost_data = ActionCost::Data.new
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters # :nodoc:
|
3
|
+
class PostgreSQLAdapter
|
4
|
+
|
5
|
+
def execute_with_action_cost(sql, name='')
|
6
|
+
Rails.logger.debug "execute_with_action_cost: #{sql}"
|
7
|
+
parser = ActionCost::SqlParser.new(sql)
|
8
|
+
ActionCost::Middleware.push_sql_parser(parser)
|
9
|
+
execute_without_action_cost(sql, name)
|
10
|
+
end
|
11
|
+
alias_method_chain :execute, :action_cost
|
12
|
+
|
13
|
+
if Rails.version =~ /^3.[12]\./
|
14
|
+
def exec_query_with_action_cost(sql, name='', binds = [])
|
15
|
+
Rails.logger.debug "exec_query_with_action_cost: #{sql}"
|
16
|
+
parser = ActionCost::SqlParser.new(sql)
|
17
|
+
ActionCost::Middleware.push_sql_parser(parser)
|
18
|
+
exec_query_without_action_cost(sql, name, binds)
|
19
|
+
end
|
20
|
+
alias_method_chain :exec_query, :action_cost
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module ActionCost
|
2
|
+
|
3
|
+
# ActionCost data store
|
4
|
+
class Data
|
5
|
+
attr_reader :request_stats, :stats_collector
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
# Per process storage
|
9
|
+
@stats_collector = ActionCost::StatsCollector.new
|
10
|
+
# Per HTTP request storage
|
11
|
+
@request_stats = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def start_request(env)
|
15
|
+
@request_stats = ActionCost::RequestStats.new(env)
|
16
|
+
end
|
17
|
+
|
18
|
+
def end_request
|
19
|
+
return unless @request_stats
|
20
|
+
@request_stats.close
|
21
|
+
@stats_collector.push(@request_stats)
|
22
|
+
@request_stats = nil
|
23
|
+
end
|
24
|
+
|
25
|
+
def push_sql_parser(parser)
|
26
|
+
return unless @request_stats
|
27
|
+
@request_stats.push(parser)
|
28
|
+
end
|
29
|
+
|
30
|
+
def accumulated_stats
|
31
|
+
return unless @stats_collector
|
32
|
+
@stats_collector.data
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Middleware responsability is to initialize and close RequestStats
|
37
|
+
# object at start and end of HTTP query.
|
38
|
+
class Middleware
|
39
|
+
|
40
|
+
def initialize(app)
|
41
|
+
@app = app
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.action_cost_data
|
45
|
+
$action_cost_data
|
46
|
+
end
|
47
|
+
|
48
|
+
def call(env)
|
49
|
+
self.class.action_cost_data.start_request(env)
|
50
|
+
@app.call(env)
|
51
|
+
ensure
|
52
|
+
self.class.action_cost_data.end_request
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.push_sql_parser(parser)
|
56
|
+
action_cost_data.push_sql_parser(parser)
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.accumulated_stats
|
60
|
+
action_cost_data.accumulated_stats
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module ActionCost
|
2
|
+
module RecordCache
|
3
|
+
module IndexHook
|
4
|
+
|
5
|
+
puts "Loading ActionCost::RecordCache::IndexHook"
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
puts "action_cost including RecordCache::IndexHook"
|
9
|
+
base.class_eval do
|
10
|
+
alias_method_chain :get_records, :action_cost
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def get_records_with_action_cost(keys)
|
15
|
+
Rails.logger.debug "get_records_with_action_cost: keys=#{keys.inspect}"
|
16
|
+
parser = ActionCost::RecordCacheParser.new(table_name)
|
17
|
+
ActionCost::Middleware.push_sql_parser(parser)
|
18
|
+
get_records_without_action_cost(keys)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module ActionCost
|
2
|
+
class RecordCacheParser
|
3
|
+
attr_reader :table_name, :operation, :join_tables, :invalid
|
4
|
+
|
5
|
+
VALID_OPERATIONS = %w{ select insert update delete }
|
6
|
+
|
7
|
+
def initialize(table_name)
|
8
|
+
@invalid = false
|
9
|
+
@table_name = table_name
|
10
|
+
@join_tables = []
|
11
|
+
@operation = 'select'
|
12
|
+
end
|
13
|
+
|
14
|
+
def parse
|
15
|
+
return true
|
16
|
+
end
|
17
|
+
|
18
|
+
def log
|
19
|
+
if @invalid
|
20
|
+
Rails.logger.debug "action_cost: record cache non parsable query"
|
21
|
+
else
|
22
|
+
Rails.logger.debug "action_cost: record_cache operation=#{@operation} table_name=#{@table_name} " +
|
23
|
+
"join_tables=#{@join_tables.inspect}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module ActionCost
|
2
|
+
class RequestStats
|
3
|
+
|
4
|
+
attr_reader :controller_name, :action_name
|
5
|
+
attr_reader :operation_stats, :table_stats, :join_stats
|
6
|
+
|
7
|
+
def initialize(env)
|
8
|
+
request = Rails.application.routes.recognize_path(env['REQUEST_URI'])
|
9
|
+
|
10
|
+
@controller_name = request[:controller]
|
11
|
+
@action_name = request[:action]
|
12
|
+
|
13
|
+
@operation_stats = { :sql => {}, :rc => {} }
|
14
|
+
ActionCost::SqlParser::VALID_OPERATIONS.each do |op|
|
15
|
+
@operation_stats[:sql][op] = 0
|
16
|
+
@operation_stats[:rc][op] = 0
|
17
|
+
end
|
18
|
+
|
19
|
+
@table_stats = { :sql => {}, :rc => {} }
|
20
|
+
@join_stats = { :sql => {}, :rc => {} }
|
21
|
+
|
22
|
+
action_cost_logfile = File.open(Rails.root.join("log", 'action_cost.log'), 'a')
|
23
|
+
action_cost_logfile.sync = true
|
24
|
+
@logger = Logger.new(action_cost_logfile)
|
25
|
+
@logger.level = Logger::DEBUG
|
26
|
+
end
|
27
|
+
|
28
|
+
def push(parser)
|
29
|
+
return unless parser.parse
|
30
|
+
parser.log
|
31
|
+
|
32
|
+
return if parser.invalid
|
33
|
+
|
34
|
+
case parser.class.to_s
|
35
|
+
when 'ActionCost::SqlParser' then query_type = :sql
|
36
|
+
when 'ActionCost::RecordCacheParser' then query_type = :rc
|
37
|
+
end
|
38
|
+
|
39
|
+
increment_item(@table_stats, query_type, parser.table_name)
|
40
|
+
increment_item(@operation_stats, query_type, parser.operation)
|
41
|
+
parser.join_tables.each do |table|
|
42
|
+
increment_item(@join_stats, query_type, join_string(parser.table_name, table))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def close
|
47
|
+
log
|
48
|
+
end
|
49
|
+
|
50
|
+
def log
|
51
|
+
@logger.debug ""
|
52
|
+
@logger.debug "=== ActionCost: #{@controller_name}##{@action_name}"
|
53
|
+
log_by_query_type(:rc)
|
54
|
+
log_by_query_type(:sql)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def increment_item(hash, query_type, key)
|
60
|
+
# Rails.logger.debug "increment_item: hash=#{hash.inspect}"
|
61
|
+
# Rails.logger.debug "increment_item: query_type=#{query_type} key=#{key}"
|
62
|
+
if hash[query_type].has_key?(key)
|
63
|
+
hash[query_type][key] += 1
|
64
|
+
else
|
65
|
+
hash[query_type][key] = 1
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def join_string(t1, t2)
|
70
|
+
"#{t1}|#{t2}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def log_by_query_type(query_type)
|
74
|
+
@logger.debug " #{query_type.to_s.upcase}:"
|
75
|
+
@logger.debug " Operations:"
|
76
|
+
ActionCost::SqlParser::VALID_OPERATIONS.each do |op|
|
77
|
+
log_count(@operation_stats, query_type, op)
|
78
|
+
end
|
79
|
+
@logger.debug " Tables:"
|
80
|
+
@table_stats[query_type].each_key do |table|
|
81
|
+
log_count(@table_stats, query_type, table)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def log_count(hash, query_type, item)
|
86
|
+
val = hash[query_type][item]
|
87
|
+
@logger.debug " #{item}: #{hash[query_type][item]}" if val>0
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module ActionCost
|
2
|
+
class SqlParser
|
3
|
+
attr_reader :table_name, :operation, :join_tables, :invalid
|
4
|
+
|
5
|
+
VALID_OPERATIONS = %w{ select insert update delete }
|
6
|
+
|
7
|
+
def initialize(sql)
|
8
|
+
@invalid = false
|
9
|
+
@sql = sql
|
10
|
+
@join_tables = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse
|
14
|
+
if @sql =~ /^\s*(\w+)/
|
15
|
+
op = $1.downcase
|
16
|
+
unless VALID_OPERATIONS.include?(op)
|
17
|
+
Rails.logger.error "action_cost: unknown operation [#{op}]"
|
18
|
+
@invalid = true
|
19
|
+
return false
|
20
|
+
end
|
21
|
+
@operation = op
|
22
|
+
else
|
23
|
+
Rails.logger.error "action_cost: could not parse [#{@sql}]"
|
24
|
+
@invalid = true
|
25
|
+
return false
|
26
|
+
end
|
27
|
+
|
28
|
+
case @operation
|
29
|
+
when 'select' then parse_select
|
30
|
+
when 'insert' then parse_insert
|
31
|
+
when 'update' then parse_update
|
32
|
+
when 'delete' then parse_delete
|
33
|
+
end
|
34
|
+
|
35
|
+
return !@invalid
|
36
|
+
end
|
37
|
+
|
38
|
+
def log
|
39
|
+
if @invalid
|
40
|
+
Rails.logger.debug "action_cost: non parsable query"
|
41
|
+
else
|
42
|
+
Rails.logger.debug "action_cost: operation=#{@operation} table_name=#{@table_name} " +
|
43
|
+
"join_tables=#{@join_tables.inspect}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def parse_select
|
50
|
+
if @sql =~ /from "?(\w+)"?\b/i
|
51
|
+
@table_name = $1.downcase
|
52
|
+
else
|
53
|
+
@invalid = true
|
54
|
+
return
|
55
|
+
end
|
56
|
+
|
57
|
+
@sql.scan(/join "?(\w+)"?\b/i) do |arr|
|
58
|
+
arr.each do |t|
|
59
|
+
@join_tables << t.downcase
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def parse_insert
|
65
|
+
if @sql =~ /insert into "?(\w+)"?\b/i
|
66
|
+
@table_name = $1.downcase
|
67
|
+
else
|
68
|
+
@invalid = true
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def parse_update
|
73
|
+
if @sql =~ /^update "?(\w+)"?\b/i
|
74
|
+
@table_name = $1.downcase
|
75
|
+
else
|
76
|
+
@invalid = true
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def parse_delete
|
81
|
+
if @sql =~ /^delete from "?(\w+)"?\b/
|
82
|
+
@table_name = $1.downcase
|
83
|
+
else
|
84
|
+
@invalid = true
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module ActionCost
|
2
|
+
class StatsCollector
|
3
|
+
def initialize
|
4
|
+
@stats = {}
|
5
|
+
end
|
6
|
+
|
7
|
+
def push(request)
|
8
|
+
ca = controller_action_string(request)
|
9
|
+
add_request(ca, request.operation_stats, request.table_stats)
|
10
|
+
end
|
11
|
+
|
12
|
+
def log
|
13
|
+
pp @stats
|
14
|
+
end
|
15
|
+
|
16
|
+
def data
|
17
|
+
@stats
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def add_request(ca, operations, tables)
|
22
|
+
if @stats[ca]
|
23
|
+
@stats[ca][:num] += 1
|
24
|
+
else
|
25
|
+
@stats[ca] = { :num => 1, :operations => { :sql => {}, :rc => {} },
|
26
|
+
:tables => { :sql => {}, :rc => {} } }
|
27
|
+
end
|
28
|
+
|
29
|
+
ActionCost::SqlParser::VALID_OPERATIONS.each do |op|
|
30
|
+
increment_item(@stats[ca][:operations][:sql], op, operations[:sql][op])
|
31
|
+
increment_item(@stats[ca][:operations][:rc], op, operations[:rc][op])
|
32
|
+
end
|
33
|
+
|
34
|
+
tables[:sql].each_key do |table|
|
35
|
+
increment_item(@stats[ca][:tables][:sql], table, tables[:sql][table])
|
36
|
+
end
|
37
|
+
tables[:rc].each_key do |table|
|
38
|
+
increment_item(@stats[ca][:tables][:rc], table, tables[:rc][table])
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def increment_item(hash, item, val)
|
43
|
+
if hash[item]
|
44
|
+
hash[item] += val
|
45
|
+
else
|
46
|
+
hash[item] = val
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def controller_action_string(request)
|
51
|
+
"#{request.controller_name}/#{request.action_name}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'test/unit'
|
11
|
+
require 'shoulda'
|
12
|
+
|
13
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
14
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
15
|
+
require 'action_cost'
|
16
|
+
|
17
|
+
class Test::Unit::TestCase
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: action_cost
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Philippe Le Rohellec
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-03-25 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: shoulda
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rdoc
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '3.12'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '3.12'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: bundler
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '1.0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: jeweler
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 1.8.4
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 1.8.4
|
78
|
+
description: ActionCost measures the performance of a Rails 3 app controller actions
|
79
|
+
in terms of number of calls to the database and to RecordCache.
|
80
|
+
email: philippe@lerohellec.com
|
81
|
+
executables: []
|
82
|
+
extensions: []
|
83
|
+
extra_rdoc_files:
|
84
|
+
- LICENSE.txt
|
85
|
+
- README.rdoc
|
86
|
+
files:
|
87
|
+
- .document
|
88
|
+
- Gemfile
|
89
|
+
- Gemfile.lock
|
90
|
+
- LICENSE.txt
|
91
|
+
- README.rdoc
|
92
|
+
- Rakefile
|
93
|
+
- VERSION
|
94
|
+
- action_cost.gemspec
|
95
|
+
- app/controllers/action_cost/dashboards_controller.rb
|
96
|
+
- app/views/action_cost/dashboards/ca.html.erb
|
97
|
+
- app/views/action_cost/dashboards/index.html.erb
|
98
|
+
- app/views/layouts/action_cost.html.erb
|
99
|
+
- config/routes.rb
|
100
|
+
- lib/action_cost.rb
|
101
|
+
- lib/action_cost/engine.rb
|
102
|
+
- lib/action_cost/extensions/postgresql_adapter.rb
|
103
|
+
- lib/action_cost/middleware.rb
|
104
|
+
- lib/action_cost/record_cache/index_hook.rb
|
105
|
+
- lib/action_cost/record_cache_parser.rb
|
106
|
+
- lib/action_cost/request_stats.rb
|
107
|
+
- lib/action_cost/sql_parser.rb
|
108
|
+
- lib/action_cost/stats_collector.rb
|
109
|
+
- test/helper.rb
|
110
|
+
- test/test_action_cost.rb
|
111
|
+
homepage: http://github.com/plerohellec/action_cost
|
112
|
+
licenses:
|
113
|
+
- MIT
|
114
|
+
post_install_message:
|
115
|
+
rdoc_options: []
|
116
|
+
require_paths:
|
117
|
+
- lib
|
118
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
119
|
+
none: false
|
120
|
+
requirements:
|
121
|
+
- - ! '>='
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
segments:
|
125
|
+
- 0
|
126
|
+
hash: 2458627961303877027
|
127
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
128
|
+
none: false
|
129
|
+
requirements:
|
130
|
+
- - ! '>='
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
133
|
+
requirements: []
|
134
|
+
rubyforge_project:
|
135
|
+
rubygems_version: 1.8.22
|
136
|
+
signing_key:
|
137
|
+
specification_version: 3
|
138
|
+
summary: ActionCost measures the cost of controller actions
|
139
|
+
test_files: []
|