refactor: rewrite benchmark in cr
This commit is contained in:
+1
-1
@@ -7,4 +7,4 @@
|
||||
/sqlite/
|
||||
.env.production
|
||||
|
||||
resource_usage.csv
|
||||
resource_usage.*
|
||||
|
||||
Executable
+268
@@ -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
@@ -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
@@ -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
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user