Skip to content

Commit

Permalink
Merge pull request #65 from Cysharp/feature/ReduceAllocations
Browse files Browse the repository at this point in the history
Reduce allocations in requests and responses.
  • Loading branch information
mayuki authored Apr 1, 2024
2 parents 4517d39 + efa047f commit c88718f
Show file tree
Hide file tree
Showing 12 changed files with 525 additions and 53 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build-push-and-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ env:
jobs:
run-build:
name: Build Libraries
uses: ./.github/workflows/build.yml
uses: ./.github/workflows/build-natives.yml
with:
build-config: debug
build-only-linux: false
Expand Down
9 changes: 9 additions & 0 deletions YetAnotherHttpHandler.sln
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
THIRD-PARTY-NOTICES = THIRD-PARTY-NOTICES
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "perf", "perf", "{49D0877D-6B51-4A83-8B93-9DC19BAEABB4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PerformanceCheck", "perf\PerformanceCheck\PerformanceCheck.csproj", "{BD12E8F7-E190-4B76-AFF5-62376CF0BD57}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -31,12 +35,17 @@ Global
{22CFEF14-D36A-4E21-B51F-F31053C0E870}.Debug|Any CPU.Build.0 = Debug|Any CPU
{22CFEF14-D36A-4E21-B51F-F31053C0E870}.Release|Any CPU.ActiveCfg = Release|Any CPU
{22CFEF14-D36A-4E21-B51F-F31053C0E870}.Release|Any CPU.Build.0 = Release|Any CPU
{BD12E8F7-E190-4B76-AFF5-62376CF0BD57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BD12E8F7-E190-4B76-AFF5-62376CF0BD57}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BD12E8F7-E190-4B76-AFF5-62376CF0BD57}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BD12E8F7-E190-4B76-AFF5-62376CF0BD57}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{22CFEF14-D36A-4E21-B51F-F31053C0E870} = {EDDCC9A6-BB1C-4AB9-A0C1-9AD888858442}
{BD12E8F7-E190-4B76-AFF5-62376CF0BD57} = {49D0877D-6B51-4A83-8B93-9DC19BAEABB4}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0F200E0E-4EC0-4C1A-BF65-BA52D3291577}
Expand Down
18 changes: 18 additions & 0 deletions perf/PerformanceCheck/PerformanceCheck.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="JetBrains.Profiler.Api" Version="1.4.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\YetAnotherHttpHandler\YetAnotherHttpHandler.csproj" />
</ItemGroup>

</Project>
68 changes: 68 additions & 0 deletions perf/PerformanceCheck/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using Cysharp.Net.Http;
using JetBrains.Profiler.Api;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

MemoryProfiler.CollectAllocations(true);

using var httpHandler = new YetAnotherHttpHandler();
using var client = new HttpClient(httpHandler);

var buffer = new byte[1024 * 64];
MemoryProfiler.GetSnapshot("Handler and HttpClient are created.");

for (var i = 0; i < 10; i++)
{
Console.WriteLine($"Request: Begin ({i})");
var stream = await client.GetStreamAsync("https://cysharp.co.jp");
while (await stream.ReadAsync(buffer) != 0)
{
}

MemoryProfiler.GetSnapshot($"Request End ({i})");
Console.WriteLine($"Request: End ({i})");
await Task.Delay(1000);
}

public class NativeLibraryResolver
{
[ModuleInitializer]
public static void Initialize()
{
NativeLibrary.SetDllImportResolver(typeof(Cysharp.Net.Http.YetAnotherHttpHandler).Assembly, (name, assembly, path) =>
{
var ext = "";
var prefix = "";
var platform = "";

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
platform = "win";
prefix = "";
ext = ".dll";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
platform = "osx";
prefix = "lib";
ext = ".dylib";
}
else
{
platform = "linux";
prefix = "lib";
ext = ".so";
}

var arch = RuntimeInformation.OSArchitecture switch
{
Architecture.Arm64 => "arm64",
Architecture.X64 => "x64",
Architecture.X86 => "x86",
_ => throw new NotSupportedException(),
};

return NativeLibrary.Load(Path.Combine($"runtimes/{platform}-{arch}/native/{prefix}{name}{ext}"));
});
}
}
2 changes: 1 addition & 1 deletion src/YetAnotherHttpHandler/InteropExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
Expand Down
59 changes: 33 additions & 26 deletions src/YetAnotherHttpHandler/NativeHttpHandlerCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
#endif

