342 lines
7.6 KiB
Crystal
342 lines
7.6 KiB
Crystal
#!/usr/bin/env crystal
|
|
|
|
require "http/client"
|
|
require "json"
|
|
|
|
PORT = "4001"
|
|
APP_URL = "http://localhost:#{PORT}"
|
|
API_URL = "#{APP_URL}/api/links"
|
|
API_KEY = "secure_api_key_1"
|
|
NUMBER_OF_REQUESTS = 100000
|
|
|
|
APP_COMMAND = "./bit"
|
|
APP_ARGS = [] of String
|
|
STATS_FILE = "resource_usage.log"
|
|
APP_LOG_FILE = "app_output.log"
|
|
|
|
DATABASE_URL = "sqlite3://./sqlite/data.benchmark.db?journal_mode=wal&synchronous=normal&foreign_keys=true"
|
|
DATABASE_FILE = "./sqlite/data.benchmark.db"
|
|
|
|
class ResourceMonitor
|
|
def initialize(@pid : Int32)
|
|
@running = false
|
|
@stats = [] of {timestamp: Time, cpu: Float64, memory: Float64}
|
|
end
|
|
|
|
def start
|
|
@running = true
|
|
@stats.clear
|
|
File.write(STATS_FILE, "Timestamp\tCPU(%)\tMemory(MiB)\n")
|
|
|
|
spawn do
|
|
while @running
|
|
if stat = capture_stats
|
|
File.open(STATS_FILE, "a") do |file|
|
|
file.puts "#{stat[:timestamp].to_unix}\t#{stat[:cpu]}\t#{stat[:memory]}"
|
|
end
|
|
@stats << stat
|
|
end
|
|
sleep 1.seconds
|
|
end
|
|
end
|
|
end
|
|
|
|
def stop
|
|
@running = false
|
|
sleep 1.seconds
|
|
end
|
|
|
|
private def capture_stats
|
|
output = IO::Memory.new
|
|
process = Process.run(
|
|
"ps", ["-p", @pid.to_s, "-o", "%cpu,%mem,rss"],
|
|
output: output
|
|
)
|
|
|
|
if process.success?
|
|
lines = output.to_s.strip.split("\n")
|
|
if lines.size >= 2
|
|
data_line = lines[1].strip.split
|
|
if data_line.size >= 3
|
|
cpu = data_line[0].to_f
|
|
# RSS is in KB on macOS, convert to MiB
|
|
memory_kb = data_line[2].to_f
|
|
memory_mib = memory_kb / 1024.0
|
|
|
|
return {timestamp: Time.utc, cpu: cpu, memory: memory_mib}
|
|
end
|
|
end
|
|
end
|
|
nil
|
|
end
|
|
|
|
def print_summary
|
|
return if @stats.empty?
|
|
|
|
total_cpu = 0.0
|
|
total_memory = 0.0
|
|
peak_cpu = 0.0
|
|
peak_memory = 0.0
|
|
|
|
@stats.each do |stat|
|
|
total_cpu += stat[:cpu]
|
|
total_memory += stat[:memory]
|
|
peak_cpu = stat[:cpu] if stat[:cpu] > peak_cpu
|
|
peak_memory = stat[:memory] if stat[:memory] > peak_memory
|
|
end
|
|
|
|
avg_cpu = total_cpu / @stats.size
|
|
avg_memory = total_memory / @stats.size
|
|
|
|
summary = <<-STATS
|
|
|
|
**** Resource Usage Statistics ****
|
|
Measurements: #{@stats.size}
|
|
Average CPU Usage: #{avg_cpu.round(2)}%
|
|
Average Memory Usage: #{avg_memory.round(2)} MiB
|
|
Peak CPU Usage: #{peak_cpu.round(2)}%
|
|
Peak Memory Usage: #{peak_memory.round(2)} MiB
|
|
|
|
STATS
|
|
|
|
File.open(STATS_FILE, "a") do |file|
|
|
file.puts summary
|
|
end
|
|
|
|
puts summary
|
|
end
|
|
end
|
|
|
|
def start_application : Process
|
|
puts "Starting application: #{APP_COMMAND}..."
|
|
puts "Application output will be saved to: #{APP_LOG_FILE}"
|
|
|
|
log_file = File.open(APP_LOG_FILE, "w")
|
|
|
|
process = Process.new(
|
|
APP_COMMAND,
|
|
APP_ARGS,
|
|
output: log_file,
|
|
error: log_file,
|
|
env: {
|
|
"DATABASE_URL" => DATABASE_URL,
|
|
"APP_URL" => APP_URL,
|
|
"PORT" => PORT,
|
|
}
|
|
)
|
|
|
|
puts "Application started with PID: #{process.pid}"
|
|
puts "Using database: #{DATABASE_FILE}"
|
|
process
|
|
end
|
|
|
|
def stop_application(process : Process)
|
|
puts "\nStopping application..."
|
|
process.signal(Signal::TERM)
|
|
|
|
# Give it a few seconds to shut down gracefully
|
|
sleep 3.seconds
|
|
|
|
# Force kill if still running
|
|
begin
|
|
process.signal(Signal::KILL)
|
|
rescue
|
|
# Process already terminated
|
|
end
|
|
|
|
puts "Application stopped."
|
|
end
|
|
|
|
def check_dependencies
|
|
{"bombardier", "sqlite3", "micrate"}.each do |cmd|
|
|
process = Process.run("which", [cmd], output: Process::Redirect::Close)
|
|
unless process.success?
|
|
puts "Error: #{cmd} is not installed. Please install it to proceed."
|
|
case cmd
|
|
when "bombardier"
|
|
puts " brew install bombardier"
|
|
when "sqlite3"
|
|
puts " brew install sqlite3"
|
|
when "micrate"
|
|
puts " shards install"
|
|
end
|
|
exit(1)
|
|
end
|
|
end
|
|
end
|
|
|
|
def wait_for_server
|
|
puts "Checking if server is ready at #{APP_URL}..."
|
|
|
|
30.times do
|
|
begin
|
|
if HTTP::Client.get("#{APP_URL}/api/ping").success?
|
|
puts "Server is ready!"
|
|
return
|
|
end
|
|
rescue
|
|
# Server not ready yet
|
|
end
|
|
sleep 1.seconds
|
|
print "."
|
|
end
|
|
|
|
puts "\nError: Server is not responding. Please start your application first."
|
|
exit(1)
|
|
end
|
|
|
|
def run_benchmark
|
|
puts "Fetching links from API..."
|
|
|
|
response = HTTP::Client.get(
|
|
"#{API_URL}?limit=10000",
|
|
headers: HTTP::Headers{"X-Api-Key" => API_KEY}
|
|
)
|
|
|
|
unless response.success?
|
|
puts "Failed to fetch links. Status: #{response.status_code}"
|
|
puts "Make sure your server is running and the API key is correct."
|
|
exit(1)
|
|
end
|
|
|
|
data = JSON.parse(response.body)
|
|
links = data["data"].as_a.map { |link| link["refer"].as_s }
|
|
|
|
if links.empty?
|
|
puts "No links found. Please seed your database first."
|
|
exit(1)
|
|
end
|
|
|
|
random_link = links.sample
|
|
puts "Selected link: #{random_link}"
|
|
puts "\nStarting benchmark with #{NUMBER_OF_REQUESTS} requests..."
|
|
|
|
sleep 2.seconds
|
|
|
|
process = Process.new(
|
|
"bombardier",
|
|
["-n", NUMBER_OF_REQUESTS.to_s, "-l", "--disableKeepAlives", random_link],
|
|
output: Process::Redirect::Inherit,
|
|
error: Process::Redirect::Inherit
|
|
)
|
|
|
|
status = process.wait
|
|
|
|
if status.success?
|
|
puts "\nBenchmark completed successfully."
|
|
else
|
|
puts "\nBombardier failed with error code: #{status.exit_code}"
|
|
exit(1)
|
|
end
|
|
end
|
|
|
|
def cleanup_database
|
|
puts "Cleaning up benchmark database..."
|
|
|
|
if File.exists?(DATABASE_FILE)
|
|
File.delete(DATABASE_FILE)
|
|
puts "Deleted existing database: #{DATABASE_FILE}"
|
|
end
|
|
|
|
# Also remove WAL and SHM files if they exist
|
|
["#{DATABASE_FILE}-wal", "#{DATABASE_FILE}-shm"].each do |file|
|
|
if File.exists?(file)
|
|
File.delete(file)
|
|
puts "Deleted: #{file}"
|
|
end
|
|
end
|
|
|
|
# Ensure sqlite directory exists
|
|
Dir.mkdir_p("./sqlite")
|
|
puts "Database cleanup completed."
|
|
end
|
|
|
|
def run_migrations
|
|
puts "Running database migrations..."
|
|
|
|
process = Process.run("which", ["micrate"], output: Process::Redirect::Close)
|
|
unless process.success?
|
|
puts "Error: micrate is not installed. Please install it to proceed."
|
|
puts " shards install"
|
|
exit(1)
|
|
end
|
|
|
|
process = Process.run(
|
|
"micrate",
|
|
["up"],
|
|
env: {"DATABASE_URL" => DATABASE_URL},
|
|
output: Process::Redirect::Inherit,
|
|
error: Process::Redirect::Inherit
|
|
)
|
|
|
|
if process.success?
|
|
puts "Migrations completed successfully."
|
|
else
|
|
puts "Error: Migrations failed."
|
|
exit(1)
|
|
end
|
|
end
|
|
|
|
def seed_database
|
|
puts "Seeding benchmark database..."
|
|
|
|
unless File.exists?("./db/seed.sql")
|
|
puts "Warning: ./db/seed.sql not found. Skipping database seeding."
|
|
return
|
|
end
|
|
|
|
unless File.exists?(DATABASE_FILE)
|
|
puts "Warning: #{DATABASE_FILE} not found. Database may not be initialized."
|
|
end
|
|
|
|
process = Process.run(
|
|
"sqlite3",
|
|
[DATABASE_FILE],
|
|
input: File.open("./db/seed.sql"),
|
|
output: Process::Redirect::Inherit,
|
|
error: Process::Redirect::Inherit
|
|
)
|
|
|
|
if process.success?
|
|
puts "Database seeded successfully."
|
|
else
|
|
puts "Warning: Database seeding failed. Continuing anyway..."
|
|
end
|
|
end
|
|
|
|
def main
|
|
check_dependencies
|
|
|
|
# Setup benchmark database
|
|
cleanup_database
|
|
run_migrations
|
|
seed_database
|
|
|
|
app_process = start_application
|
|
|
|
begin
|
|
wait_for_server
|
|
|
|
# Give it a moment to settle
|
|
sleep 2.seconds
|
|
|
|
monitor = ResourceMonitor.new(app_process.pid.to_i32)
|
|
monitor.start
|
|
|
|
run_benchmark
|
|
|
|
monitor.stop
|
|
monitor.print_summary
|
|
|
|
puts "\n**** Files Generated ****"
|
|
puts " Resource stats: #{STATS_FILE}"
|
|
puts " Application log: #{APP_LOG_FILE}"
|
|
puts " Database: #{DATABASE_FILE}"
|
|
ensure
|
|
# Always stop the application
|
|
stop_application(app_process)
|
|
end
|
|
end
|
|
|
|
main
|