diff --git a/Directory.Packages.props b/Directory.Packages.props
index 89311142cd..f9e111b389 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -9,6 +9,7 @@
+
@@ -87,4 +88,4 @@
-
+
\ No newline at end of file
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 8f89f35ac9..6722c20da6 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -22,6 +22,7 @@
+
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index eb8e310729..62f1f3d3aa 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/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 _logger;
- private readonly ConcurrentDictionary _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 _cache;
///
/// The _root folder sync lock.
@@ -150,7 +150,9 @@ namespace Emby.Server.Implementations.Library
_mediaEncoder = mediaEncoder;
_itemRepository = itemRepository;
_imageProcessor = imageProcessor;
- _cache = new ConcurrentDictionary();
+
+ _cache = new FastConcurrentLru(_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);
}
///
@@ -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
///
public T? GetItemById(Guid id)
- where T : BaseItem
+ where T : BaseItem
{
var item = GetItemById(id);
if (item is T typedItem)
diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs
index 8b88b904bb..be1d96bf0b 100644
--- a/Emby.Server.Implementations/Library/UserDataManager.cs
+++ b/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
///
public class UserDataManager : IUserDataManager
{
- private readonly ConcurrentDictionary _userData =
- new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
-
private readonly IServerConfigurationManager _config;
private readonly IDbContextFactory _repository;
+ private readonly FastConcurrentLru _cache;
///
/// Initializes a new instance of the class.
@@ -43,6 +40,7 @@ namespace Emby.Server.Implementations.Library
{
_config = config;
_repository = repository;
+ _cache = new FastConcurrentLru(Environment.ProcessorCount, _config.Configuration.CacheSize, StringComparer.OrdinalIgnoreCase);
}
///
@@ -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 keys)
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index ba4a2a59c4..d8aaf5ba01 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -18,6 +18,7 @@
+
diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs
index 474f09dc5e..4fca944771 100644
--- a/MediaBrowser.Controller/Providers/DirectoryService.cs
+++ b/MediaBrowser.Controller/Providers/DirectoryService.cs
@@ -1,23 +1,22 @@
#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
{
+ // These caches are primarily used for scanning so no reason to have them be large.
+ private static readonly FastConcurrentLru _cache = new(Environment.ProcessorCount, Math.Max(128, Environment.ProcessorCount * 10), StringComparer.Ordinal);
+ private static readonly FastConcurrentLru _fileCache = new(Environment.ProcessorCount, Math.Max(128, Environment.ProcessorCount * 10), StringComparer.Ordinal);
+ private static readonly FastConcurrentLru> _filePathCache = new(Environment.ProcessorCount, Math.Max(128, Environment.ProcessorCount * 10), StringComparer.Ordinal);
+
private readonly IFileSystem _fileSystem;
- private readonly ConcurrentDictionary _cache = new(StringComparer.Ordinal);
-
- private readonly ConcurrentDictionary _fileCache = new(StringComparer.Ordinal);
-
- private readonly ConcurrentDictionary> _filePathCache = new(StringComparer.Ordinal);
-
public DirectoryService(IFileSystem fileSystem)
{
_fileSystem = 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);
}
}
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index 693bf90e71..f4e6c8e2c5 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -177,6 +177,11 @@ public class ServerConfiguration : BaseApplicationConfiguration
/// The library update duration.
public int LibraryUpdateDuration { get; set; } = 30;
+ ///
+ /// Gets or sets the maximum amount of items to cache.
+ ///
+ public int CacheSize { get; set; } = Environment.ProcessorCount * 100;
+
///
/// Gets or sets the image saving convention.
///
diff --git a/tests/Jellyfin.Controller.Tests/DirectoryServiceTests.cs b/tests/Jellyfin.Controller.Tests/DirectoryServiceTests.cs
index 07b53bf74c..9e7a8c8440 100644
--- a/tests/Jellyfin.Controller.Tests/DirectoryServiceTests.cs
+++ b/tests/Jellyfin.Controller.Tests/DirectoryServiceTests.cs
@@ -181,8 +181,8 @@ namespace Jellyfin.Controller.Tests
fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is(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(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(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(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(x => x == path), false)).Returns(newPaths);
var secondResult = directoryService.GetFilePaths(path, true);