Initial release: Go Jdenticon library v0.1.0

- Core library with SVG and PNG generation
- CLI tool with generate and batch commands
- Cross-platform path handling for Windows compatibility
- Comprehensive test suite with integration tests
This commit is contained in:
Kevin McIntyre
2026-01-02 23:56:48 -05:00
parent f84b511895
commit d9e84812ff
292 changed files with 19725 additions and 38884 deletions

156
scripts/benchmark-hotspots.sh Executable file
View File

@@ -0,0 +1,156 @@
#!/bin/bash
# Quick Memory Hotspot Validation Script
# Usage: ./scripts/benchmark-hotspots.sh [baseline_dir]
set -e
# Colors
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
echo -e "${BLUE}🎯 Go Jdenticon Hotspot Validation${NC}"
echo -e "${BLUE}=================================${NC}"
BASELINE_DIR="${1:-./profiles/baseline}"
TEMP_DIR="./profiles/temp_$(date +%s)"
mkdir -p "$TEMP_DIR"
# Function to run quick benchmark
quick_benchmark() {
local package=$1
local name=$2
echo -e "${BLUE}⚡ Quick benchmark: ${package}${NC}"
go test -bench=BenchmarkGenerate -benchmem -count=3 "./${package}" > "${TEMP_DIR}/${name}_current.txt" 2>&1
}
# Function to extract key metrics
extract_metrics() {
local file=$1
# Extract allocation metrics from benchmark output
grep "BenchmarkGenerate" "$file" | \
awk '{
if (NF >= 5) {
ns_per_op = $3
bytes_per_op = $4
allocs_per_op = $5
gsub(/ns\/op/, "", ns_per_op)
gsub(/B\/op/, "", bytes_per_op)
gsub(/allocs\/op/, "", allocs_per_op)
print bytes_per_op "," allocs_per_op "," ns_per_op
}
}' | tail -1
}
# Function to compare metrics
compare_metrics() {
local baseline_file=$1
local current_file=$2
local component=$3
if [ ! -f "$baseline_file" ]; then
echo -e "${YELLOW}⚠️ No baseline found for ${component}${NC}"
return
fi
baseline_metrics=$(extract_metrics "$baseline_file")
current_metrics=$(extract_metrics "$current_file")
if [ -z "$baseline_metrics" ] || [ -z "$current_metrics" ]; then
echo -e "${RED}❌ Could not extract metrics for ${component}${NC}"
return
fi
IFS=',' read -r baseline_bytes baseline_allocs baseline_ns <<< "$baseline_metrics"
IFS=',' read -r current_bytes current_allocs current_ns <<< "$current_metrics"
# Calculate percentage changes
bytes_change=$(echo "scale=1; ($current_bytes - $baseline_bytes) * 100 / $baseline_bytes" | bc 2>/dev/null || echo "0")
allocs_change=$(echo "scale=1; ($current_allocs - $baseline_allocs) * 100 / $baseline_allocs" | bc 2>/dev/null || echo "0")
time_change=$(echo "scale=1; ($current_ns - $baseline_ns) * 100 / $baseline_ns" | bc 2>/dev/null || echo "0")
echo -e "\n${BLUE}📊 ${component} Comparison:${NC}"
echo -e " Memory: ${baseline_bytes}${current_bytes} B/op (${bytes_change}%)"
echo -e " Allocations: ${baseline_allocs}${current_allocs} allocs/op (${allocs_change}%)"
echo -e " Time: ${baseline_ns}${current_ns} ns/op (${time_change}%)"
# Color code the results
if (( $(echo "$bytes_change < -5" | bc -l 2>/dev/null || echo 0) )); then
echo -e " Status: ${GREEN}🚀 Memory improvement!${NC}"
elif (( $(echo "$bytes_change > 10" | bc -l 2>/dev/null || echo 0) )); then
echo -e " Status: ${RED}⚠️ Memory regression${NC}"
else
echo -e " Status: ${YELLOW}📈 Within normal range${NC}"
fi
}
# Main execution
echo -e "${YELLOW}🔥 Running hotspot validation benchmarks...${NC}\n"
# Run benchmarks for key components
quick_benchmark "internal/engine" "engine"
quick_benchmark "jdenticon" "full"
echo -e "\n${BLUE}📈 Comparing with baseline...${NC}"
# Compare with baseline if available
if [ -d "$BASELINE_DIR" ]; then
compare_metrics "${BASELINE_DIR}/engine_memory_results.txt" "${TEMP_DIR}/engine_current.txt" "Engine"
compare_metrics "${BASELINE_DIR}/full_memory_results.txt" "${TEMP_DIR}/full_current.txt" "Full Library"
else
echo -e "${YELLOW}⚠️ No baseline directory found at: ${BASELINE_DIR}${NC}"
echo -e "${BLUE}💡 Run memory-analysis.sh first to establish baseline${NC}"
fi
# Show current raw results
echo -e "\n${BLUE}📋 Current Raw Results:${NC}"
echo -e "${YELLOW}Engine Package:${NC}"
grep "BenchmarkGenerate" "${TEMP_DIR}/engine_current.txt" | head -5 || echo "No results"
echo -e "\n${YELLOW}Full Library:${NC}"
grep "BenchmarkGenerate" "${TEMP_DIR}/full_current.txt" | head -5 || echo "No results"
# Quick hotspot check
echo -e "\n${BLUE}🔍 Quick Hotspot Status Check:${NC}"
# Check if we're within expected performance bounds
engine_metrics=$(extract_metrics "${TEMP_DIR}/engine_current.txt")
if [ -n "$engine_metrics" ]; then
IFS=',' read -r bytes allocs ns <<< "$engine_metrics"
echo -e "Current Performance:"
echo -e " Memory: ${bytes} B/op"
echo -e " Allocations: ${allocs} allocs/op"
echo -e " Time: ${ns} ns/op"
# Check against known hotspot targets
if (( $(echo "$bytes > 6000" | bc -l 2>/dev/null || echo 0) )); then
echo -e " ${RED}🔥 High memory usage - optimization needed${NC}"
elif (( $(echo "$bytes < 3000" | bc -l 2>/dev/null || echo 0) )); then
echo -e " ${GREEN}✅ Excellent memory efficiency${NC}"
else
echo -e " ${YELLOW}📊 Moderate memory usage${NC}"
fi
if (( $(echo "$allocs > 60" | bc -l 2>/dev/null || echo 0) )); then
echo -e " ${RED}🔥 High allocation count - pooling opportunities${NC}"
elif (( $(echo "$allocs < 30" | bc -l 2>/dev/null || echo 0) )); then
echo -e " ${GREEN}✅ Efficient allocation pattern${NC}"
else
echo -e " ${YELLOW}📊 Moderate allocation count${NC}"
fi
fi
echo -e "\n${BLUE}💡 Quick Commands:${NC}"
echo -e " Set as new baseline: ${YELLOW}cp ${TEMP_DIR}/*_current.txt ${BASELINE_DIR}/${NC}"
echo -e " Full analysis: ${YELLOW}./scripts/memory-analysis.sh${NC}"
echo -e " Profile specific hotspot: ${YELLOW}go test -bench=BenchmarkGenerate -memprofile=temp.prof ./internal/engine && go tool pprof temp.prof${NC}"
# Cleanup
echo -e "\n${GREEN}✅ Hotspot validation completed${NC}"
echo -e "${BLUE}📁 Temp results in: ${TEMP_DIR}${NC}"

