Initial commit
This commit is contained in:
763
commands/ruby-optimize.md
Normal file
763
commands/ruby-optimize.md
Normal file
@@ -0,0 +1,763 @@
|
||||
---
|
||||
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
|
||||
Reference in New Issue
Block a user