miniprofiler 0.1.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/.gitignore +4 -0
  2. data/CHANGELOG +32 -0
  3. data/Gemfile +15 -0
  4. data/Gemfile.lock +64 -0
  5. data/README.md +110 -0
  6. data/Rakefile +40 -0
  7. data/autotest/discover.rb +2 -0
  8. data/lib/mini_profiler/body_add_proxy.rb +45 -0
  9. data/lib/mini_profiler/client_timer_struct.rb +76 -0
  10. data/lib/mini_profiler/config.rb +52 -0
  11. data/lib/mini_profiler/context.rb +10 -0
  12. data/lib/mini_profiler/page_timer_struct.rb +53 -0
  13. data/lib/mini_profiler/profiler.rb +405 -0
  14. data/lib/mini_profiler/profiling_methods.rb +73 -0
  15. data/lib/mini_profiler/request_timer_struct.rb +96 -0
  16. data/lib/mini_profiler/sql_timer_struct.rb +48 -0
  17. data/lib/mini_profiler/storage/abstract_store.rb +27 -0
  18. data/lib/mini_profiler/storage/file_store.rb +108 -0
  19. data/lib/mini_profiler/storage/memory_store.rb +68 -0
  20. data/lib/mini_profiler/storage/redis_store.rb +44 -0
  21. data/lib/mini_profiler/timer_struct.rb +31 -0
  22. data/lib/mini_profiler_rails/railtie.rb +84 -0
  23. data/lib/patches/sql_patches.rb +185 -0
  24. data/lib/rack-mini-profiler.rb +6 -0
  25. data/rack-mini-profiler.gemspec +23 -0
  26. data/spec/components/body_add_proxy_spec.rb +90 -0
  27. data/spec/components/client_timer_struct_spec.rb +165 -0
  28. data/spec/components/file_store_spec.rb +47 -0
  29. data/spec/components/memory_store_spec.rb +40 -0
  30. data/spec/components/page_timer_struct_spec.rb +33 -0
  31. data/spec/components/profiler_spec.rb +126 -0
  32. data/spec/components/redis_store_spec.rb +44 -0
  33. data/spec/components/request_timer_struct_spec.rb +148 -0
  34. data/spec/components/sql_timer_struct_spec.rb +63 -0
  35. data/spec/components/timer_struct_spec.rb +47 -0
  36. data/spec/fixtures/simple_client_request.yml +17 -0
  37. data/spec/fixtures/weird_client_request.yml +13 -0
  38. data/spec/integration/mini_profiler_spec.rb +142 -0
  39. data/spec/spec_helper.rb +31 -0
  40. data/spec/support/expand_each_to_matcher.rb +8 -0
  41. data/test_old/config.ru +41 -0
  42. metadata +155 -0
