profile_it 0.2.4

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.
@@ -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: