diff options
-rw-r--r-- | lib/Plugins.Essentials/src/EventProcessor.cs | 21 | ||||
-rw-r--r-- | lib/Plugins.Essentials/src/EventProcessorConfig.cs | 6 | ||||
-rw-r--r-- | lib/Plugins.Essentials/src/FilePathCache.cs | 123 | ||||
-rw-r--r-- | lib/Utils/src/Extensions/MemoryExtensions.cs | 17 | ||||
-rw-r--r-- | lib/Utils/src/IO/VnMemoryStream.cs | 32 |
5 files changed, 192 insertions, 7 deletions
diff --git a/lib/Plugins.Essentials/src/EventProcessor.cs b/lib/Plugins.Essentials/src/EventProcessor.cs index ba7aa3c..908ad07 100644 --- a/lib/Plugins.Essentials/src/EventProcessor.cs +++ b/lib/Plugins.Essentials/src/EventProcessor.cs @@ -159,6 +159,8 @@ namespace VNLib.Plugins.Essentials private readonly MiddlewareController _middleware = new(config); + private readonly FilePathCache _pathCache = FilePathCache.GetCacheStore(config.FilePathCacheMaxAge); + ///<inheritdoc/> public virtual async ValueTask ClientConnectedAsync(IHttpEvent httpEvent) { @@ -620,6 +622,25 @@ namespace VNLib.Plugins.Essentials /// <returns>True if the resource exists and is allowed to be accessed</returns> public bool FindResourceInRoot(string resourcePath, out string path) { + //Try to get the translated file path from cache + if (_pathCache.TryGetMappedPath(resourcePath, out path)) + { + return true; + } + + //Cache miss, force a lookup + if (FindFileResourceInternal(resourcePath, out path)) + { + //Store the path in the cache for next lookup + _pathCache.StorePathMapping(resourcePath, path); + return true; + } + + return false; + } + + private bool FindFileResourceInternal(string resourcePath, out string path) + { //Check after fully qualified path name because above is a special case path = TranslateResourcePath(resourcePath); string extension = Path.GetExtension(path); diff --git a/lib/Plugins.Essentials/src/EventProcessorConfig.cs b/lib/Plugins.Essentials/src/EventProcessorConfig.cs index 6e101eb..831f5dc 100644 --- a/lib/Plugins.Essentials/src/EventProcessorConfig.cs +++ b/lib/Plugins.Essentials/src/EventProcessorConfig.cs @@ -90,5 +90,11 @@ namespace VNLib.Plugins.Essentials /// A <see cref="TimeSpan"/> for how long a connection may remain open before all operations are cancelled /// </summary> public TimeSpan ExecutionTimeout { get; init; } = TimeSpan.Zero; + + /// <summary> + /// Enables or disables the use of the file path cache. If set to zero , the cache will be disabled, + /// otherwise sets the maximum amount of time a file path is to be cached. + /// </summary> + public TimeSpan FilePathCacheMaxAge { get; init; } = TimeSpan.Zero; } }
\ No newline at end of file diff --git a/lib/Plugins.Essentials/src/FilePathCache.cs b/lib/Plugins.Essentials/src/FilePathCache.cs new file mode 100644 index 0000000..6a53f87 --- /dev/null +++ b/lib/Plugins.Essentials/src/FilePathCache.cs @@ -0,0 +1,123 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: FilePathCache.cs +* +* FilePathCache.cs is part of VNLib.Plugins.Essentials which is part +* of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials 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.Plugins.Essentials 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; +using System.Diagnostics; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; + +namespace VNLib.Plugins.Essentials +{ + + /// <summary> + /// Represents a cache store for translated file paths to avoid + /// path probing and file system syscalls + /// </summary> + internal abstract class FilePathCache + { + + public abstract bool TryGetMappedPath(string filePath, [NotNullWhen(true)] out string? cachedPath); + + /// <summary> + /// Attempts to store a path mapping in the cache store + /// </summary> + /// <param name="requestPath">The requested input path</param> + /// <param name="filePath">The filesystem path this requested path maps to</param> + public abstract void StorePathMapping(string requestPath, string filePath); + + /// <summary> + /// Creates a new cache store with the specified max age. If max age is zero, the + /// cache store will be disabled. + /// </summary> + /// <param name="maxAge">The max time to store the cahced path reecord</param> + /// <returns>The cache store</returns> + public static FilePathCache GetCacheStore(TimeSpan maxAge) + { + return maxAge == TimeSpan.Zero + ? new DisabledCacheStore() + : new DictBackedFilePathCache(maxAge); + } + + /* + * A very basic dictionary cache that stores translated paths + * from a request input path to a filesystem path. + * + * This must be thread safe as it's called in a multithreaded context. + */ + private sealed class DictBackedFilePathCache(TimeSpan maxAge) : FilePathCache + { + private readonly ConcurrentDictionary<string, CachedPath> _pathCache = new(StringComparer.OrdinalIgnoreCase); + + ///<inheritdoc/> + public override bool TryGetMappedPath(string filePath, [NotNullWhen(true)] out string? cachedPath) + { + if (_pathCache.TryGetValue(filePath, out CachedPath cp)) + { + //TODO: Implement a cache eviction policy + cachedPath = cp.Path; + return true; + } + + cachedPath = null; + return false; + } + + ///<inheritdoc/> + public override void StorePathMapping(string requestPath, string filePath) + { + ArgumentException.ThrowIfNullOrWhiteSpace(requestPath); + + //Cache path is an internal assignment. Should never be null + Debug.Assert(filePath is not null); + + //TODO: Implement a cache eviction policy + _pathCache[requestPath] = new CachedPath { Path = filePath, LastStored = DateTime.MinValue.Ticks }; + } + + private struct CachedPath + { + public string Path; + public long LastStored; + } + } + + /* + * A cache store that does nothing, it always misses and will + * cause a normal file fetch + */ + private sealed class DisabledCacheStore : FilePathCache + { + ///<inheritdoc/> + public override void StorePathMapping(string requestPath, string filePath) + { } + + ///<inheritdoc/> + public override bool TryGetMappedPath(string filePath, [NotNullWhen(true)] out string? cachedPath) + { + cachedPath = null; + return false; + } + } + } +} diff --git a/lib/Utils/src/Extensions/MemoryExtensions.cs b/lib/Utils/src/Extensions/MemoryExtensions.cs index 99d4cf1..f2399a2 100644 --- a/lib/Utils/src/Extensions/MemoryExtensions.cs +++ b/lib/Utils/src/Extensions/MemoryExtensions.cs @@ -268,6 +268,23 @@ namespace VNLib.Utils.Extensions } /// <summary> + /// Gets a reference to the element at the specified offset from the base + /// address of the <see cref="MemoryHandle{T}"/> and casts it to a byte reference + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="block"></param> + /// <param name="offset">The number of elements to offset the base reference by</param> + /// <returns>The reinterpreted byte reference at the first byte of the element offset</returns> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref byte GetOffsetByteRef<T>(this IMemoryHandle<T> block, nint offset) + { + ArgumentOutOfRangeException.ThrowIfNegative(offset); + return ref GetOffsetByteRef(block, (nuint)offset); + } + + /// <summary> /// Gets a 64bit friendly span offset for the current <see cref="MemoryHandle{T}"/> /// </summary> /// <typeparam name="T"></typeparam> diff --git a/lib/Utils/src/IO/VnMemoryStream.cs b/lib/Utils/src/IO/VnMemoryStream.cs index 23df869..2c604b2 100644 --- a/lib/Utils/src/IO/VnMemoryStream.cs +++ b/lib/Utils/src/IO/VnMemoryStream.cs @@ -87,8 +87,12 @@ namespace VNLib.Utils.IO /// <summary> /// Converts a writable <see cref="VnMemoryStream"/> to readonly to allow shallow copies /// </summary> + /// <remarks> + /// This funciton will convert the stream passed into it to a readonly stream. + /// The function passes through the input stream as the return value + /// </remarks> /// <param name="stream">The stream to make readonly</param> - /// <returns>The readonly stream</returns> + /// <returns>A reference to the modified input stream</returns> public static VnMemoryStream CreateReadonly(VnMemoryStream stream) { ArgumentNullException.ThrowIfNull(stream); @@ -103,7 +107,7 @@ namespace VNLib.Utils.IO /// global heap instance. /// </summary> public VnMemoryStream() : this(MemoryUtil.Shared) { } - + /// <summary> /// Create a new memory stream where buffers will be allocated from the specified heap /// </summary> @@ -111,7 +115,13 @@ namespace VNLib.Utils.IO /// <exception cref="OutOfMemoryException"></exception> /// <exception cref="ArgumentNullException"></exception> public VnMemoryStream(IUnmangedHeap heap) : this(heap, DefaultBufferSize, false) { } - + + /// <summary> + /// Creates a new memory stream using the <see cref="MemoryUtil.Shared"/> + /// global heap instance. + /// </summary> + public VnMemoryStream(nuint bufferSize, bool zero) : this(MemoryUtil.Shared, bufferSize, zero) { } + /// <summary> /// Creates a new memory stream and pre-allocates the internal /// buffer of the specified size on the specified heap to avoid resizing. @@ -356,7 +366,7 @@ namespace VNLib.Utils.IO } ///<inheritdoc/> - public override unsafe int ReadByte() + public override int ReadByte() { if (LenToPosDiff == 0) { @@ -364,7 +374,7 @@ namespace VNLib.Utils.IO } //get the value at the current position - ref byte ptr = ref _buffer.GetOffsetByteRef((nuint)_position); + ref byte ptr = ref _buffer.GetOffsetByteRef(_position); //Increment position _position++; @@ -488,7 +498,7 @@ namespace VNLib.Utils.IO throw new NotSupportedException("Write operation is not allowed on readonly stream!"); } //Calculate the new final position - nint newPos = (_position + buffer.Length); + nint newPos = checked(_position + buffer.Length); //Determine if the buffer needs to be expanded if (buffer.Length > LenToPosDiff) { @@ -497,8 +507,16 @@ namespace VNLib.Utils.IO //Update length _length = newPos; } + //Copy the input buffer to the internal buffer - MemoryUtil.Copy(buffer, 0, _buffer, (nuint)_position, buffer.Length); + MemoryUtil.Copy( + source: buffer, + sourceOffset: 0, + dest: _buffer, + destOffset: (nuint)_position, + count: buffer.Length + ); + //Update the position _position = newPos; } |