class NewRelic::Agent::Threading::BacktraceService

Constants

ALL_TRANSACTIONS
MAX_BUFFER_LENGTH

Attributes

buffer[R]
effective_polling_period[R]
overhead_percent_threshold[R]
profile_agent_code[RW]
profiles[R]

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

worker_loop[R]
worker_thread[RW]

Public Class Methods

is_resque?() click to toggle source

Because of Resque’s forking, we don’t poll thread backtraces for it. To accomplish that would require starting a new backtracing thread in each forked worker, and merging profiles across the pipe channel.

# File lib/new_relic/agent/threading/backtrace_service.rb, line 18
def self.is_resque?
  NewRelic::Agent.config[:dispatcher] == :resque
end
is_supported?() click to toggle source
# File lib/new_relic/agent/threading/backtrace_service.rb, line 11
def self.is_supported?
  !is_resque?
end
new(event_listener = nil) click to toggle source
# File lib/new_relic/agent/threading/backtrace_service.rb, line 26
def initialize(event_listener = nil)
  @profiles = {}
  @buffer = {}
  @last_poll = nil

  # 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[:'thread_profiler.max_profile_overhead']
  NewRelic::Agent.config.register_callback(:'thread_profiler.max_profile_overhead') do |new_value|
    @overhead_percent_threshold = new_value
  end

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

Public Instance Methods

adjust_polling_time(now, poll_start) click to toggle source

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

# File lib/new_relic/agent/threading/backtrace_service.rb, line 249
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, bucket, thread) click to toggle source

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

# File lib/new_relic/agent/threading/backtrace_service.rb, line 130
def aggregate_backtraces(backtraces, name, start, duration, bucket, thread)
  end_time = start + duration
  backtraces.each do |(timestamp, backtrace)|
    if timestamp >= start && timestamp < end_time
      @profiles[name].aggregate(backtrace, bucket, thread)
    end
  end
end
aggregate_global_backtrace(backtrace, bucket, thread) click to toggle source

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

# File lib/new_relic/agent/threading/backtrace_service.rb, line 221
def aggregate_global_backtrace(backtrace, bucket, thread)
  @profiles[ALL_TRANSACTIONS]&.aggregate(backtrace, bucket, thread)
end
allowed_bucket?(bucket) click to toggle source
# File lib/new_relic/agent/threading/backtrace_service.rb, line 202
def allowed_bucket?(bucket)
  bucket == :request || bucket == :background
end
buffer_backtrace_for_thread(thread, timestamp, backtrace, bucket) click to toggle source

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

# File lib/new_relic/agent/threading/backtrace_service.rb, line 209
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/ThreadProfiler/DroppedBacktraces')
    end
  end
end
effective_polling_period=(new_period) click to toggle source
# File lib/new_relic/agent/threading/backtrace_service.rb, line 159
def effective_polling_period=(new_period)
  @effective_polling_period = new_period
  self.worker_loop.period = new_period
end
find_effective_polling_period() click to toggle source

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

# File lib/new_relic/agent/threading/backtrace_service.rb, line 238
def find_effective_polling_period
  @profiles.values.map { |p| p.requested_period }.min
end
harvest(transaction_name) click to toggle source
# File lib/new_relic/agent/threading/backtrace_service.rb, line 102
def harvest(transaction_name)
  @lock.synchronize do
    if @profiles[transaction_name]
      profile = @profiles.delete(transaction_name)
      profile.finished_at = Process.clock_gettime(Process::CLOCK_REALTIME)
      @profiles[transaction_name] = ThreadProfile.new(profile.command_arguments)
      profile
    end
  end
end
need_backtrace?(bucket) click to toggle source

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

# File lib/new_relic/agent/threading/backtrace_service.rb, line 189
def need_backtrace?(bucket)
  (
    bucket != :ignore &&
    (@profiles[ALL_TRANSACTIONS] || should_buffer?(bucket))
  )
