ariadna 1.3.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/ariadna.gemspec +0 -1
- data/data/agents/ariadna-codebase-mapper.md +34 -722
- data/data/agents/ariadna-debugger.md +44 -1139
- data/data/agents/ariadna-executor.md +75 -396
- data/data/agents/ariadna-planner.md +78 -1215
- data/data/agents/ariadna-roadmapper.md +55 -582
- data/data/agents/ariadna-verifier.md +60 -702
- data/data/ariadna/templates/config.json +8 -33
- data/data/ariadna/workflows/debug.md +28 -0
- data/data/ariadna/workflows/execute-phase.md +31 -513
- data/data/ariadna/workflows/map-codebase.md +20 -319
- data/data/ariadna/workflows/new-milestone.md +20 -365
- data/data/ariadna/workflows/new-project.md +19 -880
- data/data/ariadna/workflows/plan-phase.md +24 -443
- data/data/ariadna/workflows/progress.md +20 -376
- data/data/ariadna/workflows/quick.md +19 -221
- data/data/ariadna/workflows/roadmap-ops.md +28 -0
- data/data/ariadna/workflows/verify-work.md +23 -560
- data/data/commands/ariadna/add-phase.md +11 -22
- data/data/commands/ariadna/debug.md +11 -143
- data/data/commands/ariadna/execute-phase.md +12 -30
- data/data/commands/ariadna/insert-phase.md +7 -14
- data/data/commands/ariadna/map-codebase.md +16 -49
- data/data/commands/ariadna/new-milestone.md +12 -25
- data/data/commands/ariadna/new-project.md +22 -26
- data/data/commands/ariadna/plan-phase.md +13 -22
- data/data/commands/ariadna/progress.md +16 -6
- data/data/commands/ariadna/quick.md +9 -11
- data/data/commands/ariadna/remove-phase.md +9 -12
- data/data/commands/ariadna/verify-work.md +14 -19
- data/data/skills/rails-backend/API.md +138 -0
- data/data/skills/rails-backend/CONTROLLERS.md +154 -0
- data/data/skills/rails-backend/JOBS.md +132 -0
- data/data/skills/rails-backend/MODELS.md +213 -0
- data/data/skills/rails-backend/SKILL.md +169 -0
- data/data/skills/rails-frontend/ASSETS.md +154 -0
- data/data/skills/rails-frontend/COMPONENTS.md +253 -0
- data/data/skills/rails-frontend/SKILL.md +187 -0
- data/data/skills/rails-frontend/VIEWS.md +168 -0
- data/data/skills/rails-performance/PROFILING.md +106 -0
- data/data/skills/rails-performance/SKILL.md +217 -0
- data/data/skills/rails-security/AUDIT.md +118 -0
- data/data/skills/rails-security/SKILL.md +422 -0
- data/data/skills/rails-testing/FIXTURES.md +78 -0
- data/data/skills/rails-testing/SKILL.md +160 -0
- data/data/skills/rails-testing/SYSTEM-TESTS.md +73 -0
- data/lib/ariadna/installer.rb +11 -15
- data/lib/ariadna/tools/cli.rb +0 -12
- data/lib/ariadna/tools/config_manager.rb +10 -72
- data/lib/ariadna/tools/frontmatter.rb +23 -1
- data/lib/ariadna/tools/init.rb +201 -401
- data/lib/ariadna/tools/model_profiles.rb +6 -14
- data/lib/ariadna/tools/phase_manager.rb +1 -10
- data/lib/ariadna/tools/state_manager.rb +170 -451
- data/lib/ariadna/tools/template_filler.rb +4 -12
- data/lib/ariadna/tools/verification.rb +21 -399
- data/lib/ariadna/uninstaller.rb +9 -0
- data/lib/ariadna/version.rb +1 -1
- data/lib/ariadna.rb +1 -0
- metadata +20 -91
- data/data/agents/ariadna-backend-executor.md +0 -261
- data/data/agents/ariadna-frontend-executor.md +0 -259
- data/data/agents/ariadna-integration-checker.md +0 -418
- data/data/agents/ariadna-phase-researcher.md +0 -469
- data/data/agents/ariadna-plan-checker.md +0 -622
- data/data/agents/ariadna-project-researcher.md +0 -618
- data/data/agents/ariadna-research-synthesizer.md +0 -236
- data/data/agents/ariadna-test-executor.md +0 -266
- data/data/ariadna/references/checkpoints.md +0 -772
- data/data/ariadna/references/continuation-format.md +0 -249
- data/data/ariadna/references/decimal-phase-calculation.md +0 -65
- data/data/ariadna/references/git-integration.md +0 -248
- data/data/ariadna/references/git-planning-commit.md +0 -38
- data/data/ariadna/references/model-profile-resolution.md +0 -32
- data/data/ariadna/references/model-profiles.md +0 -73
- data/data/ariadna/references/phase-argument-parsing.md +0 -61
- data/data/ariadna/references/planning-config.md +0 -194
- data/data/ariadna/references/questioning.md +0 -153
- data/data/ariadna/references/rails-conventions.md +0 -416
- data/data/ariadna/references/tdd.md +0 -267
- data/data/ariadna/references/ui-brand.md +0 -160
- data/data/ariadna/references/verification-patterns.md +0 -853
- data/data/ariadna/templates/codebase/architecture.md +0 -481
- data/data/ariadna/templates/codebase/concerns.md +0 -380
- data/data/ariadna/templates/codebase/conventions.md +0 -434
- data/data/ariadna/templates/codebase/integrations.md +0 -328
- data/data/ariadna/templates/codebase/stack.md +0 -189
- data/data/ariadna/templates/codebase/structure.md +0 -418
- data/data/ariadna/templates/codebase/testing.md +0 -606
- data/data/ariadna/templates/context.md +0 -283
- data/data/ariadna/templates/continue-here.md +0 -78
- data/data/ariadna/templates/debug-subagent-prompt.md +0 -91
- data/data/ariadna/templates/phase-prompt.md +0 -609
- data/data/ariadna/templates/planner-subagent-prompt.md +0 -117
- data/data/ariadna/templates/research-project/ARCHITECTURE.md +0 -439
- data/data/ariadna/templates/research-project/FEATURES.md +0 -168
- data/data/ariadna/templates/research-project/PITFALLS.md +0 -406
- data/data/ariadna/templates/research-project/STACK.md +0 -251
- data/data/ariadna/templates/research-project/SUMMARY.md +0 -247
- data/data/ariadna/templates/state.md +0 -176
- data/data/ariadna/templates/summary-complex.md +0 -59
- data/data/ariadna/templates/summary-minimal.md +0 -41
- data/data/ariadna/templates/summary-standard.md +0 -48
- data/data/ariadna/templates/user-setup.md +0 -310
- data/data/ariadna/workflows/add-phase.md +0 -111
- data/data/ariadna/workflows/add-todo.md +0 -157
- data/data/ariadna/workflows/audit-milestone.md +0 -241
- data/data/ariadna/workflows/check-todos.md +0 -176
- data/data/ariadna/workflows/complete-milestone.md +0 -644
- data/data/ariadna/workflows/diagnose-issues.md +0 -219
- data/data/ariadna/workflows/discovery-phase.md +0 -289
- data/data/ariadna/workflows/discuss-phase.md +0 -408
- data/data/ariadna/workflows/execute-plan.md +0 -448
- data/data/ariadna/workflows/help.md +0 -470
- data/data/ariadna/workflows/insert-phase.md +0 -129
- data/data/ariadna/workflows/list-phase-assumptions.md +0 -178
- data/data/ariadna/workflows/pause-work.md +0 -122
- data/data/ariadna/workflows/plan-milestone-gaps.md +0 -256
- data/data/ariadna/workflows/remove-phase.md +0 -154
- data/data/ariadna/workflows/research-phase.md +0 -74
- data/data/ariadna/workflows/resume-project.md +0 -306
- data/data/ariadna/workflows/set-profile.md +0 -80
- data/data/ariadna/workflows/settings.md +0 -145
- data/data/ariadna/workflows/transition.md +0 -493
- data/data/ariadna/workflows/update.md +0 -212
- data/data/ariadna/workflows/verify-phase.md +0 -226
- data/data/commands/ariadna/add-todo.md +0 -42
- data/data/commands/ariadna/audit-milestone.md +0 -42
- data/data/commands/ariadna/check-todos.md +0 -41
- data/data/commands/ariadna/complete-milestone.md +0 -136
- data/data/commands/ariadna/discuss-phase.md +0 -86
- data/data/commands/ariadna/help.md +0 -22
- data/data/commands/ariadna/list-phase-assumptions.md +0 -50
- data/data/commands/ariadna/pause-work.md +0 -35
- data/data/commands/ariadna/plan-milestone-gaps.md +0 -40
- data/data/commands/ariadna/reapply-patches.md +0 -110
- data/data/commands/ariadna/research-phase.md +0 -187
- data/data/commands/ariadna/resume-work.md +0 -40
- data/data/commands/ariadna/set-profile.md +0 -34
- data/data/commands/ariadna/settings.md +0 -36
- data/data/commands/ariadna/update.md +0 -37
- data/data/guides/backend.md +0 -3069
- data/data/guides/frontend.md +0 -1479
- data/data/guides/performance.md +0 -1193
- data/data/guides/security.md +0 -1522
- data/data/guides/style-guide.md +0 -1091
- data/data/guides/testing.md +0 -504
- data/data/templates.md +0 -94
data/data/guides/performance.md
DELETED
|
@@ -1,1193 +0,0 @@
|
|
|
1
|
-
# Performance Guide for Rails Code Review
|
|
2
|
-
|
|
3
|
-
**Agent-Oriented Performance Checklist for Automated Code Verification**
|
|
4
|
-
|
|
5
|
-
This guide provides a structured checklist for verifying code performance after each development phase. Each section contains named CHECK items with severity levels, file glob patterns, and UNSAFE/SAFE code examples so an agent can systematically scan changed files and report findings.
|
|
6
|
-
|
|
7
|
-
Unlike informational performance documentation, this guide is **action-oriented** — every check tells you what to look for, where to look, and how to fix it.
|
|
8
|
-
|
|
9
|
-
**Related guides:**
|
|
10
|
-
- [Backend Patterns](backend.md) — Architecture, models, controllers, jobs, style guide
|
|
11
|
-
- [Testing Patterns](testing.md) — Testing philosophy, model/controller/job test patterns
|
|
12
|
-
- [Security Guide](security.md) — Agent-oriented security checklist for code review
|
|
13
|
-
- [Frontend Patterns](frontend.md) — Presenter pattern, view layer conventions
|
|
14
|
-
|
|
15
|
-
## Table of Contents
|
|
16
|
-
|
|
17
|
-
- [Part 1: Database Query Performance](#part-1-database-query-performance)
|
|
18
|
-
- [1.1 N+1 Queries](#11-n1-queries)
|
|
19
|
-
- [1.2 Inefficient Queries](#12-inefficient-queries)
|
|
20
|
-
- [1.3 Batch Processing](#13-batch-processing)
|
|
21
|
-
- [1.4 Query Placement](#14-query-placement)
|
|
22
|
-
- [Part 2: Database Indexing](#part-2-database-indexing)
|
|
23
|
-
- [2.1 Missing Indexes](#21-missing-indexes)
|
|
24
|
-
- [2.2 Index Anti-Patterns](#22-index-anti-patterns)
|
|
25
|
-
- [Part 3: Caching](#part-3-caching)
|
|
26
|
-
- [3.1 Cache Store Configuration](#31-cache-store-configuration)
|
|
27
|
-
- [3.2 Fragment & Collection Caching](#32-fragment--collection-caching)
|
|
28
|
-
- [3.3 Application-Level Caching](#33-application-level-caching)
|
|
29
|
-
- [Part 4: Memory & Resource Management](#part-4-memory--resource-management)
|
|
30
|
-
- [4.1 Memory-Intensive Operations](#41-memory-intensive-operations)
|
|
31
|
-
- [4.2 Background Job Offloading](#42-background-job-offloading)
|
|
32
|
-
- [4.3 Unnecessary Object Allocation](#43-unnecessary-object-allocation)
|
|
33
|
-
- [Part 5: View & Response Performance](#part-5-view--response-performance)
|
|
34
|
-
- [5.1 Collection Rendering](#51-collection-rendering)
|
|
35
|
-
- [5.2 Asset & Frontend Performance](#52-asset--frontend-performance)
|
|
36
|
-
- [Part 6: Deployment & Configuration](#part-6-deployment--configuration)
|
|
37
|
-
- [6.1 Server Tuning](#61-server-tuning)
|
|
38
|
-
- [6.2 Production Settings](#62-production-settings)
|
|
39
|
-
- [Part 7: Performance Verification Checklist](#part-7-performance-verification-checklist)
|
|
40
|
-
- [7.1 Agent Check Protocol](#71-agent-check-protocol)
|
|
41
|
-
- [7.2 Quick-Reference Checklist](#72-quick-reference-checklist)
|
|
42
|
-
|
|
43
|
-
---
|
|
44
|
-
|
|
45
|
-
# Part 1: Database Query Performance
|
|
46
|
-
|
|
47
|
-
Database queries are the most common source of performance problems in Rails applications. These checks cover N+1 queries, inefficient query patterns, batch processing, and proper query placement.
|
|
48
|
-
|
|
49
|
-
## 1.1 N+1 Queries
|
|
50
|
-
|
|
51
|
-
N+1 queries occur when code loads a collection and then executes a separate query for each item's association. This turns a single query into dozens or hundreds.
|
|
52
|
-
|
|
53
|
-
### CHECK 1.1a: Eager load associations accessed in loops
|
|
54
|
-
|
|
55
|
-
> **What to look for:** Iterating over a collection and accessing an association (e.g., `post.author`, `order.line_items`) without a preceding `includes`, `eager_load`, or `preload` call
|
|
56
|
-
> **Where to look:** `app/controllers/**/*.rb`, `app/views/**/*.erb`, `app/models/**/*.rb`
|
|
57
|
-
> **Severity:** High
|
|
58
|
-
|
|
59
|
-
**UNSAFE:**
|
|
60
|
-
|
|
61
|
-
```ruby
|
|
62
|
-
# Controller
|
|
63
|
-
def index
|
|
64
|
-
@posts = Post.all
|
|
65
|
-
end
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
```erb
|
|
69
|
-
<%# View — triggers N+1: one query per post for author %>
|
|
70
|
-
<% @posts.each do |post| %>
|
|
71
|
-
<p><%= post.author.name %></p>
|
|
72
|
-
<% end %>
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
**SAFE:**
|
|
76
|
-
|
|
77
|
-
```ruby
|
|
78
|
-
# Controller
|
|
79
|
-
def index
|
|
80
|
-
@posts = Post.includes(:author)
|
|
81
|
-
end
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
```erb
|
|
85
|
-
<%# View — author already loaded, no extra queries %>
|
|
86
|
-
<% @posts.each do |post| %>
|
|
87
|
-
<p><%= post.author.name %></p>
|
|
88
|
-
<% end %>
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
### CHECK 1.1b: Nested association eager loading
|
|
92
|
-
|
|
93
|
-
> **What to look for:** Views or serializers that access deeply nested associations (e.g., `post.comments.map(&:author)`) without nested `includes`
|
|
94
|
-
> **Where to look:** `app/controllers/**/*.rb`, `app/views/**/*.erb`
|
|
95
|
-
> **Severity:** High
|
|
96
|
-
|
|
97
|
-
**UNSAFE:**
|
|
98
|
-
|
|
99
|
-
```ruby
|
|
100
|
-
def show
|
|
101
|
-
@post = Post.includes(:comments).find(params[:id])
|
|
102
|
-
# comment.author triggers N+1 inside the view
|
|
103
|
-
end
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
**SAFE:**
|
|
107
|
-
|
|
108
|
-
```ruby
|
|
109
|
-
def show
|
|
110
|
-
@post = Post.includes(comments: :author).find(params[:id])
|
|
111
|
-
end
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
### CHECK 1.1c: Counter cache for association counts
|
|
115
|
-
|
|
116
|
-
> **What to look for:** Calling `.count` or `.size` on an association inside a loop without a `counter_cache` column
|
|
117
|
-
> **Where to look:** `app/views/**/*.erb`, `app/models/**/*.rb`
|
|
118
|
-
> **Severity:** Medium
|
|
119
|
-
|
|
120
|
-
**UNSAFE:**
|
|
121
|
-
|
|
122
|
-
```erb
|
|
123
|
-
<% @boards.each do |board| %>
|
|
124
|
-
<span><%= board.cards.count %> cards</span>
|
|
125
|
-
<% end %>
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
**SAFE:**
|
|
129
|
-
|
|
130
|
-
```ruby
|
|
131
|
-
# Migration
|
|
132
|
-
add_column :boards, :cards_count, :integer, default: 0, null: false
|
|
133
|
-
|
|
134
|
-
# Model
|
|
135
|
-
class Card < ApplicationRecord
|
|
136
|
-
belongs_to :board, counter_cache: true
|
|
137
|
-
end
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
```erb
|
|
141
|
-
<% @boards.each do |board| %>
|
|
142
|
-
<span><%= board.cards_count %> cards</span>
|
|
143
|
-
<% end %>
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
---
|
|
147
|
-
|
|
148
|
-
## 1.2 Inefficient Queries
|
|
149
|
-
|
|
150
|
-
These patterns produce correct results but waste database resources by fetching more data than needed or executing queries that could be optimized.
|
|
151
|
-
|
|
152
|
-
### CHECK 1.2a: Select only needed columns
|
|
153
|
-
|
|
154
|
-
> **What to look for:** Queries that load full ActiveRecord objects when only one or two columns are needed; missing `select` or `pluck` for data extraction
|
|
155
|
-
> **Where to look:** `app/models/**/*.rb`, `app/controllers/**/*.rb`
|
|
156
|
-
> **Severity:** Medium
|
|
157
|
-
|
|
158
|
-
**UNSAFE:**
|
|
159
|
-
|
|
160
|
-
```ruby
|
|
161
|
-
# Loads all columns for every user just to get emails
|
|
162
|
-
emails = User.where(active: true).map(&:email)
|
|
163
|
-
|
|
164
|
-
# Loads full objects to get IDs
|
|
165
|
-
ids = Order.where(status: "pending").map(&:id)
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
**SAFE:**
|
|
169
|
-
|
|
170
|
-
```ruby
|
|
171
|
-
# Pluck returns plain arrays — no ActiveRecord objects allocated
|
|
172
|
-
emails = User.where(active: true).pluck(:email)
|
|
173
|
-
|
|
174
|
-
# IDs shortcut
|
|
175
|
-
ids = Order.where(status: "pending").ids
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
### CHECK 1.2b: Use exists? instead of present? for existence checks
|
|
179
|
-
|
|
180
|
-
> **What to look for:** `.present?`, `.any?`, or `.count > 0` on ActiveRecord relations when only existence is being checked
|
|
181
|
-
> **Where to look:** `app/models/**/*.rb`, `app/controllers/**/*.rb`, `app/views/**/*.erb`
|
|
182
|
-
> **Severity:** Medium
|
|
183
|
-
|
|
184
|
-
**UNSAFE:**
|
|
185
|
-
|
|
186
|
-
```ruby
|
|
187
|
-
# Loads all matching records into memory, then checks if array is non-empty
|
|
188
|
-
if user.orders.where(status: "pending").present?
|
|
189
|
-
show_pending_banner
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
# Counts all rows just to compare with zero
|
|
193
|
-
if Comment.where(post_id: post.id).count > 0
|
|
194
|
-
show_comments_section
|
|
195
|
-
end
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
**SAFE:**
|
|
199
|
-
|
|
200
|
-
```ruby
|
|
201
|
-
# SELECT 1 ... LIMIT 1 — stops at first match
|
|
202
|
-
if user.orders.where(status: "pending").exists?
|
|
203
|
-
show_pending_banner
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
if Comment.where(post_id: post.id).exists?
|
|
207
|
-
show_comments_section
|
|
208
|
-
end
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
### CHECK 1.2c: Avoid loading records just to count them
|
|
212
|
-
|
|
213
|
-
> **What to look for:** `.length` or `.size` called on relations that haven't been loaded, or `.count` called inside loops triggering repeated COUNT queries
|
|
214
|
-
> **Where to look:** `app/views/**/*.erb`, `app/controllers/**/*.rb`
|
|
215
|
-
> **Severity:** Medium
|
|
216
|
-
|
|
217
|
-
**UNSAFE:**
|
|
218
|
-
|
|
219
|
-
```ruby
|
|
220
|
-
# .length loads all records then counts the array
|
|
221
|
-
total = Project.where(archived: false).length
|
|
222
|
-
|
|
223
|
-
# .count inside a loop fires a COUNT query per iteration
|
|
224
|
-
@categories.each do |category|
|
|
225
|
-
puts "#{category.name}: #{category.products.count}"
|
|
226
|
-
end
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
**SAFE:**
|
|
230
|
-
|
|
231
|
-
```ruby
|
|
232
|
-
# .count fires a single COUNT query
|
|
233
|
-
total = Project.where(archived: false).count
|
|
234
|
-
|
|
235
|
-
# Preload counts in a single query
|
|
236
|
-
counts = Product.group(:category_id).count
|
|
237
|
-
@categories.each do |category|
|
|
238
|
-
puts "#{category.name}: #{counts[category.id] || 0}"
|
|
239
|
-
end
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
---
|
|
243
|
-
|
|
244
|
-
## 1.3 Batch Processing
|
|
245
|
-
|
|
246
|
-
Loading an entire table into memory crashes applications with large datasets. Batch processing methods stream records in configurable chunks.
|
|
247
|
-
|
|
248
|
-
### CHECK 1.3a: Use find_each for large iterations
|
|
249
|
-
|
|
250
|
-
> **What to look for:** `.all.each`, `.where(...).each`, or `.order(...).each` iterating over unbounded or potentially large result sets without `find_each` or `find_in_batches`
|
|
251
|
-
> **Where to look:** `app/models/**/*.rb`, `app/jobs/**/*.rb`, `lib/**/*.rb`
|
|
252
|
-
> **Severity:** High
|
|
253
|
-
|
|
254
|
-
**UNSAFE:**
|
|
255
|
-
|
|
256
|
-
```ruby
|
|
257
|
-
# Loads ALL users into memory at once
|
|
258
|
-
User.all.each do |user|
|
|
259
|
-
UserMailer.weekly_digest(user).deliver_later
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
# Large result set loaded entirely
|
|
263
|
-
Order.where("created_at < ?", 1.year.ago).each do |order|
|
|
264
|
-
order.archive!
|
|
265
|
-
end
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
**SAFE:**
|
|
269
|
-
|
|
270
|
-
```ruby
|
|
271
|
-
# Loads 1000 records at a time
|
|
272
|
-
User.find_each do |user|
|
|
273
|
-
UserMailer.weekly_digest(user).deliver_later
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
# Batched with custom size
|
|
277
|
-
Order.where("created_at < ?", 1.year.ago).find_each(batch_size: 500) do |order|
|
|
278
|
-
order.archive!
|
|
279
|
-
end
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
### CHECK 1.3b: Use find_in_batches for batch operations
|
|
283
|
-
|
|
284
|
-
> **What to look for:** Collecting large result sets into arrays for bulk operations instead of using `find_in_batches` or `in_batches`
|
|
285
|
-
> **Where to look:** `app/jobs/**/*.rb`, `lib/tasks/**/*.rake`, `lib/**/*.rb`
|
|
286
|
-
> **Severity:** Medium
|
|
287
|
-
|
|
288
|
-
**UNSAFE:**
|
|
289
|
-
|
|
290
|
-
```ruby
|
|
291
|
-
# Loads all records, then slices — peak memory holds entire table
|
|
292
|
-
Product.where(discontinued: true).to_a.each_slice(100) do |batch|
|
|
293
|
-
ProductIndex.bulk_delete(batch)
|
|
294
|
-
end
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
**SAFE:**
|
|
298
|
-
|
|
299
|
-
```ruby
|
|
300
|
-
# Never holds more than 100 records in memory
|
|
301
|
-
Product.where(discontinued: true).find_in_batches(batch_size: 100) do |batch|
|
|
302
|
-
ProductIndex.bulk_delete(batch)
|
|
303
|
-
end
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
---
|
|
307
|
-
|
|
308
|
-
## 1.4 Query Placement
|
|
309
|
-
|
|
310
|
-
Sorting, filtering, and aggregating in Ruby instead of SQL wastes both memory and CPU. The database is optimized for these operations.
|
|
311
|
-
|
|
312
|
-
### CHECK 1.4a: Sort and filter at the database level
|
|
313
|
-
|
|
314
|
-
> **What to look for:** Ruby `sort_by`, `select`, `reject`, `min_by`, `max_by`, `group_by` called on ActiveRecord collections that could use SQL `ORDER BY`, `WHERE`, `MIN`, `MAX`, `GROUP BY`
|
|
315
|
-
> **Where to look:** `app/models/**/*.rb`, `app/controllers/**/*.rb`
|
|
316
|
-
> **Severity:** Medium
|
|
317
|
-
|
|
318
|
-
**UNSAFE:**
|
|
319
|
-
|
|
320
|
-
```ruby
|
|
321
|
-
# Loads all records then sorts in Ruby
|
|
322
|
-
@users = User.all.sort_by(&:created_at).reverse
|
|
323
|
-
|
|
324
|
-
# Filters in Ruby after loading everything
|
|
325
|
-
active_users = User.all.select { |u| u.active? && u.confirmed? }
|
|
326
|
-
|
|
327
|
-
# Aggregates in Ruby
|
|
328
|
-
total = Order.all.sum(&:total_price)
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
**SAFE:**
|
|
332
|
-
|
|
333
|
-
```ruby
|
|
334
|
-
# Database handles sorting
|
|
335
|
-
@users = User.order(created_at: :desc)
|
|
336
|
-
|
|
337
|
-
# Database handles filtering
|
|
338
|
-
active_users = User.where(active: true, confirmed: true)
|
|
339
|
-
|
|
340
|
-
# Database handles aggregation
|
|
341
|
-
total = Order.sum(:total_price)
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
### CHECK 1.4b: Avoid Ruby computation on large datasets
|
|
345
|
-
|
|
346
|
-
> **What to look for:** `map`, `flat_map`, `reduce`, `inject`, `each_with_object` on ActiveRecord relations that could be replaced with SQL operations
|
|
347
|
-
> **Where to look:** `app/models/**/*.rb`, `app/controllers/**/*.rb`
|
|
348
|
-
> **Severity:** Medium
|
|
349
|
-
|
|
350
|
-
**UNSAFE:**
|
|
351
|
-
|
|
352
|
-
```ruby
|
|
353
|
-
# Loads all orders into Ruby to extract unique statuses
|
|
354
|
-
statuses = Order.all.map(&:status).uniq
|
|
355
|
-
|
|
356
|
-
# Ruby-side grouping
|
|
357
|
-
grouped = Transaction.all.group_by { |t| t.created_at.to_date }
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
**SAFE:**
|
|
361
|
-
|
|
362
|
-
```ruby
|
|
363
|
-
# Single query, returns array of strings
|
|
364
|
-
statuses = Order.distinct.pluck(:status)
|
|
365
|
-
|
|
366
|
-
# Database-side grouping
|
|
367
|
-
grouped = Transaction.group("DATE(created_at)").count
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
---
|
|
371
|
-
|
|
372
|
-
# Part 2: Database Indexing
|
|
373
|
-
|
|
374
|
-
Missing or poorly designed indexes are the most common cause of slow queries in production. These checks verify that the schema supports the queries the application makes.
|
|
375
|
-
|
|
376
|
-
## 2.1 Missing Indexes
|
|
377
|
-
|
|
378
|
-
### CHECK 2.1a: Foreign key columns indexed
|
|
379
|
-
|
|
380
|
-
> **What to look for:** `belongs_to` associations or `_id` columns in migrations without a corresponding database index
|
|
381
|
-
> **Where to look:** `db/migrate/**/*.rb`, `db/schema.rb`
|
|
382
|
-
> **Severity:** High
|
|
383
|
-
|
|
384
|
-
**UNSAFE:**
|
|
385
|
-
|
|
386
|
-
```ruby
|
|
387
|
-
class CreateComments < ActiveRecord::Migration[7.1]
|
|
388
|
-
def change
|
|
389
|
-
create_table :comments do |t|
|
|
390
|
-
t.references :post, foreign_key: true, index: false
|
|
391
|
-
t.integer :user_id # No index
|
|
392
|
-
t.text :body
|
|
393
|
-
t.timestamps
|
|
394
|
-
end
|
|
395
|
-
end
|
|
396
|
-
end
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
**SAFE:**
|
|
400
|
-
|
|
401
|
-
```ruby
|
|
402
|
-
class CreateComments < ActiveRecord::Migration[7.1]
|
|
403
|
-
def change
|
|
404
|
-
create_table :comments do |t|
|
|
405
|
-
t.references :post, foreign_key: true # index: true is default
|
|
406
|
-
t.references :user, foreign_key: true
|
|
407
|
-
t.text :body
|
|
408
|
-
t.timestamps
|
|
409
|
-
end
|
|
410
|
-
end
|
|
411
|
-
end
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
### CHECK 2.1b: Frequently queried columns indexed
|
|
415
|
-
|
|
416
|
-
> **What to look for:** Columns used in `where`, `order`, `group`, or `find_by` clauses that lack indexes — especially `status`, `type`, `slug`, `email`, and date columns
|
|
417
|
-
> **Where to look:** `db/migrate/**/*.rb`, `db/schema.rb`, `app/models/**/*.rb`
|
|
418
|
-
> **Severity:** High
|
|
419
|
-
|
|
420
|
-
**UNSAFE:**
|
|
421
|
-
|
|
422
|
-
```ruby
|
|
423
|
-
# Model uses scopes on status and slug, but no indexes exist
|
|
424
|
-
class Article < ApplicationRecord
|
|
425
|
-
scope :published, -> { where(status: "published") }
|
|
426
|
-
scope :by_slug, ->(slug) { find_by!(slug: slug) }
|
|
427
|
-
end
|
|
428
|
-
|
|
429
|
-
# schema.rb shows no index on status or slug
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
**SAFE:**
|
|
433
|
-
|
|
434
|
-
```ruby
|
|
435
|
-
class AddIndexesToArticles < ActiveRecord::Migration[7.1]
|
|
436
|
-
def change
|
|
437
|
-
add_index :articles, :status
|
|
438
|
-
add_index :articles, :slug, unique: true
|
|
439
|
-
end
|
|
440
|
-
end
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
### CHECK 2.1c: Composite indexes for multi-column queries
|
|
444
|
-
|
|
445
|
-
> **What to look for:** Queries with multiple `WHERE` conditions or `WHERE` + `ORDER BY` that use separate single-column indexes instead of a composite index
|
|
446
|
-
> **Where to look:** `db/migrate/**/*.rb`, `db/schema.rb`, `app/models/**/*.rb`
|
|
447
|
-
> **Severity:** Medium
|
|
448
|
-
|
|
449
|
-
**UNSAFE:**
|
|
450
|
-
|
|
451
|
-
```ruby
|
|
452
|
-
# Frequent query pattern
|
|
453
|
-
Order.where(user_id: user.id, status: "pending").order(:created_at)
|
|
454
|
-
|
|
455
|
-
# Only single-column indexes exist
|
|
456
|
-
add_index :orders, :user_id
|
|
457
|
-
add_index :orders, :status
|
|
458
|
-
```
|
|
459
|
-
|
|
460
|
-
**SAFE:**
|
|
461
|
-
|
|
462
|
-
```ruby
|
|
463
|
-
# Composite index matches the query pattern
|
|
464
|
-
add_index :orders, [:user_id, :status, :created_at]
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
---
|
|
468
|
-
|
|
469
|
-
## 2.2 Index Anti-Patterns
|
|
470
|
-
|
|
471
|
-
### CHECK 2.2a: Concurrent index creation in production
|
|
472
|
-
|
|
473
|
-
> **What to look for:** `add_index` in migrations without `algorithm: :concurrently` for large production tables; missing `disable_ddl_transaction!`
|
|
474
|
-
> **Where to look:** `db/migrate/**/*.rb`
|
|
475
|
-
> **Severity:** High
|
|
476
|
-
|
|
477
|
-
**UNSAFE:**
|
|
478
|
-
|
|
479
|
-
```ruby
|
|
480
|
-
class AddIndexToOrdersStatus < ActiveRecord::Migration[7.1]
|
|
481
|
-
def change
|
|
482
|
-
# Locks the entire orders table during index creation
|
|
483
|
-
add_index :orders, :status
|
|
484
|
-
end
|
|
485
|
-
end
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
**SAFE:**
|
|
489
|
-
|
|
490
|
-
```ruby
|
|
491
|
-
class AddIndexToOrdersStatus < ActiveRecord::Migration[7.1]
|
|
492
|
-
disable_ddl_transaction!
|
|
493
|
-
|
|
494
|
-
def change
|
|
495
|
-
add_index :orders, :status, algorithm: :concurrently
|
|
496
|
-
end
|
|
497
|
-
end
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
### CHECK 2.2b: Partial indexes for scoped queries
|
|
501
|
-
|
|
502
|
-
> **What to look for:** Full-table indexes on columns where queries always include a filtering condition (e.g., only active records, only non-null values)
|
|
503
|
-
> **Where to look:** `db/migrate/**/*.rb`, `app/models/**/*.rb`
|
|
504
|
-
> **Severity:** Low
|
|
505
|
-
|
|
506
|
-
**UNSAFE:**
|
|
507
|
-
|
|
508
|
-
```ruby
|
|
509
|
-
# Full index when queries always filter for active
|
|
510
|
-
add_index :users, :email
|
|
511
|
-
|
|
512
|
-
# But the query is always:
|
|
513
|
-
User.where(active: true).find_by(email: email)
|
|
514
|
-
```
|
|
515
|
-
|
|
516
|
-
**SAFE:**
|
|
517
|
-
|
|
518
|
-
```ruby
|
|
519
|
-
# Partial index — smaller, faster, only covers active users
|
|
520
|
-
add_index :users, :email, where: "active = true", unique: true
|
|
521
|
-
```
|
|
522
|
-
|
|
523
|
-
---
|
|
524
|
-
|
|
525
|
-
# Part 3: Caching
|
|
526
|
-
|
|
527
|
-
Caching reduces redundant computation and database queries. These checks verify that caching is properly configured and applied where it matters most.
|
|
528
|
-
|
|
529
|
-
## 3.1 Cache Store Configuration
|
|
530
|
-
|
|
531
|
-
### CHECK 3.1a: Production cache store is not file or memory
|
|
532
|
-
|
|
533
|
-
> **What to look for:** `config.cache_store` set to `:file_store` or `:memory_store` in production; missing cache store configuration for production
|
|
534
|
-
> **Where to look:** `config/environments/production.rb`
|
|
535
|
-
> **Severity:** High
|
|
536
|
-
|
|
537
|
-
**UNSAFE:**
|
|
538
|
-
|
|
539
|
-
```ruby
|
|
540
|
-
# config/environments/production.rb
|
|
541
|
-
config.cache_store = :file_store, "/tmp/cache"
|
|
542
|
-
|
|
543
|
-
# Or worse — default memory store in production
|
|
544
|
-
# (no config.cache_store set at all)
|
|
545
|
-
```
|
|
546
|
-
|
|
547
|
-
**SAFE:**
|
|
548
|
-
|
|
549
|
-
```ruby
|
|
550
|
-
# config/environments/production.rb
|
|
551
|
-
config.cache_store = :redis_cache_store, {
|
|
552
|
-
url: ENV.fetch("REDIS_URL"),
|
|
553
|
-
expires_in: 1.hour
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
# Or Memcached:
|
|
557
|
-
config.cache_store = :mem_cache_store, ENV.fetch("MEMCACHED_URL")
|
|
558
|
-
|
|
559
|
-
# Or Rails 8+ Solid Cache:
|
|
560
|
-
config.cache_store = :solid_cache_store
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
---
|
|
564
|
-
|
|
565
|
-
## 3.2 Fragment & Collection Caching
|
|
566
|
-
|
|
567
|
-
### CHECK 3.2a: Cache expensive view partials
|
|
568
|
-
|
|
569
|
-
> **What to look for:** Partials that execute queries or expensive computations rendered without `cache` blocks; missing cache keys on frequently rendered partials
|
|
570
|
-
> **Where to look:** `app/views/**/*.erb`
|
|
571
|
-
> **Severity:** Medium
|
|
572
|
-
|
|
573
|
-
**UNSAFE:**
|
|
574
|
-
|
|
575
|
-
```erb
|
|
576
|
-
<%# Rendered on every request, executes queries each time %>
|
|
577
|
-
<%= render partial: "dashboard/stats" %>
|
|
578
|
-
```
|
|
579
|
-
|
|
580
|
-
**SAFE:**
|
|
581
|
-
|
|
582
|
-
```erb
|
|
583
|
-
<% cache("dashboard/stats", expires_in: 5.minutes) do %>
|
|
584
|
-
<%= render partial: "dashboard/stats" %>
|
|
585
|
-
<% end %>
|
|
586
|
-
|
|
587
|
-
<%# Or with record-based cache key: %>
|
|
588
|
-
<% cache(@project) do %>
|
|
589
|
-
<%= render partial: "projects/detail", locals: { project: @project } %>
|
|
590
|
-
<% end %>
|
|
591
|
-
```
|
|
592
|
-
|
|
593
|
-
### CHECK 3.2b: Collection rendering with caching
|
|
594
|
-
|
|
595
|
-
> **What to look for:** `render collection:` without the `cached: true` option on collections that don't change frequently
|
|
596
|
-
> **Where to look:** `app/views/**/*.erb`
|
|
597
|
-
> **Severity:** Medium
|
|
598
|
-
|
|
599
|
-
**UNSAFE:**
|
|
600
|
-
|
|
601
|
-
```erb
|
|
602
|
-
<%# Renders each partial individually, no caching %>
|
|
603
|
-
<%= render partial: "products/product", collection: @products %>
|
|
604
|
-
```
|
|
605
|
-
|
|
606
|
-
**SAFE:**
|
|
607
|
-
|
|
608
|
-
```erb
|
|
609
|
-
<%# Multi-fetch caching — reads all cache keys in one round-trip %>
|
|
610
|
-
<%= render partial: "products/product", collection: @products, cached: true %>
|
|
611
|
-
```
|
|
612
|
-
|
|
613
|
-
---
|
|
614
|
-
|
|
615
|
-
## 3.3 Application-Level Caching
|
|
616
|
-
|
|
617
|
-
### CHECK 3.3a: Cache expensive computations
|
|
618
|
-
|
|
619
|
-
> **What to look for:** Repeated expensive computations (API calls, complex aggregations, report generation) without `Rails.cache.fetch`
|
|
620
|
-
> **Where to look:** `app/models/**/*.rb`, `app/controllers/**/*.rb`, `lib/**/*.rb`
|
|
621
|
-
> **Severity:** Medium
|
|
622
|
-
|
|
623
|
-
**UNSAFE:**
|
|
624
|
-
|
|
625
|
-
```ruby
|
|
626
|
-
class Dashboard
|
|
627
|
-
def stats
|
|
628
|
-
# Runs complex aggregation on every call
|
|
629
|
-
{
|
|
630
|
-
total_revenue: Order.where(status: "completed").sum(:total),
|
|
631
|
-
active_users: User.where("last_sign_in_at > ?", 30.days.ago).count,
|
|
632
|
-
conversion_rate: calculate_conversion_rate
|
|
633
|
-
}
|
|
634
|
-
end
|
|
635
|
-
end
|
|
636
|
-
```
|
|
637
|
-
|
|
638
|
-
**SAFE:**
|
|
639
|
-
|
|
640
|
-
```ruby
|
|
641
|
-
class Dashboard
|
|
642
|
-
def stats
|
|
643
|
-
Rails.cache.fetch("dashboard/stats", expires_in: 15.minutes) do
|
|
644
|
-
{
|
|
645
|
-
total_revenue: Order.where(status: "completed").sum(:total),
|
|
646
|
-
active_users: User.where("last_sign_in_at > ?", 30.days.ago).count,
|
|
647
|
-
conversion_rate: calculate_conversion_rate
|
|
648
|
-
}
|
|
649
|
-
end
|
|
650
|
-
end
|
|
651
|
-
end
|
|
652
|
-
```
|
|
653
|
-
|
|
654
|
-
### CHECK 3.3b: Memoization for per-request computation
|
|
655
|
-
|
|
656
|
-
> **What to look for:** Methods called multiple times per request that perform database queries or computations without memoization
|
|
657
|
-
> **Where to look:** `app/models/**/*.rb`, `app/controllers/**/*.rb`, `app/helpers/**/*.rb`
|
|
658
|
-
> **Severity:** Low
|
|
659
|
-
|
|
660
|
-
**UNSAFE:**
|
|
661
|
-
|
|
662
|
-
```ruby
|
|
663
|
-
class User < ApplicationRecord
|
|
664
|
-
def active_subscription
|
|
665
|
-
# Queries the database every time this is called
|
|
666
|
-
subscriptions.where(active: true).order(created_at: :desc).first
|
|
667
|
-
end
|
|
668
|
-
end
|
|
669
|
-
```
|
|
670
|
-
|
|
671
|
-
**SAFE:**
|
|
672
|
-
|
|
673
|
-
```ruby
|
|
674
|
-
class User < ApplicationRecord
|
|
675
|
-
def active_subscription
|
|
676
|
-
@active_subscription ||= subscriptions.where(active: true).order(created_at: :desc).first
|
|
677
|
-
end
|
|
678
|
-
end
|
|
679
|
-
```
|
|
680
|
-
|
|
681
|
-
---
|
|
682
|
-
|
|
683
|
-
# Part 4: Memory & Resource Management
|
|
684
|
-
|
|
685
|
-
These checks prevent excessive memory usage, ensure expensive operations run in the background, and reduce unnecessary object allocation.
|
|
686
|
-
|
|
687
|
-
## 4.1 Memory-Intensive Operations
|
|
688
|
-
|
|
689
|
-
### CHECK 4.1a: Avoid loading full result sets for partial data
|
|
690
|
-
|
|
691
|
-
> **What to look for:** Loading full ActiveRecord objects when only specific columns are needed; `.all` or `.where(...)` followed by `.map` to extract attributes
|
|
692
|
-
> **Where to look:** `app/models/**/*.rb`, `app/controllers/**/*.rb`, `app/jobs/**/*.rb`
|
|
693
|
-
> **Severity:** High
|
|
694
|
-
|
|
695
|
-
**UNSAFE:**
|
|
696
|
-
|
|
697
|
-
```ruby
|
|
698
|
-
# Loads full User objects (all columns) just to get names
|
|
699
|
-
names = User.where(role: "admin").map(&:name)
|
|
700
|
-
|
|
701
|
-
# Loads all orders into memory to sum a column
|
|
702
|
-
total = Order.where(user_id: user.id).map(&:total_price).sum
|
|
703
|
-
```
|
|
704
|
-
|
|
705
|
-
**SAFE:**
|
|
706
|
-
|
|
707
|
-
```ruby
|
|
708
|
-
# Returns plain array of strings — no ActiveRecord objects
|
|
709
|
-
names = User.where(role: "admin").pluck(:name)
|
|
710
|
-
|
|
711
|
-
# Single SQL SUM — no records loaded
|
|
712
|
-
total = Order.where(user_id: user.id).sum(:total_price)
|
|
713
|
-
```
|
|
714
|
-
|
|
715
|
-
### CHECK 4.1b: Stream large file exports
|
|
716
|
-
|
|
717
|
-
> **What to look for:** CSV or file exports that build the entire file in memory before sending; `CSV.generate` on unbounded datasets
|
|
718
|
-
> **Where to look:** `app/controllers/**/*.rb`, `lib/**/*.rb`
|
|
719
|
-
> **Severity:** High
|
|
720
|
-
|
|
721
|
-
**UNSAFE:**
|
|
722
|
-
|
|
723
|
-
```ruby
|
|
724
|
-
def export
|
|
725
|
-
csv = CSV.generate do |csv|
|
|
726
|
-
csv << ["Name", "Email", "Created"]
|
|
727
|
-
User.all.each do |user|
|
|
728
|
-
csv << [user.name, user.email, user.created_at]
|
|
729
|
-
end
|
|
730
|
-
end
|
|
731
|
-
send_data csv, filename: "users.csv"
|
|
732
|
-
end
|
|
733
|
-
```
|
|
734
|
-
|
|
735
|
-
**SAFE:**
|
|
736
|
-
|
|
737
|
-
```ruby
|
|
738
|
-
def export
|
|
739
|
-
headers["Content-Disposition"] = 'attachment; filename="users.csv"'
|
|
740
|
-
headers["Content-Type"] = "text/csv"
|
|
741
|
-
|
|
742
|
-
response.status = 200
|
|
743
|
-
self.response_body = Enumerator.new do |yielder|
|
|
744
|
-
yielder << CSV.generate_line(["Name", "Email", "Created"])
|
|
745
|
-
User.find_each do |user|
|
|
746
|
-
yielder << CSV.generate_line([user.name, user.email, user.created_at])
|
|
747
|
-
end
|
|
748
|
-
end
|
|
749
|
-
end
|
|
750
|
-
```
|
|
751
|
-
|
|
752
|
-
---
|
|
753
|
-
|
|
754
|
-
## 4.2 Background Job Offloading
|
|
755
|
-
|
|
756
|
-
### CHECK 4.2a: Expensive work not in request cycle
|
|
757
|
-
|
|
758
|
-
> **What to look for:** Email delivery, PDF generation, API calls to external services, image processing, or report generation executed synchronously in controller actions
|
|
759
|
-
> **Where to look:** `app/controllers/**/*.rb`
|
|
760
|
-
> **Severity:** High
|
|
761
|
-
|
|
762
|
-
**UNSAFE:**
|
|
763
|
-
|
|
764
|
-
```ruby
|
|
765
|
-
class OrdersController < ApplicationController
|
|
766
|
-
def create
|
|
767
|
-
@order = Order.create!(order_params)
|
|
768
|
-
OrderMailer.confirmation(@order).deliver_now # Blocks response
|
|
769
|
-
PdfGenerator.generate_invoice(@order) # Blocks response
|
|
770
|
-
InventoryApi.reserve_items(@order.line_items) # Blocks response
|
|
771
|
-
redirect_to @order
|
|
772
|
-
end
|
|
773
|
-
end
|
|
774
|
-
```
|
|
775
|
-
|
|
776
|
-
**SAFE:**
|
|
777
|
-
|
|
778
|
-
```ruby
|
|
779
|
-
class OrdersController < ApplicationController
|
|
780
|
-
def create
|
|
781
|
-
@order = Order.create!(order_params)
|
|
782
|
-
OrderMailer.confirmation(@order).deliver_later # Background job
|
|
783
|
-
GenerateInvoiceJob.perform_later(@order) # Background job
|
|
784
|
-
ReserveInventoryJob.perform_later(@order.line_items) # Background job
|
|
785
|
-
redirect_to @order
|
|
786
|
-
end
|
|
787
|
-
end
|
|
788
|
-
```
|
|
789
|
-
|
|
790
|
-
### CHECK 4.2b: Use deliver_later for emails
|
|
791
|
-
|
|
792
|
-
> **What to look for:** `deliver_now` in controller actions or model callbacks; synchronous email delivery in the request cycle
|
|
793
|
-
> **Where to look:** `app/controllers/**/*.rb`, `app/models/**/*.rb`
|
|
794
|
-
> **Severity:** High
|
|
795
|
-
|
|
796
|
-
**UNSAFE:**
|
|
797
|
-
|
|
798
|
-
```ruby
|
|
799
|
-
UserMailer.welcome(user).deliver_now
|
|
800
|
-
NotificationMailer.alert(admin).deliver_now
|
|
801
|
-
```
|
|
802
|
-
|
|
803
|
-
**SAFE:**
|
|
804
|
-
|
|
805
|
-
```ruby
|
|
806
|
-
UserMailer.welcome(user).deliver_later
|
|
807
|
-
NotificationMailer.alert(admin).deliver_later
|
|
808
|
-
```
|
|
809
|
-
|
|
810
|
-
---
|
|
811
|
-
|
|
812
|
-
## 4.3 Unnecessary Object Allocation
|
|
813
|
-
|
|
814
|
-
### CHECK 4.3a: Freeze string literals used as constants
|
|
815
|
-
|
|
816
|
-
> **What to look for:** String literals assigned to constants or used repeatedly without freezing; missing `# frozen_string_literal: true` pragma
|
|
817
|
-
> **Where to look:** `app/**/*.rb`, `lib/**/*.rb`
|
|
818
|
-
> **Severity:** Low
|
|
819
|
-
|
|
820
|
-
**UNSAFE:**
|
|
821
|
-
|
|
822
|
-
```ruby
|
|
823
|
-
class PaymentProcessor
|
|
824
|
-
DEFAULT_CURRENCY = "usd" # Allocates a new string every access
|
|
825
|
-
SUPPORTED_TYPES = ["card", "bank"] # Allocates new array and strings
|
|
826
|
-
|
|
827
|
-
def process(amount)
|
|
828
|
-
currency = "usd" # New string object every call
|
|
829
|
-
# ...
|
|
830
|
-
end
|
|
831
|
-
end
|
|
832
|
-
```
|
|
833
|
-
|
|
834
|
-
**SAFE:**
|
|
835
|
-
|
|
836
|
-
```ruby
|
|
837
|
-
# frozen_string_literal: true
|
|
838
|
-
|
|
839
|
-
class PaymentProcessor
|
|
840
|
-
DEFAULT_CURRENCY = "usd"
|
|
841
|
-
SUPPORTED_TYPES = %w[card bank].freeze
|
|
842
|
-
|
|
843
|
-
def process(amount)
|
|
844
|
-
# String literals are frozen by the pragma
|
|
845
|
-
# ...
|
|
846
|
-
end
|
|
847
|
-
end
|
|
848
|
-
```
|
|
849
|
-
|
|
850
|
-
### CHECK 4.3b: Avoid repeated allocations in loops
|
|
851
|
-
|
|
852
|
-
> **What to look for:** Object creation (strings, arrays, hashes, regex) inside loops where the object could be extracted to a constant or local variable
|
|
853
|
-
> **Where to look:** `app/**/*.rb`, `lib/**/*.rb`
|
|
854
|
-
> **Severity:** Low
|
|
855
|
-
|
|
856
|
-
**UNSAFE:**
|
|
857
|
-
|
|
858
|
-
```ruby
|
|
859
|
-
users.each do |user|
|
|
860
|
-
if user.name.match?(/\A[A-Z]/) # Regex compiled every iteration
|
|
861
|
-
tags = user.bio.split(",").map(&:strip)
|
|
862
|
-
formatted = "User: #{user.name}" # Fine with frozen_string_literal
|
|
863
|
-
end
|
|
864
|
-
end
|
|
865
|
-
```
|
|
866
|
-
|
|
867
|
-
**SAFE:**
|
|
868
|
-
|
|
869
|
-
```ruby
|
|
870
|
-
STARTS_WITH_CAPITAL = /\A[A-Z]/
|
|
871
|
-
|
|
872
|
-
users.each do |user|
|
|
873
|
-
if user.name.match?(STARTS_WITH_CAPITAL)
|
|
874
|
-
tags = user.bio.split(",").map(&:strip)
|
|
875
|
-
formatted = "User: #{user.name}"
|
|
876
|
-
end
|
|
877
|
-
end
|
|
878
|
-
```
|
|
879
|
-
|
|
880
|
-
---
|
|
881
|
-
|
|
882
|
-
# Part 5: View & Response Performance
|
|
883
|
-
|
|
884
|
-
These checks cover efficient rendering of collections and frontend asset delivery.
|
|
885
|
-
|
|
886
|
-
## 5.1 Collection Rendering
|
|
887
|
-
|
|
888
|
-
### CHECK 5.1a: Use render collection instead of loops
|
|
889
|
-
|
|
890
|
-
> **What to look for:** Manual `each` loops in views rendering the same partial repeatedly instead of `render collection:`
|
|
891
|
-
> **Where to look:** `app/views/**/*.erb`
|
|
892
|
-
> **Severity:** Medium
|
|
893
|
-
|
|
894
|
-
**UNSAFE:**
|
|
895
|
-
|
|
896
|
-
```erb
|
|
897
|
-
<% @products.each do |product| %>
|
|
898
|
-
<%= render partial: "products/product", locals: { product: product } %>
|
|
899
|
-
<% end %>
|
|
900
|
-
```
|
|
901
|
-
|
|
902
|
-
**SAFE:**
|
|
903
|
-
|
|
904
|
-
```erb
|
|
905
|
-
<%= render partial: "products/product", collection: @products, as: :product %>
|
|
906
|
-
|
|
907
|
-
<%# Or shorthand: %>
|
|
908
|
-
<%= render @products %>
|
|
909
|
-
```
|
|
910
|
-
|
|
911
|
-
### CHECK 5.1b: Efficient JSON serialization
|
|
912
|
-
|
|
913
|
-
> **What to look for:** Calling `.to_json` on full ActiveRecord objects or collections; rendering JSON without selecting specific attributes
|
|
914
|
-
> **Where to look:** `app/controllers/**/*.rb`, `app/controllers/api/**/*.rb`
|
|
915
|
-
> **Severity:** Medium
|
|
916
|
-
|
|
917
|
-
**UNSAFE:**
|
|
918
|
-
|
|
919
|
-
```ruby
|
|
920
|
-
def index
|
|
921
|
-
@users = User.all
|
|
922
|
-
render json: @users.to_json
|
|
923
|
-
end
|
|
924
|
-
```
|
|
925
|
-
|
|
926
|
-
**SAFE:**
|
|
927
|
-
|
|
928
|
-
```ruby
|
|
929
|
-
def index
|
|
930
|
-
@users = User.select(:id, :name, :email)
|
|
931
|
-
render json: @users.as_json(only: [:id, :name, :email])
|
|
932
|
-
end
|
|
933
|
-
|
|
934
|
-
# Or with jbuilder for complex responses:
|
|
935
|
-
# app/views/api/users/index.json.jbuilder
|
|
936
|
-
```
|
|
937
|
-
|
|
938
|
-
---
|
|
939
|
-
|
|
940
|
-
## 5.2 Asset & Frontend Performance
|
|
941
|
-
|
|
942
|
-
### CHECK 5.2a: Pagination for large collections
|
|
943
|
-
|
|
944
|
-
> **What to look for:** Controller actions that load unbounded collections for display in views; missing pagination on index actions
|
|
945
|
-
> **Where to look:** `app/controllers/**/*.rb`
|
|
946
|
-
> **Severity:** High
|
|
947
|
-
|
|
948
|
-
**UNSAFE:**
|
|
949
|
-
|
|
950
|
-
```ruby
|
|
951
|
-
def index
|
|
952
|
-
@orders = Order.all
|
|
953
|
-
end
|
|
954
|
-
```
|
|
955
|
-
|
|
956
|
-
**SAFE:**
|
|
957
|
-
|
|
958
|
-
```ruby
|
|
959
|
-
def index
|
|
960
|
-
@orders = Order.order(created_at: :desc).page(params[:page]).per(25)
|
|
961
|
-
end
|
|
962
|
-
|
|
963
|
-
# Or with cursor-based pagination:
|
|
964
|
-
def index
|
|
965
|
-
@orders = Order.where("id < ?", params[:cursor] || Float::INFINITY)
|
|
966
|
-
.order(id: :desc)
|
|
967
|
-
.limit(25)
|
|
968
|
-
end
|
|
969
|
-
```
|
|
970
|
-
|
|
971
|
-
### CHECK 5.2b: Turbo Frame lazy loading for heavy sections
|
|
972
|
-
|
|
973
|
-
> **What to look for:** Page sections that load expensive data on every full page load when they could be deferred with Turbo Frames
|
|
974
|
-
> **Where to look:** `app/views/**/*.erb`
|
|
975
|
-
> **Severity:** Low
|
|
976
|
-
|
|
977
|
-
**UNSAFE:**
|
|
978
|
-
|
|
979
|
-
```erb
|
|
980
|
-
<%# Stats section loads on every page visit, even if user doesn't scroll to it %>
|
|
981
|
-
<div id="stats">
|
|
982
|
-
<%= render "dashboard/heavy_stats" %>
|
|
983
|
-
</div>
|
|
984
|
-
```
|
|
985
|
-
|
|
986
|
-
**SAFE:**
|
|
987
|
-
|
|
988
|
-
```erb
|
|
989
|
-
<%# Loaded lazily only when the frame enters the viewport %>
|
|
990
|
-
<%= turbo_frame_tag "stats", src: dashboard_stats_path, loading: :lazy do %>
|
|
991
|
-
<p>Loading stats...</p>
|
|
992
|
-
<% end %>
|
|
993
|
-
```
|
|
994
|
-
|
|
995
|
-
---
|
|
996
|
-
|
|
997
|
-
# Part 6: Deployment & Configuration
|
|
998
|
-
|
|
999
|
-
These checks verify that production environments are tuned for performance.
|
|
1000
|
-
|
|
1001
|
-
## 6.1 Server Tuning
|
|
1002
|
-
|
|
1003
|
-
### CHECK 6.1a: Puma threads and workers configured
|
|
1004
|
-
|
|
1005
|
-
> **What to look for:** Default Puma configuration in production; missing or improperly sized `threads` and `workers` settings
|
|
1006
|
-
> **Where to look:** `config/puma.rb`
|
|
1007
|
-
> **Severity:** High
|
|
1008
|
-
|
|
1009
|
-
**UNSAFE:**
|
|
1010
|
-
|
|
1011
|
-
```ruby
|
|
1012
|
-
# config/puma.rb — defaults, not tuned for production
|
|
1013
|
-
threads_count = 5
|
|
1014
|
-
threads threads_count, threads_count
|
|
1015
|
-
```
|
|
1016
|
-
|
|
1017
|
-
**SAFE:**
|
|
1018
|
-
|
|
1019
|
-
```ruby
|
|
1020
|
-
# config/puma.rb
|
|
1021
|
-
max_threads = ENV.fetch("RAILS_MAX_THREADS", 5).to_i
|
|
1022
|
-
min_threads = ENV.fetch("RAILS_MIN_THREADS", max_threads).to_i
|
|
1023
|
-
threads min_threads, max_threads
|
|
1024
|
-
|
|
1025
|
-
workers ENV.fetch("WEB_CONCURRENCY", 2).to_i
|
|
1026
|
-
preload_app!
|
|
1027
|
-
```
|
|
1028
|
-
|
|
1029
|
-
### CHECK 6.1b: YJIT enabled in production
|
|
1030
|
-
|
|
1031
|
-
> **What to look for:** Ruby 3.2+ applications not enabling YJIT; missing `RUBY_YJIT_ENABLE` or `--yjit` flag
|
|
1032
|
-
> **Where to look:** `config/puma.rb`, `Dockerfile`, `.ruby-version`
|
|
1033
|
-
> **Severity:** Medium
|
|
1034
|
-
|
|
1035
|
-
**UNSAFE:**
|
|
1036
|
-
|
|
1037
|
-
```dockerfile
|
|
1038
|
-
# Dockerfile — YJIT not enabled
|
|
1039
|
-
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
|
|
1040
|
-
```
|
|
1041
|
-
|
|
1042
|
-
**SAFE:**
|
|
1043
|
-
|
|
1044
|
-
```dockerfile
|
|
1045
|
-
# Dockerfile — YJIT enabled via environment variable
|
|
1046
|
-
ENV RUBY_YJIT_ENABLE=1
|
|
1047
|
-
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
|
|
1048
|
-
```
|
|
1049
|
-
|
|
1050
|
-
```ruby
|
|
1051
|
-
# Or in config/puma.rb:
|
|
1052
|
-
if defined?(RubyVM::YJIT) && RubyVM::YJIT.respond_to?(:enable)
|
|
1053
|
-
RubyVM::YJIT.enable
|
|
1054
|
-
end
|
|
1055
|
-
```
|
|
1056
|
-
|
|
1057
|
-
---
|
|
1058
|
-
|
|
1059
|
-
## 6.2 Production Settings
|
|
1060
|
-
|
|
1061
|
-
### CHECK 6.2a: Cache classes and eager load enabled
|
|
1062
|
-
|
|
1063
|
-
> **What to look for:** `config.cache_classes = false` or `config.eager_load = false` in production environment
|
|
1064
|
-
> **Where to look:** `config/environments/production.rb`
|
|
1065
|
-
> **Severity:** High
|
|
1066
|
-
|
|
1067
|
-
**UNSAFE:**
|
|
1068
|
-
|
|
1069
|
-
```ruby
|
|
1070
|
-
# config/environments/production.rb
|
|
1071
|
-
config.cache_classes = false # Reloads code on every request
|
|
1072
|
-
config.eager_load = false # Lazy loads, causing slow first requests
|
|
1073
|
-
```
|
|
1074
|
-
|
|
1075
|
-
**SAFE:**
|
|
1076
|
-
|
|
1077
|
-
```ruby
|
|
1078
|
-
# config/environments/production.rb
|
|
1079
|
-
config.cache_classes = true
|
|
1080
|
-
config.eager_load = true
|
|
1081
|
-
```
|
|
1082
|
-
|
|
1083
|
-
### CHECK 6.2b: Asset compression and digests enabled
|
|
1084
|
-
|
|
1085
|
-
> **What to look for:** Missing asset compression or digest fingerprinting in production; uncompressed CSS and JavaScript
|
|
1086
|
-
> **Where to look:** `config/environments/production.rb`
|
|
1087
|
-
> **Severity:** Medium
|
|
1088
|
-
|
|
1089
|
-
**UNSAFE:**
|
|
1090
|
-
|
|
1091
|
-
```ruby
|
|
1092
|
-
# config/environments/production.rb
|
|
1093
|
-
config.assets.compile = true # On-the-fly compilation in production
|
|
1094
|
-
config.assets.digest = false # No cache-busting fingerprints
|
|
1095
|
-
```
|
|
1096
|
-
|
|
1097
|
-
**SAFE:**
|
|
1098
|
-
|
|
1099
|
-
```ruby
|
|
1100
|
-
# config/environments/production.rb
|
|
1101
|
-
config.assets.compile = false
|
|
1102
|
-
config.assets.digest = true
|
|
1103
|
-
config.assets.css_compressor = :sass
|
|
1104
|
-
```
|
|
1105
|
-
|
|
1106
|
-
---
|
|
1107
|
-
|
|
1108
|
-
# Part 7: Performance Verification Checklist
|
|
1109
|
-
|
|
1110
|
-
## 7.1 Agent Check Protocol
|
|
1111
|
-
|
|
1112
|
-
When verifying performance after a development phase, follow this protocol to map changed files to relevant checks.
|
|
1113
|
-
|
|
1114
|
-
### Step 1: Identify changed files
|
|
1115
|
-
|
|
1116
|
-
```bash
|
|
1117
|
-
git diff --name-only HEAD~1
|
|
1118
|
-
# Or for a phase: git diff <phase-start-sha>..HEAD --name-only
|
|
1119
|
-
```
|
|
1120
|
-
|
|
1121
|
-
### Step 2: Map files to check sections
|
|
1122
|
-
|
|
1123
|
-
| Changed file pattern | Applicable sections |
|
|
1124
|
-
|---|---|
|
|
1125
|
-
| `app/models/**/*.rb` | 1.1 (N+1), 1.2 (inefficient queries), 1.4 (query placement), 3.3 (caching), 4.1 (memory) |
|
|
1126
|
-
| `app/controllers/**/*.rb` | 1.1a (eager loading), 1.2b (exists?), 4.2 (background jobs), 5.1b (JSON), 5.2a (pagination) |
|
|
1127
|
-
| `app/views/**/*.erb` | 1.1a (N+1 in views), 1.1c (counter cache), 3.2 (fragment caching), 5.1a (collection rendering), 5.2b (lazy loading) |
|
|
1128
|
-
| `app/jobs/**/*.rb` | 1.3 (batch processing), 4.1 (memory) |
|
|
1129
|
-
| `db/migrate/**/*.rb` | 2.1 (missing indexes), 2.2 (index anti-patterns) |
|
|
1130
|
-
| `db/schema.rb` | 2.1 (missing indexes) |
|
|
1131
|
-
| `config/puma.rb` | 6.1 (server tuning) |
|
|
1132
|
-
| `config/environments/production.rb` | 3.1 (cache store), 6.2 (production settings) |
|
|
1133
|
-
| `lib/**/*.rb` | 1.3 (batch processing), 4.1 (memory), 4.3 (object allocation) |
|
|
1134
|
-
| `Dockerfile` | 6.1b (YJIT) |
|
|
1135
|
-
|
|
1136
|
-
### Step 3: Run applicable checks
|
|
1137
|
-
|
|
1138
|
-
For each applicable section, scan the changed files using the "Where to look" glob pattern and "What to look for" pattern. Report findings as:
|
|
1139
|
-
|
|
1140
|
-
```
|
|
1141
|
-
PASS: CHECK 1.1a — Eager load associations (scanned 3 files)
|
|
1142
|
-
FAIL: CHECK 1.3a — User.all.each in app/jobs/digest_job.rb:12
|
|
1143
|
-
SKIP: CHECK 2.1a — No migration changes in this phase
|
|
1144
|
-
```
|
|
1145
|
-
|
|
1146
|
-
---
|
|
1147
|
-
|
|
1148
|
-
## 7.2 Quick-Reference Checklist
|
|
1149
|
-
|
|
1150
|
-
All checks from Parts 1-6 in a single table for quick scanning.
|
|
1151
|
-
|
|
1152
|
-
| Check | Name | Severity | Grep pattern | Files |
|
|
1153
|
-
|---|---|---|---|---|
|
|
1154
|
-
| 1.1a | Eager load associations in loops | High | `\.includes\|\.eager_load\|\.preload` | `app/controllers/**/*.rb` |
|
|
1155
|
-
| 1.1b | Nested association eager loading | High | `\.includes\(.*:` | `app/controllers/**/*.rb` |
|
|
1156
|
-
| 1.1c | Counter cache for counts | Medium | `\.count\b` in loops | `app/views/**/*.erb` |
|
|
1157
|
-
| 1.2a | Select only needed columns | Medium | `\.map\(&:` after query | `app/models/**/*.rb` |
|
|
1158
|
-
| 1.2b | exists? instead of present? | Medium | `\.present?\|\.any?\|\.count > 0` | `app/**/*.rb` |
|
|
1159
|
-
| 1.2c | Avoid load-to-count | Medium | `\.length` on relation | `app/**/*.rb` |
|
|
1160
|
-
| 1.3a | find_each for large iterations | High | `\.all\.each\|\.where.*\.each` | `app/jobs/**/*.rb`, `lib/**/*.rb` |
|
|
1161
|
-
| 1.3b | find_in_batches for bulk ops | Medium | `\.to_a\.each_slice` | `app/jobs/**/*.rb`, `lib/**/*.rb` |
|
|
1162
|
-
| 1.4a | Sort/filter at database level | Medium | `\.sort_by\|\.select \{` on AR | `app/models/**/*.rb` |
|
|
1163
|
-
| 1.4b | Avoid Ruby compute on large sets | Medium | `\.all\.map\|\.all\.reduce` | `app/models/**/*.rb` |
|
|
1164
|
-
| 2.1a | Foreign key columns indexed | High | `t\.integer.*_id` without index | `db/migrate/**/*.rb` |
|
|
1165
|
-
| 2.1b | Frequently queried columns indexed | High | `add_column` without `add_index` | `db/migrate/**/*.rb` |
|
|
1166
|
-
| 2.1c | Composite indexes for multi-column | Medium | `\.where.*\.where\|\.where.*\.order` | `app/models/**/*.rb` |
|
|
1167
|
-
| 2.2a | Concurrent index in production | High | `add_index` without `concurrently` | `db/migrate/**/*.rb` |
|
|
1168
|
-
| 2.2b | Partial indexes for scoped queries | Low | `add_index.*where:` | `db/migrate/**/*.rb` |
|
|
1169
|
-
| 3.1a | Production cache store | High | `cache_store.*:file_store\|:memory_store` | `config/environments/production.rb` |
|
|
1170
|
-
| 3.2a | Cache expensive view partials | Medium | `render partial:` without `cache` | `app/views/**/*.erb` |
|
|
1171
|
-
| 3.2b | Collection caching | Medium | `render.*collection:` without `cached` | `app/views/**/*.erb` |
|
|
1172
|
-
| 3.3a | Cache expensive computations | Medium | `Rails\.cache\.fetch` | `app/models/**/*.rb`, `lib/**/*.rb` |
|
|
1173
|
-
| 3.3b | Memoization for per-request | Low | `@.*\|\|=` | `app/models/**/*.rb` |
|
|
1174
|
-
| 4.1a | Avoid full loads for partial data | High | `\.map\(&:` after `.where` | `app/models/**/*.rb`, `app/jobs/**/*.rb` |
|
|
1175
|
-
| 4.1b | Stream large file exports | High | `CSV\.generate.*\.all` | `app/controllers/**/*.rb` |
|
|
1176
|
-
| 4.2a | Expensive work in background | High | `deliver_now\|\.generate.*\.render` | `app/controllers/**/*.rb` |
|
|
1177
|
-
| 4.2b | deliver_later for emails | High | `deliver_now` | `app/controllers/**/*.rb` |
|
|
1178
|
-
| 4.3a | Freeze string literals | Low | `frozen_string_literal` | `app/**/*.rb` |
|
|
1179
|
-
| 4.3b | No repeated allocs in loops | Low | `Regexp\.new\|/.*/ inside \.each` | `app/**/*.rb`, `lib/**/*.rb` |
|
|
1180
|
-
| 5.1a | render collection vs loops | Medium | `\.each.*render partial:` | `app/views/**/*.erb` |
|
|
1181
|
-
| 5.1b | Efficient JSON serialization | Medium | `\.to_json` on AR objects | `app/controllers/**/*.rb` |
|
|
1182
|
-
| 5.2a | Pagination for large collections | High | `\.all` without `.page\|.limit` | `app/controllers/**/*.rb` |
|
|
1183
|
-
| 5.2b | Turbo Frame lazy loading | Low | `turbo_frame_tag.*loading: :lazy` | `app/views/**/*.erb` |
|
|
1184
|
-
| 6.1a | Puma threads/workers configured | High | `workers\|threads` | `config/puma.rb` |
|
|
1185
|
-
| 6.1b | YJIT enabled | Medium | `RUBY_YJIT_ENABLE\|yjit` | `Dockerfile`, `config/puma.rb` |
|
|
1186
|
-
| 6.2a | cache_classes and eager_load | High | `cache_classes.*false\|eager_load.*false` | `config/environments/production.rb` |
|
|
1187
|
-
| 6.2b | Asset compression and digests | Medium | `assets\.compile.*true\|assets\.digest.*false` | `config/environments/production.rb` |
|
|
1188
|
-
|
|
1189
|
-
---
|
|
1190
|
-
|
|
1191
|
-
**Document Version**: 1.0
|
|
1192
|
-
**Last Updated**: 2026-02-15
|
|
1193
|
-
**Maintainer**: Development Team
|