Pool HttpSys request buffers (#17314)

This commit is contained in:
Chris Ross 2019-11-22 15:35:47 -08:00 committed by GitHub
parent 6f2b107b88
commit ca23b1a325
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 102 additions and 191 deletions

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6"/>
</startup>
</configuration>

View File

@ -11,20 +11,45 @@ namespace TestClient
public class Program
{
private const string Address =
// "http://localhost:5000/public/1kb.txt";
"https://localhost:9090/public/1kb.txt";
"http://localhost:5000/public/1kb.txt";
// "https://localhost:9090/public/1kb.txt";
public static void Main(string[] args)
{
WebRequestHandler handler = new WebRequestHandler();
handler.ServerCertificateValidationCallback = (_, __, ___, ____) => true;
Console.WriteLine("Ready");
Console.ReadKey();
var handler = new HttpClientHandler();
handler.MaxConnectionsPerServer = 500;
handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
// handler.UseDefaultCredentials = true;
handler.Credentials = new NetworkCredential(@"redmond\chrross", "passwird");
HttpClient client = new HttpClient(handler);
/*
RunParallelRequests(client);
// RunManualRequests(client);
// RunWebSocketClient().Wait();
Console.WriteLine("Done");
// Console.ReadKey();
}
private static void RunManualRequests(HttpClient client)
{
while (true)
{
Console.WriteLine("Press any key to send request");
Console.ReadKey();
var result = client.GetAsync(Address).Result;
Console.WriteLine(result);
}
}
private static void RunParallelRequests(HttpClient client)
{
int completionCount = 0;
int iterations = 30000;
int iterations = 100000;
for (int i = 0; i < iterations; i++)
{
client.GetAsync(Address)
@ -34,19 +59,7 @@ namespace TestClient
while (completionCount < iterations)
{
Thread.Sleep(10);
}*/
while (true)
{
Console.WriteLine("Press any key to send request");
Console.ReadKey();
var result = client.GetAsync(Address).Result;
Console.WriteLine(result);
}
// RunWebSocketClient().Wait();
// Console.WriteLine("Done");
// Console.ReadKey();
}
public static async Task RunWebSocketClient()

View File

@ -1,53 +0,0 @@
// Copyright (c) Microsoft Open Technologies, Inc.
// All Rights Reserved
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR
// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING
// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF
// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR
// NON-INFRINGEMENT.
// See the Apache 2 License for the specific language governing
// permissions and limitations under the License.
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("TestClient")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("TestClient")]
[assembly: AssemblyCopyright("Copyright © 2012")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("8db62eb3-48c0-4049-b33e-271c738140a0")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("0.5")]
[assembly: AssemblyVersion("0.5")]
[assembly: AssemblyFileVersion("0.5.40117.0")]

View File

@ -1,64 +1,13 @@

<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{8B828433-B333-4C19-96AE-00BFFF9D8841}</ProjectGuid>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>TestClient</RootNamespace>
<AssemblyName>TestClient</AssemblyName>
<TargetFrameworkVersion>v4.6</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\..\</SolutionDir>
<RestorePackages>true</RestorePackages>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<StartupObject>TestClient.Program</StartupObject>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Net.Http.WebRequest" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
<Reference Include="Microsoft.Extensions.Logging.Console" />
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="Pack" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>
</Project>

View File

@ -3,7 +3,6 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
@ -18,8 +17,6 @@ namespace Microsoft.AspNetCore.Server.HttpSys
private TaskCompletionSource<RequestContext> _tcs;
private HttpSysListener _server;
private NativeRequestContext _nativeRequestContext;
private const int DefaultBufferSize = 4096;
private const int AlignmentPadding = 8;
internal AsyncAcceptContext(HttpSysListener server)
{
@ -192,32 +189,14 @@ namespace Microsoft.AspNetCore.Server.HttpSys
{
_nativeRequestContext?.ReleasePins();
_nativeRequestContext?.Dispose();
//Debug.Assert(size != 0, "unexpected size");
// We can't reuse overlapped objects
uint newSize = size.HasValue ? size.Value : DefaultBufferSize;
var backingBuffer = new byte[newSize + AlignmentPadding];
var boundHandle = Server.RequestQueue.BoundHandle;
var nativeOverlapped = new SafeNativeOverlapped(boundHandle,
boundHandle.AllocateNativeOverlapped(IOCallback, this, backingBuffer));
boundHandle.AllocateNativeOverlapped(IOCallback, this, pinData: null));
var requestAddress = Marshal.UnsafeAddrOfPinnedArrayElement(backingBuffer, 0);
// TODO:
// Apparently the HttpReceiveHttpRequest memory alignment requirements for non - ARM processors
// are different than for ARM processors. We have seen 4 - byte - aligned buffers allocated on
// virtual x64/x86 machines which were accepted by HttpReceiveHttpRequest without errors. In
// these cases the buffer alignment may cause reading values at invalid offset. Setting buffer
// alignment to 0 for now.
//
// _bufferAlignment = (int)(requestAddress.ToInt64() & 0x07);
var bufferAlignment = 0;
var nativeRequest = (HttpApiTypes.HTTP_REQUEST*)(requestAddress + bufferAlignment);
// nativeRequest
_nativeRequestContext = new NativeRequestContext(nativeOverlapped, bufferAlignment, nativeRequest, backingBuffer, requestId);
_nativeRequestContext = new NativeRequestContext(nativeOverlapped, Server.MemoryPool, size, requestId);
}
public object AsyncState

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
@ -32,6 +33,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys
// 0.5 seconds per request. Respond with a 400 Bad Request.
private const int UnknownHeaderLimit = 1000;
internal MemoryPool<byte> MemoryPool { get; } = SlabMemoryPoolFactory.Create();
private volatile State _state; // m_State is set only within lock blocks, but often read outside locks.
private ServerSession _serverSession;

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>ASP.NET Core HTTP server that uses the Windows HTTP Server API.</Description>
@ -13,6 +13,10 @@
<ItemGroup>
<Compile Include="$(SharedSourceRoot)HttpSys\**\*.cs" />
<Compile Include="$(SharedSourceRoot)Buffers.MemoryPool\*.cs" LinkBase="MemoryPool" />
</ItemGroup>
<ItemGroup>
</ItemGroup>
<ItemGroup>

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Text;
namespace Microsoft.AspNetCore.HttpSys.Internal
@ -14,16 +15,7 @@ namespace Microsoft.AspNetCore.HttpSys.Internal
internal static unsafe string GetString(byte* pBytes, int byteCount)
{
// net451: return new string(pBytes, 0, byteCount, Encoding);
var charCount = Encoding.GetCharCount(pBytes, byteCount);
var chars = new char[charCount];
fixed (char* pChars = chars)
{
var count = Encoding.GetChars(pBytes, byteCount, pChars, charCount);
System.Diagnostics.Debug.Assert(count == charCount);
}
return new string(chars);
return Encoding.GetString(new ReadOnlySpan<byte>(pBytes, byteCount));
}
internal static byte[] GetBytes(string myString)

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
@ -17,24 +18,45 @@ namespace Microsoft.AspNetCore.HttpSys.Internal
internal unsafe class NativeRequestContext : IDisposable
{
private const int AlignmentPadding = 8;
private const int DefaultBufferSize = 4096 - AlignmentPadding;
private IntPtr _originalBufferAddress;
private HttpApiTypes.HTTP_REQUEST* _nativeRequest;
private byte[] _backingBuffer;
private IMemoryOwner<byte> _backingBuffer;
private MemoryHandle _memoryHandle;
private int _bufferAlignment;
private SafeNativeOverlapped _nativeOverlapped;
private bool _permanentlyPinned;
private bool _disposed;
// To be used by HttpSys
internal NativeRequestContext(SafeNativeOverlapped nativeOverlapped,
int bufferAlignment,
HttpApiTypes.HTTP_REQUEST* nativeRequest,
byte[] backingBuffer,
ulong requestId)
internal NativeRequestContext(SafeNativeOverlapped nativeOverlapped, MemoryPool<Byte> memoryPool, uint? bufferSize, ulong requestId)
{
_nativeOverlapped = nativeOverlapped;
_bufferAlignment = bufferAlignment;
_nativeRequest = nativeRequest;
_backingBuffer = backingBuffer;
// TODO:
// Apparently the HttpReceiveHttpRequest memory alignment requirements for non - ARM processors
// are different than for ARM processors. We have seen 4 - byte - aligned buffers allocated on
// virtual x64/x86 machines which were accepted by HttpReceiveHttpRequest without errors. In
// these cases the buffer alignment may cause reading values at invalid offset. Setting buffer
// alignment to 0 for now.
//
// _bufferAlignment = (int)(requestAddress.ToInt64() & 0x07);
_bufferAlignment = 0;
var newSize = (int)(bufferSize ?? DefaultBufferSize) + AlignmentPadding;
if (newSize <= memoryPool.MaxBufferSize)
{
_backingBuffer = memoryPool.Rent(newSize);
}
else
{
// No size limit
_backingBuffer = MemoryPool<byte>.Shared.Rent(newSize);
}
_backingBuffer.Memory.Span.Fill(0);// Zero the buffer
_memoryHandle = _backingBuffer.Memory.Pin();
_nativeRequest = (HttpApiTypes.HTTP_REQUEST*)((long)_memoryHandle.Pointer + _bufferAlignment);
RequestId = requestId;
}
@ -94,15 +116,17 @@ namespace Microsoft.AspNetCore.HttpSys.Internal
internal uint Size
{
get { return (uint)_backingBuffer.Length - AlignmentPadding; }
get { return (uint)_backingBuffer.Memory.Length - AlignmentPadding; }
}
// ReleasePins() should be called exactly once. It must be called before Dispose() is called, which means it must be called
// before an object (Request) which closes the RequestContext on demand is returned to the application.
internal void ReleasePins()
{
Debug.Assert(_nativeRequest != null || _backingBuffer == null, "RequestContextBase::ReleasePins()|ReleasePins() called twice.");
Debug.Assert(_nativeRequest != null, "RequestContextBase::ReleasePins()|ReleasePins() called twice.");
_originalBufferAddress = (IntPtr)_nativeRequest;
_memoryHandle.Dispose();
_memoryHandle = default;
_nativeRequest = null;
_nativeOverlapped?.Dispose();
_nativeOverlapped = null;
@ -110,8 +134,14 @@ namespace Microsoft.AspNetCore.HttpSys.Internal
public virtual void Dispose()
{
Debug.Assert(_nativeRequest == null, "RequestContextBase::Dispose()|Dispose() called before ReleasePins().");
_nativeOverlapped?.Dispose();
if (!_disposed)
{
_disposed = true;
Debug.Assert(_nativeRequest == null, "RequestContextBase::Dispose()|Dispose() called before ReleasePins().");
_nativeOverlapped?.Dispose();
_memoryHandle.Dispose();
_backingBuffer.Dispose();
}
}
// These methods require the HTTP_REQUEST to still be pinned in its original location.
@ -272,7 +302,7 @@ namespace Microsoft.AspNetCore.HttpSys.Internal
}
else
{
fixed (byte* pMemoryBlob = _backingBuffer)
fixed (byte* pMemoryBlob = _backingBuffer.Memory.Span)
{
var request = (HttpApiTypes.HTTP_REQUEST*)(pMemoryBlob + _bufferAlignment);
long fixup = pMemoryBlob - (byte*)_originalBufferAddress;
@ -306,7 +336,7 @@ namespace Microsoft.AspNetCore.HttpSys.Internal
else
{
// Return value.
fixed (byte* pMemoryBlob = _backingBuffer)
fixed (byte* pMemoryBlob = _backingBuffer.Memory.Span)
{
var request = (HttpApiTypes.HTTP_REQUEST*)(pMemoryBlob + _bufferAlignment);
long fixup = pMemoryBlob - (byte*)_originalBufferAddress;
@ -366,7 +396,7 @@ namespace Microsoft.AspNetCore.HttpSys.Internal
}
else
{
fixed (byte* pMemoryBlob = _backingBuffer)
fixed (byte* pMemoryBlob = _backingBuffer.Memory.Span)
{
var request = (HttpApiTypes.HTTP_REQUEST*)(pMemoryBlob + _bufferAlignment);
return GetEndPointHelper(localEndpoint, request, pMemoryBlob);
@ -426,7 +456,7 @@ namespace Microsoft.AspNetCore.HttpSys.Internal
}
else
{
fixed (byte* pMemoryBlob = _backingBuffer)
fixed (byte* pMemoryBlob = _backingBuffer.Memory.Span)
{
var request = (HttpApiTypes.HTTP_REQUEST*)(pMemoryBlob + _bufferAlignment);
long fixup = pMemoryBlob - (byte*)_originalBufferAddress;
@ -490,7 +520,7 @@ namespace Microsoft.AspNetCore.HttpSys.Internal
}
else
{
fixed (byte* pMemoryBlob = _backingBuffer)
fixed (byte* pMemoryBlob = _backingBuffer.Memory.Span)
{
var request = (HttpApiTypes.HTTP_REQUEST_V2*)(pMemoryBlob + _bufferAlignment);
return GetRequestInfo(_originalBufferAddress, request);
@ -514,7 +544,7 @@ namespace Microsoft.AspNetCore.HttpSys.Internal
var offset = (long)requestInfo.pInfo - (long)baseAddress;
info.Add(
(int)requestInfo.InfoType,
new ReadOnlyMemory<byte>(_backingBuffer, (int)offset, (int)requestInfo.InfoLength));
_backingBuffer.Memory.Slice((int)offset, (int)requestInfo.InfoLength));
}
return new ReadOnlyDictionary<int, ReadOnlyMemory<byte>>(info);
@ -528,7 +558,7 @@ namespace Microsoft.AspNetCore.HttpSys.Internal
}
else
{
fixed (byte* pMemoryBlob = _backingBuffer)
fixed (byte* pMemoryBlob = _backingBuffer.Memory.Span)
{
var request = (HttpApiTypes.HTTP_REQUEST_V2*)(pMemoryBlob + _bufferAlignment);
return GetClientCertificate(_originalBufferAddress, request);