@@ -0,0 +1,108 @@
1
+ module Rack
2
+ class MiniProfiler
3
+ class FileStore < AbstractStore
4
+
5
+ class FileCache
6
+ def initialize(path, prefix)
7
+ @path = path
8
+ @prefix = prefix
9
+ end
10
+
11
+ def [](key)
12
+ begin
13
+ data = ::File.open(path(key),"rb") {|f| f.read}
14
+ return Marshal.load data
15
+ rescue => e
16
+ return nil
17
+ end
18
+ end
19
+
20
+ def []=(key,val)
21
+ ::File.open(path(key), "wb+") {|f| f.write Marshal.dump(val)}
22
+ end
23
+
24
+ private
25
+ def path(key)
26
+ @path + "/" + @prefix + "_" + key
27
+ end
28
+ end
29
+
30
+ EXPIRE_TIMER_CACHE = 3600 * 24
31
+
32
+ def initialize(args)
33
+ @path = args[:path]
34
+ raise ArgumentError.new :path unless @path
35
+ @timer_struct_cache = FileCache.new(@path, "mp_timers")
36
+ @timer_struct_lock = Mutex.new
37
+ @user_view_cache = FileCache.new(@path, "mp_views")
38
+ @user_view_lock = Mutex.new
39
+
40
+ me = self
41
+ Thread.new do
42
+ while true do
43
+ me.cleanup_cache if MiniProfiler.instance
44
+ sleep(3600)
45
+ end
46
+ end
47
+ end
48
+
49
+ def save(page_struct)
50
+ @timer_struct_lock.synchronize {
51
+ @timer_struct_cache[page_struct['Id']] = page_struct
52
+ }
53
+ end
54
+
55
+ def load(id)
56
+ @timer_struct_lock.synchronize {
57
+ @timer_struct_cache[id]
58
+ }
59
+ end
60
+
61
+ def set_unviewed(user, id)
62
+ @user_view_lock.synchronize {
63
+ current = @user_view_cache[user]
64
+ current = [] unless Array === current
65
+ current << id
66
+ @user_view_cache[user] = current.uniq
67
+ }
68
+ end
69
+
70
+ def set_viewed(user, id)
71
+ @user_view_lock.synchronize {
72
+ @user_view_cache[user] ||= []
73
+ current = @user_view_cache[user]
74
+ current = [] unless Array === current
75
+ current.delete(id)
76
+ @user_view_cache[user] = current.uniq
77
+ }
78
+ end
79
+
80
+ def get_unviewed_ids(user)
81
+ @user_view_lock.synchronize {
82
+ @user_view_cache[user]
83
+ }
84
+ end
85
+
86
+
87
+ private
88
+
89
+
90
+ def cleanup_cache
91
+ files = Dir.entries(@path)
92
+ @timer_struct_lock.synchronize {
93
+ files.each do |f|
94
+ f = @path + '/' + f
95
+ File.delete f if f =~ /^mp_timers/ and (Time.now - File.mtime(f)) > EXPIRE_TIMER_CACHE
96
+ end
97
+ }
98
+ @user_view_lock.synchronize {
99
+ files.each do |f|
100
+ f = @path + '/' + f
101
+ File.delete f if f =~ /^mp_views/ and (Time.now - File.mtime(f)) > EXPIRE_TIMER_CACHE
102
+ end
103
+ }
104
+ end
105
+
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,68 @@
1
+ module Rack
2
+ class MiniProfiler
3
+ class MemoryStore < AbstractStore
4
+
5
+ EXPIRE_TIMER_CACHE = 3600 * 24
6
+
7
+ def initialize(args)
8
+ @timer_struct_lock = Mutex.new
9
+ @timer_struct_cache = {}
10
+ @user_view_lock = Mutex.new
11
+ @user_view_cache = {}
12
+
13
+ # TODO: fix it to use weak ref, trouble is may be broken in 1.9 so need to use the 'ref' gem
14
+ me = self
15
+ Thread.new do
16
+ while true do
17
+ me.cleanup_cache
18
+ sleep(3600)
19
+ end
20
+ end
21
+ end
22
+
23
+ def save(page_struct)
24
+ @timer_struct_lock.synchronize {
25
+ @timer_struct_cache[page_struct['Id']] = page_struct
26
+ }
27
+ end
28
+
29
+ def load(id)
30
+ @timer_struct_lock.synchronize {
31
+ @timer_struct_cache[id]
32
+ }
33
+ end
34
+
35
+ def set_unviewed(user, id)
36
+ @user_view_lock.synchronize {
37
+ @user_view_cache[user] ||= []
38
+ @user_view_cache[user] << id
39
+ }
40
+ end
41
+
42
+ def set_viewed(user, id)
43
+ @user_view_lock.synchronize {
44
+ @user_view_cache[user] ||= []
45
+ @user_view_cache[user].delete(id)
46
+ }
47
+ end
48
+
49
+ def get_unviewed_ids(user)
50
+ @user_view_lock.synchronize {
51
+ @user_view_cache[user]
52
+ }
53
+ end
54
+
55
+
56
+ private
57
+
58
+
59
+ def cleanup_cache
60
+ expire_older_than = ((Time.now.to_f - MiniProfiler::MemoryStore::EXPIRE_TIMER_CACHE) * 1000).to_i
61
+ @timer_struct_lock.synchronize {
62
+ @timer_struct_cache.delete_if { |k, v| v['Root']['StartMilliseconds'] < expire_older_than }
63
+ }
64
+ end
65
+
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,44 @@
1
+ module Rack
2
+ class MiniProfiler
3
+ class RedisStore < AbstractStore
4
+
5
+ EXPIRE_SECONDS = 60 * 60 * 24
6
+
7
+ def initialize(args)
8
+ args ||= {}
9
+ @prefix = args[:prefix] || 'MPRedisStore'
10
+ end
11
+
12
+ def save(page_struct)
13
+ redis.setex "#{@prefix}#{page_struct['Id']}", EXPIRE_SECONDS, Marshal::dump(page_struct)
14
+ end
15
+
16
+ def load(id)
17
+ raw = redis.get "#{@prefix}#{id}"
18
+ if raw
19
+ Marshal::load raw
20
+ end
21
+ end
22
+
23
+ def set_unviewed(user, id)
24
+ redis.sadd "#{@prefix}-#{user}-v", id
25
+ end
26
+
27
+ def set_viewed(user, id)
28
+ redis.srem "#{@prefix}-#{user}-v", id
29
+ end
30
+
31
+ def get_unviewed_ids(user)
32
+ redis.smembers "#{@prefix}-#{user}-v"
33
+ end
34
+
35
+ private
36
+
37
+ def redis
38
+ require 'redis' unless defined? Redis
39
+ Redis.new
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,31 @@
1
+ module Rack
2
+ class MiniProfiler
3
+
4
+ # A base class for timing structures
5
+ class TimerStruct
6
+
7
+ def initialize(attrs={})
8
+ @attributes = attrs
9
+ end
10
+
11
+ def attributes
12
+ @attributes ||= {}
13
+ end
14
+
15
+ def [](name)
16
+ attributes[name]
17
+ end
18
+
19
+ def []=(name, val)
20
+ attributes[name] = val
21
+ self
22
+ end
23
+
24
+ def to_json(*a)
25
+ ::JSON.generate(@attributes, a[0])
26
+ end
27
+
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,84 @@
1
+ require 'fileutils'
2
+
3
+ module MiniProfilerRails
4
+
5
+ class Railtie < ::Rails::Railtie
6
+
7
+ initializer "rack_mini_profiler.configure_rails_initialization" do |app|
8
+ c = Rack::MiniProfiler.config
9
+
10
+ # By default, only show the MiniProfiler in development mode, in production allow profiling if post_authorize_cb is set
11
+ c.pre_authorize_cb = lambda { |env|
12
+ Rails.env.development? || Rails.env.production?
13
+ }
14
+
15
+ if Rails.env.development?
16
+ c.skip_paths ||= []
17
+ c.skip_paths << "/assets/"
18
+ c.skip_schema_queries = true
19
+ end
20
+
21
+ if Rails.env.production?
22
+ c.authorization_mode = :whitelist
23
+ end
24
+
25
+ # The file store is just so much less flaky
26
+ tmp = Rails.root.to_s + "/tmp/miniprofiler"
27
+ FileUtils.mkdir_p(tmp) unless File.exists?(tmp)
28
+
29
+ c.storage_options = {:path => tmp}
30
+ c.storage = Rack::MiniProfiler::FileStore
31
+
32
+ # Quiet the SQL stack traces
33
+ c.backtrace_remove = Rails.root.to_s + "/"
34
+ c.backtrace_filter = /^\/?(app|config|lib|test)/
35
+ c.skip_schema_queries = Rails.env != 'production'
36
+
37
+ # Install the Middleware
38
+ app.middleware.insert(0, Rack::MiniProfiler)
39
+
40
+ # Attach to various Rails methods
41
+ ::Rack::MiniProfiler.profile_method(ActionController::Base, :process) {|action| "Executing action: #{action}"}
42
+ ::Rack::MiniProfiler.profile_method(ActionView::Template, :render) {|x,y| "Rendering: #{@virtual_path}"}
43
+
44
+ end
45
+
46
+ # TODO: Implement something better here
47
+ # config.after_initialize do
48
+ #
49
+ # class ::ActionView::Helpers::AssetTagHelper::JavascriptIncludeTag
50
+ # alias_method :asset_tag_orig, :asset_tag
51
+ # def asset_tag(source,options)
52
+ # current = Rack::MiniProfiler.current
53
+ # return asset_tag_orig(source,options) unless current
54
+ # wrapped = ""
55
+ # unless current.mpt_init
56
+ # current.mpt_init = true
57
+ # wrapped << Rack::MiniProfiler::ClientTimerStruct.init_instrumentation
58
+ # end
59
+ # name = source.split('/')[-1]
60
+ # wrapped << Rack::MiniProfiler::ClientTimerStruct.instrument(name, asset_tag_orig(source,options)).html_safe
61
+ # wrapped
62
+ # end
63
+ # end
64
+
65
+ # class ::ActionView::Helpers::AssetTagHelper::StylesheetIncludeTag
66
+ # alias_method :asset_tag_orig, :asset_tag
67
+ # def asset_tag(source,options)
68
+ # current = Rack::MiniProfiler.current
69
+ # return asset_tag_orig(source,options) unless current
70
+ # wrapped = ""
71
+ # unless current.mpt_init
72
+ # current.mpt_init = true
73
+ # wrapped << Rack::MiniProfiler::ClientTimerStruct.init_instrumentation
74
+ # end
75
+ # name = source.split('/')[-1]
76
+ # wrapped << Rack::MiniProfiler::ClientTimerStruct.instrument(name, asset_tag_orig(source,options)).html_safe
77
+ # wrapped
78
+ # end
79
+ # end
80
+
81
+ # end
82
+
83
+ end
84
+ end
@@ -0,0 +1,185 @@
1
+ class SqlPatches
2
+
3
+ def self.patched?
4
+ @patched
5
+ end
6
+
7
+ def self.patched=(val)
8
+ @patched = val
9
+ end
10
+
11
+ def self.class_exists?(name)
12
+ eval(name + ".class").to_s.eql?('Class')
13
+ rescue NameError
14
+ false
15
+ end
16
+
17
+ def self.module_exists?(name)
18
+ eval(name + ".class").to_s.eql?('Module')
19
+ rescue NameError
20
+ false
21
+ end
22
+ end
23
+
24
+ # The best kind of instrumentation is in the actual db provider, however we don't want to double instrument
25
+ if SqlPatches.class_exists? "Mysql2::Client"
26
+
27
+ class Mysql2::Result
28
+ alias_method :each_without_profiling, :each
29
+ def each(*args, &blk)
30
+ return each_without_profiling(*args, &blk) unless @miniprofiler_sql_id
31
+
32
+ start = Time.now
33
+ result = each_without_profiling(*args,&blk)
34
+ elapsed_time = ((Time.now - start).to_f * 1000).round(1)
35
+
36
+ @miniprofiler_sql_id.report_reader_duration(elapsed_time)
37
+ result
38
+ end
39
+ end
40
+
41
+ class Mysql2::Client
42
+ alias_method :query_without_profiling, :query
43
+ def query(*args,&blk)
44
+ current = ::Rack::MiniProfiler.current
45
+ return query_without_profiling(*args,&blk) unless current
46
+
47
+ start = Time.now
48
+ result = query_without_profiling(*args,&blk)
49
+ elapsed_time = ((Time.now - start).to_f * 1000).round(1)
50
+ result.instance_variable_set("@miniprofiler_sql_id", ::Rack::MiniProfiler.record_sql(args[0], elapsed_time))
51
+
52
+ result
53
+
54
+ end
55
+ end
56
+
57
+ SqlPatches.patched = true
58
+ end
59
+
60
+
61
+ # PG patches, keep in mind exec and async_exec have a exec{|r| } semantics that is yet to be implemented
62
+ if SqlPatches.class_exists? "PG::Result"
63
+
64
+ class PG::Result
65
+ alias_method :each_without_profiling, :each
66
+ alias_method :values_without_profiling, :values
67
+
68
+ def values(*args, &blk)
69
+ return values_without_profiling(*args, &blk) unless @miniprofiler_sql_id
70
+
71
+ start = Time.now
72
+ result = values_without_profiling(*args,&blk)
73
+ elapsed_time = ((Time.now - start).to_f * 1000).round(1)
74
+
75
+ @miniprofiler_sql_id.report_reader_duration(elapsed_time)
76
+ result
77
+ end
78
+
79
+ def each(*args, &blk)
80
+ return each_without_profiling(*args, &blk) unless @miniprofiler_sql_id
81
+
82
+ start = Time.now
83
+ result = each_without_profiling(*args,&blk)
84
+ elapsed_time = ((Time.now - start).to_f * 1000).round(1)
85
+
86
+ @miniprofiler_sql_id.report_reader_duration(elapsed_time)
87
+ result
88
+ end
89
+ end
90
+
91
+ class PG::Connection
92
+ alias_method :exec_without_profiling, :exec
93
+ alias_method :async_exec_without_profiling, :async_exec
94
+
95
+ def exec(*args,&blk)
96
+ current = ::Rack::MiniProfiler.current
97
+ return exec_without_profiling(*args,&blk) unless current
98
+
99
+ start = Time.now
100
+ result = exec_without_profiling(*args,&blk)
101
+ elapsed_time = ((Time.now - start).to_f * 1000).round(1)
102
+ result.instance_variable_set("@miniprofiler_sql_id", ::Rack::MiniProfiler.record_sql(args[0], elapsed_time))
103
+
104
+ result
105
+ end
106
+
107
+ def async_exec(*args,&blk)
108
+ current = ::Rack::MiniProfiler.current
109
+ return exec_without_profiling(*args,&blk) unless current
110
+
111
+ start = Time.now
112
+ result = exec_without_profiling(*args,&blk)
113
+ elapsed_time = ((Time.now - start).to_f * 1000).round(1)
114
+ result.instance_variable_set("@miniprofiler_sql_id", ::Rack::MiniProfiler.record_sql(args[0], elapsed_time))
115
+
116
+ result
117
+ end
118
+
119
+ alias_method :query, :exec
120
+ end
121
+
122
+ SqlPatches.patched = true
123
+ end
124
+
125
+
126
+
127
+ # Fallback for sequel
128
+ if SqlPatches.class_exists?("Sequel::Database") && !SqlPatches.patched?
129
+ module Sequel
130
+ class Database
131
+ alias_method :log_duration_original, :log_duration
132
+ def log_duration(duration, message)
133
+ ::Rack::MiniProfiler.record_sql(message, duration)
134
+ log_duration_original(duration, message)
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+
141
+ ## based off https://github.com/newrelic/rpm/blob/master/lib/new_relic/agent/instrumentation/active_record.rb
142
+ ## fallback for alls sorts of weird dbs
143
+ if SqlPatches.module_exists?('ActiveRecord')
144
+ module Rack
145
+ class MiniProfiler
146
+ module ActiveRecordInstrumentation
147
+ def self.included(instrumented_class)
148
+ instrumented_class.class_eval do
149
+ unless instrumented_class.method_defined?(:log_without_miniprofiler)
150
+ alias_method :log_without_miniprofiler, :log
151
+ alias_method :log, :log_with_miniprofiler
152
+ protected :log
153
+ end
154
+ end
155
+ end
156
+
157
+ def log_with_miniprofiler(*args, &block)
158
+ current = ::Rack::MiniProfiler.current
159
+ return log_without_miniprofiler(*args, &block) unless current
160
+
161
+ sql, name, binds = args
162
+ t0 = Time.now
163
+ rval = log_without_miniprofiler(*args, &block)
164
+
165
+ # Don't log schema queries if the option is set
166
+ return rval if Rack::MiniProfiler.config.skip_schema_queries and name =~ /SCHEMA/
167
+
168
+ elapsed_time = ((Time.now - t0).to_f * 1000).round(1)
169
+ Rack::MiniProfiler.record_sql(sql, elapsed_time)
170
+ rval
171
+ end
172
+ end
173
+ end
174
+
175
+ def self.insert_instrumentation
176
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.module_eval do
177
+ include ::Rack::MiniProfiler::ActiveRecordInstrumentation
178
+ end
179
+ end
180
+
181
+ if defined?(::Rails) && !SqlPatches.patched?
182
+ insert_instrumentation
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,6 @@
1
+ require File.expand_path('mini_profiler/profiler', File.dirname(__FILE__) )
2
+ require File.expand_path('patches/sql_patches', File.dirname(__FILE__) )
3
+
4
+ if defined?(::Rails) && ::Rails::VERSION::MAJOR.to_i == 3
5
+ require File.expand_path('mini_profiler_rails/railtie', File.dirname(__FILE__) )
6
+ end
@@ -0,0 +1,23 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "miniprofiler"
3
+ s.version = "0.1.7.1"
4
+ s.summary = "Profiles loading speed for rack applications."
5
+ s.authors = ["Aleks Totic","Sam Saffron", "Robin Ward"]
6
+ s.description = "Page loading speed displayed on every page. Optimize while you develop, performance is a feature."
7
+ s.email = "sam.saffron@gmail.com"
8
+ s.homepage = "http://miniprofiler.com"
9
+ s.files = `git ls-files`.split("\n")
10
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
11
+ s.extra_rdoc_files = [
12
+ "README.md",
13
+ "CHANGELOG"
14
+ ]
15
+ s.add_runtime_dependency 'rack', '>= 1.1.3'
16
+ if RUBY_VERSION < "1.9"
17
+ s.add_runtime_dependency 'json', '>= 1.6'
18
+ end
19
+
20
+ s.add_development_dependency 'rake'
21
+ s.add_development_dependency 'rack-test'
22
+ s.add_development_dependency 'activerecord', '~> 3.0'
23
+ end
@@ -0,0 +1,90 @@
1
+ require 'spec_helper'
2
+ require 'mini_profiler/body_add_proxy'
3
+
4
+ describe Rack::MiniProfiler::BodyAddProxy do
5
+
6
+ context 'body as an array' do
7
+
8
+ before do
9
+ @proxy = Rack::MiniProfiler::BodyAddProxy.new(%w(a b c), 'd')
10
+ end
11
+
12
+ it 'contains the appended value' do
13
+ @proxy.should expand_each_to %w(a b c d)
14
+ end
15
+
16
+ it 'has a correct value in to_str' do
17
+ @proxy.to_str.should == "abcd"
18
+ end
19
+
20
+ describe 'delegation' do
21
+ it 'delegates respond to <<' do
22
+ @proxy.respond_to?('<<').should be_true
23
+ end
24
+
25
+ it 'delegates respond to first' do
26
+ @proxy.respond_to?(:first).should be_true
27
+ end
28
+
29
+ it 'delegates method_missing' do
30
+ @proxy.first.should == 'a'
31
+ end
32
+ end
33
+
34
+ end
35
+
36
+ context 'body as a custom object' do
37
+
38
+ # A class and a super class to help us test appending to a custom object, such as
39
+ # Rails' ActionDispatch::Response
40
+ class Band
41
+ def style
42
+ 'rock'
43
+ end
44
+ end
45
+
46
+ class Beatles < Band
47
+ def each
48
+ yield 'john'
49
+ yield 'paul'
50
+ yield 'george'
51
+ end
52
+
53
+ def fake_method; nil; end
54
+
55
+ def method_missing(*args, &block)
56
+ 'yoko'
57
+ end
58
+ end
59
+
60
+ before do
61
+ @proxy = Rack::MiniProfiler::BodyAddProxy.new(Beatles.new, 'ringo')
62
+ end
63
+
64
+ it 'contains the appended value' do
65
+ @proxy.should expand_each_to %w(john paul george ringo)
66
+ end
67
+
68
+ it 'has a correct value in to_str' do
69
+ @proxy.to_str.should == "johnpaulgeorgeringo"
70
+ end
71
+
72
+ describe 'delegation' do
73
+ it 'delegates respond to fake_method' do
74
+ @proxy.respond_to?(:fake_method).should be_true
75
+ end
76
+
77
+ it 'delegates respond to a super class' do
78
+ @proxy.respond_to?(:style).should be_true
79
+ end
80
+
81
+ it 'delegates method_missing' do
82
+ @proxy.doesnt_exist.should == 'yoko'
83
+ end
84
+
85
+ end
86
+
87
+ end
88
+
89
+
90
+ end