action_cost 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
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
@@ -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
@@ -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.
@@ -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
+
@@ -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
@@ -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>
@@ -0,0 +1,9 @@
1
+ Rails.application.routes.draw do
2
+
3
+ namespace :action_cost do
4
+ get '/' => 'dashboards#index'
5
+ match '/ca/:ca' => 'dashboards#ca', :as => 'ca', :constraints => { :ca => /.*/ }
6
+ #resources :dashboards
7
+ end
8
+
9
+ end
@@ -0,0 +1,2 @@
1
+ require 'action_cost/engine' if defined?(Rails)
2
+
@@ -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
@@ -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
@@ -0,0 +1,7 @@
1
+ require 'helper'
2
+
3
+ class TestActionCost < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ 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: []