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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +251 -0
  3. data/README.md +18 -0
  4. data/docs/ADAPTERS.md +478 -0
  5. data/docs/ARCHITECTURE.md +222 -0
  6. data/docs/CONTENT.md +967 -0
  7. data/docs/PERFORMANCE.md +492 -0
  8. data/docs/PLUGIN_DEVELOPMENT.md +688 -0
  9. data/docs/USAGE.md +4998 -0
  10. data/docs/plugins/API_KEY_AUTH.md +232 -0
  11. data/docs/plugins/BASIC_AUTH.md +582 -0
  12. data/docs/plugins/BEARER_AUTH.md +394 -0
  13. data/docs/plugins/CACHE.md +369 -0
  14. data/docs/plugins/COMPRESSION.md +216 -0
  15. data/docs/plugins/COOKIES.md +30 -0
  16. data/docs/plugins/CORS.md +283 -0
  17. data/docs/plugins/CSRF.md +751 -0
  18. data/docs/plugins/ETAG.md +308 -0
  19. data/docs/plugins/FORM_PARSER.md +193 -0
  20. data/docs/plugins/HEALTH_CHECK.md +469 -0
  21. data/docs/plugins/JSON.md +291 -0
  22. data/docs/plugins/MULTIPART.md +427 -0
  23. data/docs/plugins/RATE_LIMITER.md +368 -0
  24. data/docs/plugins/REQUEST_ID.md +369 -0
  25. data/docs/plugins/REQUEST_LOGGER.md +151 -0
  26. data/docs/plugins/SECURITY.md +193 -0
  27. data/docs/plugins/SESSION.md +98 -0
  28. data/lib/aris/adapters/rack/adapter.rb +17 -2
  29. data/lib/aris/adapters/rack/request.rb +29 -11
  30. data/lib/aris/plugins/basic_auth.rb +3 -1
  31. data/lib/aris/plugins/cookies.rb +4 -32
  32. data/lib/aris/plugins/cors.rb +8 -1
  33. data/lib/aris/plugins/csrf.rb +63 -22
  34. data/lib/aris/plugins/flash.rb +3 -1
  35. data/lib/aris/plugins/form_parser.rb +52 -31
  36. data/lib/aris/plugins/multipart.rb +22 -2
  37. data/lib/aris/plugins/request_logger.rb +8 -1
  38. data/lib/aris/plugins/security_headers.rb +8 -1
  39. data/lib/aris/plugins/session.rb +150 -99
  40. data/lib/aris/response_helpers.rb +41 -0
  41. data/lib/aris/version.rb +2 -2
  42. metadata +31 -3
@@ -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.