Commit ad034f1d authored by Sean McGivern's avatar Sean McGivern

Merge branch 'sh-support-stackprof-profiling' into 'master'

Add support stackprof in GitLab profiler

See merge request gitlab-org/gitlab!82249
parents a138c275 4c42d3ac
......@@ -91,6 +91,73 @@ printer = RubyProf::CallStackPrinter.new(result)
printer.print(File.open('/tmp/profile.html', 'w'))
```
### Stackprof support
By default, `Gitlab::Profiler.profile` uses a tracing profiler called [`ruby-prof`](https://ruby-prof.github.io/). However, sampling profilers
[run faster and use less memory](https://jvns.ca/blog/2017/12/17/how-do-ruby---python-profilers-work-/), so they might be preferred.
You can switch to [Stackprof](https://github.com/tmm1/stackprof) (a sampling profiler) to generate a profile by passing `sampling_mode: true`.
Pass in a `profiler_options` hash to configure the output file (`out`) of the sampling data. For example:
```ruby
Gitlab::Profiler.profile('/gitlab-org/gitlab-test', user: User.first, sampling_mode: true, profiler_options: { out: 'tmp/profile.dump' })
```
You can get a summary of where time was spent by running Stackprof against the sampling data. For example:
```shell
stackprof tmp/profile.dump
```
Example sampling data:
```plaintext
==================================
Mode: wall(1000)
Samples: 8745 (6.92% miss rate)
GC: 1399 (16.00%)
==================================
TOTAL (pct) SAMPLES (pct) FRAME
1022 (11.7%) 1022 (11.7%) Sprockets::PathUtils#stat
957 (10.9%) 957 (10.9%) (marking)
493 (5.6%) 493 (5.6%) Sprockets::PathUtils#entries
576 (6.6%) 471 (5.4%) Mustermann::AST::Translator#decorator_for
439 (5.0%) 439 (5.0%) (sweeping)
630 (7.2%) 241 (2.8%) Sprockets::Cache::FileStore#get
208 (2.4%) 208 (2.4%) ActiveSupport::FileUpdateChecker#watched
206 (2.4%) 206 (2.4%) Digest::Instance#file
544 (6.2%) 176 (2.0%) Sprockets::Cache::FileStore#safe_open
176 (2.0%) 176 (2.0%) ActiveSupport::FileUpdateChecker#max_mtime
268 (3.1%) 147 (1.7%) ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#exec_no_cache
140 (1.6%) 140 (1.6%) ActiveSupport::BacktraceCleaner#add_gem_filter
116 (1.3%) 116 (1.3%) Bootsnap::CompileCache::ISeq.storage_to_output
160 (1.8%) 113 (1.3%) Gem::Version#<=>
109 (1.2%) 109 (1.2%) block in <main>
108 (1.2%) 108 (1.2%) Gem::Version.new
131 (1.5%) 105 (1.2%) Sprockets::EncodingUtils#unmarshaled_deflated
1166 (13.3%) 82 (0.9%) Mustermann::RegexpBased#initialize
82 (0.9%) 78 (0.9%) FileUtils.touch
72 (0.8%) 72 (0.8%) Sprockets::Manifest.compile_match_filter
71 (0.8%) 70 (0.8%) Grape::Router#compile!
91 (1.0%) 65 (0.7%) ActiveRecord::ConnectionAdapters::PostgreSQL::DatabaseStatements#query
93 (1.1%) 64 (0.7%) ActionDispatch::Journey::Path::Pattern::AnchoredRegexp#accept
59 (0.7%) 59 (0.7%) Mustermann::AST::Translator.dispatch_table
62 (0.7%) 59 (0.7%) Rails::BacktraceCleaner#initialize
2492 (28.5%) 49 (0.6%) Sprockets::PathUtils#stat_directory
242 (2.8%) 49 (0.6%) Gitlab::Instrumentation::RedisBase.add_call_details
47 (0.5%) 47 (0.5%) URI::RFC2396_Parser#escape
46 (0.5%) 46 (0.5%) #<Class:0x00000001090c2e70>#__setobj__
44 (0.5%) 44 (0.5%) Sprockets::Base#normalize_logical_path
```
You can also generate flamegraphs:
```shell
stackprof --d3-flamegraph tmp/profile.dump > flamegraph.html
```
See [the Stackprof documentation](https://github.com/tmm1/stackprof) for more details.
## Speedscope flamegraphs
You can generate a flamegraph for a particular URL by selecting a flamegraph sampling mode button in the performance bar or by adding the `performance_bar=flamegraph` parameter to the request.
......
......@@ -28,7 +28,7 @@ module Gitlab
].freeze
# Takes a URL to profile (can be a fully-qualified URL, or an absolute path)
# and returns the ruby-prof profile result. Formatting that result is the
# and returns the profiler result. Formatting that result is the
# caller's responsibility. Requests are GET requests unless post_data is
# passed.
#
......@@ -43,7 +43,13 @@ module Gitlab
#
# - private_token: instead of providing a user instance, the token can be
# given as a string. Takes precedence over the user option.
def self.profile(url, logger: nil, post_data: nil, user: nil, private_token: nil)
#
# - sampling_mode: When true, uses a sampling profiler (StackProf) instead of a tracing profiler (RubyProf).
#
# - profiler_options: A keyword Hash of arguments passed to the profiler. Defaults by profiler type:
# RubyProf - {}
# StackProf - { mode: :wall, out: <some temporary file>, interval: 1000, raw: true }
def self.profile(url, logger: nil, post_data: nil, user: nil, private_token: nil, sampling_mode: false, profiler_options: {})
app = ActionDispatch::Integration::Session.new(Rails.application)
verb = :get
headers = {}
......@@ -75,7 +81,9 @@ module Gitlab
with_custom_logger(logger) do
with_user(user) do
RubyProf.profile { app.public_send(verb, url, params: post_data, headers: headers) } # rubocop:disable GitlabSecurity/PublicSend
with_profiler(sampling_mode, profiler_options) do
app.public_send(verb, url, params: post_data, headers: headers) # rubocop:disable GitlabSecurity/PublicSend
end
end
end
end
......@@ -172,5 +180,16 @@ module Gitlab
RubyProf::FlatPrinter.new(result).print($stdout, default_options.merge(options))
end
def self.with_profiler(sampling_mode, profiler_options)
if sampling_mode
require 'stackprof'
args = { mode: :wall, interval: 1000, raw: true }.merge!(profiler_options)
args[:out] ||= ::Tempfile.new(["profile-#{Time.now.to_i}-", ".dump"]).path
::StackProf.run(**args) { yield }
else
RubyProf.profile(**profiler_options) { yield }
end
end
end
end
......@@ -58,6 +58,30 @@ RSpec.describe Gitlab::Profiler do
described_class.profile('/', user: user, private_token: private_token)
end
context 'with sampling profiler' do
it 'generates sampling data' do
user = double(:user)
temp_data = Tempfile.new
expect(described_class).to receive(:with_user).with(user).and_call_original
described_class.profile('/', user: user, sampling_mode: true, profiler_options: { out: temp_data.path })
expect(File.stat(temp_data).size).to be > 0
File.unlink(temp_data)
end
it 'saves sampling data with a randomly-generated filename' do
user = double(:user)
expect(described_class).to receive(:with_user).with(user).and_call_original
result = described_class.profile('/', user: user, sampling_mode: true)
expect(result).to be_a(File)
expect(File.stat(result.path).size).to be > 0
File.unlink(result.path)
end
end
end
describe '.create_custom_logger' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment