newrelic_rpm 3.6.7.159 → 3.6.8.164

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. data/CHANGELOG +14 -0
  2. data/lib/new_relic/agent/agent.rb +38 -35
  3. data/lib/new_relic/agent/agent_logger.rb +6 -47
  4. data/lib/new_relic/agent/beacon_configuration.rb +10 -4
  5. data/lib/new_relic/agent/browser_monitoring.rb +39 -33
  6. data/lib/new_relic/agent/commands/agent_command.rb +4 -4
  7. data/lib/new_relic/agent/commands/agent_command_router.rb +72 -10
  8. data/lib/new_relic/agent/commands/thread_profiler_session.rb +110 -0
  9. data/lib/new_relic/agent/commands/xray_session.rb +55 -0
  10. data/lib/new_relic/agent/commands/xray_session_collection.rb +158 -0
  11. data/lib/new_relic/agent/configuration/default_source.rb +61 -24
  12. data/lib/new_relic/agent/configuration/mask_defaults.rb +2 -2
  13. data/lib/new_relic/agent/configuration/server_source.rb +1 -1
  14. data/lib/new_relic/agent/instrumentation/action_controller_subscriber.rb +2 -0
  15. data/lib/new_relic/agent/instrumentation/controller_instrumentation.rb +4 -10
  16. data/lib/new_relic/agent/instrumentation/rails3/action_controller.rb +10 -11
  17. data/lib/new_relic/agent/memory_logger.rb +52 -0
  18. data/lib/new_relic/agent/new_relic_service.rb +4 -0
  19. data/lib/new_relic/agent/request_sampler.rb +32 -13
  20. data/lib/new_relic/agent/samplers/cpu_sampler.rb +6 -3
  21. data/lib/new_relic/agent/threading/agent_thread.rb +2 -1
  22. data/lib/new_relic/agent/threading/backtrace_node.rb +80 -27
  23. data/lib/new_relic/agent/threading/backtrace_service.rb +264 -0
  24. data/lib/new_relic/agent/threading/thread_profile.rb +79 -118
  25. data/lib/new_relic/agent/transaction/developer_mode_sample_buffer.rb +56 -0
  26. data/lib/new_relic/agent/transaction/force_persist_sample_buffer.rb +25 -0
  27. data/lib/new_relic/agent/transaction/slowest_sample_buffer.rb +25 -0
  28. data/lib/new_relic/agent/transaction/transaction_sample_buffer.rb +86 -0
  29. data/lib/new_relic/agent/transaction/xray_sample_buffer.rb +64 -0
  30. data/lib/new_relic/agent/transaction.rb +25 -4
  31. data/lib/new_relic/agent/transaction_sample_builder.rb +6 -10
  32. data/lib/new_relic/agent/transaction_sampler.rb +47 -202
  33. data/lib/new_relic/agent/worker_loop.rb +47 -39
  34. data/lib/new_relic/agent.rb +1 -1
  35. data/lib/new_relic/build.rb +2 -2
  36. data/lib/new_relic/coerce.rb +8 -0
  37. data/lib/new_relic/control/instance_methods.rb +1 -0
  38. data/lib/new_relic/rack/browser_monitoring.rb +15 -1
  39. data/lib/new_relic/rack/developer_mode.rb +1 -1
  40. data/lib/new_relic/transaction_sample.rb +20 -5
  41. data/lib/new_relic/version.rb +1 -1
  42. data/newrelic.yml +4 -6
  43. data/newrelic_rpm.gemspec +1 -1
  44. data/test/agent_helper.rb +11 -0
  45. data/test/environments/lib/environments/runner.rb +5 -1
  46. data/test/environments/rails21/Gemfile +2 -2
  47. data/test/environments/rails22/Gemfile +2 -2
  48. data/test/environments/rails23/Gemfile +2 -2
  49. data/test/environments/rails31/Gemfile +2 -2
  50. data/test/environments/rails32/Gemfile +2 -2
  51. data/test/multiverse/suites/agent_only/marshaling_test.rb +1 -1
  52. data/test/multiverse/suites/agent_only/testing_app.rb +6 -0
  53. data/test/multiverse/suites/agent_only/thread_profiling_test.rb +5 -5
  54. data/test/multiverse/suites/agent_only/xray_sessions_test.rb +163 -0
  55. data/test/multiverse/suites/rails/request_statistics_test.rb +2 -2
  56. data/test/multiverse/suites/rails/view_instrumentation_test.rb +20 -21
  57. data/test/new_relic/agent/agent/connect_test.rb +0 -10
  58. data/test/new_relic/agent/agent_test.rb +27 -44
  59. data/test/new_relic/agent/browser_monitoring_test.rb +0 -52
  60. data/test/new_relic/agent/commands/agent_command_router_test.rb +150 -12
  61. data/test/new_relic/agent/commands/{thread_profiler_test.rb → thread_profiler_session_test.rb} +58 -19
  62. data/test/new_relic/agent/commands/xray_session_collection_test.rb +332 -0
  63. data/test/new_relic/agent/commands/xray_session_test.rb +42 -0
  64. data/test/new_relic/agent/configuration/manager_test.rb +2 -1
  65. data/test/new_relic/agent/configuration/server_source_test.rb +10 -10
  66. data/test/new_relic/agent/cpu_sampler_test.rb +50 -0
  67. data/test/new_relic/agent/instrumentation/action_controller_subscriber_test.rb +31 -0
  68. data/test/new_relic/agent/instrumentation/queue_time_test.rb +0 -1
  69. data/test/new_relic/agent/instrumentation/sequel_test.rb +1 -1
  70. data/test/new_relic/agent/instrumentation/task_instrumentation_test.rb +0 -1
  71. data/test/new_relic/agent/memory_logger_test.rb +53 -0
  72. data/test/new_relic/agent/new_relic_service_test.rb +1 -1
  73. data/test/new_relic/agent/pipe_channel_manager_test.rb +4 -5
  74. data/test/new_relic/agent/request_sampler_test.rb +70 -20
  75. data/test/new_relic/agent/rules_engine_test.rb +6 -0
  76. data/test/new_relic/agent/threading/agent_thread_test.rb +2 -2
  77. data/test/new_relic/agent/threading/backtrace_node_test.rb +110 -17
  78. data/test/new_relic/agent/threading/backtrace_service_test.rb +567 -0
  79. data/test/new_relic/agent/threading/fake_thread.rb +4 -0
  80. data/test/new_relic/agent/threading/thread_profile_test.rb +141 -217
  81. data/test/new_relic/agent/threading/threaded_test_case.rb +3 -8
  82. data/test/new_relic/agent/transaction/developer_mode_sample_buffer_test.rb +69 -0
  83. data/test/new_relic/agent/transaction/force_persist_sample_buffer_test.rb +52 -0
  84. data/test/new_relic/agent/transaction/slowest_sample_buffer_test.rb +67 -0
  85. data/test/new_relic/agent/transaction/xray_sample_buffer_test.rb +71 -0
  86. data/test/new_relic/agent/transaction_sampler_test.rb +171 -307
  87. data/test/new_relic/agent/transaction_test.rb +33 -5
  88. data/test/new_relic/agent/worker_loop_test.rb +33 -11
  89. data/test/new_relic/coerce_test.rb +13 -0
  90. data/test/new_relic/fake_collector.rb +26 -3
  91. data/test/new_relic/multiverse_helpers.rb +2 -0
  92. data/test/new_relic/rack/browser_monitoring_test.rb +12 -0
  93. data/test/new_relic/rack/developer_mode_test.rb +2 -2
  94. data/test/new_relic/transaction_sample_test.rb +19 -2
  95. data/test/performance/lib/performance/console_reporter.rb +1 -1
  96. data/test/performance/lib/performance/test_case.rb +7 -3
  97. data/test/performance/script/runner +3 -0
  98. data/test/performance/suites/thread_profiling.rb +83 -0
  99. data/test/test_helper.rb +2 -2
  100. data.tar.gz.sig +0 -0
  101. metadata +32 -32
  102. metadata.gz.sig +1 -1
  103. data/lib/new_relic/agent/commands/thread_profiler.rb +0 -80
