Skip to content

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

Merged
Kludex merged 5 commits into
pydantic:mainfrom
mbeijen:no-weakref
Jun 1, 2026
Merged

Store elapsed time on stream wrapper to avoid reference cycles#948
Kludex merged 5 commits 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 (efe08c6) with main (debebf1)

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.

Copy link
Copy Markdown
Member

@Kludex Kludex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the tests were adding anything meaningful. All good here, thanks.

@Kludex Kludex enabled auto-merge (squash) June 1, 2026 11:01
@Kludex
Copy link
Copy Markdown
Member

Kludex commented Jun 1, 2026

I don't think the tests were adding anything meaningful. All good here, thanks.

CI disagrees with me.

@Kludex Kludex merged commit de30f39 into pydantic:main Jun 1, 2026
11 checks passed
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