Skip to content

Instantly share code, notes, and snippets.

@brianmichel
Created August 22, 2025 19:44
Show Gist options
  • Select an option

  • Save brianmichel/43026aeeba388695898c5d052ad2c7f6 to your computer and use it in GitHub Desktop.

Select an option

Save brianmichel/43026aeeba388695898c5d052ad2c7f6 to your computer and use it in GitHub Desktop.
#!/bin/bash
# Comprehensive Disk Benchmark Suite using Hyperfine
# Compares SSD, APFS RAM Disk, and HFS+ RAM Disk performance
set -e
# Color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
MAGENTA='\033[0;35m'
CYAN='\033[0;36m'
NC='\033[0m'
# Configuration
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
RESULTS_DIR="benchmark_results_${TIMESTAMP}"
RAMDISK_SIZE=8388608 # 4GB
TEST_FILE_SIZE="100M"
NUM_SMALL_FILES=1000
echo -e "${GREEN}=== Hyperfine Disk Benchmark Suite ===${NC}"
echo -e "${YELLOW}Results will be saved to: ${RESULTS_DIR}${NC}\n"
# Check for required tools
check_requirements() {
echo -e "${CYAN}Checking requirements...${NC}"
if ! command -v hyperfine &> /dev/null; then
echo -e "${RED}hyperfine not found. Installing...${NC}"
brew install hyperfine
fi
if ! command -v fio &> /dev/null; then
echo -e "${RED}fio not found. Installing...${NC}"
brew install fio
fi
echo -e "${GREEN}All requirements satisfied${NC}\n"
}
# Create results directory
mkdir -p "$RESULTS_DIR"
# Setup test directories
setup_test_dirs() {
echo -e "${CYAN}Setting up test directories...${NC}"
# SSD test directory
SSD_TEST_DIR="/tmp/benchmark_ssd"
mkdir -p "$SSD_TEST_DIR"
# Create APFS RAM disk
echo "Creating APFS RAM disk..."
APFS_DEV=$(hdiutil attach -nomount ram://$RAMDISK_SIZE)
diskutil erasevolume APFS "BenchAPFS" $APFS_DEV > /dev/null 2>&1
APFS_TEST_DIR="/Volumes/BenchAPFS/test"
mkdir -p "$APFS_TEST_DIR"
# Create HFS+ RAM disk
echo "Creating HFS+ RAM disk..."
HFS_DEV=$(hdiutil attach -nomount ram://$RAMDISK_SIZE)
diskutil erasevolume HFS+ "BenchHFS" $HFS_DEV > /dev/null 2>&1
HFS_TEST_DIR="/Volumes/BenchHFS/test"
mkdir -p "$HFS_TEST_DIR"
echo -e "${GREEN}Test directories ready${NC}\n"
# Export for use in tests
export SSD_TEST_DIR APFS_TEST_DIR HFS_TEST_DIR
}
# Cleanup function
cleanup() {
echo -e "\n${YELLOW}Cleaning up...${NC}"
rm -rf "$SSD_TEST_DIR"
if mount | grep -q "BenchAPFS"; then
diskutil eject /Volumes/BenchAPFS > /dev/null 2>&1
fi
if mount | grep -q "BenchHFS"; then
diskutil eject /Volumes/BenchHFS > /dev/null 2>&1
fi
}
trap cleanup EXIT
# Test 1: FIO Random Read (simplified without job file)
test_fio_random_read() {
echo -e "${BLUE}=== Test 1: FIO Random 4K Read ===${NC}"
# Create wrapper scripts for FIO
cat > "$RESULTS_DIR/fio_read_ssd.sh" << EOF
#!/bin/bash
fio --name=randread --rw=randread --bs=4k --numjobs=4 --iodepth=32 \
--size=256M --runtime=5 --time_based --ioengine=posixaio \
--group_reporting --minimal --directory=$SSD_TEST_DIR 2>/dev/null
EOF
cat > "$RESULTS_DIR/fio_read_apfs.sh" << EOF
#!/bin/bash
fio --name=randread --rw=randread --bs=4k --numjobs=4 --iodepth=32 \
--size=256M --runtime=5 --time_based --ioengine=posixaio \
--group_reporting --minimal --directory=$APFS_TEST_DIR 2>/dev/null
EOF
cat > "$RESULTS_DIR/fio_read_hfs.sh" << EOF
#!/bin/bash
fio --name=randread --rw=randread --bs=4k --numjobs=4 --iodepth=32 \
--size=256M --runtime=5 --time_based --ioengine=posixaio \
--group_reporting --minimal --directory=$HFS_TEST_DIR 2>/dev/null
EOF
chmod +x "$RESULTS_DIR"/fio_read_*.sh
# Run with hyperfine, ignoring FIO exit codes
hyperfine \
--warmup 1 \
--runs 3 \
--ignore-failure \
--export-json "$RESULTS_DIR/fio_randread.json" \
--export-markdown "$RESULTS_DIR/fio_randread.md" \
--command-name 'SSD' \
"$RESULTS_DIR/fio_read_ssd.sh" \
--command-name 'APFS-RAM' \
"$RESULTS_DIR/fio_read_apfs.sh" \
--command-name 'HFS+-RAM' \
"$RESULTS_DIR/fio_read_hfs.sh"
echo ""
}
# Test 2: FIO Random Write (simplified)
test_fio_random_write() {
echo -e "${BLUE}=== Test 2: FIO Random 4K Write ===${NC}"
# Create wrapper scripts for FIO
cat > "$RESULTS_DIR/fio_write_ssd.sh" << EOF
#!/bin/bash
fio --name=randwrite --rw=randwrite --bs=4k --numjobs=4 --iodepth=32 \
--size=256M --runtime=5 --time_based --ioengine=posixaio \
--group_reporting --minimal --directory=$SSD_TEST_DIR 2>/dev/null
EOF
cat > "$RESULTS_DIR/fio_write_apfs.sh" << EOF
#!/bin/bash
fio --name=randwrite --rw=randwrite --bs=4k --numjobs=4 --iodepth=32 \
--size=256M --runtime=5 --time_based --ioengine=posixaio \
--group_reporting --minimal --directory=$APFS_TEST_DIR 2>/dev/null
EOF
cat > "$RESULTS_DIR/fio_write_hfs.sh" << EOF
#!/bin/bash
fio --name=randwrite --rw=randwrite --bs=4k --numjobs=4 --iodepth=32 \
--size=256M --runtime=5 --time_based --ioengine=posixaio \
--group_reporting --minimal --directory=$HFS_TEST_DIR 2>/dev/null
EOF
chmod +x "$RESULTS_DIR"/fio_write_*.sh
hyperfine \
--warmup 1 \
--runs 3 \
--ignore-failure \
--export-json "$RESULTS_DIR/fio_randwrite.json" \
--export-markdown "$RESULTS_DIR/fio_randwrite.md" \
--command-name 'SSD' \
"$RESULTS_DIR/fio_write_ssd.sh" \
--command-name 'APFS-RAM' \
"$RESULTS_DIR/fio_write_apfs.sh" \
--command-name 'HFS+-RAM' \
"$RESULTS_DIR/fio_write_hfs.sh"
echo ""
}
# Test 3: Small File Creation (Xcode-like)
test_small_file_creation() {
echo -e "${BLUE}=== Test 3: Create $NUM_SMALL_FILES Small Files ===${NC}"
# Create test scripts
cat > "$RESULTS_DIR/create_files.sh" << 'EOF'
#!/bin/bash
DIR=$1
mkdir -p "$DIR"
for i in $(seq 1 1000); do
echo "#include <stdio.h>" > "$DIR/file_$i.h"
done
EOF
chmod +x "$RESULTS_DIR/create_files.sh"
hyperfine \
--warmup 1 \
--runs 5 \
--prepare "rm -rf $SSD_TEST_DIR/file_* $APFS_TEST_DIR/file_* $HFS_TEST_DIR/file_*" \
--export-json "$RESULTS_DIR/small_files.json" \
--export-markdown "$RESULTS_DIR/small_files.md" \
--command-name 'SSD' \
"$RESULTS_DIR/create_files.sh $SSD_TEST_DIR" \
--command-name 'APFS-RAM' \
"$RESULTS_DIR/create_files.sh $APFS_TEST_DIR" \
--command-name 'HFS+-RAM' \
"$RESULTS_DIR/create_files.sh $HFS_TEST_DIR"
echo ""
}
# Test 4: Compilation Simulation
test_compilation_sim() {
echo -e "${BLUE}=== Test 4: Compilation I/O Pattern ===${NC}"
cat > "$RESULTS_DIR/compile_sim.sh" << 'EOF'
#!/bin/bash
DIR=$1
mkdir -p "$DIR"
# Simulate reading headers
for i in $(seq 1 100); do
if [ -f "$DIR/file_$i.h" ]; then
cat "$DIR/file_$i.h" > /dev/null 2>&1
else
echo "test" > "$DIR/file_$i.h"
fi
done
# Simulate writing object files
for i in $(seq 1 50); do
dd if=/dev/zero of="$DIR/obj_$i.o" bs=64k count=1 2>/dev/null
done
# Simulate linking (large sequential write)
dd if=/dev/zero of="$DIR/binary" bs=1M count=10 2>/dev/null
EOF
chmod +x "$RESULTS_DIR/compile_sim.sh"
hyperfine \
--warmup 2 \
--runs 5 \
--prepare "rm -rf $SSD_TEST_DIR/* $APFS_TEST_DIR/* $HFS_TEST_DIR/*" \
--export-json "$RESULTS_DIR/compile_sim.json" \
--export-markdown "$RESULTS_DIR/compile_sim.md" \
--command-name 'SSD' \
"$RESULTS_DIR/compile_sim.sh $SSD_TEST_DIR" \
--command-name 'APFS-RAM' \
"$RESULTS_DIR/compile_sim.sh $APFS_TEST_DIR" \
--command-name 'HFS+-RAM' \
"$RESULTS_DIR/compile_sim.sh $HFS_TEST_DIR"
echo ""
}
# Test 5: Directory Operations
test_directory_ops() {
echo -e "${BLUE}=== Test 5: Directory Operations ===${NC}"
cat > "$RESULTS_DIR/dir_ops.sh" << 'EOF'
#!/bin/bash
DIR=$1
mkdir -p "$DIR"
# Create nested directories
for i in $(seq 1 50); do
mkdir -p "$DIR/level1/level2/level3/dir_$i"
done
# List directories
find "$DIR" -type d > /dev/null
# Remove directories
rm -rf "$DIR/level1"
EOF
chmod +x "$RESULTS_DIR/dir_ops.sh"
hyperfine \
--warmup 1 \
--runs 5 \
--prepare "rm -rf $SSD_TEST_DIR/* $APFS_TEST_DIR/* $HFS_TEST_DIR/*" \
--export-json "$RESULTS_DIR/dir_ops.json" \
--export-markdown "$RESULTS_DIR/dir_ops.md" \
--command-name 'SSD' \
"$RESULTS_DIR/dir_ops.sh $SSD_TEST_DIR" \
--command-name 'APFS-RAM' \
"$RESULTS_DIR/dir_ops.sh $APFS_TEST_DIR" \
--command-name 'HFS+-RAM' \
"$RESULTS_DIR/dir_ops.sh $HFS_TEST_DIR"
echo ""
}
# Test 6: Large File Sequential Write
test_sequential_write() {
echo -e "${BLUE}=== Test 6: Sequential Write (1GB) ===${NC}"
hyperfine \
--warmup 1 \
--runs 3 \
--prepare "rm -f $SSD_TEST_DIR/large $APFS_TEST_DIR/large $HFS_TEST_DIR/large" \
--export-json "$RESULTS_DIR/seq_write.json" \
--export-markdown "$RESULTS_DIR/seq_write.md" \
--command-name 'SSD' \
"dd if=/dev/zero of=$SSD_TEST_DIR/large bs=1M count=1024 2>/dev/null" \
--command-name 'APFS-RAM' \
"dd if=/dev/zero of=$APFS_TEST_DIR/large bs=1M count=1024 2>/dev/null" \
--command-name 'HFS+-RAM' \
"dd if=/dev/zero of=$HFS_TEST_DIR/large bs=1M count=1024 2>/dev/null"
echo ""
}
# Test 7: Simple DD-based tests (more reliable)
test_simple_io() {
echo -e "${BLUE}=== Test 7: Simple I/O Tests ===${NC}"
# 4K random read simulation
echo -e "${CYAN}4K Random Reads (1000 operations)${NC}"
cat > "$RESULTS_DIR/random_read.sh" << 'EOF'
#!/bin/bash
DIR=$1
# Create test file if it doesn't exist
if [ ! -f "$DIR/testfile" ]; then
dd if=/dev/zero of="$DIR/testfile" bs=1M count=100 2>/dev/null
fi
# Random reads
for i in $(seq 1 1000); do
dd if="$DIR/testfile" of=/dev/null bs=4k count=1 skip=$((RANDOM % 1000)) 2>/dev/null
done
EOF
chmod +x "$RESULTS_DIR/random_read.sh"
hyperfine \
--warmup 1 \
--runs 3 \
--export-json "$RESULTS_DIR/simple_randread.json" \
--export-markdown "$RESULTS_DIR/simple_randread.md" \
--command-name 'SSD' \
"$RESULTS_DIR/random_read.sh $SSD_TEST_DIR" \
--command-name 'APFS-RAM' \
"$RESULTS_DIR/random_read.sh $APFS_TEST_DIR" \
--command-name 'HFS+-RAM' \
"$RESULTS_DIR/random_read.sh $HFS_TEST_DIR"
echo ""
}
# Generate combined report
generate_report() {
echo -e "${MAGENTA}=== Generating Combined Report ===${NC}"
cat > "$RESULTS_DIR/report.md" << EOF
# Disk Benchmark Results
**Date:** $(date)
**System:** $(uname -a)
**Test Locations:**
- SSD: $SSD_TEST_DIR
- APFS RAM: $APFS_TEST_DIR
- HFS+ RAM: $HFS_TEST_DIR
## Summary
EOF
# Append all markdown results
for md_file in "$RESULTS_DIR"/*.md; do
if [ "$md_file" != "$RESULTS_DIR/report.md" ]; then
echo "### $(basename $md_file .md)" >> "$RESULTS_DIR/report.md"
cat "$md_file" >> "$RESULTS_DIR/report.md"
echo "" >> "$RESULTS_DIR/report.md"
fi
done
# Create CSV summary
echo -e "${CYAN}Creating CSV summary...${NC}"
cat > "$RESULTS_DIR/summary.csv" << EOF
Test,SSD_Mean,APFS_RAM_Mean,HFS_RAM_Mean,Unit
EOF
# Parse JSON results for CSV (requires jq)
if command -v jq &> /dev/null; then
for json_file in "$RESULTS_DIR"/*.json; do
if [ -f "$json_file" ]; then
TEST_NAME=$(basename "$json_file" .json)
SSD_MEAN=$(jq '.results[0].mean' "$json_file" 2>/dev/null || echo "N/A")
APFS_MEAN=$(jq '.results[1].mean' "$json_file" 2>/dev/null || echo "N/A")
HFS_MEAN=$(jq '.results[2].mean' "$json_file" 2>/dev/null || echo "N/A")
echo "$TEST_NAME,$SSD_MEAN,$APFS_MEAN,$HFS_MEAN,seconds" >> "$RESULTS_DIR/summary.csv"
fi
done
fi
echo -e "${GREEN}Report saved to: $RESULTS_DIR/report.md${NC}"
echo -e "${GREEN}CSV summary saved to: $RESULTS_DIR/summary.csv${NC}"
}
# Debug function to test individual components
debug_test() {
echo -e "${YELLOW}=== Debug Mode ===${NC}"
echo "Testing FIO installation..."
which fio || echo "FIO not found!"
fio --version || echo "FIO version check failed!"
echo -e "\nTesting directories:"
echo "SSD_TEST_DIR: $SSD_TEST_DIR"
ls -la "$SSD_TEST_DIR" 2>/dev/null || echo "SSD dir not accessible"
echo "APFS_TEST_DIR: $APFS_TEST_DIR"
ls -la "$APFS_TEST_DIR" 2>/dev/null || echo "APFS dir not accessible"
echo "HFS_TEST_DIR: $HFS_TEST_DIR"
ls -la "$HFS_TEST_DIR" 2>/dev/null || echo "HFS dir not accessible"
echo -e "\nTrying simple FIO command:"
fio --name=test --rw=randread --bs=4k --size=10M --runtime=1 \
--directory="$SSD_TEST_DIR" || echo "Simple FIO test failed"
}
# Main execution
main() {
check_requirements
setup_test_dirs
# Optional debug mode
if [ "$1" == "--debug" ]; then
debug_test
exit 0
fi
# Run tests based on arguments or all
if [ "$1" == "--quick" ]; then
echo -e "${YELLOW}Running quick tests only...${NC}"
test_small_file_creation
test_compilation_sim
test_simple_io
else
# Run all tests
test_fio_random_read
test_fio_random_write
test_small_file_creation
test_compilation_sim
test_directory_ops
test_sequential_write
test_simple_io
fi
# Generate report
generate_report
echo -e "\n${GREEN}=== All Benchmarks Complete ===${NC}"
echo -e "${YELLOW}Results saved in: $RESULTS_DIR/${NC}"
echo -e "${CYAN}View the report: cat $RESULTS_DIR/report.md${NC}"
echo -e "${CYAN}View CSV summary: cat $RESULTS_DIR/summary.csv${NC}"
# Show quick summary
echo -e "\n${MAGENTA}Quick Summary:${NC}"
if [ -f "$RESULTS_DIR/small_files.md" ]; then
echo -e "${YELLOW}Small File Creation Results:${NC}"
tail -n 4 "$RESULTS_DIR/small_files.md"
fi
}
# Run main
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment