8e6642e8c9
CI / lint (pull_request) Successful in 13s
CI / typecheck (pull_request) Successful in 31s
CI / security (pull_request) Successful in 32s
CI / quality (pull_request) Successful in 15s
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 1m17s
CI / integration_tests (pull_request) Successful in 2m50s
CI / coverage (pull_request) Successful in 4m16s
CI / unit_tests (pull_request) Successful in 6m28s
CI / docker (pull_request) Successful in 39s
CI / lint (push) Successful in 12s
CI / typecheck (push) Successful in 31s
CI / security (push) Successful in 29s
CI / quality (push) Successful in 15s
CI / build (push) Successful in 21s
CI / unit_tests (push) Successful in 2m11s
CI / benchmark-regression (push) Has been skipped
CI / docker (push) Successful in 40s
CI / integration_tests (push) Successful in 2m58s
CI / coverage (push) Successful in 6m20s
CI / benchmark-publish (push) Successful in 14m5s
CI / benchmark-regression (pull_request) Successful in 33m18s
Extended _compute_affected_subtree() to BFS over both the structural tree (parent-child plan relationships) and decision_dependencies edges (influence DAG). The algorithm performs a single O(V+E) BFS pass that unions neighbors from both edge sources, using a visited set for cycle detection to guard against data corruption. Decision creation now supports dependency_decision_ids parameter in record_decision() which populates the in-memory influence DAG store. get_influence_edges() returns the adjacency list format consumed by CorrectionService. All public CorrectionService methods (analyze_impact, execute_revert, execute_correction, generate_dry_run_report) accept an optional influence_edges parameter while remaining backward-compatible (defaults to None, preserving structural-only traversal when not provided). Key design decisions: - Single BFS pass over union of structural + influence edges rather than separate traversals, ensuring O(V+E) complexity and consistent visit order - Cycle detection via visited set with warning log (not an error) since cycles indicate data corruption, not a programming error - Influence edge logging: traversal emits count of influence edges processed for observability - Backward-compatible API: existing callers that only pass decision_tree continue to work identically ISSUES CLOSED: #542
191 lines
7.4 KiB
Python
191 lines
7.4 KiB
Python
"""Airspeed Velocity benchmarks for influence DAG traversal.
|
|
|
|
Measures BFS traversal performance over combined structural tree and
|
|
influence DAG edges at varying graph sizes, including cycle detection
|
|
overhead.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from cleveragents.application.services.correction_service import CorrectionService
|
|
from cleveragents.domain.models.core.correction import CorrectionMode
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Graph generators
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_PLAN_ID = "01HGZ6FE0AQDYTR4BXVQZ6EA00"
|
|
|
|
|
|
def _linear_tree(root: str, depth: int) -> dict[str, list[str]]:
|
|
"""Build a linear chain of *depth* nodes starting at *root*."""
|
|
tree: dict[str, list[str]] = {}
|
|
current = root
|
|
for i in range(1, depth):
|
|
child = f"{root}_c{i}"
|
|
tree[current] = [child]
|
|
current = child
|
|
return tree
|
|
|
|
|
|
def _star_influence(root: str, fan_out: int) -> dict[str, list[str]]:
|
|
"""Build a star influence DAG: root influences *fan_out* nodes."""
|
|
return {root: [f"{root}_inf{i}" for i in range(fan_out)]}
|
|
|
|
|
|
def _chain_influence(root: str, depth: int) -> dict[str, list[str]]:
|
|
"""Build a linear chain influence DAG of *depth* nodes."""
|
|
dag: dict[str, list[str]] = {}
|
|
current = root
|
|
for i in range(1, depth):
|
|
nxt = f"{root}_d{i}"
|
|
dag[current] = [nxt]
|
|
current = nxt
|
|
return dag
|
|
|
|
|
|
def _mixed_graph(
|
|
root: str,
|
|
tree_depth: int,
|
|
influence_fan_out: int,
|
|
) -> tuple[dict[str, list[str]], dict[str, list[str]]]:
|
|
"""Build a combined tree + influence DAG."""
|
|
tree = _linear_tree(root, tree_depth)
|
|
dag = _star_influence(root, influence_fan_out)
|
|
return tree, dag
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Structural-only BFS benchmarks (baseline)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class StructuralOnlyBfsSuite:
|
|
"""Baseline: BFS over structural tree only (no influence edges)."""
|
|
|
|
def setup(self) -> None:
|
|
"""Prepare trees and service instances."""
|
|
self.svc = CorrectionService()
|
|
self.small_tree = _linear_tree("D1", 10)
|
|
self.medium_tree = _linear_tree("D1", 100)
|
|
self.large_tree = _linear_tree("D1", 1000)
|
|
|
|
def time_small_structural_only(self) -> None:
|
|
"""BFS 10-node linear tree, no influence edges."""
|
|
req = self.svc.request_correction(_PLAN_ID, "D1", CorrectionMode.REVERT)
|
|
self.svc.analyze_impact(req.correction_id, self.small_tree)
|
|
|
|
def time_medium_structural_only(self) -> None:
|
|
"""BFS 100-node linear tree, no influence edges."""
|
|
req = self.svc.request_correction(_PLAN_ID, "D1", CorrectionMode.REVERT)
|
|
self.svc.analyze_impact(req.correction_id, self.medium_tree)
|
|
|
|
def time_large_structural_only(self) -> None:
|
|
"""BFS 1000-node linear tree, no influence edges."""
|
|
req = self.svc.request_correction(_PLAN_ID, "D1", CorrectionMode.REVERT)
|
|
self.svc.analyze_impact(req.correction_id, self.large_tree)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Influence-only BFS benchmarks
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class InfluenceOnlyBfsSuite:
|
|
"""BFS over influence DAG only (no structural tree)."""
|
|
|
|
def setup(self) -> None:
|
|
"""Prepare influence DAGs."""
|
|
self.svc = CorrectionService()
|
|
self.empty_tree: dict[str, list[str]] = {}
|
|
self.small_dag = _chain_influence("D1", 10)
|
|
self.medium_dag = _chain_influence("D1", 100)
|
|
self.large_dag = _chain_influence("D1", 1000)
|
|
self.star_dag = _star_influence("D1", 500)
|
|
|
|
def time_small_influence_only(self) -> None:
|
|
"""BFS 10-node influence chain."""
|
|
req = self.svc.request_correction(_PLAN_ID, "D1", CorrectionMode.REVERT)
|
|
self.svc.analyze_impact(req.correction_id, self.empty_tree, self.small_dag)
|
|
|
|
def time_medium_influence_only(self) -> None:
|
|
"""BFS 100-node influence chain."""
|
|
req = self.svc.request_correction(_PLAN_ID, "D1", CorrectionMode.REVERT)
|
|
self.svc.analyze_impact(req.correction_id, self.empty_tree, self.medium_dag)
|
|
|
|
def time_large_influence_only(self) -> None:
|
|
"""BFS 1000-node influence chain."""
|
|
req = self.svc.request_correction(_PLAN_ID, "D1", CorrectionMode.REVERT)
|
|
self.svc.analyze_impact(req.correction_id, self.empty_tree, self.large_dag)
|
|
|
|
def time_star_influence(self) -> None:
|
|
"""BFS star DAG with 500 fan-out."""
|
|
req = self.svc.request_correction(_PLAN_ID, "D1", CorrectionMode.REVERT)
|
|
self.svc.analyze_impact(req.correction_id, self.empty_tree, self.star_dag)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Combined structural + influence BFS benchmarks
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class CombinedBfsSuite:
|
|
"""BFS over combined structural tree + influence DAG."""
|
|
|
|
def setup(self) -> None:
|
|
"""Prepare combined graphs."""
|
|
self.svc = CorrectionService()
|
|
self.small_tree, self.small_dag = _mixed_graph("D1", 10, 10)
|
|
self.medium_tree, self.medium_dag = _mixed_graph("D1", 100, 50)
|
|
self.large_tree, self.large_dag = _mixed_graph("D1", 500, 200)
|
|
|
|
def time_small_combined(self) -> None:
|
|
"""BFS combined 10+10 graph."""
|
|
req = self.svc.request_correction(_PLAN_ID, "D1", CorrectionMode.REVERT)
|
|
self.svc.analyze_impact(req.correction_id, self.small_tree, self.small_dag)
|
|
|
|
def time_medium_combined(self) -> None:
|
|
"""BFS combined 100+50 graph."""
|
|
req = self.svc.request_correction(_PLAN_ID, "D1", CorrectionMode.REVERT)
|
|
self.svc.analyze_impact(req.correction_id, self.medium_tree, self.medium_dag)
|
|
|
|
def time_large_combined(self) -> None:
|
|
"""BFS combined 500+200 graph."""
|
|
req = self.svc.request_correction(_PLAN_ID, "D1", CorrectionMode.REVERT)
|
|
self.svc.analyze_impact(req.correction_id, self.large_tree, self.large_dag)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Cycle detection overhead benchmarks
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class CycleDetectionSuite:
|
|
"""Measure overhead of cycle detection in corrupt DAGs."""
|
|
|
|
def setup(self) -> None:
|
|
"""Prepare DAGs with cycles."""
|
|
self.svc = CorrectionService()
|
|
self.empty_tree: dict[str, list[str]] = {}
|
|
# Small cycle: D1 → D2 → D3 → D1
|
|
self.small_cycle = {"D1": ["D2"], "D2": ["D3"], "D3": ["D1"]}
|
|
# Medium: chain of 50 with cycle at end
|
|
dag: dict[str, list[str]] = {}
|
|
current = "D1"
|
|
for i in range(1, 50):
|
|
nxt = f"D1_cyc{i}"
|
|
dag[current] = [nxt]
|
|
current = nxt
|
|
dag[current] = ["D1"] # cycle back to start
|
|
self.medium_cycle = dag
|
|
|
|
def time_small_cycle(self) -> None:
|
|
"""BFS with 3-node cycle."""
|
|
req = self.svc.request_correction(_PLAN_ID, "D1", CorrectionMode.REVERT)
|
|
self.svc.analyze_impact(req.correction_id, self.empty_tree, self.small_cycle)
|
|
|
|
def time_medium_cycle(self) -> None:
|
|
"""BFS with 50-node chain + cycle."""
|
|
req = self.svc.request_correction(_PLAN_ID, "D1", CorrectionMode.REVERT)
|
|
self.svc.analyze_impact(req.correction_id, self.empty_tree, self.medium_cycle)
|