Make all UvRequest objects normal GC handles (#1698)

* Make all UvRequest objects normal GC handles
- This avoids the cost of using GCHandle.Alloc per operation.
- It *does* mean that we need to explicitly dispose UvRequest objects
after using them (which we did before anyways). This change does
add a few try catch statements to make sure we always dispose the UvRequest
if there are synchronous exceptions.
- This is ~1.5% of the overhead in the benchmarks today
- Keep track of all allocated UvRequest objects with a WeakReference in DEBUG
and assert none are kept around after cleaning up.
- Fixed a leak where we don't clean up UvWriteReq objects when writing
to the named pipe.
This commit is contained in:
David Fowler 2017-04-19 15:16:11 -07:00 committed by GitHub
parent e4ba1d01ce
commit 42d82a507d
13 changed files with 125 additions and 65 deletions

View File

@ -47,14 +47,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
if (!buffer.IsEmpty) if (!buffer.IsEmpty)
{ {
var writeReq = _writeReqPool.Allocate(); var writeReq = _writeReqPool.Allocate();
var writeResult = await writeReq.WriteAsync(_socket, buffer);
_writeReqPool.Return(writeReq);
LogWriteInfo(writeResult.Status, writeResult.Error); try
if (writeResult.Error != null)
{ {
throw writeResult.Error; var writeResult = await writeReq.WriteAsync(_socket, buffer);
LogWriteInfo(writeResult.Status, writeResult.Error);
if (writeResult.Error != null)
{
throw writeResult.Error;
}
}
finally
{
// Make sure we return the writeReq to the pool
_writeReqPool.Return(writeReq);
} }
} }
@ -104,15 +112,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
_log.ConnectionWriteFin(_connectionId); _log.ConnectionWriteFin(_connectionId);
var shutdownReq = new UvShutdownReq(_log); var shutdownReq = new UvShutdownReq(_log);
shutdownReq.Init(_thread.Loop); try
shutdownReq.Shutdown(_socket, (req, status, state) =>
{ {
req.Dispose(); shutdownReq.Init(_thread);
_log.ConnectionWroteFin(_connectionId, status); shutdownReq.Shutdown(_socket, (req, status, state) =>
{
req.Dispose();
_log.ConnectionWroteFin(_connectionId, status);
tcs.TrySetResult(null); tcs.TrySetResult(null);
}, },
this); this);
}
catch (Exception)
{
shutdownReq.Dispose();
throw;
}
return tcs.Task; return tcs.Task;
} }

View File

@ -3,13 +3,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.ExceptionServices; using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networking;
using Microsoft.AspNetCore.Server.Kestrel.Internal.System.IO.Pipelines; using Microsoft.AspNetCore.Server.Kestrel.Internal.System.IO.Pipelines;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networking;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
@ -76,6 +77,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
public WriteReqPool WriteReqPool { get; } public WriteReqPool WriteReqPool { get; }
#if DEBUG
public List<WeakReference> Requests { get; } = new List<WeakReference>();
#endif
public ExceptionDispatchInfo FatalError { get { return _closeError; } } public ExceptionDispatchInfo FatalError { get { return _closeError; } }
public Action<Action<IntPtr>, IntPtr> QueueCloseHandle { get; } public Action<Action<IntPtr>, IntPtr> QueueCloseHandle { get; }
@ -141,6 +146,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
} }
} }
#if DEBUG
private void CheckUvReqLeaks()
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
// Detect leaks in UvRequest objects
foreach (var request in Requests)
{
Debug.Assert(request.Target == null, $"{request.Target?.GetType()} object is still alive.");
}
}
#endif
private async Task DisposeConnectionsAsync() private async Task DisposeConnectionsAsync()
{ {
// Close and wait for all connections // Close and wait for all connections
@ -299,6 +319,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
WriteReqPool.Dispose(); WriteReqPool.Dispose();
thisHandle.Free(); thisHandle.Free();
_threadTcs.SetResult(null); _threadTcs.SetResult(null);
#if DEBUG
// Check for handle leaks after disposing everything
CheckUvReqLeaks();
#endif
} }
} }

View File

