
Performance Profiler
- 86 installs
- 451 repo stars
- Updated July 21, 2026
- borghei/claude-skills
performance-profiler is a Claude Code skill for systematic performance profiling of Node.js, Python, and Go apps, covering CPU, memory, bundle, database, and load testing.
About
performance-profiler is a skill for systematic performance profiling of Node.js, Python, and Go applications. It covers CPU flamegraphs, memory leak detection, bundle analysis, database query optimization, N+1 detection, and load testing with k6, enforcing a measure-first methodology of baseline, fix, and verify. A developer uses it when diagnosing slow endpoints, memory growth, large bundles, or preparing for a traffic spike.
- Systematic profiling for Node.js, Python, and Go with a measure-first methodology
- CPU flamegraphs, heap-snapshot memory leak detection, and bundle analysis
- Database query optimization, N+1 detection, and k6 load testing
Performance Profiler by the numbers
- 86 all-time installs (skills.sh)
- Ranked #254 of 596 Debugging skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
performance-profiler capabilities & compatibility
- Capabilities
- load testing · memory profiling · database optimization · bundle analysis
- Use cases
- debugging · testing
What performance-profiler says it does
Systematic performance profiling for Node.js, Python, and Go applications.
Use when diagnosing slow endpoints, memory growth, large bundles, or
Profile → Confirm bottleneck → Fix → Measure again → Verify improvement
npx skills add https://github.com/borghei/claude-skills --skill performance-profilerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 86 |
|---|---|
| repo stars | ★ 451 |
| Last updated | July 21, 2026 |
| Repository | borghei/claude-skills ↗ |
What it does
Profile a slow app to find and verify the fix for the real bottleneck before release.
Who is it for?
Diagnosing slow endpoints, memory growth, large bundles, and preparing for traffic spikes.
Skip if: Frontend visual design or feature development unrelated to performance.
When should I use this skill?
You are diagnosing slow endpoints, memory growth, large bundles, or preparing for a traffic spike.
What you get
A profiled bottleneck fixed and verified with before/after metrics and a delta calculation.
- Baseline and post-fix performance metrics
- Profiler evidence (flamegraphs, heap snapshots)
- Load test results with latency percentiles
By the numbers
- 3 runtimes covered (Node.js, Python, Go)
- 5 profiling areas: CPU, memory, database, bundle, load
- Frameworks: clinic, py-spy, pprof, k6, webpack-bundle-analyzer
Files
Performance Profiler
Tier: POWERFUL Category: Engineering / Performance Maintainer: Claude Skills Team
Overview
Systematic performance profiling for Node.js, Python, and Go applications. Identifies CPU bottlenecks with flamegraphs, detects memory leaks with heap snapshots, analyzes bundle sizes, optimizes database queries, detects N+1 patterns, and runs load tests with k6 and Artillery. Enforces a measure-first methodology: establish baseline, identify bottleneck, fix, and verify improvement.
Keywords
performance profiling, flamegraph, memory leak, bundle analysis, N+1 queries, load testing, k6, latency, P99, CPU profiling, heap snapshot, database optimization
Golden Rule: Measure First
WRONG: "I think the N+1 query is slow, let me fix it"
RIGHT: Profile → Confirm bottleneck → Fix → Measure again → Verify improvement
Every optimization must have:
1. Baseline metrics (before)
2. Profiler evidence (what's actually slow)
3. The fix
4. Post-fix metrics (after)
5. Delta calculation (improvement %)Core Capabilities
1. CPU Profiling
- Node.js: Clinic.js flamegraphs, V8 CPU profiles
- Python: py-spy flamegraphs, cProfile, scalene
- Go: pprof CPU profiles, trace visualization
- Browser: Chrome DevTools Performance panel
2. Memory Profiling
- Heap snapshots and comparison (before/after)
- Garbage collection pressure analysis
- Memory leak detection patterns
- Retained object graph analysis
3. Database Optimization
- EXPLAIN ANALYZE for query plan analysis
- N+1 query detection and batching
- Slow query log analysis
- Missing index identification
- Connection pool sizing
4. Bundle Analysis
- webpack-bundle-analyzer visualization
- Next.js bundle analyzer
- Tree-shaking effectiveness
- Dynamic import opportunities
- Heavy dependency identification
5. Load Testing
- k6 scripts with ramp-up patterns
- SLA threshold enforcement in CI
- Latency percentile tracking (P50, P95, P99)
- Concurrent user simulation
When to Use
- App is slow and you do not know where the bottleneck is
- P99 latency exceeds SLA before a release
- Memory usage grows over time (suspected leak)
- Bundle size increased after adding dependencies
- Preparing for a traffic spike (load test before launch)
- Database queries taking >100ms
- After a dependency upgrade to verify no regressions
Node.js CPU Profiling
Method 1: Clinic.js Flamegraph
# Install
npm install -g clinic
# Generate flamegraph (starts server, applies load, generates HTML report)
clinic flame -- node server.js
# With specific load profile
clinic flame --autocannon [ /api/endpoint -c 10 -d 30 ] -- node server.js
# Analyze specific scenario
clinic flame --on-port 'autocannon -c 50 -d 60 http://localhost:$PORT/api/heavy-endpoint' -- node server.jsMethod 2: V8 CPU Profile
# Start Node with inspector
node --inspect server.js
# Or profile on demand
node --cpu-prof --cpu-prof-dir=./profiles server.js
# Load the .cpuprofile file in Chrome DevTools > Performance
# Programmatic profiling of a specific function
const { Session } = require('inspector');
const session = new Session();
session.connect();
session.post('Profiler.enable', () => {
session.post('Profiler.start', () => {
// Run the code you want to profile
runHeavyOperation();
session.post('Profiler.stop', (err, { profile }) => {
require('fs').writeFileSync('profile.cpuprofile', JSON.stringify(profile));
});
});
});Memory Leak Detection
Node.js Heap Snapshots
// Take heap snapshots programmatically
const v8 = require('v8');
const fs = require('fs');
function takeHeapSnapshot(label) {
const snapshotPath = `heap-${label}-${Date.now()}.heapsnapshot`;
const stream = v8.writeHeapSnapshot(snapshotPath);
console.log(`Heap snapshot written to: ${snapshotPath}`);
return snapshotPath;
}
// Leak detection pattern: compare two snapshots
// 1. Take snapshot at startup
takeHeapSnapshot('baseline');
// 2. Run operations that you suspect leak
// ... process 1000 requests ...
// 3. Force GC and take another snapshot
if (global.gc) global.gc(); // requires --expose-gc flag
takeHeapSnapshot('after-load');
// Load both .heapsnapshot files in Chrome DevTools > Memory
// Use "Comparison" view to find objects that grewPython Memory Profiling
# Install tracemalloc-based profiler
pip install memray
# Profile a script
memray run my_script.py
memray flamegraph memray-output.bin -o flamegraph.html
# Profile a specific function
python -c "
import tracemalloc
tracemalloc.start()
# Run your code
from my_module import heavy_function
heavy_function()
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
print('Top 10 memory allocations:')
for stat in top_stats[:10]:
print(stat)
"Database Query Optimization
EXPLAIN ANALYZE Workflow
-- Step 1: Get the actual execution plan (not just estimated)
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT t.*, p.name as project_name
FROM tasks t
JOIN projects p ON p.id = t.project_id
WHERE p.workspace_id = 'ws_abc123'
AND t.status = 'in_progress'
AND t.deleted_at IS NULL
ORDER BY t.updated_at DESC
LIMIT 20;
-- What to look for in the output:
-- Seq Scan on tasks → MISSING INDEX (should be Index Scan)
-- Rows Removed by Filter: 99000 → INDEX NOT SELECTIVE ENOUGH
-- Sort Method: external merge → NOT ENOUGH work_mem
-- Nested Loop with inner Seq Scan → MISSING INDEX ON JOIN COLUMN
-- Actual rows=1000 vs estimated rows=1 → STALE STATISTICS (run ANALYZE)N+1 Query Detection
// PROBLEM: N+1 query pattern
async function getProjectsWithTasks(workspaceId: string) {
const projects = await db.query.projects.findMany({
where: eq(projects.workspaceId, workspaceId),
});
// This executes N additional queries (one per project)
for (const project of projects) {
project.tasks = await db.query.tasks.findMany({
where: eq(tasks.projectId, project.id),
});
}
return projects;
}
// Total queries: 1 + N (where N = number of projects)
// FIX: Single query with JOIN or relation loading
async function getProjectsWithTasks(workspaceId: string) {
return db.query.projects.findMany({
where: eq(projects.workspaceId, workspaceId),
with: {
tasks: true, // Drizzle generates a single JOIN or subquery
},
});
}
// Total queries: 1-2 (depending on ORM strategy)N+1 Detection Script
# Log query count per request (add to middleware)
# Node.js with Drizzle:
let queryCount = 0;
const originalQuery = db.execute;
db.execute = (...args) => { queryCount++; return originalQuery.apply(db, args); };
// After request completes:
if (queryCount > 10) {
console.warn(`N+1 ALERT: ${req.method} ${req.path} executed ${queryCount} queries`);
}Bundle Analysis
Next.js Bundle Analyzer
# Install
pnpm add -D @next/bundle-analyzer
# next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer(nextConfig);
# Run analysis
ANALYZE=true pnpm build
# Opens browser with interactive treemapQuick Bundle Size Check
# Check what you're shipping
npx source-map-explorer .next/static/chunks/*.js
# Size of individual imports
npx import-cost # VS Code extension for inline size
# Find heavy dependencies
npx depcheck --json | jq '.dependencies'
npx bundlephobia-cli <package-name>Common Bundle Wins
| Before | After | Savings |
|---|---|---|
import _ from 'lodash' | import groupBy from 'lodash/groupBy' | ~70KB |
import moment from 'moment' | import { format } from 'date-fns' | ~60KB |
import { icons } from 'lucide-react' | import { Search } from 'lucide-react' | ~50KB |
| Static import of heavy component | dynamic(() => import('./HeavyChart')) | Deferred |
| All routes in one chunk | Code splitting per route (automatic in Next.js) | Per-route |
Load Testing with k6
// load-test.k6.js
import http from 'k6/http'
import { check, sleep } from 'k6'
import { Trend, Rate } from 'k6/metrics'
const apiLatency = new Trend('api_latency')
const errorRate = new Rate('errors')
export const options = {
stages: [
{ duration: '1m', target: 20 }, // ramp up
{ duration: '3m', target: 100 }, // sustain
{ duration: '1m', target: 0 }, // ramp down
],
thresholds: {
http_req_duration: ['p(95)<200', 'p(99)<500'],
errors: ['rate<0.01'],
api_latency: ['p(95)<150'],
},
}
export default function () {
const res = http.get(`${__ENV.BASE_URL}/api/v1/projects?limit=20`, {
headers: { Authorization: `Bearer ${__ENV.TOKEN}` },
})
apiLatency.add(res.timings.duration)
check(res, {
'status 200': (r) => r.status === 200,
'body has data': (r) => JSON.parse(r.body).data !== undefined,
}) || errorRate.add(1)
sleep(1)
}# Run locally
k6 run load-test.k6.js -e BASE_URL=http://localhost:3000 -e TOKEN=$TOKEN
# Run with cloud reporting
k6 cloud load-test.k6.jsBefore/After Measurement Template
## Performance Optimization: [What You Fixed]
**Date:** YYYY-MM-DD
**Ticket:** PROJ-123
### Problem
[1-2 sentences: what was slow, how it was observed]
### Root Cause
[What the profiler revealed — include flamegraph link or screenshot]
### Baseline (Before)
| Metric | Value |
|--------|-------|
| P50 latency | XXms |
| P95 latency | XXms |
| P99 latency | XXms |
| Throughput (RPS) | XX |
| DB queries/request | XX |
| Bundle size | XXkB |
### Fix Applied
[Brief description + link to PR]
### After
| Metric | Before | After | Delta |
|--------|--------|-------|-------|
| P50 | XXms | XXms | -XX% |
| P95 | XXms | XXms | -XX% |
| P99 | XXms | XXms | -XX% |
| RPS | XX | XX | +XX% |
| DB queries/req | XX | XX | -XX% |
### Verification
[Link to k6 output, CI run, or monitoring dashboard]Quick-Win Optimization Checklist
DATABASE
[ ] Missing indexes on WHERE/ORDER BY columns
[ ] N+1 queries (check query count per request)
[ ] SELECT * when only 2-3 columns needed
[ ] No LIMIT on unbounded queries
[ ] Missing connection pool (new connection per request)
[ ] Stale statistics (run ANALYZE on busy tables)
NODE.JS
[ ] Sync I/O (fs.readFileSync) in request handlers
[ ] JSON.parse/stringify of large objects in hot loops
[ ] Missing response compression (gzip/brotli)
[ ] Dependencies loaded inside request handlers (move to module level)
[ ] Sequential awaits that could be Promise.all
BUNDLE
[ ] Full lodash/moment import instead of specific functions
[ ] Static imports of heavy components (use dynamic import)
[ ] Images not optimized / not using next/image
[ ] No code splitting on routes
API
[ ] No pagination on list endpoints
[ ] No Cache-Control headers on stable responses
[ ] Serial fetches that could run in parallel
[ ] Fetching related data in loops instead of JOINsCommon Pitfalls
- Optimizing without measuring — you will optimize the wrong thing
- Testing with development data — 10 rows in dev vs millions in prod reveals different bottlenecks
- Ignoring P99 — P50 can look fine while P99 is catastrophic for some users
- Premature optimization — fix correctness first, then measure and optimize
- Not re-measuring after the fix — always verify the fix actually improved the metrics
- Load testing production — use staging with production-sized data volumes instead
Best Practices
1. Baseline first, always — record P50/P95/P99, RPS, and error rate before touching anything 2. One change at a time — isolate the variable to confirm causation, not correlation 3. Profile with realistic data volumes — performance characteristics change dramatically with scale 4. Set performance budgets — p(95) < 200ms as a CI gate with k6 5. Monitor continuously — add Datadog/Prometheus/Grafana metrics for key code paths 6. Cache aggressively, invalidate precisely — cache is the fastest optimization but hardest to debug 7. Document the win — before/after in the PR description motivates the team and creates institutional knowledge
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
Flamegraph shows only (idle) frames | Profiling during low-load period; no meaningful CPU work captured | Apply realistic load with autocannon or k6 during profiling, target the specific endpoint under investigation |
| Heap snapshot comparison shows no growth but memory still climbs | Native memory leak outside V8 heap (e.g., native addon, file descriptor leak) | Use process.memoryUsage().rss tracking alongside heap snapshots; profile with Valgrind or memray for native allocations |
EXPLAIN ANALYZE shows Index Scan but query is still slow | Index exists but is not selective enough, or query returns too many rows for index to help | Check index selectivity with SELECT count(DISTINCT col)/count(*) FROM table; consider composite index or partial index |
| k6 load test passes locally but fails in CI | CI runner has limited CPU/memory; network latency differs from local | Run k6 against a dedicated staging environment, not localhost in CI; adjust thresholds for CI-specific baselines |
| Bundle analyzer shows expected size but app still loads slowly | Large bundle is code-split but critical path has render-blocking resources | Audit the critical rendering path separately with Lighthouse; check for synchronous scripts and unoptimized images |
py-spy cannot attach to running process | Insufficient permissions or SIP (System Integrity Protection) on macOS | Run with sudo py-spy record --pid <PID>; on macOS, disable SIP or use --subprocesses flag with a fresh process |
| N+1 detection middleware reports false positives | Legitimate batch operations trigger high query counts per request | Add endpoint-level allowlists to the detection middleware; distinguish between N+1 patterns and intentional batch queries by checking for repeated identical query templates |
Success Criteria
- Baseline coverage: Every optimization PR includes documented before/after metrics with P50, P95, and P99 latency values
- Latency targets met: P95 API response time stays below 200ms and P99 below 500ms as validated by k6 threshold checks in CI
- Memory stability: No heap growth exceeding 10% over a 24-hour soak test under sustained load
- Bundle budget enforced: JavaScript bundle size for initial page load remains under 200kB gzipped, verified by CI gate
- N+1 elimination: Query count per API request stays below 10 for all critical endpoints, validated by request-level query logging
- Load test confidence: Staging load tests demonstrate the system handles 2x expected peak traffic with error rate below 1%
- Regression detection: Performance regressions are caught within one CI cycle, not discovered in production monitoring
Scope & Limitations
This skill covers:
- CPU and memory profiling for Node.js, Python, and Go applications using flamegraphs and heap snapshots
- Database query optimization including EXPLAIN ANALYZE interpretation, N+1 detection, and index recommendations
- Frontend bundle analysis and size reduction strategies for webpack and Next.js projects
- Load testing methodology with k6 including ramp-up patterns, threshold enforcement, and CI integration
This skill does NOT cover:
- Application Performance Monitoring (APM) platform setup and configuration (Datadog, New Relic, Grafana) — see
engineering/observability-designer - Infrastructure-level performance tuning (kernel parameters, network stack, container resource limits) — see
engineering/senior-devops - Security-focused performance concerns such as DDoS mitigation or rate limiting — see
engineering/senior-security - Mobile application profiling (iOS Instruments, Android Profiler) — see
engineering/senior-mobile
Integration Points
| Skill | Integration | Data Flow |
|---|---|---|
engineering/observability-designer | Performance profiling findings feed into observability dashboard design; alerting thresholds derived from profiling baselines | Profiler baselines and SLA thresholds → Prometheus/Grafana alert rules and dashboard panels |
engineering/ci-cd-pipeline-builder | k6 load tests and bundle size checks integrate as CI pipeline gates | k6 threshold configs and bundle budget scripts → CI pipeline stage definitions |
engineering/database-designer | Query optimization recommendations inform schema design decisions; index suggestions feed back to schema migrations | EXPLAIN ANALYZE findings and index recommendations → schema migration files and index definitions |
engineering/senior-backend | Backend architecture decisions incorporate profiling data; connection pool sizing and caching strategies validated by load tests | Profiling reports and load test results → architecture decision records and implementation guidance |
engineering/tech-debt-tracker | Performance regressions and unresolved bottlenecks are tracked as technical debt items with measured impact | Before/after measurement reports and unresolved findings → tech debt backlog with quantified cost |
engineering/senior-frontend | Bundle analysis results drive frontend optimization work; code-splitting and lazy-loading decisions backed by profiler data | Bundle analyzer output and Lighthouse scores → frontend optimization tasks and component refactoring plans |
#!/usr/bin/env python3
"""Benchmark Reporter — Parse benchmark results and generate comparison reports.
Reads JSON benchmark results (baseline and current), computes deltas,
detects regressions, and generates a formatted comparison report.
Input format: JSON with "baseline" and "current" keys, each containing
a list of benchmark entries with name, iterations, and timing metrics.
"""
import argparse
import json
import math
import sys
from collections import defaultdict
def parse_input(source):
"""Read JSON benchmark data from file or stdin."""
if source == "-":
raw = sys.stdin.read()
else:
with open(source, "r") as f:
raw = f.read()
data = json.loads(raw.strip())
return data
def normalize_entries(entries):
"""Normalize benchmark entries into a consistent format."""
by_name = {}
for entry in entries:
name = entry.get("name") or entry.get("benchmark") or entry.get("test", "unknown")
by_name[name] = {
"name": name,
"iterations": entry.get("iterations") or entry.get("runs") or entry.get("n", 1),
"mean_ms": entry.get("mean_ms") or entry.get("avg_ms") or entry.get("mean", 0),
"median_ms": entry.get("median_ms") or entry.get("median", 0),
"min_ms": entry.get("min_ms") or entry.get("min", 0),
"max_ms": entry.get("max_ms") or entry.get("max", 0),
"stddev_ms": entry.get("stddev_ms") or entry.get("stddev") or entry.get("sd", 0),
"ops_per_sec": entry.get("ops_per_sec") or entry.get("ops_s") or entry.get("throughput", 0),
"memory_mb": entry.get("memory_mb") or entry.get("memory") or entry.get("mem_mb", 0),
}
return by_name
def compute_delta(baseline_val, current_val):
"""Calculate percentage change. Negative means improvement for latency."""
if baseline_val == 0:
return 0.0 if current_val == 0 else float("inf")
return ((current_val - baseline_val) / baseline_val) * 100
def classify_change(delta_pct, regression_threshold, improvement_threshold):
"""Classify a change as regression, improvement, or stable."""
if delta_pct > regression_threshold:
return "regression"
elif delta_pct < -improvement_threshold:
return "improvement"
else:
return "stable"
def compare_benchmarks(baseline, current, regression_pct, improvement_pct):
"""Compare baseline vs current benchmark results."""
base_map = normalize_entries(baseline)
curr_map = normalize_entries(current)
all_names = sorted(set(list(base_map.keys()) + list(curr_map.keys())))
comparisons = []
for name in all_names:
base = base_map.get(name)
curr = curr_map.get(name)
if base and curr:
mean_delta = compute_delta(base["mean_ms"], curr["mean_ms"])
median_delta = compute_delta(base["median_ms"], curr["median_ms"]) if base["median_ms"] else 0
ops_delta = compute_delta(base["ops_per_sec"], curr["ops_per_sec"]) if base["ops_per_sec"] else 0
mem_delta = compute_delta(base["memory_mb"], curr["memory_mb"]) if base["memory_mb"] else 0
# For latency: positive delta = regression (slower)
latency_status = classify_change(mean_delta, regression_pct, improvement_pct)
# For ops/sec: negative delta = regression (lower throughput)
ops_status = classify_change(-ops_delta, regression_pct, improvement_pct) if curr["ops_per_sec"] else "n/a"
# For memory: positive delta = regression (more memory)
mem_status = classify_change(mem_delta, regression_pct, improvement_pct) if curr["memory_mb"] else "n/a"
comparisons.append({
"name": name,
"status": "compared",
"baseline_mean_ms": base["mean_ms"],
"current_mean_ms": curr["mean_ms"],
"mean_delta_pct": round(mean_delta, 2),
"baseline_median_ms": base["median_ms"],
"current_median_ms": curr["median_ms"],
"median_delta_pct": round(median_delta, 2),
"baseline_ops_per_sec": base["ops_per_sec"],
"current_ops_per_sec": curr["ops_per_sec"],
"ops_delta_pct": round(ops_delta, 2),
"baseline_memory_mb": base["memory_mb"],
"current_memory_mb": curr["memory_mb"],
"memory_delta_pct": round(mem_delta, 2),
"latency_classification": latency_status,
"throughput_classification": ops_status,
"memory_classification": mem_status,
"iterations": curr["iterations"],
"current_stddev_ms": curr["stddev_ms"],
})
elif curr and not base:
comparisons.append({
"name": name,
"status": "new",
"current_mean_ms": curr["mean_ms"],
"current_ops_per_sec": curr["ops_per_sec"],
"current_memory_mb": curr["memory_mb"],
"iterations": curr["iterations"],
"latency_classification": "new",
})
elif base and not curr:
comparisons.append({
"name": name,
"status": "removed",
"baseline_mean_ms": base["mean_ms"],
"latency_classification": "removed",
})
return comparisons
def build_report(comparisons, regression_pct):
"""Build the full report with summary statistics."""
regressions = [c for c in comparisons if c.get("latency_classification") == "regression"]
improvements = [c for c in comparisons if c.get("latency_classification") == "improvement"]
stable = [c for c in comparisons if c.get("latency_classification") == "stable"]
new_benchmarks = [c for c in comparisons if c.get("status") == "new"]
removed = [c for c in comparisons if c.get("status") == "removed"]
has_regressions = len(regressions) > 0
worst_regression = max((c["mean_delta_pct"] for c in regressions), default=0)
best_improvement = min((c["mean_delta_pct"] for c in improvements), default=0)
return {
"summary": {
"total_benchmarks": len(comparisons),
"compared": len([c for c in comparisons if c["status"] == "compared"]),
"regressions": len(regressions),
"improvements": len(improvements),
"stable": len(stable),
"new": len(new_benchmarks),
"removed": len(removed),
"has_regressions": has_regressions,
"regression_threshold_pct": regression_pct,
"worst_regression_pct": round(worst_regression, 2),
"best_improvement_pct": round(best_improvement, 2),
"verdict": "FAIL" if has_regressions else "PASS",
},
"regressions": regressions,
"improvements": improvements,
"stable": stable,
"new_benchmarks": new_benchmarks,
"removed_benchmarks": removed,
"all_comparisons": comparisons,
}
def format_delta(delta_pct):
"""Format delta with sign and color hint."""
if delta_pct > 0:
return f"+{delta_pct:.2f}%"
elif delta_pct < 0:
return f"{delta_pct:.2f}%"
return "0.00%"
def format_human(report):
"""Format the report as human-readable text."""
lines = []
s = report["summary"]
verdict_marker = "FAIL" if s["verdict"] == "FAIL" else "PASS"
lines.append("=" * 72)
lines.append(f"BENCHMARK COMPARISON REPORT [{verdict_marker}]")
lines.append("=" * 72)
lines.append(f"Total benchmarks: {s['total_benchmarks']} | Compared: {s['compared']} | New: {s['new']} | Removed: {s['removed']}")
lines.append(f"Regression threshold: {s['regression_threshold_pct']}%")
lines.append(f"Regressions: {s['regressions']} | Improvements: {s['improvements']} | Stable: {s['stable']}")
if s["worst_regression_pct"] > 0:
lines.append(f"Worst regression: {format_delta(s['worst_regression_pct'])}")
if s["best_improvement_pct"] < 0:
lines.append(f"Best improvement: {format_delta(s['best_improvement_pct'])}")
lines.append("")
if report["regressions"]:
lines.append("-" * 72)
lines.append("REGRESSIONS (performance degraded)")
lines.append("-" * 72)
for c in sorted(report["regressions"], key=lambda x: x["mean_delta_pct"], reverse=True):
lines.append(f" [REGRESS] {c['name']}")
lines.append(f" Mean: {c['baseline_mean_ms']:.2f}ms -> {c['current_mean_ms']:.2f}ms ({format_delta(c['mean_delta_pct'])})")
if c.get("current_ops_per_sec"):
lines.append(f" Ops/s: {c['baseline_ops_per_sec']:.0f} -> {c['current_ops_per_sec']:.0f} ({format_delta(c['ops_delta_pct'])})")
if c.get("current_memory_mb"):
lines.append(f" Memory: {c['baseline_memory_mb']:.1f}MB -> {c['current_memory_mb']:.1f}MB ({format_delta(c['memory_delta_pct'])})")
lines.append("")
if report["improvements"]:
lines.append("-" * 72)
lines.append("IMPROVEMENTS (performance improved)")
lines.append("-" * 72)
for c in sorted(report["improvements"], key=lambda x: x["mean_delta_pct"]):
lines.append(f" [IMPROVE] {c['name']}")
lines.append(f" Mean: {c['baseline_mean_ms']:.2f}ms -> {c['current_mean_ms']:.2f}ms ({format_delta(c['mean_delta_pct'])})")
if c.get("current_ops_per_sec"):
lines.append(f" Ops/s: {c['baseline_ops_per_sec']:.0f} -> {c['current_ops_per_sec']:.0f} ({format_delta(c['ops_delta_pct'])})")
lines.append("")
if report["stable"]:
lines.append("-" * 72)
lines.append("STABLE (within threshold)")
lines.append("-" * 72)
for c in sorted(report["stable"], key=lambda x: x["name"]):
lines.append(f" [STABLE ] {c['name']} | Mean: {c['current_mean_ms']:.2f}ms ({format_delta(c['mean_delta_pct'])})")
lines.append("")
if report["new_benchmarks"]:
lines.append("-" * 72)
lines.append("NEW BENCHMARKS (no baseline)")
lines.append("-" * 72)
for c in report["new_benchmarks"]:
lines.append(f" [NEW ] {c['name']} | Mean: {c['current_mean_ms']:.2f}ms")
lines.append("")
if report["removed_benchmarks"]:
lines.append("-" * 72)
lines.append("REMOVED BENCHMARKS (no longer present)")
lines.append("-" * 72)
for c in report["removed_benchmarks"]:
lines.append(f" [REMOVED] {c['name']} | Was: {c['baseline_mean_ms']:.2f}ms")
lines.append("")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Parse benchmark results and generate comparison reports with regression detection.",
epilog='Input: JSON with "baseline" and "current" arrays of benchmark entries.',
)
parser.add_argument("input", nargs="?", default="-",
help="Input file path or '-' for stdin (default: stdin)")
parser.add_argument("--regression-threshold", type=float, default=5.0,
help="Percentage increase in latency to flag as regression (default: 5.0)")
parser.add_argument("--improvement-threshold", type=float, default=5.0,
help="Percentage decrease in latency to flag as improvement (default: 5.0)")
parser.add_argument("--fail-on-regression", action="store_true",
help="Exit with code 1 if any regressions are detected (useful for CI)")
parser.add_argument("--json", action="store_true",
help="Output results as JSON")
args = parser.parse_args()
try:
data = parse_input(args.input)
except (json.JSONDecodeError, FileNotFoundError) as e:
print(f"Error reading input: {e}", file=sys.stderr)
sys.exit(1)
baseline = data.get("baseline") or data.get("before") or []
current = data.get("current") or data.get("after") or data.get("results") or []
if not baseline:
print("Error: no baseline benchmark data found (expected 'baseline' or 'before' key)", file=sys.stderr)
sys.exit(1)
if not current:
print("Error: no current benchmark data found (expected 'current' or 'after' key)", file=sys.stderr)
sys.exit(1)
comparisons = compare_benchmarks(baseline, current, args.regression_threshold, args.improvement_threshold)
report = build_report(comparisons, args.regression_threshold)
if args.json:
print(json.dumps(report, indent=2))
else:
print(format_human(report))
if args.fail_on_regression and report["summary"]["has_regressions"]:
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Bottleneck Detector — Analyze application logs/traces to identify performance bottlenecks.
Parses JSON-formatted request logs or trace data and detects:
- Slow endpoints exceeding latency thresholds
- N+1 query patterns (repeated similar queries per request)
- High-latency external calls (DB, HTTP, cache)
- Endpoints with high variance (unstable performance)
Input format: JSON array of request log entries or newline-delimited JSON.
Each entry should have: endpoint, duration_ms, and optionally queries/spans.
"""
import argparse
import json
import math
import sys
from collections import defaultdict
def parse_input(source):
"""Read JSON log entries from file or stdin."""
if source == "-":
raw = sys.stdin.read()
else:
with open(source, "r") as f:
raw = f.read()
raw = raw.strip()
if not raw:
print("Error: empty input", file=sys.stderr)
sys.exit(1)
# Support both JSON array and newline-delimited JSON
if raw.startswith("["):
return json.loads(raw)
else:
entries = []
for line in raw.splitlines():
line = line.strip()
if line:
entries.append(json.loads(line))
return entries
def percentile(values, pct):
"""Calculate the given percentile from a sorted list of values."""
if not values:
return 0
sorted_vals = sorted(values)
k = (len(sorted_vals) - 1) * (pct / 100.0)
f = math.floor(k)
c = math.ceil(k)
if f == c:
return sorted_vals[int(k)]
return sorted_vals[f] * (c - k) + sorted_vals[c] * (k - f)
def stddev(values):
"""Calculate standard deviation."""
if len(values) < 2:
return 0.0
mean = sum(values) / len(values)
variance = sum((x - mean) ** 2 for x in values) / (len(values) - 1)
return math.sqrt(variance)
def detect_slow_endpoints(entries, threshold_ms):
"""Find endpoints where P95 latency exceeds the threshold."""
by_endpoint = defaultdict(list)
for entry in entries:
ep = entry.get("endpoint") or entry.get("path") or entry.get("url", "unknown")
dur = entry.get("duration_ms") or entry.get("latency_ms") or entry.get("response_time_ms", 0)
by_endpoint[ep].append(dur)
findings = []
for ep, durations in sorted(by_endpoint.items()):
p50 = percentile(durations, 50)
p95 = percentile(durations, 95)
p99 = percentile(durations, 99)
avg = sum(durations) / len(durations)
if p95 > threshold_ms:
findings.append({
"endpoint": ep,
"request_count": len(durations),
"avg_ms": round(avg, 2),
"p50_ms": round(p50, 2),
"p95_ms": round(p95, 2),
"p99_ms": round(p99, 2),
"max_ms": round(max(durations), 2),
"severity": "critical" if p95 > threshold_ms * 3 else "warning",
})
findings.sort(key=lambda x: x["p95_ms"], reverse=True)
return findings
def detect_n_plus_one(entries, query_threshold):
"""Detect N+1 query patterns: requests with excessive query counts."""
findings = []
by_endpoint = defaultdict(list)
for entry in entries:
queries = entry.get("queries") or entry.get("db_queries") or []
query_count = entry.get("query_count", len(queries))
ep = entry.get("endpoint") or entry.get("path") or entry.get("url", "unknown")
by_endpoint[ep].append({
"query_count": query_count,
"queries": queries,
"duration_ms": entry.get("duration_ms", 0),
})
for ep, reqs in sorted(by_endpoint.items()):
counts = [r["query_count"] for r in reqs]
avg_count = sum(counts) / len(counts) if counts else 0
max_count = max(counts) if counts else 0
if avg_count > query_threshold:
# Look for repeated query templates as N+1 evidence
all_queries = []
for r in reqs:
all_queries.extend(r.get("queries", []))
template_counts = defaultdict(int)
for q in all_queries:
tpl = q.get("template") or q.get("sql") or str(q)
template_counts[tpl] += 1
repeated = {t: c for t, c in template_counts.items() if c > len(reqs) * 2}
findings.append({
"endpoint": ep,
"request_count": len(reqs),
"avg_queries_per_request": round(avg_count, 1),
"max_queries_per_request": max_count,
"suspected_n_plus_one": bool(repeated),
"repeated_query_templates": len(repeated),
"severity": "critical" if avg_count > query_threshold * 3 else "warning",
})
findings.sort(key=lambda x: x["avg_queries_per_request"], reverse=True)
return findings
def detect_high_latency_spans(entries, span_threshold_ms):
"""Identify high-latency spans (DB calls, HTTP calls, cache lookups)."""
span_stats = defaultdict(list)
for entry in entries:
spans = entry.get("spans") or entry.get("traces") or []
for span in spans:
name = span.get("name") or span.get("operation") or "unknown"
stype = span.get("type") or span.get("service") or "unknown"
dur = span.get("duration_ms") or span.get("latency_ms", 0)
key = f"{stype}:{name}"
span_stats[key].append(dur)
findings = []
for key, durations in sorted(span_stats.items()):
p95 = percentile(durations, 95)
avg = sum(durations) / len(durations)
if p95 > span_threshold_ms:
stype, name = key.split(":", 1)
findings.append({
"span": name,
"type": stype,
"call_count": len(durations),
"avg_ms": round(avg, 2),
"p95_ms": round(p95, 2),
"max_ms": round(max(durations), 2),
"severity": "critical" if p95 > span_threshold_ms * 3 else "warning",
})
findings.sort(key=lambda x: x["p95_ms"], reverse=True)
return findings
def detect_high_variance(entries, cv_threshold):
"""Find endpoints with high coefficient of variation (unstable performance)."""
by_endpoint = defaultdict(list)
for entry in entries:
ep = entry.get("endpoint") or entry.get("path") or entry.get("url", "unknown")
dur = entry.get("duration_ms") or entry.get("latency_ms") or entry.get("response_time_ms", 0)
by_endpoint[ep].append(dur)
findings = []
for ep, durations in sorted(by_endpoint.items()):
if len(durations) < 5:
continue
avg = sum(durations) / len(durations)
if avg == 0:
continue
sd = stddev(durations)
cv = sd / avg
if cv > cv_threshold:
findings.append({
"endpoint": ep,
"request_count": len(durations),
"avg_ms": round(avg, 2),
"stddev_ms": round(sd, 2),
"coefficient_of_variation": round(cv, 3),
"min_ms": round(min(durations), 2),
"max_ms": round(max(durations), 2),
"severity": "warning",
})
findings.sort(key=lambda x: x["coefficient_of_variation"], reverse=True)
return findings
def format_human(report):
"""Format the report as human-readable text."""
lines = []
lines.append("=" * 70)
lines.append("PERFORMANCE BOTTLENECK ANALYSIS REPORT")
lines.append("=" * 70)
lines.append(f"Total requests analyzed: {report['summary']['total_requests']}")
lines.append(f"Unique endpoints: {report['summary']['unique_endpoints']}")
lines.append(f"Total findings: {report['summary']['total_findings']}")
lines.append(f"Critical: {report['summary']['critical_count']} | Warning: {report['summary']['warning_count']}")
lines.append("")
if report["slow_endpoints"]:
lines.append("-" * 70)
lines.append("SLOW ENDPOINTS (P95 exceeds threshold)")
lines.append("-" * 70)
for f in report["slow_endpoints"]:
sev = "CRITICAL" if f["severity"] == "critical" else "WARNING "
lines.append(f" [{sev}] {f['endpoint']}")
lines.append(f" Requests: {f['request_count']} | Avg: {f['avg_ms']}ms | P95: {f['p95_ms']}ms | P99: {f['p99_ms']}ms | Max: {f['max_ms']}ms")
lines.append("")
if report["n_plus_one"]:
lines.append("-" * 70)
lines.append("N+1 QUERY PATTERNS")
lines.append("-" * 70)
for f in report["n_plus_one"]:
sev = "CRITICAL" if f["severity"] == "critical" else "WARNING "
n1 = " [N+1 CONFIRMED]" if f["suspected_n_plus_one"] else ""
lines.append(f" [{sev}] {f['endpoint']}{n1}")
lines.append(f" Avg queries/req: {f['avg_queries_per_request']} | Max: {f['max_queries_per_request']} | Repeated templates: {f['repeated_query_templates']}")
lines.append("")
if report["high_latency_spans"]:
lines.append("-" * 70)
lines.append("HIGH-LATENCY SPANS")
lines.append("-" * 70)
for f in report["high_latency_spans"]:
sev = "CRITICAL" if f["severity"] == "critical" else "WARNING "
lines.append(f" [{sev}] {f['type']}:{f['span']}")
lines.append(f" Calls: {f['call_count']} | Avg: {f['avg_ms']}ms | P95: {f['p95_ms']}ms | Max: {f['max_ms']}ms")
lines.append("")
if report["high_variance"]:
lines.append("-" * 70)
lines.append("HIGH VARIANCE ENDPOINTS (unstable performance)")
lines.append("-" * 70)
for f in report["high_variance"]:
lines.append(f" [WARNING ] {f['endpoint']}")
lines.append(f" CV: {f['coefficient_of_variation']} | Avg: {f['avg_ms']}ms | StdDev: {f['stddev_ms']}ms | Range: {f['min_ms']}-{f['max_ms']}ms")
lines.append("")
if report["summary"]["total_findings"] == 0:
lines.append("No bottlenecks detected. All endpoints within thresholds.")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Analyze application logs/traces to identify performance bottlenecks.",
epilog="Input: JSON array or newline-delimited JSON with request log entries.",
)
parser.add_argument("input", nargs="?", default="-",
help="Input file path or '-' for stdin (default: stdin)")
parser.add_argument("--latency-threshold", type=float, default=200,
help="P95 latency threshold in ms for slow endpoint detection (default: 200)")
parser.add_argument("--query-threshold", type=int, default=10,
help="Avg queries per request threshold for N+1 detection (default: 10)")
parser.add_argument("--span-threshold", type=float, default=100,
help="P95 span latency threshold in ms (default: 100)")
parser.add_argument("--cv-threshold", type=float, default=1.5,
help="Coefficient of variation threshold for high variance detection (default: 1.5)")
parser.add_argument("--json", action="store_true",
help="Output results as JSON")
args = parser.parse_args()
try:
entries = parse_input(args.input)
except (json.JSONDecodeError, FileNotFoundError) as e:
print(f"Error reading input: {e}", file=sys.stderr)
sys.exit(1)
if not entries:
print("Error: no log entries found in input", file=sys.stderr)
sys.exit(1)
slow = detect_slow_endpoints(entries, args.latency_threshold)
n1 = detect_n_plus_one(entries, args.query_threshold)
spans = detect_high_latency_spans(entries, args.span_threshold)
variance = detect_high_variance(entries, args.cv_threshold)
all_findings = slow + n1 + spans + variance
endpoints = set()
for e in entries:
ep = e.get("endpoint") or e.get("path") or e.get("url", "unknown")
endpoints.add(ep)
report = {
"summary": {
"total_requests": len(entries),
"unique_endpoints": len(endpoints),
"total_findings": len(all_findings),
"critical_count": sum(1 for f in all_findings if f.get("severity") == "critical"),
"warning_count": sum(1 for f in all_findings if f.get("severity") == "warning"),
},
"slow_endpoints": slow,
"n_plus_one": n1,
"high_latency_spans": spans,
"high_variance": variance,
}
if args.json:
print(json.dumps(report, indent=2))
else:
print(format_human(report))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Resource Analyzer — Analyze CPU, memory, and disk usage data for anomalies and trends.
Reads time-series resource usage data in JSON format and detects:
- Sustained high utilization (CPU, memory, disk above thresholds)
- Memory leaks (monotonically increasing memory over time)
- CPU spikes (sudden jumps in CPU usage)
- Disk pressure (approaching capacity limits)
- Trend analysis with linear regression for capacity planning
Input format: JSON array of timestamped resource snapshots.
Each entry should have: timestamp, and cpu/memory/disk metrics.
"""
import argparse
import json
import math
import sys
from collections import defaultdict
from datetime import datetime
def parse_input(source):
"""Read JSON resource data from file or stdin."""
if source == "-":
raw = sys.stdin.read()
else:
with open(source, "r") as f:
raw = f.read()
raw = raw.strip()
if not raw:
print("Error: empty input", file=sys.stderr)
sys.exit(1)
if raw.startswith("["):
return json.loads(raw)
else:
entries = []
for line in raw.splitlines():
line = line.strip()
if line:
entries.append(json.loads(line))
return entries
def parse_timestamp(ts):
"""Try to parse various timestamp formats into epoch seconds."""
if isinstance(ts, (int, float)):
# Already epoch seconds or milliseconds
if ts > 1e12:
return ts / 1000.0
return float(ts)
if isinstance(ts, str):
for fmt in ("%Y-%m-%dT%H:%M:%S.%fZ", "%Y-%m-%dT%H:%M:%SZ",
"%Y-%m-%dT%H:%M:%S%z", "%Y-%m-%dT%H:%M:%S",
"%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M:%S.%f"):
try:
dt = datetime.strptime(ts.replace("+00:00", "Z").rstrip("Z") if "Z" not in fmt else ts, fmt)
return dt.timestamp()
except (ValueError, AttributeError):
continue
return 0.0
def extract_metric(entry, keys, default=None):
"""Extract a metric value trying multiple key names."""
for key in keys:
if key in entry:
val = entry[key]
if isinstance(val, dict):
return val
return val
return default
def linear_regression(x_values, y_values):
"""Simple linear regression returning slope, intercept, and R-squared."""
n = len(x_values)
if n < 2:
return 0, 0, 0
sum_x = sum(x_values)
sum_y = sum(y_values)
sum_xy = sum(x * y for x, y in zip(x_values, y_values))
sum_x2 = sum(x * x for x in x_values)
sum_y2 = sum(y * y for y in y_values)
denom = n * sum_x2 - sum_x * sum_x
if denom == 0:
return 0, sum_y / n if n else 0, 0
slope = (n * sum_xy - sum_x * sum_y) / denom
intercept = (sum_y - slope * sum_x) / n
# R-squared
ss_tot = sum_y2 - (sum_y ** 2) / n
ss_res = sum((y - (slope * x + intercept)) ** 2 for x, y in zip(x_values, y_values))
r_squared = 1 - (ss_res / ss_tot) if ss_tot != 0 else 0
return slope, intercept, r_squared
def analyze_cpu(entries, cpu_threshold):
"""Analyze CPU usage patterns."""
cpu_values = []
timestamps = []
for entry in entries:
cpu = extract_metric(entry, ["cpu_percent", "cpu_usage", "cpu", "cpu_pct"])
if cpu is None:
continue
if isinstance(cpu, dict):
cpu = cpu.get("percent") or cpu.get("usage") or cpu.get("total", 0)
cpu_values.append(float(cpu))
ts = extract_metric(entry, ["timestamp", "ts", "time", "date"], 0)
timestamps.append(parse_timestamp(ts))
if not cpu_values:
return None
avg = sum(cpu_values) / len(cpu_values)
peak = max(cpu_values)
minimum = min(cpu_values)
# Detect spikes: values > 2 standard deviations above mean
if len(cpu_values) >= 3:
mean = avg
sd = math.sqrt(sum((x - mean) ** 2 for x in cpu_values) / len(cpu_values))
spike_threshold = mean + 2 * sd if sd > 0 else cpu_threshold
spikes = sum(1 for v in cpu_values if v > spike_threshold)
else:
spikes = 0
# Sustained high usage: percentage of samples above threshold
high_samples = sum(1 for v in cpu_values if v > cpu_threshold)
sustained_pct = (high_samples / len(cpu_values)) * 100
# Trend
if timestamps and timestamps[0] > 0 and len(timestamps) >= 3:
t_norm = [(t - timestamps[0]) / 3600.0 for t in timestamps] # hours
slope, _, r2 = linear_regression(t_norm, cpu_values)
else:
slope, r2 = 0, 0
findings = []
if sustained_pct > 50:
findings.append({
"type": "sustained_high_cpu",
"severity": "critical" if sustained_pct > 80 else "warning",
"detail": f"CPU above {cpu_threshold}% for {sustained_pct:.1f}% of samples",
})
if spikes > 0:
findings.append({
"type": "cpu_spikes",
"severity": "warning",
"detail": f"{spikes} spike(s) detected (>2 std devs above mean)",
})
if slope > 1.0 and r2 > 0.5:
findings.append({
"type": "cpu_trend_increasing",
"severity": "warning",
"detail": f"CPU trending up at {slope:.2f}%/hour (R²={r2:.2f})",
})
return {
"samples": len(cpu_values),
"avg_percent": round(avg, 2),
"min_percent": round(minimum, 2),
"max_percent": round(peak, 2),
"sustained_high_pct": round(sustained_pct, 1),
"spike_count": spikes,
"trend_slope_per_hour": round(slope, 3),
"trend_r_squared": round(r2, 3),
"findings": findings,
}
def analyze_memory(entries, mem_threshold):
"""Analyze memory usage patterns, detect leaks."""
mem_values = []
timestamps = []
for entry in entries:
mem = extract_metric(entry, ["memory_percent", "memory_usage", "memory", "mem_pct", "mem_percent"])
if mem is None:
mem_mb = extract_metric(entry, ["memory_mb", "mem_mb", "rss_mb"])
mem_total = extract_metric(entry, ["memory_total_mb", "total_memory_mb"])
if mem_mb is not None and mem_total and mem_total > 0:
mem = (float(mem_mb) / float(mem_total)) * 100
elif mem_mb is not None:
mem = float(mem_mb) # Use raw MB if no total available
else:
continue
if isinstance(mem, dict):
mem = mem.get("percent") or mem.get("usage") or mem.get("used_pct", 0)
mem_values.append(float(mem))
ts = extract_metric(entry, ["timestamp", "ts", "time", "date"], 0)
timestamps.append(parse_timestamp(ts))
if not mem_values:
return None
avg = sum(mem_values) / len(mem_values)
peak = max(mem_values)
minimum = min(mem_values)
high_samples = sum(1 for v in mem_values if v > mem_threshold)
sustained_pct = (high_samples / len(mem_values)) * 100
# Memory leak detection via linear regression
if timestamps and timestamps[0] > 0 and len(timestamps) >= 3:
t_norm = [(t - timestamps[0]) / 3600.0 for t in timestamps]
slope, _, r2 = linear_regression(t_norm, mem_values)
else:
# Use index as proxy
indices = list(range(len(mem_values)))
slope, _, r2 = linear_regression(indices, mem_values)
# Monotonic increase check (leak indicator)
if len(mem_values) >= 5:
window = max(1, len(mem_values) // 5)
windows = [mem_values[i:i + window] for i in range(0, len(mem_values), window)]
window_avgs = [sum(w) / len(w) for w in windows if w]
monotonic = all(window_avgs[i] <= window_avgs[i + 1] for i in range(len(window_avgs) - 1))
else:
monotonic = False
findings = []
if sustained_pct > 50:
findings.append({
"type": "sustained_high_memory",
"severity": "critical" if sustained_pct > 80 else "warning",
"detail": f"Memory above {mem_threshold}% for {sustained_pct:.1f}% of samples",
})
if slope > 0.5 and r2 > 0.7 and monotonic:
findings.append({
"type": "probable_memory_leak",
"severity": "critical",
"detail": f"Memory monotonically increasing at {slope:.2f} units/hour (R²={r2:.2f})",
})
elif slope > 0.5 and r2 > 0.5:
findings.append({
"type": "memory_trend_increasing",
"severity": "warning",
"detail": f"Memory trending up at {slope:.2f} units/hour (R²={r2:.2f})",
})
return {
"samples": len(mem_values),
"avg_percent": round(avg, 2),
"min_percent": round(minimum, 2),
"max_percent": round(peak, 2),
"sustained_high_pct": round(sustained_pct, 1),
"trend_slope_per_hour": round(slope, 3),
"trend_r_squared": round(r2, 3),
"monotonic_increase": monotonic,
"findings": findings,
}
def analyze_disk(entries, disk_threshold):
"""Analyze disk usage and detect capacity pressure."""
disk_values = []
timestamps = []
for entry in entries:
disk = extract_metric(entry, ["disk_percent", "disk_usage", "disk", "disk_pct"])
if disk is None:
disk_used = extract_metric(entry, ["disk_used_gb", "disk_used_mb"])
disk_total = extract_metric(entry, ["disk_total_gb", "disk_total_mb"])
if disk_used is not None and disk_total and float(disk_total) > 0:
disk = (float(disk_used) / float(disk_total)) * 100
else:
continue
if isinstance(disk, dict):
disk = disk.get("percent") or disk.get("usage") or disk.get("used_pct", 0)
disk_values.append(float(disk))
ts = extract_metric(entry, ["timestamp", "ts", "time", "date"], 0)
timestamps.append(parse_timestamp(ts))
if not disk_values:
return None
avg = sum(disk_values) / len(disk_values)
current = disk_values[-1]
peak = max(disk_values)
# Trend for capacity planning
if timestamps and timestamps[0] > 0 and len(timestamps) >= 3:
t_norm = [(t - timestamps[0]) / 3600.0 for t in timestamps]
slope, intercept, r2 = linear_regression(t_norm, disk_values)
# Estimate hours until threshold
if slope > 0 and r2 > 0.5:
hours_to_threshold = (disk_threshold - current) / slope if current < disk_threshold else 0
hours_to_full = (100 - current) / slope
else:
hours_to_threshold = None
hours_to_full = None
else:
slope, r2 = 0, 0
hours_to_threshold = None
hours_to_full = None
findings = []
if current > disk_threshold:
findings.append({
"type": "disk_pressure",
"severity": "critical" if current > 95 else "warning",
"detail": f"Disk usage at {current:.1f}%, above {disk_threshold}% threshold",
})
if hours_to_full is not None and hours_to_full < 168: # less than 7 days
days = hours_to_full / 24
findings.append({
"type": "disk_capacity_warning",
"severity": "critical" if days < 2 else "warning",
"detail": f"At current growth rate, disk reaches 100% in {days:.1f} days",
})
result = {
"samples": len(disk_values),
"avg_percent": round(avg, 2),
"current_percent": round(current, 2),
"peak_percent": round(peak, 2),
"trend_slope_per_hour": round(slope, 3),
"trend_r_squared": round(r2, 3),
"findings": findings,
}
if hours_to_full is not None:
result["estimated_days_to_full"] = round(hours_to_full / 24, 1)
return result
def format_human(report):
"""Format the report as human-readable text."""
lines = []
lines.append("=" * 70)
lines.append("RESOURCE USAGE ANALYSIS REPORT")
lines.append("=" * 70)
s = report["summary"]
lines.append(f"Total samples: {s['total_samples']} | Findings: {s['total_findings']}")
lines.append(f"Critical: {s['critical_count']} | Warning: {s['warning_count']}")
lines.append("")
for section_key, title in [("cpu", "CPU ANALYSIS"), ("memory", "MEMORY ANALYSIS"), ("disk", "DISK ANALYSIS")]:
data = report.get(section_key)
if data is None:
continue
lines.append("-" * 70)
lines.append(title)
lines.append("-" * 70)
if section_key == "cpu":
lines.append(f" Samples: {data['samples']} | Avg: {data['avg_percent']}% | Min: {data['min_percent']}% | Max: {data['max_percent']}%")
lines.append(f" Sustained high: {data['sustained_high_pct']}% of samples | Spikes: {data['spike_count']}")
lines.append(f" Trend: {data['trend_slope_per_hour']:+.3f}%/hr (R²={data['trend_r_squared']:.3f})")
elif section_key == "memory":
lines.append(f" Samples: {data['samples']} | Avg: {data['avg_percent']}% | Min: {data['min_percent']}% | Max: {data['max_percent']}%")
lines.append(f" Sustained high: {data['sustained_high_pct']}% of samples | Monotonic increase: {'Yes' if data['monotonic_increase'] else 'No'}")
lines.append(f" Trend: {data['trend_slope_per_hour']:+.3f} units/hr (R²={data['trend_r_squared']:.3f})")
elif section_key == "disk":
lines.append(f" Samples: {data['samples']} | Avg: {data['avg_percent']}% | Current: {data['current_percent']}% | Peak: {data['peak_percent']}%")
lines.append(f" Trend: {data['trend_slope_per_hour']:+.3f}%/hr (R²={data['trend_r_squared']:.3f})")
if "estimated_days_to_full" in data:
lines.append(f" Estimated days to 100%: {data['estimated_days_to_full']}")
for f in data.get("findings", []):
sev = "CRITICAL" if f["severity"] == "critical" else "WARNING "
lines.append(f" [{sev}] {f['detail']}")
lines.append("")
if s["total_findings"] == 0:
lines.append("All resource metrics within normal thresholds.")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Analyze resource usage data (CPU, memory, disk) and flag anomalies and trends.",
epilog="Input: JSON array of timestamped resource snapshots.",
)
parser.add_argument("input", nargs="?", default="-",
help="Input file path or '-' for stdin (default: stdin)")
parser.add_argument("--cpu-threshold", type=float, default=80,
help="CPU usage percent threshold for high-usage alerts (default: 80)")
parser.add_argument("--memory-threshold", type=float, default=85,
help="Memory usage percent threshold for high-usage alerts (default: 85)")
parser.add_argument("--disk-threshold", type=float, default=90,
help="Disk usage percent threshold for capacity alerts (default: 90)")
parser.add_argument("--json", action="store_true",
help="Output results as JSON")
args = parser.parse_args()
try:
entries = parse_input(args.input)
except (json.JSONDecodeError, FileNotFoundError) as e:
print(f"Error reading input: {e}", file=sys.stderr)
sys.exit(1)
if not entries:
print("Error: no resource data entries found", file=sys.stderr)
sys.exit(1)
cpu_result = analyze_cpu(entries, args.cpu_threshold)
mem_result = analyze_memory(entries, args.memory_threshold)
disk_result = analyze_disk(entries, args.disk_threshold)
all_findings = []
total_samples = 0
for result in [cpu_result, mem_result, disk_result]:
if result:
all_findings.extend(result.get("findings", []))
total_samples = max(total_samples, result.get("samples", 0))
report = {
"summary": {
"total_samples": total_samples,
"total_findings": len(all_findings),
"critical_count": sum(1 for f in all_findings if f["severity"] == "critical"),
"warning_count": sum(1 for f in all_findings if f["severity"] == "warning"),
},
"cpu": cpu_result,
"memory": mem_result,
"disk": disk_result,
}
if args.json:
print(json.dumps(report, indent=2))
else:
print(format_human(report))
if __name__ == "__main__":
main()
Related skills
FAQ
Which runtimes does it profile?
Node.js (Clinic.js, V8), Python (py-spy, cProfile, scalene, memray), and Go (pprof).
What methodology does it enforce?
Measure-first: establish a baseline, get profiler evidence, apply the fix, measure again, and calculate the improvement delta.