aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2024-06-18 21:17:28 -0400
committerLibravatar vnugent <public@vaughnnugent.com>2024-06-18 21:17:28 -0400
commitee3620b8168a42c8e571e853c751ad5999a9b907 (patch)
tree4134b47ace8b5b46ff4909ab198fcd6f50c3bd18
parentff0926be56fc6eafdce36411847d73bf4ce9f183 (diff)
feat: Add file path caching support
-rw-r--r--lib/Plugins.Essentials/src/EventProcessor.cs21
-rw-r--r--lib/Plugins.Essentials/src/EventProcessorConfig.cs6
-rw-r--r--lib/Plugins.Essentials/src/FilePathCache.cs123
-rw-r--r--lib/Utils/src/Extensions/MemoryExtensions.cs17
-rw-r--r--lib/Utils/src/IO/VnMemoryStream.cs32
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;
}