@@ -6,8 +6,8 @@ module NewRelic
6
6
  module Agent
7
7
  module Configuration
8
8
  MASK_DEFAULTS = {
9
- :'thread_profiler' => Proc.new { !NewRelic::Agent::Commands::ThreadProfiler.is_supported? },
10
- :'thread_profiler.enabled' => Proc.new { !NewRelic::Agent::Commands::ThreadProfiler.is_supported? }
9
+ :'thread_profiler' => Proc.new { !NewRelic::Agent::Threading::BacktraceService.is_supported? },
10
+ :'thread_profiler.enabled' => Proc.new { !NewRelic::Agent::Threading::BacktraceService.is_supported? }
11
11
  }
12
12
  end
13
13
  end
@@ -33,7 +33,7 @@ module NewRelic
33
33
  :'transaction_tracer.enabled' => 'collect_traces',
34
34
  :'slow_sql.enabled' => 'collect_traces',
35
35
  :'error_collector.enabled' => 'collect_errors',
36
- :'request_sampler.enabled' => 'collect_analytics_events'
36
+ :'analytics_events.enabled' => 'collect_analytics_events'
37
37
  }
38
38
  gated_features.each do |feature, gate_key|
39
39
  if server_config.has_key?(gate_key)
@@ -99,6 +99,8 @@ module NewRelic
99
99
  :request => event.payload[:request],
