-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve zero-byte read handling in SslStream to reduce memory usage #49123
Conversation
… to stream conformance tests
…d on underlying stream when appropriate
Tagging subscribers to this area: @dotnet/ncl, @vcsjones Issue DetailsWhen a user issues a zero-byte read against a Stream, they are indicating that they want to be notified when data is available, without having to hold on to a read buffer to perform an actual read. In other words, they are willing to trade off some added CPU cost for reduced memory usage. This PR improves the zero-byte handling in SslStream so that, when the user performs a zero-byte read and we have no data available, we will perform a zero-byte read against the underlying stream before allocating a buffer for the read. This should hopefully reduce memory usage in cases where the user does zero-byte reads and the connection is idle for periods of time, e.g. idle WebSocket or HTTP connections. Also, add a test to the stream conformance tests that validates this behavior for arbitrary wrapping streams. This is behind a flag and I've only enabled the flag for SslStream for now, since as far as I know, no other Stream implements similar behavior (though arguably they should). Also, ensure we are never holding on to an empty read buffer in SslStream -- there were some exception cases where this was happening currently. And some minor SslStream cleanup. Fixes #49019 cc @wfurt @scalablecory @stephentoub @davidfowl
|
/azp run runtime-libraries-coreclr outerloop |
Azure Pipelines successfully started running 1 pipeline(s). |
/azp run runtime-libraries-coreclr outerloop |
Azure Pipelines successfully started running 1 pipeline(s). |
/azp run runtime-libraries-coreclr outerloop |
Azure Pipelines successfully started running 1 pipeline(s). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src changes LGTM. Thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM.
build broken by this change so we should hold back merge until green CI.
Yeah, I've broken a bunch of test projects that use the conformance tests. Working on a fix. |
I pushed a change to encapsulate the StreamConformanceTests in their own assembly. This means that test projects that use these tests can simply reference the assembly, instead of having to include the code manually. And it means that future updates to the StreamConformanceTests will not require updating every test project that uses them. @stephentoub Can you take a look? |
/azp run runtime-libraries-coreclr outerloop |
Azure Pipelines successfully started running 1 pipeline(s). |
If you add a new file you mean, right? |
<Compile Include="$(CommonPath)System\Threading\Tasks\TaskToApm.cs" Link="System\Threading\Tasks\TaskToApm.cs" /> | ||
<Compile Include="$(CommonPath)System\Net\ArrayBuffer.cs" Link="Common\System\Net\ArrayBuffer.cs" /> | ||
<Compile Include="$(CommonPath)System\Net\MultiArrayBuffer.cs" Link="Common\System\Net\MultiArrayBuffer.cs" /> | ||
<Compile Include="$(CommonPath)System\Net\StreamBuffer.cs" Link="Common\System\Net\StreamBuffer.cs" /> | ||
<Compile Include="$(CommonPath)System\IO\DelegatingStream.cs" Link="Common\System\IO\DelegatingStream.cs" /> | ||
<Compile Include="$(CommonTestPath)System\IO\CallTrackingStream.cs" Link="Common\System\IO\CallTrackingStream.cs" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should most of these files be in TestUtilities instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(edit to explain in more detail)
The ones in CommonPath are shared source files (not test files) and thus have to be internal, and can't be in TestUtilities.
CallTrackingStream is a shared test code, so we have more flexibility there. We could move it to TestUtilities, but it wasn't there before so I didn't. Right now it's only used in the stream conformance tests. so another possibility would be to move it out of shared test code and into the stream conformance tests proper. I don't have a strong opinion here.
@@ -759,6 +759,8 @@ private async ValueTask<int> ReadAsyncInternal<TIOAdapter>(TIOAdapter adapter, M | |||
throw new NotSupportedException(SR.Format(SR.net_io_invalidnestedcall, nameof(SslStream.ReadAsync), "read")); | |||
} | |||
|
|||
Debug.Assert(_internalBuffer is null || _internalBufferCount > 0 || _decryptedBytesCount > 0, "_internalBuffer allocated when no data is buffered."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the reason for the last part of the condition around _decryptedBytesCount > 0
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's because there can be two different kinds of data in the _internalBuffer
-- encrypted or decrypted -- and the two checks here test for each.
The field names aren't ideal; _internalBufferCount
should probably be something like _encryptedBytesCount
.
Note that ReturnReadBufferIfEmpty
(just above) does the same check before returning the unused buffer to the pool.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As noted offline, it's strange to me that we now have two different ways of including shared tests: some suites putting them in assemblies (streams), and some suites putting them in files pulled into each test project (collections). If we're moving the former to use a shared assembly, I'd like us to do the same for the others as well. I don't remember if there's more than just collections.
My opinion is that assemblies are much easier to deal with -- you just need to reference the assembly from the test project. I don't see any downside to using assemblies, either. So I'd suggest we simply move to that in general. |
In fact, in general I don't think there's a good reason to ever share test code on a file-by-file basis. (This gets to your question above re TestUtilities.) General purpose test helper code can go in TestUtilities. Area-specific shared test code -- either stuff like full-on test suites like StreamConformanceTests, or just area-specific helper classes -- should go in some sort of area-specific shared assembly as appropriate. I don't plan on actively reorganizing any test code, but it's something to consider moving forward as we make changes. |
When a user issues a zero-byte read against a Stream, they are indicating that they want to be notified when data is available, without having to hold on to a read buffer to perform an actual read. In other words, they are willing to trade off some added CPU cost for reduced memory usage.
This PR improves the zero-byte handling in SslStream so that, when the user performs a zero-byte read and we have no data available, we will perform a zero-byte read against the underlying stream before allocating a buffer for the read. This should hopefully reduce memory usage in cases where the user does zero-byte reads and the connection is idle for periods of time, e.g. idle WebSocket or HTTP connections.
Also, add a test to the stream conformance tests that validates this behavior for arbitrary wrapping streams. This is behind a flag and I've only enabled the flag for SslStream for now, since as far as I know, no other Stream implements similar behavior (though arguably they should).
Also, ensure we are never holding on to an empty read buffer in SslStream -- there were some exception cases where this was happening currently. And some minor SslStream cleanup.
Fixes #49019
cc @wfurt @scalablecory @stephentoub @davidfowl