Performance Check
by GangWooLee
Performance monitoring and optimization. Use when user needs N+1 detection, slow query analysis, memory profiling, or says "performance issue", "slow queries", "N+1 problem", "optimize performance", "memory leak".
Skill Details
Repository Files
2 files in this skill directory
name: performance-check description: Performance monitoring and optimization. Use when user needs N+1 detection, slow query analysis, memory profiling, or says "performance issue", "slow queries", "N+1 problem", "optimize performance", "memory leak".
Performance Check
N+1 쿼리 감지, 느린 쿼리 분석, 메모리 프로파일링 등 성능 최적화 작업을 수행합니다.
Quick Start
Task Progress (copy and check off):
- [ ] 1. Identify performance issue
- [ ] 2. Profile the problem
- [ ] 3. Analyze results
- [ ] 4. Apply optimization
- [ ] 5. Measure improvement
Performance Tools
1. Bullet (N+1 Query Detection)
N+1 쿼리를 자동으로 감지하고 경고합니다.
Installation:
# Gemfile
group :development do
gem 'bullet'
end
Configuration:
# config/environments/development.rb
config.after_initialize do
Bullet.enable = true
Bullet.alert = true # JavaScript alert
Bullet.bullet_logger = true # Log to bullet.log
Bullet.console = true # Browser console
Bullet.rails_logger = true # Rails log
Bullet.add_footer = true # Bottom of page
# Alert on N+1 queries
Bullet.n_plus_one_query_enable = true
# Alert on unused eager loading
Bullet.unused_eager_loading_enable = true
# Alert on missing counter cache
Bullet.counter_cache_enable = true
end
Example N+1 Problem:
# ❌ N+1 Query (1 query for posts + N queries for users)
@posts = Post.all
@posts.each do |post|
puts post.user.name # N queries!
end
# ✅ Fixed with includes
@posts = Post.includes(:user).all
@posts.each do |post|
puts post.user.name # 2 queries total
end
2. Rack Mini Profiler
페이지 로딩 시간과 쿼리를 실시간으로 프로파일링합니다.
Installation:
# Gemfile
group :development do
gem 'rack-mini-profiler'
gem 'memory_profiler' # Memory profiling
gem 'stackprof' # CPU profiling (MRI Ruby)
end
Usage:
- 자동으로 페이지 상단에 타이밍 정보 표시
?pp=helpURL 파라미터로 모든 옵션 확인?pp=profile-memory메모리 프로파일링?pp=profile-gcGC 프로파일링
Configuration:
# config/initializers/rack_profiler.rb
if Rails.env.development?
require 'rack-mini-profiler'
Rack::MiniProfiler.config.position = 'bottom-right'
Rack::MiniProfiler.config.start_hidden = false
end
3. Query Analysis
Slow Query Detection:
# config/initializers/query_logging.rb (development only)
if Rails.env.development?
ActiveSupport::Notifications.subscribe('sql.active_record') do |name, start, finish, id, payload|
duration = (finish - start) * 1000 # ms
if duration > 100 # Queries slower than 100ms
Rails.logger.warn "SLOW QUERY (#{duration.round(2)}ms): #{payload[:sql]}"
end
end
end
Common Slow Query Patterns:
Problem 1: Missing Index
# Slow without index
Post.where(status: 'published').count
# Add index
add_index :posts, :status
Problem 2: N+1 Queries
# N+1
posts = Post.all
posts.map { |p| p.user.name }
# Fixed
posts = Post.includes(:user).all
posts.map { |p| p.user.name }
Problem 3: Loading Too Much Data
# Loads all columns
Post.all.map(&:title)
# Only load needed columns
Post.select(:id, :title).all
Problem 4: Inefficient Counting
# Loads all records
if user.posts.any?
# Use exists? (stops at first match)
if user.posts.exists?
# Use counter cache for frequent counts
class User < ApplicationRecord
has_many :posts
end
# Migration
add_column :users, :posts_count, :integer, default: 0, null: false
4. Memory Profiling
Using memory_profiler:
require 'memory_profiler'
report = MemoryProfiler.report do
# Code to profile
Post.includes(:user, :comments).limit(100).to_a
end
report.pretty_print
Check Memory Usage:
# In development console
def memory_usage
`ps -o rss= -p #{Process.pid}`.to_i / 1024 # MB
end
before = memory_usage
# Run code
after = memory_usage
puts "Memory used: #{after - before}MB"
5. Caching Strategies
Fragment Caching:
<%# Cache expensive view rendering %>
<% cache @post do %>
<%= render @post %>
<% end %>
<%# Cache collection %>
<% cache @posts do %>
<%= render @posts %>
<% end %>
Query Caching (automatic in development):
# First query hits database
Post.where(status: 'published').to_a
# Second identical query uses cache (within same request)
Post.where(status: 'published').to_a
Russian Doll Caching:
<%# Outer cache depends on post and comments %>
<% cache [@post, @post.comments.maximum(:updated_at)] do %>
<%= render @post %>
<% @post.comments.each do |comment| %>
<%# Inner cache for each comment %>
<% cache comment do %>
<%= render comment %>
<% end %>
<% end %>
<% end %>
Counter Cache (avoid COUNT queries):
# Migration
class AddCommentsCountToPosts < ActiveRecord::Migration[8.0]
def change
add_column :posts, :comments_count, :integer, default: 0, null: false
# Backfill existing data
Post.find_each do |post|
Post.reset_counters(post.id, :comments)
end
end
end
# Model
class Comment < ApplicationRecord
belongs_to :post, counter_cache: true
end
# Usage
post.comments_count # No query!
6. Database Optimization
Use select to load only needed columns:
# ❌ Loads all columns (inefficient)
Post.all.map(&:title)
# ✅ Only loads id and title
Post.select(:id, :title).all
Use pluck for simple arrays:
# ❌ Instantiates full ActiveRecord objects
Post.all.map(&:id)
# ✅ Direct SQL query, returns array
Post.pluck(:id)
# Multiple columns
Post.pluck(:id, :title)
Batch processing for large datasets:
# ❌ Loads all records into memory
User.all.each do |user|
user.send_email
end
# ✅ Processes in batches of 1000
User.find_each(batch_size: 1000) do |user|
user.send_email
end
# Or with find_in_batches
User.find_in_batches(batch_size: 1000) do |users|
UserMailer.batch_send(users).deliver_later
end
Eager loading with includes/joins:
# ❌ N+1 queries
posts = Post.all
posts.each { |p| puts "#{p.user.name}: #{p.title}" }
# ✅ includes (LEFT OUTER JOIN, loads associations)
posts = Post.includes(:user).all
# ✅ joins (INNER JOIN, doesn't load associations)
posts = Post.joins(:user).where(users: { active: true })
# ✅ preload (separate queries, good for multiple associations)
posts = Post.preload(:user, :comments).all
# ✅ eager_load (LEFT OUTER JOIN, forces eager loading)
posts = Post.eager_load(:user).all
7. View Optimization
Avoid logic in views:
<%# ❌ Bad: Complex logic in view %>
<% if @post.published? && @post.user.premium? && @post.comments.count > 10 %>
Popular post!
<% end %>
<%# ✅ Good: Move to helper or presenter %>
<% if popular_post?(@post) %>
Popular post!
<% end %>
Minimize database queries in loops:
<%# ❌ N+1 in view %>
<% @posts.each do |post| %>
<%= post.user.name %> <%# N queries! %>
<% end %>
<%# ✅ Eager load in controller %>
<%# PostsController: @posts = Post.includes(:user) %>
<% @posts.each do |post| %>
<%= post.user.name %>
<% end %>
8. Background Jobs
Move slow operations to background:
# ❌ Slow synchronous operation
class UsersController < ApplicationController
def create
@user = User.create(user_params)
UserMailer.welcome_email(@user).deliver_now # Slow!
redirect_to root_path
end
end
# ✅ Fast async operation
class UsersController < ApplicationController
def create
@user = User.create(user_params)
UserMailer.welcome_email(@user).deliver_later # Fast!
redirect_to root_path
end
end
9. Asset Optimization
Compress assets:
# config/environments/production.rb
config.assets.compress = true
config.assets.js_compressor = :uglifier
config.assets.css_compressor = :sass
Use CDN for assets:
# config/environments/production.rb
config.asset_host = 'https://cdn.example.com'
Image optimization:
- Use appropriate formats (WebP, AVIF)
- Resize images before upload
- Use image processing gems (image_processing)
10. HTTP Optimization
Use HTTP/2 (automatic with Puma on HTTPS)
Enable Gzip compression:
# config.ru
use Rack::Deflater
Cache-Control headers:
# config/environments/production.rb
config.public_file_server.headers = {
'Cache-Control' => 'public, max-age=31536000'
}
Automation Script
Performance Check Runner
# Run via: ruby .claude/skills/performance-check/scripts/performance_check.rb
The script checks:
- N+1 query detection setup
- Missing indexes
- Slow query patterns
- Memory usage
- Caching configuration
Common Performance Issues
Issue 1: N+1 Queries
Symptoms: Slow page loads, many SQL queries
Detection:
- Bullet gem alerts
- Rails log shows repeated similar queries
Fix:
# Use includes, joins, or preload
Post.includes(:user, :comments)
Issue 2: Missing Indexes
Symptoms: Slow queries on WHERE clauses
Detection:
# Check query EXPLAIN
Post.where(status: 'published').explain
Fix:
# Add index in migration
add_index :posts, :status
add_index :posts, [:user_id, :status] # Composite
Issue 3: Loading Too Much Data
Symptoms: High memory usage, slow queries
Fix:
# Use select, pluck, or limit
Post.select(:id, :title).limit(50)
Issue 4: No Caching
Symptoms: Repeated expensive computations
Fix:
# Add fragment caching
<% cache @post do %>
<%= expensive_calculation(@post) %>
<% end %>
# Add counter caches
belongs_to :post, counter_cache: true
Issue 5: Synchronous Long Operations
Symptoms: Slow response times
Fix:
# Move to background job
SendEmailJob.perform_later(user_id)
Benchmarking
Simple benchmark:
require 'benchmark'
time = Benchmark.measure do
Post.includes(:user).limit(100).to_a
end
puts "Time: #{time.real}s"
Compare implementations:
require 'benchmark/ips'
Benchmark.ips do |x|
x.report("map") { Post.all.map(&:id) }
x.report("pluck") { Post.pluck(:id) }
x.compare!
end
Monitoring in Production
Application Performance Monitoring (APM):
- Skylight
- New Relic
- Scout APM
- AppSignal
Custom metrics (with logging-setup skill):
duration = Benchmark.realtime do
# Operation
end
Loggers::BusinessLogger.log_performance(
'complex_report',
duration * 1000, # ms
{ user_id: current_user.id }
)
Best Practices
-
Profile before optimizing
- Measure current performance
- Identify actual bottlenecks
- Don't optimize prematurely
-
Use appropriate caching
- Fragment caching for views
- Query caching (automatic)
- Counter caches for counts
-
Optimize database queries
- Add indexes for WHERE clauses
- Use eager loading (includes)
- Select only needed columns
-
Background jobs for slow operations
- Email sending
- File processing
- External API calls
-
Monitor continuously
- Use APM tools
- Set up performance alerts
- Regular performance reviews
-
Load test before release
- Test with production-like data
- Identify bottlenecks early
- Plan scaling strategy
Checklist
- Bullet gem configured (development)
- No N+1 queries in critical paths
- Indexes on all foreign keys
- Indexes on frequently queried columns
- Counter caches for frequent counts
- Fragment caching on expensive views
- Background jobs for slow operations
- Asset compression enabled (production)
- CDN configured (production)
- APM tool integrated (production)
- Regular performance reviews scheduled
Related Skills
Attack Tree Construction
Build comprehensive attack trees to visualize threat paths. Use when mapping attack scenarios, identifying defense gaps, or communicating security risks to stakeholders.
Grafana Dashboards
Create and manage production Grafana dashboards for real-time visualization of system and application metrics. Use when building monitoring dashboards, visualizing metrics, or creating operational observability interfaces.
Matplotlib
Foundational plotting library. Create line plots, scatter, bar, histograms, heatmaps, 3D, subplots, export PNG/PDF/SVG, for scientific visualization and publication figures.
Scientific Visualization
Create publication figures with matplotlib/seaborn/plotly. Multi-panel layouts, error bars, significance markers, colorblind-safe, export PDF/EPS/TIFF, for journal-ready scientific plots.
Seaborn
Statistical visualization. Scatter, box, violin, heatmaps, pair plots, regression, correlation matrices, KDE, faceted plots, for exploratory analysis and publication figures.
Shap
Model interpretability and explainability using SHAP (SHapley Additive exPlanations). Use this skill when explaining machine learning model predictions, computing feature importance, generating SHAP plots (waterfall, beeswarm, bar, scatter, force, heatmap), debugging models, analyzing model bias or fairness, comparing models, or implementing explainable AI. Works with tree-based models (XGBoost, LightGBM, Random Forest), deep learning (TensorFlow, PyTorch), linear models, and any black-box model
Pydeseq2
Differential gene expression analysis (Python DESeq2). Identify DE genes from bulk RNA-seq counts, Wald tests, FDR correction, volcano/MA plots, for RNA-seq analysis.
Query Writing
For writing and executing SQL queries - from simple single-table queries to complex multi-table JOINs and aggregations
Pydeseq2
Differential gene expression analysis (Python DESeq2). Identify DE genes from bulk RNA-seq counts, Wald tests, FDR correction, volcano/MA plots, for RNA-seq analysis.
Scientific Visualization
Meta-skill for publication-ready figures. Use when creating journal submission figures requiring multi-panel layouts, significance annotations, error bars, colorblind-safe palettes, and specific journal formatting (Nature, Science, Cell). Orchestrates matplotlib/seaborn/plotly with publication styles. For quick exploration use seaborn or plotly directly.