100
100
  :filtered_params => filter(event.payload[:params]))
101
101
  txn.apdex_start = (event.queue_start || event.time)
102
+ txn.name = event.metric_name
103
+
102
104
  event.scope = Agent.instance.stats_engine \
103
105
  .push_scope(:action_controller, event.time)
104
106
  end
@@ -320,17 +320,11 @@ module NewRelic
320
320
 
321
321
  begin
322
322
  NewRelic::Agent::BusyCalculator.dispatcher_start txn.start_time
323
- result = if block_given?
324
- yield
325
- else
326
- perform_action_without_newrelic_trace(*args)
327
- end
328
- if defined?(request) && request && defined?(response) && response
329
- if !Agent.config[:disable_mobile_headers]
330
- NewRelic::Agent::BrowserMonitoring.insert_mobile_response_header(request, response)
331
- end
323
+ if block_given?
324
+ yield
325
+ else
326
+ perform_action_without_newrelic_trace(*args)
332
327
  end
333
- result
334
328
  rescue => e
335
329
  txn.notice_error(e)
336
330
  raise
@@ -132,7 +132,7 @@ end
132
132
  DependencyDetection.defer do
133
133
  @name = :rails31_view
134
134
 
135
- # We can't be sure that this wil work with future versions of Rails 3.
135
+ # We can't be sure that this will work with future versions of Rails 3.
136
136
  # Currently enabled for Rails 3.1 and 3.2
137
137
  depends_on do
138
138
  defined?(::Rails) && ::Rails::VERSION::MAJOR.to_i == 3 && ([1,2].member?(::Rails::VERSION::MINOR.to_i))
@@ -155,8 +155,8 @@ DependencyDetection.defer do
155
155
  # This is needed for rails 3.2 compatibility
156
156
  @details = extract_details(options) if respond_to? :extract_details, true
157
157
  identifier = determine_template(options) ? determine_template(options).identifier : nil
158
- str = "View/#{NewRelic::Agent::Instrumentation::Rails3::ActionView::NewRelic.template_metric(identifier, options)}/Rendering"
159
- trace_execution_scoped str do
158
+ scope_name = "View/#{NewRelic::Agent::Instrumentation::Rails3::ActionView::NewRelic.template_metric(identifier, options)}/Rendering"
159
+ trace_execution_scoped scope_name do
160
160
  render_without_newrelic(context, options)
161
161
  end
162
162
  end
@@ -168,17 +168,16 @@ DependencyDetection.defer do
168
168
  ActionView::PartialRenderer.class_eval do
169
169
  include NewRelic::Agent::MethodTracer
170
170
 
171
- def render_with_newrelic(*args, &block)
172
- setup(*args, &block)
173
- identifier = find_partial ? find_partial.identifier : nil
174
- str = "View/#{NewRelic::Agent::Instrumentation::Rails3::ActionView::NewRelic.template_metric(identifier)}/Partial"
175
- trace_execution_scoped str do
176
- render_without_newrelic(*args, &block)
171
+ def instrument_with_newrelic(name, payload = {}, &block)
172
+ identifier = payload[:identifier]
173
+ scope_name = "View/#{NewRelic::Agent::Instrumentation::Rails3::ActionView::NewRelic.template_metric(identifier)}/Partial"
174
+ trace_execution_scoped(scope_name) do
175
+ instrument_without_newrelic(name, payload, &block)
177
176
  end
178
177
  end
179
178
 
180
- alias_method :render_without_newrelic, :render
181
- alias_method :render, :render_with_newrelic
179
+ alias_method :instrument_without_newrelic, :instrument
180
+ alias_method :instrument, :instrument_with_newrelic
182
181
  end
