Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ jobs:
ports:
- 6379:6379
memcached:
image: memcached:1.6.9
image: memcached:1.6
ports:
- 11211:11211
options: --health-cmd "timeout 5 bash -c 'cat < /dev/null > /dev/udp/127.0.0.1/11211'" --health-interval 10s --health-timeout 5s --health-retries 5
strategy:
fail-fast: false
matrix:
ruby: ["3.4", "3.3", "3.2"]
ruby: ["4.0", "3.4", "3.3", "3.2"]
rack: ["2.2.0", "3.1.0", "3.2.0"]
env:
RACK_VERSION: ${{ matrix.rack }}
Expand Down
Binary file added docs/screenshots/rmp_01_toolbar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/rmp_02_expanded.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/rmp_03_flamegraph.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
79 changes: 78 additions & 1 deletion lib/mini_profiler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,50 @@ def call(env)
env['HTTP_ACCEPT_ENCODING'] = 'identity' if config.suppress_encoding

if matches_action?('flamegraph', env) || matches_action?('async-flamegraph', env) || env['HTTP_REFERER'] =~ /pp=async-flamegraph/
if defined?(StackProf) && StackProf.respond_to?(:run)
profiler_param = action_parameters(env)['flamegraph_profiler']
stackprof_available = defined?(StackProf) && StackProf.respond_to?(:run)
use_rperf = if profiler_param
profiler_param == 'rperf'
else
config.flamegraph_profiler == :rperf ||
(config.flamegraph_profiler == :auto &&
!stackprof_available &&
defined?(Rperf) && Rperf.respond_to?(:start))
end

if use_rperf
if defined?(Rperf) && Rperf.respond_to?(:start)
current.measure = false
match_data = action_parameters(env)['flamegraph_sample_rate']
frequency = if match_data && !match_data[1].to_f.zero?
match_data[1].to_f
else
config.flamegraph_sample_rate
end
frequency = frequency.to_i
frequency = 500 if frequency < 1

mode_match = action_parameters(env)['flamegraph_mode']
mode = if mode_match && [:cpu, :wall].include?(mode_match.to_sym)
mode_match.to_sym
else
config.flamegraph_mode == :object ? :wall : config.flamegraph_mode
end

Rperf.start(mode: mode, frequency: frequency)
status, headers, body = @app.call(env)
raw_data = Rperf.stop
flamegraph = rperf_to_speedscope(raw_data)
else
message = "Please install the rperf gem and require it: add gem 'rperf' to your Gemfile"
status, headers, body = @app.call(env)
body.close if body.respond_to? :close

return client_settings.handle_cookie(
text_result(message, status: status, headers: headers)
)
end
elsif stackprof_available
# do not sully our profile with mini profiler timings
current.measure = false
match_data = action_parameters(env)['flamegraph_sample_rate']
Expand Down Expand Up @@ -619,6 +662,40 @@ def cache_control_value
86400
end

def rperf_to_speedscope(data)
frame_index = {}
frames = []
samples = []
weights = []

(data[:samples] || []).each do |stack_frames, weight_ns, _thread|
sample_indices = stack_frames.map do |file, name|
key = "#{file}\0#{name}"
unless frame_index.key?(key)
frame_index[key] = frames.size
frames << { "name" => name.to_s, "file" => file.to_s }
end
frame_index[key]
end
samples << sample_indices
weights << weight_ns.to_i
end

{
"$schema" => "https://www.speedscope.app/file-format-schema.json",
"profiles" => [{
"type" => "sampled",
"name" => "rperf #{data[:mode]}",
"unit" => "nanoseconds",
"startValue" => 0,
"endValue" => (data[:duration_ns] || 0).to_i,
"samples" => samples,
"weights" => weights,
}],
"shared" => { "frames" => frames },
}
end

private

