playbook/antigravity-awesome-skills/skills/skill-sentinel/scripts/analyzers/performance.py

165 lines
6.2 KiB
Python

"""
Analyzer de performance.
Verifica: chamadas API sequenciais, caching, N+1 queries,
connection reuse, retry/backoff, timeouts.
"""
from __future__ import annotations
import re
from pathlib import Path
from typing import Any, Dict, List, Tuple
def analyze(skill_data: Dict[str, Any]) -> Tuple[float, List[Dict[str, Any]]]:
"""Analisa performance de uma skill. Retorna (score, findings)."""
score = 100.0
findings: List[Dict[str, Any]] = []
skill_name = skill_data["name"]
skill_path = Path(skill_data["path"])
has_retry = False
has_timeout = False
has_caching = False
has_connection_pool = False
has_async = False
has_concurrency = False
api_call_files = []
for rel_path in skill_data.get("python_files", []):
filepath = skill_path / rel_path
if not filepath.exists():
continue
try:
source = filepath.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
# Detectar patterns de performance
if re.search(r'(?:retry|backoff|MAX_RETRIES|RETRY_BACKOFF)', source, re.I):
has_retry = True
if re.search(r'(?:timeout|REQUEST_TIMEOUT)', source, re.I):
has_timeout = True
if re.search(r'(?:cache|lru_cache|functools\.cache|_cache)', source, re.I):
has_caching = True
if re.search(r'(?:session|Session\(\)|httpx\.Client)', source, re.I):
has_connection_pool = True
if re.search(r'(?:async\s+def|asyncio|aiohttp|httpx\.AsyncClient)', source):
has_async = True
if re.search(r'(?:concurrent|ThreadPool|ProcessPool|asyncio\.gather|--concurrency)', source, re.I):
has_concurrency = True
# Contar chamadas API (requests.get/post, httpx, etc)
api_calls = len(re.findall(
r'(?:requests\.\w+|httpx\.\w+|self\.\w*(?:get|post|put|delete|patch))\s*\(',
source
))
if api_calls > 0:
api_call_files.append((rel_path, api_calls))
# Detectar N+1 patterns (loop com query SQL dentro)
# Analise linha-a-linha para evitar backtracking em regex DOTALL
lines = source.splitlines()
in_for_loop = False
for line_text in lines:
stripped = line_text.strip()
if stripped.startswith("for ") and stripped.endswith(":"):
in_for_loop = True
elif in_for_loop and stripped and not stripped[0].isspace() and not line_text[0].isspace():
in_for_loop = False
elif in_for_loop and re.search(r'(?:SELECT|\.execute)\s*\(', stripped):
findings.append({
"skill_name": skill_name,
"dimension": "performance",
"severity": "medium",
"category": "n_plus_1",
"title": f"Possivel N+1 query em {rel_path}",
"description": "Loop com query SQL pode causar muitas chamadas ao banco",
"file_path": rel_path,
"recommendation": "Carregar dados em batch antes do loop",
"effort": "medium",
"impact": "high",
})
score -= 8
break
# Criar conexao dentro de loop (analise simples)
has_connect_in_loop = False
in_for_loop = False
for line_text in lines:
stripped = line_text.strip()
if stripped.startswith("for ") and stripped.endswith(":"):
in_for_loop = True
elif in_for_loop and stripped and not line_text[0].isspace():
in_for_loop = False
elif in_for_loop and "_connect()" in stripped:
has_connect_in_loop = True
break
if has_connect_in_loop:
findings.append({
"skill_name": skill_name,
"dimension": "performance",
"severity": "medium",
"category": "connection_per_iteration",
"title": f"Conexao criada dentro de loop em {rel_path}",
"file_path": rel_path,
"recommendation": "Mover _connect() para fora do loop, reutilizar conexao",
"effort": "low",
"impact": "high",
})
score -= 5
# Verificar ausencia de boas praticas
if not has_retry and api_call_files:
findings.append({
"skill_name": skill_name,
"dimension": "performance",
"severity": "medium",
"category": "no_retry",
"title": "Sem retry/backoff para chamadas API",
"description": f"Encontradas chamadas API em {len(api_call_files)} arquivo(s) sem mecanismo de retry",
"recommendation": "Implementar retry com exponential backoff (ex: tenacity, ou manual)",
"effort": "medium",
"impact": "high",
})
score -= 10
if not has_timeout and api_call_files:
findings.append({
"skill_name": skill_name,
"dimension": "performance",
"severity": "medium",
"category": "no_timeout",
"title": "Sem timeout configurado para chamadas HTTP",
"recommendation": "Adicionar timeout= em todas as chamadas requests/httpx",
"effort": "low",
"impact": "medium",
})
score -= 5
if not has_connection_pool and len(api_call_files) > 2:
findings.append({
"skill_name": skill_name,
"dimension": "performance",
"severity": "low",
"category": "no_connection_reuse",
"title": "Sem reuso de conexoes HTTP",
"description": "Multiplos arquivos fazem chamadas HTTP sem Session/Client compartilhado",
"recommendation": "Usar requests.Session() ou httpx.Client() para reutilizar conexoes",
"effort": "low",
"impact": "medium",
})
score -= 3
# Bonus
if has_retry:
score = min(100.0, score + 5)
if has_async or has_concurrency:
score = min(100.0, score + 5)
if has_caching:
score = min(100.0, score + 3)
return max(0.0, min(100.0, score)), findings