Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 539
Add stackprof based profiler#2024
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
77d645bd5ca8c4dbc504d1b5046d621414ae07925f6ebe2b7f44a6ef0eab247cc8a4afe99575b7c7139ccd5dc0a207fc04d345a3a227e5d3d6ba93fec0a24fFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,222 @@ | ||
| # frozen_string_literal: true | ||
| require 'securerandom' | ||
| module Sentry | ||
| class Profiler | ||
| VERSION = '1' | ||
| PLATFORM = 'ruby' | ||
| # 101 Hz in microseconds | ||
| DEFAULT_INTERVAL = 1e6 / 101 | ||
| MICRO_TO_NANO_SECONDS = 1e3 | ||
| attr_reader :sampled, :started, :event_id | ||
| def initialize(configuration) | ||
| @event_id = SecureRandom.uuid.delete('-') | ||
| @started = false | ||
| @sampled = nil | ||
| @profiling_enabled = defined?(StackProf) && 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 start | ||
| return unless @sampled | ||
| @started = StackProf.start(interval: DEFAULT_INTERVAL, | ||
| mode: :wall, | ||
| raw: true, | ||
| aggregate: false) | ||
| @started ? log('Started') : log('Not started since running elsewhere') | ||
| end | ||
| def stop | ||
| return unless @sampled | ||
| return unless @started | ||
| StackProf.stop | ||
| log('Stopped') | ||
| end | ||
| # Sets initial sampling decision of the profile. | ||
| # @return [void] | ||
| 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 to_hash | ||
| return {} unless @sampled | ||
| return {} unless @started | ||
| results = StackProf.results | ||
| return {} unless results | ||
| return {} if results.empty? | ||
| return {} if results[:samples] == 0 | ||
| return {} unless results[:raw] | ||
| frame_map = {} | ||
| frames = results[:frames].to_enum.with_index.map do |frame, idx| | ||
Zylphrex marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| frame_id, frame_data = frame | ||
| # need to map over stackprof frame ids to ours | ||
| frame_map[frame_id] = idx | ||
| file_path = frame_data[:file] | ||
| in_app = in_app?(file_path) | ||
| filename = compute_filename(file_path, in_app) | ||
| function, mod = split_module(frame_data[:name]) | ||
| frame_hash = { | ||
| abs_path: file_path, | ||
| function: function, | ||
| filename: filename, | ||
| in_app: in_app | ||
| } | ||
| frame_hash[:module] = mod if mod | ||
| frame_hash[:lineno] = frame_data[:line] if frame_data[:line] | ||
| frame_hash | ||
| end | ||
| idx = 0 | ||
| stacks = [] | ||
| num_seen = [] | ||
| # extract stacks from raw | ||
| # raw is a single array of [.., len_stack, *stack_frames(len_stack), num_stack_seen , ..] | ||
| while (len = results[:raw][idx]) | ||
| idx += 1 | ||
| # our call graph is reversed | ||
| stack = results[:raw].slice(idx, len).map { |id| frame_map[id] }.compact.reverse | ||
| stacks << stack | ||
| num_seen << results[:raw][idx + len] | ||
| idx += len + 1 | ||
| log('Unknown frame in stack') if stack.size != len | ||
| end | ||
Zylphrex marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| idx = 0 | ||
| elapsed_since_start_ns = 0 | ||
| samples = [] | ||
| num_seen.each_with_index do |n, i| | ||
| n.times do | ||
| # stackprof deltas are in microseconds | ||
| delta = results[:raw_timestamp_deltas][idx] | ||
| elapsed_since_start_ns += (delta * MICRO_TO_NANO_SECONDS).to_i | ||
| idx += 1 | ||
| # Not sure why but some deltas are very small like 0/1 values, | ||
| # they pollute our flamegraph so just ignore them for now. | ||
| # Open issue at https://github.com/tmm1/stackprof/issues/201 | ||
| next if delta < 10 | ||
| samples << { | ||
| stack_id: i, | ||
| # TODO-neel-profiler we need to patch rb_profile_frames and write our own C extension to enable threading info. | ||
| # Till then, on multi-threaded servers like puma, we will get frames from other active threads when the one | ||
| # we're profiling is idle/sleeping/waiting for IO etc. | ||
| # https://bugs.ruby-lang.org/issues/10602 | ||
| thread_id: '0', | ||
| elapsed_since_start_ns: elapsed_since_start_ns.to_s | ||
| } | ||
| end | ||
| end | ||
| log('Some samples thrown away') if samples.size != results[:samples] | ||
| if samples.size <= 2 | ||
Zylphrex marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| log('Not enough samples, discarding profiler') | ||
| return {} | ||
| end | ||
| profile = { | ||
| frames: frames, | ||
| stacks: stacks, | ||
| samples: samples | ||
| } | ||
| { | ||
| event_id: @event_id, | ||
| platform: PLATFORM, | ||
| version: VERSION, | ||
| profile: profile | ||
| } | ||
| end | ||
| private | ||
| def log(message) | ||
| Sentry.logger.debug(LOGGER_PROGNAME) { "[Profiler] #{message}" } | ||
| end | ||
| 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 | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.