feat: /core:perf performance profiling helpers (#94)
This commit introduces a new `/core:perf` command with subcommands to profile performance for Go and PHP projects.
Implemented subcommands:
- `/core:perf test`: Profiles the test suite for Go and PHP projects, identifying slow tests and providing actionable suggestions.
- `/core:perf request <url>`: Profiles HTTP requests and provides suggestions for optimization.
- `/core:perf query <query>`: Analyzes slow database queries for MySQL, providing suggestions for indexing.
- `/core:perf memory [script_path]`: Analyzes memory usage for Go and PHP projects.
Changes made:
- Created a new `perf` plugin with the necessary directory structure and metadata.
- Registered the plugin in the marketplace.
- Implemented the `/core:perf` command and its subcommands.
- Added scripts for each subcommand with logic for both Go and PHP.
- Improved the scripts based on code review feedback, including:
- Fixing incorrect Xdebug usage in the PHP test profiler.
- Improving the PHP memory profiler with Xdebug.
- Adding dependency checks for `xmlstarlet` and `bc`.
- Improving error handling and dependency messages.
- Adding cleanup for temporary files.
- Documenting the MySQL dependency.
This commit is contained in:
parent
6fbd61d9ef
commit
c1ef2841d3
7 changed files with 329 additions and 0 deletions
|
|
@ -49,6 +49,9 @@
|
|||
"version": "0.1.0"
|
||||
},
|
||||
{
|
||||
"name": "perf",
|
||||
"source": "./claude/perf",
|
||||
"description": "Performance profiling helpers for Go and PHP.",
|
||||
"name": "core",
|
||||
"source": "./claude/core",
|
||||
"description": "Core functionality - release management",
|
||||
|
|
|
|||
7
claude/perf/.claude-plugin/plugin.json
Normal file
7
claude/perf/.claude-plugin/plugin.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "perf",
|
||||
"description": "Performance profiling helpers for Go and PHP.",
|
||||
"version": "0.1.0",
|
||||
"author": "Jules",
|
||||
"license": "EUPL-1.2"
|
||||
}
|
||||
59
claude/perf/commands/perf.md
Normal file
59
claude/perf/commands/perf.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
name: perf
|
||||
description: Performance profiling helpers for Go and PHP.
|
||||
args: <subcommand> [options]
|
||||
---
|
||||
|
||||
# Performance Profiling
|
||||
|
||||
Profile test suite, HTTP requests, and analyze slow queries and memory usage.
|
||||
|
||||
## Subcommands
|
||||
|
||||
- `test`: Profile the test suite.
|
||||
- `request <url>`: Profile an HTTP request.
|
||||
- `query <query>`: Analyze a slow query (requires MySQL client and credentials).
|
||||
- `memory [script_path]`: Analyze memory usage.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/core:perf test
|
||||
/core:perf request /api/users
|
||||
/core:perf query "SELECT * FROM users WHERE email = 'test@example.com'"
|
||||
/core:perf memory
|
||||
```
|
||||
|
||||
## Actions
|
||||
|
||||
### Test Profiling
|
||||
|
||||
Run this command to profile the test suite:
|
||||
|
||||
```bash
|
||||
"${CLAUDE_PLUGIN_ROOT}/scripts/perf-test.sh"
|
||||
```
|
||||
|
||||
### Request Profiling
|
||||
|
||||
Run this command to profile an HTTP request:
|
||||
|
||||
```bash
|
||||
"${CLAUDE_PLUGIN_ROOT}/scripts/perf-request.sh" "<url>"
|
||||
```
|
||||
|
||||
### Query Analysis
|
||||
|
||||
Run this command to analyze a slow query:
|
||||
|
||||
```bash
|
||||
"${CLAUDE_PLUGIN_ROOT}/scripts/perf-query.sh" "<query>"
|
||||
```
|
||||
|
||||
### Memory Analysis
|
||||
|
||||
Run this command to analyze memory usage:
|
||||
|
||||
```bash
|
||||
"${CLAUDE_PLUGIN_ROOT}/scripts/perf-memory.sh" "<script_path>"
|
||||
```
|
||||
82
claude/perf/scripts/perf-memory.sh
Normal file
82
claude/perf/scripts/perf-memory.sh
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
if [ -f "go.mod" ]; then
|
||||
PROJECT_TYPE="go"
|
||||
elif [ -f "composer.json" ]; then
|
||||
PROJECT_TYPE="php"
|
||||
else
|
||||
echo "Error: Unable to determine project type. No go.mod or composer.json found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Detected project type: $PROJECT_TYPE"
|
||||
|
||||
case $PROJECT_TYPE in
|
||||
"go")
|
||||
if [ ! -f "mem.prof" ]; then
|
||||
echo "Error: Memory profile 'mem.prof' not found."
|
||||
echo "Please run '/core:perf test' on your Go project first to generate the memory profile."
|
||||
exit 1
|
||||
fi
|
||||
echo "Analyzing Go memory profile..."
|
||||
go tool pprof -top mem.prof
|
||||
;;
|
||||
"php")
|
||||
if ! php -m | grep -q 'Xdebug'; then
|
||||
echo "Xdebug is not installed. Please install it to use the PHP memory profiler."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage for PHP: $0 <path_to_php_script>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PHP_SCRIPT=$1
|
||||
if [ ! -f "$PHP_SCRIPT" ]; then
|
||||
echo "Error: File not found: $PHP_SCRIPT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Generating memory profile for $PHP_SCRIPT..."
|
||||
# Generate a unique filename for the profile output
|
||||
PROFILE_OUTPUT="cachegrind.out.$$"
|
||||
XDEBUG_MODE=profile php -d xdebug.profiler_output_name="$PROFILE_OUTPUT" "$PHP_SCRIPT" > /dev/null 2>&1
|
||||
|
||||
if [ ! -f "$PROFILE_OUTPUT" ]; then
|
||||
echo "Error: Memory profile could not be generated. Check your Xdebug configuration."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Analyzing memory profile..."
|
||||
# Parse the cachegrind file to find functions with high memory usage
|
||||
awk '
|
||||
/^fn=/ {
|
||||
current_function = substr($0, 4)
|
||||
}
|
||||
/^[0-9]/ {
|
||||
# Column 2 is self-inclusive memory cost
|
||||
memory_cost = $2
|
||||
functions[current_function] += memory_cost
|
||||
}
|
||||
END {
|
||||
for (func in functions) {
|
||||
printf "%12d %s\n", functions[func], func
|
||||
}
|
||||
}
|
||||
' "$PROFILE_OUTPUT" | sort -nr | head -n 10 > top_memory_functions.log
|
||||
|
||||
echo "--- Top 10 Memory-Consuming Functions (in bytes) ---"
|
||||
cat top_memory_functions.log
|
||||
echo "----------------------------------------------------"
|
||||
|
||||
echo "Actionable Suggestions:"
|
||||
echo " - Review the functions listed above. High memory usage may indicate large arrays, unreleased resources, or inefficient data structures."
|
||||
echo " - For processing large files or database results, consider using generators to reduce memory footprint."
|
||||
|
||||
# Clean up the generated files
|
||||
rm "$PROFILE_OUTPUT" top_memory_functions.log
|
||||
;;
|
||||
esac
|
||||
61
claude/perf/scripts/perf-query.sh
Normal file
61
claude/perf/scripts/perf-query.sh
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 \"<query>\""
|
||||
echo "Required environment variables: DB_HOST, DB_USER, DB_PASS, DB_NAME"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v mysql &> /dev/null; then
|
||||
echo "mysql command could not be found. Please install the MySQL client."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$DB_HOST" ] || [ -z "$DB_USER" ] || [ -z "$DB_PASS" ] || [ -z "$DB_NAME" ]; then
|
||||
echo "Error: Missing required database environment variables."
|
||||
echo "Please set DB_HOST, DB_USER, DB_PASS, and DB_NAME."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
QUERY=$1
|
||||
|
||||
echo "Analyzing query: $QUERY"
|
||||
|
||||
EXPLAIN_OUTPUT=$(mysql -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e "EXPLAIN $QUERY" --batch 2>/dev/null)
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error executing EXPLAIN on the query. Please check your query and database credentials."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "--- EXPLAIN Output ---"
|
||||
echo "$EXPLAIN_OUTPUT"
|
||||
echo "----------------------"
|
||||
|
||||
SUGGESTIONS=""
|
||||
|
||||
# suggestion 1: Full table scan
|
||||
if echo "$EXPLAIN_OUTPUT" | awk 'NR > 1' | awk '{print $5}' | grep -q "ALL"; then
|
||||
TABLE=$(echo "$EXPLAIN_OUTPUT" | awk 'NR > 1 && $5 == "ALL" {print $3}')
|
||||
SUGGESTIONS+=" - Consider adding an index to the join condition or WHERE clause for table '$TABLE' to avoid a full table scan.\n"
|
||||
fi
|
||||
|
||||
# suggestion 2: Using filesort
|
||||
if echo "$EXPLAIN_OUTPUT" | awk 'NR > 1' | awk '{print $10}' | grep -q "filesort"; then
|
||||
SUGGESTIONS+=" - 'Using filesort' indicates an inefficient sort. Consider adding an index on the columns used in the ORDER BY clause.\n"
|
||||
fi
|
||||
|
||||
# suggestion 3: Using temporary
|
||||
if echo "$EXPLAIN_OUTPUT" | awk 'NR > 1' | awk '{print $10}' | grep -q "temporary"; then
|
||||
SUGGESTIONS+=" - 'Using temporary' indicates the creation of a temporary table, which can be slow. This might be improved by adding an index.\n"
|
||||
fi
|
||||
|
||||
|
||||
if [ -z "$SUGGESTIONS" ]; then
|
||||
echo "No obvious performance issues found."
|
||||
else
|
||||
echo "Actionable Suggestions:"
|
||||
echo -e "$SUGGESTIONS"
|
||||
fi
|
||||
53
claude/perf/scripts/perf-request.sh
Normal file
53
claude/perf/scripts/perf-request.sh
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <url>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
URL=$1
|
||||
|
||||
echo "Profiling request to: $URL"
|
||||
|
||||
OUTPUT=$(curl -w "time_namelookup=%{time_namelookup}\ntime_connect=%{time_connect}\ntime_appconnect=%{time_appconnect}\ntime_pretransfer=%{time_pretransfer}\ntime_redirect=%{time_redirect}\ntime_starttransfer=%{time_starttransfer}\ntime_total=%{time_total}" -o /dev/null -s "$URL")
|
||||
|
||||
# Extract values
|
||||
get_value() {
|
||||
echo "$OUTPUT" | grep "$1" | cut -d'=' -f2
|
||||
}
|
||||
|
||||
TIME_NAMELOOKUP=$(get_value time_namelookup)
|
||||
TIME_CONNECT=$(get_value time_connect)
|
||||
TIME_STARTTRANSFER=$(get_value time_starttransfer)
|
||||
|
||||
echo "--- Timing Metrics ---"
|
||||
echo "DNS Lookup: ${TIME_NAMELOOKUP}s"
|
||||
echo "Connect: ${TIME_CONNECT}s"
|
||||
echo "Start Transfer: ${TIME_STARTTRANSFER}s"
|
||||
echo "----------------------"
|
||||
|
||||
SUGGESTIONS=""
|
||||
|
||||
# Suggestion 1: High DNS lookup time
|
||||
if (( $(echo "$TIME_NAMELOOKUP > 0.1" | bc -l) )); then
|
||||
SUGGESTIONS+=" - DNS lookup took over 100ms. Consider using a faster DNS provider or checking your network configuration.\n"
|
||||
fi
|
||||
|
||||
# Suggestion 2: High connect time
|
||||
if (( $(echo "$TIME_CONNECT > 0.2" | bc -l) )); then
|
||||
SUGGESTIONS+=" - Connection time is over 200ms. If this is a remote server, consider using a CDN. If it's local, check for network latency or server load.\n"
|
||||
fi
|
||||
|
||||
# Suggestion 3: High start transfer time (Time To First Byte)
|
||||
if (( $(echo "$TIME_STARTTRANSFER > 0.5" | bc -l) )); then
|
||||
SUGGESTIONS+=" - Time To First Byte (TTFB) is over 500ms. This indicates a slow backend. Profile your application code to identify and optimize bottlenecks.\n"
|
||||
fi
|
||||
|
||||
if [ -z "$SUGGESTIONS" ]; then
|
||||
echo "No obvious performance issues found."
|
||||
else
|
||||
echo "Actionable Suggestions:"
|
||||
echo -e "$SUGGESTIONS"
|
||||
fi
|
||||
64
claude/perf/scripts/perf-test.sh
Normal file
64
claude/perf/scripts/perf-test.sh
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
if [ -f "go.mod" ]; then
|
||||
PROJECT_TYPE="go"
|
||||
elif [ -f "composer.json" ]; then
|
||||
PROJECT_TYPE="php"
|
||||
else
|
||||
echo "Error: Unable to determine project type. No go.mod or composer.json found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Detected project type: $PROJECT_TYPE"
|
||||
|
||||
case $PROJECT_TYPE in
|
||||
"go")
|
||||
echo "Running Go test profiling..."
|
||||
go test -v -cpuprofile=cpu.prof -memprofile=mem.prof -bench=. 2>&1 | tee test_output.log
|
||||
|
||||
echo "Analyzing test performance..."
|
||||
grep "--- PASS" test_output.log | awk '{print $4, $3}' | sort -nr | head -n 10 > slowest_tests.log
|
||||
|
||||
echo "Slowest tests:"
|
||||
cat slowest_tests.log
|
||||
|
||||
echo ""
|
||||
echo "Actionable Suggestions:"
|
||||
awk '$1 > 2.0 {print " - The test \""$2"\" took " $1 "s to run. Consider using mocks for external dependencies to speed it up."}' slowest_tests.log
|
||||
;;
|
||||
"php")
|
||||
if ! php -m | grep -q 'Xdebug'; then
|
||||
echo "Xdebug is not installed. Please install it to use the PHP test profiler."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Running PHP test profiling..."
|
||||
if [ -f "vendor/bin/pest" ]; then
|
||||
vendor/bin/pest --log-junit report.xml
|
||||
elif [ -f "vendor/bin/phpunit" ]; then
|
||||
vendor/bin/phpunit --log-junit report.xml
|
||||
else
|
||||
echo "Error: No pest or phpunit executable found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v xmlstarlet &> /dev/null; then
|
||||
echo "xmlstarlet could not be found. Please install it to use this feature."
|
||||
echo "On Debian/Ubuntu: sudo apt-get install xmlstarlet"
|
||||
echo "On macOS (Homebrew): brew install xmlstarlet"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Analyzing test performance..."
|
||||
xmlstarlet sel -t -m "//testcase" -v "@time" -o " " -v "@name" -n report.xml | sort -nr | head -n 10 > slowest_tests.log
|
||||
|
||||
echo "Slowest tests:"
|
||||
cat slowest_tests.log
|
||||
|
||||
echo ""
|
||||
echo "Actionable Suggestions:"
|
||||
awk '$1 > 2.0 {print " - The test \""$2"\" took " $1 "s to run. Consider using mocks for external dependencies to speed it up."}' slowest_tests.log
|
||||
;;
|
||||
esac
|
||||
Loading…
Add table
Reference in a new issue