Phase 1: profiling timers for /api/upload-project#318
Merged
Conversation
Adds ?profile=1 opt-in timing instrumentation so cold-cache latency on medium repos can be measured without adding any logging overhead to normal requests. All timers are guarded by the query param and never fire in production traffic. https://claude.ai/code/session_014rqwarDVK9fwzxWV2pzS2z
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
There was a problem hiding this comment.
Pull request overview
Adds opt-in profiling instrumentation to the /api/upload-project pipeline so users (or support/debug sessions) can measure where time is spent on cold-cache uploads without impacting normal requests.
Changes:
- Add
?profile=1support in/api/upload-project, returning an additional_profiledict in the response. - Instrument analyzer stages (walk, parse pass,
nx.compose_all) and accumulate parse timing/counts via a thread-local collector. - Instrument exporter stages (SCC/cycle detection and edge/output dict build) and plumb
_profilethrough.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
app/routers/upload.py |
Adds ?profile=1 flag handling, end-to-end timers for analyze/export, and conditionally returns _profile. |
analyst/analyzer.py |
Adds thread-local profiling accumulator and directory-analysis timers; plumbs optional _profile kwarg. |
analyst/exporter.py |
Adds optional _profile kwarg and timings for SCC and export build steps. |
Comments suppressed due to low confidence (1)
analyst/exporter.py:60
export_to_react_flow(..., _profile=...)adds new profiling outputs (scc_ms,export_build_ms) but there are no tests validating these keys are written when profiling is enabled. Sincetests/test_export.pyalready covers this exporter, consider adding a small test passing a dict for_profileand asserting the expected keys are present and numeric.
# Detect cycles
# Optimization: Use strongly_connected_components O(V+E) instead of simple_cycles O((V+E)C)
# An edge is part of a cycle if both endpoints belong to the same SCC of size > 1.
cycle_edges = set()
_t_scc = time.perf_counter() if _profile is not None else 0.0
try:
node_to_component = {}
for i, component in enumerate(nx.strongly_connected_components(graph)):
if len(component) > 1:
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
156
to
160
| tree = ast.parse(source, filename=filename) | ||
| with _AST_CACHE_LOCK: | ||
| _AST_CACHE[key] = tree | ||
| _AST_CACHE.move_to_end(key) | ||
| if len(_AST_CACHE) > _AST_CACHE_MAX: |
Comment on lines
+276
to
278
| if profile_data is not None: | ||
| return JSONResponse(content={**response_data, "_profile": profile_data}) | ||
| return response_data |
Comment on lines
+226
to
+231
| profile_mode = request.query_params.get("profile") == "1" | ||
| profile_data: dict | None = {} if profile_mode else None | ||
| _t0 = time.perf_counter() if profile_data is not None else 0.0 | ||
| result = CodeAnalyzer().analyze_file(tmp_dir, _profile=profile_data) | ||
| if profile_data is not None: | ||
| profile_data["analyze_total_ms"] = round((time.perf_counter() - _t0) * 1000, 2) |
Comment on lines
451
to
466
| @@ -445,7 +462,7 @@ def analyze_file(self, target_path: str) -> dict[str, Any]: | |||
| return {"error": f"Path not found: {safe_path}"} | |||
|
|
|||
| if os.path.isdir(target_path): | |||
| return self._analyze_directory(target_path) | |||
| return self._analyze_directory(target_path, _profile=_profile) | |||
| else: | |||
Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
Two weeks ago commit
d74265b("Perf: content-hash AST cache for /api/upload-project #250") landed. The follow-up question was whether first-upload (cold-cache) latency on medium repos is still dominant.Investigation result (2026-05-11): Zero open issues, zero performance complaints on GitHub since the cache merged. Production telemetry was not reachable from this run (no log access from a fresh session, as expected). Shipping the timers preemptively — they are gated behind
?profile=1so they are completely free in normal operation. The data they collect will answer the latency question on the next user upload.What this adds
?profile=1onPOST /api/upload-projectreturns a_profiledict alongside the normal response withtime.perf_counter()resolution on every significant work unit:walk_msos.scandirdirectory traversalparse_count_parse_cachedparse_cached_hitsparse_total_ms_parse_cachedtime across all filescompose_all_msnx.compose_all()graph mergescc_msnx.strongly_connected_components(cycle detection)export_build_msoutput_datadict constructionanalyze_total_msanalyze_file()callexport_total_msexport_to_react_flow()callInstrumented files
analyst/analyzer.py— thread-local_PROFILE_DATAaccumulator in_parse_cached; timing added to_analyze_directory(walk, parse pass,compose_all);analyze_filegains an optional_profile: dict | None = Nonekwarg.analyst/exporter.py—_profilekwarg onexport_to_react_flow; timers around the SCC and dict-build blocks.app/routers/upload.py— reads?profile=1, creates the collector dict, wraps both calls, returnsJSONResponse({…, "_profile": profile_data})only when profiling is enabled.No new dependencies. Normal requests have zero overhead (all guards are
if _profile is not None).Sample profile — VibeGraph itself (cold cache, 68 Python files, 1174 nodes / 2646 edges)
{ "walk_ms": 2.33, "parse_count": 68, "parse_cached_hits": 2, "parse_total_ms": 85.48, "compose_all_ms": 6.56, "scc_ms": 2.54, "export_build_ms": 2.71, "analyze_total_ms": 102.1, "export_total_ms": 5.9 }Reading:
parse_total_ms(85 ms) is ~83 % of theanalyze_total_msbudget on a cold cache.walk,compose_all, and the export steps are all sub-7 ms and not worth optimising yet. This confirms that any further latency work should focus on reducing cold-parse time (e.g. persistent on-disk AST cache or parallel parsing), not on the graph or export stages.Sample profile — synthetic 60-function file (cold cache)
{ "walk_ms": 0.08, "parse_count": 1, "parse_cached_hits": 0, "parse_total_ms": 1.57, "compose_all_ms": 0.41, "scc_ms": 0.34, "export_build_ms": 0.06 }Checklist
GROQ_API_KEY=dummy python3 -m pytest tests/ -q→ 251 passed, 0 failedruff check analyst/analyzer.py analyst/exporter.py app/routers/upload.py→ All checks passedrequirements.txt?profile=1) requestsGenerated by Claude Code