using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Net;
using System.Text;
using System.IO.Pipelines;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
Expand Down Expand Up @@ -162,22 +164,23 @@ private unsafe void Initialize(YahaNativeContext* ctx, NativeClientSettings sett
if (YahaEventSource.Log.IsEnabled()) YahaEventSource.Log.Info($"{nameof(NativeHttpHandlerCore)} created");
}

public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (YahaEventSource.Log.IsEnabled()) YahaEventSource.Log.Info($"HttpMessageHandler.SendAsync: {request.RequestUri}");

var requestContext = Send(request, cancellationToken);

if (request.Content != null)
{
if (YahaEventSource.Log.IsEnabled()) YahaEventSource.Log.Info($"Start sending the request body: {request.Content.GetType().FullName}");
_ = SendBodyAsync(request.Content, requestContext.Writer, cancellationToken);
if (YahaEventSource.Log.IsEnabled()) YahaEventSource.Log.Info($"Start sending the request body: {request.Content!.GetType().FullName}");
_ = SendBodyAsync(request.Content!, requestContext.Writer, cancellationToken);
}
else
{
await requestContext.Writer.CompleteAsync().ConfigureAwait(false);
requestContext.Writer.Complete();
}

return await requestContext.Response.GetResponseAsync().ConfigureAwait(false);
return requestContext.Response.GetResponseAsync();

