Class: NewRelic::Agent::Threading::BacktraceService

Inherits:
Object
  • Object
show all
Defined in:
lib/new_relic/agent/threading/backtrace_service.rb

Constant Summary collapse

ALL_TRANSACTIONS =
"**ALL**".freeze
MAX_BUFFER_LENGTH =
500

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(event_listener = nil) ⇒ BacktraceService

Returns a new instance of BacktraceService.



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 19

def initialize(event_listener=nil)
  @profiles = {}
  @buffer = {}

  # synchronizes access to @profiles and @buffer above
  @lock = Mutex.new

  @running = false
  @profile_agent_code = false
  @worker_loop = NewRelic::Agent::WorkerLoop.new

  # Memoize overhead % to avoid getting stale OR looked up every poll
  @overhead_percent_threshold = NewRelic::Agent.config[:'xray_session.max_profile_overhead']
  NewRelic::Agent.config.register_callback(:'xray_session.max_profile_overhead') do |new_value|
    @overhead_percent_threshold = new_value
  end

  if event_listener
    event_listener.subscribe(:transaction_finished, &method(:on_transaction_finished))
  end
end

Instance Attribute Details

#bufferObject (readonly)

Returns the value of attribute buffer.



15
16
17
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 15

def buffer
  @buffer
end

#effective_polling_periodObject

Returns the value of attribute effective_polling_period.



15
16
17
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 15

def effective_polling_period
  @effective_polling_period
end

#overhead_percent_thresholdObject (readonly)

Returns the value of attribute overhead_percent_threshold.



15
16
17
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 15

def overhead_percent_threshold
  @overhead_percent_threshold
end

#profile_agent_codeObject

Returns the value of attribute profile_agent_code.



17
18
19
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 17

def profile_agent_code
  @profile_agent_code
end

#profilesObject (readonly)

This method is expected to be called with @lock held.



172
173
174
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 172

def profiles
  @profiles
end

#worker_loopObject (readonly)

Returns the value of attribute worker_loop.



15
16
17
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 15

def worker_loop
  @worker_loop
end

#worker_threadObject

Returns the value of attribute worker_thread.



17
18
19
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 17

def worker_thread
  @worker_thread
end

Class Method Details

.is_supported?Boolean

Returns:

  • (Boolean)


11
12
13
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 11

def self.is_supported?
  RUBY_VERSION >= "1.9.2"
end

Instance Method Details

#adjust_polling_time(now, poll_start) ⇒ Object

If our overhead % exceeds the threshold, bump the next poll period relative to how much larger our overhead is than allowed



232
233
234
235
236
237
238
239
240
241
242
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 232

def adjust_polling_time(now, poll_start)
  duration = now - poll_start
  overhead_percent = duration / effective_polling_period

  if overhead_percent > self.overhead_percent_threshold
    scale_up_by = overhead_percent / self.overhead_percent_threshold
    worker_loop.period = effective_polling_period * scale_up_by
  else
    worker_loop.period = effective_polling_period
  end
end

#aggregate_backtraces(backtraces, name, start, duration, thread) ⇒ Object

This method is expected to be called with @lock held.



118
119
120
121
122
123
124
125
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 118

def aggregate_backtraces(backtraces, name, start, duration, thread)
  end_time = start + duration
  backtraces.each do |(timestamp, backtrace)|
    if timestamp >= start && timestamp < end_time
      @profiles[name].aggregate(backtrace, :request, thread)
    end
  end
end

#aggregate_global_backtrace(backtrace, bucket, thread) ⇒ Object

This method is expected to be called with @lock held.



202
203
204
205
206
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 202

def aggregate_global_backtrace(backtrace, bucket, thread)
  if @profiles[ALL_TRANSACTIONS]
    @profiles[ALL_TRANSACTIONS].aggregate(backtrace, bucket, thread)
  end
end

#buffer_backtrace_for_thread(thread, timestamp, backtrace, bucket) ⇒ Object

This method is expected to be called with @lock held.



190
191
192
193
194
195
196
197
198
199
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 190

def buffer_backtrace_for_thread(thread, timestamp, backtrace, bucket)
  if should_buffer?(bucket)
    @buffer[thread] ||= []
    if @buffer[thread].length < MAX_BUFFER_LENGTH
      @buffer[thread] << [timestamp, backtrace]
    else
      NewRelic::Agent.increment_metric('Supportability/XraySessions/DroppedBacktraces')
    end
  end
end

#find_effective_polling_periodObject

This method is expected to be called with @lock held.



221
222
223
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 221

def find_effective_polling_period
  @profiles.values.map { |p| p.requested_period }.min
end

#harvest(transaction_name) ⇒ Object



91
92
93
94
95
96
97
98
99
100
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 91

def harvest(transaction_name)
  @lock.synchronize do
    if @profiles[transaction_name]
      profile = @profiles.delete(transaction_name)
      profile.finished_at = Time.now
      @profiles[transaction_name] = ThreadProfile.new(profile.command_arguments)
      profile
    end
  end
end

#need_backtrace?(bucket) ⇒ Boolean

This method is expected to be called with @lock held.

Returns:

  • (Boolean)


180
181
182
183
184
185
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 180