183
182
  end
184
183
  end
@@ -0,0 +1,52 @@
1
+ # encoding: utf-8
2
+ # This file is distributed under New Relic's license terms.
3
+ # See https://github.com/newrelic/rpm/blob/master/LICENSE for complete details.
4
+
5
+ # Base class for startup logging and testing in multiverse
6
+
7
+ module NewRelic
8
+ module Agent
9
+ class MemoryLogger
10
+ def initialize
11
+ @messages = []
12
+ end
13
+
14
+ def is_startup_logger?
15
+ true
16
+ end
17
+
18
+ attr_accessor :messages, :level
19
+
20
+ def fatal(*msgs)
21
+ messages << [:fatal, msgs]
22
+ end
23
+
24
+ def error(*msgs)
25
+ messages << [:error, msgs]
26
+ end
27
+
28
+ def warn(*msgs)
29
+ messages << [:warn, msgs]
30
+ end
31
+
32
+ def info(*msgs)
33
+ messages << [:info, msgs]
34
+ end
35
+
36
+ def debug(*msgs)
37
+ messages << [:debug, msgs]
38
+ end
39
+
40
+ def log_exception(level, e, backtrace_level=level)
41
+ messages << [:log_exception, [level, e, backtrace_level]]
42
+ end
43
+
44
+ def dump(logger)
45
+ messages.each do |(method, args)|
46
+ logger.send(method, *args)
47
+ end
48
+ messages.clear
49
+ end
50
+ end
51
+ end
52
+ end
@@ -141,6 +141,10 @@ module NewRelic
141
141
  invoke_remote(:agent_command_results, @agent_id, results)
142
142
  end
143
143
 
144
+ def get_xray_metadata(xray_ids)
145
+ invoke_remote(:get_xray_metadata, @agent_id, *xray_ids)
146
+ end
147
+
144
148
  # Send fine-grained analytic data to the collector.
145
149
  def analytic_event_data(data)
146
150
  data.collect! {|hash| [hash] }
@@ -13,9 +13,9 @@ class NewRelic::Agent::RequestSampler
13
13
  MonitorMixin
14
14
 
15
15
  # The namespace and keys of config values
16
- CONFIG_NAMESPACE = 'request_sampler'
17
- MAX_SAMPLES_KEY = "#{CONFIG_NAMESPACE}.max_samples".to_sym
18
- ENABLED_KEY = "#{CONFIG_NAMESPACE}.enabled".to_sym
16
+ MAX_SAMPLES_KEY = :'analytics_events.max_samples_stored'
17
+ ENABLED_KEY = :'analytics_events.enabled'
18
+ ENABLED_TXN_KEY = :'analytics_events.transactions.enabled'
19
19
 
20
20
  # The type field of the sample
21
21
  SAMPLE_TYPE = 'Transaction'
@@ -42,26 +42,39 @@ class NewRelic::Agent::RequestSampler
42
42
  public
43
43
  ######
44
44
 
45
- ### Fetch a copy of the sampler's gathered samples. (Synchronized)
45
+ # Fetch a copy of the sampler's gathered samples. (Synchronized)
46
46
  def samples
47
47
  return self.synchronize { @samples.to_a }
48
48
  end
49
49
 
50
-
51
- # Clear any existing samples and reset the last sample time. (Synchronized)
52
- def reset
50
+ # Clear any existing samples, reset the last sample time, and return the
51
+ # previous set of samples. (Synchronized)
52
+ def harvest
53
53
  NewRelic::Agent.logger.debug "Resetting RequestSampler"
54
54
 
55
55
  sample_count, request_count = 0
56
+ old_samples = nil
56
57
 
57
58
  self.synchronize do
58
59
  sample_count = @samples.size
59
60
  request_count = @samples.seen
61
+ old_samples = @samples.to_a
60
62
  @samples.reset
61
63
  @notified_full = false
62
64
  end
63
65
 
64
66
  record_sampling_rate(request_count, sample_count) if @enabled
67
+ old_samples
68
+ end
69
+
70
+ alias_method :reset, :harvest
71
+
72
+ # Merge samples back into the buffer, for example after a failed
73
+ # transmission to the collector. (Synchronized)
74
+ def merge(old_samples)
75
+ self.synchronize do
76
+ old_samples.each { |s| @samples.append(s) }
77
+ end
65
78
  end
66
79
 
