scout_apm 2.1.32 → 2.2.0.pre0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (117) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/CHANGELOG.markdown +2 -161
  4. data/Rakefile +2 -2
  5. data/ext/allocations/allocations.c +0 -6
  6. data/ext/allocations/extconf.rb +0 -1
  7. data/ext/stacks/extconf.rb +33 -0
  8. data/ext/stacks/scout_atomics.h +86 -0
  9. data/ext/stacks/stacks.c +744 -0
  10. data/lib/scout_apm.rb +16 -24
  11. data/lib/scout_apm/agent.rb +38 -93
  12. data/lib/scout_apm/agent/logging.rb +1 -6
  13. data/lib/scout_apm/agent/reporting.rb +6 -8
  14. data/lib/scout_apm/app_server_load.rb +10 -21
  15. data/lib/scout_apm/attribute_arranger.rb +2 -0
  16. data/lib/scout_apm/background_job_integrations/delayed_job.rb +1 -71
  17. data/lib/scout_apm/background_job_integrations/sidekiq.rb +27 -66
  18. data/lib/scout_apm/background_worker.rb +15 -19
  19. data/lib/scout_apm/capacity.rb +57 -0
  20. data/lib/scout_apm/config.rb +29 -135
  21. data/lib/scout_apm/context.rb +5 -9
  22. data/lib/scout_apm/deploy_integrations/capistrano_2.cap +12 -0
  23. data/lib/scout_apm/deploy_integrations/capistrano_2.rb +83 -0
  24. data/lib/scout_apm/deploy_integrations/capistrano_3.cap +12 -0
  25. data/lib/scout_apm/deploy_integrations/capistrano_3.rb +88 -0
  26. data/lib/scout_apm/environment.rb +15 -22
  27. data/lib/scout_apm/histogram.rb +2 -11
  28. data/lib/scout_apm/instant/assets/xmlhttp_instrumentation.html +2 -2
  29. data/lib/scout_apm/instant/middleware.rb +57 -198
  30. data/lib/scout_apm/instruments/action_controller_rails_2.rb +2 -1
  31. data/lib/scout_apm/instruments/action_controller_rails_3_rails4.rb +59 -90
  32. data/lib/scout_apm/instruments/active_record.rb +5 -7
  33. data/lib/scout_apm/instruments/delayed_job.rb +57 -0
  34. data/lib/scout_apm/instruments/grape.rb +3 -4
  35. data/lib/scout_apm/instruments/middleware_detailed.rb +6 -4
  36. data/lib/scout_apm/instruments/middleware_summary.rb +1 -39
  37. data/lib/scout_apm/instruments/mongoid.rb +3 -24
  38. data/lib/scout_apm/instruments/net_http.rb +2 -7
  39. data/lib/scout_apm/instruments/percentile_sampler.rb +19 -36
  40. data/lib/scout_apm/instruments/process/process_cpu.rb +2 -3
  41. data/lib/scout_apm/instruments/process/process_memory.rb +3 -3
  42. data/lib/scout_apm/layaway.rb +33 -76
  43. data/lib/scout_apm/layer.rb +59 -16
  44. data/lib/scout_apm/layer_converters/converter_base.rb +0 -199
  45. data/lib/scout_apm/layer_converters/job_converter.rb +1 -1
  46. data/lib/scout_apm/layer_converters/metric_converter.rb +1 -1
  47. data/lib/scout_apm/layer_converters/slow_job_converter.rb +90 -15
  48. data/lib/scout_apm/layer_converters/slow_request_converter.rb +101 -13
  49. data/lib/scout_apm/metric_set.rb +1 -9
  50. data/lib/scout_apm/metric_stats.rb +8 -8
  51. data/lib/scout_apm/reporter.rb +15 -51
  52. data/lib/scout_apm/request_histograms.rb +0 -4
  53. data/lib/scout_apm/request_manager.rb +1 -2
  54. data/lib/scout_apm/scored_item_set.rb +0 -7
  55. data/lib/scout_apm/serializers/deploy_serializer.rb +16 -0
  56. data/lib/scout_apm/serializers/payload_serializer.rb +3 -9
  57. data/lib/scout_apm/serializers/payload_serializer_to_json.rb +5 -2
  58. data/lib/scout_apm/serializers/slow_jobs_serializer_to_json.rb +1 -2
  59. data/lib/scout_apm/server_integrations/puma.rb +2 -5
  60. data/lib/scout_apm/slow_item_set.rb +80 -0
  61. data/lib/scout_apm/slow_job_record.rb +1 -6
  62. data/lib/scout_apm/slow_transaction.rb +2 -20
  63. data/lib/scout_apm/store.rb +12 -50
  64. data/lib/scout_apm/trace_compactor.rb +311 -0
  65. data/lib/scout_apm/tracked_request.rb +37 -128
  66. data/lib/scout_apm/utils/backtrace_parser.rb +5 -7
  67. data/lib/scout_apm/utils/fake_stacks.rb +83 -0
  68. data/lib/scout_apm/version.rb +1 -1
  69. data/scout_apm.gemspec +4 -6
  70. data/test/test_helper.rb +0 -56
  71. data/test/unit/config_test.rb +9 -60
  72. data/test/unit/histogram_test.rb +0 -14
  73. data/test/unit/layaway_test.rb +16 -31
  74. data/test/unit/serializers/payload_serializer_test.rb +105 -3
  75. data/test/unit/slow_item_set_test.rb +94 -0
  76. data/test/unit/slow_job_policy_test.rb +49 -0
  77. data/test/unit/slow_request_policy_test.rb +5 -4
  78. data/test/unit/utils/backtrace_parser_test.rb +0 -19
  79. data/tester.rb +53 -0
  80. metadata +29 -124
  81. data/.rubocop.yml +0 -8
  82. data/Guardfile +0 -42
  83. data/ext/rusage/README.md +0 -26
  84. data/ext/rusage/extconf.rb +0 -5
  85. data/ext/rusage/rusage.c +0 -52
  86. data/lib/scout_apm/background_job_integrations/resque.rb +0 -85
  87. data/lib/scout_apm/background_recorder.rb +0 -43
  88. data/lib/scout_apm/debug.rb +0 -37
  89. data/lib/scout_apm/git_revision.rb +0 -51
  90. data/lib/scout_apm/instruments/action_view.rb +0 -49
  91. data/lib/scout_apm/instruments/resque.rb +0 -40
  92. data/lib/scout_apm/layer_children_set.rb +0 -77
  93. data/lib/scout_apm/limited_layer.rb +0 -122
  94. data/lib/scout_apm/rack.rb +0 -26
  95. data/lib/scout_apm/remote/message.rb +0 -23
  96. data/lib/scout_apm/remote/recorder.rb +0 -57
  97. data/lib/scout_apm/remote/router.rb +0 -49
  98. data/lib/scout_apm/remote/server.rb +0 -58
  99. data/lib/scout_apm/serializers/histograms_serializer_to_json.rb +0 -21
  100. data/lib/scout_apm/synchronous_recorder.rb +0 -26
  101. data/lib/scout_apm/utils/gzip_helper.rb +0 -24
  102. data/lib/scout_apm/utils/numbers.rb +0 -14
  103. data/lib/scout_apm/utils/scm.rb +0 -14
  104. data/test/unit/background_job_integrations/sidekiq_test.rb +0 -104
  105. data/test/unit/context_test.rb +0 -30
  106. data/test/unit/git_revision_test.rb +0 -15
  107. data/test/unit/instruments/net_http_test.rb +0 -21
  108. data/test/unit/instruments/percentile_sampler_test.rb +0 -137
  109. data/test/unit/layer_children_set_test.rb +0 -88
  110. data/test/unit/limited_layer_test.rb +0 -53
  111. data/test/unit/remote/test_message.rb +0 -13
  112. data/test/unit/remote/test_router.rb +0 -33
  113. data/test/unit/remote/test_server.rb +0 -15
  114. data/test/unit/store_test.rb +0 -89
  115. data/test/unit/test_tracked_request.rb +0 -87
  116. data/test/unit/utils/numbers_test.rb +0 -15
  117. data/test/unit/utils/scm.rb +0 -17