@ -105,17 +105,25 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
DetachFromIOCP(socket); DetachFromIOCP(socket);
var dispatchPipe = _dispatchPipes[index]; var dispatchPipe = _dispatchPipes[index];
var write = new UvWriteReq(Log); var write = new UvWriteReq(Log);
write.Init(Thread.Loop); try
write.Write2( {
dispatchPipe, write.Init(Thread);
_dummyMessage, write.Write2(
socket, dispatchPipe,
(write2, status, error, state) => _dummyMessage,
{ socket,
write2.Dispose(); (write2, status, error, state) =>
((UvStreamHandle)state).Dispose(); {
}, write2.Dispose();
socket); ((UvStreamHandle)state).Dispose();
},
socket);
}
catch (UvException)
{
write.Dispose();
throw;
}
} }
} }
@ -230,7 +238,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
if (correctMessage) if (correctMessage)
{ {
_listener._dispatchPipes.Add((UvPipeHandle) dispatchPipe); _listener._dispatchPipes.Add((UvPipeHandle)dispatchPipe);
dispatchPipe.ReadStop(); dispatchPipe.ReadStop();
_bufHandle.Free(); _bufHandle.Free();
} }

View File

@ -59,11 +59,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
private void StartedCallback(TaskCompletionSource<int> tcs) private void StartedCallback(TaskCompletionSource<int> tcs)
{ {
var connect = new UvConnectRequest(Log);
try try
{ {
DispatchPipe.Init(Thread.Loop, Thread.QueueCloseHandle, true); DispatchPipe.Init(Thread.Loop, Thread.QueueCloseHandle, true);
var connect = new UvConnectRequest(Log); connect.Init(Thread);
connect.Init(Thread.Loop);
connect.Connect( connect.Connect(
DispatchPipe, DispatchPipe,
_pipeName, _pipeName,
@ -73,6 +73,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
catch (Exception ex) catch (Exception ex)
{ {
DispatchPipe.Dispose(); DispatchPipe.Dispose();
connect.Dispose();
tcs.SetException(ex); tcs.SetException(ex);
} }
} }
@ -101,7 +102,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
(handle, status2, state) => ((ListenerSecondary)state).ReadStartCallback(handle, status2), (handle, status2, state) => ((ListenerSecondary)state).ReadStartCallback(handle, status2),
this); this);
writeReq.Init(Thread.Loop); writeReq.Init(Thread);
var result = await writeReq.WriteAsync( var result = await writeReq.WriteAsync(
DispatchPipe, DispatchPipe,
new ArraySegment<ArraySegment<byte>>(new [] { new ArraySegment<byte>(_pipeMessage) })); new ArraySegment<ArraySegment<byte>>(new [] { new ArraySegment<byte>(_pipeMessage) }));
@ -117,10 +118,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
} }
catch (Exception ex) catch (Exception ex)
{ {
writeReq.Dispose();
DispatchPipe.Dispose(); DispatchPipe.Dispose();
tcs.SetException(ex); tcs.SetException(ex);
} }
finally
{
writeReq.Dispose();
}
} }
private void ReadStartCallback(UvStreamHandle handle, int status) private void ReadStartCallback(UvStreamHandle handle, int status)

View File

