aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2024-06-26 21:01:15 -0400
committerLibravatar vnugent <public@vaughnnugent.com>2024-06-26 21:01:15 -0400
commit12391e9a207b60b41a074600fc2373ad3eb1c3ab (patch)
tree5a3396a889a2226aaabdf9aee5cd7cb4f5b04e31
parent92e182ceaf843f8d859d38faa8b2c0ff53207ff6 (diff)
feat(server): Server arch update, Memory struct access
-rw-r--r--lib/Net.Http/src/Core/HttpServerBase.cs105
-rw-r--r--lib/Net.Http/src/Core/HttpServerProcessing.cs16
-rw-r--r--lib/Net.Http/src/Core/HttpTransportBinding.cs39
-rw-r--r--lib/Net.Http/src/Core/Request/HttpInputStream.cs1
-rw-r--r--lib/Net.Http/src/Core/Request/HttpRequest.cs1
-rw-r--r--lib/Net.Http/src/Core/TransportManager.cs (renamed from lib/Net.Http/src/Core/Response/TransportManager.cs)2
-rw-r--r--lib/Net.Http/src/HttpConfig.cs4
-rw-r--r--lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs28
-rw-r--r--lib/Plugins.Essentials/src/Extensions/JsonResponse.cs79
-rw-r--r--lib/Plugins.Essentials/src/HttpEntity.cs75
-rw-r--r--lib/Utils/src/IO/VnMemoryStream.cs58
-rw-r--r--lib/Utils/tests/IO/VnMemoryStreamTests.cs44
12 files changed, 363 insertions, 89 deletions
diff --git a/lib/Net.Http/src/Core/HttpServerBase.cs b/lib/Net.Http/src/Core/HttpServerBase.cs
index 5954057..1ccace9 100644
--- a/lib/Net.Http/src/Core/HttpServerBase.cs
+++ b/lib/Net.Http/src/Core/HttpServerBase.cs
@@ -35,6 +35,21 @@
* to function safely with async programming practices.
*/
+/*
+ * 6-26-2024
+ *
+ * Server has been transformed to ultilse a single configuration to listen
+ * on a map of transport servers and isolate those connections to individual
+ * virtual hosts. It allows multiple virtual hosts to be mapped to a single
+ * transport server, but also allow a many-to-many relationship between
+ * transport servers and virtual hosts.
+ *
+ * The reason for this is HTTP server resource efficiency. A single HTTP server
+ * isolates its caching and memory pools. By sharing caches across transport
+ * bindings, we can still have the security isolation of transport : virtual host
+ * but share the resources of the server.
+ */
+
using System;
using System.Linq;
using System.Threading;
@@ -75,9 +90,8 @@ namespace VNLib.Net.Http
/// </summary>
internal static readonly Memory<byte> WriteOnlyScratchBuffer = new byte[64 * 1024];
- private readonly ITransportProvider[] Transports;
- private readonly FrozenDictionary<string, IWebRoot> ServerRoots;
- private readonly IWebRoot? _wildcardRoot;
+ private readonly ListenerState[] Transports;
+
private readonly HttpConfig _config;
#region caches
@@ -115,21 +129,20 @@ namespace VNLib.Net.Http
/// Immutable data structures are initialzed.
/// </summary>
/// <param name="config">The configuration used to create the instance</param>
- /// <param name="transports">An enumeration of transports to listen for connections on</param>
- /// <param name="sites">A collection of <see cref="IWebRoot"/>s that route incomming connetctions</param>
+ /// <param name="bindings">One to many relational mapping between a transport provider and it's routes</param>
/// <exception cref="ArgumentException"></exception>
- public HttpServer(HttpConfig config, IEnumerable<ITransportProvider> transports, IEnumerable<IWebRoot> sites)
+ public HttpServer(HttpConfig config, IEnumerable<HttpTransportBinding> bindings)
{
//Validate the configuration
ValidateConfig(in config);
_config = config;
- //Configure roots and their directories
- ServerRoots = sites.ToFrozenDictionary(static r => r.Hostname, static tv => tv, StringComparer.OrdinalIgnoreCase);
+
//Compile and store the timeout keepalive header
- KeepAliveTimeoutHeaderValue = $"timeout={(int)_config.ConnectionKeepAlive.TotalSeconds}";
-
- Transports = transports.ToArray();
+ KeepAliveTimeoutHeaderValue = $"timeout={(int)_config.ConnectionKeepAlive.TotalSeconds}";
+
+ //Map transport listeners to their virtual hosts
+ Transports = MapListeners(bindings);
//Cache supported compression methods, or none if compressor is null
SupportedCompressionMethods = config.CompressorManager == null
@@ -138,9 +151,6 @@ namespace VNLib.Net.Http
//Create a new context store
ContextStore = ObjectRental.CreateReusable(() => new HttpContext(this, SupportedCompressionMethods));
-
- //Cache wildcard root
- _wildcardRoot = ServerRoots.GetValueOrDefault(WILDCARD_KEY);
}
private static void ValidateConfig(in HttpConfig conf)
@@ -232,6 +242,29 @@ namespace VNLib.Net.Http
}
}
+ private static ListenerState[] MapListeners(IEnumerable<HttpTransportBinding> bindings)
+ {
+ /*
+ * Transform the bindings to individual http listeners
+ * which also requires a frozen mapping of hostnames to
+ * virtual host
+ */
+
+ return bindings.Select(static b => new ListenerState
+ {
+ OriginServer = b.Transport,
+
+ Roots = b.Roots.ToFrozenDictionary(
+ static r => r.Hostname,
+ static tv => tv,
+ StringComparer.OrdinalIgnoreCase
+ ),
+
+ //Yoink the wildcard route if it's set
+ DefaultRoute = b.Roots.FirstOrDefault(static r => string.Equals(r.Hostname, WILDCARD_KEY, StringComparison.OrdinalIgnoreCase))
+ }).ToArray();
+ }
+
/// <summary>
/// Begins listening for connections on configured interfaces for configured hostnames.
/// </summary>
@@ -246,7 +279,7 @@ namespace VNLib.Net.Http
StopToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
//Start servers with the new token source before listening for connections
- Array.ForEach(Transports, p => p.Start(StopToken.Token));
+ Array.ForEach(Transports, p => p.OriginServer.Start(StopToken.Token));
//Listen to connections on all transports async
IEnumerable<Task> runTasks = Transports.Select(ListenAsync);
@@ -254,6 +287,7 @@ namespace VNLib.Net.Http
//Set running flag and will be reset when all listening tasks are done
Running = true;
+ //Calling WhenAll() will force the numeration and schedule listening tasks
return Task.WhenAll(runTasks)
.ContinueWith(
OnAllStopped,
@@ -263,7 +297,7 @@ namespace VNLib.Net.Http
);
//Defer listening tasks to the task scheduler to avoid blocking this thread
- Task ListenAsync(ITransportProvider tp) => Task.Run(() => ListenWorkerDoWork(tp), cancellationToken);
+ Task ListenAsync(ListenerState tp) => Task.Run(() => ListenWorkerDoWork(tp), cancellationToken);
void OnAllStopped(Task _) => Running = false;
}
@@ -271,10 +305,9 @@ namespace VNLib.Net.Http
/*
* A worker task that listens for connections from the transport
*/
- private async Task ListenWorkerDoWork(ITransportProvider transport)
+ private async Task ListenWorkerDoWork(ListenerState state)
{
- //Set running flag
- Running = true;
+ state.Running = true;
_config.ServerLog.Information("HTTP server {hc} listening for connections", GetHashCode());
@@ -284,10 +317,10 @@ namespace VNLib.Net.Http
try
{
//Listen for new connection
- ITransportContext ctx = await transport.AcceptAsync(StopToken!.Token);
+ ITransportContext ctx = await state.OriginServer.AcceptAsync(StopToken!.Token);
//Try to dispatch the received event
- _ = DataReceivedAsync(ctx).ConfigureAwait(false);
+ _ = DataReceivedAsync(state, ctx).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
@@ -303,8 +336,8 @@ namespace VNLib.Net.Http
//Clear all caches before leaving to aid gc
CacheHardClear();
- //Clear running flag
- Running = false;
+ state.Running = false;
+
_config.ServerLog.Information("HTTP server {hc} exiting", GetHashCode());
}
@@ -340,5 +373,31 @@ namespace VNLib.Net.Http
break;
}
}
+
+ private sealed class ListenerState
+ {
+ /*
+ * Indexers ensure correct access during debug builds, but fields
+ * can be used directly for tiny performance boost in release builds
+ */
+
+ public bool Running;
+
+#if DEBUG
+
+ public required ITransportProvider OriginServer { get; init; }
+
+ public required FrozenDictionary<string, IWebRoot> Roots { get; init; }
+
+ public required IWebRoot? DefaultRoute { get; init; }
+
+#else
+ public required ITransportProvider OriginServer;
+ public required FrozenDictionary<string, IWebRoot> Roots;
+ public required IWebRoot? DefaultRoute;
+#endif
+
+ }
+
}
} \ No newline at end of file
diff --git a/lib/Net.Http/src/Core/HttpServerProcessing.cs b/lib/Net.Http/src/Core/HttpServerProcessing.cs
index dfa006f..f5dbbc7 100644
--- a/lib/Net.Http/src/Core/HttpServerProcessing.cs
+++ b/lib/Net.Http/src/Core/HttpServerProcessing.cs
@@ -47,7 +47,7 @@ namespace VNLib.Net.Http
private int OpenConnectionCount;
//Event handler method for processing incoming data events
- private async Task DataReceivedAsync(ITransportContext transportContext)
+ private async Task DataReceivedAsync(ListenerState listenState, ITransportContext transportContext)
{
Interlocked.Increment(ref OpenConnectionCount);
@@ -77,7 +77,7 @@ namespace VNLib.Net.Http
//Return read timeout to active connection timeout after data is received
stream.ReadTimeout = _config.ActiveConnectionRecvTimeout;
- bool keepAlive = await ProcessHttpEventAsync(context);
+ bool keepAlive = await ProcessHttpEventAsync(listenState, context);
//If not keepalive, exit the listening loop and clean up connection
if (!keepAlive)
@@ -169,9 +169,10 @@ namespace VNLib.Net.Http
/// <summary>
/// Main event handler for all incoming connections
/// </summary>
+ /// <param name="listenState"></param>
/// <param name="context">Reusable context object</param>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
- private async Task<bool> ProcessHttpEventAsync(HttpContext context)
+ private async Task<bool> ProcessHttpEventAsync(ListenerState listenState, HttpContext context)
{
HttpPerfCounterState counter = default;
@@ -201,7 +202,7 @@ namespace VNLib.Net.Http
return false;
}
- bool processSuccess = await ProcessRequestAsync(context);
+ bool processSuccess = await ProcessRequestAsync(listenState, context);
#if DEBUG
static void WriteConnectionDebugLog(HttpServer server, HttpContext context)
@@ -382,10 +383,13 @@ namespace VNLib.Net.Http
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
- private async Task<bool> ProcessRequestAsync(HttpContext context)
+ private async Task<bool> ProcessRequestAsync(ListenerState listenState, HttpContext context)
{
//Get the server root for the specified location or fallback to a wildcard host if one is selected
- IWebRoot? root = ServerRoots!.GetValueOrDefault(context.Request.State.Location.DnsSafeHost, _wildcardRoot);
+ IWebRoot? root = listenState.Roots.GetValueOrDefault(
+ context.Request.State.Location.DnsSafeHost,
+ listenState.DefaultRoute
+ );
if (root == null)
{
diff --git a/lib/Net.Http/src/Core/HttpTransportBinding.cs b/lib/Net.Http/src/Core/HttpTransportBinding.cs
new file mode 100644
index 0000000..eda83aa
--- /dev/null
+++ b/lib/Net.Http/src/Core/HttpTransportBinding.cs
@@ -0,0 +1,39 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Net.Http
+* File: HttpTransportBinding.cs
+*
+* HttpTransportBinding.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
+* published by the Free Software Foundation, either version 3 of the
+* License, or (at your option) any later version.
+*
+* VNLib.Net.Http is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU Affero General Public License for more details.
+*
+* You should have received a copy of the GNU Affero General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+
+using System.Collections.Generic;
+
+namespace VNLib.Net.Http
+{
+ /// <summary>
+ /// Presents a one-to-many relationship between a transport provider and it's virtual hosts
+ /// </summary>
+ /// <param name="Transport">The transport to listen for incomming connections on</param>
+ /// <param name="Roots">The enumeration of web roots that will route connections</param>
+ /// <remarks>
+ /// An HTTP server accepts a collection of these bindings to allow for a many-to-many
+ /// relationship between transport providers and virtual hosts.
+ /// </remarks>
+ public sealed record HttpTransportBinding(ITransportProvider Transport, IEnumerable<IWebRoot> Roots);
+} \ No newline at end of file
diff --git a/lib/Net.Http/src/Core/Request/HttpInputStream.cs b/lib/Net.Http/src/Core/Request/HttpInputStream.cs
index 9ad0218..29dda2d 100644
--- a/lib/Net.Http/src/Core/Request/HttpInputStream.cs
+++ b/lib/Net.Http/src/Core/Request/HttpInputStream.cs
@@ -32,7 +32,6 @@ using System.Runtime.CompilerServices;
using VNLib.Utils;
using VNLib.Utils.Memory;
using VNLib.Utils.Extensions;
-using VNLib.Net.Http.Core.Response;
namespace VNLib.Net.Http.Core
{
diff --git a/lib/Net.Http/src/Core/Request/HttpRequest.cs b/lib/Net.Http/src/Core/Request/HttpRequest.cs
index 9263e0f..cbe6bc0 100644
--- a/lib/Net.Http/src/Core/Request/HttpRequest.cs
+++ b/lib/Net.Http/src/Core/Request/HttpRequest.cs
@@ -29,7 +29,6 @@ using System.Runtime.CompilerServices;
using VNLib.Utils;
using VNLib.Utils.Memory;
using VNLib.Utils.Extensions;
-using VNLib.Net.Http.Core.Response;
namespace VNLib.Net.Http.Core
{
diff --git a/lib/Net.Http/src/Core/Response/TransportManager.cs b/lib/Net.Http/src/Core/TransportManager.cs
index 45efc4b..2632fc5 100644
--- a/lib/Net.Http/src/Core/Response/TransportManager.cs
+++ b/lib/Net.Http/src/Core/TransportManager.cs
@@ -27,7 +27,7 @@ using System.Buffers;
using System.Diagnostics;
using System.Threading.Tasks;
-namespace VNLib.Net.Http.Core.Response
+namespace VNLib.Net.Http.Core
{
internal sealed class TransportManager
{
diff --git a/lib/Net.Http/src/HttpConfig.cs b/lib/Net.Http/src/HttpConfig.cs
index ff0434f..aa6e34a 100644
--- a/lib/Net.Http/src/HttpConfig.cs
+++ b/lib/Net.Http/src/HttpConfig.cs
@@ -77,12 +77,12 @@ namespace VNLib.Net.Http
/// <summary>
/// A log provider that all server related log entiries will be written to
/// </summary>
- public ILogProvider ServerLog { get; init; }
+ public readonly ILogProvider ServerLog { get; init; }
/// <summary>
/// Server memory pool to use for allocating buffers
/// </summary>
- public IHttpMemoryPool MemoryPool { get; init; }
+ public readonly IHttpMemoryPool MemoryPool { get; init; }
/// <summary>
/// The absolute request entity body size limit in bytes
diff --git a/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs b/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs
index 0ca5b8f..7c0cf94 100644
--- a/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs
+++ b/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs
@@ -44,16 +44,21 @@ namespace VNLib.Plugins.Essentials.Extensions
/// Provides extension methods for manipulating <see cref="HttpEvent"/>s
/// </summary>
public static class EssentialHttpEventExtensions
- {
-
+ {
/*
* Pooled/tlocal serializers
*/
- private static ThreadLocal<Utf8JsonWriter> LocalSerializer { get; } = new(() => new(Stream.Null));
- private static IObjectRental<JsonResponse> ResponsePool { get; } = ObjectRental.Create(ResponseCtor);
+ private static readonly ThreadLocal<Utf8JsonWriter> LocalSerializer = new(() => new(Stream.Null));
+ private static readonly ObjectRental<JsonResponse> ResponsePool = ObjectRental.Create(ResponseCtor);
+
private static JsonResponse ResponseCtor() => new(ResponsePool);
+ /// <summary>
+ /// Purges any idle cached JSON responses from the static pool.
+ /// </summary>
+ public static void PurgeJsonResponseCache() => ResponsePool.CacheClear();
+
#region Response Configuring
/// <summary>
@@ -366,8 +371,7 @@ namespace VNLib.Plugins.Essentials.Extensions
ArgumentNullException.ThrowIfNull(encoding, nameof(encoding));
//Get new simple memory response
- IMemoryResponseReader reader = new SimpleMemoryResponse(data, encoding);
- ev.CloseResponse(code, type, reader);
+ ev.CloseResponse(code, type, entity: new SimpleMemoryResponse(data, encoding));
}
/// <summary>
@@ -435,7 +439,7 @@ namespace VNLib.Plugins.Essentials.Extensions
/// <exception cref="UriFormatException"></exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Redirect(this IHttpEvent ev, RedirectType type, string location)
- => Redirect(ev, type, new Uri(location, UriKind.RelativeOrAbsolute));
+ => Redirect(ev, type, location: new Uri(location, UriKind.RelativeOrAbsolute));
/// <summary>
/// Redirects a client using the specified <see cref="RedirectType"/>
@@ -939,7 +943,12 @@ namespace VNLib.Plugins.Essentials.Extensions
/// <returns>True if operation succeeds.</returns>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="InvalidOperationException"></exception>
- public static bool AcceptWebSocket(this IHttpEvent entity, WebSocketAcceptedCallback socketOpenedCallback, string? subProtocol = null, TimeSpan keepAlive = default)
+ public static bool AcceptWebSocket(
+ this IHttpEvent entity,
+ WebSocketAcceptedCallback socketOpenedCallback,
+ string? subProtocol = null,
+ TimeSpan keepAlive = default
+ )
{
//Must define an accept callback
ArgumentNullException.ThrowIfNull(entity);
@@ -971,7 +980,7 @@ namespace VNLib.Plugins.Essentials.Extensions
private static string GetNewSocketId() => Guid.NewGuid().ToString("N");
- private static bool PrepWebSocket(this IHttpEvent entity, string? subProtocol = null)
+ private static bool PrepWebSocket(this IHttpEvent entity, string? subProtocol)
{
ArgumentNullException.ThrowIfNull(entity);
@@ -1006,6 +1015,7 @@ namespace VNLib.Plugins.Essentials.Extensions
return true;
}
}
+
//Set the client up for a bad request response, nod a valid websocket request
entity.CloseResponse(HttpStatusCode.BadRequest);
return false;
diff --git a/lib/Plugins.Essentials/src/Extensions/JsonResponse.cs b/lib/Plugins.Essentials/src/Extensions/JsonResponse.cs
index b418b6f..2b1a9ef 100644
--- a/lib/Plugins.Essentials/src/Extensions/JsonResponse.cs
+++ b/lib/Plugins.Essentials/src/Extensions/JsonResponse.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2023 Vaughn Nugent
+* Copyright (c) 2024 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials
@@ -24,66 +24,60 @@
using System;
using System.IO;
-using System.Buffers;
+using System.Diagnostics;
using VNLib.Net.Http;
using VNLib.Utils.IO;
-using VNLib.Utils.Memory;
-using VNLib.Utils.Extensions;
using VNLib.Utils.Memory.Caching;
namespace VNLib.Plugins.Essentials.Extensions
{
- internal sealed class JsonResponse : IJsonSerializerBuffer, IMemoryResponseReader
+ internal sealed class JsonResponse(IObjectRental<JsonResponse> pool) : IJsonSerializerBuffer, IMemoryResponseReader, IDisposable
{
- private readonly IObjectRental<JsonResponse> _pool;
+ const int InitBufferSize = 4096;
+ const int MaxSizeThreshold = 24 * 1024; //24KB
+
+ private readonly IObjectRental<JsonResponse> _pool = pool;
- private readonly MemoryHandle<byte> _handle;
- private readonly IMemoryOwner<byte> _memoryOwner;
//Stream "owns" the handle, so we cannot dispose the stream
- private readonly VnMemoryStream _asStream;
-
- private int _written;
+ private readonly VnMemoryStream _stream = new(InitBufferSize, false);
- internal JsonResponse(IObjectRental<JsonResponse> pool)
- {
- /*
- * I am breaking the memoryhandle rules by referrencing the same
- * memory handle in two different wrappers.
- */
+ private int _read;
+ private ReadOnlyMemory<byte> _dataSegToSend;
- _pool = pool;
-
- //Alloc buffer
- _handle = MemoryUtil.Shared.Alloc<byte>(4096, false);
-
- //Create stream around handle and not own it
- _asStream = VnMemoryStream.FromHandle(_handle, false, 0, false);
-
- //Get memory owner from handle
- _memoryOwner = _handle.ToMemoryManager(false);
- }
+ //Cleanup any dangling resources dangling somehow
+ ~JsonResponse() => Dispose();
- ~JsonResponse()
+ ///<inheritdoc/>
+ public void Dispose()
{
- _handle.Dispose();
+ _stream.Dispose();
+ GC.SuppressFinalize(this);
}
///<inheritdoc/>
public Stream GetSerialzingStream()
{
//Reset stream position
- _asStream.Seek(0, SeekOrigin.Begin);
- return _asStream;
+ _stream.Seek(0, SeekOrigin.Begin);
+ return _stream;
}
///<inheritdoc/>
public void SerializationComplete()
{
- //Reset written position
- _written = 0;
+ //Reset data read position
+ _read = 0;
+
//Update remaining pointer
- Remaining = Convert.ToInt32(_asStream.Position);
+ Remaining = Convert.ToInt32(_stream.Position);
+
+ /*
+ * Store the written segment for streaming now that the
+ * serialization is complete. This is the entire window of
+ * the stream, from 0 - length
+ */
+ _dataSegToSend = _stream.AsMemory();
}
@@ -94,21 +88,30 @@ namespace VNLib.Plugins.Essentials.Extensions
void IMemoryResponseReader.Advance(int written)
{
//Update position
- _written += written;
+ _read += written;
Remaining -= written;
+
+ Debug.Assert(Remaining > 0);
}
///<inheritdoc/>
void IMemoryResponseReader.Close()
{
//Reset and return to pool
- _written = 0;
+ _read = 0;
Remaining = 0;
+
+ //if the stream size was pretty large, shrink it before returning to the pool
+ if (_stream.Length > MaxSizeThreshold)
+ {
+ _stream.SetLength(InitBufferSize);
+ }
+
//Return self back to pool
_pool.Return(this);
}
///<inheritdoc/>
- ReadOnlyMemory<byte> IMemoryResponseReader.GetMemory() => _memoryOwner.Memory.Slice(_written, Remaining);
+ ReadOnlyMemory<byte> IMemoryResponseReader.GetMemory() => _dataSegToSend.Slice(_read, Remaining);
}
} \ No newline at end of file
diff --git a/lib/Plugins.Essentials/src/HttpEntity.cs b/lib/Plugins.Essentials/src/HttpEntity.cs
index ff728e3..a4788a3 100644
--- a/lib/Plugins.Essentials/src/HttpEntity.cs
+++ b/lib/Plugins.Essentials/src/HttpEntity.cs
@@ -30,7 +30,9 @@ using System.Diagnostics;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
+using VNLib.Utils.IO;
using VNLib.Net.Http;
+using VNLib.Plugins.Essentials.Content;
using VNLib.Plugins.Essentials.Sessions;
using VNLib.Plugins.Essentials.Extensions;
@@ -220,7 +222,41 @@ namespace VNLib.Plugins.Essentials
Entity.CloseResponse(
code,
type,
- new MemStreamWrapper(ms, (int)length)
+ entity: new MemStreamWrapper(ms, (int)length)
+ );
+
+ return;
+ }
+
+ /*
+ * Readonly vn streams can also use a shortcut to avoid http buffer allocation and
+ * async streaming. This is done by wrapping the stream in a memory response reader
+ *
+ * Allocating a memory manager requires that the stream is readonly
+ */
+ if (stream is VnMemoryStream vms && length < int.MaxValue)
+ {
+ Entity.CloseResponse(
+ code,
+ type,
+ entity: new VnStreamWrapper(vms, (int)length)
+ );
+
+ return;
+ }
+
+ /*
+ * Files can have a bit more performance using the RandomAccess library when reading
+ * sequential segments without buffering. It avoids a user-space copy and async reading
+ * performance without the file being opened as async.
+ */
+ if(stream is FileStream fs)
+ {
+ Entity.CloseResponse(
+ code,
+ type,
+ entity: new DirectFileStream(fs.SafeFileHandle),
+ length
);
return;
@@ -270,6 +306,40 @@ namespace VNLib.Plugins.Essentials
void IHttpEvent.DangerousChangeProtocol(IAlternateProtocol protocolHandler) => Entity.DangerousChangeProtocol(protocolHandler);
+ private sealed class VnStreamWrapper(VnMemoryStream memStream, int length) : IMemoryResponseReader
+ {
+ //Store memory buffer, causes an internal allocation, so avoid calling mutliple times
+ readonly ReadOnlyMemory<byte> _memory = memStream.AsMemory();
+
+ readonly int length = length;
+
+ /*
+ * Stream may be offset by the caller, it needs
+ * to be respected during streaming.
+ */
+ int read = (int)memStream.Position;
+
+ ///<inheritdoc/>
+ public int Remaining
+ {
+ get
+ {
+ Debug.Assert(length - read >= 0);
+ return length - read;
+ }
+ }
+
+ ///<inheritdoc/>
+ public void Advance(int written) => read += written;
+
+ ///<inheritdoc/>
+ public void Close() => memStream.Dispose();
+
+ ///<inheritdoc/>
+ public ReadOnlyMemory<byte> GetMemory() => _memory.Slice(read, Remaining);
+ }
+
+
private sealed class MemStreamWrapper(MemoryStream memStream, int length) : IMemoryResponseReader
{
readonly int length = length;
@@ -280,6 +350,7 @@ namespace VNLib.Plugins.Essentials
*/
int read = (int)memStream.Position;
+ ///<inheritdoc/>
public int Remaining
{
get
@@ -289,11 +360,13 @@ namespace VNLib.Plugins.Essentials
}
}
+ ///<inheritdoc/>
public void Advance(int written) => read += written;
///<inheritdoc/>
public void Close() => memStream.Dispose();
+ ///<inheritdoc/>
public ReadOnlyMemory<byte> GetMemory()
{
byte[] intBuffer = memStream.GetBuffer();
diff --git a/lib/Utils/src/IO/VnMemoryStream.cs b/lib/Utils/src/IO/VnMemoryStream.cs
index 2c604b2..4d51a08 100644
--- a/lib/Utils/src/IO/VnMemoryStream.cs
+++ b/lib/Utils/src/IO/VnMemoryStream.cs
@@ -49,9 +49,13 @@ namespace VNLib.Utils.IO
//Memory
private readonly IResizeableMemoryHandle<byte> _buffer;
+
//Default owns handle
private readonly bool OwnsHandle = true;
+ //Lazy loaded memory wrapper
+ private MemoryManager<byte>? _memoryWrapper;
+
/// <summary>
/// Creates a new <see cref="VnMemoryStream"/> pointing to the begining of memory, and consumes the handle.
/// </summary>
@@ -259,27 +263,31 @@ namespace VNLib.Utils.IO
cancellationToken.ThrowIfCancellationRequested();
+ //Memory manager requires 32bit or less in length
if(_length < Int32.MaxValue)
{
- //Safe to alloc a memory manager to do copy
- using MemoryManager<byte> asMemManager = _buffer.ToMemoryManager(false);
+ //Get/alloc the internal memory manager and get the block
+ ReadOnlyMemory<byte> asMemory = AsMemory();
+
+ Debug.Assert(asMemory.Length >= LenToPosDiff, "Internal memory block smaller than desired for stream copy");
/*
* CopyTo starts at the current position, as if calling Read()
* so the reader must be offset to match and the _length gives us the
* actual length of the stream and therefor the segment size
- */
+ */
- while(LenToPosDiff > 0)
+ while (LenToPosDiff > 0)
{
int blockSize = Math.Min((int)LenToPosDiff, bufferSize);
- Memory<byte> window = asMemManager.Memory.Slice((int)_position, blockSize);
+
+ ReadOnlyMemory<byte> window = asMemory.Slice((int)_position, blockSize);
//write async
await destination.WriteAsync(window, cancellationToken);
//Update position
- _position+= bufferSize;
+ _position += bufferSize;
}
}
else
@@ -581,6 +589,40 @@ namespace VNLib.Utils.IO
//Get span with no offset
return _buffer.AsSpan(0, len);
}
+
+ /// <summary>
+ /// Returns a <see cref="ReadOnlyMemory{T}"/> structure which is a window of the buffered
+ /// data as it currently sits. For writeable straems, you must call this function
+ /// every time the size of the stream changes. The memory structure is just a "pointer" to
+ /// the internal buffer.
+ /// </summary>
+ /// <returns>
+ /// A memory snapshot of the stream.
+ /// </returns>
+ /// <remarks>
+ /// This function causes an internal allocation on the first call. After the first call
+ /// to this function, all calls are thread-safe.
+ /// </remarks>
+ public ReadOnlyMemory<byte> AsMemory()
+ {
+ /*
+ * Safe cast stram length to int, because memory window requires a 32bit
+ * integer. Also will throw before allocating the mmemory manager
+ */
+
+ int len = Convert.ToInt32(_length);
+
+ //Defer/lazy init the memory manager
+ MemoryManager<byte> asMemory = LazyInitializer.EnsureInitialized(ref _memoryWrapper, AllocMemManager);
+
+ Debug.Assert(asMemory != null);
+
+ /*
+ * Buffer window may be larger than the actual stream legnth, so
+ * slice the memory to the actual length of the stream
+ */
+ return asMemory.Memory[..len];
+ }
/// <summary>
/// If the current stream is a readonly stream, creates a shallow copy for reading only.
@@ -589,5 +631,7 @@ namespace VNLib.Utils.IO
/// <exception cref="NotSupportedException"></exception>
public object Clone() => GetReadonlyShallowCopy();
+ private MemoryManager<byte> AllocMemManager() => _buffer.ToMemoryManager(false);
+
}
-} \ No newline at end of file
+}
diff --git a/lib/Utils/tests/IO/VnMemoryStreamTests.cs b/lib/Utils/tests/IO/VnMemoryStreamTests.cs
index 9742197..9bcb823 100644
--- a/lib/Utils/tests/IO/VnMemoryStreamTests.cs
+++ b/lib/Utils/tests/IO/VnMemoryStreamTests.cs
@@ -95,5 +95,49 @@ namespace VNLib.Utils.IO.Tests
Assert.ThrowsException<NotSupportedException>(() => vms.WriteByte(0));
}
+
+ [TestMethod()]
+ public void GetMemOrSpanTest()
+ {
+ //Alloc stream with some initial buffer size
+ using VnMemoryStream vms = new(1024, false);
+
+ //Ensure since no data was written, the returned windows are empty
+ Assert.IsTrue(vms.AsSpan().IsEmpty);
+ Assert.IsTrue(vms.AsMemory().IsEmpty);
+
+ //Write some data
+ byte[] testData = [1, 2, 3, 4, 5, 6, 7, 8];
+ vms.Write(testData);
+
+ Assert.AreEqual(vms.Length, testData.Length);
+
+ //Get the data as a span
+ ReadOnlySpan<byte> span = vms.AsSpan();
+ Assert.AreEqual(span.Length, testData.Length);
+
+ for (int i = 0; i < span.Length; i++)
+ {
+ Assert.AreEqual(span[i], testData[i]);
+ }
+
+ //Get the data as a memory
+ ReadOnlyMemory<byte> memory = vms.AsMemory();
+ Assert.AreEqual(memory.Length, testData.Length);
+
+ for (int i = 0; i < memory.Length; i++)
+ {
+ Assert.AreEqual(memory.Span[i], testData[i]);
+ }
+
+ //Get the data as a byte array
+ byte[] array = vms.ToArray();
+ Assert.AreEqual(array.Length, testData.Length);
+
+ for (int i = 0; i < array.Length; i++)
+ {
+ Assert.AreEqual(array[i], testData[i]);
+ }
+ }
}
} \ No newline at end of file