@@ -39,10 +39,6 @@ module ScoutApm
39
39
  initialize_histograms_hash
40
40
  end
41
41
 
42
- def raw(item)
43
- @histograms[item]
44
- end
45
-
46
42
  def initialize_histograms_hash
47
43
  @histograms = Hash.new { |h, k| h[k] = NumericHistogram.new(histogram_size) }
48
44
  end
@@ -11,7 +11,7 @@ module ScoutApm
11
11
  def self.find
12
12
  req = Thread.current[:scout_request]
13
13
 
14
- if req && (req.stopping? || req.recorded?)
14
+ if req && req.recorded?
15
15
  nil
16
16
  else
17
17
  req
@@ -25,7 +25,6 @@ module ScoutApm
25
25
  else
26
26
  ScoutApm::FakeStore.new
27
27
  end
28
-
29
28
  Thread.current[:scout_request] = TrackedRequest.new(store)
30
29
  end
31
30
  end
@@ -63,12 +63,6 @@ module ScoutApm
63
63
  end
64
64
  end
65
65
 
66
- # Equal to another set only if exactly the same set of items is inside
67
- def eql?(other)
68
- items == other.items
69
- end
70
-
71
- alias :== :eql?
72
66
 
73
67
  private
74
68
 
@@ -81,6 +75,5 @@ module ScoutApm
81
75
  items[new_item.name] = [new_item.score, new_item.call]
