aris 1.4.2 → 1.5.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/CHANGELOG.md +251 -0
- data/README.md +18 -0
- data/docs/ADAPTERS.md +478 -0
- data/docs/ARCHITECTURE.md +222 -0
- data/docs/CONTENT.md +967 -0
- data/docs/PERFORMANCE.md +492 -0
- data/docs/PLUGIN_DEVELOPMENT.md +688 -0
- data/docs/USAGE.md +4998 -0
- data/docs/plugins/API_KEY_AUTH.md +232 -0
- data/docs/plugins/BASIC_AUTH.md +582 -0
- data/docs/plugins/BEARER_AUTH.md +394 -0
- data/docs/plugins/CACHE.md +369 -0
- data/docs/plugins/COMPRESSION.md +216 -0
- data/docs/plugins/COOKIES.md +30 -0
- data/docs/plugins/CORS.md +283 -0
- data/docs/plugins/CSRF.md +751 -0
- data/docs/plugins/ETAG.md +308 -0
- data/docs/plugins/FORM_PARSER.md +193 -0
- data/docs/plugins/HEALTH_CHECK.md +469 -0
- data/docs/plugins/JSON.md +291 -0
- data/docs/plugins/MULTIPART.md +427 -0
- data/docs/plugins/RATE_LIMITER.md +368 -0
- data/docs/plugins/REQUEST_ID.md +369 -0
- data/docs/plugins/REQUEST_LOGGER.md +151 -0
- data/docs/plugins/SECURITY.md +193 -0
- data/docs/plugins/SESSION.md +98 -0
- data/lib/aris/adapters/rack/adapter.rb +17 -2
- data/lib/aris/adapters/rack/request.rb +29 -11
- data/lib/aris/plugins/basic_auth.rb +3 -1
- data/lib/aris/plugins/cookies.rb +4 -32
- data/lib/aris/plugins/cors.rb +8 -1
- data/lib/aris/plugins/csrf.rb +63 -22
- data/lib/aris/plugins/flash.rb +3 -1
- data/lib/aris/plugins/form_parser.rb +52 -31
- data/lib/aris/plugins/multipart.rb +22 -2
- data/lib/aris/plugins/request_logger.rb +8 -1
- data/lib/aris/plugins/security_headers.rb +8 -1
- data/lib/aris/plugins/session.rb +150 -99
- data/lib/aris/response_helpers.rb +41 -0
- data/lib/aris/version.rb +2 -2
- metadata +31 -3
data/docs/PERFORMANCE.md
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
# Performance Guide
|
|
2
|
+
|
|
3
|
+
Aris was built for speed. This guide explains why it's fast, shows benchmark data, and teaches you how to optimize routing in your applications.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Quick Summary
|
|
8
|
+
|
|
9
|
+
- **2.5-3.8× faster than Roda** across all routing scenarios
|
|
10
|
+
- **Sub-microsecond routing** for most requests (500-1,000 nanoseconds)
|
|
11
|
+
- **O(k) lookup complexity** where k = path depth, not route count
|
|
12
|
+
- **Zero allocation** in the hot path after compilation
|
|
13
|
+
- **Thread-safe** for concurrent request processing
|
|
14
|
+
|
|
15
|
+
Adding 1,000 routes has zero impact on routing speed—only compilation time increases (3ms).
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Benchmark Data
|
|
20
|
+
|
|
21
|
+
All benchmarks run on Ruby 3.3.5 on Apple M1. Your numbers will vary, but relative performance should be similar.
|
|
22
|
+
|
|
23
|
+
### vs Roda (Head-to-Head)
|
|
24
|
+
|
|
25
|
+
These benchmarks compare Aris's pure routing against Roda's full request cycle (Rack parsing, middleware, everything). Even with that disadvantage, Aris wins decisively.
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
Benchmark: Root path (/)
|
|
29
|
+
Aris: 1,754,152 req/s (570 ns/req)
|
|
30
|
+
Roda: 571,119 req/s (1.75 μs/req)
|
|
31
|
+
Result: 3.07× faster
|
|
32
|
+
|
|
33
|
+
Benchmark: Simple literal (/users)
|
|
34
|
+
Aris: 1,323,223 req/s (756 ns/req)
|
|
35
|
+
Roda: 349,001 req/s (2.87 μs/req)
|
|
36
|
+
Result: 3.79× faster
|
|
37
|
+
|
|
38
|
+
Benchmark: Single parameter (/users/:id)
|
|
39
|
+
Aris: 1,002,314 req/s (998 ns/req)
|
|
40
|
+
Roda: 360,031 req/s (2.78 μs/req)
|
|
41
|
+
Result: 2.78× faster
|
|
42
|
+
|
|
43
|
+
Benchmark: Two parameters (/users/:user_id/posts/:post_id)
|
|
44
|
+
Aris: 764,682 req/s (1.31 μs/req)
|
|
45
|
+
Roda: 257,198 req/s (3.89 μs/req)
|
|
46
|
+
Result: 2.97× faster
|
|
47
|
+
|
|
48
|
+
Benchmark: Three parameters (/users/:user_id/posts/:post_id/comments/:comment_id)
|
|
49
|
+
Aris: 624,800 req/s (1.60 μs/req)
|
|
50
|
+
Roda: 250,989 req/s (3.98 μs/req)
|
|
51
|
+
Result: 2.49× faster
|
|
52
|
+
|
|
53
|
+
Benchmark: 404 - No route match
|
|
54
|
+
Aris: 1,654,160 req/s (605 ns/req)
|
|
55
|
+
Roda: 573,374 req/s (1.74 μs/req)
|
|
56
|
+
Result: 2.88× faster
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Important note**: These compare different things. Roda's numbers include the full HTTP processing stack. If you added HTTP parsing overhead to Aris, the gap would narrow. But pure routing-to-routing, Aris is substantially faster.
|
|
60
|
+
|
|
61
|
+
### Detailed Performance Profile
|
|
62
|
+
|
|
63
|
+
These benchmarks isolate different aspects of Aris's performance.
|
|
64
|
+
|
|
65
|
+
**Throughput by route type:**
|
|
66
|
+
```
|
|
67
|
+
Root path: 1,823,231 req/s (548 ns/req)
|
|
68
|
+
Literal match: 1,316,645 req/s (760 ns/req)
|
|
69
|
+
Single parameter: 984,276 req/s (1.02 μs/req)
|
|
70
|
+
Two parameters: 759,900 req/s (1.32 μs/req)
|
|
71
|
+
Three parameters: 625,294 req/s (1.60 μs/req)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Priority resolution (same path, different types):**
|
|
75
|
+
```
|
|
76
|
+
Literal segment: 1,015,040 req/s (985 ns/req)
|
|
77
|
+
Parameter segment: 987,754 req/s (1.01 μs/req)
|
|
78
|
+
Wildcard segment: 475,257 req/s (2.10 μs/req)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Wildcards are slower because they have to try multiple capture lengths. Still fast, but noticeably slower than exact matches.
|
|
82
|
+
|
|
83
|
+
**Domain resolution:**
|
|
84
|
+
```
|
|
85
|
+
Exact domain: 1,320,740 req/s (757 ns/req)
|
|
86
|
+
Wildcard domain: 1,142,909 req/s (875 ns/req)
|
|
87
|
+
Fallback to "*": 995,791 req/s (1.00 μs/req)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Domain lookup is fast regardless of type. The fallback is slightly slower because it checks the specific domain first.
|
|
91
|
+
|
|
92
|
+
**Compilation time (route count → compilation duration):**
|
|
93
|
+
```
|
|
94
|
+
10 routes: 0.12 ms
|
|
95
|
+
100 routes: 0.24 ms
|
|
96
|
+
1,000 routes: 3.14 ms
|
|
97
|
+
5,000 routes: 15.00 ms
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Compilation is linear with route count and happens once at boot. Even 5,000 routes compile in 15ms.
|
|
101
|
+
|
|
102
|
+
**Memory allocation per match:**
|
|
103
|
+
```
|
|
104
|
+
Literal match: 960 bytes (18 objects)
|
|
105
|
+
Parameterized match: 1,160 bytes (23 objects)
|
|
106
|
+
Path helper: 1,520 bytes (26 objects)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
These allocations are unavoidable—you need to build the params hash and return result objects. But they're minimal.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Why It's Fast
|
|
114
|
+
|
|
115
|
+
### 1. Compilation, Not Evaluation
|
|
116
|
+
|
|
117
|
+
Most routers execute code on every request. They evaluate blocks, call methods, check conditions. Aris compiles routes into a Trie structure at boot time. Matching is pure data structure traversal—no method calls, no block evaluation, no conditionals.
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
# Other routers (conceptual):
|
|
121
|
+
def match(path)
|
|
122
|
+
routes.each do |route|
|
|
123
|
+
return route.handler if route.pattern.match?(path) # Code executes
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Aris (simplified from actual implementation):
|
|
128
|
+
def match(domain, method, path)
|
|
129
|
+
segments = path.split('/').reject(&:empty?)
|
|
130
|
+
node = @tries[domain]
|
|
131
|
+
|
|
132
|
+
segments.each do |segment|
|
|
133
|
+
# Try literal match first (fastest)
|
|
134
|
+
if node[:literal_children][segment]
|
|
135
|
+
node = node[:literal_children][segment]
|
|
136
|
+
# Fall back to parameter match
|
|
137
|
+
elsif node[:param_child]
|
|
138
|
+
node = node[:param_child][:node]
|
|
139
|
+
else
|
|
140
|
+
return nil # No match
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
node[:handlers][method] # Return handler metadata
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 2. O(k) Lookup Complexity
|
|
149
|
+
|
|
150
|
+
Route matching is O(k) where k is the path depth, not the route count. Matching `/users/123/posts/456` performs exactly 4 lookups regardless of whether you have 10 routes or 10,000 routes.
|
|
151
|
+
|
|
152
|
+
This is why compilation time increases with route count but matching time doesn't. The Trie grows larger, but lookups remain constant depth.
|
|
153
|
+
|
|
154
|
+
### 3. Structural Sharing
|
|
155
|
+
|
|
156
|
+
Routes with common prefixes share Trie nodes. If you define:
|
|
157
|
+
- `/users/123/posts`
|
|
158
|
+
- `/users/123/comments`
|
|
159
|
+
- `/users/456/posts`
|
|
160
|
+
|
|
161
|
+
The Trie only stores `/users/:id` once, then branches at the next segment. Memory usage scales with unique path segments, not total route count.
|
|
162
|
+
|
|
163
|
+
### 4. Zero Allocation in Hot Path
|
|
164
|
+
|
|
165
|
+
After compilation, the Trie is immutable. Matching a route doesn't allocate new objects for the Trie structure itself—it just walks existing nodes. The only allocations are for the result hash and params extraction, which you need anyway.
|
|
166
|
+
|
|
167
|
+
Aris also caches normalized path segments up to a configurable limit (default 1,000), avoiding repeated string operations.
|
|
168
|
+
|
|
169
|
+
### 5. No Regular Expression Matching in Routing
|
|
170
|
+
|
|
171
|
+
Route patterns use simple string comparisons for literal segments and single captures for parameters. Constraints use regex, but they're optional and only run after structural matching succeeds.
|
|
172
|
+
|
|
173
|
+
This means common case routing (literal paths and simple parameters) involves zero regex operations.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Profiling Your Application
|
|
178
|
+
|
|
179
|
+
The included profiler helps identify bottlenecks in your routing configuration.
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
# benchmark/profiler.rb
|
|
183
|
+
require 'aris'
|
|
184
|
+
require_relative '../test/profiler' # Use the profiler from tests
|
|
185
|
+
|
|
186
|
+
Profiler.new.run_all
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Output shows timing for compilation, matching, path generation, and memory usage:
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
PROFILING COMPILATION
|
|
193
|
+
tiny: 0.000s (10 routes)
|
|
194
|
+
small: 0.001s (100 routes)
|
|
195
|
+
medium: 0.002s (1,000 routes)
|
|
196
|
+
large: 0.015s (5,000 routes)
|
|
197
|
+
|
|
198
|
+
PROFILING MATCHING
|
|
199
|
+
root: 0.001s (1,000 iterations)
|
|
200
|
+
literal: 0.001s
|
|
201
|
+
param_1: 0.001s
|
|
202
|
+
param_2: 0.002s
|
|
203
|
+
param_3: 0.002s
|
|
204
|
+
wildcard: 0.007s
|
|
205
|
+
deep_nest: 0.002s
|
|
206
|
+
miss_path: 0.001s
|
|
207
|
+
miss_method: 0.001s
|
|
208
|
+
miss_domain: 0.000s
|
|
209
|
+
|
|
210
|
+
PROFILING PATH GENERATION
|
|
211
|
+
simple: 0.001s (1,000 iterations)
|
|
212
|
+
param_1: 0.002s
|
|
213
|
+
param_2: 0.002s
|
|
214
|
+
query: 0.003s
|
|
215
|
+
encoded: 0.003s
|
|
216
|
+
|
|
217
|
+
PROFILING MEMORY
|
|
218
|
+
memory growth: -128KB (after 1,000 matches + 1,000 path generations)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
If you see any operation taking >0.1s for 1,000 iterations, investigate. Normal operations should be <0.01s per 1,000 iterations.
|
|
222
|
+
|
|
223
|
+
### Custom Profiling
|
|
224
|
+
|
|
225
|
+
Add your own routes to the profiler to test realistic scenarios:
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
# In benchmark/profiler.rb
|
|
229
|
+
def my_app_config
|
|
230
|
+
{
|
|
231
|
+
"myapp.com": {
|
|
232
|
+
# Your actual routes
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def profile_my_app
|
|
238
|
+
Aris::Router.define(my_app_config)
|
|
239
|
+
|
|
240
|
+
# Test your most common paths
|
|
241
|
+
paths = [
|
|
242
|
+
"/users/123",
|
|
243
|
+
"/api/v1/projects/456",
|
|
244
|
+
"/dashboard"
|
|
245
|
+
]
|
|
246
|
+
|
|
247
|
+
time = Benchmark.measure do
|
|
248
|
+
10_000.times do
|
|
249
|
+
paths.each do |path|
|
|
250
|
+
Aris::Router.match(domain: "myapp.com", method: :get, path: path)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end.real
|
|
254
|
+
|
|
255
|
+
puts "30,000 matches: #{time}s"
|
|
256
|
+
puts "Per match: #{(time / 30_000 * 1_000_000).round(2)}μs"
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
profile_my_app
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Optimization Techniques
|
|
265
|
+
|
|
266
|
+
### 1. Minimize Wildcard Routes
|
|
267
|
+
|
|
268
|
+
Wildcards are 2× slower than exact matches because they try multiple capture lengths. Use them when you need them, but prefer exact segments when possible.
|
|
269
|
+
|
|
270
|
+
```ruby
|
|
271
|
+
# Slower
|
|
272
|
+
"/files/*path": { get: { to: FileHandler } }
|
|
273
|
+
|
|
274
|
+
# Faster (if you know the structure)
|
|
275
|
+
"/files/:year/:month/:day/:filename": { get: { to: FileHandler } }
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### 2. Keep Route Depth Shallow
|
|
279
|
+
|
|
280
|
+
Route matching is O(k) where k is depth. Prefer flat structures over deep nesting when performance matters.
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
# Slower (depth = 6)
|
|
284
|
+
"/api/v1/organizations/:org_id/teams/:team_id/members/:member_id"
|
|
285
|
+
|
|
286
|
+
# Faster (depth = 4)
|
|
287
|
+
"/api/v1/members/:member_id" # Look up org/team from member
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
This trades routing performance for a database lookup. Usually worth it, but measure your specific case.
|
|
291
|
+
|
|
292
|
+
### 3. Use Constraints Sparingly
|
|
293
|
+
|
|
294
|
+
Constraints run regex matches after structural routing succeeds. They're fast, but not as fast as no regex.
|
|
295
|
+
|
|
296
|
+
```ruby
|
|
297
|
+
# Adds regex overhead
|
|
298
|
+
constraints: { id: /\A\d{1,8}\z/ }
|
|
299
|
+
|
|
300
|
+
# No regex overhead
|
|
301
|
+
# (validate in handler instead)
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Only use constraints when you need to fail at routing time. If validation can happen in the handler, do it there.
|
|
305
|
+
|
|
306
|
+
### 4. Cache Path Generation
|
|
307
|
+
|
|
308
|
+
Path generation is fast (~1μs), but if you're generating the same path thousands of times per second, cache it.
|
|
309
|
+
|
|
310
|
+
```ruby
|
|
311
|
+
# In a hot loop
|
|
312
|
+
users.each do |user|
|
|
313
|
+
url = Aris.url(:user, id: user.id) # Regenerates every time
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Cached
|
|
317
|
+
@user_url_template = "/users/%d"
|
|
318
|
+
users.each do |user|
|
|
319
|
+
url = @user_url_template % user.id
|
|
320
|
+
end
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
This is micro-optimization. Only do it if profiling shows path generation is a bottleneck.
|
|
324
|
+
|
|
325
|
+
### 5. Use Literal Routes Over Parameters When Possible
|
|
326
|
+
|
|
327
|
+
Literal segments are slightly faster than parameters because they use hash lookup instead of capture.
|
|
328
|
+
|
|
329
|
+
```ruby
|
|
330
|
+
# If you only have a few known values:
|
|
331
|
+
"/users/admin": { get: { to: AdminHandler } }
|
|
332
|
+
"/users/moderator": { get: { to: ModeratorHandler } }
|
|
333
|
+
"/users/:role": { get: { to: RoleHandler } }
|
|
334
|
+
|
|
335
|
+
# Better performance for the common cases
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Again, this is micro-optimization. The difference is 50-100 nanoseconds.
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## When Performance Matters
|
|
343
|
+
|
|
344
|
+
**It usually doesn't.** Routing is rarely your bottleneck. Database queries, external API calls, and business logic are almost always slower than routing.
|
|
345
|
+
|
|
346
|
+
Consider these numbers:
|
|
347
|
+
- Routing: 1μs (0.001ms)
|
|
348
|
+
- Database query: 10ms (10,000× slower)
|
|
349
|
+
- External API call: 100ms (100,000× slower)
|
|
350
|
+
- Complex computation: 50ms (50,000× slower)
|
|
351
|
+
|
|
352
|
+
If your response time is 50ms, routing consumes 0.002% of it. Optimizing routing from 1μs to 0.5μs saves 0.5μs out of 50,000μs. Not worth your time.
|
|
353
|
+
|
|
354
|
+
**When it does matter:**
|
|
355
|
+
|
|
356
|
+
1. **Very high request rates** - If you're handling 100,000+ requests per second, routing overhead adds up. At 100K req/s, 1μs routing = 100ms of CPU time per second, or 10% of a core.
|
|
357
|
+
|
|
358
|
+
2. **Extremely simple handlers** - If your handlers are trivial (return cached data, proxy to another service), routing becomes a larger percentage of total time.
|
|
359
|
+
|
|
360
|
+
3. **Microservices doing pure routing** - If you have a router service that does nothing but route requests to other services, routing performance is your only job.
|
|
361
|
+
|
|
362
|
+
4. **Real-time systems** - If you need p99 latency under 5ms, every microsecond counts.
|
|
363
|
+
|
|
364
|
+
For most applications, focus on handler performance. Make database queries faster, cache aggressively, optimize algorithms. Routing will take care of itself.
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## Concurrent Performance
|
|
369
|
+
|
|
370
|
+
Routing is completely thread-safe after compilation. Multiple threads can match routes concurrently with no locks, no synchronization overhead, no contention.
|
|
371
|
+
|
|
372
|
+
This makes Aris ideal for multi-threaded servers like Puma:
|
|
373
|
+
|
|
374
|
+
```ruby
|
|
375
|
+
# config/puma.rb
|
|
376
|
+
workers 4 # Fork 4 processes
|
|
377
|
+
threads 5, 5 # 5 threads per worker = 20 concurrent requests
|
|
378
|
+
|
|
379
|
+
# Each thread routes independently
|
|
380
|
+
# No locking, no waiting, no contention
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Route redefinition (calling `Aris.routes`) is not thread-safe and should only happen at boot or during controlled maintenance windows.
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## Production Monitoring
|
|
388
|
+
|
|
389
|
+
Monitor these metrics in production to catch routing-related performance issues:
|
|
390
|
+
|
|
391
|
+
**Request rate distribution by route:**
|
|
392
|
+
```ruby
|
|
393
|
+
class MetricsPlugin
|
|
394
|
+
def self.call(request, response)
|
|
395
|
+
route_name = Thread.current[:aris_matched_route_name]
|
|
396
|
+
Metrics.increment("requests.route.#{route_name}")
|
|
397
|
+
nil
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
If one route suddenly gets 10× more traffic, you'll see it here before it becomes a problem.
|
|
403
|
+
|
|
404
|
+
**p50/p95/p99 latency:**
|
|
405
|
+
```ruby
|
|
406
|
+
class LatencyPlugin
|
|
407
|
+
def self.call(request, response)
|
|
408
|
+
start = Time.now
|
|
409
|
+
response.headers['X-Start-Time'] = start.to_f.to_s
|
|
410
|
+
nil
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
class LatencyReporter
|
|
415
|
+
def self.call(request, response)
|
|
416
|
+
start = response.headers.delete('X-Start-Time')&.to_f
|
|
417
|
+
return nil unless start
|
|
418
|
+
|
|
419
|
+
duration = Time.now.to_f - start
|
|
420
|
+
route_name = Thread.current[:aris_matched_route_name]
|
|
421
|
+
|
|
422
|
+
Metrics.histogram("latency.route.#{route_name}", duration)
|
|
423
|
+
nil
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Watch for routes where p99 latency is much higher than p50. Those routes have occasional slowness that needs investigation.
|
|
429
|
+
|
|
430
|
+
**Memory growth:**
|
|
431
|
+
```ruby
|
|
432
|
+
# In a monitoring process
|
|
433
|
+
def check_memory
|
|
434
|
+
before = memory_usage
|
|
435
|
+
sleep 60
|
|
436
|
+
after = memory_usage
|
|
437
|
+
|
|
438
|
+
growth = after - before
|
|
439
|
+
alert if growth > threshold
|
|
440
|
+
end
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
Routing itself shouldn't cause memory growth. If you see it growing, you likely have a leak in handlers or plugins, not in routing.
|
|
444
|
+
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
## Benchmarking Your Own App
|
|
448
|
+
|
|
449
|
+
Run benchmarks against your actual routes with your actual traffic patterns.
|
|
450
|
+
|
|
451
|
+
```ruby
|
|
452
|
+
# benchmark/my_app.rb
|
|
453
|
+
require 'benchmark/ips'
|
|
454
|
+
require 'aris'
|
|
455
|
+
|
|
456
|
+
# Load your routes
|
|
457
|
+
require_relative '../config/routes'
|
|
458
|
+
|
|
459
|
+
# Benchmark your most common paths
|
|
460
|
+
Benchmark.ips do |x|
|
|
461
|
+
x.config(time: 5, warmup: 2)
|
|
462
|
+
|
|
463
|
+
x.report("dashboard") do
|
|
464
|
+
Aris::Router.match(domain: "app.myapp.com", method: :get, path: "/dashboard")
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
x.report("user profile") do
|
|
468
|
+
Aris::Router.match(domain: "app.myapp.com", method: :get, path: "/users/12345")
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
x.report("api endpoint") do
|
|
472
|
+
Aris::Router.match(domain: "api.myapp.com", method: :get, path: "/v1/projects/67890")
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
x.compare!
|
|
476
|
+
end
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
Run this before and after making routing changes to ensure you haven't regressed performance.
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## The Bottom Line
|
|
484
|
+
|
|
485
|
+
Aris is fast enough that routing performance should never be your bottleneck. Focus on:
|
|
486
|
+
|
|
487
|
+
1. **Handler performance** - Database queries, API calls, business logic
|
|
488
|
+
2. **Caching** - Don't compute what you can cache
|
|
489
|
+
3. **Database optimization** - Indexes, query optimization, connection pooling
|
|
490
|
+
4. **Algorithmic improvements** - O(n²) → O(n log n) matters more than routing speed
|
|
491
|
+
|
|
492
|
+
Routing is solved so you can tackle your actual problems.
|