@ -20,7 +20,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networkin
{ {
} }
public void Init(UvLoopHandle loop) public override void Init(LibuvThread thread)
{
DangerousInit(thread.Loop);
base.Init(thread);
}
public void DangerousInit(UvLoopHandle loop)
{ {
var requestSize = loop.Libuv.req_size(LibuvFunctions.RequestType.CONNECT); var requestSize = loop.Libuv.req_size(LibuvFunctions.RequestType.CONNECT);
CreateMemory( CreateMemory(
@ -38,14 +45,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networkin
_callback = callback; _callback = callback;
_state = state; _state = state;
Pin();
Libuv.pipe_connect(this, pipe, name, _uv_connect_cb); Libuv.pipe_connect(this, pipe, name, _uv_connect_cb);
} }
private static void UvConnectCb(IntPtr ptr, int status) private static void UvConnectCb(IntPtr ptr, int status)
{ {
var req = FromIntPtr<UvConnectRequest>(ptr); var req = FromIntPtr<UvConnectRequest>(ptr);
req.Unpin();
var callback = req._callback; var callback = req._callback;
req._callback = null; req._callback = null;

View File

@ -17,10 +17,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networkin
protected LibuvFunctions _uv; protected LibuvFunctions _uv;
protected int _threadId; protected int _threadId;
protected readonly ILibuvTrace _log; protected readonly ILibuvTrace _log;
private readonly GCHandleType _handleType;
protected UvMemory(ILibuvTrace logger) : base(IntPtr.Zero, true) protected UvMemory(ILibuvTrace logger, GCHandleType handleType = GCHandleType.Weak) : base(IntPtr.Zero, true)
{ {
_log = logger; _log = logger;
_handleType = handleType;
} }
public LibuvFunctions Libuv { get { return _uv; } } public LibuvFunctions Libuv { get { return _uv; } }
@ -51,7 +53,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networkin
ThreadId = threadId; ThreadId = threadId;
handle = Marshal.AllocCoTaskMem(size); handle = Marshal.AllocCoTaskMem(size);
*(IntPtr*)handle = GCHandle.ToIntPtr(GCHandle.Alloc(this, GCHandleType.Weak)); *(IntPtr*)handle = GCHandle.ToIntPtr(GCHandle.Alloc(this, _handleType));
} }
unsafe protected static void DestroyMemory(IntPtr memory) unsafe protected static void DestroyMemory(IntPtr memory)

View File

@ -8,28 +8,25 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networkin
{ {
public class UvRequest : UvMemory public class UvRequest : UvMemory
{ {
private GCHandle _pin; protected UvRequest(ILibuvTrace logger) : base(logger, GCHandleType.Normal)
protected UvRequest(ILibuvTrace logger) : base (logger)
{ {
} }
public virtual void Init(LibuvThread thread)
{
#if DEBUG
// Store weak handles to all UvRequest objects so we can do leak detection
// while running tests
thread.Requests.Add(new WeakReference(this));
#endif
}
protected override bool ReleaseHandle() protected override bool ReleaseHandle()
{ {
DestroyMemory(handle); DestroyMemory(handle);
handle = IntPtr.Zero; handle = IntPtr.Zero;
return true; return true;
} }
public virtual void Pin()
{
_pin = GCHandle.Alloc(this, GCHandleType.Normal);
}
public virtual void Unpin()
{
_pin.Free();
}
} }
} }

View File

@ -19,26 +19,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networkin
{ {
} }
public void Init(UvLoopHandle loop) public override void Init(LibuvThread thread)
{ {
var loop = thread.Loop;
CreateMemory( CreateMemory(
loop.Libuv, loop.Libuv,
loop.ThreadId, loop.ThreadId,
loop.Libuv.req_size(LibuvFunctions.RequestType.SHUTDOWN)); loop.Libuv.req_size(LibuvFunctions.RequestType.SHUTDOWN));
base.Init(thread);
} }
public void Shutdown(UvStreamHandle handle, Action<UvShutdownReq, int, object> callback, object state) public void Shutdown(UvStreamHandle handle, Action<UvShutdownReq, int, object> callback, object state)
{ {
_callback = callback; _callback = callback;
_state = state; _state = state;
Pin();
_uv.shutdown(this, handle, _uv_shutdown_cb); _uv.shutdown(this, handle, _uv_shutdown_cb);
} }
private static void UvShutdownCb(IntPtr ptr, int status) private static void UvShutdownCb(IntPtr ptr, int status)
{ {
var req = FromIntPtr<UvShutdownReq>(ptr); var req = FromIntPtr<UvShutdownReq>(ptr);
req.Unpin();
req._callback(req, status, req._state); req._callback(req, status, req._state);
req._callback = null; req._callback = null;
req._state = null; req._state = null;

View File

@ -31,7 +31,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networkin
{ {
} }
public void Init(UvLoopHandle loop) public override void Init(LibuvThread thread)
{
DangerousInit(thread.Loop);
base.Init(thread);
}
public void DangerousInit(UvLoopHandle loop)
{ {
var requestSize = loop.Libuv.req_size(LibuvFunctions.RequestType.WRITE); var requestSize = loop.Libuv.req_size(LibuvFunctions.RequestType.WRITE);
var bufferSize = Marshal.SizeOf<LibuvFunctions.uv_buf_t>() * BUFFER_COUNT; var bufferSize = Marshal.SizeOf<LibuvFunctions.uv_buf_t>() * BUFFER_COUNT;
@ -62,9 +69,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networkin
{ {
try try
{ {
// add GCHandle to keeps this SafeHandle alive while request processing
_pins.Add(GCHandle.Alloc(this, GCHandleType.Normal));
var nBuffers = 0; var nBuffers = 0;
if (buffer.IsSingleSpan) if (buffer.IsSingleSpan)
{ {
@ -157,9 +161,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networkin
{ {
try try
{ {
// add GCHandle to keeps this SafeHandle alive while request processing
_pins.Add(GCHandle.Alloc(this, GCHandleType.Normal));
var pBuffers = (LibuvFunctions.uv_buf_t*)_bufs; var pBuffers = (LibuvFunctions.uv_buf_t*)_bufs;
var nBuffers = bufs.Count; var nBuffers = bufs.Count;
if (nBuffers > BUFFER_COUNT) if (nBuffers > BUFFER_COUNT)

View File

@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
else else
{ {
req = new UvWriteReq(_log); req = new UvWriteReq(_log);
req.Init(_thread.Loop); req.Init(_thread);
} }
return req; return req;

View File

@ -131,7 +131,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
var connectReq = new UvConnectRequest(connectionTrace); var connectReq = new UvConnectRequest(connectionTrace);
pipe.Init(libuvThreadPrimary.Loop, libuvThreadPrimary.QueueCloseHandle); pipe.Init(libuvThreadPrimary.Loop, libuvThreadPrimary.QueueCloseHandle);
connectReq.Init(libuvThreadPrimary.Loop); connectReq.Init(libuvThreadPrimary);
connectReq.Connect( connectReq.Connect(
pipe, pipe,

View File

@ -64,7 +64,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
} }
var writeRequest = new UvWriteReq(_logger); var writeRequest = new UvWriteReq(_logger);
writeRequest.Init(loop); writeRequest.DangerousInit(loop);
await writeRequest.WriteAsync( await writeRequest.WriteAsync(
serverConnectionPipe, serverConnectionPipe,
@ -84,7 +84,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
loop2.Init(_uv); loop2.Init(_uv);
clientConnectionPipe.Init(loop2, (a, b) => { }, true); clientConnectionPipe.Init(loop2, (a, b) => { }, true);
connect.Init(loop2); connect.DangerousInit(loop2);
connect.Connect(clientConnectionPipe, pipeName, (handle, status, error, state) => connect.Connect(clientConnectionPipe, pipeName, (handle, status, error, state) =>
{ {
var buf = loop2.Libuv.buf_init(Marshal.AllocHGlobal(8192), 8192); var buf = loop2.Libuv.buf_init(Marshal.AllocHGlobal(8192), 8192);
@ -157,7 +157,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
serverConnectionPipeAcceptedEvent.WaitOne(); serverConnectionPipeAcceptedEvent.WaitOne();
var writeRequest = new UvWriteReq(_logger); var writeRequest = new UvWriteReq(_logger);
writeRequest.Init(loop); writeRequest.DangerousInit(loop);
writeRequest.Write2( writeRequest.Write2(
serverConnectionPipe, serverConnectionPipe,
new ArraySegment<ArraySegment<byte>>(new ArraySegment<byte>[] { new ArraySegment<byte>(new byte[] { 1, 2, 3, 4 }) }), new ArraySegment<ArraySegment<byte>>(new ArraySegment<byte>[] { new ArraySegment<byte>(new byte[] { 1, 2, 3, 4 }) }),
@ -182,7 +182,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
loop2.Init(_uv); loop2.Init(_uv);
clientConnectionPipe.Init(loop2, (a, b) => { }, true); clientConnectionPipe.Init(loop2, (a, b) => { }, true);
connect.Init(loop2); connect.DangerousInit(loop2);
connect.Connect(clientConnectionPipe, pipeName, (handle, status, error, state) => connect.Connect(clientConnectionPipe, pipeName, (handle, status, error, state) =>
{ {
connect.Dispose(); connect.Dispose();

View File

@ -160,7 +160,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
for (var x = 0; x < 2; x++) for (var x = 0; x < 2; x++)
{ {
var req = new UvWriteReq(_logger); var req = new UvWriteReq(_logger);
req.Init(loop); req.DangerousInit(loop);
var block = ReadableBuffer.Create(new byte[] { 65, 66, 67, 68, 69 }); var block = ReadableBuffer.Create(new byte[] { 65, 66, 67, 68, 69 });
await req.WriteAsync( await req.WriteAsync(