82
76
  end
83
77
  end
84
-
85
78
  end
86
79
  end
@@ -0,0 +1,16 @@
1
+ # Serialize & deserialize deploy data up to the APM server
2
+ module ScoutApm
3
+ module Serializers
4
+ class DeploySerializer
5
+ HTTP_HEADERS = {'Content-Type' => 'application/x-www-form-urlencoded'}
6
+
7
+ def self.serialize(data)
8
+ URI.encode_www_form(data)
9
+ end
10
+
11
+ def self.deserialize(data)
12
+ Marshal.load(data)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -2,9 +2,9 @@
2
2
  module ScoutApm
3
3
  module Serializers
4
4
  class PayloadSerializer
5
- def self.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms)
5
+ def self.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs)
6
6
  if ScoutApm::Agent.instance.config.value("report_format") == 'json'
7
- ScoutApm::Serializers::PayloadSerializerToJson.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms)
7
+ ScoutApm::Serializers::PayloadSerializerToJson.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs)
8
8
  else
9
9
  metadata = metadata.dup
10
10
  metadata.default = nil
@@ -15,13 +15,7 @@ module ScoutApm
15
15
  :metrics => metrics,
16
16
  :slow_transactions => slow_transactions,
17
17
  :jobs => jobs,
18
- :slow_jobs => slow_jobs,
19
-
20
- # as_json returns a ruby object. Since it's not a simple
21
- # array, use this to maintain compatibility with json
22
- # payloads. At this point, the marshal code branch is
23
- # very rarely used anyway.
24
- :histograms => HistogramsSerializerToJson.new(histograms).as_json)
18
+ :slow_jobs => slow_jobs)
25
19
  end
26
20
  end
27
21
 
@@ -2,7 +2,7 @@ module ScoutApm
2
2
  module Serializers
3
3
  module PayloadSerializerToJson
4
4
  class << self
5
- def serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms)
5
+ def serialize(metadata, metrics, slow_transactions, jobs, slow_jobs)
6
6
  metadata.merge!({:payload_version => 2})
7
7
 
8
8
  jsonify_hash({:metadata => metadata,
@@ -10,7 +10,6 @@ module ScoutApm
10
10
  :slow_transactions => rearrange_the_slow_transactions(slow_transactions),
11
11
  :jobs => JobsSerializerToJson.new(jobs).as_json,
12
12
  :slow_jobs => SlowJobsSerializerToJson.new(slow_jobs).as_json,
13
- :histograms => HistogramsSerializerToJson.new(histograms).as_json,
14
13
  })
15
14
  end
16
15
 
@@ -68,6 +67,10 @@ module ScoutApm
68
67
  %Q["#{formatee.iso8601}"]
69
68
  when nil
70
69
  "null"
70
+ when TrueClass
71
+ "true"
72
+ when FalseClass
73
+ "false"
71
74
  else # strings and everything
72
75
  %Q["#{escape(formatee)}"]
73
76
  end
@@ -21,11 +21,9 @@ module ScoutApm
21
21
  "allocations" => job.allocations,
22
22
  "seconds_since_startup" => job.seconds_since_startup,
23
23
  "hostname" => job.hostname,
24
- "git_sha" => job.git_sha,
25
24
  "metrics" => MetricsToJsonSerializer.new(job.metrics).as_json, # New style of metrics
26
25
  "allocation_metrics" => MetricsToJsonSerializer.new(job.allocation_metrics).as_json, # New style of metrics
27
26
  "context" => job.context.to_hash,
28
- "truncated_metrics" => job.truncated_metrics,
29
27
 