67
80
  def record_sampling_rate(request_count, sample_count)
@@ -90,7 +103,12 @@ class NewRelic::Agent::RequestSampler
90
103
 
91
104
  NewRelic::Agent.config.register_callback(ENABLED_KEY) do |enabled|
92
105
  NewRelic::Agent.logger.info "%sabling the Request Sampler." % [ enabled ? 'En' : 'Dis' ]
93
- @enabled = enabled
106
+ @enabled = enabled && NewRelic::Agent.config[ENABLED_TXN_KEY]
107
+ end
108
+
109
+ NewRelic::Agent.config.register_callback(ENABLED_TXN_KEY) do |enabled|
110
+ NewRelic::Agent.logger.info "%sabling the Request Sampler." % [ enabled ? 'En' : 'Dis' ]
111
+ @enabled = enabled && NewRelic::Agent.config[ENABLED_KEY]
94
112
  end
95
113
  end
96
114
 
@@ -100,14 +118,15 @@ class NewRelic::Agent::RequestSampler
100
118
  end
101
119
 
102
120
  # Event handler for the :transaction_finished event.
103
- def on_transaction_finished( metric, start_timestamp, duration, options={} )
121
+ def on_transaction_finished(payload)
104
122
  return unless @enabled
123
+ return unless NewRelic::Agent::Transaction.transaction_type_is_web?(payload[:type])
105
124
  sample = {
106
- TIMESTAMP_KEY => float(start_timestamp),
107
- NAME_KEY => string(metric),
108
- DURATION_KEY => float(duration),
125
+ TIMESTAMP_KEY => float(payload[:start_timestamp]),
126
+ NAME_KEY => string(payload[:name]),
127
+ DURATION_KEY => float(payload[:duration]),
109
128
  TYPE_KEY => SAMPLE_TYPE
110
- }.merge(options)
129
+ }.merge((payload[:overview_metrics] || {}))
111
130
 
112
131
  is_full = self.synchronize { @samples.append(sample) }
113
132
  notify_full if is_full && !@notified_full
@@ -36,9 +36,12 @@ module NewRelic
36
36
  end
37
37
 
38
38
  def self.supported_on_this_platform?
39
- # Process.times on JRuby reports wall clock elapsed time,
40
- # not actual cpu time used, so we cannot use this sampler there.
41
- not defined?(JRuby)
39
+ # Process.times on JRuby < 1.7.0 reports wall clock elapsed time,
40
+ # not actual cpu time used, so this sampler can only be used on JRuby >= 1.7.0.
41
+ if defined?(JRuby)
42
+ return JRUBY_VERSION >= '1.7.0'
43
+ end
44
+ true
42
45
  end
43
46
 
44
47
  def poll
@@ -32,7 +32,8 @@ module NewRelic
32
32
  ::NewRelic::Agent.logger.debug("Failed to backtrace #{thread.inspect}: #{e.class.name}: #{e.to_s}")
33
33
  end
34
34
  return nil unless bt
35
- profile_agent_code ? bt : bt.select { |t| t !~ /\/newrelic_rpm-\d/ }
35
+ bt.reject! { |t| t.include?('/newrelic_rpm-') } unless profile_agent_code
36
+ bt
36
37
  end
37
38
  end
38
39
 
@@ -7,62 +7,115 @@ module NewRelic
7
7
  module Threading
8
8
 
9
9
  class BacktraceNode
