-
Notifications
You must be signed in to change notification settings - Fork 5k
SNIPacket memory allocation perf improvement (revised) #27758
Conversation
There are a couple of changes from #27743 that might conflict with these changes. Hold off on this until that gets merged in. |
All Manual Tests including |
{ | ||
_capacity = capacity; | ||
_data = new Byte[capacity]; | ||
if (_data == null || _data.Length != bufferSize) |
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.
I asked this last time, but I don't remember the answer. Why can't this be _data.Length < bufferSize
?
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.
There were 2 reasons:
- 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 toAllocate()
method. So I rolled back to original implementation. - 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; |
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 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?
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.
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.
} | ||
}); | ||
); |
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.
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.
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.
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
@@ -298,6 +343,11 @@ public void WriteToStream(Stream stream) | |||
stream.Write(_data, 0, _length); | |||
} | |||
|
|||
public Task WriteToStreamAsync(Stream stream) |
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.
This seems out of sync with master. Please sync it
@@ -67,6 +72,14 @@ public int Length | |||
} | |||
} | |||
|
|||
public int Capacity |
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.
Nit: public int Capacity => _capacity;
_isBufferFromArrayPool = isArrayFromArrayPool; | ||
} | ||
|
||
public void SetData(byte[] data, int length, bool isArrayFromArrayPool) |
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.
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) |
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.
Type: int capacity = -1
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.
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.
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.
Also the parameter int capacity can be removed since no other method passes in these parameters
{ | ||
_offset = 0; | ||
_capacity = -1; |
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.
Why can't this be 0 instead of -1 ?
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.
capacity = 0 means _data = new byte[0];
capacity = -1 means _data = null;
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 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); |
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.
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.
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.
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.
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.
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; | |||
} | |||
|
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.
Nit: Extra spaces.
packet.Allocate(_bufferSize); | ||
|
||
packet = new SNIPacket(_bufferSize); | ||
|
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.
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(); |
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.
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
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.
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.
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.
fixed.
} | ||
finally | ||
{ | ||
sniError = WritePacket(packet, sync); |
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.
Why remove try{} finally {} ?
05649b3
to
29c881e
Compare
@@ -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> |
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.
buffersize from comment doesn't exist
It's capacity
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.
fixed.
} | ||
else | ||
{ | ||
return handle.SendAsync(packet.Clone()); | ||
clonedPacket.DisposeAfterWriteAsync = true; |
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.
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.
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.
@saurabh500
I am not very clear about what you meant.
corefx/src/System.Data.SqlClient/src/System/Data/SqlClient/SNI/SNITcpHandle.cs
Lines 527 to 535 in 68cfdde
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()
?
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.
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
?
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.
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.
@keeratsingh Can you run this thru the Sql Tests ? |
@saurabh500 |
This is how MARS packet is sent.
|
@@ -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); |
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.
Pass the disposePacketAfterSendAsync to the packet.WriteToStreamAsync(... ) in SNINpHandle.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.
Approved after the SNINPHandle.cs modification.
@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. |
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 whenSNIPacket.Release()
is called.