30
28
  "score" => job.score,
31
29
  }
@@ -34,3 +32,4 @@ module ScoutApm
34
32
  end
35
33
  end
36
34
  end
35
+
@@ -24,13 +24,10 @@ module ScoutApm
24
24
  end
25
25
 
26
26
  def install
27
- old = ::Puma.cli_config.options[:before_worker_boot] || []
28
- new = Array(old) + [Proc.new do
27
+ ::Puma.cli_config.options[:before_worker_boot] << Proc.new do
29
28
  logger.info "Installing Puma worker loop."
30
29
  ScoutApm::Agent.instance.start_background_worker
31
- end]
32
-
33
- ::Puma.cli_config.options[:before_worker_boot] = new
30
+ end
34
31
  rescue
35
32
  logger.warn "Unable to install Puma worker loop: #{$!.message}"
36
33
  end
@@ -0,0 +1,80 @@
1
+ # In order to keep load down, only record a sample of Slow Items (Transactions
2
+ # or Jobs). In order to make that sampling as fair as possible, follow a basic
3
+ # algorithm:
4
+ #
5
+ # When adding a new Slow Item:
6
+ # * Just add it if there is an open spot
7
+ # * If there isn't an open spot, attempt to remove an over-represented
8
+ # item instead ("attempt_to_evict"). Overrepresented is simply "has more
9
+ # than @fair number of Matching Items in the set". The fastest of the
10
+ # overrepresented items is removed.
11
+ # * If there isn't an open spot, and no Item is valid to evict, drop the
12
+ # incoming Item without adding.
13
+ #
14
+ # There is no way to remove Items from this set, create a new object
15
+ # for each reporting period.
16
+ #
17
+ # Item must respond to:
18
+ # #metric_name - string - grouping key to see if one kind of thing is overrepresented
19
+ # #total_call_time - float - duration of the item
20
+
21
+ module ScoutApm
22
+ class SlowItemSet
23
+ include Enumerable
24
+
25
+ DEFAULT_TOTAL = 10
26
+ DEFAULT_FAIR = 1
27
+
28
+ attr_reader :total
29
+ attr_reader :fair
30
+
31
+ def initialize(total=DEFAULT_TOTAL, fair=DEFAULT_FAIR)
32
+ @total = total
33
+ @fair = fair
34
+ @items = []
35
+ end
36
+
37
+ def each
38
+ @items.each { |s| yield s }
39
+ end
40
+
41
+ def <<(item)
42
+ return if attempt_append(item)
43
+ attempt_to_evict
44
+ attempt_append(item)
45
+ end
46
+
47
+ def empty_slot?
48
+ @items.length < total
49
+ end
50
+
51
+ def attempt_append(item)
52
+ if empty_slot?
53
+ @items.push(item)
54
+ true
55
+ else
56
+ false
57
+ end
58
+ end
59
+
60
+ def attempt_to_evict
61
+ return if @items.length == 0
62
+
63
+ overrepresented = @items.
64
+ group_by { |item| unique_name_for(item) }.
65
+ to_a.
66
+ sort_by { |(_, items)| items.length }.
67
+ last
68
+
69
+ if overrepresented[1].length > fair
70
+ fastest = overrepresented[1].sort_by { |item| item.total_call_time }.first
71
+ @items.delete(fastest)
72
+ end
73
+ end
74
+
75
+ # Determine this items' "hash key"
76
+ def unique_name_for(item)
77
+ item.metric_name
78
+ end
79
+ end
80
+ end
@@ -20,10 +20,8 @@ module ScoutApm
20
20
  attr_reader :hostname
21
21
  attr_reader :seconds_since_startup
22
22
  attr_reader :score
23
- attr_reader :git_sha
24
- attr_reader :truncated_metrics
25
23
 
26
- def initialize(queue_name, job_name, time, total_time, exclusive_time, context, metrics, allocation_metrics, mem_delta, allocations, score, truncated_metrics)
24
+ def initialize(queue_name, job_name, time, total_time, exclusive_time, context, metrics, allocation_metrics, mem_delta, allocations, score)
27
25
  @queue_name = queue_name
28
26
  @job_name = job_name
29
27
  @time = time
@@ -36,10 +34,7 @@ module ScoutApm
36
34
  @allocations = allocations
37
35
  @seconds_since_startup = (Time.now - ScoutApm::Agent.instance.process_start_time)
38
36
  @hostname = ScoutApm::Environment.instance.hostname
