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

decrease allocations during ssl handshake #1350

Closed
wants to merge 4 commits into from
Closed

Conversation

wfurt
Copy link
Member

@wfurt wfurt commented Jan 7, 2020

The test I'm using is essentially handshake test from Performance repo with one round of setup and 1000 test iterations:

for (int i = 0; i < max; i++)
{
    using (var sslClient = new SslStream(_client, leaveInnerStreamOpen: true, delegate { return true; }))
    using (var sslServer = new SslStream(_server, leaveInnerStreamOpen: true, delegate { return true; }))
    {
        await Task.WhenAll(
        sslClient.AuthenticateAsClientAsync("localhost", null, SslProtocols.None, checkCertificateRevocation: false),
        sslServer.AuthenticateAsServerAsync(_cert, clientCertificateRequired: false, SslProtocols.None, checkCertificateRevocation: false));

        // Workaround for corefx#37765
        await sslServer.WriteAsync(_serverBuffer, CancellationToken.None);
        await sslClient.ReadAsync(_clientBuffer, CancellationToken.None);     
    }
}

When running with OpenSSL we get following allocations:

image

There are two parts in this change:

ProtocolToken is container to hold status and buffer. It can be allocated once and reused. That brings 5 allocation to 1. We may possibly convert it to struct in future.

We already use shared buffers for encryption.decryption. Wit this change we will also use it for outgoing handshake messages when openssl is used.

@@ -300,6 +300,7 @@ internal static bool DoSslHandshake(SafeSslHandle context, ReadOnlySpan<byte> in
{
if (sendCount <= 0)
{
ArrayPool<byte>.Shared.Return(sendBuf);
Copy link
Member

Choose a reason for hiding this comment

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

I'd feel ever so slightly better if this were instead:

byte[] tmp = sendBuf;
sendBuf = null;
ArrayPool<byte>.Shared.Return(tmp);

If, for example, Crypto.ErrClearError were to throw an exception, with the code as written we could have returned the buffer to the pool but the caller could still have a reference via the out argument that was already assigned.

@@ -718,35 +719,45 @@ private bool AcquireServerCredentials(ref byte[] thumbPrint, ReadOnlySpan<byte>
}

//
internal ProtocolToken NextMessage(byte[] incoming, int offset, int count)
internal ProtocolToken NextMessage(byte[] incoming, int offset, int count, ProtocolToken message = null)
Copy link
Member

Choose a reason for hiding this comment

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

Can we just make ProtocolToken a readonly struct?

{
if (SslStreamPal.RentsBufferForHanshake && _payload != null)
{
ArrayPool<byte>.Shared.Return(_payload);
Copy link
Member

Choose a reason for hiding this comment

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

Ooh, yeah, this definitely makes me nervous. Erroneous setting of Payload could now result in the same buffer being returned multiple times to the pool, e.g. if we accidentally access it concurrently. And it'll be worse if we do make ProtocolToken a struct (maybe that's why you chose not to).

Does _payload need to actually be a separate buffer? We already have _internalBuffer on the SslStream... can we just use that?

Copy link
Member Author

Choose a reason for hiding this comment

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

I was wondering about it but the ProtocolToken use is currently pretty simple and controlled by us. I originally did not have it IDisposable and I was worrying about loosing rented buffer in error cases and causing memory leaks. I call Reset() when we are done with token to release buffer as soon as possible. We can possibly assert or throw if buffer is not null when assigning new value. That would make sure we don't mingle usages.

ProtocolToken is IMHO convenience wrapper and we could possibly abandon it and track status and buffers separately. That would make it easier to track life cycle.

I was also thinking about using _internalBuffer. Handshake happens before we would normally use it and we rent buffer big enough for whole TLS record. The trouble would be handling renegotiation. It still may be possible but the _internalBuffer could have chunk of data while we would start new handshake. We could use same strategy on separate variable and keep the rented buffer in class variable instead of holding to it via ProtocolToken.

Let me know what seems most appropriate.

@wfurt
Copy link
Member Author

wfurt commented Jan 18, 2020

I'll find another solution and I'll open new PR when ready.

@wfurt wfurt closed this Jan 18, 2020
@karelz karelz added this to the 5.0.0 milestone Aug 18, 2020
@ghost ghost locked as resolved and limited conversation to collaborators Dec 11, 2020
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.

3 participants