Skip to content

Commit cb12f00

Browse files
perf: throttle markdown rendering to prevent O(n²) performance degradation
- Implement time-based throttling for markdown rendering during streaming - Limit re-renders to once every 100ms per part to avoid quadratic complexity - Add final render on PartEndEvent to ensure complete content is displayed - Track per-part render timestamps to coordinate throttling across multiple streams - Remove render time tracking when parts end to clean up state - Enable vertical overflow scrolling for long markdown content
1 parent e724d2f commit cb12f00

1 file changed

Lines changed: 34 additions & 10 deletions

File tree

code_puppy/agents/base_agent.py

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1279,6 +1279,8 @@ async def _event_stream_handler(
12791279
ctx: The run context.
12801280
events: Async iterable of streaming events (PartStartEvent, PartDeltaEvent, etc.).
12811281
"""
1282+
import time as time_module
1283+
12821284
from pydantic_ai import PartDeltaEvent, PartStartEvent
12831285
from pydantic_ai.messages import TextPartDelta, ThinkingPartDelta
12841286
from rich.console import Console
@@ -1299,6 +1301,8 @@ async def _event_stream_handler(
12991301
text_buffer: dict[int, list[str]] = {} # Buffer text for markdown
13001302
live_displays: dict[int, Live] = {} # Live displays for streaming markdown
13011303
did_stream_anything = False # Track if we streamed any content
1304+
last_render_time: dict[int, float] = {} # Track last render time per part
1305+
render_interval = 0.1 # Only re-render markdown every 100ms (throttle)
13021306

13031307
def _print_thinking_banner() -> None:
13041308
"""Print the THINKING banner with spinner pause and line clear."""
@@ -1383,19 +1387,27 @@ def _print_response_banner() -> None:
13831387
Markdown(""),
13841388
console=console,
13851389
refresh_per_second=10,
1390+
vertical_overflow="visible", # Allow scrolling for long content
13861391
)
13871392
live.start()
13881393
live_displays[event.index] = live
1389-
# Accumulate and update markdown
1394+
# Accumulate text and throttle markdown rendering
1395+
# (Markdown parsing is O(n), doing it on every token = O(n²) death)
13901396
text_buffer[event.index].append(delta.content_delta)
1391-
content = "".join(text_buffer[event.index])
1392-
if event.index in live_displays:
1393-
try:
1394-
live_displays[event.index].update(
1395-
Markdown(content)
1396-
)
1397-
except Exception:
1398-
pass
1397+
now = time_module.monotonic()
1398+
last_render = last_render_time.get(event.index, 0)
1399+
1400+
# Only re-render if enough time has passed (throttle)
1401+
if now - last_render >= render_interval:
1402+
content = "".join(text_buffer[event.index])
1403+
if event.index in live_displays:
1404+
try:
1405+
live_displays[event.index].update(
1406+
Markdown(content)
1407+
)
1408+
last_render_time[event.index] = now
1409+
except Exception:
1410+
pass
13991411
else:
14001412
# For thinking parts, stream immediately (dim)
14011413
if event.index not in banner_printed:
@@ -1407,8 +1419,18 @@ def _print_response_banner() -> None:
14071419
# PartEndEvent - finish the streaming with a newline
14081420
elif isinstance(event, PartEndEvent):
14091421
if event.index in streaming_parts:
1410-
# For text parts, stop the Live display
1422+
# For text parts, do final render then stop the Live display
14111423
if event.index in text_parts:
1424+
# Final render to ensure we show complete content
1425+
# (throttling may have skipped the last few tokens)
1426+
if event.index in live_displays and event.index in text_buffer:
1427+
try:
1428+
final_content = "".join(text_buffer[event.index])
1429+
live_displays[event.index].update(
1430+
Markdown(final_content)
1431+
)
1432+
except Exception:
1433+
pass
14121434
if event.index in live_displays:
14131435
try:
14141436
live_displays[event.index].stop()
@@ -1417,6 +1439,8 @@ def _print_response_banner() -> None:
14171439
del live_displays[event.index]
14181440
if event.index in text_buffer:
14191441
del text_buffer[event.index]
1442+
# Clean up render time tracking
1443+
last_render_time.pop(event.index, None)
14201444
# For thinking parts, just print newline
14211445
elif event.index in banner_printed:
14221446
console.print() # Final newline after streaming

0 commit comments

Comments
 (0)