39
- @git_sha = ScoutApm::Environment.instance.git_revision.sha
40
37
  @score = score
41
- @truncated_metrics = truncated_metrics
42
-
43
38
  ScoutApm::Agent.instance.logger.debug { "Slow Job [#{metric_name}] - Call Time: #{total_call_time} Mem Delta: #{mem_delta}"}
44
39
  end
45
40
 
@@ -15,11 +15,8 @@ module ScoutApm
15
15
  attr_reader :allocations
16
16
  attr_accessor :hostname # hack - we need to reset these server side.
17
17
  attr_accessor :seconds_since_startup # hack - we need to reset these server side.
18
- attr_accessor :git_sha # hack - we need to reset these server side.
19
18
 
20
- attr_reader :truncated_metrics # True/False that says if we had to truncate the metrics of this trace
21
-
22
- def initialize(uri, metric_name, total_call_time, metrics, allocation_metrics, context, time, raw_stackprof, mem_delta, allocations, score, truncated_metrics)
19
+ def initialize(uri, metric_name, total_call_time, metrics, allocation_metrics, context, time, raw_stackprof, mem_delta, allocations, score)
23
20
  @uri = uri
24
21
  @metric_name = metric_name
25
22
  @total_call_time = total_call_time
@@ -33,9 +30,6 @@ module ScoutApm
33
30
  @seconds_since_startup = (Time.now - ScoutApm::Agent.instance.process_start_time)
34
31
  @hostname = ScoutApm::Environment.instance.hostname
35
32
  @score = score
36
- @git_sha = ScoutApm::Environment.instance.git_revision.sha
37
- @truncated_metrics = truncated_metrics
38
-
39
33
  ScoutApm::Agent.instance.logger.debug { "Slow Request [#{uri}] - Call Time: #{total_call_time} Mem Delta: #{mem_delta} Score: #{score}"}
40
34
  end
41
35
 
@@ -50,19 +44,7 @@ module ScoutApm
50
44
  end
51
45
 
52
46
  def as_json
53
- json_attributes = [:key,
54
- :time,
55
- :total_call_time,
56
- :uri,
57
- [:context, :context_hash],
58
- :score,
59
- :prof,
60
- :mem_delta,
61
- :allocations,
62
- :seconds_since_startup,
63
- :hostname,
64
- :git_sha,
65
- :truncated_metrics]
47
+ json_attributes = [:key, :time, :total_call_time, :uri, [:context, :context_hash], :score, :prof, :mem_delta, :allocations, :seconds_since_startup, :hostname]
66
48
  ScoutApm::AttributeArranger.call(self, json_attributes)
67
49
  end
68
50
 
@@ -26,23 +26,7 @@ module ScoutApm
26
26
  # Save newly collected metrics
27
27
  def track!(metrics, options={})
28
28
  @mutex.synchronize {
29
- period = if options[:timestamp]
30
- @reporting_periods[options[:timestamp]]
31
- else
32
- current_period
33
- end
34
- period.absorb_metrics!(metrics)
35
- }
36
- end
37
-
38
- def track_histograms!(histograms, options={})
39
- @mutex.synchronize {
40
- period = if options[:timestamp]
41
- @reporting_periods[options[:timestamp]]
42
- else
43
- current_period
44
- end
45
- period.merge_histograms!(histograms)
29
+ current_period.absorb_metrics!(metrics)
46
30
  }
47
31
  end
48
32
 
@@ -83,21 +67,14 @@ module ScoutApm
83
67
  def write_to_layaway(layaway, force=false)
84
68
  ScoutApm::Agent.instance.logger.debug("Writing to layaway#{" (Forced)" if force}")
85
69
 
86
- reporting_periods.select { |time, rp| force || (time.timestamp < current_timestamp.timestamp) }.
87
- each { |time, rp| collect_samplers(rp) }.
88
- each { |time, rp| write_reporting_period(layaway, time, rp) }
89
- end
90
-
91
- def write_reporting_period(layaway, time, rp)
92
70
  @mutex.synchronize {
93
- layaway.write_reporting_period(rp)
71
+ reporting_periods.select { |time, rp| force || time.timestamp < current_timestamp.timestamp}.
72
+ each { |time, rp|
73
+ collect_samplers(rp)
74
+ layaway.write_reporting_period(rp)
75
+ reporting_periods.delete(time)
76
+ }
94
77
  }
