e11y 0.1.0 → 0.2.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/.rspec +1 -0
- data/.rubocop.yml +20 -0
- data/CHANGELOG.md +151 -13
- data/README.md +1138 -104
- data/RELEASE.md +254 -0
- data/Rakefile +377 -0
- data/benchmarks/OPTIMIZATION.md +246 -0
- data/benchmarks/README.md +103 -0
- data/benchmarks/allocation_profiling.rb +253 -0
- data/benchmarks/e11y_benchmarks.rb +447 -0
- data/benchmarks/ruby_baseline_allocations.rb +175 -0
- data/benchmarks/run_all.rb +9 -21
- data/docs/00-ICP-AND-TIMELINE.md +2 -2
- data/docs/ADR-001-architecture.md +1 -1
- data/docs/ADR-004-adapter-architecture.md +247 -0
- data/docs/ADR-009-cost-optimization.md +231 -115
- data/docs/ADR-017-multi-rails-compatibility.md +103 -0
- data/docs/ADR-INDEX.md +99 -0
- data/docs/CONTRIBUTING.md +312 -0
- data/docs/IMPLEMENTATION_PLAN.md +1 -1
- data/docs/QUICK-START.md +0 -6
- data/docs/use_cases/UC-019-retention-based-routing.md +584 -0
- data/e11y.gemspec +28 -17
- data/lib/e11y/adapters/adaptive_batcher.rb +3 -0
- data/lib/e11y/adapters/audit_encrypted.rb +10 -4
- data/lib/e11y/adapters/base.rb +15 -0
- data/lib/e11y/adapters/file.rb +4 -1
- data/lib/e11y/adapters/in_memory.rb +6 -0
- data/lib/e11y/adapters/loki.rb +9 -0
- data/lib/e11y/adapters/otel_logs.rb +11 -9
- data/lib/e11y/adapters/sentry.rb +9 -0
- data/lib/e11y/adapters/yabeda.rb +54 -10
- data/lib/e11y/buffers.rb +8 -8
- data/lib/e11y/console.rb +52 -60
- data/lib/e11y/event/base.rb +75 -10
- data/lib/e11y/event/value_sampling_config.rb +10 -4
- data/lib/e11y/events/rails/http/request.rb +1 -1
- data/lib/e11y/instruments/active_job.rb +6 -3
- data/lib/e11y/instruments/rails_instrumentation.rb +51 -28
- data/lib/e11y/instruments/sidekiq.rb +7 -7
- data/lib/e11y/logger/bridge.rb +24 -54
- data/lib/e11y/metrics/cardinality_protection.rb +257 -12
- data/lib/e11y/metrics/cardinality_tracker.rb +17 -0
- data/lib/e11y/metrics/registry.rb +6 -2
- data/lib/e11y/metrics/relabeling.rb +0 -56
- data/lib/e11y/metrics.rb +6 -1
- data/lib/e11y/middleware/audit_signing.rb +12 -9
- data/lib/e11y/middleware/pii_filter.rb +18 -10
- data/lib/e11y/middleware/request.rb +10 -4
- data/lib/e11y/middleware/routing.rb +117 -90
- data/lib/e11y/middleware/sampling.rb +47 -28
- data/lib/e11y/middleware/trace_context.rb +40 -11
- data/lib/e11y/middleware/validation.rb +20 -2
- data/lib/e11y/middleware/versioning.rb +1 -1
- data/lib/e11y/pii.rb +7 -7
- data/lib/e11y/railtie.rb +24 -20
- data/lib/e11y/reliability/circuit_breaker.rb +3 -0
- data/lib/e11y/reliability/dlq/file_storage.rb +16 -5
- data/lib/e11y/reliability/dlq/filter.rb +3 -0
- data/lib/e11y/reliability/retry_handler.rb +4 -0
- data/lib/e11y/sampling/error_spike_detector.rb +16 -5
- data/lib/e11y/sampling/load_monitor.rb +13 -4
- data/lib/e11y/self_monitoring/reliability_monitor.rb +3 -0
- data/lib/e11y/version.rb +1 -1
- data/lib/e11y.rb +86 -9
- metadata +83 -38
- data/docs/use_cases/UC-019-tiered-storage-migration.md +0 -562
- data/lib/e11y/middleware/pii_filtering.rb +0 -280
- data/lib/e11y/middleware/slo.rb +0 -168
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# E11y Performance Benchmark Suite
|
|
5
|
+
#
|
|
6
|
+
# Tests performance at 3 scale levels:
|
|
7
|
+
# - Small: 1K events/sec
|
|
8
|
+
# - Medium: 10K events/sec
|
|
9
|
+
# - Large: 100K events/sec
|
|
10
|
+
#
|
|
11
|
+
# Run:
|
|
12
|
+
# bundle exec ruby benchmarks/e11y_benchmarks.rb
|
|
13
|
+
#
|
|
14
|
+
# Run specific scale:
|
|
15
|
+
# SCALE=small bundle exec ruby benchmarks/e11y_benchmarks.rb
|
|
16
|
+
#
|
|
17
|
+
# ADR-001 §5: Performance Requirements
|
|
18
|
+
|
|
19
|
+
require "bundler/setup"
|
|
20
|
+
require "benchmark"
|
|
21
|
+
require "benchmark/ips"
|
|
22
|
+
require "memory_profiler"
|
|
23
|
+
require "e11y"
|
|
24
|
+
|
|
25
|
+
# ============================================================================
|
|
26
|
+
# Configuration
|
|
27
|
+
# ============================================================================
|
|
28
|
+
|
|
29
|
+
SCALE = (ENV["SCALE"] || "all").downcase
|
|
30
|
+
WARMUP_TIME = 2 # seconds
|
|
31
|
+
BENCHMARK_TIME = 5 # seconds
|
|
32
|
+
|
|
33
|
+
# Performance targets
|
|
34
|
+
TARGETS = {
|
|
35
|
+
small: {
|
|
36
|
+
name: "Small Scale (1K events/sec)",
|
|
37
|
+
track_latency_p99_us: 50, # <50μs p99
|
|
38
|
+
buffer_throughput: 10_000, # 10K events/sec
|
|
39
|
+
memory_mb: 100, # <100MB
|
|
40
|
+
cpu_percent: 5 # <5%
|
|
41
|
+
},
|
|
42
|
+
medium: {
|
|
43
|
+
name: "Medium Scale (10K events/sec)",
|
|
44
|
+
track_latency_p99_us: 1000, # <1ms p99
|
|
45
|
+
buffer_throughput: 50_000, # 50K events/sec
|
|
46
|
+
memory_mb: 500, # <500MB
|
|
47
|
+
cpu_percent: 10 # <10%
|
|
48
|
+
},
|
|
49
|
+
large: {
|
|
50
|
+
name: "Large Scale (100K events/sec)",
|
|
51
|
+
track_latency_p99_us: 5000, # <5ms p99
|
|
52
|
+
buffer_throughput: 100_000, # 100K events/sec (per process)
|
|
53
|
+
memory_mb: 2000, # <2GB
|
|
54
|
+
cpu_percent: 15 # <15%
|
|
55
|
+
}
|
|
56
|
+
}.freeze
|
|
57
|
+
|
|
58
|
+
# ============================================================================
|
|
59
|
+
# Test Event Classes
|
|
60
|
+
# ============================================================================
|
|
61
|
+
|
|
62
|
+
# BenchmarkEvent is a test event class for benchmarking purposes.
|
|
63
|
+
# It contains user action tracking with user_id, action, and timestamp fields.
|
|
64
|
+
class BenchmarkEvent < E11y::Event::Base
|
|
65
|
+
schema do
|
|
66
|
+
required(:user_id).filled(:string)
|
|
67
|
+
required(:action).filled(:string)
|
|
68
|
+
required(:timestamp).filled(:time)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# SimpleBenchmarkEvent is a minimal test event class for benchmarking.
|
|
73
|
+
# It contains only a single integer value field for performance testing.
|
|
74
|
+
class SimpleBenchmarkEvent < E11y::Event::Base
|
|
75
|
+
schema do
|
|
76
|
+
required(:value).filled(:integer)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# ============================================================================
|
|
81
|
+
# Helper Methods
|
|
82
|
+
# ============================================================================
|
|
83
|
+
|
|
84
|
+
def setup_e11y(_buffer_size: 10_000)
|
|
85
|
+
E11y.configure do |config|
|
|
86
|
+
config.enabled = true
|
|
87
|
+
|
|
88
|
+
# Use InMemory adapter for clean benchmarks (no I/O overhead)
|
|
89
|
+
config.adapters = [
|
|
90
|
+
E11y::Adapters::InMemory.new
|
|
91
|
+
]
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
96
|
+
# Benchmark code prioritizes clarity and performance over complexity metrics
|
|
97
|
+
def measure_track_latency(event_class:, count:, _scale_name:)
|
|
98
|
+
latencies = []
|
|
99
|
+
|
|
100
|
+
count.times do
|
|
101
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
|
102
|
+
event_class.track(
|
|
103
|
+
user_id: "user_#{rand(1000)}",
|
|
104
|
+
action: "test_action",
|
|
105
|
+
timestamp: Time.now
|
|
106
|
+
)
|
|
107
|
+
finish = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
|
108
|
+
latencies << (finish - start)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
latencies.sort!
|
|
112
|
+
p50_index = (count * 0.5).to_i
|
|
113
|
+
p99_index = (count * 0.99).to_i
|
|
114
|
+
p999_index = (count * 0.999).to_i
|
|
115
|
+
|
|
116
|
+
{
|
|
117
|
+
p50: latencies[p50_index],
|
|
118
|
+
p99: latencies[p99_index],
|
|
119
|
+
p999: latencies[p999_index],
|
|
120
|
+
min: latencies.first,
|
|
121
|
+
max: latencies.last,
|
|
122
|
+
mean: (latencies.sum / count.to_f)
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
126
|
+
|
|
127
|
+
def measure_buffer_throughput(event_class:, duration_sec:)
|
|
128
|
+
count = 0
|
|
129
|
+
start_time = Time.now
|
|
130
|
+
|
|
131
|
+
while Time.now - start_time < duration_sec
|
|
132
|
+
event_class.track(value: count)
|
|
133
|
+
count += 1
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
actual_duration = Time.now - start_time
|
|
137
|
+
throughput = (count / actual_duration).round
|
|
138
|
+
|
|
139
|
+
{ count: count, duration: actual_duration, throughput: throughput }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def measure_memory_usage(event_count:)
|
|
143
|
+
GC.start # Clean slate
|
|
144
|
+
|
|
145
|
+
report = MemoryProfiler.report do
|
|
146
|
+
event_count.times do |i|
|
|
147
|
+
SimpleBenchmarkEvent.track(value: i)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
memory_mb = (report.total_allocated_memsize / 1024.0 / 1024.0).round(2)
|
|
152
|
+
memory_per_event_kb = ((report.total_allocated_memsize / event_count.to_f) / 1024.0).round(2)
|
|
153
|
+
|
|
154
|
+
{
|
|
155
|
+
total_mb: memory_mb,
|
|
156
|
+
per_event_kb: memory_per_event_kb,
|
|
157
|
+
total_allocated: report.total_allocated,
|
|
158
|
+
total_retained: report.total_retained
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def print_header(scale_name)
|
|
163
|
+
puts "\n"
|
|
164
|
+
puts "=" * 80
|
|
165
|
+
puts " #{TARGETS[scale_name][:name]}"
|
|
166
|
+
puts "=" * 80
|
|
167
|
+
puts ""
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def print_result(name, value, unit, target, passed)
|
|
171
|
+
status = passed ? "✅ PASS" : "❌ FAIL"
|
|
172
|
+
target_str = target ? "(target: #{target}#{unit})" : ""
|
|
173
|
+
puts " #{name.ljust(30)} #{value}#{unit} #{target_str} #{status}"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# rubocop:disable Metrics/AbcSize
|
|
177
|
+
# Benchmark code prioritizes clarity over complexity metrics
|
|
178
|
+
def print_summary(results)
|
|
179
|
+
puts "\n"
|
|
180
|
+
puts "=" * 80
|
|
181
|
+
puts " SUMMARY"
|
|
182
|
+
puts "=" * 80
|
|
183
|
+
|
|
184
|
+
results.each do |scale, data|
|
|
185
|
+
puts "\n#{TARGETS[scale][:name]}:"
|
|
186
|
+
puts " Total checks: #{data[:total]}"
|
|
187
|
+
puts " Passed: #{data[:passed]} ✅"
|
|
188
|
+
puts " Failed: #{data[:failed]} ❌"
|
|
189
|
+
puts " Status: #{data[:passed] == data[:total] ? '✅ ALL PASS' : '❌ SOME FAILED'}"
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
# rubocop:enable Metrics/AbcSize
|
|
193
|
+
|
|
194
|
+
# ============================================================================
|
|
195
|
+
# Benchmark Suite
|
|
196
|
+
# ============================================================================
|
|
197
|
+
|
|
198
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
199
|
+
# Benchmark code prioritizes clarity and performance over complexity metrics
|
|
200
|
+
def run_small_scale_benchmark
|
|
201
|
+
scale = :small
|
|
202
|
+
print_header(scale)
|
|
203
|
+
|
|
204
|
+
setup_e11y(buffer_size: 1000)
|
|
205
|
+
|
|
206
|
+
results = { total: 0, passed: 0, failed: 0 }
|
|
207
|
+
|
|
208
|
+
# 1. track() Latency
|
|
209
|
+
puts "📊 Benchmark: track() Latency (1000 iterations)"
|
|
210
|
+
latency = measure_track_latency(
|
|
211
|
+
event_class: BenchmarkEvent,
|
|
212
|
+
count: 1000,
|
|
213
|
+
scale_name: scale
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
target_p99 = TARGETS[scale][:track_latency_p99_us]
|
|
217
|
+
passed_p99 = latency[:p99] <= target_p99
|
|
218
|
+
|
|
219
|
+
puts " p50: #{latency[:p50].round(2)}μs"
|
|
220
|
+
puts " p99: #{latency[:p99].round(2)}μs (target: <#{target_p99}μs) #{passed_p99 ? '✅' : '❌'}"
|
|
221
|
+
puts " p999: #{latency[:p999].round(2)}μs"
|
|
222
|
+
puts " mean: #{latency[:mean].round(2)}μs"
|
|
223
|
+
|
|
224
|
+
results[:total] += 1
|
|
225
|
+
passed_p99 ? results[:passed] += 1 : results[:failed] += 1
|
|
226
|
+
|
|
227
|
+
# 2. Buffer Throughput
|
|
228
|
+
puts "\n📊 Benchmark: Buffer Throughput (3 seconds)"
|
|
229
|
+
throughput = measure_buffer_throughput(
|
|
230
|
+
event_class: SimpleBenchmarkEvent,
|
|
231
|
+
duration_sec: 3
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
target_throughput = TARGETS[scale][:buffer_throughput]
|
|
235
|
+
passed_throughput = throughput[:throughput] >= target_throughput
|
|
236
|
+
|
|
237
|
+
print_result(
|
|
238
|
+
"Buffer Throughput",
|
|
239
|
+
throughput[:throughput],
|
|
240
|
+
" events/sec",
|
|
241
|
+
">#{target_throughput}",
|
|
242
|
+
passed_throughput
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
results[:total] += 1
|
|
246
|
+
passed_throughput ? results[:passed] += 1 : results[:failed] += 1
|
|
247
|
+
|
|
248
|
+
# 3. Memory Usage
|
|
249
|
+
puts "\n📊 Benchmark: Memory Usage (1K events)"
|
|
250
|
+
memory = measure_memory_usage(event_count: 1000)
|
|
251
|
+
|
|
252
|
+
target_memory = TARGETS[scale][:memory_mb]
|
|
253
|
+
passed_memory = memory[:total_mb] <= target_memory
|
|
254
|
+
|
|
255
|
+
print_result(
|
|
256
|
+
"Memory Usage (1K events)",
|
|
257
|
+
memory[:total_mb],
|
|
258
|
+
" MB",
|
|
259
|
+
"<#{target_memory}",
|
|
260
|
+
passed_memory
|
|
261
|
+
)
|
|
262
|
+
puts " Memory per event: #{memory[:per_event_kb]} KB"
|
|
263
|
+
|
|
264
|
+
results[:total] += 1
|
|
265
|
+
passed_memory ? results[:passed] += 1 : results[:failed] += 1
|
|
266
|
+
|
|
267
|
+
# 4. CPU Overhead (informational, no strict check)
|
|
268
|
+
puts "\n📊 Benchmark: CPU Overhead (informational)"
|
|
269
|
+
puts " Note: CPU measurement is approximate"
|
|
270
|
+
puts " Target: <#{TARGETS[scale][:cpu_percent]}%"
|
|
271
|
+
puts " (Manual profiling recommended for accurate CPU %)"
|
|
272
|
+
|
|
273
|
+
results
|
|
274
|
+
end
|
|
275
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
276
|
+
|
|
277
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
278
|
+
# Benchmark code prioritizes clarity and performance over complexity metrics
|
|
279
|
+
def run_medium_scale_benchmark
|
|
280
|
+
scale = :medium
|
|
281
|
+
print_header(scale)
|
|
282
|
+
|
|
283
|
+
setup_e11y(buffer_size: 10_000)
|
|
284
|
+
|
|
285
|
+
results = { total: 0, passed: 0, failed: 0 }
|
|
286
|
+
|
|
287
|
+
# 1. track() Latency
|
|
288
|
+
puts "📊 Benchmark: track() Latency (10K iterations)"
|
|
289
|
+
latency = measure_track_latency(
|
|
290
|
+
event_class: BenchmarkEvent,
|
|
291
|
+
count: 10_000,
|
|
292
|
+
scale_name: scale
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
target_p99 = TARGETS[scale][:track_latency_p99_us]
|
|
296
|
+
passed_p99 = latency[:p99] <= target_p99
|
|
297
|
+
|
|
298
|
+
puts " p50: #{latency[:p50].round(2)}μs"
|
|
299
|
+
puts " p99: #{latency[:p99].round(2)}μs (target: <#{target_p99}μs) #{passed_p99 ? '✅' : '❌'}"
|
|
300
|
+
puts " p999: #{latency[:p999].round(2)}μs"
|
|
301
|
+
|
|
302
|
+
results[:total] += 1
|
|
303
|
+
passed_p99 ? results[:passed] += 1 : results[:failed] += 1
|
|
304
|
+
|
|
305
|
+
# 2. Buffer Throughput
|
|
306
|
+
puts "\n📊 Benchmark: Buffer Throughput (5 seconds)"
|
|
307
|
+
throughput = measure_buffer_throughput(
|
|
308
|
+
event_class: SimpleBenchmarkEvent,
|
|
309
|
+
duration_sec: 5
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
target_throughput = TARGETS[scale][:buffer_throughput]
|
|
313
|
+
passed_throughput = throughput[:throughput] >= target_throughput
|
|
314
|
+
|
|
315
|
+
print_result(
|
|
316
|
+
"Buffer Throughput",
|
|
317
|
+
throughput[:throughput],
|
|
318
|
+
" events/sec",
|
|
319
|
+
">#{target_throughput}",
|
|
320
|
+
passed_throughput
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
results[:total] += 1
|
|
324
|
+
passed_throughput ? results[:passed] += 1 : results[:failed] += 1
|
|
325
|
+
|
|
326
|
+
# 3. Memory Usage
|
|
327
|
+
puts "\n📊 Benchmark: Memory Usage (10K events)"
|
|
328
|
+
memory = measure_memory_usage(event_count: 10_000)
|
|
329
|
+
|
|
330
|
+
target_memory = TARGETS[scale][:memory_mb]
|
|
331
|
+
passed_memory = memory[:total_mb] <= target_memory
|
|
332
|
+
|
|
333
|
+
print_result(
|
|
334
|
+
"Memory Usage (10K events)",
|
|
335
|
+
memory[:total_mb],
|
|
336
|
+
" MB",
|
|
337
|
+
"<#{target_memory}",
|
|
338
|
+
passed_memory
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
results[:total] += 1
|
|
342
|
+
passed_memory ? results[:passed] += 1 : results[:failed] += 1
|
|
343
|
+
|
|
344
|
+
results
|
|
345
|
+
end
|
|
346
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
347
|
+
|
|
348
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
349
|
+
# Benchmark code prioritizes clarity and performance over complexity metrics
|
|
350
|
+
def run_large_scale_benchmark
|
|
351
|
+
scale = :large
|
|
352
|
+
print_header(scale)
|
|
353
|
+
|
|
354
|
+
setup_e11y(buffer_size: 100_000)
|
|
355
|
+
|
|
356
|
+
results = { total: 0, passed: 0, failed: 0 }
|
|
357
|
+
|
|
358
|
+
# 1. track() Latency
|
|
359
|
+
puts "📊 Benchmark: track() Latency (100K iterations)"
|
|
360
|
+
latency = measure_track_latency(
|
|
361
|
+
event_class: BenchmarkEvent,
|
|
362
|
+
count: 100_000,
|
|
363
|
+
scale_name: scale
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
target_p99 = TARGETS[scale][:track_latency_p99_us]
|
|
367
|
+
passed_p99 = latency[:p99] <= target_p99
|
|
368
|
+
|
|
369
|
+
puts " p50: #{latency[:p50].round(2)}μs"
|
|
370
|
+
puts " p99: #{latency[:p99].round(2)}μs (target: <#{target_p99}μs) #{passed_p99 ? '✅' : '❌'}"
|
|
371
|
+
puts " p999: #{latency[:p999].round(2)}μs"
|
|
372
|
+
|
|
373
|
+
results[:total] += 1
|
|
374
|
+
passed_p99 ? results[:passed] += 1 : results[:failed] += 1
|
|
375
|
+
|
|
376
|
+
# 2. Buffer Throughput
|
|
377
|
+
puts "\n📊 Benchmark: Buffer Throughput (10 seconds)"
|
|
378
|
+
throughput = measure_buffer_throughput(
|
|
379
|
+
event_class: SimpleBenchmarkEvent,
|
|
380
|
+
duration_sec: 10
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
target_throughput = TARGETS[scale][:buffer_throughput]
|
|
384
|
+
passed_throughput = throughput[:throughput] >= target_throughput
|
|
385
|
+
|
|
386
|
+
print_result(
|
|
387
|
+
"Buffer Throughput",
|
|
388
|
+
throughput[:throughput],
|
|
389
|
+
" events/sec",
|
|
390
|
+
">#{target_throughput}",
|
|
391
|
+
passed_throughput
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
results[:total] += 1
|
|
395
|
+
passed_throughput ? results[:passed] += 1 : results[:failed] += 1
|
|
396
|
+
|
|
397
|
+
# 3. Memory Usage
|
|
398
|
+
puts "\n📊 Benchmark: Memory Usage (100K events)"
|
|
399
|
+
memory = measure_memory_usage(event_count: 100_000)
|
|
400
|
+
|
|
401
|
+
target_memory = TARGETS[scale][:memory_mb]
|
|
402
|
+
passed_memory = memory[:total_mb] <= target_memory
|
|
403
|
+
|
|
404
|
+
print_result(
|
|
405
|
+
"Memory Usage (100K events)",
|
|
406
|
+
memory[:total_mb],
|
|
407
|
+
" MB",
|
|
408
|
+
"<#{target_memory}",
|
|
409
|
+
passed_memory
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
results[:total] += 1
|
|
413
|
+
passed_memory ? results[:passed] += 1 : results[:failed] += 1
|
|
414
|
+
|
|
415
|
+
results
|
|
416
|
+
end
|
|
417
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
418
|
+
|
|
419
|
+
# ============================================================================
|
|
420
|
+
# Main Runner
|
|
421
|
+
# ============================================================================
|
|
422
|
+
|
|
423
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
424
|
+
# Benchmark code prioritizes clarity over complexity metrics
|
|
425
|
+
def main
|
|
426
|
+
puts "🚀 E11y Performance Benchmark Suite"
|
|
427
|
+
puts "ADR-001 §5: Performance Requirements"
|
|
428
|
+
puts "Ruby: #{RUBY_VERSION}"
|
|
429
|
+
puts ""
|
|
430
|
+
|
|
431
|
+
all_results = {}
|
|
432
|
+
|
|
433
|
+
all_results[:small] = run_small_scale_benchmark if SCALE == "all" || SCALE == "small"
|
|
434
|
+
|
|
435
|
+
all_results[:medium] = run_medium_scale_benchmark if SCALE == "all" || SCALE == "medium"
|
|
436
|
+
|
|
437
|
+
all_results[:large] = run_large_scale_benchmark if SCALE == "all" || SCALE == "large"
|
|
438
|
+
|
|
439
|
+
print_summary(all_results)
|
|
440
|
+
|
|
441
|
+
# Exit with error code if any benchmark failed
|
|
442
|
+
failed_count = all_results.values.sum { |r| r[:failed] }
|
|
443
|
+
exit(failed_count.positive? ? 1 : 0)
|
|
444
|
+
end
|
|
445
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
446
|
+
|
|
447
|
+
main if __FILE__ == $PROGRAM_NAME
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
# Ruby Baseline Allocation Measurement
|
|
7
|
+
#
|
|
8
|
+
# Purpose: Measure Ruby's theoretical minimum allocations
|
|
9
|
+
# to establish baseline for E11y comparison.
|
|
10
|
+
#
|
|
11
|
+
# Usage: ruby benchmarks/ruby_baseline_allocations.rb
|
|
12
|
+
|
|
13
|
+
# ============================================================================
|
|
14
|
+
# Helper Functions
|
|
15
|
+
# ============================================================================
|
|
16
|
+
|
|
17
|
+
def measure_allocations
|
|
18
|
+
before = GC.stat(:total_allocated_objects)
|
|
19
|
+
yield
|
|
20
|
+
after = GC.stat(:total_allocated_objects)
|
|
21
|
+
after - before
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# ============================================================================
|
|
25
|
+
# Test Cases
|
|
26
|
+
# ============================================================================
|
|
27
|
+
|
|
28
|
+
puts "=" * 80
|
|
29
|
+
puts " Ruby Baseline Allocation Measurement"
|
|
30
|
+
puts "=" * 80
|
|
31
|
+
puts "Ruby: #{RUBY_VERSION}"
|
|
32
|
+
puts ""
|
|
33
|
+
|
|
34
|
+
# Warmup (initialize GC stats)
|
|
35
|
+
100.times { {} }
|
|
36
|
+
GC.start
|
|
37
|
+
|
|
38
|
+
puts "📊 Test 1: Empty method call"
|
|
39
|
+
puts "-" * 80
|
|
40
|
+
|
|
41
|
+
class EmptyClass
|
|
42
|
+
def self.empty_method
|
|
43
|
+
# Nothing
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
10.times { EmptyClass.empty_method } # warmup
|
|
48
|
+
|
|
49
|
+
allocations = measure_allocations do
|
|
50
|
+
1000.times { EmptyClass.empty_method }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
puts "Empty method × 1000: #{allocations} allocations"
|
|
54
|
+
puts "Per call: #{(allocations / 1000.0).round(2)} allocations"
|
|
55
|
+
puts ""
|
|
56
|
+
|
|
57
|
+
# ============================================================================
|
|
58
|
+
|
|
59
|
+
puts "📊 Test 2: Method with keyword arguments (Hash allocation)"
|
|
60
|
+
puts "-" * 80
|
|
61
|
+
|
|
62
|
+
class KwargsClass
|
|
63
|
+
def self.kwargs_method(**payload)
|
|
64
|
+
payload # return the hash
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
10.times { KwargsClass.kwargs_method(key: "value") } # warmup
|
|
69
|
+
|
|
70
|
+
allocations = measure_allocations do
|
|
71
|
+
1000.times { KwargsClass.kwargs_method(key: "value") }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
puts "Kwargs method × 1000: #{allocations} allocations"
|
|
75
|
+
puts "Per call: #{(allocations / 1000.0).round(2)} allocations"
|
|
76
|
+
puts "Expected minimum: 2.0 allocations (1 Hash for kwargs, 1 Hash return)"
|
|
77
|
+
puts ""
|
|
78
|
+
|
|
79
|
+
# ============================================================================
|
|
80
|
+
|
|
81
|
+
puts "📊 Test 3: Method returning Hash (E11y pattern)"
|
|
82
|
+
puts "-" * 80
|
|
83
|
+
|
|
84
|
+
class HashReturnClass
|
|
85
|
+
def self.return_hash(**payload)
|
|
86
|
+
{
|
|
87
|
+
event_name: "TestEvent",
|
|
88
|
+
payload: payload,
|
|
89
|
+
timestamp: Time.now.utc.iso8601(3)
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
10.times { HashReturnClass.return_hash(key: "value") } # warmup
|
|
95
|
+
|
|
96
|
+
allocations = measure_allocations do
|
|
97
|
+
1000.times { HashReturnClass.return_hash(key: "value") }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
puts "Hash return × 1000: #{allocations} allocations"
|
|
101
|
+
puts "Per call: #{(allocations / 1000.0).round(2)} allocations"
|
|
102
|
+
puts ""
|
|
103
|
+
|
|
104
|
+
# ============================================================================
|
|
105
|
+
|
|
106
|
+
puts "📊 Test 4: Method with Time.now.utc (timestamp overhead)"
|
|
107
|
+
puts "-" * 80
|
|
108
|
+
|
|
109
|
+
class TimestampClass
|
|
110
|
+
def self.with_timestamp(**payload)
|
|
111
|
+
{
|
|
112
|
+
payload: payload,
|
|
113
|
+
timestamp: Time.now.utc
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
10.times { TimestampClass.with_timestamp(key: "value") } # warmup
|
|
119
|
+
|
|
120
|
+
allocations = measure_allocations do
|
|
121
|
+
1000.times { TimestampClass.with_timestamp(key: "value") }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
puts "With timestamp × 1000: #{allocations} allocations"
|
|
125
|
+
puts "Per call: #{(allocations / 1000.0).round(2)} allocations"
|
|
126
|
+
puts ""
|
|
127
|
+
|
|
128
|
+
# ============================================================================
|
|
129
|
+
|
|
130
|
+
puts "📊 Test 5: Method with iso8601(3) (string conversion)"
|
|
131
|
+
puts "-" * 80
|
|
132
|
+
|
|
133
|
+
class ISOTimestampClass
|
|
134
|
+
def self.with_iso_timestamp(**payload)
|
|
135
|
+
{
|
|
136
|
+
payload: payload,
|
|
137
|
+
timestamp: Time.now.utc.iso8601(3)
|
|
138
|
+
}
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
10.times { ISOTimestampClass.with_iso_timestamp(key: "value") } # warmup
|
|
143
|
+
|
|
144
|
+
allocations = measure_allocations do
|
|
145
|
+
1000.times { ISOTimestampClass.with_iso_timestamp(key: "value") }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
puts "With ISO timestamp × 1000: #{allocations} allocations"
|
|
149
|
+
puts "Per call: #{(allocations / 1000.0).round(2)} allocations"
|
|
150
|
+
puts ""
|
|
151
|
+
|
|
152
|
+
# ============================================================================
|
|
153
|
+
# Summary
|
|
154
|
+
# ============================================================================
|
|
155
|
+
|
|
156
|
+
puts "=" * 80
|
|
157
|
+
puts " SUMMARY: Ruby Allocation Baseline"
|
|
158
|
+
puts "=" * 80
|
|
159
|
+
puts ""
|
|
160
|
+
puts "Key Findings:"
|
|
161
|
+
puts "1. Empty method call: ~0 allocations (baseline)"
|
|
162
|
+
puts "2. Kwargs method: 2-3 allocations (Hash for params + return)"
|
|
163
|
+
puts "3. Hash return: 4-5 allocations (kwargs + return Hash + nested payload)"
|
|
164
|
+
puts "4. With timestamp: 5-7 allocations (+ Time object)"
|
|
165
|
+
puts "5. With ISO string: 6-8 allocations (+ String conversion)"
|
|
166
|
+
puts ""
|
|
167
|
+
puts "Expected E11y minimum: 6-8 allocations per event"
|
|
168
|
+
puts "For 1K events: 6,000-8,000 allocations"
|
|
169
|
+
puts ""
|
|
170
|
+
puts "DoD target: <100 allocations per 1K events"
|
|
171
|
+
puts "Conclusion: ❌ IMPOSSIBLE (60-80x too low)"
|
|
172
|
+
puts ""
|
|
173
|
+
puts "Realistic target: <10,000 allocations per 1K events (10 per event)"
|
|
174
|
+
puts " or <10 allocations per event"
|
|
175
|
+
puts ""
|
data/benchmarks/run_all.rb
CHANGED
|
@@ -6,28 +6,16 @@
|
|
|
6
6
|
# Run all benchmarks:
|
|
7
7
|
# bundle exec ruby benchmarks/run_all.rb
|
|
8
8
|
#
|
|
9
|
-
# Run with specific
|
|
10
|
-
#
|
|
9
|
+
# Run with specific scale:
|
|
10
|
+
# SCALE=small bundle exec ruby benchmarks/run_all.rb
|
|
11
|
+
# SCALE=medium bundle exec ruby benchmarks/run_all.rb
|
|
12
|
+
# SCALE=large bundle exec ruby benchmarks/run_all.rb
|
|
11
13
|
|
|
12
14
|
require "bundler/setup"
|
|
13
|
-
require "benchmark"
|
|
14
|
-
require "e11y"
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
# Load main benchmark suite
|
|
17
|
+
load File.expand_path("e11y_benchmarks.rb", __dir__)
|
|
17
18
|
|
|
18
|
-
puts "
|
|
19
|
-
puts "
|
|
20
|
-
|
|
21
|
-
# Benchmarks will be implemented in Phase 1+
|
|
22
|
-
# Examples:
|
|
23
|
-
# - Event creation (zero-allocation goal)
|
|
24
|
-
# - Buffer operations (push/flush)
|
|
25
|
-
# - Middleware chain execution
|
|
26
|
-
# - Adapter send performance
|
|
27
|
-
|
|
28
|
-
puts "\n⚠️ Benchmarks will be implemented in Phase 1+"
|
|
29
|
-
puts "Expected metrics (from ADR-009):"
|
|
30
|
-
puts " - Event creation: < 10µs per event (zero-allocation)"
|
|
31
|
-
puts " - Buffer push: < 1µs (lock-free)"
|
|
32
|
-
puts " - Middleware: < 5µs per middleware"
|
|
33
|
-
puts " - Memory: < 100KB baseline, < 10MB under load"
|
|
19
|
+
puts "\n✅ All benchmarks completed"
|
|
20
|
+
puts "\nFor detailed benchmarks, run:"
|
|
21
|
+
puts " bundle exec ruby benchmarks/e11y_benchmarks.rb"
|
data/docs/00-ICP-AND-TIMELINE.md
CHANGED
|
@@ -228,7 +228,7 @@
|
|
|
228
228
|
- [ ] Migration guide (from Rails.logger, other gems)
|
|
229
229
|
- [ ] Video tutorials (YouTube, Quick Start)
|
|
230
230
|
- [ ] Blog posts (announcement, case studies)
|
|
231
|
-
- [ ] RubyGems
|
|
231
|
+
- [ ] RubyGems v0.1.0 release
|
|
232
232
|
- [ ] HackerNews / Reddit launch
|
|
233
233
|
|
|
234
234
|
**Success Criteria:**
|
|
@@ -270,7 +270,7 @@
|
|
|
270
270
|
|
|
271
271
|
---
|
|
272
272
|
|
|
273
|
-
###
|
|
273
|
+
### v0.1.0 (End of Phase 5) - July 2025
|
|
274
274
|
**Focus:** Production-grade, battle-tested
|
|
275
275
|
|
|
276
276
|
**Polish:**
|
|
@@ -2167,7 +2167,7 @@ graph TB
|
|
|
2167
2167
|
Version Format: MAJOR.MINOR.PATCH
|
|
2168
2168
|
|
|
2169
2169
|
Examples:
|
|
2170
|
-
- 1.0
|
|
2170
|
+
- 0.1.0 - Initial release (all 22 use cases)
|
|
2171
2171
|
- 1.1.0 - New adapter (backward compatible)
|
|
2172
2172
|
- 1.0.1 - Bug fix
|
|
2173
2173
|
- 2.0.0 - Breaking change (API change, Rails 9 support)
|