Performance Profiling Stack
Date: November 26, 2025 (Updated: November 30, 2025)
Category: Development Tools
Status: Active
Overview
Section titled “Overview”A modern, open-source performance debugging stack for Rails development and local analysis. These tools complement AppSignal which provides production APM.
Development vs Production
Section titled “Development vs Production”| Environment | Tools | Purpose |
|---|---|---|
| Development | rack-mini-profiler, Prosopite, PgHero | Interactive debugging, N+1 detection |
| Production | AppSignal APM | Request tracing, error tracking, host metrics |
Components
Section titled “Components”1. Rack Mini Profiler
Section titled “1. Rack Mini Profiler”Purpose: Visual performance dashboard showing request timing breakdown
# Gemfile (development group)gem 'rack-mini-profiler'gem 'stackprof' # Enables flamegraphsgem 'memory_profiler' # Memory leak detectiongem 'flamegraph' # Flamegraph visualizationUsage:
- Enable with
DEV_TRACE=trueenvironment variable - Speed badge appears in top-left corner of pages
- Click badge for detailed breakdown (SQL, Ruby, Views)
- Click “flamegraph” for CPU profiling visualization
Configuration: config/initializers/rack_mini_profiler.rb
Rack::MiniProfiler.config.enabled = ENV.fetch('DEV_TRACE', false).to_bRack::MiniProfiler.config.position = 'top-left'Rack::MiniProfiler.config.enable_advanced_debugging_tools = true if defined?(StackProf)Excluded Paths:
/assets,/packs,/javascripts/webpack/sidekiq,/pghero,/letter_opener/v1,/api/cable,/media
2. Prosopite (N+1 Query Detection)
Section titled “2. Prosopite (N+1 Query Detection)”Purpose: Detect N+1 queries with zero false positives
Configuration: config/initializers/prosopite.rb
Prosopite.prosopite_logger = Rails.loggerProsopite.ignore_queries = [ /sidekiq/i, /pghero/i, /devise/i, /paper_trail/i, %r{app/workers/}, %r{app/mailers/}, %r{lib/tasks/} # ... more patterns]How it works:
- Monitors transaction call stacks (more accurate than Bullet)
- Logs warnings to Rails logger in development
- Can raise exceptions in test environment (disabled by default)
3. PgHero (PostgreSQL Dashboard)
Section titled “3. PgHero (PostgreSQL Dashboard)”Purpose: PostgreSQL performance analysis and index recommendations
Access: https://crm.warmlyyours.me:3000/pghero (admin login required)
Features:
- Slow query detection
- Index suggestions (with hypopg for what-if analysis)
- Connection monitoring
- Vacuum health
- Space usage analysis
- EXPLAIN ANALYZE for queries
Configuration: config/pghero.yml
Required PostgreSQL Extensions:
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;CREATE EXTENSION IF NOT EXISTS hypopg;4. AppSignal APM (Production)
Section titled “4. AppSignal APM (Production)”Purpose: Production performance monitoring, error tracking, and host metrics
Access: https://appsignal.com/warmlyyours
Features:
- Request performance tracking with timing breakdown
- Slow endpoint detection
- Error tracking with stack traces
- Host metrics (CPU, memory, disk)
- Deploy markers for correlation
- Anomaly detection and alerting
Configuration: config/appsignal.rb
Documentation: Infrastructure/MONITORING.md
Quick Reference
Section titled “Quick Reference”| Tool | Environment | Enable | Access |
|---|---|---|---|
| rack-mini-profiler | Development | DEV_TRACE=true rails s | Speed badge (top-left) |
| Flamegraphs | Development | Click badge → “flamegraph” | In mini-profiler |
| Prosopite | Development | Automatic | Check Rails logs for N+1 warnings |
| PgHero | Dev/Staging | Automatic | /pghero (admin only) |
| memory_profiler | Development | bin/rails memory:expr EXPR='code' or manual | stdout report (or pipe to file) |
| AppSignal | Production/Staging | Automatic | https://appsignal.com/warmlyyours |
When to Use Each Tool
Section titled “When to Use Each Tool”| Scenario | Tool |
|---|---|
| Debug slow page in development | rack-mini-profiler |
| Find N+1 queries while coding | Prosopite (check Rails logs) |
| Analyze database performance | PgHero |
| CPU profiling / hotspots | Flamegraphs (via mini-profiler) |
| Memory leak investigation | memory_profiler (bin/rails memory:expr or ?_profile_memory=1 on staging) |
| Per-gem startup cost | bundle exec derailed bundle:mem |
| Production slow endpoints | AppSignal APM |
| Production errors | AppSignal Error Tracking |
| Post-deploy performance check | AppSignal Deploy Markers |
Memory Profiling Recipes
Section titled “Memory Profiling Recipes”The memory_profiler gem ships in dev/test bundles. Three entry points:
1. From a controller/action on staging
Section titled “1. From a controller/action on staging”Append ?_profile_memory=1 to any URL. The RequestMemoryTracer
middleware (config/initializers/request_memory_tracer.rb) wraps the
request in MemoryProfiler.report and writes the report inside the
running Kamal app container to
/var/www/heatwave/shared/log/memory_profiler/<timestamp>-<path>.txt
(CRASH_LOG_DIR is still hardcoded to that legacy Capistrano-era path in
the initializer; under Kamal/Docker it’s a plain container-internal
directory, not a host bind-mount). Pull a report off the staging host with
kamal app exec --reuse "cat /var/www/heatwave/shared/log/memory_profiler/<file>"
or docker cp <container>:/var/www/heatwave/shared/log/memory_profiler/. ..
Rate-limited to one capture per 60s per worker so you can’t accidentally
DoS the staging servers.
2. From the command line — arbitrary expression
Section titled “2. From the command line — arbitrary expression”bin/rails memory:expr EXPR='Order.pending.includes(:customer).to_a'bin/rails memory:expr EXPR='Search.find(123).search_results.to_a' MAX=253. From the command line — wrap another rake task
Section titled “3. From the command line — wrap another rake task”bin/rails memory:rake TASK=ahrefs:stats4. Boot-time gem analysis
Section titled “4. Boot-time gem analysis”bundle exec derailed bundle:mem # how much each gem costs to requirebundle exec derailed bundle:objects # objects allocated per gemConcrete patterns to grep for memory wins
Section titled “Concrete patterns to grep for memory wins”When auditing a hot path that allocates too much:
Model.where(...).map(&:id)→Model.where(...).pluck(:id)(≈90% allocation reduction per the pawelurbanek.com benchmark).Model.where(...).map(&:single_column)→pluck(:single_column).Model.where(...).map { |r| [r.id, r.name] }→pluck(:id, :name).- Need a few model methods but not full records?
.select(:id, :col_a, :col_b)returns lightweight model instances (~65% reduction); accessing unfetched columns raisesActiveModel::MissingAttributeErrorso this catches over-fetching at runtime. Model.all.each→find_each(orin_batches) for any iteration over more than a few hundred records.- Loading full records just to call
.count/.exists?→ use those query methods directly.
To find candidates in this repo:
grep -rn "\.map(&:id)\|\.map(&:name)\|\.map(&:email)\|\.map(&:sku)" app/ lib/| File | Purpose |
|---|---|
config/initializers/rack_mini_profiler.rb | Mini profiler config |
config/initializers/prosopite.rb | N+1 detection config |
config/initializers/pghero.rb | PgHero controller setup |
config/pghero.yml | PgHero database config |
config/appsignal.rb | AppSignal APM config |
Removed
Section titled “Removed”scout_apmgem (replaced by this stack + AppSignal)config/scout_apm.yml.obsolete
See Also
Section titled “See Also”- AppSignal Monitoring — Production APM & error tracking
- AppSignal MCP Prompts — AI-assisted error investigation
- PgHero GitHub
- rack-mini-profiler
- Prosopite