time [options] command [arguments]
/usr/bin/time [options] command [arguments]
The time command measures and reports how long a command takes to execute. It provides three key timing measurements: real time (wall-clock time from start to finish), user time (CPU time spent in user mode), and system time (CPU time spent in kernel mode). These metrics are essential for performance analysis, benchmarking, and optimization.
There are actually two versions of time: the shell builtin (available in bash, zsh, etc.)
and the standalone /usr/bin/time command. The shell builtin is simpler and faster,
providing basic timing. The standalone version (GNU time) offers extensive formatting options,
memory statistics, I/O metrics, and detailed resource usage reporting. Understanding which version
you're using and when to use each is crucial for effective performance measurement.
# Time a simple command
time ls -lR /usr/lib > /dev/null
# Time a script
time ./backup_script.sh
# Time a pipeline
time find /var/log -name "*.log" | wc -l
# Time a compound command
time { sleep 2; echo "done"; }
# Check which time you're using
type time
# Output: time is a shell keyword (builtin)
# Compare different operations
echo "=== Timing CPU-intensive task ==="
time python3 -c "sum(range(10000000))"
echo "=== Timing I/O-intensive task ==="
time find / -name "*.conf" 2>/dev/null | wc -l
# For ls command:
real 0m2.341s
user 0m0.145s
sys 0m0.234s
# For CPU-intensive task:
real 0m0.856s
user 0m0.842s
sys 0m0.012s
# Note: user+sys ≈ real (CPU-bound, little waiting)
# For I/O-intensive task:
real 0m15.234s
user 0m0.234s
sys 0m2.145s
# Note: user+sys << real (lots of I/O waiting)
Real time is always >= user + sys time. If real is much larger, the process spent significant time waiting (I/O, network, or blocked by other processes). If they're close, the process was CPU-bound.
# Use GNU time explicitly (bypass shell builtin)
/usr/bin/time -v ls -lR /usr/lib > /dev/null
# Time a memory-intensive application
/usr/bin/time -v python3 -c "x = [i for i in range(10000000)]"
# Compare with shell builtin for contrast
echo "=== Shell builtin time ==="
time sleep 1
echo "=== GNU time with verbose ==="
/usr/bin/time -v sleep 1
# Practical example: time a compilation
/usr/bin/time -v make clean && /usr/bin/time -v make all
Command being timed: "ls -lR /usr/lib"
User time (seconds): 0.14
System time (seconds): 0.23
Percent of CPU this job got: 87%
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:02.34
Average shared text size (kbytes): 0
Average unshared data size (kbytes): 0
Average stack size (kbytes): 0
Average total size (kbytes): 0
Maximum resident set size (kbytes): 3456
Average resident set size (kbytes): 0
Major (requiring I/O) page faults: 12
Minor (reclaiming a frame) page faults: 234
Voluntary context switches: 45
Involuntary context switches: 23
Swaps: 0
File system inputs: 256
File system outputs: 0
Socket messages sent: 0
Socket messages received: 0
Signals delivered: 0
Page size (bytes): 4096
Exit status: 0
Maximum resident set size shows peak memory usage. Major page faults indicate disk I/O for memory pages. Context switches show how often the process was interrupted. This data is invaluable for performance optimization.
# Custom format for specific metrics
/usr/bin/time -f "Elapsed: %E\nCPU: %P\nMax Memory: %M KB" ls -lR /usr
# Format for CSV output (scripting/logging)
/usr/bin/time -f "%C,%e,%U,%S,%P,%M" python3 script.py
# Detailed format for performance analysis
/usr/bin/time -f "\
Command: %C
Real time: %e seconds
User time: %U seconds
System time: %S seconds
CPU usage: %P
Max memory: %M KB
Page faults: %F major, %R minor
Context switches: %c voluntary, %w involuntary" \
./benchmark_test
# Create a timing function for scripts
time_cmd() {
/usr/bin/time -f "[$1] Time: %es | CPU: %P | Memory: %MKB" "$@"
}
# Use it
time_cmd "Database Query" mysql -e "SELECT COUNT(*) FROM large_table;"
time_cmd "File Processing" grep -r "error" /var/log/
# Log timing to file
/usr/bin/time -f "%C: real=%e user=%U sys=%S mem=%MKB" \
-o performance.log \
./production_task.sh
# Simple format:
Elapsed: 0:02.45
CPU: 87%
Max Memory: 3456 KB
# CSV format:
ls -lR /usr,2.45,0.14,0.23,87%,3456
# Detailed format:
Command: ./benchmark_test
Real time: 5.234 seconds
User time: 4.123 seconds
System time: 0.892 seconds
CPU usage: 95%
Max memory: 125678 KB
Page faults: 5 major, 1234 minor
Context switches: 23 voluntary, 8 involuntary
# In performance.log:
mysql -e SELECT COUNT(*) FROM large_table;: real=3.45 user=0.12 sys=0.08 mem=12345KB
grep -r error /var/log/: real=8.92 user=1.23 sys=2.45 mem=2345KB
Available format specifiers: %E (elapsed), %e (elapsed seconds), %U (user), %S (system), %P (CPU %), %M (max memory KB), %F (major faults), %R (minor faults), %c (voluntary switches), %w (involuntary switches), %C (command). See man time for complete list.
# Compare different grep implementations
echo "=== Standard grep ==="
time grep -r "error" /var/log/ > /dev/null
echo "=== ripgrep (rg) ==="
time rg "error" /var/log/ > /dev/null
echo "=== ag (silver searcher) ==="
time ag "error" /var/log/ > /dev/null
# Compare sorting methods
dd if=/dev/urandom of=/tmp/random_data bs=1M count=100 2>/dev/null
echo "=== GNU sort ==="
time sort /tmp/random_data > /dev/null
echo "=== Sort with LC_ALL=C (faster) ==="
time LC_ALL=C sort /tmp/random_data > /dev/null
# Compare compression algorithms
echo "=== gzip compression ==="
time gzip -c /tmp/random_data > /tmp/test.gz
echo "=== bzip2 compression ==="
time bzip2 -c /tmp/random_data > /tmp/test.bz2
echo "=== xz compression ==="
time xz -c /tmp/random_data > /tmp/test.xz
# Check compression ratios
ls -lh /tmp/test.* | awk '{print $9, $5}'
# Compare script implementations
cat > /tmp/test1.sh << 'EOF'
#!/bin/bash
for i in {1..10000}; do
echo "Line $i" >> /tmp/output1.txt
done
EOF
cat > /tmp/test2.sh << 'EOF'
#!/bin/bash
for i in {1..10000}; do
echo "Line $i"
done > /tmp/output2.txt
EOF
chmod +x /tmp/test1.sh /tmp/test2.sh
echo "=== Appending in loop (slower) ==="
time /tmp/test1.sh
echo "=== Single redirection (faster) ==="
time /tmp/test2.sh
=== Standard grep ===
real 0m8.234s
user 0m1.234s
sys 0m2.456s
=== ripgrep (rg) ===
real 0m0.892s
user 0m0.567s
sys 0m0.234s
=== ag (silver searcher) ===
real 0m1.234s
user 0m0.678s
sys 0m0.345s
=== gzip compression ===
real 0m2.345s (6.2MB)
=== bzip2 compression ===
real 0m8.901s (5.8MB - better compression, slower)
=== xz compression ===
real 0m12.456s (5.1MB - best compression, slowest)
=== Appending in loop ===
real 0m15.678s
=== Single redirection ===
real 0m0.234s
These benchmarks show ripgrep is ~9x faster than grep, single redirection is ~67x faster than appending in a loop, and xz provides best compression at cost of speed. Real-world optimization decisions need this data.
# Redirect program output, keep timing visible
time ls -lR /usr > file_list.txt
# Timing goes to screen, ls output goes to file
# Redirect timing to file, keep program output visible
time ls -lR /usr 2> timing.txt
# Program output to screen, timing to file
# Redirect both separately
time ls -lR /usr > output.txt 2> timing.txt
# GNU time: redirect timing and program output separately
/usr/bin/time -o timing.log ls -lR /usr > output.txt
# Append timing to log file
/usr/bin/time -a -o performance.log \
-f "[%C] %e seconds, %M KB memory" \
./script.sh > script_output.log
# Complex example: separate streams for analysis
{
/usr/bin/time -f "TIMING: %e,%U,%S,%M" \
python3 data_processor.py > results.csv
} 2> timing_stats.log
# Parse timing separately
grep "^TIMING:" timing_stats.log | cut -d: -f2
# Script that logs both output and timing
run_and_log() {
local cmd="$1"
local timestamp=$(date +%Y%m%d_%H%M%S)
/usr/bin/time -f "Runtime: %e seconds\nMemory: %M KB" \
-o "logs/timing_${timestamp}.log" \
bash -c "$cmd" > "logs/output_${timestamp}.log" 2>&1
echo "Command completed. Check logs/ directory."
}
run_and_log "find /var -name '*.log' | xargs wc -l"
# output.txt contains:
total 12345
drwxr-xr-x 142 root root 12288 Dec 14 14:00 .
drwxr-xr-x 20 root root 4096 Nov 28 09:12 ..
...
# timing.txt contains:
real 0m2.341s
user 0m0.145s
sys 0m0.234s
# timing.log (GNU time with -o):
Command being timed: "ls -lR /usr"
User time (seconds): 0.14
System time (seconds): 0.23
...
# performance.log (appended format):
[./script.sh] 5.234 seconds, 12345 KB memory
[python3 data_processor.py] 8.901 seconds, 98765 KB memory
The -o flag (GNU time) writes timing to a file instead of stderr. Use -a to append instead of overwrite. This is essential for building performance monitoring systems and keeping logs organized.
# Simple loop timing
echo "Running 5 iterations..."
for i in {1..5}; do
echo "Iteration $i:"
time python3 script.py
done
# Collect timing data for analysis
cat > benchmark.sh << 'EOF'
#!/bin/bash
COMMAND="$1"
ITERATIONS="${2:-10}"
OUTPUT_FILE="benchmark_results.txt"
echo "Benchmarking: $COMMAND"
echo "Iterations: $ITERATIONS"
echo "Results saved to: $OUTPUT_FILE"
echo ""
# Clear previous results
> "$OUTPUT_FILE"
for i in $(seq 1 $ITERATIONS); do
echo -n "Run $i/$ITERATIONS... "
# Run and capture timing
{ time $COMMAND > /dev/null; } 2>&1 | \
grep real | \
awk '{print $2}' >> "$OUTPUT_FILE"
echo "done"
done
# Calculate statistics
echo ""
echo "=== Results ==="
echo "All times (seconds):"
cat "$OUTPUT_FILE"
echo ""
echo "=== Statistics ==="
# Average
AVG=$(awk '{sum+=$1; count++} END {print sum/count}' "$OUTPUT_FILE")
echo "Average: ${AVG}s"
# Min/Max
MIN=$(sort -n "$OUTPUT_FILE" | head -1)
MAX=$(sort -n "$OUTPUT_FILE" | tail -1)
echo "Minimum: ${MIN}s"
echo "Maximum: ${MAX}s"
# Median
MEDIAN=$(sort -n "$OUTPUT_FILE" | awk '{arr[NR]=$1} END {
if (NR % 2) print arr[(NR+1)/2]
else print (arr[NR/2] + arr[NR/2+1])/2
}')
echo "Median: ${MEDIAN}s"
EOF
chmod +x benchmark.sh
# Use it
./benchmark.sh "grep -r 'error' /var/log" 10
./benchmark.sh "python3 -c 'sum(range(1000000))'" 20
# Advanced: GNU time with iterations
for i in {1..5}; do
/usr/bin/time -f "Run $i: %e seconds, %M KB" \
-a -o detailed_benchmark.log \
./test_program
done
# Statistical analysis with awk
cat detailed_benchmark.log | \
awk '{print $3}' | \
awk '{sum+=$1; sumsq+=$1*$1} END {
print "Mean:", sum/NR
print "StdDev:", sqrt(sumsq/NR - (sum/NR)^2)
}'
Benchmarking: grep -r 'error' /var/log
Iterations: 10
Results saved to: benchmark_results.txt
Run 1/10... done
Run 2/10... done
...
Run 10/10... done
=== Results ===
All times (seconds):
0m8.234s
0m7.891s
0m8.456s
0m7.723s
0m8.901s
0m7.654s
0m8.123s
0m8.345s
0m7.987s
0m8.234s
=== Statistics ===
Average: 8.1548s
Minimum: 7.654s
Maximum: 8.901s
Median: 8.1785s
Mean: 8.155
StdDev: 0.347
Multiple iterations reveal performance consistency. High standard deviation indicates variable performance (system load, caching effects, I/O contention). Always benchmark multiple times for reliable data.
# Create a script with timed sections
cat > profiled_script.sh << 'EOF'
#!/bin/bash
# Function to time sections
time_section() {
local section_name="$1"
shift
echo "=== $section_name ==="
/usr/bin/time -f " Time: %es | Memory: %MKB" "$@"
echo ""
}
echo "Starting data processing pipeline..."
echo ""
time_section "Data Download" \
curl -s https://example.com/data.json -o /tmp/data.json
time_section "Data Parsing" \
python3 -c "import json; data = json.load(open('/tmp/data.json')); print(len(data))"
time_section "Database Import" \
mysql mydb < /tmp/import.sql
time_section "Index Creation" \
mysql mydb -e "CREATE INDEX idx_name ON table_name(column)"
time_section "Report Generation" \
python3 generate_report.py > report.html
echo "Pipeline complete!"
EOF
chmod +x profiled_script.sh
./profiled_script.sh
# Alternative: inline timing with SECONDS variable
cat > timing_with_seconds.sh << 'EOF'
#!/bin/bash
# Reset timer
SECONDS=0
echo "Step 1: Processing..."
sleep 2
step1_time=$SECONDS
echo "Step 2: Analysis..."
sleep 3
step2_time=$((SECONDS - step1_time))
echo "Step 3: Output..."
sleep 1
step3_time=$((SECONDS - SECONDS + step1_time + step2_time))
total_time=$SECONDS
echo ""
echo "=== Timing Breakdown ==="
echo "Step 1: ${step1_time}s"
echo "Step 2: ${step2_time}s"
echo "Step 3: ${step3_time}s"
echo "Total: ${total_time}s"
EOF
# Detailed profiling with timestamps
cat > detailed_profile.sh << 'EOF'
#!/bin/bash
log_time() {
local label="$1"
local timestamp=$(date +%s.%N)
echo "$timestamp|$label" >> /tmp/profile.log
}
> /tmp/profile.log # Clear log
log_time "START"
log_time "DB_QUERY_START"
mysql -e "SELECT COUNT(*) FROM large_table" > /dev/null
log_time "DB_QUERY_END"
log_time "FILE_PROCESS_START"
find /var/log -name "*.log" -exec wc -l {} \; > /dev/null
log_time "FILE_PROCESS_END"
log_time "END"
# Analyze timing
echo "=== Profile Results ==="
awk -F'|' '
NR==1 {start=$1; prev=$1}
NR>1 {
duration = $1 - prev
total = $1 - start
printf "%-20s: %6.3fs (total: %6.3fs)\n", $2, duration, total
prev = $1
}
' /tmp/profile.log
EOF
chmod +x detailed_profile.sh
./detailed_profile.sh
Starting data processing pipeline...
=== Data Download ===
Time: 1.234s | Memory: 2345KB
=== Data Parsing ===
Time: 0.567s | Memory: 45678KB
=== Database Import ===
Time: 8.901s | Memory: 12345KB
=== Index Creation ===
Time: 3.456s | Memory: 8901KB
=== Report Generation ===
Time: 2.345s | Memory: 23456KB
Pipeline complete!
# Timing breakdown shows database import is the bottleneck (8.9s)
=== Profile Results ===
DB_QUERY_START : 0.000s (total: 0.000s)
DB_QUERY_END : 5.234s (total: 5.234s)
FILE_PROCESS_START : 0.001s (total: 5.235s)
FILE_PROCESS_END : 3.456s (total: 8.691s)
END : 0.000s (total: 8.691s)
Profiling reveals that database operations take 5.2s and file processing takes 3.5s - together they account for the entire runtime. This data-driven insight directs optimization efforts to these specific sections.
# Monitor memory usage over iterations
cat > memory_monitor.sh << 'EOF'
#!/bin/bash
PROGRAM="$1"
ITERATIONS="${2:-10}"
echo "Monitoring memory usage: $PROGRAM"
echo "Iterations: $ITERATIONS"
echo ""
for i in $(seq 1 $ITERATIONS); do
MEM=$(/usr/bin/time -f "%M" $PROGRAM 2>&1 | tail -1)
echo "Iteration $i: ${MEM} KB"
sleep 1
done
echo ""
echo "If memory increases with each iteration, suspect a memory leak."
EOF
chmod +x memory_monitor.sh
# Test with a leaky program
cat > leaky.py << 'EOF'
# Simulated memory leak
import sys
leaked = []
for i in range(1000000):
leaked.append(i)
if i % 100000 == 0:
print(f"Iteration {i}", file=sys.stderr)
EOF
./memory_monitor.sh "python3 leaky.py" 5
# Compare with non-leaky program
cat > clean.py << 'EOF'
# Clean program
for i in range(1000000):
if i % 100000 == 0:
print(f"Iteration {i}")
EOF
./memory_monitor.sh "python3 clean.py" 5
# Detailed memory tracking
cat > track_memory.sh << 'EOF'
#!/bin/bash
COMMAND="$1"
DURATION="${2:-60}" # seconds
INTERVAL="${3:-5}" # seconds
OUTPUT="memory_tracking_$(date +%Y%m%d_%H%M%S).csv"
echo "timestamp,memory_kb" > "$OUTPUT"
# Start the command in background
$COMMAND &
PID=$!
echo "Tracking PID $PID for ${DURATION} seconds..."
START_TIME=$(date +%s)
while kill -0 $PID 2>/dev/null; do
CURRENT_TIME=$(date +%s)
ELAPSED=$((CURRENT_TIME - START_TIME))
if [ $ELAPSED -ge $DURATION ]; then
kill $PID 2>/dev/null
break
fi
# Get current memory usage
MEM=$(ps -o rss= -p $PID 2>/dev/null)
if [ -n "$MEM" ]; then
echo "$ELAPSED,$MEM" >> "$OUTPUT"
fi
sleep $INTERVAL
done
echo "Tracking complete. Data saved to $OUTPUT"
# Generate simple report
echo ""
echo "=== Memory Usage Summary ==="
awk -F',' 'NR>1 {
if (NR==2) min=max=$2
if ($2 < min) min=$2
if ($2 > max) max=$2
sum+=$2; count++
}
END {
print "Minimum:", min, "KB"
print "Maximum:", max, "KB"
print "Average:", sum/count, "KB"
print "Growth:", max-min, "KB"
}' "$OUTPUT"
EOF
chmod +x track_memory.sh
# Use it
./track_memory.sh "python3 long_running_app.py" 120 10
Monitoring memory usage: python3 leaky.py
Iterations: 5
Iteration 1: 45678 KB
Iteration 2: 67890 KB
Iteration 3: 89012 KB
Iteration 4: 110234 KB
Iteration 5: 131456 KB
If memory increases with each iteration, suspect a memory leak.
# Clean program:
Iteration 1: 12345 KB
Iteration 2: 12456 KB
Iteration 3: 12398 KB
Iteration 4: 12423 KB
Iteration 5: 12411 KB
# Leaky program shows steady increase, clean program stays constant
=== Memory Usage Summary ===
Minimum: 45678 KB
Maximum: 131456 KB
Average: 88854 KB
Growth: 85778 KB # Significant growth indicates leak
Memory growth over iterations is a strong indicator of memory leaks. Use tools like valgrind for detailed leak analysis, but time's -v flag provides a quick first check. Growing maximum resident set size across identical runs suggests leaks.
# Performance regression test framework
cat > perf_test.sh << 'EOF'
#!/bin/bash
# Configuration
BASELINE_FILE="performance_baseline.txt"
THRESHOLD_PERCENT=20 # Fail if >20% slower
# Test cases
declare -A TESTS=(
["db_query"]="mysql -e 'SELECT COUNT(*) FROM users'"
["file_search"]="find /var/log -name '*.log'"
["compression"]="gzip -c /tmp/testfile.dat > /dev/null"
)
run_test() {
local test_name="$1"
local test_cmd="$2"
# Run test and capture timing
local runtime=$(/usr/bin/time -f "%e" bash -c "$test_cmd" 2>&1 | tail -1)
echo "$runtime"
}
initialize_baseline() {
echo "Creating performance baseline..."
> "$BASELINE_FILE"
for test_name in "${!TESTS[@]}"; do
test_cmd="${TESTS[$test_name]}"
echo -n " $test_name... "
# Run 3 times, take median
times=()
for i in {1..3}; do
runtime=$(run_test "$test_name" "$test_cmd")
times+=($runtime)
done
median=$(printf '%s\n' "${times[@]}" | sort -n | sed -n '2p')
echo "$test_name=$median" >> "$BASELINE_FILE"
echo "${median}s"
done
echo "Baseline created: $BASELINE_FILE"
}
run_regression_tests() {
if [ ! -f "$BASELINE_FILE" ]; then
echo "Error: No baseline found. Run with --init first."
exit 1
fi
echo "Running performance regression tests..."
echo ""
local failed=0
local total=0
for test_name in "${!TESTS[@]}"; do
test_cmd="${TESTS[$test_name]}"
((total++))
# Get baseline
baseline=$(grep "^$test_name=" "$BASELINE_FILE" | cut -d= -f2)
if [ -z "$baseline" ]; then
echo "❌ $test_name: No baseline found"
((failed++))
continue
fi
# Run current test
echo -n "Testing $test_name... "
current=$(run_test "$test_name" "$test_cmd")
# Calculate difference
diff=$(awk "BEGIN {print ($current - $baseline) / $baseline * 100}")
diff_int=${diff%.*}
# Check threshold
if [ "${diff_int#-}" -gt "$THRESHOLD_PERCENT" ]; then
echo "❌ FAIL (${current}s vs ${baseline}s baseline, ${diff_int}% slower)"
((failed++))
else
echo "✓ PASS (${current}s vs ${baseline}s baseline, ${diff_int}% difference)"
fi
done
echo ""
echo "=== Results ==="
echo "Total tests: $total"
echo "Passed: $((total - failed))"
echo "Failed: $failed"
[ $failed -eq 0 ] && exit 0 || exit 1
}
# Main
case "$1" in
--init)
initialize_baseline
;;
"")
run_regression_tests
;;
*)
echo "Usage: $0 [--init]"
echo " --init: Create performance baseline"
echo " (no args): Run regression tests"
exit 1
;;
esac
EOF
chmod +x perf_test.sh
# Initialize baseline
./perf_test.sh --init
# Run tests (in CI/CD pipeline)
./perf_test.sh
# Integration with CI
cat > .gitlab-ci.yml << 'EOF'
performance_test:
stage: test
script:
- ./perf_test.sh
only:
- merge_requests
artifacts:
when: always
reports:
junit: performance_results.xml
EOF
# Initialization:
Creating performance baseline...
db_query... 0.234s
file_search... 2.345s
compression... 1.123s
Baseline created: performance_baseline.txt
# Running tests:
Running performance regression tests...
Testing db_query... ✓ PASS (0.245s vs 0.234s baseline, 4% difference)
Testing file_search... ❌ FAIL (3.012s vs 2.345s baseline, 28% slower)
Testing compression... ✓ PASS (1.156s vs 1.123s baseline, 2% difference)
=== Results ===
Total tests: 3
Passed: 2
Failed: 1
# Exit code 1 - CI pipeline fails
This framework catches performance regressions automatically. The file_search test shows 28% degradation, triggering a CI failure. Investigate before merging. Update baseline when intentional changes affect performance.
#!/bin/bash
# File: /usr/local/bin/perf-monitor
# Production performance monitoring system
set -euo pipefail
# ============================================
# Configuration
# ============================================
readonly MONITOR_DIR="/var/lib/perf-monitor"
readonly LOG_DIR="/var/log/perf-monitor"
readonly DATA_DIR="$MONITOR_DIR/data"
readonly ALERT_THRESHOLD_SECONDS=10
readonly ALERT_MEMORY_MB=1024
readonly RETENTION_DAYS=30
# Ensure directories exist
mkdir -p "$MONITOR_DIR" "$LOG_DIR" "$DATA_DIR"
# ============================================
# Functions
# ============================================
log_message() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_DIR/monitor.log"
}
record_metrics() {
local job_name="$1"
local command="$2"
local timestamp=$(date +%Y%m%d_%H%M%S)
local metrics_file="$DATA_DIR/${job_name}_${timestamp}.json"
log_message "Starting monitoring: $job_name"
# Run command with comprehensive timing
/usr/bin/time -f '{
"job": "%C",
"timestamp": "'$(date -Iseconds)'",
"elapsed_seconds": %e,
"user_seconds": %U,
"system_seconds": %S,
"cpu_percent": "%P",
"max_memory_kb": %M,
"avg_memory_kb": %K,
"page_faults_major": %F,
"page_faults_minor": %R,
"io_inputs": %I,
"io_outputs": %O,
"context_switches_voluntary": %c,
"context_switches_involuntary": %w,
"exit_status": %x
}' \
-o "$metrics_file" \
bash -c "$command" > "$LOG_DIR/${job_name}_output_${timestamp}.log" 2>&1
local exit_code=$?
log_message "Completed: $job_name (exit: $exit_code)"
# Check for alerts
check_alerts "$job_name" "$metrics_file"
return $exit_code
}
check_alerts() {
local job_name="$1"
local metrics_file="$2"
# Extract metrics (requires jq)
if ! command -v jq &>/dev/null; then
log_message "Warning: jq not installed, skipping alert checks"
return
fi
local elapsed=$(jq -r '.elapsed_seconds' "$metrics_file")
local memory_kb=$(jq -r '.max_memory_kb' "$metrics_file")
local memory_mb=$((memory_kb / 1024))
local exit_status=$(jq -r '.exit_status' "$metrics_file")
# Check exit status
if [ "$exit_status" != "0" ]; then
send_alert "FAILURE" "$job_name failed with exit code $exit_status"
fi
# Check elapsed time
if (( $(echo "$elapsed > $ALERT_THRESHOLD_SECONDS" | bc -l) )); then
send_alert "SLOW" "$job_name took ${elapsed}s (threshold: ${ALERT_THRESHOLD_SECONDS}s)"
fi
# Check memory usage
if [ "$memory_mb" -gt "$ALERT_MEMORY_MB" ]; then
send_alert "HIGH_MEMORY" "$job_name used ${memory_mb}MB (threshold: ${ALERT_MEMORY_MB}MB)"
fi
}
send_alert() {
local severity="$1"
local message="$2"
log_message "ALERT [$severity]: $message"
# Send email (if configured)
if [ -n "${ALERT_EMAIL:-}" ]; then
echo "$message" | mail -s "Performance Alert: $severity" "$ALERT_EMAIL"
fi
# Log to syslog
logger -t perf-monitor -p user.warning "[$severity] $message"
}
generate_report() {
local job_name="${1:-all}"
local days="${2:-7}"
log_message "Generating report for $job_name (last $days days)"
if ! command -v jq &> /dev/null; then
log_message "Error: jq required for reports"
return 1
fi
echo "=== Performance Report: $job_name ==="
echo "Period: Last $days days"
echo ""
# Find relevant metrics files
local cutoff_date=$(date -d "$days days ago" +%Y%m%d)
local files=()
if [ "$job_name" = "all" ]; then
files=($(find "$DATA_DIR" -name "*.json" -type f))
else
files=($(find "$DATA_DIR" -name "${job_name}_*.json" -type f))
fi
# Filter by date
local recent_files=()
for file in "${files[@]}"; do
local file_date=$(basename "$file" | grep -o '[0-9]\{8\}' | head -1)
if [ "$file_date" -ge "$cutoff_date" ]; then
recent_files+=("$file")
fi
done
if [ ${#recent_files[@]} -eq 0 ]; then
echo "No data found for specified period"
return
fi
# Aggregate statistics
jq -s '
group_by(.job) |
map({
job: .[0].job,
runs: length,
avg_elapsed: (map(.elapsed_seconds) | add / length),
min_elapsed: (map(.elapsed_seconds) | min),
max_elapsed: (map(.elapsed_seconds) | max),
avg_memory_mb: (map(.max_memory_kb) | add / length / 1024),
max_memory_mb: (map(.max_memory_kb) | max / 1024),
failures: (map(select(.exit_status != 0)) | length)
})
' "${recent_files[@]}" | \
jq -r '
(["Job", "Runs", "Avg Time", "Min Time", "Max Time", "Avg Mem", "Max Mem", "Failures"] | @tsv),
(.[] | [.job, .runs, (.avg_elapsed|tostring+"s"), (.min_elapsed|tostring+"s"),
(.max_elapsed|tostring+"s"), (.avg_memory_mb|tostring+"MB"),
(.max_memory_mb|tostring+"MB"), .failures] | @tsv)
' | column -t
}
cleanup_old_data() {
log_message "Cleaning up data older than $RETENTION_DAYS days"
find "$DATA_DIR" -name "*.json" -type f -mtime +$RETENTION_DAYS -delete
find "$LOG_DIR" -name "*_output_*.log" -type f -mtime +$RETENTION_DAYS -delete
log_message "Cleanup complete"
}
# ============================================
# Main CLI
# ============================================
usage() {
cat << EOF
Usage: $(basename $0) <command> [options]
Commands:
run <job-name> <command> Monitor and time a command
report [job] [days] Generate performance report
cleanup Remove old data
Examples:
$(basename $0) run backup "tar czf /backup/data.tar.gz /data"
$(basename $0) report backup 30
$(basename $0) report all 7
$(basename $0) cleanup
EOF
}
main() {
case "${1:-}" in
run)
[ $# -lt 3 ] && { usage; exit 1; }
record_metrics "$2" "$3"
;;
report)
generate_report "${2:-all}" "${3:-7}"
;;
cleanup)
cleanup_old_data
;;
*)
usage
exit 1
;;
esac
}
main "$@"
# Record a job:
$ perf-monitor run database_backup "mysqldump --all-databases > /backup/db.sql"
[2025-12-14 16:30:00] Starting monitoring: database_backup
[2025-12-14 16:32:15] Completed: database_backup (exit: 0)
# Generate report:
$ perf-monitor report all 7
=== Performance Report: all ===
Period: Last 7 days
Job Runs Avg Time Min Time Max Time Avg Mem Max Mem Failures
database_backup 14 135.2s 128.4s 145.7s 234.5MB 267.8MB 0
log_rotation 28 3.4s 2.9s 4.1s 12.3MB 15.6MB 0
report_generation 7 45.6s 42.1s 52.3s 456.7MB 523.1MB 1
# Alert example:
[2025-12-14 16:32:15] ALERT [SLOW]: database_backup took 145.7s (threshold: 10s)
# Cleanup:
$ perf-monitor cleanup
[2025-12-14 16:35:00] Cleaning up data older than 30 days
[2025-12-14 16:35:01] Cleanup complete
This production system provides comprehensive performance monitoring: automatic timing, memory tracking, alerting on thresholds, historical reporting, and data retention management. Perfect for production cron jobs and critical batch processes.
type time to check which version you're using. Shell builtin provides basic timing.
Use /usr/bin/time explicitly for detailed metrics, memory stats, and custom formatting.
/usr/bin/time -v provides memory usage, page faults, context switches, and I/O stats.
This is invaluable for identifying performance bottlenecks beyond just execution time.
command > output.txt to separate program output
from timing. Or use /usr/bin/time -o timing.log to save timing separately.
-f flag with format specifiers for scripts and logging:
/usr/bin/time -f "%e,%U,%S,%M" produces CSV-parseable output perfect for automation.
-v to track page faults and swapping.
uptime before
benchmarking. For consistent results, benchmark on idle systems or use nice/ionice to isolate tests.