Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions code_review_graph/tools/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ def _run_postprocess(
When *full_rebuild* is False and *changed_files* are available,
uses incremental flow/community detection for faster updates.

Records structured stage durations in ``build_result["postprocess_timing"]``.
Minimal processing reports ``signatures_s`` and ``fts_s``; full processing
additionally reports ``flows_s``, ``communities_s``, and ``summaries_s``.
Each duration is a nonnegative float measured in seconds.

Returns a list of warning strings (empty on success).
"""
warnings: list[str] = []
Expand All @@ -34,6 +39,8 @@ def _run_postprocess(
return warnings

# -- Signatures + FTS (fast, always run unless "none") --
timing: dict[str, float] = {}
stage_started = time.perf_counter()
try:
rows = store.get_nodes_without_signature()
for row in rows:
Expand All @@ -58,7 +65,12 @@ def _run_postprocess(
except (sqlite3.OperationalError, TypeError, KeyError) as e:
logger.warning("Signature computation failed: %s", e)
warnings.append(f"Signature computation failed: {type(e).__name__}: {e}")
timing["signatures_s"] = max(
0.0,
round(time.perf_counter() - stage_started, 6),
)

stage_started = time.perf_counter()
try:
from code_review_graph.search import rebuild_fts_index

Expand All @@ -68,13 +80,19 @@ def _run_postprocess(
except (sqlite3.OperationalError, ImportError) as e:
logger.warning("FTS index rebuild failed: %s", e)
warnings.append(f"FTS index rebuild failed: {type(e).__name__}: {e}")
timing["fts_s"] = max(
0.0,
round(time.perf_counter() - stage_started, 6),
)

if postprocess == "minimal":
build_result["postprocess_timing"] = timing
return warnings

# -- Expensive: flows + communities (only for "full") --
use_incremental = not full_rebuild and bool(changed_files)

stage_started = time.perf_counter()
try:
if use_incremental:
from code_review_graph.flows import incremental_trace_flows
Expand All @@ -90,7 +108,12 @@ def _run_postprocess(
except (sqlite3.OperationalError, ImportError) as e:
logger.warning("Flow detection failed: %s", e)
warnings.append(f"Flow detection failed: {type(e).__name__}: {e}")
timing["flows_s"] = max(
0.0,
round(time.perf_counter() - stage_started, 6),
)

stage_started = time.perf_counter()
try:
if use_incremental:
from code_review_graph.communities import (
Expand All @@ -112,14 +135,24 @@ def _run_postprocess(
except (sqlite3.OperationalError, ImportError) as e:
logger.warning("Community detection failed: %s", e)
warnings.append(f"Community detection failed: {type(e).__name__}: {e}")
timing["communities_s"] = max(
0.0,
round(time.perf_counter() - stage_started, 6),
)

# -- Compute pre-computed summary tables --
stage_started = time.perf_counter()
try:
_compute_summaries(store)
build_result["summaries_computed"] = True
except (sqlite3.OperationalError, Exception) as e:
logger.warning("Summary computation failed: %s", e)
warnings.append(f"Summary computation failed: {type(e).__name__}: {e}")
timing["summaries_s"] = max(
0.0,
round(time.perf_counter() - stage_started, 6),
)
build_result["postprocess_timing"] = timing

store.set_metadata(
"last_postprocessed_at",
Expand Down
24 changes: 22 additions & 2 deletions tests/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -1306,7 +1306,7 @@ def test_postprocess_none_produces_nodes_no_flows(self):
assert "communities_detected" not in result
assert "fts_indexed" not in result

def test_postprocess_minimal_has_fts_no_flows(self):
def test_postprocess_minimal_has_fts_no_flows(self, capsys):
from unittest.mock import patch

from code_review_graph.tools.build import build_or_update_graph
Expand All @@ -1324,8 +1324,15 @@ def test_postprocess_minimal_has_fts_no_flows(self):
assert result.get("signatures_updated") is True
assert "flows_detected" not in result
assert "communities_detected" not in result
timing = result["postprocess_timing"]
assert set(timing) == {"signatures_s", "fts_s"}
assert all(
isinstance(value, float) and value >= 0
for value in timing.values()
)
assert capsys.readouterr().out == ""

def test_postprocess_full_matches_default(self):
def test_postprocess_full_matches_default(self, capsys):
from unittest.mock import patch

from code_review_graph.tools.build import build_or_update_graph
Expand All @@ -1343,6 +1350,19 @@ def test_postprocess_full_matches_default(self):
# Full postprocess should have flows and communities
assert "flows_detected" in result
assert "communities_detected" in result
timing = result["postprocess_timing"]
assert set(timing) == {
"signatures_s",
"fts_s",
"flows_s",
"communities_s",
"summaries_s",
}
assert all(
isinstance(value, float) and value >= 0
for value in timing.values()
)
assert capsys.readouterr().out == ""


class TestComputeSummaries:
Expand Down