Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

SNIPacket memory allocation perf improvement (revised) #27758

Merged
merged 6 commits into from
Mar 14, 2018

Conversation

geleems
Copy link

@geleems geleems commented Mar 6, 2018

Previous version of SNIPacket change (#27187) contains couple of issues, and caused several test failures (https://github.com/dotnet/corefx/issues/27587, https://github.com/dotnet/corefx/issues/27574).
One of the failures caused by missing locks, and other ones were caused by unreleased byte array on SNIPacket.Release(). This revised fix excludes dealing with locks, and also properly deallocates byte array when SNIPacket.Release() is called.

@corivera
Copy link
Member

corivera commented Mar 6, 2018

There are a couple of changes from #27743 that might conflict with these changes.

Hold off on this until that gets merged in.

@geleems geleems self-assigned this Mar 6, 2018
@geleems
Copy link
Author

geleems commented Mar 6, 2018

All Manual Tests including System.Data.SqlClient.ManualTesting.Tests.CommandCancelTest were passed in Release mode in Linux. (TvpTest was skipped since it is failed in master)

{
_capacity = capacity;
_data = new Byte[capacity];
if (_data == null || _data.Length != bufferSize)
Copy link
Member

Choose a reason for hiding this comment

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

I asked this last time, but I don't remember the answer. Why can't this be _data.Length < bufferSize?

Copy link
Author

Choose a reason for hiding this comment

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

There were 2 reasons:

  1. I changed to _data.Length < bufferSize, and faced several test failures during network operation. It seemed they needed exact packet length as a parameter the tests handed over to Allocate() method. So I rolled back to original implementation.
  2. The packet size is initially 4,096 for login process, and then becomes 8,000 constantly until connection is closed. So, in this situation, there is not much benefit for changing to _data.Length < bufferSize.

@@ -217,9 +211,8 @@ public int TakeData(byte[] buffer, int dataOffset, int size)
/// </summary>
public void Release()
{
_length = 0;
_capacity = 0;
_data = null;
Copy link
Member

@stephentoub stephentoub Mar 6, 2018

Choose a reason for hiding this comment

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

What is the purpose of Release? If the object is just going to get dropped, there shouldn't be a need to null out the byte[], and if it's going to get reused, it'd be nice to be able to reuse the same byte[] if it's large enough. Is the issue that someone else still holds a reference to _data and we don't want to reuse it, or something like that?

Copy link
Author

Choose a reason for hiding this comment

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

That was exactly what I thought. But it turned out the Manual Test https://github.com/dotnet/corefx/issues/27574 were failed due to non-nulled out byte[] after Release() method called. Release() is now only used for disposing SNIPacket object when an error occurred.

}
});
);
Copy link
Member

Choose a reason for hiding this comment

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

This should pass , CancellationToken.None, TaskContinuationOptions.DenyChildAttach, TaskScheduler.Default, and ideally it would also pass ExecuteSynchronously |'d into the continuation options, and ideally pass the necessary state in as the object state rather than closing over it. But all of that goes away when this is changed to use await.

Copy link
Contributor

@saurabh500 saurabh500 left a comment

Choose a reason for hiding this comment

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

I ran these changes through the tests in Manual Tests with connection string Encrypt=True;TrustServerCertificate=True for one of our test servers and I see that the tests hang with this change. If I use Encrypt=True and TrustServerCertificate=True with EF tests, I see that the tests hang there as well.
Please test encryption.
Please also test MARS with encryption

@saurabh500 saurabh500 changed the title SNIPacket memeory allocation perf improvement (revised) SNIPacket memory allocation perf improvement (revised) Mar 9, 2018
@@ -298,6 +343,11 @@ public void WriteToStream(Stream stream)
stream.Write(_data, 0, _length);
}

public Task WriteToStreamAsync(Stream stream)
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems out of sync with master. Please sync it

@@ -67,6 +72,14 @@ public int Length
}
}

public int Capacity
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: public int Capacity => _capacity;

_isBufferFromArrayPool = isArrayFromArrayPool;
}

public void SetData(byte[] data, int length, bool isArrayFromArrayPool)
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems like an unused method.

@@ -146,12 +176,18 @@ public void GetData(byte[] buffer, ref int dataSize)
/// </summary>
/// <param name="data">Data</param>
/// <param name="length">Length</param>
public void SetData(byte[] data, int length)
public void SetData(byte[] data, int length, int capaticy = -1, bool isArrayFromArrayPool = false)
Copy link
Contributor

Choose a reason for hiding this comment

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

Type: int capacity = -1

Copy link
Contributor

Choose a reason for hiding this comment

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

The SetData below, which is unused seems like the only method calling this overload with isArrayFromArrayPool. If the method below is unused, the method parameter isArrayFromArrayPool can be removed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also the parameter int capacity can be removed since no other method passes in these parameters

{
_offset = 0;
_capacity = -1;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why can't this be 0 instead of -1 ?

Copy link
Author

Choose a reason for hiding this comment

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

capacity = 0 means _data = new byte[0];
capacity = -1 means _data = null;

Copy link
Contributor

Choose a reason for hiding this comment

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

It doesnt make sense to overload the value of negative to signify what _data looks like. Can't you simply check if _data = null instead of relying on a negative value of capacity?

@@ -240,11 +240,11 @@ public uint WritePacket(SNIHandle handle, SNIPacket packet, bool sync)
{
if (sync)
{
return handle.Send(packet.Clone());
return handle.Send(packet);
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't seem right. Earlier, we were setting the _outBuff into the SNIPacket and then sending a Clone of the SNI packet over the network. Now we are sending the reference to _outBuff over the wire.
This can cause issues if _outBuff in TdsParserStateObject is changed and the data transmission is still in progress.

Copy link
Author

Choose a reason for hiding this comment

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

if _outBuff in TdsParserStateObject is changed and the data transmission is still in progress.

Can that situation actually happen?
If so, handle.Send(packet.Clone()); should be atomic.

Copy link
Contributor

Choose a reason for hiding this comment

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

Why should it be atomic? There is a buffer that State Object operates on, and when that buffer is sent to network, it should be dissociated, since the state object can continue to modify it. I am not talking about multiple threads calling this method, but data modifications at multiple locations.

@@ -533,6 +533,7 @@ public override uint SendAsync(SNIPacket packet, SNIAsyncCallback callback = nul
}
return TdsEnums.SNI_SUCCESS_IO_PENDING;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Extra spaces.

packet.Allocate(_bufferSize);

packet = new SNIPacket(_bufferSize);

Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Extra spaces

@@ -238,14 +238,18 @@ public uint GetConnectionId(SNIHandle handle, ref Guid clientConnectionId)
/// <returns>SNI error status</returns>
public uint WritePacket(SNIHandle handle, SNIPacket packet, bool sync)
{
SNIPacket clonedPacket = packet.Clone();
Copy link
Contributor

Choose a reason for hiding this comment

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

This can be changed to

using(SNIPacket clonedPacket = packet.Clone()) {
    // send logic
}

This way even if an exception occurs in Send or SendAsync will result in clonedPacket being disposed

Copy link
Contributor

Choose a reason for hiding this comment

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

On Second thoughts, should the Cloned packet be disposed here? SendAsync will return immideately, which means that the I/O may not complete and the Dispose will set the _data to null and allow it to be GCed before I/O completes.

Copy link
Author

Choose a reason for hiding this comment

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

fixed.

}
finally
{
sniError = WritePacket(packet, sync);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why remove try{} finally {} ?

@geleems geleems force-pushed the newpub1 branch 2 times, most recently from 05649b3 to 29c881e Compare March 11, 2018 09:41
@@ -109,11 +91,27 @@ public void InvokeCompletionCallback(uint sniErrorCode)
/// <summary>
/// Allocate space for data
/// </summary>
/// <param name="capacity">Bytes to allocate</param>
/// <param name="bufferSize">Length of byte array to be allocated</param>
Copy link
Contributor

Choose a reason for hiding this comment

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

buffersize from comment doesn't exist
It's capacity

Copy link
Author

Choose a reason for hiding this comment

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

fixed.

}
else
{
return handle.SendAsync(packet.Clone());
clonedPacket.DisposeAfterWriteAsync = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of maintaining another state inside SNIPacket, wouldn't it be better to tell the SNIHandle to Dispose the packet after SendAsync completes? You could put an optional parameter in handle.SendAsync(SNIPacket packet, bool disposePacket) and it can be set to true from this call. This way the packet doesn't need to maintain a state.

Copy link
Author

@geleems geleems Mar 13, 2018

Choose a reason for hiding this comment

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

@saurabh500
I am not very clear about what you meant.

public override uint SendAsync(SNIPacket packet, SNIAsyncCallback callback = null)
{
SNIAsyncCallback cb = callback ?? _sendCallback;
lock(this)
{
packet.WriteToStreamAsync(_stream, cb, SNIProviders.TCP_PROV);
}
return TdsEnums.SNI_SUCCESS_IO_PENDING;
}

Line#532 runs asynchronously, so I cannot put packet.Dispose() after the line.
Otherwise, packet will be disposed immediately after calling WriteToStreamAsync().
Dispose() should happen in the async context, not the main context.
I could not find anywhere for Dispose() other than inside of SNIPacket.WriteToStreamAsync()
Which code spot did you mean exactly for putting Dispose()?

Copy link
Author

Choose a reason for hiding this comment

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

tell the SNIHandle to Dispose the packet after SendAsync completes

Did you mean main thread waits at line#532 until packet.WriteToStreamAsync() is finished, and then begins to dispose the packet?

Copy link
Contributor

Choose a reason for hiding this comment

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

I was saying, pass the disposePacket=true from SNIHandle.SendAsync -> packet.writeToStreamAsync() so that that writeToStreamAsync can dispose the packet in continuation when this parameter is set to true.

@saurabh500
Copy link
Contributor

@keeratsingh Can you run this thru the Sql Tests ?

@keeratsingh
Copy link

@saurabh500
Code changes look good from SQLTests perspective.
Tests Run:
System.Data.SqlClient\tests\FunctionalTests
System.Data.SqlClient\tests\ManualTests

@geleems
Copy link
Author

geleems commented Mar 14, 2018

This is how MARS packet is sent.

SNIProxy.WritePacket() <== MARS SNIPacket is cloned here
--> SNIHandle.SendAsync() 
--> SNIMarsHandle.SendAsync()
--> SNIMarsHandle.SendPeningPackets()
--> SNIMarsHandle.InternalSendAsync()
--> SNIMarsConnection.SendAsync()
--> SNITcpHandle.SendAsync()

@@ -213,7 +211,7 @@ public override uint Send(SNIPacket packet)
}
}

public override uint SendAsync(SNIPacket packet, SNIAsyncCallback callback = null)
public override uint SendAsync(SNIPacket packet, bool disposePacketAfterSendAsync, SNIAsyncCallback callback = null)
{
SNIAsyncCallback cb = callback ?? _sendCallback;
packet.WriteToStreamAsync(_stream, cb, SNIProviders.NP_PROV);
Copy link
Contributor

Choose a reason for hiding this comment

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

Pass the disposePacketAfterSendAsync to the packet.WriteToStreamAsync(... ) in SNINpHandle.cs

Copy link
Contributor

@saurabh500 saurabh500 left a comment

Choose a reason for hiding this comment

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

Approved after the SNINPHandle.cs modification.

@saurabh500
Copy link
Contributor

@geleems please make sure that we get a test pass for EF Core with this change for the test matrix that we have been running.

@geleems geleems merged commit 1de2b37 into dotnet:master Mar 14, 2018
@karelz karelz added this to the 2.1.0 milestone Mar 18, 2018
ericstj pushed a commit to ericstj/corefx that referenced this pull request Mar 28, 2018
picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
6 participants