perf(core): Optimize DAG engine, orchestrator loop, and simulations
This commit is contained in:
+66
-50
@@ -48,19 +48,29 @@ class TrackDAG:
|
||||
def cascade_blocks(self) -> None:
|
||||
"""
|
||||
Transitively marks `todo` tickets as `blocked` if any dependency is `blocked`.
|
||||
Runs until stable (handles multi-hop chains: A→B→C where A blocked cascades to B then C).
|
||||
Propagates 'blocked' status from initially blocked nodes to their dependents.
|
||||
"""
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
for ticket in self.tickets:
|
||||
if ticket.status == 'todo':
|
||||
for dep_id in ticket.depends_on:
|
||||
dep = self.ticket_map.get(dep_id)
|
||||
if dep and dep.status == 'blocked':
|
||||
ticket.status = 'blocked'
|
||||
changed = True
|
||||
break
|
||||
with get_monitor().scope("dag_cascade_blocks"):
|
||||
# Build adjacency list of dependents using object references to avoid lookups
|
||||
dependents = {t.id: [] for t in self.tickets}
|
||||
for t in self.tickets:
|
||||
for dep_id in t.depends_on:
|
||||
if dep_id in dependents:
|
||||
dependents[dep_id].append(t)
|
||||
|
||||
# Use a queue-based propagation (BFS) from all currently blocked tickets
|
||||
queue = [t for t in self.tickets if t.status == 'blocked']
|
||||
idx = 0
|
||||
while idx < len(queue):
|
||||
curr = queue[idx]
|
||||
idx += 1
|
||||
for dep_ticket in dependents.get(curr.id, []):
|
||||
if dep_ticket.status == 'todo':
|
||||
dep_ticket.status = 'blocked'
|
||||
# Optional: preserve the reason for blocking
|
||||
if not dep_ticket.blocked_reason:
|
||||
dep_ticket.blocked_reason = f"Dependency {curr.id} is blocked."
|
||||
queue.append(dep_ticket)
|
||||
|
||||
def is_ticket_ready(self, ticket: Ticket) -> bool:
|
||||
"""Returns True if all dependencies of the ticket are completed."""
|
||||
@@ -84,62 +94,68 @@ class TrackDAG:
|
||||
|
||||
def has_cycle(self) -> bool:
|
||||
"""
|
||||
Performs a Depth-First Search to detect cycles in the dependency graph.
|
||||
Performs an iterative Depth-First Search to detect cycles in the dependency graph.
|
||||
Returns:
|
||||
True if a cycle is detected, False otherwise.
|
||||
"""
|
||||
with get_monitor().scope("dag_has_cycle"):
|
||||
visited = set()
|
||||
rec_stack = set()
|
||||
|
||||
def is_cyclic(ticket_id: str) -> bool:
|
||||
"""Internal recursive helper for cycle detection."""
|
||||
if ticket_id in rec_stack:
|
||||
return True
|
||||
if ticket_id in visited:
|
||||
return False
|
||||
visited.add(ticket_id)
|
||||
rec_stack.add(ticket_id)
|
||||
ticket = self.ticket_map.get(ticket_id)
|
||||
if ticket:
|
||||
for neighbor in ticket.depends_on:
|
||||
if is_cyclic(neighbor):
|
||||
return True
|
||||
rec_stack.remove(ticket_id)
|
||||
return False
|
||||
for ticket in self.tickets:
|
||||
if ticket.id not in visited:
|
||||
if is_cyclic(ticket.id):
|
||||
for start_ticket in self.tickets:
|
||||
if start_ticket.id in visited:
|
||||
continue
|
||||
stack = [(start_ticket.id, False)] # (id, is_backtracking)
|
||||
path = set()
|
||||
while stack:
|
||||
node_id, is_backtracking = stack.pop()
|
||||
if is_backtracking:
|
||||
path.remove(node_id)
|
||||
continue
|
||||
if node_id in path:
|
||||
return True
|
||||
if node_id in visited:
|
||||
continue
|
||||
visited.add(node_id)
|
||||
path.add(node_id)
|
||||
stack.append((node_id, True))
|
||||
ticket = self.ticket_map.get(node_id)
|
||||
if ticket:
|
||||
for neighbor_id in ticket.depends_on:
|
||||
stack.append((neighbor_id, False))
|
||||
return False
|
||||
|
||||
def topological_sort(self) -> List[str]:
|
||||
"""
|
||||
Returns a list of ticket IDs in topological order (dependencies before dependents).
|
||||
Uses Kahn's algorithm for efficient O(V+E) sorting and cycle detection.
|
||||
Returns:
|
||||
A list of ticket ID strings.
|
||||
Raises:
|
||||
ValueError: If a dependency cycle is detected.
|
||||
"""
|
||||
with get_monitor().scope("dag_topological_sort"):
|
||||
if self.has_cycle():
|
||||
in_degree = {t.id: len(t.depends_on) for t in self.tickets}
|
||||
dependents = {t.id: [] for t in self.tickets}
|
||||
for t in self.tickets:
|
||||
for dep_id in t.depends_on:
|
||||
if dep_id in dependents:
|
||||
dependents[dep_id].append(t.id)
|
||||
|
||||
# Queue starts with nodes having no dependencies
|
||||
queue = [t.id for t in self.tickets if in_degree[t.id] == 0]
|
||||
result = []
|
||||
idx = 0
|
||||
while idx < len(queue):
|
||||
u = queue[idx]
|
||||
idx += 1
|
||||
result.append(u)
|
||||
for v_id in dependents.get(u, []):
|
||||
in_degree[v_id] -= 1
|
||||
if in_degree[v_id] == 0:
|
||||
queue.append(v_id)
|
||||
|
||||
if len(result) < len(self.tickets):
|
||||
raise ValueError("Dependency cycle detected")
|
||||
visited = set()
|
||||
stack = []
|
||||
|
||||
def visit(ticket_id: str) -> None:
|
||||
"""Internal recursive helper for topological sorting."""
|
||||
if ticket_id in visited:
|
||||
return
|
||||
visited.add(ticket_id)
|
||||
ticket = self.ticket_map.get(ticket_id)
|
||||
if ticket:
|
||||
for dep_id in ticket.depends_on:
|
||||
visit(dep_id)
|
||||
stack.append(ticket_id)
|
||||
for ticket in self.tickets:
|
||||
visit(ticket.id)
|
||||
return stack
|
||||
return result
|
||||
|
||||
class ExecutionEngine:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user