refactor: rewrite benchmark in cr

This commit is contained in:
sjdonado
2025-03-20 12:01:13 +01:00
parent bf717dc38f
commit d1be283318
4 changed files with 274 additions and 171 deletions
+1 -1
View File
@@ -7,4 +7,4 @@
/sqlite/
.env.production
resource_usage.csv
resource_usage.*
Executable
+268
View File
@@ -0,0 +1,268 @@
#!/usr/bin/env crystal
require "http/client"
require "json"
require "file_utils"
# Configuration variables
SERVER_URL = "http://localhost:4000"
API_URL = "#{SERVER_URL}/api/links"
API_KEY = "secure_api_key_1"
NUM_LINKS = 10_000
NUM_REQUESTS = 100_000
CONCURRENCY = 100
RESOURCE_USAGE_INTERVAL = 1
CONTAINER_NAME = "bit"
STATS_FILE = "resource_usage.txt"
class ResourceMonitor
def initialize(@container_name : String, @interval : Int32)
@running = false
@stats = [] of {timestamp: Time, cpu: Float64, memory: Float64}
end
def start
@running = true
@stats.clear
# Initialize stats file with header
File.write(STATS_FILE, "Timestamp\tCPU(%)\tMemory(MiB)\n")
spawn do
while @running
if stat = capture_stats
# Append each measurement directly to the file
File.open(STATS_FILE, "a") do |file|
file.puts "#{stat[:timestamp].to_unix}\t#{stat[:cpu]}\t#{stat[:memory]}"
end
@stats << stat
end
sleep @interval.seconds
end
end
end
def stop
@running = false
end
def avg_stats
return {cpu: 0.0, memory: 0.0} if @stats.empty?
total_cpu = 0.0
total_memory = 0.0
@stats.each do |stat|
total_cpu += stat[:cpu]
total_memory += stat[:memory]
end
{
cpu: total_cpu / @stats.size,
memory: total_memory / @stats.size
}
end
private def capture_stats
output = IO::Memory.new
process = Process.run(
"docker", ["stats", "--no-stream", "--format", "{{.CPUPerc}},{{.MemUsage}}", @container_name],
output: output
)
if process.success?
line = output.to_s.strip
parts = line.split(",")
if parts.size == 2
cpu_part = parts[0].gsub("%", "").to_f
mem_part = parts[1].split.first.to_f
return {timestamp: Time.utc, cpu: cpu_part, memory: mem_part}
end
end
nil
end
end
def check_dependencies
{"docker", "jq", "bombardier"}.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."
exit(1)
end
end
end
def setup_containers
puts "Setting up..."
process = Process.run("docker", ["compose", "up", "-d"])
unless process.success?
puts "Failed to start Docker containers."
exit(1)
end
puts "Waiting for the application to be ready..."
until begin
HTTP::Client.get("#{SERVER_URL}/api/ping").success?
rescue
false
end
sleep 1.seconds
end
puts "Seeding the database..."
process = Process.run("docker", ["compose", "exec", "app", "sh", "-c", "sqlite3 ./sqlite/data.db < ./db/seed.sql"])
unless process.success?
puts "Error on seeding database"
exit(1)
end
puts "Checking seed results..."
until begin
HTTP::Client.get(
"#{API_URL}",
headers: HTTP::Headers{"X-Api-Key" => API_KEY}
).success?
rescue
false
end
sleep 1.seconds
end
end
def run_benchmark
puts "Fetching all created links from /api/links..."
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}"
exit(1)
end
data = JSON.parse(response.body)
links = data["data"].as_a.map { |link| link["refer"].as_s }
if links.size != NUM_LINKS
puts "Error: Expected #{NUM_LINKS} links but found #{links.size}."
exit(1)
end
random_link = links.sample
puts "Selected link for benchmarking: #{random_link}"
puts "Starting benchmark with Bombardier..."
# Run bombardier for benchmarking
stdout = IO::Memory.new
stderr = IO::Memory.new
process = Process.run(
"bombardier",
["-c", CONCURRENCY.to_s, "-n", NUM_REQUESTS.to_s, random_link],
output: stdout,
error: stderr
)
unless process.success?
puts "Bombardier failed with error:"
puts stderr.to_s
exit(1)
end
# Display bombardier results
puts stdout.to_s
puts "Benchmark completed."
# Save bombardier results to file
File.write("bombardier_results.txt", stdout.to_s)
puts "Bombardier results saved to bombardier_results.txt"
end
def analyze_resource_usage
puts "Analyzing resource usage..."
# Read stats directly from file for more accurate results
if File.exists?(STATS_FILE)
lines = File.read_lines(STATS_FILE)
# Skip header
lines = lines[1..-1] if lines.size > 0
if lines.size > 0
total_cpu = 0.0
total_memory = 0.0
lines.each do |line|
fields = line.split("\t")
if fields.size >= 3
begin
cpu = fields[1].to_f
memory = fields[2].to_f
total_cpu += cpu
total_memory += memory
rescue
# Skip invalid lines
end
end
end
avg_cpu = total_cpu / lines.size
avg_memory = total_memory / lines.size
# Format statistics summary
stats_summary = <<-STATS
**** Resource Usage Statistics ****
Measurements: #{lines.size}
Average CPU Usage: #{avg_cpu.round(2)}%
Average Memory Usage: #{avg_memory.round(2)} MiB
Peak CPU Usage: #{lines.max_by { |l| l.split("\t")[1].to_f rescue 0.0 }.split("\t")[1].to_f rescue 0.0}%
Peak Memory Usage: #{lines.max_by { |l| l.split("\t")[2].to_f rescue 0.0 }.split("\t")[2].to_f rescue 0.0} MiB
STATS
puts stats_summary
# Append summary to stats file
File.open(STATS_FILE, "a") do |file|
file.puts "\n" + stats_summary
end
else
puts "No resource usage data collected."
end
else
puts "Resource usage file not found."
end
end
def cleanup
Process.run("docker", ["compose", "down"])
puts "Cleanup completed. Resource usage data saved in #{STATS_FILE}"
puts "Bombardier results saved in bombardier_results.txt"
end
def main
check_dependencies
setup_containers
monitor = ResourceMonitor.new(CONTAINER_NAME, RESOURCE_USAGE_INTERVAL)
monitor.start
begin
run_benchmark
monitor.stop
analyze_resource_usage
ensure
cleanup
end
end
main
-156
View File
@@ -1,156 +0,0 @@
#!/bin/bash
# Configuration variables
server_url="http://localhost:4000"
api_url="${server_url}/api/links"
num_links=10000
num_requests=100000
concurrency=100
create_links_concurrency=100
resource_usage_interval=1
container_name="bit"
check_dependencies() {
if ! command -v bombardier &> /dev/null; then
echo "Error: bombardier is not installed. Please install it to proceed."
exit 1
fi
if ! command -v jq &> /dev/null; then
echo "Error: jq is not installed. Please install it to proceed."
exit 1
fi
}
setup_containers() {
echo "Setting up..."
docker compose up -d
if [ $? -ne 0 ]; then
echo "Failed to start Docker containers."
exit 1
fi
output=$(docker compose exec -T app cli --create-user=Admin)
api_key=$(echo "$output" | awk -F' ' '/X-Api-Key:/{print $NF}')
echo "Captured API Key: $api_key"
if [[ -z "$api_key" ]]; then
echo "Error: API key could not be retrieved."
exit 1
fi
echo "Waiting for the application to be ready..."
until curl --silent --head --fail --header "X-Api-Key: $api_key" "$server_url/api/ping"; do
sleep 2
done
}
monitor_resource_usage() {
echo "Starting resource usage monitoring..."
echo "Timestamp,CPU,Memory" > resource_usage.csv
while :; do
stats=$(docker stats --no-stream --format "{{.CPUPerc}},{{.MemUsage}}" $container_name)
cpu=$(echo $stats | awk -F',' '{print $1}' | sed 's/%//')
mem=$(echo $stats | awk -F',' '{print $2}' | awk '{print $1}')
timestamp=$(date +%s)
echo "$timestamp,$cpu,$mem" >> resource_usage.csv
sleep $resource_usage_interval
done
}
create_links() {
local temp_file=$(mktemp)
echo "Creating $num_links short links with $create_links_concurrency concurrent requests..."
for ((i=1; i<=num_links; i++)); do
url="https://httpbin.org/anything/$i"
echo "--next" >> "$temp_file"
echo "--request POST" >> "$temp_file"
echo "--url \"$api_url\"" >> "$temp_file"
echo "--header \"X-Api-Key: $api_key\"" >> "$temp_file"
echo "--header \"Content-Type: application/json\"" >> "$temp_file"
echo "--data \"{ \\\"url\\\": \\\"$url\\\" }\"" >> "$temp_file"
done
curl --parallel --parallel-immediate --parallel-max $create_links_concurrency --config "$temp_file" --silent --write-out "%{http_code}\n" > /dev/null
echo "Link creation complete: $num_links links created using httpbin's anything endpoint."
rm -f "$temp_file"
}
run_benchmark() {
echo "Fetching all created links from /api/links..."
all_links_response=$(curl --silent --request GET \
--url "$api_url" \
--header "X-Api-Key: $api_key" \
--header "Content-Type: application/json")
links=($(echo "$all_links_response" | jq -r '.data[] | .refer'))
if [[ ${#links[@]} -ne $num_links ]]; then
echo "Error: Expected $num_links links but found ${#links[@]}."
exit 1
fi
random_link="${links[RANDOM % ${#links[@]}]}"
echo "Selected link for benchmarking: $random_link"
echo "Starting benchmark with Bombardier..."
bombardier -c $concurrency -n $num_requests "$random_link"
echo "Benchmark completed."
}
analyze_resource_usage() {
echo "Analyzing resource usage..."
total_cpu=0
total_mem=0
count=0
while IFS=',' read -r timestamp cpu mem; do
# Skip header line and lines with empty cpu or mem values
if [[ $timestamp != "Timestamp" && -n $cpu && -n $mem ]]; then
mem=${mem%MiB}
total_cpu=$(echo "$total_cpu + $cpu" | bc)
total_mem=$(echo "$total_mem + $mem" | bc)
((count++))
fi
done < resource_usage.csv
avg_cpu=0.00
avg_mem=0.00
if (( count > 0 )); then
avg_cpu=$(echo "scale=2; $total_cpu / $count" | bc)
avg_mem=$(echo "scale=2; $total_mem / $count" | bc)
fi
echo "**** Results ****"
echo "Average CPU Usage: $avg_cpu%"
echo "Average Memory Usage: $avg_mem MiB"
}
cleanup() {
rm -f resource_usage.csv
docker compose down
}
main() {
check_dependencies
setup_containers
monitor_resource_usage & # Start monitoring in the background
monitor_pid=$!
trap 'kill $monitor_pid; cleanup; exit' INT
create_links
run_benchmark
kill $monitor_pid
analyze_resource_usage
cleanup
}
main
+5 -14
View File
@@ -1,32 +1,23 @@
-- Create 10 users
INSERT INTO users (name, api_key)
VALUES
('User 1', 'secure_api_key_1'),
('User 2', 'secure_api_key_2'),
('User 3', 'secure_api_key_3'),
('User 4', 'secure_api_key_4'),
('User 5', 'secure_api_key_5'),
('User 6', 'secure_api_key_6'),
('User 7', 'secure_api_key_7'),
('User 8', 'secure_api_key_8'),
('User 9', 'secure_api_key_9'),
('User 10', 'secure_api_key_10');
('User 2', 'secure_api_key_2');
-- Create 1,000 links (100 per user)
-- Create 200,000 links (100,000 per user)
WITH RECURSIVE link_numbers(n) AS (
SELECT 1
UNION ALL
SELECT n+1 FROM link_numbers
LIMIT 1000
LIMIT 200000
)
INSERT INTO links (user_id, slug, url)
SELECT
((n-1) % 10) + 1, -- User ID (1-10)
((n-1) % 2) + 1, -- User ID (1-2)
'slug' || n, -- Unique slug
'https://sjdonado.com/page/' || n
FROM link_numbers;
-- Create 1,000,000 clicks (1,000 per link)
-- Create 200,000,000 clicks (1,000 per link)
WITH RECURSIVE link_numbers(link_id) AS (
SELECT id FROM links
),