Implement limiting caches (#13605)

* Implement basic expiring cache for LibraryManager

* Add expiring cache to more places

* Rider why

* Make DirectoryService caches static

* Use FastConcurrentLru

* Reduce default cache size

* Simplify DirectoryService caches

* Make directory service cache size at least 128
This commit is contained in:
Cody Robibero 2025-03-27 18:16:54 -06:00 committed by GitHub
parent e9331fe9d7
commit 88ceaa39b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 37 additions and 31 deletions

View File

@ -9,6 +9,7 @@
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" /> <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
<PackageVersion Include="AutoFixture" Version="4.18.1" /> <PackageVersion Include="AutoFixture" Version="4.18.1" />
<PackageVersion Include="BDInfo" Version="0.8.0" /> <PackageVersion Include="BDInfo" Version="0.8.0" />
<PackageVersion Include="BitFaster.Caching" Version="2.5.3" />
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.4" /> <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.4" />
<PackageVersion Include="BlurHashSharp" Version="1.3.4" /> <PackageVersion Include="BlurHashSharp" Version="1.3.4" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" /> <PackageVersion Include="CommandLineParser" Version="2.9.1" />

View File

@ -22,6 +22,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BitFaster.Caching" />
<PackageReference Include="DiscUtils.Udf" /> <PackageReference Include="DiscUtils.Udf" />
<PackageReference Include="Microsoft.Data.Sqlite" /> <PackageReference Include="Microsoft.Data.Sqlite" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" />

View File

@ -2,7 +2,6 @@
#pragma warning disable CA5394 #pragma warning disable CA5394
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
@ -11,6 +10,7 @@ using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BitFaster.Caching.Lru;
using Emby.Naming.Common; using Emby.Naming.Common;
using Emby.Naming.TV; using Emby.Naming.TV;
using Emby.Server.Implementations.Library.Resolvers; using Emby.Server.Implementations.Library.Resolvers;
@ -64,7 +64,6 @@ namespace Emby.Server.Implementations.Library
private const string ShortcutFileExtension = ".mblink"; private const string ShortcutFileExtension = ".mblink";
private readonly ILogger<LibraryManager> _logger; private readonly ILogger<LibraryManager> _logger;
private readonly ConcurrentDictionary<Guid, BaseItem> _cache;
private readonly ITaskManager _taskManager; private readonly ITaskManager _taskManager;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataRepository; private readonly IUserDataManager _userDataRepository;
@ -81,6 +80,7 @@ namespace Emby.Server.Implementations.Library
private readonly IPeopleRepository _peopleRepository; private readonly IPeopleRepository _peopleRepository;
private readonly ExtraResolver _extraResolver; private readonly ExtraResolver _extraResolver;
private readonly IPathManager _pathManager; private readonly IPathManager _pathManager;
private readonly FastConcurrentLru<Guid, BaseItem> _cache;
/// <summary> /// <summary>
/// The _root folder sync lock. /// The _root folder sync lock.
@ -150,7 +150,9 @@ namespace Emby.Server.Implementations.Library
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
_itemRepository = itemRepository; _itemRepository = itemRepository;
_imageProcessor = imageProcessor; _imageProcessor = imageProcessor;
_cache = new ConcurrentDictionary<Guid, BaseItem>();
_cache = new FastConcurrentLru<Guid, BaseItem>(_configurationManager.Configuration.CacheSize);
_namingOptions = namingOptions; _namingOptions = namingOptions;
_peopleRepository = peopleRepository; _peopleRepository = peopleRepository;
_pathManager = pathManager; _pathManager = pathManager;
@ -158,7 +160,7 @@ namespace Emby.Server.Implementations.Library
_configurationManager.ConfigurationUpdated += ConfigurationUpdated; _configurationManager.ConfigurationUpdated += ConfigurationUpdated;
RecordConfigurationValues(configurationManager.Configuration); RecordConfigurationValues(_configurationManager.Configuration);
} }
/// <summary> /// <summary>
@ -306,7 +308,7 @@ namespace Emby.Server.Implementations.Library
} }
} }
_cache[item.Id] = item; _cache.AddOrUpdate(item.Id, item);
} }
public void DeleteItem(BaseItem item, DeleteOptions options) public void DeleteItem(BaseItem item, DeleteOptions options)
@ -460,14 +462,13 @@ namespace Emby.Server.Implementations.Library
item.SetParent(null); item.SetParent(null);
_itemRepository.DeleteItem(item.Id); _itemRepository.DeleteItem(item.Id);
_cache.TryRemove(item.Id, out _);
foreach (var child in children) foreach (var child in children)
{ {
_itemRepository.DeleteItem(child.Id); _itemRepository.DeleteItem(child.Id);
_cache.TryRemove(child.Id, out _); _cache.TryRemove(child.Id, out _);
} }
_cache.TryRemove(item.Id, out _);
ReportItemRemoved(item, parent); ReportItemRemoved(item, parent);
} }
@ -1255,7 +1256,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentException("Guid can't be empty", nameof(id)); throw new ArgumentException("Guid can't be empty", nameof(id));
} }
if (_cache.TryGetValue(id, out BaseItem? item)) if (_cache.TryGet(id, out var item))
{ {
return item; return item;
} }
@ -1272,7 +1273,7 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc /> /// <inheritdoc />
public T? GetItemById<T>(Guid id) public T? GetItemById<T>(Guid id)
where T : BaseItem where T : BaseItem
{ {
var item = GetItemById(id); var item = GetItemById(id);
if (item is T typedItem) if (item is T typedItem)

View File

@ -1,14 +1,13 @@
#pragma warning disable RS0030 // Do not use banned APIs #pragma warning disable RS0030 // Do not use banned APIs
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using BitFaster.Caching.Lru;
using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@ -26,11 +25,9 @@ namespace Emby.Server.Implementations.Library
/// </summary> /// </summary>
public class UserDataManager : IUserDataManager public class UserDataManager : IUserDataManager
{ {
private readonly ConcurrentDictionary<string, UserItemData> _userData =
new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
private readonly IDbContextFactory<JellyfinDbContext> _repository; private readonly IDbContextFactory<JellyfinDbContext> _repository;
private readonly FastConcurrentLru<string, UserItemData> _cache;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="UserDataManager"/> class. /// Initializes a new instance of the <see cref="UserDataManager"/> class.
@ -43,6 +40,7 @@ namespace Emby.Server.Implementations.Library
{ {
_config = config; _config = config;
_repository = repository; _repository = repository;
_cache = new FastConcurrentLru<string, UserItemData>(Environment.ProcessorCount, _config.Configuration.CacheSize, StringComparer.OrdinalIgnoreCase);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -81,7 +79,7 @@ namespace Emby.Server.Implementations.Library
var userId = user.InternalId; var userId = user.InternalId;
var cacheKey = GetCacheKey(userId, item.Id); var cacheKey = GetCacheKey(userId, item.Id);
_userData.AddOrUpdate(cacheKey, userData, (_, _) => userData); _cache.AddOrUpdate(cacheKey, userData);
UserDataSaved?.Invoke(this, new UserDataSaveEventArgs UserDataSaved?.Invoke(this, new UserDataSaveEventArgs
{ {
@ -182,7 +180,7 @@ namespace Emby.Server.Implementations.Library
{ {
var cacheKey = GetCacheKey(user.InternalId, itemId); var cacheKey = GetCacheKey(user.InternalId, itemId);
if (_userData.TryGetValue(cacheKey, out var data)) if (_cache.TryGet(cacheKey, out var data))
{ {
return data; return data;
} }
@ -197,7 +195,7 @@ namespace Emby.Server.Implementations.Library
}; };
} }
return _userData.GetOrAdd(cacheKey, data); return _cache.GetOrAdd(cacheKey, _ => data);
} }
private UserItemData? GetUserDataInternal(Guid userId, Guid itemId, List<string> keys) private UserItemData? GetUserDataInternal(Guid userId, Guid itemId, List<string> keys)

View File

@ -18,6 +18,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BitFaster.Caching" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
<PackageReference Include="System.Threading.Tasks.Dataflow" /> <PackageReference Include="System.Threading.Tasks.Dataflow" />

View File

@ -1,23 +1,22 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using BitFaster.Caching.Lru;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
namespace MediaBrowser.Controller.Providers namespace MediaBrowser.Controller.Providers
{ {
public class DirectoryService : IDirectoryService public class DirectoryService : IDirectoryService
{ {
// These caches are primarily used for scanning so no reason to have them be large.
private static readonly FastConcurrentLru<string, FileSystemMetadata[]> _cache = new(Environment.ProcessorCount, Math.Max(128, Environment.ProcessorCount * 10), StringComparer.Ordinal);
private static readonly FastConcurrentLru<string, FileSystemMetadata> _fileCache = new(Environment.ProcessorCount, Math.Max(128, Environment.ProcessorCount * 10), StringComparer.Ordinal);
private static readonly FastConcurrentLru<string, List<string>> _filePathCache = new(Environment.ProcessorCount, Math.Max(128, Environment.ProcessorCount * 10), StringComparer.Ordinal);
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly ConcurrentDictionary<string, FileSystemMetadata[]> _cache = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, FileSystemMetadata> _fileCache = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, List<string>> _filePathCache = new(StringComparer.Ordinal);
public DirectoryService(IFileSystem fileSystem) public DirectoryService(IFileSystem fileSystem)
{ {
_fileSystem = fileSystem; _fileSystem = fileSystem;
@ -74,13 +73,13 @@ namespace MediaBrowser.Controller.Providers
public FileSystemMetadata? GetFileSystemEntry(string path) public FileSystemMetadata? GetFileSystemEntry(string path)
{ {
if (!_fileCache.TryGetValue(path, out var result)) if (!_fileCache.TryGet(path, out var result))
{ {
var file = _fileSystem.GetFileSystemInfo(path); var file = _fileSystem.GetFileSystemInfo(path);
if (file?.Exists ?? false) if (file?.Exists ?? false)
{ {
result = file; result = file;
_fileCache.TryAdd(path, result); _fileCache.AddOrUpdate(path, result);
} }
} }

View File

@ -177,6 +177,11 @@ public class ServerConfiguration : BaseApplicationConfiguration
/// <value>The library update duration.</value> /// <value>The library update duration.</value>
public int LibraryUpdateDuration { get; set; } = 30; public int LibraryUpdateDuration { get; set; } = 30;
/// <summary>
/// Gets or sets the maximum amount of items to cache.
/// </summary>
public int CacheSize { get; set; } = Environment.ProcessorCount * 100;
/// <summary> /// <summary>
/// Gets or sets the image saving convention. /// Gets or sets the image saving convention.
/// </summary> /// </summary>

View File

@ -181,8 +181,8 @@ namespace Jellyfin.Controller.Tests
fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is<string>(x => x == path))).Returns(newFileSystemMetadata); fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is<string>(x => x == path))).Returns(newFileSystemMetadata);
var secondResult = directoryService.GetFile(path); var secondResult = directoryService.GetFile(path);
Assert.Equal(cachedFileSystemMetadata, result); Assert.Equivalent(cachedFileSystemMetadata, result);
Assert.Equal(cachedFileSystemMetadata, secondResult); Assert.Equivalent(cachedFileSystemMetadata, secondResult);
} }
[Fact] [Fact]
@ -209,7 +209,7 @@ namespace Jellyfin.Controller.Tests
fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(cachedPaths); fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(cachedPaths);
var directoryService = new DirectoryService(fileSystemMock.Object); var directoryService = new DirectoryService(fileSystemMock.Object);
var result = directoryService.GetFilePaths(path); var result = directoryService.GetFilePaths(path, true);
fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(newPaths); fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(newPaths);
var secondResult = directoryService.GetFilePaths(path); var secondResult = directoryService.GetFilePaths(path);
@ -241,7 +241,7 @@ namespace Jellyfin.Controller.Tests
fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(cachedPaths); fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(cachedPaths);
var directoryService = new DirectoryService(fileSystemMock.Object); var directoryService = new DirectoryService(fileSystemMock.Object);
var result = directoryService.GetFilePaths(path); var result = directoryService.GetFilePaths(path, true);
fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(newPaths); fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(newPaths);
var secondResult = directoryService.GetFilePaths(path, true); var secondResult = directoryService.GetFilePaths(path, true);