95
- rescue => e
96
- ScoutApm::Agent.instance.logger.warn("Failed writing data to layaway file: #{e.message} / #{e.backtrace}")
97
- ensure
98
- ScoutApm::Agent.instance.logger.debug("Before delete, reporting periods length: #{reporting_periods.size}")
99
- deleted_items = reporting_periods.delete(time)
100
- ScoutApm::Agent.instance.logger.debug("After delete, reporting periods length: #{reporting_periods.size}. Did delete #{deleted_items}")
101
78
  end
102
79
 
103
80
  ######################################
@@ -109,10 +86,12 @@ module ScoutApm
109
86
  def collect_samplers(rp)
110
87
  @samplers.each do |sampler|
111
88
  begin
112
- sampler.metrics(rp.timestamp, self)
89
+ metrics = sampler.metrics(rp.timestamp)
90
+ rp.absorb_metrics!(metrics)
113
91
  rescue => e
114
92
  ScoutApm::Agent.instance.logger.info "Error reading #{sampler.human_name} for period: #{rp}"
115
- ScoutApm::Agent.instance.logger.debug "#{e.message}\n\t#{e.backtrace.join("\n\t")}"
93
+ ScoutApm::Agent.instance.logger.debug e.message
94
+ ScoutApm::Agent.instance.logger.debug e.backtrace.join("\n")
116
95
  end
117
96
  end
118
97
  end
@@ -175,9 +154,6 @@ module ScoutApm
175
154
  # A ScoredItemSet holding the "best" traces for the period
176
155
  attr_reader :job_traces
177
156
 
178
- # An Array of HistogramsReport
179
- attr_reader :histograms
180
-
181
157
  # A StoreReportingPeriodTimestamp representing the time that this
182
158
  # collection of metrics is for
183
159
  attr_reader :timestamp
@@ -190,8 +166,6 @@ module ScoutApm
190
166
  @request_traces = ScoredItemSet.new
191
167
  @job_traces = ScoredItemSet.new
192
168
 
193
- @histograms = []
194
-
195
169
  @metric_set = MetricSet.new
196
170
  @jobs = Hash.new
197
171
  end
@@ -202,8 +176,7 @@ module ScoutApm
202
176
  merge_metrics!(other.metric_set).
203
177
  merge_slow_transactions!(other.slow_transactions_payload).
204
178
  merge_jobs!(other.jobs).
205
- merge_slow_jobs!(other.slow_jobs_payload).
206
- merge_histograms!(other.histograms)
179
+ merge_slow_jobs!(other.slow_jobs_payload)
207
180
  self
208
181
  end
209
182
 
@@ -252,17 +225,6 @@ module ScoutApm
252
225
  self
253
226
  end
254
227
 
255
- def merge_histograms!(new_histograms)
256
- new_histograms = Array(new_histograms)
257
- @histograms = (histograms + new_histograms).
258
- group_by { |histo| histo.name }.
259
- map { |(_, histos)|
260
- histos.inject { |merged, histo| merged.combine!(histo) }
261
- }
262
-
263
- self
264
- end
265
-
266
228
  #################################
267
229
  # Retrieve Metrics for reporting
268
230
  #################################
