Skip to content
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

Merged
merged 6 commits into from
Mar 7, 2021

Conversation

geoffkizer
Copy link
Contributor

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

@geoffkizer geoffkizer added this to the 6.0.0 milestone Mar 4, 2021
@ghost
Copy link

ghost commented Mar 4, 2021

Tagging subscribers to this area: @dotnet/ncl, @vcsjones
See info in area-owners.md if you want to be subscribed.

Issue Details

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

Author: geoffkizer
Assignees: -
Labels:

area-System.Net.Security

Milestone: 6.0.0

@geoffkizer
Copy link
Contributor Author

/azp run runtime-libraries-coreclr outerloop

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@geoffkizer
Copy link
Contributor Author

/azp run runtime-libraries-coreclr outerloop

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@geoffkizer
Copy link
Contributor Author

/azp run runtime-libraries-coreclr outerloop

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

Copy link
Member

@stephentoub stephentoub left a 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.

Copy link
Member

@wfurt wfurt left a 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.

@geoffkizer
Copy link
Contributor Author

Yeah, I've broken a bunch of test projects that use the conformance tests. Working on a fix.

@geoffkizer
Copy link
Contributor Author

geoffkizer commented Mar 6, 2021

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?

@geoffkizer
Copy link
Contributor Author

/azp run runtime-libraries-coreclr outerloop

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@stephentoub
Copy link
Member

And it means that future updates to the StreamConformanceTests will not require updating every test project that uses them.

If you add a new file you mean, right?

Comment on lines +11 to +16
<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" />
Copy link
Member

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?

Copy link
Contributor Author

@geoffkizer geoffkizer Mar 7, 2021

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.");
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

@stephentoub stephentoub left a 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.

@geoffkizer
Copy link
Contributor Author

it's strange to me that we now have two different ways of including shared tests

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.

@geoffkizer
Copy link
Contributor Author

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.

@geoffkizer geoffkizer merged commit 027c093 into dotnet:main Mar 7, 2021
@ghost ghost locked as resolved and limited conversation to collaborators Apr 6, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Reduce memory usage by SSLStream
3 participants