10
- attr_reader :file, :method, :line_no, :children
11
- attr_accessor :runnable_count, :to_prune, :depth
12
-
13
- def initialize(line, parent=nil)
14
- line =~ /(.*)\:(\d+)\:in `(.*)'/
15
- @file = $1
16
- @method = $3
17
- @line_no = $2.to_i
10
+ attr_reader :file, :method, :line_no, :children, :raw_line
11
+ attr_accessor :runnable_count, :depth
12
+
13
+ def initialize(line)
14
+ if line
15
+ @raw_line = line
16
+ @root = false
17
+ else
18
+ @root = true
19
+ end
20
+
18
21
  @children = []
19
22
  @runnable_count = 0
20
- @to_prune = false
21
23
  @depth = 0
24
+ end
25
+
26
+ def root?
27
+ @root
28
+ end
29
+
30
+ def empty?
31
+ root? && @children.empty?
32
+ end
22
33
 
23
- parent.add_child(self) if parent
34
+ def find(raw_line)
35
+ @children.find { |child| child.raw_line == raw_line }
24
36
  end
25
37
 
26
38
  def ==(other)
27
- @file == other.file &&
28
- @method == other.method &&
29
- @line_no == other.line_no
39
+ (
40
+ (root? && other.root? || @raw_line == other.raw_line) &&
41
+ (
42
+ @depth == other.depth &&
43
+ @runnable_count == other.runnable_count
44
+ )
45
+ )
30
46
  end
31
47
 
32
48
  def total_count
33
49
  @runnable_count
34
50
  end
35
51
 
52
+ def aggregate(backtrace)
53
+ current = self
54
+
55
+ backtrace.reverse_each do |frame|
56
+ existing_node = current.find(frame)
57
+ if existing_node
58
+ node = existing_node
59
+ else
60
+ node = Threading::BacktraceNode.new(frame)
61
+ current.add_child_unless_present(node)
62
+ end
63
+
64
+ node.runnable_count += 1
65
+ current = node
66
+ end
67
+ end
68
+
36
69
  # Descending order on count, ascending on depth of nodes
37
- def order_for_pruning(y)
38
- [-runnable_count, depth] <=> [-y.runnable_count, y.depth]
70
+ def <=>(other)
71
+ [-runnable_count, depth] <=> [-other.runnable_count, other.depth]
72
+ end
73
+
74
+ def flatten
75
+ initial = self.root? ? [] : [self]
76
+ @children.inject(initial) { |all, child| all.concat(child.flatten) }
39
77
  end
40
78
 
41
79
  include NewRelic::Coerce
42
80
 
43
81
  def to_array
44
- [[
45
- string(@file),
46
- string(@method),
47
- int(@line_no)
48
- ],
82
+ child_arrays = @children.map { |c| c.to_array }
83
+ return child_arrays if root?
84
+ file, method, line = parse_backtrace_frame(@raw_line)
85
+ [
86
+ [
87
+ string(file),
88
+ string(method),
89
+ int(line)
90
+ ],
49
91
  int(@runnable_count),
50
92
  0,
51
- @children.map {|c| c.to_array}]
93
+ child_arrays
94
+ ]
52
95
  end
53
96
 
54
- def add_child(child)
97
+ def add_child_unless_present(child)
55
98
  child.depth = @depth + 1
56
99
  @children << child unless @children.include? child
57
100
  end
58
101
 
59
- def prune!
60
- BacktraceNode.prune!(@children)
102
+ def prune!(targets)
103
+ @children.delete_if { |child| targets.include?(child) }
104
+ @children.each { |child| child.prune!(targets) }
105
+ end
106
+
107
+ def dump_string(indent=0)
108
+ file, method, line = parse_backtrace_frame(@raw_line)
109
+ result = "#{" " * indent}#<BacktraceNode:#{object_id} [#{@runnable_count}] #{@file}:#{@line_no} in #{@method}>"
110
+ child_results = @children.map { |c| c.dump_string(indent+2) }.join("\n")
111
+ result << "\n" unless child_results.empty?
112
+ result << child_results
61
113
  end
62
114
 
63
- def self.prune!(kids)
64
- kids.delete_if { |child| child.to_prune }
65
- kids.each { |child| child.prune! }
115
+ # Returns [filename, method, line number]
116
+ def parse_backtrace_frame(frame)
117
+ frame =~ /(.*)\:(\d+)\:in `(.*)'/
118
+ [$1, $3, $2] # sic
66
119
  end
67
120
  end
68
121
 
@@ -0,0 +1,264 @@
1
+ # encoding: utf-8
2
+ # This file is distributed under New Relic's license terms.
3
+ # See https://github.com/newrelic/rpm/blob/master/LICENSE for complete details.
4
+
5
+ module NewRelic
6
+ module Agent
7
+ module Threading
8
+ class BacktraceService
9
+ ALL_TRANSACTIONS = "**ALL**".freeze
10
+
11
+ def self.is_supported?
12
+ RUBY_VERSION >= "1.9.2"
13
+ end
14
+
15
+ attr_reader :worker_loop, :buffer, :effective_polling_period,
16
+ :overhead_percent_threshold
17
+ attr_accessor :worker_thread, :profile_agent_code
18
+
19
+ def initialize(event_listener=nil)
20
+ @profiles = {}
21
+ @buffer = {}
22
+
23
+ # synchronizes access to @profiles and @buffer above
24
+ @lock = Mutex.new
25
+
26
+ @running = false
27
+ @profile_agent_code = false
28
+ @worker_loop = NewRelic::Agent::WorkerLoop.new
29
+
30
+ # Memoize overhead % to avoid getting stale OR looked up every poll
31
+ @overhead_percent_threshold = NewRelic::Agent.config[:'xray_session.max_profile_overhead']
32
+ NewRelic::Agent.config.register_callback(:'xray_session.max_profile_overhead') do |new_value|
33
+ @overhead_percent_threshold = new_value
34
+ end
35
+
36
+ if event_listener
37
+ event_listener.subscribe(:transaction_finished, &method(:on_transaction_finished))
38
+ end
39
+ end
40
+
41
+ # Public interface
42
+
43
+ def running?
44
+ @running
45
+ end
46
+
47
+ def subscribe(transaction_name, command_arguments={})
48
+ if !self.class.is_supported?
49
+ NewRelic::Agent.logger.debug("Backtracing not supported, so not subscribing transaction '#{transaction_name}'")
50
+ return
51
+ end
52
+
53
+ NewRelic::Agent.logger.debug("Backtrace Service subscribing transaction '#{transaction_name}'")
54
+
55
+ profile = ThreadProfile.new(command_arguments)
56
+
57
+ @lock.synchronize do
58
+ @profiles[transaction_name] = profile
59
+ update_values_from_profiles
60
+ end
61
+
62
+ start
63
+ profile
64
+ end
65
+
66
+ def unsubscribe(transaction_name)
67
+ return unless self.class.is_supported?
68
+
69
+ NewRelic::Agent.logger.debug("Backtrace Service unsubscribing transaction '#{transaction_name}'")
70
+ @lock.synchronize do
71
+ @profiles.delete(transaction_name)
72
+ if @profiles.empty?
73
+ stop
74
+ else
75
+ update_values_from_profiles
76
+ end
77
+ end
78
+ end
79
+
80
+ def update_values_from_profiles
81
+ self.effective_polling_period = find_effective_polling_period
82
+ self.profile_agent_code = should_profile_agent_code?
83
+ end
84
+
85
+ def subscribed?(transaction_name)
86
+ @lock.synchronize do
87
+ @profiles.has_key?(transaction_name)
88
+ end
89
+ end
90
+
91
+ def harvest(transaction_name)
92
+ @lock.synchronize do
93
+ if @profiles[transaction_name]
94
+ profile = @profiles.delete(transaction_name)
95
+ profile.finished_at = Time.now
96
+ @profiles[transaction_name] = ThreadProfile.new(profile.command_arguments)
97
+ profile
98
+ end
99
+ end
100
+ end
101
+
102
+ def on_transaction_finished(payload)
103
+ name = payload[:name]
104
+ start = payload[:start_timestamp]
105
+ duration = payload[:duration]
106
+ thread = payload[:thread] || Thread.current
107
+ @lock.synchronize do
108
+ backtraces = @buffer.delete(thread)
109
+ if backtraces && @profiles.has_key?(name)
110
+ aggregate_backtraces(backtraces, name, start, duration, thread)
111
+ end
112
+ end
113
+ end
114
+
115
+ # Internals
116
+
117
+ # This method is expected to be called with @lock held.
118
+ def aggregate_backtraces(backtraces, name, start, duration, thread)
119
+ end_time = start + duration
120
+ backtraces.each do |(timestamp, backtrace)|
121
+ if timestamp >= start && timestamp < end_time
122
+ @profiles[name].aggregate(backtrace, :request, thread)
123
+ end
124
+ end
125
+ end
126
+
127
+ def start
128
+ return if @running || !self.class.is_supported?
129
+
130
+ @running = true
131
+ self.worker_thread = AgentThread.new('Backtrace Service') do
132
+ begin
133
+ # Not passing period because we expect it's already been set.
134
+ self.worker_loop.run(&method(:poll))
135
+ ensure
136
+ NewRelic::Agent.logger.debug("Exiting New Relic thread: Backtrace Service")
137
+ end
138
+ end
139
+ end
140
+
141
+ # This method is expected to be called with @lock held
142
+ def stop
143
+ return unless @running
144
+ @running = false
145
+ self.worker_loop.stop
146
+
147
+ @buffer = {}
148
+ end
149
+
150
+ def effective_polling_period=(new_period)
151
+ @effective_polling_period = new_period
152
+ self.worker_loop.period = new_period
153
+ end
154
+
155
+ def poll
156
+ poll_start = Time.now
157
+
158
+ @lock.synchronize do
159
+ AgentThread.list.each do |thread|
160
+ sample_thread(thread)
161
+ end
162
+ @profiles.values.each { |c| c.increment_poll_count }
163
+ @buffer.delete_if { |thread, _| !thread.alive? }
164
+ end
165
+
166
+ end_time = Time.now
167
+ adjust_polling_time(end_time, poll_start)
168
+ record_supportability_metrics(end_time, poll_start)
169
+ end
170
+
171
+ # This method is expected to be called with @lock held.
172
+ attr_reader :profiles
173
+
174
+ # This method is expected to be called with @lock held.
175
+ def should_buffer?(bucket)
176
+ bucket == :request && @profiles.keys.any? { |k| k != ALL_TRANSACTIONS }
177
+ end
178
+
179
+ # This method is expected to be called with @lock held.
180
+ def need_backtrace?(bucket)
181
+ (
182
+ bucket != :ignore &&
183
+ (@profiles[ALL_TRANSACTIONS] || should_buffer?(bucket))
184
+ )
185
+ end
186
+
187
+ MAX_BUFFER_LENGTH = 500
188
+
189
+ # This method is expected to be called with @lock held.
190
+ def buffer_backtrace_for_thread(thread, timestamp, backtrace, bucket)
191
+ if should_buffer?(bucket)
192
+ @buffer[thread] ||= []
193
+ if @buffer[thread].length < MAX_BUFFER_LENGTH
194
+ @buffer[thread] << [timestamp, backtrace]
195
+ else
196
+ NewRelic::Agent.increment_metric('Supportability/XraySessions/DroppedBacktraces')
197
+ end
198
+ end
199
+ end
200
+
201
+ # This method is expected to be called with @lock held.
202
+ def aggregate_global_backtrace(backtrace, bucket, thread)
203
+ if @profiles[ALL_TRANSACTIONS]
204
+ @profiles[ALL_TRANSACTIONS].aggregate(backtrace, bucket, thread)
205
+ end
206
+ end
207
+
208
+ # This method is expected to be called with @lock held.
209
+ def sample_thread(thread)
210
+ bucket = AgentThread.bucket_thread(thread, @profile_agent_code)
211
+
212
+ if need_backtrace?(bucket)
213
+ timestamp = Time.now.to_f
214
+ backtrace = AgentThread.scrub_backtrace(thread, @profile_agent_code)
215
+ aggregate_global_backtrace(backtrace, bucket, thread)
216
+ buffer_backtrace_for_thread(thread, timestamp, backtrace, bucket)
217
+ end
218
+ end
219
+
220
+ # This method is expected to be called with @lock held.
221
+ def find_effective_polling_period
222
+ @profiles.values.map { |p| p.requested_period }.min
223
+ end
224
+
225
+ # This method is expected to be called with @lock held.
226
+ def should_profile_agent_code?
227
+ @profiles.values.any? { |p| p.profile_agent_code }
228
+ end
229
+
230
+ # If our overhead % exceeds the threshold, bump the next poll period
231
+ # relative to how much larger our overhead is than allowed
232
+ def adjust_polling_time(now, poll_start)
233
+ duration = now - poll_start
234
+ overhead_percent = duration / effective_polling_period
235
+
236
+ if overhead_percent > self.overhead_percent_threshold
237
+ scale_up_by = overhead_percent / self.overhead_percent_threshold
238
+ worker_loop.period = effective_polling_period * scale_up_by
239
+ else
240
+ worker_loop.period = effective_polling_period
241
+ end
242
+ end
243
+
244
+ def record_supportability_metrics(now, poll_start)
245
+ record_polling_time(now, poll_start)
246
+ record_skew(poll_start)
247
+ end
248
+
249
+ def record_polling_time(now, poll_start)
250
+ NewRelic::Agent.record_metric('Supportability/ThreadProfiler/PollingTime', now - poll_start)
251
+ end
252
+
253
+ def record_skew(poll_start)
254
+ if @last_poll
255
+ skew = poll_start - @last_poll - worker_loop.period
256
+ NewRelic::Agent.record_metric('Supportability/ThreadProfiler/Skew', skew)
257
+ end
258
+ @last_poll = poll_start
259
+ end
260
+
261
+ end
262
+ end
263
+ end
264
+ end