Просмотр исходного кода

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
Cody Robibero 2 месяцев назад
Родитель
Сommit
88ceaa39b0

+ 2 - 1
Directory.Packages.props

@@ -9,6 +9,7 @@
     <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
     <PackageVersion Include="AutoFixture" Version="4.18.1" />
     <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" Version="1.3.4" />
     <PackageVersion Include="CommandLineParser" Version="2.9.1" />
@@ -87,4 +88,4 @@
     <PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
     <PackageVersion Include="xunit" Version="2.9.3" />
   </ItemGroup>
-</Project>
+</Project>

+ 1 - 0
Emby.Server.Implementations/Emby.Server.Implementations.csproj

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

+ 10 - 9
Emby.Server.Implementations/Library/LibraryManager.cs

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

+ 6 - 8
Emby.Server.Implementations/Library/UserDataManager.cs

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

+ 1 - 0
MediaBrowser.Controller/MediaBrowser.Controller.csproj

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

+ 8 - 9
MediaBrowser.Controller/Providers/DirectoryService.cs

@@ -1,22 +1,21 @@
 #pragma warning disable CS1591
 
 using System;
-using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Linq;
+using BitFaster.Caching.Lru;
 using MediaBrowser.Model.IO;
 
 namespace MediaBrowser.Controller.Providers
 {
     public class DirectoryService : IDirectoryService
     {
-        private readonly IFileSystem _fileSystem;
-
-        private readonly ConcurrentDictionary<string, FileSystemMetadata[]> _cache = new(StringComparer.Ordinal);
+        // 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 ConcurrentDictionary<string, FileSystemMetadata> _fileCache = new(StringComparer.Ordinal);
-
-        private readonly ConcurrentDictionary<string, List<string>> _filePathCache = new(StringComparer.Ordinal);
+        private readonly IFileSystem _fileSystem;
 
         public DirectoryService(IFileSystem fileSystem)
         {
@@ -74,13 +73,13 @@ namespace MediaBrowser.Controller.Providers
 
         public FileSystemMetadata? GetFileSystemEntry(string path)
         {
-            if (!_fileCache.TryGetValue(path, out var result))
+            if (!_fileCache.TryGet(path, out var result))
             {
                 var file = _fileSystem.GetFileSystemInfo(path);
                 if (file?.Exists ?? false)
                 {
                     result = file;
-                    _fileCache.TryAdd(path, result);
+                    _fileCache.AddOrUpdate(path, result);
                 }
             }
 

+ 5 - 0
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -177,6 +177,11 @@ public class ServerConfiguration : BaseApplicationConfiguration
     /// <value>The library update duration.</value>
     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>
     /// Gets or sets the image saving convention.
     /// </summary>

+ 4 - 4
tests/Jellyfin.Controller.Tests/DirectoryServiceTests.cs

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