| 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]}..." |
|
|