File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff line change @@ -41,6 +41,7 @@ def _build_graph_for_future(
4141 future : futures .Future ,
4242 * ,
4343 limit : int | None = None ,
44+ seen : set [int ] | None = None ,
4445) -> FutureCallGraph :
4546 if not isinstance (future , futures .Future ):
4647 raise TypeError (
@@ -68,9 +69,15 @@ def _build_graph_for_future(
6869 else :
6970 break
7071
71- if future ._asyncio_awaited_by :
72+ if seen is None :
73+ seen = set ()
74+
75+ # gh-156860: "awaited by" is a DAG, not a tree. Expand each future once
76+ if future ._asyncio_awaited_by and id (future ) not in seen :
77+ seen .add (id (future ))
7278 for parent in future ._asyncio_awaited_by :
73- awaited_by .append (_build_graph_for_future (parent , limit = limit ))
79+ awaited_by .append (
80+ _build_graph_for_future (parent , limit = limit , seen = seen ))
7481
7582 if limit is not None :
7683 if limit > 0 :
@@ -170,8 +177,10 @@ def capture_call_graph(
170177
171178 awaited_by = []
172179 if future ._asyncio_awaited_by :
180+ seen = {id (future )}
173181 for parent in future ._asyncio_awaited_by :
174- awaited_by .append (_build_graph_for_future (parent , limit = limit ))
182+ awaited_by .append (
183+ _build_graph_for_future (parent , limit = limit , seen = seen ))
175184
176185 if limit is not None :
177186 limit *= - 1
Original file line number Diff line number Diff line change @@ -434,6 +434,42 @@ async def main():
434434
435435 self .assertTrue (stack_for_fut [1 ].startswith ('* Future(id=' ))
436436
437+ async def test_build_graph_for_future_expands_dag_once (self ):
438+ # gh-156860: a future reachable by several paths is expanded once.
439+ async def waits_for (* deps ):
440+ await asyncio .gather (* deps )
441+
442+ fut = asyncio .Future ()
443+ layer = [fut ]
444+ for _ in range (3 ):
445+ layer = [asyncio .ensure_future (waits_for (* layer )) for _ in range (2 )]
446+ await asyncio .sleep (0 )
447+ captured = asyncio .format_call_graph (fut )
448+
449+ fut .set_result (None )
450+ await asyncio .gather (* layer )
451+
452+ self .assertEqual (captured .count ('* Task' ), 10 )
453+
454+ async def test_capture_call_graph_expands_dag_once (self ):
455+ # gh-156860
456+ captured = None
457+
458+ async def waits_for (* deps ):
459+ await asyncio .gather (* deps )
460+
461+ async def root ():
462+ nonlocal captured
463+ await asyncio .sleep (0 )
464+ captured = asyncio .format_call_graph ()
465+
466+ layer = [asyncio .ensure_future (root ())]
467+ for _ in range (3 ):
468+ layer = [asyncio .ensure_future (waits_for (* layer )) for _ in range (2 )]
469+ await asyncio .gather (* layer )
470+
471+ self .assertEqual (captured .count ('* Task' ), 13 )
472+
437473 async def test_capture_call_graph_positive_limit (self ):
438474 captured = None
439475
Original file line number Diff line number Diff line change 1+ Fix exponential growth of :func: `asyncio.print_call_graph ` output when
2+ several tasks await the same future.
You can’t perform that action at this time.
0 commit comments