profile_it 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,50 @@
1
+ # Stats that are associated with each instrumented method.
2
+ class ProfileIt::MetricStats
3
+ attr_accessor :call_count
4
+ attr_accessor :min_call_time
5
+ attr_accessor :max_call_time
6
+ attr_accessor :total_call_time
7
+ attr_accessor :total_exclusive_time
8
+ attr_accessor :sum_of_squares
9
+
10
+ def initialize(scoped = false)
11
+ @scoped = scoped
12
+ self.call_count = 0
13
+ self.total_call_time = 0.0
14
+ self.total_exclusive_time = 0.0
15
+ self.min_call_time = 0.0
16
+ self.max_call_time = 0.0
17
+ self.sum_of_squares = 0.0
18
+ end
19
+
20
+ def update!(call_time,exclusive_time)
21
+ # If this metric is scoped inside another, use exclusive time for min/max and sum_of_squares. Non-scoped metrics
22
+ # (like controller actions) track the total call time.
23
+ t = (@scoped ? exclusive_time : call_time)
24
+ self.min_call_time = t if self.call_count == 0 or t < min_call_time
25
+ self.max_call_time = t if self.call_count == 0 or t > max_call_time
26
+ self.call_count +=1
27
+ self.total_call_time += call_time
28
+ self.total_exclusive_time += exclusive_time
29
+ self.sum_of_squares += (t * t)
30
+ self
31
+ end
32
+
33
+ # combines data from another MetricStats object
34
+ def combine!(other)
35
+ self.call_count += other.call_count
36
+ self.total_call_time += other.total_call_time
37
+ self.total_exclusive_time += other.total_exclusive_time
38
+ self.min_call_time = other.min_call_time if self.min_call_time.zero? or other.min_call_time < self.min_call_time
39
+ self.max_call_time = other.max_call_time if other.max_call_time > self.max_call_time
40
+ self.sum_of_squares += other.sum_of_squares
41
+ self
42
+ end
43
+
44
+ # THIS ISN'T USED ANYMORE. STILL A CONFLICT ISSUE?
45
+ # To avoid conflicts with different JSON libaries handle JSON ourselves.
46
+ # Time-based metrics are converted to milliseconds from seconds.
47
+ def to_json(*a)
48
+ %Q[{"total_exclusive_time":#{total_exclusive_time*1000},"min_call_time":#{min_call_time*1000},"call_count":#{call_count},"sum_of_squares":#{sum_of_squares*1000},"total_call_time":#{total_call_time*1000},"max_call_time":#{max_call_time*1000}}]
49
+ end
50
+ end # class MetricStats
@@ -0,0 +1,39 @@
1
+ class ProfileIt::Profile
2
+ BACKTRACE_THRESHOLD = 0.5 # the minimum threshold to record the backtrace for a metric.
3
+ BACKTRACE_LIMIT = 5 # Max length of callers to display
4
+ MAX_SIZE = 100 # Limits the size of the metric hash to prevent a metric explosion.
5
+ attr_reader :metric_name, :total_call_time, :metrics, :meta, :uri, :request_id
6
+
7
+ # Given a call stack, generates a filtered backtrace that:
8
+ # * Limits to the app/models, app/controllers, or app/views directories
9
+ # * Limits to 5 total callers
10
+ # * Makes the app folder the top-level folder used in trace info
11
+ def self.backtrace_parser(backtrace)
12
+ stack = []
13
+ backtrace.each do |c|
14
+ if m=c.match(/(\/app\/(controllers|models|views)\/.+)/)
15
+ stack << m[1]
16
+ break if stack.size == BACKTRACE_LIMIT
17
+ end
18
+ end
19
+ stack
20
+ end
21
+
22
+ def initialize(uri,request_id,metric_name,total_call_time,metrics)
23
+ @uri = uri
24
+ @metric_name = metric_name
25
+ @total_call_time = total_call_time
26
+ @request_id = request_id
27
+ @metrics = metrics
28
+ end
29
+
30
+ def to_form_data
31
+ {
32
+ "profile[uri]" => uri,
33
+ "profile[metric_name]" => metric_name,
34
+ "profile[total_call_time]" => total_call_time,
35
+ "profile[id]" => request_id,
36
+ "profile[metrics]" => Marshal.dump(metrics)
37
+ }
38
+ end
39
+ end
@@ -0,0 +1,18 @@
1
+ class ProfileIt::StackItem
2
+ attr_accessor :children_time
3
+ attr_reader :metric_name, :start_time
4
+
5
+ def initialize(metric_name)
6
+ @metric_name = metric_name
7
+ @start_time = Time.now
8
+ @children_time = 0
9
+ end
10
+
11
+ def ==(o)
12
+ self.eql?(o)
13
+ end
14
+
15
+ def eql?(o)
16
+ self.class == o.class && metric_name.eql?(o.metric_name)
17
+ end
18
+ end # class StackItem
@@ -0,0 +1,166 @@
1
+ # The store encapsolutes the logic that (1) saves instrumented data by Metric name to memory and (2) maintains a stack (just an Array)
2
+ # of instrumented methods that are being called. It's accessed via +ProfileIt::Agent.instance.store+.
3
+ class ProfileIt::Store
4
+
5
+ # Limits the size of the metric hash to prevent a metric explosion.
6
+ MAX_SIZE = 1000
7
+
8
+ attr_accessor :metric_hash
9
+ attr_accessor :profile_hash
10
+ attr_accessor :stack
11
+ attr_accessor :sample
12
+ attr_reader :profile_sample_lock
13
+
14
+ def initialize
15
+ @metric_hash = Hash.new
16
+ # Stores aggregate metrics for the current profile. When the profile is finished, metrics
17
+ # are merged with the +metric_hash+.
18
+ @profile_hash = Hash.new
19
+ @stack = Array.new
20
+ # ensure background thread doesn't manipulate profile sample while the store is.
21
+ @profile_sample_lock = Mutex.new
22
+ end
23
+
24
+ # Called when the last stack item completes for the current profile to clear
25
+ # for the next run.
26
+ def reset_profile!
27
+ Thread::current[:profile_it_ignore] = nil
28
+ Thread::current[:profile_it_scope_name] = nil
29
+ @profile_hash = Hash.new
30
+ @stack = Array.new
31
+ end
32
+
33
+ def ignore_profile!
34
+ Thread::current[:profile_it_ignore] = true
35
+ end
36
+
37
+ # Called at the start of Tracer#instrument:
38
+ # (1) Either finds an existing MetricStats object in the metric_hash or
39
+ # initialize a new one. An existing MetricStats object is present if this +metric_name+ has already been instrumented.
40
+ # (2) Adds a StackItem to the stack. This StackItem is returned and later used to validate the item popped off the stack
41
+ # when an instrumented code block completes.
42
+ def record(metric_name)
43
+ item = ProfileIt::StackItem.new(metric_name)
44
+ stack << item
45
+ item
46
+ end
47
+
48
+ def stop_recording(sanity_check_item, options={})
49
+ item = stack.pop
50
+ stack_empty = stack.empty?
51
+
52
+ # if ignoring the profile, the item is popped but nothing happens.
53
+ if Thread::current[:profile_it_ignore]
54
+ return
55
+ end
56
+ # unbalanced stack check - unreproducable cases have seen this occur. when it does, sets a Thread variable
57
+ # so we ignore further recordings. +Store#reset_profile!+ resets this.
58
+ if item != sanity_check_item
59
+ ProfileIt::Agent.instance.logger.warn "Scope [#{Thread::current[:profile_it_scope_name]}] Popped off stack: #{item.inspect} Expected: #{sanity_check_item.inspect}. Aborting."
60
+ ignore_profile!
61
+ return
62
+ end
63
+ duration = Time.now - item.start_time
64
+ if last=stack.last
65
+ last.children_time += duration
66
+ end
67
+ meta = ProfileIt::MetricMeta.new(item.metric_name, :desc => options[:desc])
68
+ meta.scope = nil if stack_empty
69
+
70
+ # add backtrace for slow calls ... how is exclusive time handled?
71
+ if duration > ProfileIt::Profile::BACKTRACE_THRESHOLD and !stack_empty
72
+ meta.extra = {:backtrace => ProfileIt::Profile.backtrace_parser(caller)}
73
+ end
74
+ stat = profile_hash[meta] || ProfileIt::MetricStats.new(!stack_empty)
75
+ stat.update!(duration,duration-item.children_time)
76
+ profile_hash[meta] = stat if store_metric?(stack_empty)
77
+ # Uses controllers as the entry point for a profile. Otherwise, stats are ignored.
78
+ if stack_empty and meta.metric_name.match(/\AController\//)
79
+ aggs=aggregate_calls(profile_hash.dup,meta)
80
+ store_profile(options[:uri],options[:request_id],profile_hash.dup.merge(aggs),meta,stat)
81
+ # deep duplicate
82
+ duplicate = aggs.dup
83
+ duplicate.each_pair do |k,v|
84
+ duplicate[k.dup] = v.dup
85
+ end
86
+ merge_data(duplicate.merge({meta.dup => stat.dup})) # aggregrates + controller
87
+ end
88
+ end
89
+
90
+ # TODO - Move more logic to Profile
91
+ #
92
+ # Limits the size of the profile hash to prevent a large profiles. The final item on the stack
93
+ # is allowed to be stored regardless of hash size to wrapup the profile sample w/the parent metric.
94
+ def store_metric?(stack_empty)
95
+ profile_hash.size < ProfileIt::Profile::MAX_SIZE or stack_empty
96
+ end
97
+
98
+ # Returns the top-level category names used in the +metrics+ hash.
99
+ def categories(metrics)
100
+ cats = Set.new
101
+ metrics.keys.each do |meta|
102
+ next if meta.scope.nil? # ignore controller
103
+ if match=meta.metric_name.match(/\A([\w|\d]+)\//)
104
+ cats << match[1]
105
+ end
106
+ end # metrics.each
107
+ cats
108
+ end
109
+
110
+ # Takes a metric_hash of calls and generates aggregates for ActiveRecord and View calls.
111
+ def aggregate_calls(metrics,parent_meta)
112
+ categories = categories(metrics)
113
+ aggregates = {}
114
+ categories.each do |cat|
115
+ agg_meta=ProfileIt::MetricMeta.new("#{cat}/all")
116
+ agg_meta.scope = parent_meta.metric_name
117
+ agg_stats = ProfileIt::MetricStats.new
118
+ metrics.each do |meta,stats|
119
+ if meta.metric_name =~ /\A#{cat}\//
120
+ agg_stats.combine!(stats)
121
+ end
122
+ end # metrics.each
123
+ aggregates[agg_meta] = agg_stats unless agg_stats.call_count.zero?
124
+ end # categories.each
125
+ aggregates
126
+ end
127
+
128
+ def store_profile(uri,request_id,profile_hash,parent_meta,parent_stat,options = {})
129
+ profile = ProfileIt::Profile.new(uri,request_id,parent_meta.metric_name,parent_stat.total_call_time,profile_hash.dup)
130
+ ProfileIt::Agent.instance.send_profile(profile)
131
+ end
132
+
133
+ # Finds or creates the metric w/the given name in the metric_hash, and updates the time. Primarily used to
134
+ # record sampled metrics. For instrumented methods, #record and #stop_recording are used.
135
+ #
136
+ # Options:
137
+ # :scope => If provided, overrides the default scope.
138
+ # :exclusive_time => Sets the exclusive time for the method. If not provided, uses +call_time+.
139
+ def track!(metric_name,call_time,options = {})
140
+ meta = ProfileIt::MetricMeta.new(metric_name)
141
+ meta.scope = options[:scope] if options.has_key?(:scope)
142
+ stat = metric_hash[meta] || ProfileIt::MetricStats.new
143
+ stat.update!(call_time,options[:exclusive_time] || call_time)
144
+ metric_hash[meta] = stat
145
+ end
146
+
147
+ # Combines old and current data
148
+ def merge_data(old_data)
149
+ old_data.each do |old_meta,old_stats|
150
+ if stats = metric_hash[old_meta]
151
+ metric_hash[old_meta] = stats.combine!(old_stats)
152
+ elsif metric_hash.size < MAX_SIZE
153
+ metric_hash[old_meta] = old_stats
154
+ end
155
+ end
156
+ metric_hash
157
+ end
158
+
159
+ # Merges old and current data, clears the current in-memory metric hash, and returns
160
+ # the merged data
161
+ def merge_data_and_clear(old_data)
162
+ merged = merge_data(old_data)
163
+ self.metric_hash = {}
164
+ merged
165
+ end
166
+ end # class Store
@@ -0,0 +1,105 @@
1
+ # Contains the methods that instrument blocks of code.
2
+ #
3
+ # When a code block is wrapped inside #instrument(metric_name):
4
+ # * The #instrument method pushes a StackItem onto Store#stack
5
+ # * When a code block is finished, #instrument pops the last item off the stack and verifies it's the StackItem
6
+ # we created earlier.
7
+ # * Once verified, the metrics for the recording session are merged into the in-memory Store#metric_hash. The current scope
8
+ # is also set for the metric (if Thread::current[:profile_it_scope_name] isn't nil).
9
+ module ProfileIt::Tracer
10
+ def self.included(klass)
11
+ klass.extend ClassMethods
12
+ end
13
+
14
+ module ClassMethods
15
+
16
+ # Use to trace a method call, possibly reporting slow profile traces to profile_it.
17
+ def profile_request(metric_name, options = {}, &block)
18
+ ProfileIt::Agent.instance.store.reset_profile!
19
+ profile_it_instrument(metric_name, options) do
20
+ Thread::current[:profile_it_scope_name] = metric_name
21
+ yield
22
+ Thread::current[:profile_it_scope_name] = nil
23
+ end
24
+ end
25
+
26
+ # Options:
27
+ # - :scope => If specified, sets the sub-scope for the metric. We allow additional scope level. This is used
28
+ # when rendering the profile tree in the UI.
29
+ def profile_it_instrument(metric_name, options={}, &block)
30
+ # why was this here? this would remove the scope name so the request wouldn't be instrumented.
31
+ # ProfileIt::Agent.instance.store.reset_profile!
32
+ # don't instrument if (1) NOT inside a profile and (2) NOT a Controller metric.
33
+ if !Thread::current[:profile_it_scope_name] and metric_name !~ /\AController\//
34
+ return yield
35
+ end
36
+ if options.delete(:scope)
37
+ Thread::current[:profile_it_sub_scope] = metric_name
38
+ end
39
+ stack_item = ProfileIt::Agent.instance.store.record(metric_name)
40
+ begin
41
+ yield
42
+ ensure
43
+ Thread::current[:profile_it_sub_scope] = nil if Thread::current[:profile_it_sub_scope] == metric_name
44
+ ProfileIt::Agent.instance.store.stop_recording(stack_item,options)
45
+ end
46
+ end
47
+
48
+ def profile_it_instrument_method(method,options = {})
49
+ metric_name = options[:metric_name] || default_metric_name(method)
50
+ return if !instrumentable?(method) or instrumented?(method,metric_name)
51
+ class_eval instrumented_method_string(method, {:metric_name => metric_name, :scope => options[:scope]}), __FILE__, __LINE__
52
+
53
+ alias_method _profile_it_uninstrumented_method_name(method, metric_name), method
54
+ alias_method method, _profile_it_instrumented_method_name(method, metric_name)
55
+ end
56
+
57
+ private
58
+
59
+ def instrumented_method_string(method, options)
60
+ klass = (self === Module) ? "self" : "self.class"
61
+ "def #{_profile_it_instrumented_method_name(method, options[:metric_name])}(*args, &block)
62
+ result = #{klass}.profile_it_instrument(\"#{options[:metric_name]}\",{:scope => #{options[:scope] || false}}) do
63
+ #{_profile_it_uninstrumented_method_name(method, options[:metric_name])}(*args, &block)
64
+ end
65
+ result
66
+ end"
67
+ end
68
+
69
+ # The method must exist to be instrumented.
70
+ def instrumentable?(method)
71
+ exists = method_defined?(method) || private_method_defined?(method)
72
+ ProfileIt::Agent.instance.logger.warn "The method [#{self.name}##{method}] does not exist and will not be instrumented" unless exists
73
+ exists
74
+ end
75
+
76
+ # +True+ if the method is already instrumented.
77
+ def instrumented?(method,metric_name)
78
+ instrumented = method_defined?(_profile_it_instrumented_method_name(method, metric_name))
79
+ ProfileIt::Agent.instance.logger.warn "The method [#{self.name}##{method}] has already been instrumented" if instrumented
80
+ instrumented
81
+ end
82
+
83
+ def default_metric_name(method)
84
+ "Custom/#{self.name}/#{method.to_s}"
85
+ end
86
+
87
+ # given a method and a metric, this method returns the
88
+ # untraced alias of the method name
89
+ def _profile_it_uninstrumented_method_name(method, metric_name)
90
+ "#{_sanitize_name(method)}_without_profile_it_instrument_#{_sanitize_name(metric_name)}"
91
+ end
92
+
93
+ # given a method and a metric, this method returns the traced
94
+ # alias of the method name
95
+ def _profile_it_instrumented_method_name(method, metric_name)
96
+ name = "#{_sanitize_name(method)}_with_profile_it_instrument_#{_sanitize_name(metric_name)}"
97
+ end
98
+
99
+ # Method names like +any?+ or +replace!+ contain a trailing character that would break when
100
+ # eval'd as ? and ! aren't allowed inside method names.
101
+ def _sanitize_name(name)
102
+ name.to_s.tr_s('^a-zA-Z0-9', '_')
103
+ end
104
+ end # ClassMethods
105
+ end # module Tracer
@@ -0,0 +1,3 @@
1
+ module ProfileIt
2
+ VERSION = "0.2.4"
3
+ end
data/lib/profile_it.rb ADDED
@@ -0,0 +1,34 @@
1
+ module ProfileIt
2
+ end
3
+ require 'socket'
4
+ require 'set'
5
+ require 'net/http'
6
+ require 'net/https'
7
+ require 'logger'
8
+ require 'yaml'
9
+ require 'cgi'
10
+ require File.expand_path('../profile_it/version.rb', __FILE__)
11
+ require File.expand_path('../profile_it/agent.rb', __FILE__)
12
+ require File.expand_path('../profile_it/agent/logging.rb', __FILE__)
13
+ require File.expand_path('../profile_it/agent/reporting.rb', __FILE__)
14
+ require File.expand_path('../profile_it/config.rb', __FILE__)
15
+ require File.expand_path('../profile_it/environment.rb', __FILE__)
16
+ require File.expand_path('../profile_it/metric_meta.rb', __FILE__)
17
+ require File.expand_path('../profile_it/metric_stats.rb', __FILE__)
18
+ require File.expand_path('../profile_it/stack_item.rb', __FILE__)
19
+ require File.expand_path('../profile_it/store.rb', __FILE__)
20
+ require File.expand_path('../profile_it/tracer.rb', __FILE__)
21
+ require File.expand_path('../profile_it/profile.rb', __FILE__)
22
+
23
+ if defined?(Rails) and Rails.respond_to?(:version) and Rails.version >= '3'
24
+ module ProfileIt
25
+ class Railtie < Rails::Railtie
26
+ initializer "profile_it.start" do |app|
27
+ ProfileIt::Agent.instance.start
28
+ end
29
+ end
30
+ end
31
+ else
32
+ ProfileIt::Agent.instance.start
33
+ end
34
+
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "profile_it/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "profile_it"
7
+ s.version = ProfileIt::VERSION
8
+ s.authors = ["Derek Haynes",'Andre Lewis']
9
+ s.email = ["support@scoutapp.com"]
10
+ s.homepage = "https://github.com/scoutapp/profile_it"
11
+ s.summary = "Rails Profiler UI"
12
+ s.description = "Profile a Ruby on Rails application in your browser and reports detailed metrics to profileit.io."
13
+
14
+ s.rubyforge_project = "profile_it"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ # specify any dependencies here; for example:
22
+ # s.add_development_dependency "rspec"
23
+ # s.add_runtime_dependency "rest-client"
24
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: profile_it
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.4
5
+ platform: ruby
6
+ authors:
7
+ - Derek Haynes
8
+ - Andre Lewis
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2015-02-09 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Profile a Ruby on Rails application in your browser and reports detailed
15
+ metrics to profileit.io.
16
+ email:
17
+ - support@scoutapp.com
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - ".gitignore"
23
+ - ".ruby-version"
24
+ - CHANGELOG.markdown
25
+ - Gemfile
26
+ - LICENSE.markdown
27
+ - README.markdown
28
+ - Rakefile
29
+ - data/cacert.pem
30
+ - lib/profile_it.rb
31
+ - lib/profile_it/agent.rb
32
+ - lib/profile_it/agent/logging.rb
33
+ - lib/profile_it/agent/reporting.rb
34
+ - lib/profile_it/config.rb
35
+ - lib/profile_it/environment.rb
36
+ - lib/profile_it/instruments/active_record_instruments.rb
37
+ - lib/profile_it/instruments/mongoid_instruments.rb
38
+ - lib/profile_it/instruments/moped_instruments.rb
39
+ - lib/profile_it/instruments/net_http.rb
40
+ - lib/profile_it/instruments/rails/action_controller_instruments.rb
41
+ - lib/profile_it/instruments/rails3_or_4/action_controller_instruments.rb
42
+ - lib/profile_it/metric_meta.rb
43
+ - lib/profile_it/metric_stats.rb
44
+ - lib/profile_it/profile.rb
45
+ - lib/profile_it/stack_item.rb
46
+ - lib/profile_it/store.rb
47
+ - lib/profile_it/tracer.rb
48
+ - lib/profile_it/version.rb
49
+ - profile_it.gemspec
50
+ homepage: https://github.com/scoutapp/profile_it
51
+ licenses: []
52
+ metadata: {}
53
+ post_install_message:
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubyforge_project: profile_it
69
+ rubygems_version: 2.2.2
70
+ signing_key:
71
+ specification_version: 4
72
+ summary: Rails Profiler UI
73
+ test_files: []
74
+ has_rdoc: