Skip to content

Commit 4fd359c

Browse files
committed
gh-156860: Fix exponential asyncio.print_call_graph() output
1 parent d59d4e7 commit 4fd359c

3 files changed

Lines changed: 50 additions & 3 deletions

File tree

Lib/asyncio/graph.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff 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

Lib/test/test_asyncio/test_graph.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff 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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix exponential growth of :func:`asyncio.print_call_graph` output when
2+
several tasks await the same future.

0 commit comments

Comments
 (0)