@@ -0,0 +1,311 @@
1
+ # Takes in a ton of traces. Structure is a several nested arrays:
2
+ # [ # Traces
3
+ # [ # Trace
4
+ # [file,line,method,klass] # TraceLine (raw)
5
+ # ]
6
+ # ]
7
+ #
8
+ # Cleans them
9
+ # Merges them by gem/app
10
+ #
11
+ module ScoutApm
12
+ class TraceSet
13
+ # A TraceCube object which is a glorified hash of { Trace -> Count }. Used to
14
+ # collect up the count of each unique trace we've seen
15
+ attr_reader :cube
16
+
17
+ # Allow layer to push values in
18
+ attr_accessor :raw_traces
19
+ attr_accessor :skipped_in_gc
20
+ attr_accessor :skipped_in_handler
21
+ attr_accessor :skipped_in_job_registered
22
+
23
+ def initialize
24
+ @raw_traces = []
25
+ @cube = TraceCube.new
26
+ end
27
+
28
+ # We need to know what the "Start" of this trace is. An untrimmed trace generally is:
29
+ #
30
+ # Gem
31
+ # Gem
32
+ # App
33
+ # App
34
+ # App <---- set root_class of this.
35
+ # Rails
36
+ # Rails
37
+ def set_root_class(klass_name)
38
+ @root_klass = klass_name.to_s
39
+ end
40
+
41
+ def to_a
42
+ res = []
43
+ create_cube!
44
+ @cube.each do |(trace, count)|
45
+ res << [trace.to_a, count]
46
+ end
47
+
48
+ res
49
+ end
50
+
51
+ def as_json
52
+ res = []
53
+ create_cube!
54
+ @cube.each do |(trace, count)|
55
+ res << [trace.as_json, count]
56
+ end
57
+
58
+ res
59
+ end
60
+
61
+ def create_cube!
62
+ while raw_trace = @raw_traces.shift
63
+ clean_trace = ScoutApm::CleanTrace.new(raw_trace, @root_klass)
64
+ @cube << clean_trace
65
+ end
66
+ @raw_traces = []
67
+ end
68
+
69
+ def total_count
70
+ create_cube!
71
+ cube.inject(0) do |sum, (_, count)|
72
+ sum + count
73
+ end
74
+ end
75
+
76
+ def inspect
77
+ create_cube!
78
+ cube.map do |(trace, count)|
79
+ "\t#{count} -- #{trace.first.klass}##{trace.first.method}\n\t\t#{trace.to_a[1].try(:klass)}##{trace.to_a[1].try(:method)}"
80
+ end.join("\n")
81
+ end
82
+ end
83
+
84
+ # A trace is a list of individual lines, where one called another, forming a backtrace.
85
+ # Each line is made up of File, Line #, Klass, Method
86
+ #
87
+ # For the purpouses of this class:
88
+ # "Top" of the trace means the currently-running method.
89
+ # "Bottom" means the root of the call tree, from program start into rails and so on.
90
+ #
91
+ # This class trims off top and bottom to get a the meat of the trace
92
+ class CleanTrace
93
+ include Enumerable
94
+
95
+ attr_reader :lines
96
+
97
+ def initialize(raw_trace, root_klass=nil)
98
+ @lines = Array(raw_trace).map {|frame, lineno| TraceLine.new(frame, lineno)}
99
+ @root_klass = root_klass
100
+
101
+ # A trace has interesting data in the middle of it, since normally it'll go
102
+ # RailsCode -> App Code -> Gem Code.
103
+ #
104
+ # So we drop the code that leads up to your app, since a deep trace that
105
+ # always says that you went through middleware and the rails router doesn't
106
+ # help diagnose issues.
107
+ drop_below_app
108
+
109
+ # Then we drop most of the Gem Code, since you didn't write it, and in the
110
+ # vast majority of the cases, the time spent there is because your app code
111
+ # asked, not because of inherent issues with the gem. For instance, if you
112
+ # fire off a slow query to a database gem, you probably want to be
113
+ # optimizing the query, not trying to make the database gem faster.
114
+ drop_above_app
115
+ end
116
+
117
+ # Iterate starting at END of array until a controller line is found. Pop off at that index - 1.
118
+ def drop_below_app
119
+ pops = 0
120
+ index = lines.size - 1 # last index, not size.
121
+
122
+ while index >= 0 && !lines[index].controller?(@root_klass)
123
+ index -= 1
124
+ pops += 1
125
+ end
126
+
127
+ lines.pop(pops)
128
+ end
129
+
130
+ # Find the closest mention of the application code from the currently-running method.
131
+ # Then adjust by 1 if possible to capture the "first" line
132
+ def drop_above_app
133
+ ai = @lines.find_index(&:app?)
134
+ if ai
135
+ ai -= 1 if ai > 0
136
+ @lines = @lines[ai .. -1]
137
+ else
138
+ @lines = [] # No app line in backtrace, nothing to show?
139
+ end
140
+ end
141
+
142
+ def each
143
+ @lines.each { |line| yield line }
144
+ end
145
+
146
+ def empty?
147
+ @lines.empty?
148
+ end
149
+
150
+ def as_json
151
+ @lines.map { |line| line.as_json }
152
+ end
153
+
154
+ ###############################
155
+ # Hash Key interface
156
+ def hash
157
+ @lines.hash
158
+ end
159
+
160
+ def eql?(other)
161
+ @lines.eql?(other.lines)
162
+ end
163
+ ###############################
164
+ end
165
+
166
+ class TraceLine
167
+ # An opaque C object, only call Stacks#frame_* methods on this.
168
+ attr_reader :frame
169
+
170
+ # The line number. This doesn't appear to be obtainable from the frame itself
171
+ attr_reader :lineno
172
+
173
+ def initialize(frame, lineno)
174
+ @frame = frame
175
+ @lineno = lineno
176
+ end
177
+
178
+ # Returns the name of the last gem in the line
179
+ def gem_name
180
+ @gem_name ||= begin
181
+ r = %r{\/gems/(.*?)/}.freeze
182
+ results = file.scan(r)
183
+ results[-1][0] # Scan will return a nested array, so extract out that nesting
184
+ rescue
185
+ nil
186
+ end
187
+ end
188
+
189
+ def stdlib_name
190
+ @stdlib_name ||= begin
191
+ r = %r{#{Regexp.escape(RbConfig::TOPDIR)}/(.*?)}.freeze
192
+ results = file.scan(r)
193
+ results[-1][0] # Scan will return a nested array, so extract out that nesting
194
+ rescue
195
+ nil
196
+ end
197
+ end
198
+
199
+ def file
200
+ ScoutApm::Instruments::Stacks.frame_file(frame)
201
+ end
202
+
203
+
204
+ # If we ever want to get the "first line of the method" - ScoutApm::Instruments::Stacks.frame_lineno(frame)
205
+ def line
206
+ lineno
207
+ end
208
+
209
+ def klass
210
+ ScoutApm::Instruments::Stacks.frame_klass(frame)
211
+ end
212
+
213
+ def method
214
+ ScoutApm::Instruments::Stacks.frame_method(frame)
215
+ end
216
+
217
+ def gem?
218
+ !!gem_name
219
+ end
220
+
221
+ def stdlib?
222
+ !!stdlib_name
223
+ end
224
+
225
+ def app?
226
+ r = %r|^#{Regexp.escape(ScoutApm::Environment.instance.root.to_s)}/|.freeze
227
+ result = !gem_name && !stdlib_name && file =~ r
228
+ !!result # coerce to a bool
229
+ end
230
+
231
+ def trim_file(file_path)
232
+ return if file_path.nil?
233
+ if gem?
234
+ r = %r{.*gems/.*?/}.freeze
235
+ file_path.sub(r, "/")
236
+ elsif stdlib?
237
+ file_path.sub(RbConfig::TOPDIR, '')
238
+ elsif app?
239
+ file_path.sub(ScoutApm::Environment.instance.root.to_s, '')
240
+ end
241
+ end
242
+
243
+ # If root_klass is provided, just see if this is exactly that class. If not,
244
+ # fall back on "is this in the app"
245
+ def controller?(root_klass)
246
+ return false if klass.nil? # main function doesn't have a file associated
247
+
248
+ if root_klass
249
+ klass == root_klass
250
+ else
251
+ app?
252
+ end
253
+ end
254
+
255
+ def formatted_to_s
256
+ "#{stdlib_name} #{klass}##{method} -- #{file}:#{line}"
257
+ end
258
+
259
+ def as_json
260
+ [ trim_file(file), line, klass, method, app?, gem_name, stdlib_name ]
261
+ end
262
+
263
+ ###############################
264
+ # Hash Key interface
265
+
266
+ def hash
267
+ # Note that this does not include line number. It caused a few situations
268
+ # where we had a bunch of time spent in one method, but across a few lines,
269
+ # we decided that it made sense to group them together.
270
+ file.hash ^ method.hash ^ klass.hash
271
+ end
272
+
273
+ def eql?(other)
274
+ file == other.file &&
275
+ method == other.method &&
276
+ klass == other.klass
277
+ end
278
+
279
+ ###############################
280
+ end
281
+
282
+ # Collects clean traces and counts how many of each we have.
283
+ class TraceCube
284
+ include Enumerable
285
+
286
+ attr_reader :traces
287
+ attr_reader :total_count
288
+
289
+ def initialize
290
+ @traces = Hash.new{ |h,k| h[k] = 0 }
291
+ @total_count = 0
292
+ end
293
+
294
+ def <<(clean_trace)
295
+ @total_count += 1
296
+ @traces[clean_trace] += 1
297
+ end
298
+
299
+ # Yields two element array, the trace and the count of that trace
300
+ # In descending order of count.
301
+ def each
302
+ @traces
303
+ .to_a
304
+ .each { |(trace, count)|
305
+ next if trace.empty?
306
+ yield [trace, count]
307
+ }
308
+ end
309
+ end
310
+ end
311
+