-
-
Notifications
You must be signed in to change notification settings - Fork 493
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Introduce `Sentry::Vernier::Profiler` - Introduce `Sentry.config.profiler` that can be set to a class that should be used for profiling. By default it's set to `Sentry::Profiler`
- Loading branch information
Showing
12 changed files
with
699 additions
and
97 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
# frozen_string_literal: true | ||
|
||
require "securerandom" | ||
|
||
module Sentry | ||
class Profiler | ||
module Helpers | ||
def in_app?(abs_path) | ||
abs_path.match?(@in_app_pattern) | ||
end | ||
|
||
# copied from stacktrace.rb since I don't want to touch existing code | ||
# TODO-neel-profiler try to fetch this from stackprof once we patch | ||
# the native extension | ||
def compute_filename(abs_path, in_app) | ||
return nil if abs_path.nil? | ||
|
||
under_project_root = @project_root && abs_path.start_with?(@project_root) | ||
|
||
prefix = | ||
if under_project_root && in_app | ||
@project_root | ||
else | ||
longest_load_path = $LOAD_PATH.select { |path| abs_path.start_with?(path.to_s) }.max_by(&:size) | ||
|
||
if under_project_root | ||
longest_load_path || @project_root | ||
else | ||
longest_load_path | ||
end | ||
end | ||
|
||
prefix ? abs_path[prefix.to_s.chomp(File::SEPARATOR).length + 1..-1] : abs_path | ||
end | ||
|
||
def split_module(name) | ||
# last module plus class/instance method | ||
i = name.rindex("::") | ||
function = i ? name[(i + 2)..-1] : name | ||
mod = i ? name[0...i] : nil | ||
|
||
[function, mod] | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
# frozen_string_literal: true | ||
|
||
require "json" | ||
require "rbconfig" | ||
|
||
module Sentry | ||
module Vernier | ||
class Output | ||
include Profiler::Helpers | ||
|
||
attr_reader :profile | ||
|
||
def initialize(profile, project_root:, in_app_pattern:, app_dirs_pattern:) | ||
@profile = profile | ||
@project_root = project_root | ||
@in_app_pattern = in_app_pattern | ||
@app_dirs_pattern = app_dirs_pattern | ||
end | ||
|
||
def to_h | ||
@to_h ||= { | ||
frames: frames, | ||
stacks: stacks, | ||
samples: samples, | ||
thread_metadata: thread_metadata | ||
} | ||
end | ||
|
||
private | ||
|
||
def thread_metadata | ||
profile.threads.map { |thread_id, thread_info| | ||
[thread_id, { name: thread_info[:name] }] | ||
}.to_h | ||
end | ||
|
||
def samples | ||
profile.threads.flat_map { |thread_id, thread_info| | ||
started_at = thread_info[:started_at] | ||
samples, timestamps = thread_info.values_at(:samples, :timestamps) | ||
|
||
samples.zip(timestamps).map { |stack_id, timestamp| | ||
elapsed_since_start_ns = timestamp - started_at | ||
|
||
next if elapsed_since_start_ns < 0 | ||
|
||
{ | ||
thread_id: thread_id.to_s, | ||
stack_id: stack_id, | ||
elapsed_since_start_ns: elapsed_since_start_ns.to_s | ||
} | ||
}.compact | ||
} | ||
end | ||
|
||
def frames | ||
funcs = stack_table_hash[:frame_table].fetch(:func) | ||
lines = stack_table_hash[:func_table].fetch(:first_line) | ||
|
||
funcs.map { |idx| | ||
function, mod = split_module(stack_table_hash[:func_table][:name][idx]) | ||
|
||
abs_path = stack_table_hash[:func_table][:filename][idx] | ||
in_app = in_app?(abs_path) | ||
filename = compute_filename(abs_path, in_app) | ||
|
||
{ | ||
function: function, | ||
module: mod, | ||
filename: filename, | ||
abs_path: abs_path, | ||
lineno: (lineno = lines[idx]) > 0 ? lineno : nil, | ||
in_app: in_app | ||
}.compact | ||
} | ||
end | ||
|
||
def stacks | ||
sample_stacks = profile.threads.flat_map { |_, thread_info| | ||
samples = thread_info[:samples].map { |stack_id| profile.stack(stack_id) } | ||
|
||
samples.map { |stack| [stack.idx, stack.frames.map(&:idx)] } | ||
}.to_h | ||
|
||
stacks = profile._stack_table.stack_count.times.map do |stack_id| | ||
(sample_stacks[stack_id] || profile.stack(stack_id).frames.map(&:idx)) | ||
end | ||
|
||
stacks | ||
end | ||
|
||
def stack_table_hash | ||
@stack_table_hash ||= profile._stack_table.to_h | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
# frozen_string_literal: true | ||
|
||
require "securerandom" | ||
require_relative "../profiler/helpers" | ||
require_relative "output" | ||
|
||
begin | ||
require "vernier" | ||
rescue LoadError | ||
end | ||
|
||
module Sentry | ||
module Vernier | ||
class Profiler | ||
EMPTY_RESULT = {}.freeze | ||
|
||
attr_reader :started, :event_id, :result | ||
|
||
def initialize(configuration) | ||
@event_id = SecureRandom.uuid.delete("-") | ||
|
||
@started = false | ||
@sampled = nil | ||
|
||
@profiling_enabled = defined?(Vernier) && configuration.profiling_enabled? | ||
@profiles_sample_rate = configuration.profiles_sample_rate | ||
@project_root = configuration.project_root | ||
@app_dirs_pattern = configuration.app_dirs_pattern || Backtrace::APP_DIRS_PATTERN | ||
@in_app_pattern = Regexp.new("^(#{@project_root}/)?#{@app_dirs_pattern}") | ||
end | ||
|
||
def set_initial_sample_decision(transaction_sampled) | ||
unless @profiling_enabled | ||
@sampled = false | ||
return | ||
end | ||
|
||
unless transaction_sampled | ||
@sampled = false | ||
log("Discarding profile because transaction not sampled") | ||
return | ||
end | ||
|
||
case @profiles_sample_rate | ||
when 0.0 | ||
@sampled = false | ||
log("Discarding profile because sample_rate is 0") | ||
return | ||
when 1.0 | ||
@sampled = true | ||
return | ||
else | ||
@sampled = Random.rand < @profiles_sample_rate | ||
end | ||
|
||
log("Discarding profile due to sampling decision") unless @sampled | ||
end | ||
|
||
def start | ||
return unless @sampled && !@started | ||
|
||
::Vernier.start_profile | ||
@started = true | ||
|
||
log("Started") | ||
|
||
@started | ||
rescue RuntimeError => e | ||
# TODO: once Vernier raises something more dedicated, we should catch that instead | ||
if e.message.include?("Profile already started") | ||
log("Not started since running elsewhere") | ||
else | ||
log("Failed to start: #{e.message}") | ||
raise e | ||
end | ||
end | ||
|
||
def stop | ||
return unless @sampled | ||
return unless @started | ||
|
||
@result = ::Vernier.stop_profile | ||
|
||
log("Stopped") | ||
end | ||
|
||
def to_hash | ||
return EMPTY_RESULT unless @started | ||
|
||
unless @sampled | ||
record_lost_event(:sample_rate) | ||
return EMPTY_RESULT | ||
end | ||
|
||
{ **profile_meta, profile: output.to_h } | ||
end | ||
|
||
private | ||
|
||
def log(message) | ||
Sentry.logger.debug(LOGGER_PROGNAME) { "[Profiler::Vernier] #{message}" } | ||
end | ||
|
||
def record_lost_event(reason) | ||
Sentry.get_current_client&.transport&.record_lost_event(reason, "profile") | ||
end | ||
|
||
def profile_meta | ||
{ | ||
event_id: @event_id, | ||
version: "1", | ||
platform: "ruby" | ||
} | ||
end | ||
|
||
def output | ||
@output ||= Output.new( | ||
result, | ||
project_root: @project_root, | ||
app_dirs_pattern: @app_dirs_pattern, | ||
in_app_pattern: @in_app_pattern | ||
) | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.