-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbenchmark.rb
More file actions
executable file
·378 lines (306 loc) · 10.9 KB
/
benchmark.rb
File metadata and controls
executable file
·378 lines (306 loc) · 10.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'fileutils'
require 'optparse'
class BenchmarkRunner
BASE_APP_PATH = File.expand_path('base_app', __dir__)
METHODS_PATH = File.expand_path('methods', __dir__)
attr_reader :verbose
def initialize(method_name, verbose: false)
@method_name = method_name
@method_path = File.join(METHODS_PATH, method_name)
@backup_gemfile = nil
@backup_gemfile_lock = nil
@backup_seeds = nil
@verbose = verbose
validate_method!
end
def log(msg)
puts msg if @verbose
end
def run
log '=' * 60
log "Benchmarking: #{@method_name}"
log '=' * 60
setup_method
install_gems
reset_and_migrate_database
load_operations
seed_data
run_benchmarks
restore_project_files
log "\n✅ Benchmark complete!"
rescue StandardError => e
puts "\n❌ Error: #{e.message}"
puts e.backtrace.first(5)
restore_project_files
exit 1
end
def self.run_all(verbose: false)
methods = Dir.glob(File.join(METHODS_PATH, '*'))
.select { |d| File.directory?(d) }
.map { |d| File.basename(d) }
.sort
puts "Running all #{methods.length} benchmarks...\n\n" if verbose
script_path = File.expand_path(__FILE__)
results = {}
methods.each do |method|
puts "\n" + '=' * 60
puts "Starting #{methods.index(method) + 1}/#{methods.length}: #{method}"
puts '=' * 60 + "\n"
# Run each benchmark in a separate Ruby process for isolation
cmd = verbose ? "ruby #{script_path} -v #{method}" : "ruby #{script_path} #{method}"
success = system(cmd)
results[method] = success ? '✅' : '❌'
puts "\n" + '=' * 60
puts "Completed #{methods.index(method) + 1}/#{methods.length}: #{method} #{results[method]}"
puts '=' * 60 + "\n"
end
puts "\n" + '=' * 60
puts 'ALL BENCHMARKS COMPLETE'
puts '=' * 60
methods.each { |m| puts "#{results[m]} #{m}" }
end
private
def validate_method!
unless File.directory?(@method_path)
puts "❌ Method '#{@method_name}' not found in methods/"
puts 'Available methods:'
Dir.glob(File.join(METHODS_PATH, '*')).each do |dir|
puts " - #{File.basename(dir)}" if File.directory?(dir)
end
exit 1
end
return if File.exist?(File.join(@method_path, 'Gemfile'))
puts "❌ Gemfile not found in #{@method_path}"
exit 1
end
def setup_method
log "\n📦 Setting up #{@method_name}..."
# Backup original Gemfile and Gemfile.lock
gemfile_path = File.join(BASE_APP_PATH, 'Gemfile')
gemfile_lock_path = File.join(BASE_APP_PATH, 'Gemfile.lock')
@backup_gemfile = File.read(gemfile_path)
@backup_gemfile_lock = File.read(gemfile_lock_path) if File.exist?(gemfile_lock_path)
# Add method gems to Gemfile
method_gems = File.read(File.join(@method_path, 'Gemfile'))
File.open(gemfile_path, 'a') do |f|
f.puts "\n# #{@method_name} method gems"
f.puts method_gems
end
log ' ✓ Added gems to Gemfile'
# Copy migrations
migrations_src = File.join(@method_path, 'migrations')
migrations_dst = File.join(BASE_APP_PATH, 'db', 'migrate')
FileUtils.mkdir_p(migrations_dst)
Dir.glob(File.join(migrations_src, '*.rb')).each do |migration|
dst_file = File.join(migrations_dst, File.basename(migration))
FileUtils.cp(migration, dst_file)
log " ✓ Copied migration: #{File.basename(migration)}"
end
# Copy models
models_src = File.join(@method_path, 'models')
models_dst = File.join(BASE_APP_PATH, 'app', 'models')
FileUtils.mkdir_p(models_dst)
Dir.glob(File.join(models_src, '*.rb')).each do |model|
dst_file = File.join(models_dst, File.basename(model))
FileUtils.cp(model, dst_file)
log " ✓ Copied model: #{File.basename(model)}"
end
# Copy operations
copy_operations
# Copy seeds.rb if method defines custom seeds
copy_seeds
end
def copy_operations
operations_file = File.join(@method_path, 'operations.rb')
return unless File.exist?(operations_file)
operations_dst = File.join(BASE_APP_PATH, 'lib', 'operations')
FileUtils.mkdir_p(operations_dst)
dst_file = File.join(operations_dst, "#{@method_name}.rb")
FileUtils.cp(operations_file, dst_file)
log " ✓ Copied operations: #{@method_name}.rb"
end
def copy_seeds
seeds_file = File.join(@method_path, 'seeds.rb')
return unless File.exist?(seeds_file)
seeds_dst = File.join(BASE_APP_PATH, 'db', 'seeds.rb')
@backup_seeds = File.exist?(seeds_dst) ? File.read(seeds_dst) : nil
FileUtils.cp(seeds_file, seeds_dst)
log ' ✓ Copied custom seeds.rb'
end
def install_gems
log "\n💎 Installing gems..."
run_command("cd #{BASE_APP_PATH} && bundle install --quiet", silent: true)
log ' ✓ Gems installed'
end
def reset_and_migrate_database
log "\n🗄️ Resetting database..."
db_name = 'array_enum_development'
# Disconnect any active ActiveRecord connections before dropping
if defined?(ActiveRecord::Base)
ActiveRecord::Base.connection_pool.disconnect!
end
run_command("cd #{BASE_APP_PATH} && bundle exec rake db:drop db:create", silent: true)
log ' ✓ Database reset'
log "\n📋 Running migrations..."
run_command("cd #{BASE_APP_PATH} && bundle exec rake db:migrate", silent: true)
log ' ✓ Migrations complete'
end
def load_operations
operations_file = File.join(BASE_APP_PATH, 'lib', 'operations', "#{@method_name}.rb")
require operations_file if File.exist?(operations_file)
end
def seed_data
log "\n🌱 Seeding data..."
require_relative 'base_app/config/environment'
# Load custom seeds if method defines them
custom_seeds_path = File.join(@method_path, 'seeds.rb')
if File.exist?(custom_seeds_path)
log ' Loading custom seeds...'
load custom_seeds_path
end
# Clear existing users (but preserve colors for junction table)
User.delete_all if defined?(User)
# Create sample users with favorite colors using Operations module
# Use subsets of COLORS constant
users_data = [
COLORS[0..2], # red, green, blue
COLORS[3..6], # yellow, purple, orange, pink
COLORS[7..11], # cyan, magenta, lime, teal, indigo
[COLORS[0], COLORS[3]], # red, yellow
COLORS[2..4] + [COLORS[7]] # blue, purple, pink, cyan
]
users_data.each do |colors|
Operations.create_user(colors)
end
log " ✓ Created #{User.count} users"
end
def run_benchmarks
puts "\n⚡ Running benchmarks..."
require 'benchmark'
n = 1000
test_colors = [COLORS[0], COLORS[2]] # red, blue
test_color = COLORS[0] # red
update_colors = [COLORS[1], COLORS[3]] # green, yellow
Benchmark.bm(20) do |x|
x.report('Create user:') do
n.times do
Operations.create_user(test_colors)
end
end
x.report('Find by color:') do
n.times do
users = Operations.find_by_color(test_color)
raise "incorrect result #{users.length}" if users.length != n + 2
end
end
x.report('Update colors:') do
User.limit(100).each do |user|
Operations.update_user_colors(user, update_colors)
end
end
end
raise "Unexpected number of end user count: #{User.count}" if User.count != n + 5
if Operations.count_by_color(test_color) != (n - 100) + 5
raise "Unexpected number of users liking '#{test_color}': #{Operations.count_by_color(test_color)}"
end
puts "\n📊 Database stats:"
puts " Total users: #{User.count}"
puts " Users liking '#{test_color}': #{Operations.count_by_color(test_color)}"
end
def restore_project_files
log "\n🧹 Restoring project files..."
# Restore original Gemfile
if @backup_gemfile
gemfile_path = File.join(BASE_APP_PATH, 'Gemfile')
File.write(gemfile_path, @backup_gemfile)
log ' ✓ Restored Gemfile'
end
# Restore original Gemfile.lock
if @backup_gemfile_lock
gemfile_lock_path = File.join(BASE_APP_PATH, 'Gemfile.lock')
File.write(gemfile_lock_path, @backup_gemfile_lock)
log ' ✓ Restored Gemfile.lock'
end
# Remove schema.rb (generated during migrations)
schema_path = File.join(BASE_APP_PATH, 'db', 'schema.rb')
if File.exist?(schema_path)
FileUtils.rm_f(schema_path)
log ' ✓ Removed schema.rb'
end
# Remove copied migrations
migrations_src = File.join(@method_path, 'migrations')
migrations_dst = File.join(BASE_APP_PATH, 'db', 'migrate')
Dir.glob(File.join(migrations_src, '*.rb')).each do |migration|
dst_file = File.join(migrations_dst, File.basename(migration))
FileUtils.rm_f(dst_file)
log " ✓ Removed migration: #{File.basename(migration)}"
end
# Remove copied models
models_src = File.join(@method_path, 'models')
models_dst = File.join(BASE_APP_PATH, 'app', 'models')
Dir.glob(File.join(models_src, '*.rb')).each do |model|
dst_file = File.join(models_dst, File.basename(model))
FileUtils.rm_f(dst_file)
log " ✓ Removed model: #{File.basename(model)}"
end
# Remove copied operations
remove_operations
# Restore original seeds.rb
restore_seeds
# NOTE: Database cleanup happens at the start of the next run
end
def remove_operations
operations_dst = File.join(BASE_APP_PATH, 'lib', 'operations', "#{@method_name}.rb")
return unless File.exist?(operations_dst)
FileUtils.rm_f(operations_dst)
log " ✓ Removed operations: #{@method_name}.rb"
end
def restore_seeds
seeds_dst = File.join(BASE_APP_PATH, 'db', 'seeds.rb')
if @backup_seeds
File.write(seeds_dst, @backup_seeds)
log ' ✓ Restored original seeds.rb'
elsif File.exist?(seeds_dst)
FileUtils.rm_f(seeds_dst)
log ' ✓ Removed custom seeds.rb'
end
end
def run_command(cmd, silent: false)
output = silent ? ' > /dev/null 2>&1' : ''
system("#{cmd}#{output}") || raise("Command failed: #{cmd}")
end
end
# Parse command line arguments
options = {}
OptionParser.new do |opts|
opts.banner = 'Usage: ruby benchmark.rb [options] METHOD'
opts.on('-a', '--all', 'Run all benchmarks') do
options[:all] = true
end
opts.on('-v', '--verbose', 'Show detailed logs') do
options[:verbose] = true
end
opts.on('-h', '--help', 'Show this help message') do
puts opts
exit
end
end.parse!
verbose = options[:verbose] || false
if options[:all]
BenchmarkRunner.run_all(verbose: verbose)
else
method_name = ARGV[0]
if method_name.nil?
puts '❌ Please provide a method name'
puts 'Usage: ruby benchmark.rb METHOD'
puts "\nExample:"
puts ' ruby benchmark.rb array_enum'
puts "\nOr run all benchmarks:"
puts ' ruby benchmark.rb --all'
exit 1
end
runner = BenchmarkRunner.new(method_name, verbose: verbose)
runner.run
end