end
on_transaction_finished(payload) click to toggle source
# File lib/new_relic/agent/threading/backtrace_service.rb, line 113
def on_transaction_finished(payload)
  name = payload[:name]
  start = payload[:start_timestamp]
  duration = payload[:duration]
  thread = payload[:thread] || Thread.current
  bucket = payload[:bucket]
  @lock.synchronize do
    backtraces = @buffer.delete(thread)
    if backtraces && @profiles.has_key?(name)
      aggregate_backtraces(backtraces, name, start, duration, bucket, thread)
    end
  end
end
poll() click to toggle source
# File lib/new_relic/agent/threading/backtrace_service.rb, line 164
def poll
  poll_start = Process.clock_gettime(Process::CLOCK_REALTIME)

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

  end_time = Process.clock_gettime(Process::CLOCK_REALTIME)
  adjust_polling_time(end_time, poll_start)
  record_supportability_metrics(end_time, poll_start)
end
record_polling_time(now, poll_start) click to toggle source
# File lib/new_relic/agent/threading/backtrace_service.rb, line 266
def record_polling_time(now, poll_start)
  NewRelic::Agent.record_metric('Supportability/ThreadProfiler/PollingTime', now - poll_start)
end
record_skew(poll_start) click to toggle source
# File lib/new_relic/agent/threading/backtrace_service.rb, line 270
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) click to toggle source
# File lib/new_relic/agent/threading/backtrace_service.rb, line 261
def record_supportability_metrics(now, poll_start)
  record_polling_time(now, poll_start)
  record_skew(poll_start)
end
running?() click to toggle source

Public interface

# File lib/new_relic/agent/threading/backtrace_service.rb, line 49
def running?
  @running
end
sample_thread(thread) click to toggle source

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

# File lib/new_relic/agent/threading/backtrace_service.rb, line 226
def sample_thread(thread)
  bucket = AgentThread.bucket_thread(thread, @profile_agent_code)

  if need_backtrace?(bucket)
    timestamp = Process.clock_gettime(Process::CLOCK_REALTIME)
    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) click to toggle source

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

# File lib/new_relic/agent/threading/backtrace_service.rb, line 184
def should_buffer?(bucket)
  allowed_bucket?(bucket) && watching_for_transaction?
end
should_profile_agent_code?() click to toggle source

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

# File lib/new_relic/agent/threading/backtrace_service.rb, line 243
def should_profile_agent_code?
  @profiles.values.any? { |p| p.profile_agent_code }
end
start() click to toggle source
# File lib/new_relic/agent/threading/backtrace_service.rb, line 139
def start
  return if @running || !self.class.is_supported?

  @running = true
  self.worker_thread = AgentThread.create('Backtrace Service') do
    # Not passing period because we expect it's already been set.
    self.worker_loop.run(&method(:poll))
  end
end
stop() click to toggle source

This method is expected to be called with @lock held

# File lib/new_relic/agent/threading/backtrace_service.rb, line 150
def stop
  return unless @running

  @running = false
  self.worker_loop.stop

  @buffer = {}
end
subscribe(transaction_name, command_arguments = {}) click to toggle source
# File lib/new_relic/agent/threading/backtrace_service.rb, line 53
def subscribe(transaction_name, command_arguments = {})
  if self.class.is_resque?
    NewRelic::Agent.logger.info("Backtracing threads on Resque is not supported, so not subscribing transaction '#{transaction_name}'")
    return
  end

  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) click to toggle source
# File lib/new_relic/agent/threading/backtrace_service.rb, line 96
def subscribed?(transaction_name)
  @lock.synchronize do
    @profiles.has_key?(transaction_name)
  end
end
unsubscribe(transaction_name) click to toggle source
# File lib/new_relic/agent/threading/backtrace_service.rb, line 77
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_profiles() click to toggle source
# File lib/new_relic/agent/threading/backtrace_service.rb, line 91
def update_values_from_profiles
  self.effective_polling_period = find_effective_polling_period
  self.profile_agent_code = should_profile_agent_code?
end
watching_for_transaction?() click to toggle source

This method is expected to be called with @lock held

# File lib/new_relic/agent/threading/backtrace_service.rb, line 197
def watching_for_transaction?
  @profiles.size > 1 ||
    (@profiles.size == 1 && @profiles[ALL_TRANSACTIONS].nil?)
end