764 lines
16 KiB
Markdown
764 lines
16 KiB
Markdown
---
|
|
description: Analyze and optimize Ruby code for performance, memory usage, and idiomatic patterns
|
|
---
|
|
|
|
# Ruby Optimize Command
|
|
|
|
Analyzes Ruby code and provides optimization recommendations for performance, memory usage, code readability, and idiomatic Ruby patterns.
|
|
|
|
## Arguments
|
|
|
|
- **$1: path** (required) - File or directory path to optimize
|
|
- **$2: focus** (optional) - Optimization focus: `performance`, `memory`, `readability`, or `all` (default: `all`)
|
|
|
|
## Usage Examples
|
|
|
|
```bash
|
|
# Analyze and optimize all aspects
|
|
/ruby-optimize app/models/user.rb
|
|
|
|
# Focus on performance only
|
|
/ruby-optimize app/services/ performance
|
|
|
|
# Focus on memory optimization
|
|
/ruby-optimize lib/data_processor.rb memory
|
|
|
|
# Focus on readability and idioms
|
|
/ruby-optimize app/ readability
|
|
|
|
# Optimize entire project
|
|
/ruby-optimize . all
|
|
```
|
|
|
|
## Workflow
|
|
|
|
### Step 1: Profile and Analyze Code
|
|
|
|
**Discovery Phase:**
|
|
|
|
1. Parse Ruby files in specified path
|
|
2. Identify methods and code patterns
|
|
3. Detect performance anti-patterns
|
|
4. Analyze memory allocation patterns
|
|
5. Check for idiomatic Ruby usage
|
|
6. Measure complexity metrics
|
|
|
|
**Analysis Tools:**
|
|
```ruby
|
|
# Use Ruby parser
|
|
require 'parser/current'
|
|
|
|
# AST analysis for pattern detection
|
|
ast = Parser::CurrentRuby.parse(source_code)
|
|
|
|
# Complexity analysis
|
|
require 'flog'
|
|
flog = Flog.new
|
|
flog.flog(file_path)
|
|
```
|
|
|
|
### Step 2: Performance Analysis
|
|
|
|
**Detect Performance Anti-Patterns:**
|
|
|
|
**1. Inefficient Enumeration:**
|
|
```ruby
|
|
# ISSUE: Using each when map is appropriate
|
|
def process_users
|
|
result = []
|
|
users.each do |user|
|
|
result << user.name.upcase
|
|
end
|
|
result
|
|
end
|
|
|
|
# OPTIMIZED: Use map
|
|
def process_users
|
|
users.map { |user| user.name.upcase }
|
|
end
|
|
|
|
# Benchmark improvement: 15-20% faster, less memory
|
|
```
|
|
|
|
**2. Repeated Object Creation:**
|
|
```ruby
|
|
# ISSUE: Creating regex in loop
|
|
def filter_emails(emails)
|
|
emails.select { |email| email.match(/@gmail\.com/) }
|
|
end
|
|
|
|
# OPTIMIZED: Create regex once
|
|
EMAIL_PATTERN = /@gmail\.com/
|
|
|
|
def filter_emails(emails)
|
|
emails.select { |email| email.match(EMAIL_PATTERN) }
|
|
end
|
|
|
|
# Benchmark improvement: 30-40% faster for large datasets
|
|
```
|
|
|
|
**3. N+1 Query Detection:**
|
|
```ruby
|
|
# ISSUE: N+1 queries
|
|
def user_with_posts
|
|
users = User.all
|
|
users.map do |user|
|
|
{
|
|
name: user.name,
|
|
posts_count: user.posts.count # Separate query for each user
|
|
}
|
|
end
|
|
end
|
|
|
|
# OPTIMIZED: Eager load or use counter cache
|
|
def user_with_posts
|
|
users = User.eager(:posts).all
|
|
users.map do |user|
|
|
{
|
|
name: user.name,
|
|
posts_count: user.posts.count
|
|
}
|
|
end
|
|
end
|
|
|
|
# Or with counter cache
|
|
def user_with_posts
|
|
users = User.all
|
|
users.map do |user|
|
|
{
|
|
name: user.name,
|
|
posts_count: user.posts_count # From counter cache
|
|
}
|
|
end
|
|
end
|
|
|
|
# Benchmark improvement: 10-100x faster depending on data size
|
|
```
|
|
|
|
**4. Inefficient String Building:**
|
|
```ruby
|
|
# ISSUE: String concatenation in loop
|
|
def build_csv(records)
|
|
csv = ""
|
|
records.each do |record|
|
|
csv += "#{record.id},#{record.name}\n"
|
|
end
|
|
csv
|
|
end
|
|
|
|
# OPTIMIZED: Use array join or StringIO
|
|
def build_csv(records)
|
|
records.map { |r| "#{r.id},#{r.name}" }.join("\n")
|
|
end
|
|
|
|
# Or for very large datasets
|
|
require 'stringio'
|
|
|
|
def build_csv(records)
|
|
StringIO.new.tap do |io|
|
|
records.each do |record|
|
|
io.puts "#{record.id},#{record.name}"
|
|
end
|
|
end.string
|
|
end
|
|
|
|
# Benchmark improvement: 5-10x faster for large datasets
|
|
```
|
|
|
|
**5. Unnecessary Sorting:**
|
|
```ruby
|
|
# ISSUE: Sorting entire collection when only need max/min
|
|
def highest_score(users)
|
|
users.sort_by(&:score).last
|
|
end
|
|
|
|
# OPTIMIZED: Use max_by
|
|
def highest_score(users)
|
|
users.max_by(&:score)
|
|
end
|
|
|
|
# Benchmark improvement: O(n) vs O(n log n)
|
|
```
|
|
|
|
**6. Block Performance:**
|
|
```ruby
|
|
# ISSUE: Symbol#to_proc with arguments
|
|
users.map { |u| u.name.upcase }
|
|
|
|
# OPTIMIZED: Use method chaining where possible
|
|
users.map(&:name).map(&:upcase)
|
|
|
|
# ISSUE: Creating proc in loop
|
|
items.select { |item| item.active? }
|
|
|
|
# OPTIMIZED: Use symbol to_proc
|
|
items.select(&:active?)
|
|
|
|
# Benchmark improvement: 10-15% faster
|
|
```
|
|
|
|
**7. Hash Access Patterns:**
|
|
```ruby
|
|
# ISSUE: Checking key and accessing value separately
|
|
if hash.key?(:name)
|
|
value = hash[:name]
|
|
process(value)
|
|
end
|
|
|
|
# OPTIMIZED: Use fetch or safe navigation
|
|
if value = hash[:name]
|
|
process(value)
|
|
end
|
|
|
|
# Or with default
|
|
value = hash.fetch(:name, default_value)
|
|
process(value)
|
|
|
|
# ISSUE: Using Hash#merge in loop
|
|
result = {}
|
|
items.each do |item|
|
|
result = result.merge(item.to_hash)
|
|
end
|
|
|
|
# OPTIMIZED: Use Hash#merge! or each_with_object
|
|
result = items.each_with_object({}) do |item, hash|
|
|
hash.merge!(item.to_hash)
|
|
end
|
|
|
|
# Benchmark improvement: 2-3x faster
|
|
```
|
|
|
|
### Step 3: Memory Optimization
|
|
|
|
**Detect Memory Issues:**
|
|
|
|
**1. String Allocation:**
|
|
```ruby
|
|
# ISSUE: Creating new strings in loop
|
|
1000.times do
|
|
hash['key'] = value # Creates new 'key' string each time
|
|
end
|
|
|
|
# OPTIMIZED: Use symbols or frozen strings
|
|
1000.times do
|
|
hash[:key] = value # Reuses same symbol
|
|
end
|
|
|
|
# Or with frozen string literal
|
|
# frozen_string_literal: true
|
|
|
|
# Memory saved: ~40 bytes per string
|
|
```
|
|
|
|
**2. Array/Hash Allocation:**
|
|
```ruby
|
|
# ISSUE: Building large array without size hint
|
|
data = []
|
|
10_000.times do |i|
|
|
data << i
|
|
end
|
|
|
|
# OPTIMIZED: Preallocate size
|
|
data = Array.new(10_000)
|
|
10_000.times do |i|
|
|
data[i] = i
|
|
end
|
|
|
|
# Or use a different approach
|
|
data = (0...10_000).to_a
|
|
|
|
# Memory improvement: Fewer reallocations
|
|
```
|
|
|
|
**3. Object Copying:**
|
|
```ruby
|
|
# ISSUE: Unnecessary duplication
|
|
def process(data)
|
|
temp = data.dup
|
|
temp.map! { |item| item * 2 }
|
|
temp
|
|
end
|
|
|
|
# OPTIMIZED: Use map without dup if original not needed
|
|
def process(data)
|
|
data.map { |item| item * 2 }
|
|
end
|
|
|
|
# Memory saved: Full array copy avoided
|
|
```
|
|
|
|
**4. Lazy Evaluation:**
|
|
```ruby
|
|
# ISSUE: Loading everything into memory
|
|
File.readlines('large_file.txt').each do |line|
|
|
process(line)
|
|
end
|
|
|
|
# OPTIMIZED: Process line by line
|
|
File.foreach('large_file.txt') do |line|
|
|
process(line)
|
|
end
|
|
|
|
# Or use lazy enumeration
|
|
File.readlines('large_file.txt').lazy.each do |line|
|
|
process(line)
|
|
end
|
|
|
|
# Memory saved: File size - line size
|
|
```
|
|
|
|
**5. Memoization Leaks:**
|
|
```ruby
|
|
# ISSUE: Unbounded memoization cache
|
|
def expensive_calculation(input)
|
|
@cache ||= {}
|
|
@cache[input] ||= perform_calculation(input)
|
|
end
|
|
|
|
# OPTIMIZED: Use bounded cache (LRU)
|
|
require 'lru_redux'
|
|
|
|
def expensive_calculation(input)
|
|
@cache ||= LruRedux::Cache.new(1000)
|
|
@cache.getset(input) { perform_calculation(input) }
|
|
end
|
|
|
|
# Memory saved: Prevents cache from growing unbounded
|
|
```
|
|
|
|
### Step 4: Readability and Idiom Analysis
|
|
|
|
**Detect Non-Idiomatic Code:**
|
|
|
|
**1. Conditional Assignment:**
|
|
```ruby
|
|
# NON-IDIOMATIC
|
|
if user.name.nil?
|
|
user.name = 'Guest'
|
|
end
|
|
|
|
# IDIOMATIC
|
|
user.name ||= 'Guest'
|
|
|
|
# NON-IDIOMATIC
|
|
if value == nil
|
|
value = default
|
|
else
|
|
value = value
|
|
end
|
|
|
|
# IDIOMATIC
|
|
value ||= default
|
|
```
|
|
|
|
**2. Safe Navigation:**
|
|
```ruby
|
|
# NON-IDIOMATIC
|
|
if user && user.profile && user.profile.avatar
|
|
display(user.profile.avatar)
|
|
end
|
|
|
|
# IDIOMATIC
|
|
display(user&.profile&.avatar) if user&.profile&.avatar
|
|
# or
|
|
if avatar = user&.profile&.avatar
|
|
display(avatar)
|
|
end
|
|
```
|
|
|
|
**3. Enumerable Methods:**
|
|
```ruby
|
|
# NON-IDIOMATIC
|
|
found = nil
|
|
users.each do |user|
|
|
if user.active?
|
|
found = user
|
|
break
|
|
end
|
|
end
|
|
|
|
# IDIOMATIC
|
|
found = users.find(&:active?)
|
|
|
|
# NON-IDIOMATIC
|
|
actives = []
|
|
users.each do |user|
|
|
actives << user if user.active?
|
|
end
|
|
|
|
# IDIOMATIC
|
|
actives = users.select(&:active?)
|
|
|
|
# NON-IDIOMATIC
|
|
total = 0
|
|
prices.each { |price| total += price }
|
|
|
|
# IDIOMATIC
|
|
total = prices.sum
|
|
# or
|
|
total = prices.reduce(:+)
|
|
```
|
|
|
|
**4. Guard Clauses:**
|
|
```ruby
|
|
# NON-IDIOMATIC
|
|
def process(user)
|
|
if user
|
|
if user.active?
|
|
if user.verified?
|
|
# Main logic here
|
|
perform_action(user)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# IDIOMATIC
|
|
def process(user)
|
|
return unless user
|
|
return unless user.active?
|
|
return unless user.verified?
|
|
|
|
perform_action(user)
|
|
end
|
|
```
|
|
|
|
**5. Pattern Matching (Ruby 3.0+):**
|
|
```ruby
|
|
# LESS IDIOMATIC (Ruby 3.0+)
|
|
if response.is_a?(Hash) && response[:status] == 'success'
|
|
handle_success(response[:data])
|
|
elsif response.is_a?(Hash) && response[:status] == 'error'
|
|
handle_error(response[:error])
|
|
end
|
|
|
|
# MORE IDIOMATIC (Ruby 3.0+)
|
|
case response
|
|
in { status: 'success', data: }
|
|
handle_success(data)
|
|
in { status: 'error', error: }
|
|
handle_error(error)
|
|
end
|
|
```
|
|
|
|
**6. Block Syntax:**
|
|
```ruby
|
|
# NON-IDIOMATIC: do/end for single line
|
|
users.map do |u| u.name end
|
|
|
|
# IDIOMATIC: braces for single line
|
|
users.map { |u| u.name }
|
|
|
|
# NON-IDIOMATIC: braces for multi-line
|
|
users.select { |u|
|
|
u.active? &&
|
|
u.verified?
|
|
}
|
|
|
|
# IDIOMATIC: do/end for multi-line
|
|
users.select do |u|
|
|
u.active? && u.verified?
|
|
end
|
|
```
|
|
|
|
**7. String Interpolation:**
|
|
```ruby
|
|
# NON-IDIOMATIC
|
|
"Hello " + user.name + "!"
|
|
|
|
# IDIOMATIC
|
|
"Hello #{user.name}!"
|
|
|
|
# NON-IDIOMATIC
|
|
'Total: ' + total.to_s
|
|
|
|
# IDIOMATIC
|
|
"Total: #{total}"
|
|
```
|
|
|
|
### Step 5: Generate Benchmarks
|
|
|
|
**Create Benchmark Comparisons:**
|
|
|
|
```ruby
|
|
# Generated benchmark file: benchmarks/optimization_comparison.rb
|
|
require 'benchmark'
|
|
|
|
puts "Performance Comparison"
|
|
puts "=" * 50
|
|
|
|
# Original implementation
|
|
def original_method
|
|
# Original code
|
|
end
|
|
|
|
# Optimized implementation
|
|
def optimized_method
|
|
# Optimized code
|
|
end
|
|
|
|
Benchmark.bm(20) do |x|
|
|
x.report("Original:") do
|
|
10_000.times { original_method }
|
|
end
|
|
|
|
x.report("Optimized:") do
|
|
10_000.times { optimized_method }
|
|
end
|
|
end
|
|
|
|
# Memory profiling
|
|
require 'memory_profiler'
|
|
|
|
puts "\nMemory Comparison"
|
|
puts "=" * 50
|
|
|
|
report = MemoryProfiler.report do
|
|
original_method
|
|
end
|
|
|
|
puts "Original Memory Usage:"
|
|
puts " Total allocated: #{report.total_allocated_memsize} bytes"
|
|
puts " Total retained: #{report.total_retained_memsize} bytes"
|
|
|
|
report = MemoryProfiler.report do
|
|
optimized_method
|
|
end
|
|
|
|
puts "\nOptimized Memory Usage:"
|
|
puts " Total allocated: #{report.total_allocated_memsize} bytes"
|
|
puts " Total retained: #{report.total_retained_memsize} bytes"
|
|
```
|
|
|
|
### Step 6: Generate Optimization Report
|
|
|
|
**Comprehensive Report Structure:**
|
|
|
|
```
|
|
================================================================================
|
|
RUBY OPTIMIZATION REPORT
|
|
================================================================================
|
|
|
|
File: app/services/data_processor.rb
|
|
Focus: all
|
|
Date: 2024-01-15
|
|
|
|
--------------------------------------------------------------------------------
|
|
SUMMARY
|
|
--------------------------------------------------------------------------------
|
|
Total Issues Found: 18
|
|
Performance: 8
|
|
Memory: 5
|
|
Readability: 5
|
|
|
|
Potential Improvements:
|
|
Estimated Speed Gain: 2.5x faster
|
|
Estimated Memory Reduction: 45%
|
|
Code Quality: +15 readability score
|
|
|
|
--------------------------------------------------------------------------------
|
|
PERFORMANCE OPTIMIZATIONS
|
|
--------------------------------------------------------------------------------
|
|
|
|
1. Inefficient Enumeration (Line 23)
|
|
Severity: Medium
|
|
Impact: 20% speed improvement
|
|
|
|
Current:
|
|
result = []
|
|
users.each { |u| result << u.name.upcase }
|
|
result
|
|
|
|
Optimized:
|
|
users.map { |u| u.name.upcase }
|
|
|
|
Benchmark:
|
|
Before: 1.45ms per 1000 items
|
|
After: 1.15ms per 1000 items
|
|
Improvement: 20.7% faster
|
|
|
|
2. N+1 Query Pattern (Line 45)
|
|
Severity: High
|
|
Impact: 10-100x speed improvement
|
|
|
|
Current:
|
|
users.map { |u| { name: u.name, posts: u.posts.count } }
|
|
|
|
Optimized:
|
|
users.eager(:posts).map { |u| { name: u.name, posts: u.posts.count } }
|
|
|
|
Benchmark:
|
|
Before: 1250ms for 100 users with 10 posts each
|
|
After: 25ms for 100 users with 10 posts each
|
|
Improvement: 50x faster
|
|
|
|
[... more performance issues ...]
|
|
|
|
--------------------------------------------------------------------------------
|
|
MEMORY OPTIMIZATIONS
|
|
--------------------------------------------------------------------------------
|
|
|
|
1. String Allocation in Loop (Line 67)
|
|
Severity: Medium
|
|
Impact: 400 bytes saved per 1000 iterations
|
|
|
|
Current:
|
|
1000.times { hash['key'] = value }
|
|
|
|
Optimized:
|
|
1000.times { hash[:key] = value }
|
|
|
|
Memory:
|
|
Before: 40KB allocated
|
|
After: 160 bytes allocated
|
|
Savings: 99.6%
|
|
|
|
[... more memory issues ...]
|
|
|
|
--------------------------------------------------------------------------------
|
|
READABILITY IMPROVEMENTS
|
|
--------------------------------------------------------------------------------
|
|
|
|
1. Non-Idiomatic Conditional (Line 89)
|
|
Severity: Low
|
|
Impact: Improved code clarity
|
|
|
|
Current:
|
|
if user.name.nil?
|
|
user.name = 'Guest'
|
|
end
|
|
|
|
Idiomatic:
|
|
user.name ||= 'Guest'
|
|
|
|
[... more readability issues ...]
|
|
|
|
--------------------------------------------------------------------------------
|
|
COMPLEXITY METRICS
|
|
--------------------------------------------------------------------------------
|
|
|
|
Method Complexity (Flog scores):
|
|
process_data: 45.2 (High - consider refactoring)
|
|
transform_records: 23.1 (Medium)
|
|
validate_input: 8.5 (Low)
|
|
|
|
Recommendations:
|
|
- Extract methods from process_data to reduce complexity
|
|
- Consider using service objects for complex operations
|
|
|
|
--------------------------------------------------------------------------------
|
|
BENCHMARKS
|
|
--------------------------------------------------------------------------------
|
|
|
|
File Generated: benchmarks/data_processor_comparison.rb
|
|
|
|
Run benchmarks:
|
|
ruby benchmarks/data_processor_comparison.rb
|
|
|
|
Expected Results:
|
|
Original: 2.450s
|
|
Optimized: 0.980s
|
|
Speedup: 2.5x
|
|
|
|
--------------------------------------------------------------------------------
|
|
ACTION ITEMS
|
|
--------------------------------------------------------------------------------
|
|
|
|
High Priority:
|
|
1. Fix N+1 query in line 45 (50x performance gain)
|
|
2. Optimize string building in line 67 (99% memory reduction)
|
|
3. Refactor process_data method (complexity: 45.2)
|
|
|
|
Medium Priority:
|
|
4. Use map instead of each+append (20% speed gain)
|
|
5. Cache regex patterns (30% speed gain)
|
|
6. Implement guard clauses in validate_input
|
|
|
|
Low Priority:
|
|
7. Use idiomatic Ruby patterns throughout
|
|
8. Apply consistent block syntax
|
|
9. Improve variable naming
|
|
|
|
--------------------------------------------------------------------------------
|
|
AUTOMATIC FIXES
|
|
--------------------------------------------------------------------------------
|
|
|
|
Low-risk changes that can be auto-applied:
|
|
- String to symbol conversion (5 occurrences)
|
|
- each to map conversion (3 occurrences)
|
|
- Conditional to ||= conversion (4 occurrences)
|
|
|
|
Apply automatic fixes? [y/N]
|
|
|
|
================================================================================
|
|
END REPORT
|
|
================================================================================
|
|
```
|
|
|
|
### Step 7: Optional - Apply Automatic Fixes
|
|
|
|
**Safe Transformations:**
|
|
|
|
For low-risk, well-defined improvements:
|
|
|
|
```ruby
|
|
# Create optimized version of file
|
|
# app/services/data_processor_optimized.rb
|
|
|
|
# Apply automatic transformations:
|
|
# - String literals to symbols
|
|
# - each+append to map
|
|
# - if/nil? to ||=
|
|
# - Block syntax corrections
|
|
|
|
# Generate diff
|
|
# Show side-by-side comparison
|
|
# Offer to replace original or keep both
|
|
```
|
|
|
|
## Output Formats
|
|
|
|
### Console Output
|
|
- Colored severity indicators (red/yellow/green)
|
|
- Progress indicator during analysis
|
|
- Summary statistics
|
|
- Top issues highlighted
|
|
|
|
### Report Files
|
|
- Detailed markdown report
|
|
- Generated benchmark files
|
|
- Optional optimized code files
|
|
- Diff files for review
|
|
|
|
### JSON Output (Optional)
|
|
```json
|
|
{
|
|
"file": "app/services/data_processor.rb",
|
|
"summary": {
|
|
"total_issues": 18,
|
|
"performance": 8,
|
|
"memory": 5,
|
|
"readability": 5
|
|
},
|
|
"issues": [
|
|
{
|
|
"type": "performance",
|
|
"severity": "high",
|
|
"line": 45,
|
|
"description": "N+1 query pattern",
|
|
"impact": "50x speed improvement",
|
|
"suggestion": "Use eager loading"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
- Handle invalid Ruby syntax gracefully
|
|
- Skip non-Ruby files
|
|
- Report files that cannot be parsed
|
|
- Handle missing dependencies
|
|
- Warn about risky optimizations
|
|
- Preserve backups before modifications
|