Limit cache size again

Co-Authored-By: Cody Robibero <cody@robibe.ro>
This commit is contained in:
Shadowghost
2026-09-05 07:08:14 +02:00
parent 46dd7d8e99
commit e5cd3381ac
3 changed files with 152 additions and 15 deletions
@@ -5,13 +5,19 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using MediaBrowser.Model.IO;
namespace MediaBrowser.Controller.Providers
{
public class DirectoryService : IDirectoryService
{
// TODO make static and switch to FastConcurrentLru.
// TODO replace with one shared bounded cache.
private const int MaxCachedRecords = 100_000;
private const int AccessIntervalMs = 1_000;
// Timeout cache if no access for 5 minutes.
private const int IdleTimeoutMs = 5 * 60 * 1_000;
private readonly ConcurrentDictionary<string, FileSystemMetadata[]> _cache = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, FileSystemMetadata> _fileCache = new(StringComparer.Ordinal);
@@ -20,6 +26,12 @@ namespace MediaBrowser.Controller.Providers
private readonly IFileSystem _fileSystem;
// ConcurrentDictionary.Count locks the dictionary, so keep an estimated counter.
// Concurrent factory runs can overcount and a clear racing an add can undercount,
// it only has to be roughly right.
private int _recordCount;
private long _lastAccess = Environment.TickCount64;
public DirectoryService(IFileSystem fileSystem)
{
_fileSystem = fileSystem;
@@ -27,20 +39,26 @@ namespace MediaBrowser.Controller.Providers
public FileSystemMetadata[] GetFileSystemEntries(string path)
{
DropCacheIfIdleOrFull();
return _cache.GetOrAdd(
path,
static (p, fileSystem) =>
static (p, state) =>
{
FileSystemMetadata[] entries;
try
{
return fileSystem.GetFileSystemEntries(p).ToArray();
entries = state.FileSystem.GetFileSystemEntries(p).ToArray();
}
catch (DirectoryNotFoundException)
{
return [];
entries = [];
}
Interlocked.Add(ref state.Service._recordCount, entries.Length + 1);
return entries;
},
_fileSystem);
(FileSystem: _fileSystem, Service: this));
}
public List<FileSystemMetadata> GetDirectories(string path)
@@ -89,13 +107,18 @@ namespace MediaBrowser.Controller.Providers
public FileSystemMetadata? GetFileSystemEntry(string path)
{
DropCacheIfIdleOrFull();
if (!_fileCache.TryGetValue(path, out var result))
{
var file = _fileSystem.GetFileSystemInfo(path);
if (file?.Exists ?? false)
{
result = file;
_fileCache.TryAdd(path, result);
if (_fileCache.TryAdd(path, result))
{
Interlocked.Increment(ref _recordCount);
}
}
}
@@ -107,25 +130,31 @@ namespace MediaBrowser.Controller.Providers
public IReadOnlyList<string> GetFilePaths(string path, bool clearCache)
{
if (clearCache)
if (clearCache && _filePathCache.TryRemove(path, out var cached))
{
_filePathCache.TryRemove(path, out _);
Interlocked.Add(ref _recordCount, -(cached.Count + 1));
}
DropCacheIfIdleOrFull();
var filePaths = _filePathCache.GetOrAdd(
path,
static (p, fileSystem) =>
static (p, state) =>
{
List<string> filePaths;
try
{
return fileSystem.GetFilePaths(p).OrderBy(x => x).ToList();
filePaths = state.FileSystem.GetFilePaths(p).OrderBy(x => x).ToList();
}
catch (DirectoryNotFoundException)
{
return [];
filePaths = [];
}
Interlocked.Add(ref state.Service._recordCount, filePaths.Count + 1);
return filePaths;
},
_fileSystem);
(FileSystem: _fileSystem, Service: this));
return filePaths;
}
@@ -146,11 +175,43 @@ namespace MediaBrowser.Controller.Providers
return _fileSystem.GetFileSystemEntryPaths(path).Any();
}
private void DropCacheIfIdleOrFull()
{
var nowMs = Environment.TickCount64;
var idleMs = nowMs - Volatile.Read(ref _lastAccess);
if (idleMs >= IdleTimeoutMs || Volatile.Read(ref _recordCount) >= MaxCachedRecords)
{
_cache.Clear();
_fileCache.Clear();
_filePathCache.Clear();
Volatile.Write(ref _recordCount, 0);
Volatile.Write(ref _lastAccess, nowMs);
return;
}
if (idleMs >= AccessIntervalMs)
{
Volatile.Write(ref _lastAccess, nowMs);
}
}
private void Forget(string path)
{
_cache.TryRemove(path, out _);
_fileCache.TryRemove(path, out _);
_filePathCache.TryRemove(path, out _);
if (_cache.TryRemove(path, out var entries))
{
Interlocked.Add(ref _recordCount, -(entries.Length + 1));
}
if (_fileCache.TryRemove(path, out _))
{
Interlocked.Decrement(ref _recordCount);
}
if (_filePathCache.TryRemove(path, out var filePaths))
{
Interlocked.Add(ref _recordCount, -(filePaths.Count + 1));
}
}
}
}