def rails_route_from_path(path, method)
Expand Down
3 changes: 2 additions & 1 deletion lib/mini_profiler/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def self.default
@flamegraph_sample_rate = 0.5
@flamegraph_mode = :wall
@flamegraph_ignore_gc = false
@flamegraph_profiler = :auto
@storage_failure = Proc.new do |exception|
if @logger
@logger.warn("MiniProfiler storage failure: #{exception.message}")
Expand Down Expand Up @@ -77,7 +78,7 @@ def self.default
:storage_options, :user_provider, :enable_advanced_debugging_tools,
:skip_sql_param_names, :suppress_encoding, :max_sql_param_length,
:content_security_policy_nonce, :enable_hotwire_turbo_drive_support,
:flamegraph_mode, :flamegraph_ignore_gc, :profile_parameter
:flamegraph_mode, :flamegraph_ignore_gc, :flamegraph_profiler, :profile_parameter

# ui accessors
attr_accessor :collapse_results, :max_traces_to_show, :position,
Expand Down
2 changes: 1 addition & 1 deletion lib/mini_profiler/storage/memcache_store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def get_unviewed_ids(user)
end

def flush_tokens
@client.set("#{@prefix}-tokens", nil)
@client.delete("#{@prefix}-tokens")
end

def allowed_tokens
Expand Down
2 changes: 2 additions & 0 deletions lib/mini_profiler/views.rb
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ def help(client_settings, env)
#{make_link "flamegraph&flamegraph_sample_rate=1", env}: creates a flamegraph with the specified sample rate (in ms). Overrides value set in config
#{make_link "flamegraph&flamegraph_mode=cpu", env}: creates a flamegraph with the specified mode (one of cpu, wall, object, or custom). Overrides value set in config
#{make_link "flamegraph&flamegraph_ignore_gc=true", env}: ignore garbage collection frames in flamegraphs. Overrides value set in config
#{make_link "flamegraph&flamegraph_profiler=rperf", env} : flamegraph using rperf (requires the rperf gem, Ruby >= 3.4).
#{make_link "flamegraph&flamegraph_profiler=rperf&flamegraph_mode=wall", env} : rperf wall-mode flamegraph (shows GVL/GC time attribution).
#{make_link "flamegraph_embed", env} : a graph representing sampled activity (requires the stackprof gem), embedded resources for use on an intranet.
#{make_link "trace-exceptions", env} : will return all the spots where your application raises exceptions
#{make_link "analyze-memory", env} : will perform basic memory analysis of heap
Expand Down
4 changes: 2 additions & 2 deletions lib/mini_profiler_rails/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def self.initialize!(app)
c.skip_paths << wp_assets_path if wp_assets_path
end

unless Rails.env.development? || Rails.env.test?
unless Rails.env.local?
c.authorization_mode = :allow_authorized
end

Expand All @@ -55,7 +55,7 @@ def self.initialize!(app)
# Quiet the SQL stack traces
c.backtrace_remove = Rails.root.to_s + "/"
c.backtrace_includes = [/^\/?(app|config|lib|test)/]
c.skip_schema_queries = (Rails.env.development? || Rails.env.test?)
c.skip_schema_queries = (Rails.env.local?)

# Install the Middleware
app.middleware.insert(0, Rack::MiniProfiler)
Expand Down
33 changes: 33 additions & 0 deletions spec/lib/rperf_flamegraph_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

require "spec_helper"

RSpec.describe "rperf flamegraph integration" do # rubocop:disable RSpec/DescribeClass
before do
skip "rperf not available" unless defined?(Rperf) && Rperf.respond_to?(:start)
end

let(:profiler) { Rack::MiniProfiler.new(app) }
let(:app) { lambda { |_env| [200, {}, ["ok"]] } }

before do
Rack::MiniProfiler.reset_config
Rack::MiniProfiler.config.storage = Rack::MiniProfiler::MemoryStore
Rack::MiniProfiler.config.flamegraph_profiler = :rperf
end

it "returns error when rperf is not installed" do
hide_const("Rperf")
response = profiler.call({ "PATH_INFO" => "/", "QUERY_STRING" => "pp=flamegraph" })
expect(response[2]).to include("Please install the rperf gem")
end

it "generates valid speedscope JSON for flamegraph" do
require "rperf"
Rack::MiniProfiler.config.flamegraph_profiler = :rperf
response = profiler.call({ "PATH_INFO" => "/", "QUERY_STRING" => "pp=flamegraph" })
expect(response[0]).to eq(200)
body = response[2].join
expect(body).to include("speedscope")
end
end