def need_backtrace?(bucket)
  (
    bucket != :ignore &&
    (@profiles[ALL_TRANSACTIONS] || should_buffer?(bucket))
  )
end

#on_transaction_finished(payload) ⇒ Object



102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 102

def on_transaction_finished(payload)
  name     = payload[:name]
  start    = payload[:start_timestamp]
  duration = payload[:duration]
  thread   = payload[:thread] || Thread.current
  @lock.synchronize do
    backtraces = @buffer.delete(thread)
    if backtraces && @profiles.has_key?(name)
      aggregate_backtraces(backtraces, name, start, duration, thread)
    end
  end
end

#pollObject



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 155

def poll
  poll_start = Time.now

  @lock.synchronize do
    AgentThread.list.each do |thread|
      sample_thread(thread)
    end
    @profiles.values.each { |c| c.increment_poll_count }
    @buffer.delete_if { |thread, _| !thread.alive? }
  end

  end_time = Time.now
  adjust_polling_time(end_time, poll_start)
  record_supportability_metrics(end_time, poll_start)
end

#record_polling_time(now, poll_start) ⇒ Object



249
250
251
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 249

def record_polling_time(now, poll_start)
  NewRelic::Agent.record_metric('Supportability/ThreadProfiler/PollingTime', now - poll_start)
end

#record_skew(poll_start) ⇒ Object



253
254
255
256
257
258
259
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 253

def record_skew(poll_start)
  if @last_poll
    skew = poll_start - @last_poll - worker_loop.period
    NewRelic::Agent.record_metric('Supportability/ThreadProfiler/Skew', skew)
  end
  @last_poll = poll_start
end

#record_supportability_metrics(now, poll_start) ⇒ Object



244
245
246
247
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 244

def record_supportability_metrics(now, poll_start)
  record_polling_time(now, poll_start)
  record_skew(poll_start)
end

#running?Boolean

Public interface

Returns:

  • (Boolean)


43
44
45
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 43

def running?
  @running
end

#sample_thread(thread) ⇒ Object

This method is expected to be called with @lock held.



209
210
211
212
213
214
215
216
217
218
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 209

def sample_thread(thread)
  bucket = AgentThread.bucket_thread(thread, @profile_agent_code)

  if need_backtrace?(bucket)
    timestamp = Time.now.to_f
    backtrace = AgentThread.scrub_backtrace(thread, @profile_agent_code)
    aggregate_global_backtrace(backtrace, bucket, thread)
    buffer_backtrace_for_thread(thread, timestamp, backtrace, bucket)
  end
end

#should_buffer?(bucket) ⇒ Boolean

This method is expected to be called with @lock held.

Returns:

  • (Boolean)


175
176
177
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 175

def should_buffer?(bucket)
  bucket == :request && @profiles.keys.any? { |k| k != ALL_TRANSACTIONS }
end

#should_profile_agent_code?Boolean

This method is expected to be called with @lock held.

Returns:

  • (Boolean)


226
227
228
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 226

def should_profile_agent_code?
  @profiles.values.any? { |p| p.profile_agent_code }
end

#startObject



127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 127

def start
  return if @running || !self.class.is_supported?

  @running = true
  self.worker_thread = AgentThread.new('Backtrace Service') do
    begin
      # Not passing period because we expect it's already been set.
      self.worker_loop.run(&method(:poll))
    ensure
      NewRelic::Agent.logger.debug("Exiting New Relic thread: Backtrace Service")
    end
  end
end

#stopObject

This method is expected to be called with @lock held



142
143
144
145
146
147
148
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 142

def stop
  return unless @running
  @running = false
  self.worker_loop.stop

  @buffer = {}
end

#subscribe(transaction_name, command_arguments = {}) ⇒ Object



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 47

def subscribe(transaction_name, command_arguments={})
  if !self.class.is_supported?
    NewRelic::Agent.logger.debug("Backtracing not supported, so not subscribing transaction '#{transaction_name}'")
    return
  end

  NewRelic::Agent.logger.debug("Backtrace Service subscribing transaction '#{transaction_name}'")

  profile = ThreadProfile.new(command_arguments)

  @lock.synchronize do
    @profiles[transaction_name] = profile
    update_values_from_profiles
  end

  start
  profile
end

#subscribed?(transaction_name) ⇒ Boolean

Returns:

  • (Boolean)


85
86
87
88
89
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 85

def subscribed?(transaction_name)
  @lock.synchronize do
    @profiles.has_key?(transaction_name)
  end
end

#unsubscribe(transaction_name) ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 66

def unsubscribe(transaction_name)
  return unless self.class.is_supported?

  NewRelic::Agent.logger.debug("Backtrace Service unsubscribing transaction '#{transaction_name}'")
  @lock.synchronize do
    @profiles.delete(transaction_name)
    if @profiles.empty?
      stop
    else
      update_values_from_profiles
    end
  end
end

#update_values_from_profilesObject



80
81
82
83
# File 'lib/new_relic/agent/threading/backtrace_service.rb', line 80

def update_values_from_profiles
  self.effective_polling_period = find_effective_polling_period
  self.profile_agent_code = should_profile_agent_code?
end