SwarmAudit / app /agents /performance_agent.py
Pranoy Mukherjee
Update SwarmAudit Space demo
9237011
Raw
History Blame Contribute Delete
6.75 kB
import re
from app.agents.llm_enrichment import LLMEnrichmentMixin
from app.config import Settings
from app.schemas import AgentOutput, CodeChunk, Finding, Severity
from app.services.llm_client import LLMClient
REQUEST_WITHOUT_TIMEOUT = re.compile(r"\brequests\.(get|post|put|patch|delete)\s*\((?!.*\btimeout\s*=)")
SYNC_FS_JS = re.compile(r"\b(readFileSync|writeFileSync|readdirSync|statSync)\s*\(")
PYTHON_LOOP = re.compile(r"^(\s*)(for|while)\b")
PYTHON_FILE_READ = re.compile(r"\b(open\s*\(|Path\s*\([^)]*\)\.read_(text|bytes)\s*\()")
class PerformanceAgent(LLMEnrichmentMixin):
name = "Performance Agent"
def __init__(self, llm_client: LLMClient | None = None):
self.llm_client = llm_client or LLMClient(Settings())
async def analyze(self, chunks: list[CodeChunk]) -> AgentOutput:
findings: list[Finding] = []
for chunk in chunks:
findings.extend(self._scan_chunk(chunk))
llm_output = await self._run_llm_enrichment(
chunks,
"Review these code chunks for high-confidence performance issues such as algorithmic bottlenecks, blocking I/O, inefficient repeated work, or expensive hot paths.",
)
findings.extend(llm_output.findings)
return AgentOutput(
agent_name=self.name,
findings=findings,
metadata=self._llm_metadata(chunks, llm_output),
)
def _scan_chunk(self, chunk: CodeChunk) -> list[Finding]:
findings: list[Finding] = []
lines = chunk.content.splitlines()
loop_stack: list[int] = []
async_indent_stack: list[int] = []
for offset, line in enumerate(lines):
actual_line = chunk.line_start + offset
stripped = line.strip()
indent = len(line) - len(line.lstrip(" "))
loop_stack = [loop_indent for loop_indent in loop_stack if indent > loop_indent]
async_indent_stack = [async_indent for async_indent in async_indent_stack if indent > async_indent]
if stripped.startswith("async def "):
async_indent_stack.append(indent)
loop_match = PYTHON_LOOP.match(line)
if loop_match:
if loop_stack:
findings.append(
self._finding(
"Nested loop may become expensive",
Severity.low,
chunk,
actual_line,
"A loop nested inside another loop can turn small inputs into slow O(n^2) work.",
"Consider indexing data with a dictionary/set, batching work, or documenting why nested iteration is bounded.",
)
)
loop_stack.append(len(loop_match.group(1)))
if REQUEST_WITHOUT_TIMEOUT.search(line):
call_snippet = self._snippet(line)
findings.append(
self._finding(
"HTTP request without timeout",
Severity.medium,
chunk,
actual_line,
f"`{call_snippet}` does not pass `timeout=`, so this request can wait indefinitely.",
f"Add a bounded timeout to this call, for example `{call_snippet.rstrip(')')}, timeout=10)` if the arguments fit that shape.",
why_it_matters="This specific network call can tie up a worker or thread when the remote service stalls.",
)
)
if async_indent_stack and "time.sleep(" in line:
sleep_snippet = self._snippet(line)
findings.append(
self._finding(
"Blocking sleep inside async function",
Severity.medium,
chunk,
actual_line,
f"`{sleep_snippet}` runs inside an async scope and blocks the event loop.",
"Replace this call with `await asyncio.sleep(...)` or move blocking work out of the async path.",
why_it_matters="Blocking the event loop here delays unrelated coroutines that should be able to keep running.",
)
)
if loop_stack and PYTHON_FILE_READ.search(line):
read_snippet = self._snippet(line)
findings.append(
self._finding(
"File read inside loop",
Severity.low,
chunk,
actual_line,
f"`{read_snippet}` appears inside a loop, so the same path may hit disk repeatedly.",
"Read once before the loop, cache by file path, or stream deliberately if every iteration needs fresh data.",
why_it_matters="Repeated disk I/O in this loop can dominate runtime as the input size grows.",
)
)
if SYNC_FS_JS.search(line):
fs_snippet = self._snippet(line)
findings.append(
self._finding(
"Synchronous filesystem call",
Severity.low,
chunk,
actual_line,
f"`{fs_snippet}` uses a synchronous filesystem API.",
"Use `fs.promises` or move this filesystem work outside latency-sensitive request paths.",
why_it_matters="This call blocks the Node.js event loop while disk I/O completes.",
)
)
return findings
def _finding(
self,
title: str,
severity: Severity,
chunk: CodeChunk,
line_number: int,
description: str,
suggested_fix: str,
why_it_matters: str | None = None,
) -> Finding:
return Finding(
title=title,
severity=severity,
file_path=chunk.file_path,
line_start=line_number,
line_end=line_number,
description=description,
why_it_matters=why_it_matters
or "Performance issues in hot paths can increase latency, resource usage, and demo analysis time.",
suggested_fix=suggested_fix,
agent_source=self.name,
)
def _snippet(self, line: str, max_length: int = 96) -> str:
normalized = " ".join(line.strip().split())
if len(normalized) <= max_length:
return normalized
return f"{normalized[: max_length - 3]}..."