186
scripts/memory-analysis.sh Executable file
View File

@@ -0,0 +1,186 @@
#!/bin/bash
# Comprehensive Memory Profiling Script for go-jdenticon
# Usage: ./scripts/memory-analysis.sh [output_dir]
set -e
# Configuration
OUTPUT_DIR="${1:-./profiles}"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
REPORT_DIR="${OUTPUT_DIR}/report_${TIMESTAMP}"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${BLUE}🔍 Go Jdenticon Memory Analysis${NC}"
echo -e "${BLUE}================================${NC}"
# Create output directories
mkdir -p "${REPORT_DIR}"
echo -e "${YELLOW}📁 Output directory: ${REPORT_DIR}${NC}"
# Function to run memory profiling benchmark
run_memory_benchmark() {
local package=$1
local profile_name=$2
local benchmark_pattern=${3:-"."}
echo -e "${BLUE}🧪 Running memory benchmark: ${package}${NC}"
go test -bench="${benchmark_pattern}" \
-memprofile="${REPORT_DIR}/${profile_name}.prof" \
-benchmem \
-benchtime=3s \
"./${package}" > "${REPORT_DIR}/${profile_name}_results.txt" 2>&1
if [ $? -eq 0 ]; then
echo -e "${GREEN}${package} profiling completed${NC}"
else
echo -e "${RED}${package} profiling failed${NC}"
return 1
fi
}
# Function to analyze memory profile
analyze_profile() {
local profile_path=$1
local analysis_name=$2
echo -e "${BLUE}📊 Analyzing ${analysis_name}${NC}"
# Generate top allocations
go tool pprof -top "${profile_path}" > "${REPORT_DIR}/${analysis_name}_top.txt" 2>/dev/null
# Generate allocation list for hotspot functions
go tool pprof -list="renderShape|AddPolygon|GenerateColorTheme|svgValue" "${profile_path}" > "${REPORT_DIR}/${analysis_name}_hotspots.txt" 2>/dev/null
# Generate memory usage by package
go tool pprof -top -cum "${profile_path}" > "${REPORT_DIR}/${analysis_name}_cumulative.txt" 2>/dev/null
echo -e "${GREEN}${analysis_name} analysis completed${NC}"
}
# Function to generate comparison report
generate_comparison() {
local baseline_prof=$1
local current_prof=$2
local comparison_name=$3
if [ -f "${baseline_prof}" ] && [ -f "${current_prof}" ]; then
echo -e "${BLUE}🔄 Generating comparison: ${comparison_name}${NC}"
go tool pprof -base="${baseline_prof}" -top "${current_prof}" > "${REPORT_DIR}/${comparison_name}_diff.txt" 2>/dev/null
echo -e "${GREEN}✅ Comparison ${comparison_name} completed${NC}"
fi
}
# Main execution
echo -e "${YELLOW}🚀 Starting memory profiling...${NC}"
# 1. Profile engine package
echo -e "\n${BLUE}1. Engine Package Profiling${NC}"
run_memory_benchmark "internal/engine" "engine_memory"
# 2. Profile renderer package
echo -e "\n${BLUE}2. Renderer Package Profiling${NC}"
run_memory_benchmark "internal/renderer" "renderer_memory"
# 3. Profile full library
echo -e "\n${BLUE}3. Full Library Profiling${NC}"
run_memory_benchmark "jdenticon" "full_memory"
# 4. Analyze profiles
echo -e "\n${BLUE}4. Analyzing Memory Profiles${NC}"
for profile in "${REPORT_DIR}"/*.prof; do
if [ -f "$profile" ]; then
basename=$(basename "$profile" .prof)
analyze_profile "$profile" "$basename"
fi
done
# 5. Generate comparison with baseline if available
BASELINE_DIR="./profiles/baseline"
if [ -d "$BASELINE_DIR" ]; then
echo -e "\n${BLUE}5. Comparing with Baseline${NC}"
for profile in "${REPORT_DIR}"/*.prof; do
basename=$(basename "$profile")
if [ -f "${BASELINE_DIR}/${basename}" ]; then
comparison_name=$(basename "$basename" .prof)
generate_comparison "${BASELINE_DIR}/${basename}" "$profile" "$comparison_name"
fi
done
fi
# 6. Generate summary report
echo -e "\n${BLUE}6. Generating Summary Report${NC}"
cat > "${REPORT_DIR}/ANALYSIS_SUMMARY.md" << EOF
# Memory Analysis Report - ${TIMESTAMP}
## Benchmark Results
### Engine Package
\`\`\`
$(cat "${REPORT_DIR}/engine_memory_results.txt" | grep "Benchmark" | head -10)
\`\`\`
### Renderer Package
\`\`\`
$(cat "${REPORT_DIR}/renderer_memory_results.txt" | grep "Benchmark" | head -10)
\`\`\`
### Full Library
\`\`\`
$(cat "${REPORT_DIR}/full_memory_results.txt" | grep "Benchmark" | head -10)
\`\`\`
## Top Memory Allocations
### Engine Hotspots
\`\`\`
$(head -20 "${REPORT_DIR}/engine_memory_top.txt")
\`\`\`
### Renderer Hotspots
\`\`\`
$(head -20 "${REPORT_DIR}/renderer_memory_top.txt")
\`\`\`
## Analysis Files Generated
- \`*_top.txt\`: Top allocation sources
- \`*_hotspots.txt\`: Known hotspot function analysis
- \`*_cumulative.txt\`: Cumulative allocation by package
- \`*_diff.txt\`: Comparison with baseline (if available)
## Next Steps
1. Review top allocation sources in \`*_top.txt\` files
2. Focus optimization on functions listed in hotspot analysis
3. Use \`go tool pprof\` for interactive analysis:
\`\`\`bash
go tool pprof ${REPORT_DIR}/engine_memory.prof
\`\`\`
EOF
echo -e "${GREEN}✅ Summary report generated${NC}"
# 7. Interactive analysis option
echo -e "\n${YELLOW}🎯 Analysis complete!${NC}"
echo -e "${BLUE}📁 Results saved to: ${REPORT_DIR}${NC}"
echo -e "\n${YELLOW}Quick commands for manual analysis:${NC}"
echo -e " ${BLUE}Interactive engine analysis:${NC} go tool pprof ${REPORT_DIR}/engine_memory.prof"
echo -e " ${BLUE}Interactive renderer analysis:${NC} go tool pprof ${REPORT_DIR}/renderer_memory.prof"
echo -e " ${BLUE}Web interface:${NC} go tool pprof -http=:8080 ${REPORT_DIR}/engine_memory.prof"
# 8. Set baseline option
if [ ! -d "./profiles/baseline" ]; then
echo -e "\n${YELLOW}💡 Tip: Set this as baseline for future comparisons?${NC}"
echo -e " ${BLUE}mkdir -p ./profiles/baseline && cp ${REPORT_DIR}/*.prof ./profiles/baseline/${NC}"
fi
echo -e "\n${GREEN}🎉 Memory analysis completed successfully!${NC}"

362
scripts/memory-leak-test.sh Executable file
View File

@@ -0,0 +1,362 @@
#!/bin/bash
# Memory Leak Detection Script for go-jdenticon
# This script runs extended memory testing and captures heap profiles for leak analysis
set -e
# Configuration
TEST_DURATION="15m"
WORKERS=8
CACHE_SIZE=1000
PPROF_PORT=6060
PROFILE_DIR="./profiles"
SNAPSHOT_INTERVAL="2m"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${BLUE}=== go-jdenticon Memory Leak Detection Test ===${NC}"
echo "Test duration: $TEST_DURATION"
echo "Workers: $WORKERS"
echo "Cache size: $CACHE_SIZE"
echo "Profile interval: $SNAPSHOT_INTERVAL"
echo ""
# Create profiles directory
mkdir -p "$PROFILE_DIR"
cd "$PROFILE_DIR" && rm -f *.prof *.json && cd ..
# Build the memory load test tool
echo -e "${YELLOW}Building memory-load-test tool...${NC}"
go build -o memory-load-test cmd/memory-load-test/main.go
# Start the load test in background
echo -e "${YELLOW}Starting extended memory load test...${NC}"
./memory-load-test \
-duration="$TEST_DURATION" \
-workers="$WORKERS" \
-cache-size="$CACHE_SIZE" \
-pprof-port="$PPROF_PORT" \
-sample-interval="30s" \
-report-interval="1m" \
-varied-inputs=true &
LOAD_TEST_PID=$!
# Function to cleanup on exit
cleanup() {
echo -e "\n${YELLOW}Cleaning up...${NC}"
if kill -0 $LOAD_TEST_PID 2>/dev/null; then
kill $LOAD_TEST_PID
wait $LOAD_TEST_PID 2>/dev/null || true
fi
rm -f memory-load-test
}
trap cleanup EXIT
# Wait for pprof server to start
echo -e "${YELLOW}Waiting for pprof server to start...${NC}"
sleep 5
# Function to capture heap profile
capture_heap_profile() {
local suffix=$1
local gc_param=""
# Force GC for some snapshots to get clean measurements
if [[ "$suffix" == *"gc"* ]]; then
gc_param="?gc=1"
fi
echo -e "${BLUE}Capturing heap profile: $suffix${NC}"
curl -s "http://localhost:$PPROF_PORT/debug/pprof/heap$gc_param" > "$PROFILE_DIR/heap_$suffix.prof"
# Also capture goroutine profile to check for goroutine leaks
curl -s "http://localhost:$PPROF_PORT/debug/pprof/goroutine" > "$PROFILE_DIR/goroutine_$suffix.prof"
# Get current memory stats from pprof
curl -s "http://localhost:$PPROF_PORT/debug/pprof/heap?debug=1" | head -20 > "$PROFILE_DIR/memstats_$suffix.txt"
}
# Function to wait and show progress
wait_with_progress() {
local duration=$1
local message=$2
local seconds=$(echo "$duration" | sed 's/m$//' | awk '{print $1*60}')
echo -e "${YELLOW}$message (${duration})...${NC}"
for ((i=1; i<=seconds; i++)); do
if ((i % 30 == 0)); then
echo -e "${BLUE}Progress: ${i}s / ${seconds}s${NC}"
fi
sleep 1
# Check if load test is still running
if ! kill -0 $LOAD_TEST_PID 2>/dev/null; then
echo -e "${RED}Load test process died unexpectedly!${NC}"
exit 1
fi
done
}
# Capture initial baseline profiles
echo -e "${GREEN}=== Capturing Baseline Profiles ===${NC}"
capture_heap_profile "0min_baseline"
# Wait and capture profiles at intervals
profiles_captured=1
# 2 minute mark
wait_with_progress "2m" "Waiting for 2-minute mark"
capture_heap_profile "2min"
((profiles_captured++))
# 5 minute mark
wait_with_progress "3m" "Waiting for 5-minute mark"
capture_heap_profile "5min_gc"
((profiles_captured++))
# 8 minute mark
wait_with_progress "3m" "Waiting for 8-minute mark"
capture_heap_profile "8min"
((profiles_captured++))
# 10 minute mark (with GC)
wait_with_progress "2m" "Waiting for 10-minute mark"
capture_heap_profile "10min_gc"
((profiles_captured++))
# 13 minute mark
wait_with_progress "3m" "Waiting for 13-minute mark"
capture_heap_profile "13min"
((profiles_captured++))
# Wait for test completion and capture final profile
echo -e "${BLUE}Waiting for test completion...${NC}"
wait $LOAD_TEST_PID
LOAD_TEST_EXIT_CODE=$?
# Capture final profile after test completion
sleep 2
capture_heap_profile "15min_final"
((profiles_captured++))
echo -e "${GREEN}=== Memory Leak Analysis ===${NC}"
echo "Captured $profiles_captured heap profiles"
# Analyze profiles for memory leaks
echo -e "${YELLOW}Analyzing memory growth patterns...${NC}"
# Check if we have go tool pprof available
if ! command -v go &> /dev/null; then
echo -e "${RED}Go toolchain not found - cannot analyze profiles${NC}"
exit 1
fi
# Create leak analysis report
REPORT_FILE="$PROFILE_DIR/MEMORY_LEAK_ANALYSIS_$(date +%Y%m%d_%H%M%S).md"
cat > "$REPORT_FILE" << 'EOF'
# Memory Leak Analysis Report
## Test Configuration
- **Duration**: 15 minutes sustained load
- **Workers**: 8 concurrent goroutines
- **Cache Size**: 1000 entries
- **Input Variation**: Diverse input patterns to stress different code paths
## Profile Collection Points
1. **0min_baseline**: Initial state after startup
2. **2min**: Early runtime behavior
3. **5min_gc**: Mid-test with forced GC
4. **8min**: Sustained load behavior
5. **10min_gc**: Late-test with forced GC
6. **13min**: Near end-of-test
7. **15min_final**: Final state after completion
EOF
# Function to analyze profile comparison
analyze_profile_diff() {
local base_profile=$1
local comp_profile=$2
local period_name=$3
echo -e "${BLUE}Analyzing: $base_profile vs $comp_profile ($period_name)${NC}"
# Add section to report
cat >> "$REPORT_FILE" << EOF
## $period_name Analysis
### Profile Comparison: $(basename "$base_profile" .prof) → $(basename "$comp_profile" .prof)
EOF
# Generate diff analysis and save to report
if go tool pprof -base="$base_profile" "$comp_profile" -top10 -cum >> "$REPORT_FILE" 2>/dev/null; then
cat >> "$REPORT_FILE" << EOF
### Memory Growth Details
\`\`\`
EOF
go tool pprof -base="$base_profile" "$comp_profile" -list=".*" 2>/dev/null | head -50 >> "$REPORT_FILE" || true
cat >> "$REPORT_FILE" << EOF
\`\`\`
EOF
else
cat >> "$REPORT_FILE" << EOF
**⚠️ Profile analysis failed - may indicate inconsistent data or tool issues**
EOF
fi
}
# Perform comparative analysis
analyze_profile_diff "$PROFILE_DIR/heap_0min_baseline.prof" "$PROFILE_DIR/heap_5min_gc.prof" "Early Growth (0-5 min)"
analyze_profile_diff "$PROFILE_DIR/heap_5min_gc.prof" "$PROFILE_DIR/heap_10min_gc.prof" "Mid Growth (5-10 min)"
analyze_profile_diff "$PROFILE_DIR/heap_10min_gc.prof" "$PROFILE_DIR/heap_15min_final.prof" "Late Growth (10-15 min)"
analyze_profile_diff "$PROFILE_DIR/heap_0min_baseline.prof" "$PROFILE_DIR/heap_15min_final.prof" "Overall Growth (0-15 min)"
# Analyze goroutine growth
echo -e "${YELLOW}Analyzing goroutine patterns...${NC}"
cat >> "$REPORT_FILE" << 'EOF'
## Goroutine Analysis
### Goroutine Count Over Time
EOF
for profile in "$PROFILE_DIR"/goroutine_*.prof; do
if [[ -f "$profile" ]]; then
timestamp=$(basename "$profile" .prof | sed 's/goroutine_//')
count=$(go tool pprof -top "$profile" 2>/dev/null | grep "Total:" | awk '{print $2}' || echo "unknown")
echo "- **$timestamp**: $count goroutines" >> "$REPORT_FILE"
fi
done
# Memory stats analysis
echo -e "${YELLOW}Analyzing memory statistics...${NC}"
cat >> "$REPORT_FILE" << 'EOF'
## Memory Statistics Summary
### Heap Memory Usage Over Time
EOF
for memstat in "$PROFILE_DIR"/memstats_*.txt; do
if [[ -f "$memstat" ]]; then
timestamp=$(basename "$memstat" .txt | sed 's/memstats_//')
heap_inuse=$(grep "# HeapInuse" "$memstat" | awk '{print $3}' || echo "unknown")
heap_objects=$(grep "# HeapObjects" "$memstat" | awk '{print $3}' || echo "unknown")
echo "- **$timestamp**: HeapInuse=$heap_inuse, HeapObjects=$heap_objects" >> "$REPORT_FILE"
fi
done
# Load test results analysis
if [[ $LOAD_TEST_EXIT_CODE -eq 0 ]]; then
cat >> "$REPORT_FILE" << 'EOF'
## Load Test Results
✅ **Load test completed successfully** - No crashes or critical errors detected.
EOF
else
cat >> "$REPORT_FILE" << EOF
## Load Test Results
❌ **Load test failed with exit code $LOAD_TEST_EXIT_CODE** - May indicate memory issues or resource exhaustion.
EOF
fi
# Generate conclusions
cat >> "$REPORT_FILE" << 'EOF'
## Analysis Conclusions
### Memory Leak Assessment
EOF
# Try to detect obvious memory leaks from profile sizes
baseline_size=$(stat -c%s "$PROFILE_DIR/heap_0min_baseline.prof" 2>/dev/null || echo "0")
final_size=$(stat -c%s "$PROFILE_DIR/heap_15min_final.prof" 2>/dev/null || echo "0")
if [[ $baseline_size -gt 0 && $final_size -gt 0 ]]; then
size_growth_ratio=$(echo "scale=2; $final_size / $baseline_size" | bc -l 2>/dev/null || echo "unknown")
cat >> "$REPORT_FILE" << EOF
- **Profile Size Growth**: ${size_growth_ratio}x (${baseline_size} → ${final_size} bytes)
EOF
if (( $(echo "$size_growth_ratio > 3.0" | bc -l 2>/dev/null || echo "0") )); then
cat >> "$REPORT_FILE" << EOF
- ⚠️ **High profile growth** - Indicates potential memory accumulation
EOF
elif (( $(echo "$size_growth_ratio > 1.5" | bc -l 2>/dev/null || echo "0") )); then
cat >> "$REPORT_FILE" << EOF
- ⚠️ **Moderate profile growth** - Monitor for sustained increase trends
EOF
else
cat >> "$REPORT_FILE" << EOF
- ✅ **Reasonable profile growth** - Within expected bounds for sustained load
EOF
fi
fi
cat >> "$REPORT_FILE" << 'EOF'
### Recommendations
1. **Profile Inspection**: Use `go tool pprof -http=:8080 profiles/heap_15min_final.prof` for interactive analysis
2. **Comparative Analysis**: Run `go tool pprof -base=profiles/heap_0min_baseline.prof profiles/heap_15min_final.prof` for detailed diff
3. **Goroutine Monitoring**: Check `go tool pprof profiles/goroutine_15min_final.prof` for goroutine leaks
4. **Long-term Testing**: Consider running tests for 1+ hours to detect slow leaks
### Commands for Manual Analysis
```bash
# Interactive heap analysis
go tool pprof -http=:8080 profiles/heap_15min_final.prof
# Memory growth analysis
go tool pprof -base=profiles/heap_0min_baseline.prof profiles/heap_15min_final.prof
# Goroutine analysis
go tool pprof profiles/goroutine_15min_final.prof
# View memory stats
cat profiles/memstats_15min_final.txt
```
EOF
echo -e "${GREEN}=== Memory Leak Test Complete ===${NC}"
echo -e "${BLUE}Results:${NC}"
echo " - Profiles captured: $profiles_captured"
echo " - Profile directory: $PROFILE_DIR"
echo " - Analysis report: $REPORT_FILE"
echo " - Load test exit code: $LOAD_TEST_EXIT_CODE"
if [[ $LOAD_TEST_EXIT_CODE -eq 0 ]]; then
echo -e "${GREEN} ✅ Load test completed successfully${NC}"
else
echo -e "${RED} ❌ Load test failed (exit code: $LOAD_TEST_EXIT_CODE)${NC}"
fi
echo ""
echo -e "${YELLOW}Next steps:${NC}"
echo "1. Review the analysis report: cat $REPORT_FILE"
echo "2. Interactive analysis: go tool pprof -http=:8080 $PROFILE_DIR/heap_15min_final.prof"
echo "3. Compare profiles: go tool pprof -base=$PROFILE_DIR/heap_0min_baseline.prof $PROFILE_DIR/heap_15min_final.prof"
# Final cleanup handled by trap

390
scripts/memory-profile.sh Executable file
View File

@@ -0,0 +1,390 @@
#!/bin/bash
# Memory profiling script for go-jdenticon
# This script provides easy-to-use commands for profiling memory usage
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
PROFILES_DIR="$PROJECT_ROOT/profiles"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Ensure profiles directory exists
mkdir -p "$PROFILES_DIR"
usage() {
cat << EOF
Usage: $0 [COMMAND] [OPTIONS]
Memory profiling commands for go-jdenticon:
COMMANDS:
benchmark Run benchmarks with memory profiling
load-test Run sustained load test with profiling
analyze-heap Analyze existing heap profile
analyze-cpu Analyze existing CPU profile
web-ui Start web UI for profile analysis
continuous Run continuous profiling session
compare Compare two profile files
OPTIONS:
--duration=5m Test duration (default: 5m)
--workers=4 Number of workers (default: 4)
--cache=1000 Cache size (default: 1000)
--output=FILE Output profile file
--baseline=FILE Baseline profile for comparison
EXAMPLES:
$0 benchmark --duration=10m
$0 load-test --workers=8 --cache=500
$0 analyze-heap profiles/heap-latest.prof
$0 continuous --duration=1h
$0 compare profiles/baseline.prof profiles/after-fix.prof
PROFILING WORKFLOW:
1. Run baseline: $0 benchmark --output=baseline
2. Make changes to code
3. Run comparison: $0 benchmark --output=after-changes
4. Compare results: $0 compare profiles/baseline.prof profiles/after-changes.prof
EOF
}
log() {
echo -e "${GREEN}[$(date +'%H:%M:%S')]${NC} $1"
}
warn() {
echo -e "${YELLOW}[$(date +'%H:%M:%S')] WARNING:${NC} $1"
}
error() {
echo -e "${RED}[$(date +'%H:%M:%S')] ERROR:${NC} $1"
exit 1
}
# Parse command line arguments
COMMAND=""
DURATION="5m"
WORKERS="4"
CACHE_SIZE="1000"
OUTPUT_FILE=""
BASELINE_FILE=""
while [[ $# -gt 0 ]]; do
case $1 in
benchmark|load-test|analyze-heap|analyze-cpu|web-ui|continuous|compare)
COMMAND="$1"
shift
;;
--duration=*)
DURATION="${1#*=}"
shift
;;
--workers=*)
WORKERS="${1#*=}"
shift
;;
--cache=*)
CACHE_SIZE="${1#*=}"
shift
;;
--output=*)
OUTPUT_FILE="${1#*=}"
shift
;;
--baseline=*)
BASELINE_FILE="${1#*=}"
shift
;;
-h|--help)
usage
exit 0
;;
*)
if [[ -z "$COMMAND" ]]; then
COMMAND="$1"
else
error "Unknown argument: $1"
fi
shift
;;
esac
done
if [[ -z "$COMMAND" ]]; then
usage
exit 1
fi
# Generate timestamp for output files
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
run_benchmark() {
log "Running memory leak validation benchmarks..."
if [[ -n "$OUTPUT_FILE" ]]; then
HEAP_PROF="$PROFILES_DIR/${OUTPUT_FILE}_heap.prof"
CPU_PROF="$PROFILES_DIR/${OUTPUT_FILE}_cpu.prof"
MEM_PROF="$PROFILES_DIR/${OUTPUT_FILE}_mem.prof"
else
HEAP_PROF="$PROFILES_DIR/benchmark_${TIMESTAMP}_heap.prof"
CPU_PROF="$PROFILES_DIR/benchmark_${TIMESTAMP}_cpu.prof"
MEM_PROF="$PROFILES_DIR/benchmark_${TIMESTAMP}_mem.prof"
fi
cd "$PROJECT_ROOT"
log "Memory profiling benchmark with duration: $DURATION"
log "Output files: heap=$HEAP_PROF, cpu=$CPU_PROF, mem=$MEM_PROF"
# Run memory leak benchmarks with profiling
go test -bench=BenchmarkMemoryLeak \
-benchtime="$DURATION" \
-memprofile="$MEM_PROF" \
-memprofilerate=1 \
-cpuprofile="$CPU_PROF" \
-benchmem \
./jdenticon
# Capture heap profile at the end
if pgrep -f "go.*test.*BenchmarkMemoryLeak" > /dev/null; then
warn "Test process still running, heap profile may not be available"
fi
log "Benchmark completed. Profile files saved:"
log " Memory: $MEM_PROF"
log " CPU: $CPU_PROF"
# Create symlinks to latest profiles
ln -sf "$(basename "$MEM_PROF")" "$PROFILES_DIR/mem-latest.prof"
ln -sf "$(basename "$CPU_PROF")" "$PROFILES_DIR/cpu-latest.prof"
log "Analysis commands:"
log " go tool pprof $MEM_PROF"
log " go tool pprof -http=:8080 $MEM_PROF"
}
run_load_test() {
log "Running sustained load test with profiling..."
cd "$PROJECT_ROOT"
# Build the load test binary if it doesn't exist
if [[ ! -f "cmd/memory-load-test/memory-load-test" ]]; then
log "Building memory load test binary..."
go build -o cmd/memory-load-test/memory-load-test ./cmd/memory-load-test
fi
HEAP_PROF="$PROFILES_DIR/loadtest_${TIMESTAMP}_heap.prof"
CPU_PROF="$PROFILES_DIR/loadtest_${TIMESTAMP}_cpu.prof"
log "Starting load test: duration=$DURATION, workers=$WORKERS, cache=$CACHE_SIZE"
log "pprof server will be available at http://localhost:6060/debug/pprof/"
log "Use Ctrl+C to stop the test gracefully"
# Start the load test in background
./cmd/memory-load-test/memory-load-test \
-duration="$DURATION" \
-workers="$WORKERS" \
-cache-size="$CACHE_SIZE" \
-pprof-port=6060 &
LOAD_TEST_PID=$!
# Give it time to start
sleep 5
log "Load test started with PID $LOAD_TEST_PID"
log "Monitoring will capture profiles every 30 seconds"
# Wait for the test to complete or user interrupt
trap "kill $LOAD_TEST_PID 2>/dev/null; wait $LOAD_TEST_PID 2>/dev/null" EXIT
wait $LOAD_TEST_PID || true
log "Load test completed"
}
analyze_heap() {
local profile_file="$1"
if [[ -z "$profile_file" ]]; then
profile_file="$PROFILES_DIR/mem-latest.prof"
fi
if [[ ! -f "$profile_file" ]]; then
error "Profile file not found: $profile_file"
fi
log "Analyzing heap profile: $profile_file"
# Interactive analysis
echo -e "\n${BLUE}Starting interactive pprof session...${NC}"
echo -e "${BLUE}Useful commands:${NC}"
echo " top - Show top memory allocations"
echo " top -cum - Show top cumulative allocations"
echo " list <func> - Show source code for function"
echo " web - Open web interface"
echo " svg - Generate SVG call graph"
echo " png - Generate PNG call graph"
echo " traces - Show allocation traces"
echo " help - Show all commands"
echo ""
go tool pprof "$profile_file"
}
analyze_cpu() {
local profile_file="$1"
if [[ -z "$profile_file" ]]; then
profile_file="$PROFILES_DIR/cpu-latest.prof"
fi
if [[ ! -f "$profile_file" ]]; then
error "Profile file not found: $profile_file"
fi
log "Analyzing CPU profile: $profile_file"
go tool pprof "$profile_file"
}
start_web_ui() {
local profile_file="$1"
if [[ -z "$profile_file" ]]; then
profile_file="$PROFILES_DIR/mem-latest.prof"
fi
if [[ ! -f "$profile_file" ]]; then
error "Profile file not found: $profile_file"
fi
log "Starting web UI for profile: $profile_file"
log "Open http://localhost:8080 in your browser"
go tool pprof -http=:8080 "$profile_file"
}
run_continuous() {
log "Running continuous profiling session for $DURATION"
cd "$PROJECT_ROOT"
# Build load test if needed
if [[ ! -f "cmd/memory-load-test/memory-load-test" ]]; then
log "Building memory load test binary..."
go build -o cmd/memory-load-test/memory-load-test ./cmd/memory-load-test
fi
log "Starting continuous load test..."
# Start load test in background
./cmd/memory-load-test/memory-load-test \
-duration="$DURATION" \
-workers="$WORKERS" \
-cache-size="$CACHE_SIZE" \
-pprof-port=6060 \
-varied-inputs=true &
LOAD_TEST_PID=$!
sleep 3
log "Load test PID: $LOAD_TEST_PID"
log "pprof endpoints available at http://localhost:6060/debug/pprof/"
# Capture profiles at intervals
INTERVAL=300 # 5 minutes
PROFILES_CAPTURED=0
while kill -0 $LOAD_TEST_PID 2>/dev/null; do
sleep $INTERVAL
PROFILES_CAPTURED=$((PROFILES_CAPTURED + 1))
SNAPSHOT_TIME=$(date +"%H%M%S")
log "Capturing profile snapshot $PROFILES_CAPTURED at $SNAPSHOT_TIME"
# Capture heap profile
curl -s "http://localhost:6060/debug/pprof/heap" > \
"$PROFILES_DIR/continuous_${TIMESTAMP}_${PROFILES_CAPTURED}_heap.prof" || true
# Capture goroutine profile
curl -s "http://localhost:6060/debug/pprof/goroutine" > \
"$PROFILES_DIR/continuous_${TIMESTAMP}_${PROFILES_CAPTURED}_goroutine.prof" || true
log " Saved heap and goroutine profiles"
done
log "Continuous profiling completed. Captured $PROFILES_CAPTURED snapshots."
log "Profile files are in: $PROFILES_DIR/"
}
compare_profiles() {
local baseline="$1"
local current="$2"
if [[ -z "$baseline" || -z "$current" ]]; then
error "Usage: $0 compare <baseline_profile> <current_profile>"
fi
if [[ ! -f "$baseline" ]]; then
error "Baseline profile not found: $baseline"
fi
if [[ ! -f "$current" ]]; then
error "Current profile not found: $current"
fi
log "Comparing profiles:"
log " Baseline: $baseline"
log " Current: $current"
# Use pprof to compare profiles
echo -e "\n${BLUE}Memory allocation differences:${NC}"
go tool pprof -base="$baseline" -top "$current"
echo -e "\n${BLUE}Interactive comparison session:${NC}"
echo "Useful commands:"
echo " top - Show top differences"
echo " top -cum - Show cumulative differences"
echo " web - Visual diff in browser"
go tool pprof -base="$baseline" "$current"
}
# Execute the requested command
case "$COMMAND" in
benchmark)
run_benchmark
;;
load-test)
run_load_test
;;
analyze-heap)
analyze_heap "$2"
;;
analyze-cpu)
analyze_cpu "$2"
;;
web-ui)
start_web_ui "$2"
;;
continuous)
run_continuous
;;
compare)
compare_profiles "$2" "$3"
;;
*)
error "Unknown command: $COMMAND"
;;
esac