Skip to content

Store elapsed time on stream wrapper to avoid reference cycles#948

Open
mbeijen wants to merge 1 commit into
pydantic:mainfrom
mbeijen:no-weakref
Open

Store elapsed time on stream wrapper to avoid reference cycles#948
mbeijen wants to merge 1 commit into
pydantic:mainfrom
mbeijen:no-weakref

Conversation

@mbeijen
Copy link
Copy Markdown
Contributor

@mbeijen mbeijen commented May 16, 2026

Instead of holding a reference back to the Response in BoundSyncStream/ BoundAsyncStream (which creates a reference cycle), store the elapsed timedelta on the stream itself after close. Response.elapsed reads it back from self.stream via duck typing, falling back to a directly-set _elapsed value for cases like mocking.

This avoids creating reference cycles that can result in significant extra memory usage.

It's an alternative approach to encode/httpx#3733 and I prefer my approach, because it does not use references at all (no weakref).

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 24, 2026

Merging this PR will not alter performance

✅ 15 untouched benchmarks
⏩ 7 skipped benchmarks1


Comparing mbeijen:no-weakref (5b32b91) with main (c2a2b8d)

Open in CodSpeed

Footnotes

  1. 7 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@Kludex
Copy link
Copy Markdown
Member

Kludex commented May 24, 2026

Do we even need the Bound*Stream classes?

Instead of holding a reference back to the Response in BoundSyncStream/
BoundAsyncStream (which creates a reference cycle), store the elapsed
timedelta on the stream itself after close. Response.elapsed reads it
back from self.stream via duck typing, falling back to a directly-set
_elapsed value for cases like mocking.

This avoids creating reference cycles that can result in significant
extra memory usage.

It's an alternative approach to encode/httpx#3733
 and I prefer my approach, because it does not use references at all
(no weakref).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mbeijen
Copy link
Copy Markdown
Contributor Author

mbeijen commented May 24, 2026

Do we even need the Bound*Stream classes?

Interesting observation! It would modify the behavior a bit: elapsed is captured when Response.close() is called, not when the underlying stream closes. Typically these are the same (Response.close calls stream.close), but anyone closing response.stream directly would no longer get an elapsed value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants