aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller
diff options
context:
space:
mode:
authorShadowghost <Ghost_of_Stone@web.de>2026-09-02 07:06:08 +0200
committerShadowghost <Ghost_of_Stone@web.de>2026-09-02 07:06:08 +0200
commitd73e3d964e3ce8197ec86bdaad447072305bc0d6 (patch)
treeac6e3add8f901f4b17fe5815fbfc30832235d955 /MediaBrowser.Controller
parente5dc3b8a54fdad218ba062f8adb58fb5e42d8f9d (diff)
Optimize Caches
Co-Authored-By: Cody Robibero <cody@robibe.ro>
Diffstat (limited to 'MediaBrowser.Controller')
-rw-r--r--MediaBrowser.Controller/Providers/DirectoryService.cs73
-rw-r--r--MediaBrowser.Controller/Providers/IDirectoryService.cs6
2 files changed, 57 insertions, 22 deletions
diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs
index 43f0f11dfe..336e35e293 100644
--- a/MediaBrowser.Controller/Providers/DirectoryService.cs
+++ b/MediaBrowser.Controller/Providers/DirectoryService.cs
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Runtime.CompilerServices;
using BitFaster.Caching.Lru;
using MediaBrowser.Model.IO;
@@ -11,33 +12,20 @@ namespace MediaBrowser.Controller.Providers
{
public class DirectoryService : IDirectoryService
{
- private const int DirectoryCacheSize = 2048;
- private const int FileCacheSize = 4096;
-
- // A bounded LRU sizes its table up front, so it costs several kilobytes while still empty.
- // One instance is a DI singleton and lives for the process, but the library code also news
- // one up per item in several loops and hands it to QueueRefresh, which holds on to it until
- // the refresh runs. Those instances usually ask about a single path, so each cache waits
- // until something actually looks in it.
- private readonly Lazy<FastConcurrentLru<string, FileSystemMetadata[]>> _cache
- = new(static () => new(Environment.ProcessorCount, DirectoryCacheSize, StringComparer.Ordinal));
-
- private readonly Lazy<FastConcurrentLru<string, FileSystemMetadata>> _fileCache
- = new(static () => new(Environment.ProcessorCount, FileCacheSize, StringComparer.Ordinal));
-
- private readonly Lazy<FastConcurrentLru<string, List<string>>> _filePathCache
- = new(static () => new(Environment.ProcessorCount, DirectoryCacheSize, StringComparer.Ordinal));
+ private static readonly ConditionalWeakTable<IFileSystem, DirectoryCache> _caches = [];
private readonly IFileSystem _fileSystem;
+ private readonly DirectoryCache _cache;
public DirectoryService(IFileSystem fileSystem)
{
_fileSystem = fileSystem;
+ _cache = _caches.GetValue(fileSystem, static _ => new DirectoryCache());
}
public FileSystemMetadata[] GetFileSystemEntries(string path)
{
- return _cache.Value.GetOrAdd(
+ return _cache.Entries.GetOrAdd(
path,
static (p, fileSystem) =>
{
@@ -99,7 +87,7 @@ namespace MediaBrowser.Controller.Providers
public FileSystemMetadata? GetFileSystemEntry(string path)
{
- if (!_fileCache.Value.TryGet(path, out var result))
+ if (!_cache.Files.TryGet(path, out var result))
{
var file = _fileSystem.GetFileSystemInfo(path);
@@ -108,7 +96,7 @@ namespace MediaBrowser.Controller.Providers
if (file?.Exists ?? false)
{
result = file;
- _fileCache.Value.AddOrUpdate(path, result);
+ _cache.Files.AddOrUpdate(path, result);
}
}
@@ -122,10 +110,12 @@ namespace MediaBrowser.Controller.Providers
{
if (clearCache)
{
- _filePathCache.Value.TryRemove(path, out _);
+ // Only what is remembered about this directory. Invalidate() also drops the parent,
+ // which a write needs but which is needless churn on a shared cache here.
+ Forget(path);
}
- var filePaths = _filePathCache.Value.GetOrAdd(
+ return _cache.FilePaths.GetOrAdd(
path,
static (p, fileSystem) =>
{
@@ -139,13 +129,52 @@ namespace MediaBrowser.Controller.Providers
}
},
_fileSystem);
+ }
- return filePaths;
+ public void Invalidate(string path)
+ {
+ // Everything remembered about the path itself, and the listing of the directory holding
+ // it, since writing a file changes what its directory contains. The caches belong to the
+ // file system rather than to this instance, so this is felt by every reader of it.
+ Forget(path);
+
+ var parent = Path.GetDirectoryName(path);
+ if (!string.IsNullOrEmpty(parent))
+ {
+ Forget(parent);
+ }
}
public bool IsAccessible(string path)
{
return _fileSystem.GetFileSystemEntryPaths(path).Any();
}
+
+ private void Forget(string path)
+ {
+ _cache.Entries.TryRemove(path, out _);
+ _cache.Files.TryRemove(path, out _);
+ _cache.FilePaths.TryRemove(path, out _);
+ }
+
+ private sealed class DirectoryCache
+ {
+ private const int DirectoryCacheSize = 2048;
+ private const int FileCacheSize = 8192;
+
+ // A DirectoryService no longer bounds how long its answers are trusted by dying, so a
+ // lifetime does. This is a staleness bound, not a snapshot: a long refresh can outlive it
+ // and re-read a directory partway through.
+ private static readonly TimeSpan _entryLifetime = TimeSpan.FromMinutes(1);
+
+ public ConcurrentTLru<string, FileSystemMetadata[]> Entries { get; }
+ = new(Environment.ProcessorCount, DirectoryCacheSize, StringComparer.Ordinal, _entryLifetime);
+
+ public ConcurrentTLru<string, FileSystemMetadata> Files { get; }
+ = new(Environment.ProcessorCount, FileCacheSize, StringComparer.Ordinal, _entryLifetime);
+
+ public ConcurrentTLru<string, List<string>> FilePaths { get; }
+ = new(Environment.ProcessorCount, DirectoryCacheSize, StringComparer.Ordinal, _entryLifetime);
+ }
}
}
diff --git a/MediaBrowser.Controller/Providers/IDirectoryService.cs b/MediaBrowser.Controller/Providers/IDirectoryService.cs
index 8a3fa33da3..609d094254 100644
--- a/MediaBrowser.Controller/Providers/IDirectoryService.cs
+++ b/MediaBrowser.Controller/Providers/IDirectoryService.cs
@@ -23,6 +23,12 @@ namespace MediaBrowser.Controller.Providers
IReadOnlyList<string> GetFilePaths(string path, bool clearCache);
+ /// <summary>
+ /// Forgets what is cached about a path and about the directory containing it.
+ /// </summary>
+ /// <param name="path">The file or directory path that changed.</param>
+ void Invalidate(string path);
+
bool IsAccessible(string path);
}
}