static async Task SendBodyAsync(HttpContent requestContent, PipeWriter writer, CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -239,39 +242,28 @@ private unsafe RequestContext UnsafeSend(YahaContextSafeHandle ctxHandle, YahaRe
var reqCtx = reqCtxHandle.DangerousGet();

// Set request headers
var headers = request.Content is null
? request.Headers
: request.Headers.Concat(request.Content.Headers);
foreach (var header in headers)
SetRequestHeaders(request.Headers, ctx, reqCtx);
if (request.Content is not null)
{
var keyBytes = Encoding.UTF8.GetBytes(header.Key);
var valueBytes = Encoding.UTF8.GetBytes(string.Join(",", header.Value));

fixed (byte* pKey = keyBytes)
fixed (byte* pValue = valueBytes)
{
var bufKey = new StringBuffer(pKey, keyBytes.Length);
var bufValue = new StringBuffer(pValue, valueBytes.Length);
ThrowHelper.ThrowIfFailed(NativeMethods.yaha_request_set_header(ctx, reqCtx, &bufKey, &bufValue));
}
SetRequestHeaders(request.Content.Headers, ctx, reqCtx);
}

// Set HTTP method
{
var strBytes = Encoding.UTF8.GetBytes(request.Method.ToString());
fixed (byte* p = strBytes)
using var strBytes = Utf8Strings.HttpMethods.FromHttpMethod(request.Method);
fixed (byte* p = strBytes.Span)
{
var buf = new StringBuffer(p, strBytes.Length);
var buf = new StringBuffer(p, strBytes.Span.Length);
ThrowHelper.ThrowIfFailed(NativeMethods.yaha_request_set_method(ctx, reqCtx, &buf));
}
}

// Set URI
{
var strBytes = Encoding.UTF8.GetBytes( UriHelper.ToSafeUriString(request.RequestUri));
fixed (byte* p = strBytes)
using var strBytes = new TempUtf8String(UriHelper.ToSafeUriString(request.RequestUri));
fixed (byte* p = strBytes.Span)
{
var buf = new StringBuffer(p, strBytes.Length);
var buf = new StringBuffer(p, strBytes.Span.Length);
ThrowHelper.ThrowIfFailed(NativeMethods.yaha_request_set_uri(ctx, reqCtx, &buf));
}
}
Expand Down Expand Up @@ -305,7 +297,7 @@ private unsafe RequestContext UnsafeSend(YahaContextSafeHandle ctxHandle, YahaRe
{
if (YahaEventSource.Log.IsEnabled()) YahaEventSource.Log.Info($"[ReqSeq:{requestSequence}:State:0x{requestContextManaged.Handle:X}] Begin HTTP request to the server.");
ThrowHelper.ThrowIfFailed(NativeMethods.yaha_request_begin(ctx, reqCtx, requestContextManaged.Handle));
requestContextManaged.Start(); // NOTE: ReadRequestLoop must be started after `request_begin`.
requestContextManaged.Start(hasBody: request.Content != null); // NOTE: ReadRequestLoop must be started after `request_begin`.
}
catch
{
Expand All @@ -317,6 +309,21 @@ private unsafe RequestContext UnsafeSend(YahaContextSafeHandle ctxHandle, YahaRe
return requestContextManaged;
}

private static unsafe void SetRequestHeaders(HttpHeaders headers, YahaNativeContext* ctx, YahaNativeRequestContext* reqCtx)
{
foreach (var header in headers)
{
using var key = new TempUtf8String(header.Key);
using var value = new TempUtf8String(string.Join(",", header.Value));
fixed (byte* pKey = key.Span)
fixed (byte* pValue = value.Span)
{
var bufKey = new StringBuffer(pKey, key.Span.Length);
var bufValue = new StringBuffer(pValue, value.Span.Length);
ThrowHelper.ThrowIfFailed(NativeMethods.yaha_request_set_header(ctx, reqCtx, &bufKey, &bufValue));
}
}
}

#if USE_FUNCTION_POINTER
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
Expand Down
18 changes: 13 additions & 5 deletions src/YetAnotherHttpHandler/RequestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ namespace Cysharp.Net.Http
{
internal class RequestContext : IDisposable
{
private readonly Pipe _pipe = new Pipe(System.IO.Pipelines.PipeOptions.Default);
private readonly Pipe _pipe = new(PipeOptions.Default);
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly int _requestSequence;
private readonly object _handleLock = new object();
private readonly ManualResetEventSlim _fullyCompleted = new ManualResetEventSlim(false);
private readonly object _handleLock = new();
private readonly ManualResetEventSlim _fullyCompleted = new(false);
private readonly bool _hasRequestContextHandleRef;
private GCHandle _handle;

Expand Down Expand Up @@ -46,9 +46,17 @@ internal RequestContext(YahaContextSafeHandle ctx, YahaRequestContextSafeHandle
_requestContextHandle.DangerousAddRef(ref _hasRequestContextHandleRef);
}

internal void Start()
internal void Start(bool hasBody)
{
_readRequestTask = RunReadRequestLoopAsync(_cancellationTokenSource.Token);
if (hasBody)
{
_readRequestTask = RunReadRequestLoopAsync(_cancellationTokenSource.Token);
}
else
{
if (YahaEventSource.Log.IsEnabled()) YahaEventSource.Log.Trace($"[ReqSeq:{_requestSequence}:State:0x{Handle:X}] The request has no body. Complete immediately.");
TryCompleteBody();
}
}

public void Allocate()
Expand Down
27 changes: 10 additions & 17 deletions src/YetAnotherHttpHandler/ResponseContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,23 +61,18 @@ public void Write(ReadOnlySpan<byte> data)
}
}

public void SetHeader(string name, string value)
{
if (!_message.Headers.TryAddWithoutValidation(name, value))
{
_message.Content.Headers.TryAddWithoutValidation(name, value);
}
}

public void SetHeader(ReadOnlySpan<byte> nameBytes, ReadOnlySpan<byte> valueBytes)
{
var name = UnsafeUtilities.GetStringFromUtf8Bytes(nameBytes);
var (name, isHttpContentHeader) = Utf8Strings.HttpHeaders.FromSpan(nameBytes);
var value = UnsafeUtilities.GetStringFromUtf8Bytes(valueBytes);

if (!_message.Headers.TryAddWithoutValidation(name, value))
if (isHttpContentHeader)
{
_message.Content.Headers.TryAddWithoutValidation(name, value);
}
else
{
_message.Headers.TryAddWithoutValidation(name, value);
}
}

internal void SetVersion(YahaHttpVersion version)
Expand All @@ -91,14 +86,12 @@ internal void SetVersion(YahaHttpVersion version)
};
}

public void SetTrailer(string name, string value)
public void SetTrailer(ReadOnlySpan<byte> nameBytes, ReadOnlySpan<byte> valueBytes)
{
_message.TrailingHeaders().TryAddWithoutValidation(name, value);
}
var (name, isHttpContentHeader) = Utf8Strings.HttpHeaders.FromSpan(nameBytes);
var value = UnsafeUtilities.GetStringFromUtf8Bytes(valueBytes);

public void SetTrailer(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
{
_message.TrailingHeaders().TryAddWithoutValidation(UnsafeUtilities.GetStringFromUtf8Bytes(name), UnsafeUtilities.GetStringFromUtf8Bytes(value));
_message.TrailingHeaders().TryAddWithoutValidation(name, value);
}

public void SetStatusCode(int statusCode)
Expand Down
Loading

0 comments on commit c88718f

Please sign in to comment.