diff options
author | vnugent <public@vaughnnugent.com> | 2024-06-13 21:57:34 -0400 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2024-06-13 21:57:34 -0400 |
commit | 7d2987f1d4048c30808a85798e32c99747f6cfe3 (patch) | |
tree | a1d1ecc8e479a12abbe7cfa637b584101ecee27d /lib/Net.Http | |
parent | 75c1d0cbf9a5a7856c544671a45f1b4312ffe7ce (diff) |
perf: Async pre-buffer to avoid sync buffer
Diffstat (limited to 'lib/Net.Http')
-rw-r--r-- | lib/Net.Http/src/Core/Buffering/ContextLockedBufferManager.cs | 42 | ||||
-rw-r--r-- | lib/Net.Http/src/Core/Buffering/HttpBufferElement.cs | 7 | ||||
-rw-r--r-- | lib/Net.Http/src/Core/HttpContext.cs | 68 | ||||
-rw-r--r-- | lib/Net.Http/src/Core/HttpServerBase.cs | 23 | ||||
-rw-r--r-- | lib/Net.Http/src/Core/HttpServerProcessing.cs | 24 | ||||
-rw-r--r-- | lib/Net.Http/src/Core/PerfCounter/HttpPerfCounterState.cs | 6 | ||||
-rw-r--r-- | lib/Net.Http/src/Core/TransportReader.cs | 26 | ||||
-rw-r--r-- | lib/Net.Http/src/HttpConfig.cs | 59 |
8 files changed, 172 insertions, 83 deletions
diff --git a/lib/Net.Http/src/Core/Buffering/ContextLockedBufferManager.cs b/lib/Net.Http/src/Core/Buffering/ContextLockedBufferManager.cs index d8def92..b99e3ff 100644 --- a/lib/Net.Http/src/Core/Buffering/ContextLockedBufferManager.cs +++ b/lib/Net.Http/src/Core/Buffering/ContextLockedBufferManager.cs @@ -53,8 +53,6 @@ namespace VNLib.Net.Http.Core.Buffering private IMemoryOwner<byte>? _handle; private HttpBufferSegments<byte> _segments; - #region LifeCycle - ///<inheritdoc/> public void AllocateBuffer(IHttpMemoryPool allocator) { @@ -68,24 +66,21 @@ namespace VNLib.Net.Http.Core.Buffering Memory<byte> full = _handle.Memory; //Header parse buffer is a special case as it will be double the size due to the char buffer - int headerParseBufferSize = GetMaxHeaderBufferSize(in Config); - + int headerParseBufferSize = GetMaxHeaderBufferSize(in Config); int responseAndFormDataSize = ComputeResponseAndFormDataBuffer(in Config); - - _segments = new() - { - //Shared header buffer - HeaderAccumulator = GetNextSegment(ref full, headerParseBufferSize), - //Shared response and form data buffer - ResponseAndFormData = GetNextSegment(ref full, responseAndFormDataSize), + //Shared header buffer + _segments.HeaderAccumulator = GetNextSegment(ref full, headerParseBufferSize); + _segments.ResponseAndFormData = GetNextSegment(ref full, responseAndFormDataSize); - /* - * The chunk accumulator buffer cannot be shared. It is also only - * stored if chunking is enabled. - */ - ChunkedResponseAccumulator = _chunkingEnabled ? GetNextSegment(ref full, Config.ChunkedResponseAccumulatorSize) : default - }; + /* + * The chunk accumulator buffer cannot be shared. It is also only + * stored if chunking is enabled. + */ + _segments.ChunkedResponseAccumulator = _chunkingEnabled + ? GetNextSegment(ref full, Config.ChunkedResponseAccumulatorSize) + : default; + /* * ************* WARNING **************** @@ -133,8 +128,7 @@ namespace VNLib.Net.Http.Core.Buffering //Clear segments _segments = default; - - //Free buffer + if (_handle != null) { _handle.Dispose(); @@ -142,8 +136,6 @@ namespace VNLib.Net.Http.Core.Buffering } } - #endregion - ///<inheritdoc/> public IHttpHeaderParseBuffer RequestHeaderParseBuffer => _requestHeaderBuffer; @@ -212,11 +204,11 @@ namespace VNLib.Net.Http.Core.Buffering } - readonly struct HttpBufferSegments<T> + struct HttpBufferSegments<T> { - public readonly Memory<T> HeaderAccumulator { get; init; } - public readonly Memory<T> ChunkedResponseAccumulator { get; init; } - public readonly Memory<T> ResponseAndFormData { get; init; } + public Memory<T> HeaderAccumulator; + public Memory<T> ChunkedResponseAccumulator; + public Memory<T> ResponseAndFormData; } diff --git a/lib/Net.Http/src/Core/Buffering/HttpBufferElement.cs b/lib/Net.Http/src/Core/Buffering/HttpBufferElement.cs index 774ed6a..173048a 100644 --- a/lib/Net.Http/src/Core/Buffering/HttpBufferElement.cs +++ b/lib/Net.Http/src/Core/Buffering/HttpBufferElement.cs @@ -76,7 +76,9 @@ namespace VNLib.Net.Http.Core.Buffering ///<inheritdoc/> [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual Span<byte> GetBinSpan(int offset, int size) - => (offset + size) < _handle.Size ? _handle.GetSpan(offset, size) : throw new ArgumentOutOfRangeException(nameof(offset)); + => (offset + size) < _handle.Size + ? _handle.GetSpan(offset, size) + : throw new ArgumentOutOfRangeException(nameof(offset)); private struct HandleState @@ -87,6 +89,7 @@ namespace VNLib.Net.Http.Core.Buffering public readonly int Size; public readonly Memory<byte> Memory; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public HandleState(Memory<byte> mem) { Memory = mem; @@ -97,12 +100,14 @@ namespace VNLib.Net.Http.Core.Buffering public readonly void Unpin() => _handle.Dispose(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly Span<byte> GetSpan(int offset, int size) { Debug.Assert((offset + size) < Size, "Call to GetSpan failed because the offset/size was out of valid range"); return MemoryUtil.GetSpan<byte>(IntPtr.Add(_pointer, offset), size); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly ref byte GetRef() => ref MemoryUtil.GetRef<byte>(_pointer); } } diff --git a/lib/Net.Http/src/Core/HttpContext.cs b/lib/Net.Http/src/Core/HttpContext.cs index dc857b8..bd553e3 100644 --- a/lib/Net.Http/src/Core/HttpContext.cs +++ b/lib/Net.Http/src/Core/HttpContext.cs @@ -25,6 +25,7 @@ using System; using System.IO; using System.Text; +using System.Threading; using System.Diagnostics; using System.Threading.Tasks; @@ -129,14 +130,73 @@ namespace VNLib.Net.Http.Core HttpVersion IHttpContextInformation.CurrentVersion => Request.State.HttpVersion; ///<inheritdoc/> - public ref readonly HttpEncodedSegment CrlfSegment => ref ParentServer.CrlfBytes; + public ref readonly HttpEncodedSegment CrlfSegment => ref ParentServer.Config.CrlfBytes; ///<inheritdoc/> - public ref readonly HttpEncodedSegment FinalChunkSegment => ref ParentServer.FinalChunkBytes; + public ref readonly HttpEncodedSegment FinalChunkSegment => ref ParentServer.Config.FinalChunkBytes; ///<inheritdoc/> public Stream GetTransport() => _ctx!.ConnectionStream; + int _bytesRead; + + /* + * The following functions operate in tandem. Data should be buffered + * by a call to BufferTransportAsync() and then made availbe by a call to + * GetReader(). This set of functions only happens once per request/response + * cycle. This allows a async buffer filling before a syncronous transport + * read. + */ + + public void GetReader(out TransportReader reader) + { + Debug.Assert(_ctx != null, "Request to transport reader was called by the connection context was null"); + + reader = new( + _ctx!.ConnectionStream, + Buffers.RequestHeaderParseBuffer, + ParentServer.Config.HttpEncoding, + ParentServer.Config.HeaderLineTermination + ); + + /* + * Specal function to set available data + * NOTE: this can be dangerous as the buffer is + */ + reader.SetAvailableData(_bytesRead); + + Debug.Assert(reader.Available == _bytesRead); + } + + public async ValueTask BufferTransportAsync(CancellationToken cancellation) + { + /* + * This function allows for pre-buffering of the transport + * before parsing the response. It also allows waiting for more data async + * when an http1 request is in keep-alive mode waiting for more data. + * + * We can asynchronously read data when its available and preload + * the transport reader. The only catch is we need to access the + * raw Memory<byte> structure within the buffer. So the binary + * buffer size MUST be respected. + */ + + Debug.Assert(_ctx != null, "Request to buffer transport was called by the connection context was null"); + + _bytesRead = 0; + + Memory<byte> dataBuffer = Buffers.ResponseHeaderBuffer.GetMemory(); + + /* + * Since this buffer must be shared with char buffers, size + * must be respected. Remember that split buffesr store binary + * data at the head of the buffer and char data at the tail + */ + dataBuffer = dataBuffer[..Buffers.ResponseHeaderBuffer.BinSize]; + + _bytesRead = await _ctx!.ConnectionStream.ReadAsync(dataBuffer, cancellation); + } + #endregion #region LifeCycle Hooks @@ -173,6 +233,8 @@ namespace VNLib.Net.Http.Core ///<inheritdoc/> public void EndRequest() { + _bytesRead = 0; //Must reset after every request + Request.OnComplete(); Response.OnComplete(); ResponseBody.OnComplete(); @@ -203,4 +265,4 @@ namespace VNLib.Net.Http.Core #endregion } -}
\ No newline at end of file +} diff --git a/lib/Net.Http/src/Core/HttpServerBase.cs b/lib/Net.Http/src/Core/HttpServerBase.cs index f5f3563..ec6e73d 100644 --- a/lib/Net.Http/src/Core/HttpServerBase.cs +++ b/lib/Net.Http/src/Core/HttpServerBase.cs @@ -89,11 +89,7 @@ namespace VNLib.Net.Http /// Reusable store for obtaining <see cref="HttpContext"/> /// </summary> private readonly ObjectRental<HttpContext> ContextStore; - - /// <summary> - /// The cached header-line termination value - /// </summary> - private readonly ReadOnlyMemory<byte> HeaderLineTermination; + #endregion /// <summary> @@ -111,16 +107,6 @@ namespace VNLib.Net.Http /// </summary> internal readonly CompressionMethod SupportedCompressionMethods; - /// <summary> - /// Pre-encoded CRLF bytes - /// </summary> - internal readonly HttpEncodedSegment CrlfBytes; - - /// <summary> - /// Pre-encoded HTTP chunking final chunk segment - /// </summary> - internal readonly HttpEncodedSegment FinalChunkBytes; - private CancellationTokenSource? StopToken; /// <summary> @@ -153,13 +139,6 @@ namespace VNLib.Net.Http //Cache wildcard root _wildcardRoot = ServerRoots.GetValueOrDefault(WILDCARD_KEY); - - //Init pre-encded segments - CrlfBytes = HttpEncodedSegment.FromString(HttpHelpers.CRLF, config.HttpEncoding); - FinalChunkBytes = HttpEncodedSegment.FromString("0\r\n\r\n", config.HttpEncoding); - - //Store a ref to the crlf memory segment - HeaderLineTermination = CrlfBytes.Buffer.AsMemory(); } private static void ValidateConfig(in HttpConfig conf) diff --git a/lib/Net.Http/src/Core/HttpServerProcessing.cs b/lib/Net.Http/src/Core/HttpServerProcessing.cs index b6dbfef..2116341 100644 --- a/lib/Net.Http/src/Core/HttpServerProcessing.cs +++ b/lib/Net.Http/src/Core/HttpServerProcessing.cs @@ -36,7 +36,6 @@ using VNLib.Utils.Memory; using VNLib.Utils.Logging; using VNLib.Utils.Extensions; using VNLib.Net.Http.Core; -using VNLib.Net.Http.Core.Buffering; using VNLib.Net.Http.Core.Response; using VNLib.Net.Http.Core.PerfCounter; @@ -60,8 +59,14 @@ namespace VNLib.Net.Http { Stream stream = transportContext.ConnectionStream; - //Set write timeout + /* + * Write timeout is constant for the duration of an HTTP + * connection. Read timeout must be set to active on initial + * loop because a fresh connection is assumed to have data + * ready. + */ stream.WriteTimeout = _config.SendTimeout; + stream.ReadTimeout = _config.ActiveConnectionRecvTimeout; //Init stream context.InitializeContext(transportContext); @@ -69,6 +74,9 @@ namespace VNLib.Net.Http //Keep the transport open and listen for messages as long as keepalive is enabled do { + //Attempt to buffer a new (or keepalive) connection async + await context.BufferTransportAsync(StopToken!.Token); + //Set rx timeout low for initial reading stream.ReadTimeout = _config.ActiveConnectionRecvTimeout; @@ -84,9 +92,6 @@ namespace VNLib.Net.Http //Reset inactive keeaplive timeout, when expired the following read will throw a cancealltion exception stream.ReadTimeout = (int)_config.ConnectionKeepAlive.TotalMilliseconds; - //"Peek" or wait for more data to begin another request (may throw timeout exception when timmed out) - await stream.ReadAsync(Memory<byte>.Empty, StopToken!.Token); - } while (true); //Check if an alternate protocol was specified @@ -276,17 +281,14 @@ namespace VNLib.Net.Http //TODO: future support for http2 and http3 over tls } - //Get the parse buffer - IHttpHeaderParseBuffer parseBuffer = ctx.Buffers.RequestHeaderParseBuffer; - - TransportReader reader = new (ctx.GetTransport(), parseBuffer, _config.HttpEncoding, HeaderLineTermination); + ctx.GetReader(out TransportReader reader); HttpStatusCode code; try { //Get the char span - Span<char> lineBuf = parseBuffer.GetCharSpan(); + Span<char> lineBuf = ctx.Buffers.RequestHeaderParseBuffer.GetCharSpan(); Http11ParseExtensions.Http1ParseState parseState = new(); @@ -476,4 +478,4 @@ namespace VNLib.Net.Http } } -}
\ No newline at end of file +} diff --git a/lib/Net.Http/src/Core/PerfCounter/HttpPerfCounterState.cs b/lib/Net.Http/src/Core/PerfCounter/HttpPerfCounterState.cs index a86ac40..89b622d 100644 --- a/lib/Net.Http/src/Core/PerfCounter/HttpPerfCounterState.cs +++ b/lib/Net.Http/src/Core/PerfCounter/HttpPerfCounterState.cs @@ -3,10 +3,10 @@ * * Library: VNLib * Package: VNLib.Net.Http -* File: ConnectionInfo.cs +* File: HttpPerfCounterState.cs * -* ConnectionInfo.cs is part of VNLib.Net.Http which is part of the larger -* VNLib collection of libraries and utilities. +* HttpPerfCounterState.cs is part of VNLib.Net.Http which is part of the +* larger VNLib collection of libraries and utilities. * * VNLib.Net.Http is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as diff --git a/lib/Net.Http/src/Core/TransportReader.cs b/lib/Net.Http/src/Core/TransportReader.cs index 0b38121..7cd8b8b 100644 --- a/lib/Net.Http/src/Core/TransportReader.cs +++ b/lib/Net.Http/src/Core/TransportReader.cs @@ -45,16 +45,14 @@ namespace VNLib.Net.Http.Core /// <param name="buffer">The shared binary buffer</param> /// <param name="encoding">The encoding to use when reading bianry</param> /// <param name="lineTermination">The line delimiter to search for</param> - internal struct TransportReader(Stream transport, IHttpHeaderParseBuffer buffer, Encoding encoding, ReadOnlyMemory<byte> lineTermination) : IVnTextReader + internal struct TransportReader(Stream transport, IHttpHeaderParseBuffer buffer, Encoding encoding, ReadOnlyMemory<byte> lineTermination) + : IVnTextReader { ///<inheritdoc/> public readonly Encoding Encoding => encoding; ///<inheritdoc/> - public readonly ReadOnlyMemory<byte> LineTermination => lineTermination; - - ///<inheritdoc/> - public readonly Stream BaseStream => transport; + public readonly ReadOnlyMemory<byte> LineTermination => lineTermination; private readonly uint MaxBufferSize = (uint)buffer.BinSize; @@ -79,15 +77,23 @@ namespace VNLib.Net.Http.Core ///<inheritdoc/> public void Advance(int count) { - if (count < 0) - { - throw new ArgumentOutOfRangeException(nameof(count), "Count must be positive"); - } + ArgumentOutOfRangeException.ThrowIfNegative(count); //Advance the window start by the count and set the position _position = _position.AdvanceStart(count); } + /// <summary> + /// Sets the number of bytes to read from the transport stream + /// </summary> + /// <param name="start">The number of bytes to make available within the buffer window</param> + public void SetAvailableData(int start) + { + Debug.Assert(start <= MaxBufferSize, "Stream buffer would overflow"); + + _position = _position.AdvanceEnd(start); + } + ///<inheritdoc/> public void FillBuffer() { @@ -110,7 +116,7 @@ namespace VNLib.Net.Http.Core { //Get a ref to the entire buffer segment, then do an in-place move to shift the data to the start of the buffer ref byte ptr = ref buffer.DangerousGetBinRef(0); - MemoryUtil.Memmove(ref ptr, _position.WindowStart, ref ptr, 0, windowSize); + MemoryUtil.Memmove(ref ptr, _position.WindowStart, ref ptr, dstOffset: 0, windowSize); /* * Now that data has been shifted, update the position to diff --git a/lib/Net.Http/src/HttpConfig.cs b/lib/Net.Http/src/HttpConfig.cs index c74bdbb..ff0434f 100644 --- a/lib/Net.Http/src/HttpConfig.cs +++ b/lib/Net.Http/src/HttpConfig.cs @@ -25,6 +25,7 @@ using System; using System.Text; +using VNLib.Net.Http.Core; using VNLib.Utils.Logging; namespace VNLib.Net.Http @@ -32,15 +33,57 @@ namespace VNLib.Net.Http /// <summary> /// Represents configration variables used to create the instance and manage http connections /// </summary> - /// <param name="ServerLog"> - /// A log provider that all server related log entiries will be written to - /// </param> - /// <param name="MemoryPool"> - /// Server memory pool to use for allocating buffers - /// </param> - public readonly record struct HttpConfig(ILogProvider ServerLog, IHttpMemoryPool MemoryPool) + public readonly record struct HttpConfig { - + /// <summary> + /// Pre-encoded CRLF bytes + /// </summary> + internal readonly HttpEncodedSegment CrlfBytes; + + /// <summary> + /// Pre-encoded HTTP chunking final chunk segment + /// </summary> + internal readonly HttpEncodedSegment FinalChunkBytes; + + /// <summary> + /// The cached header-line termination value + /// </summary> + internal readonly ReadOnlyMemory<byte> HeaderLineTermination; + + /// <summary> + /// Initializes a new instance of the <see cref="HttpConfig"/> struct + /// </summary> + /// <param name="serverLog"></param> + /// <param name="memoryPool"></param> + /// <param name="httpEncoding"></param> + public HttpConfig(ILogProvider serverLog, IHttpMemoryPool memoryPool, Encoding httpEncoding) + { + ArgumentNullException.ThrowIfNull(serverLog); + ArgumentNullException.ThrowIfNull(memoryPool); + ArgumentNullException.ThrowIfNull(httpEncoding); + + ServerLog = serverLog; + MemoryPool = memoryPool; + HttpEncoding = httpEncoding; + + //Init pre-encded segments + CrlfBytes = HttpEncodedSegment.FromString(HttpHelpers.CRLF, httpEncoding); + FinalChunkBytes = HttpEncodedSegment.FromString("0\r\n\r\n", httpEncoding); + + //Store a ref to the crlf memory segment + HeaderLineTermination = CrlfBytes.Buffer.AsMemory(); + } + + /// <summary> + /// A log provider that all server related log entiries will be written to + /// </summary> + public ILogProvider ServerLog { get; init; } + + /// <summary> + /// Server memory pool to use for allocating buffers + /// </summary> + public IHttpMemoryPool MemoryPool { get; init; } + /// <summary> /// The absolute request entity body size limit in bytes /// </summary> |