mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-07-09 03:04:24 -04:00
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:
parent
e9331fe9d7
commit
88ceaa39b0
@ -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" />
|
||||||
@ -87,4 +88,4 @@
|
|||||||
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
|
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
|
||||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
@ -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" />
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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" />
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user