Add performance profiling tools and test data generator
Add Xdebug configuration and container setup for performance profiling Add comprehensive performance testing script with profiling and test data generation Add test data statistics and cleanup commands Add documentation for all performance testing features
This commit is contained in:
parent
47cf4d8652
commit
ccfca94b57
3 changed files with 338 additions and 1 deletions
14
devel/Containerfile
Normal file
14
devel/Containerfile
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
FROM php:8.4.14-apache
|
||||
|
||||
# Install Xdebug for performance profiling
|
||||
RUN pecl install xdebug-3.5.0 && \
|
||||
docker-php-ext-enable xdebug
|
||||
|
||||
# Configure Xdebug for profiling
|
||||
RUN echo "xdebug.mode=develop,profile" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini && \
|
||||
echo "xdebug.output_dir=/tmp" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini && \
|
||||
echo "xdebug.start_with_request=trigger" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini && \
|
||||
echo "xdebug.profiler_output_name=cachegrind.out.%p" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
|
||||
|
||||
# Note: To trigger profiling, add ?XDEBUG_PROFILE=1 to any URL
|
||||
# Profile files will be generated in /tmp/ inside the container
|
||||
|
|
@ -15,7 +15,7 @@ services:
|
|||
# - "4040:80"
|
||||
# command: bash -c "a2enconf custom && a2enmod rewrite && apache2-foreground"
|
||||
default:
|
||||
image: php:8.4.14-apache
|
||||
build: ./
|
||||
container_name: folderweb-default
|
||||
working_dir: /var/www/html/
|
||||
volumes:
|
||||
|
|
|
|||
323
devel/perf.sh
Executable file
323
devel/perf.sh
Executable file
|
|
@ -0,0 +1,323 @@
|
|||
#!/bin/bash
|
||||
# FolderWeb Performance Testing Tool
|
||||
# All-in-one script for profiling, test data generation, and analysis
|
||||
|
||||
set -e
|
||||
|
||||
CONTAINER="${CONTAINER:-folderweb-default}"
|
||||
PORT="${PORT:-8080}"
|
||||
ANALYZER="/tmp/analyze_profile.php"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
show_help() {
|
||||
cat << EOF
|
||||
FolderWeb Performance Testing Tool
|
||||
|
||||
SETUP (First Time):
|
||||
cd devel && podman-compose build && podman-compose up -d
|
||||
|
||||
PROFILING COMMANDS:
|
||||
profile <url> Profile a URL (add ?XDEBUG_PROFILE=1 and analyze)
|
||||
analyze [file] Analyze a cachegrind file (latest if not specified)
|
||||
list List all available cachegrind files
|
||||
clean Remove all cachegrind profile files
|
||||
|
||||
TEST DATA COMMANDS:
|
||||
generate <size> Generate synthetic test data in /tmp/test-content
|
||||
Sizes: small (~100 posts), medium (~500), large (~1500), huge (~5000+)
|
||||
generate custom N M D Custom: N categories, M posts/cat, D depth
|
||||
testdata-stats Show test data statistics
|
||||
testdata-clean Remove test data from container
|
||||
|
||||
USING TEST DATA:
|
||||
Test data is generated at /tmp/test-content inside the container.
|
||||
|
||||
Option 1 - Symlink (quick):
|
||||
podman exec folderweb-default ln -s /tmp/test-content /var/www/html
|
||||
|
||||
Option 2 - Edit router.php (temporary):
|
||||
Change \$ctx->contentDir to '/tmp/test-content'
|
||||
|
||||
EXAMPLES:
|
||||
$0 profile / # Profile homepage
|
||||
$0 generate medium # Generate ~500 posts
|
||||
$0 testdata-stats # View stats
|
||||
$0 profile /blog-321/ # Profile test data
|
||||
|
||||
# Stress test with huge dataset
|
||||
$0 generate huge && $0 profile /
|
||||
|
||||
# Export for KCachegrind
|
||||
podman cp folderweb-default:/tmp/cachegrind.out.123 ./profile.out
|
||||
kcachegrind ./profile.out
|
||||
|
||||
ENVIRONMENT VARIABLES:
|
||||
CONTAINER=name Target container (default: folderweb-default)
|
||||
PORT=port Target port (default: 8080)
|
||||
|
||||
RESULTS:
|
||||
Focus on functions taking >5% of execution time.
|
||||
Time (ms) = total time spent | Memory (KB) = total allocated
|
||||
Calls = invocation count | Percentage = % of total time
|
||||
EOF
|
||||
}
|
||||
|
||||
ensure_analyzer() {
|
||||
if ! podman exec "$CONTAINER" test -f "$ANALYZER" 2>/dev/null; then
|
||||
echo -e "${YELLOW}Installing profile analyzer...${NC}" >&2
|
||||
podman exec "$CONTAINER" bash -c "cat > $ANALYZER" << 'EOFANALYZER'
|
||||
<?php
|
||||
if ($argc < 2) { echo "Usage: php analyze_profile.php <cachegrind_file>\n"; exit(1); }
|
||||
$file = $argv[1];
|
||||
if (!file_exists($file)) { echo "Error: File not found: $file\n"; exit(1); }
|
||||
|
||||
$lines = file($file);
|
||||
$functions = [];
|
||||
$currentFile = '';
|
||||
$currentFunction = '';
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if (str_starts_with($line, 'fl=')) {
|
||||
preg_match('/fl=\(?\d*\)?\s*(.+)/', $line, $matches);
|
||||
$currentFile = $matches[1] ?? '';
|
||||
} elseif (str_starts_with($line, 'fn=')) {
|
||||
preg_match('/fn=\(?\d*\)?\s*(.+)/', $line, $matches);
|
||||
$currentFunction = $matches[1] ?? '';
|
||||
} elseif (preg_match('/^(\d+)\s+(\d+)\s+(\d+)/', $line, $matches)) {
|
||||
$time = (int)$matches[2];
|
||||
$memory = (int)$matches[3];
|
||||
if ($currentFunction && $time > 0) {
|
||||
if (!isset($functions[$currentFunction])) {
|
||||
$functions[$currentFunction] = ['name' => $currentFunction, 'file' => $currentFile, 'time' => 0, 'memory' => 0, 'calls' => 0];
|
||||
}
|
||||
$functions[$currentFunction]['time'] += $time;
|
||||
$functions[$currentFunction]['memory'] += $memory;
|
||||
$functions[$currentFunction]['calls']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
usort($functions, fn($a, $b) => $b['time'] <=> $a['time']);
|
||||
echo "\n=== TOP 20 SLOWEST FUNCTIONS ===\n\n";
|
||||
echo str_pad("Time (ms)", 12) . str_pad("Memory (KB)", 14) . str_pad("Calls", 8) . "Function\n";
|
||||
echo str_repeat("-", 100) . "\n";
|
||||
|
||||
$total_time = array_sum(array_column($functions, 'time'));
|
||||
foreach (array_slice($functions, 0, 20) as $fn) {
|
||||
$time_ms = round($fn['time'] / 100000, 2);
|
||||
$memory_kb = round($fn['memory'] / 1024, 2);
|
||||
$percentage = round(($fn['time'] / $total_time) * 100, 1);
|
||||
$funcName = strlen($fn['name']) > 60 ? substr($fn['name'], 0, 57) . '...' : $fn['name'];
|
||||
echo str_pad($time_ms, 12) . str_pad($memory_kb, 14) . str_pad($fn['calls'], 8) . $funcName . " ({$percentage}%)\n";
|
||||
if ($fn['file'] && $fn['file'] !== 'php:internal') {
|
||||
echo str_repeat(" ", 34) . "└─ {$fn['file']}\n";
|
||||
}
|
||||
}
|
||||
echo "\nTotal execution time: " . round($total_time / 100000, 2) . " ms\n\n";
|
||||
EOFANALYZER
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_profile() {
|
||||
local url="${1:-/}"
|
||||
echo -e "${GREEN}Profiling: $url (container: $CONTAINER, port: $PORT)${NC}"
|
||||
|
||||
local response=$(curl -s -I "http://localhost:${PORT}${url}?XDEBUG_PROFILE=1")
|
||||
local profile_file=$(echo "$response" | grep -i "X-Xdebug-Profile-Filename" | cut -d' ' -f2 | tr -d '\r')
|
||||
|
||||
if [ -z "$profile_file" ]; then
|
||||
echo -e "${RED}Error: No profile generated. Is Xdebug installed?${NC}"
|
||||
echo -e "${YELLOW}Tip: Rebuild container with: cd devel && podman-compose build${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Profile generated: $profile_file${NC}"
|
||||
ensure_analyzer
|
||||
echo ""
|
||||
podman exec "$CONTAINER" php "$ANALYZER" "$profile_file"
|
||||
}
|
||||
|
||||
cmd_analyze() {
|
||||
local file="$1"
|
||||
ensure_analyzer
|
||||
|
||||
if [ -z "$file" ]; then
|
||||
file=$(podman exec "$CONTAINER" sh -c 'ls -t /tmp/cachegrind.out.* 2>/dev/null | head -1')
|
||||
if [ -z "$file" ]; then
|
||||
echo -e "${RED}No cachegrind files found. Run a profile first.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${YELLOW}Analyzing: $file${NC}"
|
||||
fi
|
||||
|
||||
podman exec "$CONTAINER" php "$ANALYZER" "$file"
|
||||
}
|
||||
|
||||
cmd_list() {
|
||||
echo -e "${GREEN}Available cachegrind profiles in $CONTAINER:${NC}"
|
||||
if ! podman exec "$CONTAINER" sh -c 'ls -lh /tmp/cachegrind.out.* 2>/dev/null'; then
|
||||
echo -e "${YELLOW}No profile files found.${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_clean() {
|
||||
echo -e "${YELLOW}Removing cachegrind files from $CONTAINER...${NC}"
|
||||
podman exec "$CONTAINER" sh -c 'rm -f /tmp/cachegrind.out.*' 2>/dev/null || true
|
||||
echo -e "${GREEN}Done!${NC}"
|
||||
}
|
||||
|
||||
cmd_generate() {
|
||||
local size="${1:-medium}"
|
||||
local categories posts depth
|
||||
|
||||
case "$size" in
|
||||
small) categories=5; posts=20; depth=2 ;;
|
||||
medium) categories=10; posts=50; depth=3 ;;
|
||||
large) categories=15; posts=100; depth=3 ;;
|
||||
huge) categories=25; posts=200; depth=4 ;;
|
||||
custom) categories=${2:-10}; posts=${3:-50}; depth=${4:-3} ;;
|
||||
*) echo -e "${RED}Unknown size. Use: small, medium, large, huge, custom${NC}"; exit 1 ;;
|
||||
esac
|
||||
|
||||
echo -e "${GREEN}Generating $size test data in $CONTAINER:/tmp/test-content${NC}"
|
||||
echo -e "${BLUE}Config: $categories categories, $posts posts/category, $depth levels${NC}"
|
||||
|
||||
# Create generator script in container
|
||||
cat << 'EOFGEN' | podman exec -i "$CONTAINER" tee /tmp/gen.php > /dev/null
|
||||
<?php
|
||||
$baseDir = '/tmp/test-content';
|
||||
$numCategories = (int)($argv[1] ?? 10);
|
||||
$postsPerCategory = (int)($argv[2] ?? 50);
|
||||
$maxDepth = (int)($argv[3] ?? 3);
|
||||
|
||||
$titles = ['Getting Started with', 'Introduction to', 'Advanced Techniques in', 'Understanding', 'Best Practices for', 'Common Mistakes in', 'A Comprehensive Guide to', 'Mastering', 'Quick Tips for', 'The Ultimate Guide to', 'Exploring', 'Deep Dive into'];
|
||||
$topics = ['Web Development', 'Performance Optimization', 'Database Design', 'API Integration', 'Security', 'Testing', 'Deployment', 'Caching Strategies', 'Microservices', 'Cloud Architecture', 'DevOps', 'Monitoring', 'Scalability', 'User Experience', 'Mobile Development', 'Machine Learning', 'Data Analysis'];
|
||||
$categories = ['tutorials', 'guides', 'blog', 'documentation', 'examples', 'case-studies', 'best-practices', 'architecture', 'tools', 'resources'];
|
||||
|
||||
function generateMarkdown($title, $paragraphs = 5) {
|
||||
$md = "# $title\n\n";
|
||||
for ($i = 0; $i < $paragraphs; $i++) {
|
||||
$sentences = rand(3, 6);
|
||||
$paragraph = '';
|
||||
for ($j = 0; $j < $sentences; $j++) {
|
||||
$words = rand(10, 20);
|
||||
$sentence = implode(' ', array_fill(0, $words, 'lorem ipsum dolor sit amet'));
|
||||
$paragraph .= ucfirst($sentence) . '. ';
|
||||
}
|
||||
$md .= $paragraph . "\n\n";
|
||||
if ($i % 2 == 0) $md .= "## Section " . ($i + 1) . "\n\n";
|
||||
if ($i % 3 == 0) $md .= "- Bullet point one\n- Bullet point two\n- Bullet point three\n\n";
|
||||
}
|
||||
return $md;
|
||||
}
|
||||
|
||||
function generateMetadata($title, $hasChildren = false) {
|
||||
$ini = "[metadata]\ntitle = \"$title\"\nsummary = \"Summary for $title\"\ndate = " . date('Y-m-d', strtotime('-' . rand(1, 365) . ' days')) . "\n";
|
||||
if ($hasChildren) { $ini .= "menu = 1\nmenu_order = " . rand(1, 10) . "\n"; }
|
||||
return $ini;
|
||||
}
|
||||
|
||||
function generateSlug($title) { return strtolower(preg_replace('/[^a-z0-9]+/i', '-', $title)); }
|
||||
|
||||
function generateContent($baseDir, $depth, $maxDepth, $postsPerCategory, $parentPath = '') {
|
||||
global $titles, $topics, $categories;
|
||||
if ($depth > $maxDepth) return;
|
||||
|
||||
$numCategories = $depth === 0 ? 10 : rand(3, 8);
|
||||
for ($c = 0; $c < $numCategories; $c++) {
|
||||
$categoryName = $categories[array_rand($categories)] . '-' . rand(100, 999);
|
||||
$categoryPath = $baseDir . $parentPath . '/' . $categoryName;
|
||||
if (!is_dir($categoryPath)) mkdir($categoryPath, 0755, true);
|
||||
|
||||
$categoryTitle = ucwords(str_replace('-', ' ', $categoryName));
|
||||
file_put_contents("$categoryPath/metadata.ini", generateMetadata($categoryTitle, true));
|
||||
|
||||
if (rand(0, 1)) {
|
||||
file_put_contents("$categoryPath/index.md", generateMarkdown("Welcome to $categoryTitle", 3));
|
||||
}
|
||||
|
||||
$numPosts = $depth === $maxDepth ? rand(5, $postsPerCategory) : rand(5, 15);
|
||||
for ($p = 0; $p < $numPosts; $p++) {
|
||||
$title = $titles[array_rand($titles)] . ' ' . $topics[array_rand($topics)];
|
||||
$date = date('Y-m-d', strtotime('-' . rand(1, 730) . ' days'));
|
||||
$slug = generateSlug($title);
|
||||
$postDir = "$categoryPath/$date-$slug";
|
||||
if (!is_dir($postDir)) mkdir($postDir, 0755, true);
|
||||
file_put_contents("$postDir/index.md", generateMarkdown($title, rand(3, 8)));
|
||||
if (rand(0, 2) === 0) {
|
||||
file_put_contents("$postDir/metadata.ini", generateMetadata($title, false));
|
||||
}
|
||||
}
|
||||
|
||||
if ($depth < $maxDepth && rand(0, 1)) {
|
||||
generateContent($baseDir, $depth + 1, $maxDepth, $postsPerCategory, $parentPath . '/' . $categoryName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (is_dir($baseDir)) exec("rm -rf $baseDir");
|
||||
mkdir($baseDir, 0755, true);
|
||||
file_put_contents("$baseDir/00-intro.md", generateMarkdown("Test Site", 2));
|
||||
|
||||
echo "Generating test data...\n";
|
||||
$start = microtime(true);
|
||||
generateContent($baseDir, 0, $maxDepth, $postsPerCategory);
|
||||
$duration = round(microtime(true) - $start, 2);
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($baseDir, RecursiveDirectoryIterator::SKIP_DOTS));
|
||||
$fileCount = 0; foreach ($iterator as $file) if ($file->isFile()) $fileCount++;
|
||||
echo "\nGenerated $fileCount files in {$duration}s\n";
|
||||
echo "Location: $baseDir\n";
|
||||
EOFGEN
|
||||
|
||||
# Run generator
|
||||
podman exec "$CONTAINER" php /tmp/gen.php "$categories" "$posts" "$depth"
|
||||
|
||||
echo -e "\n${GREEN}Test data created at /tmp/test-content inside container${NC}"
|
||||
echo -e "${YELLOW}To use: Update app/router.php to use /tmp/test-content${NC}"
|
||||
echo -e "${YELLOW}Or profile directly: $0 profile /blog-123/${NC}"
|
||||
}
|
||||
|
||||
cmd_testdata_stats() {
|
||||
if ! podman exec "$CONTAINER" test -d /tmp/test-content 2>/dev/null; then
|
||||
echo -e "${RED}No test data found in $CONTAINER:/tmp/test-content${NC}"
|
||||
echo -e "${YELLOW}Generate with: $0 generate [size]${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Test Data Statistics ($CONTAINER:/tmp/test-content):${NC}\n"
|
||||
podman exec "$CONTAINER" bash -c '
|
||||
cd /tmp/test-content
|
||||
echo "Directories: $(find . -type d | wc -l)"
|
||||
echo "Total files: $(find . -type f | wc -l)"
|
||||
echo "Markdown files: $(find . -name "*.md" | wc -l)"
|
||||
echo "Metadata files: $(find . -name "*.ini" | wc -l)"
|
||||
echo "Total size: $(du -sh . | cut -f1)"
|
||||
'
|
||||
}
|
||||
|
||||
cmd_testdata_clean() {
|
||||
echo -e "${YELLOW}Removing test data from $CONTAINER:/tmp/test-content...${NC}"
|
||||
podman exec "$CONTAINER" rm -rf /tmp/test-content 2>/dev/null || true
|
||||
echo -e "${GREEN}Done!${NC}"
|
||||
}
|
||||
|
||||
# Main command dispatcher
|
||||
case "${1:-help}" in
|
||||
profile) cmd_profile "$2" ;;
|
||||
analyze) cmd_analyze "$2" ;;
|
||||
list) cmd_list ;;
|
||||
clean) cmd_clean ;;
|
||||
generate|gen) shift; cmd_generate "$@" ;;
|
||||
testdata-stats|stats) cmd_testdata_stats ;;
|
||||
testdata-clean) cmd_testdata_clean ;;
|
||||
help|--help|-h) show_help ;;
|
||||
*) echo -e "${RED}Unknown command: $1${NC}\n"; show_help; exit 1 ;;
|
||||
esac
|
||||
Loading…
Add table
Reference in a new issue