improve file cache manager performance
This commit is contained in:
@@ -15,7 +15,7 @@ public sealed class FileCacheManager : IDisposable
|
|||||||
private const string _penumbraPrefix = "{penumbra}";
|
private const string _penumbraPrefix = "{penumbra}";
|
||||||
private readonly MareConfigService _configService;
|
private readonly MareConfigService _configService;
|
||||||
private readonly string _csvPath;
|
private readonly string _csvPath;
|
||||||
private readonly ConcurrentDictionary<string, FileCacheEntity> _fileCaches = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, List<FileCacheEntity>> _fileCaches = new(StringComparer.Ordinal);
|
||||||
private readonly object _fileWriteLock = new();
|
private readonly object _fileWriteLock = new();
|
||||||
private readonly IpcManager _ipcManager;
|
private readonly IpcManager _ipcManager;
|
||||||
private readonly ILogger<FileCacheManager> _logger;
|
private readonly ILogger<FileCacheManager> _logger;
|
||||||
@@ -39,8 +39,15 @@ public sealed class FileCacheManager : IDisposable
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Failed to move BAK to ORG, deleting BAK");
|
_logger.LogWarning(ex, "Failed to move BAK to ORG, deleting BAK");
|
||||||
if (File.Exists(CsvBakPath))
|
try
|
||||||
File.Delete(CsvBakPath);
|
{
|
||||||
|
if (File.Exists(CsvBakPath))
|
||||||
|
File.Delete(CsvBakPath);
|
||||||
|
}
|
||||||
|
catch (Exception ex1)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex1, "Could not delete bak file");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +62,7 @@ public sealed class FileCacheManager : IDisposable
|
|||||||
var hash = splittedEntry[0];
|
var hash = splittedEntry[0];
|
||||||
var path = splittedEntry[1];
|
var path = splittedEntry[1];
|
||||||
var time = splittedEntry[2];
|
var time = splittedEntry[2];
|
||||||
_fileCaches[path] = ReplacePathPrefixes(new FileCacheEntity(hash, path, time));
|
AddHashedFile(ReplacePathPrefixes(new FileCacheEntity(hash, path, time)));
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
@@ -96,7 +103,7 @@ public sealed class FileCacheManager : IDisposable
|
|||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<FileCacheEntity> GetAllFileCaches() => _fileCaches.Values.ToList();
|
public List<FileCacheEntity> GetAllFileCaches() => _fileCaches.Values.SelectMany(v => v).ToList();
|
||||||
|
|
||||||
public string GetCacheFilePath(string hash, bool isTemporaryFile)
|
public string GetCacheFilePath(string hash, bool isTemporaryFile)
|
||||||
{
|
{
|
||||||
@@ -105,19 +112,18 @@ public sealed class FileCacheManager : IDisposable
|
|||||||
|
|
||||||
public FileCacheEntity? GetFileCacheByHash(string hash)
|
public FileCacheEntity? GetFileCacheByHash(string hash)
|
||||||
{
|
{
|
||||||
var entry = _fileCaches.FirstOrDefault(f => string.Equals(f.Value.Hash, hash, StringComparison.Ordinal));
|
if (_fileCaches.TryGetValue(hash, out var hashes))
|
||||||
if (!EqualityComparer<KeyValuePair<string, FileCacheEntity>>.Default.Equals(entry, default))
|
|
||||||
{
|
{
|
||||||
return GetValidatedFileCache(entry.Value);
|
var item = hashes.FirstOrDefault();
|
||||||
|
if (item != null) return GetValidatedFileCache(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public FileCacheEntity? GetFileCacheByPath(string path)
|
public FileCacheEntity? GetFileCacheByPath(string path)
|
||||||
{
|
{
|
||||||
var cleanedPath = path.Replace("/", "\\", StringComparison.OrdinalIgnoreCase).ToLowerInvariant().Replace(_ipcManager.PenumbraModDirectory!.ToLowerInvariant(), "", StringComparison.OrdinalIgnoreCase);
|
var cleanedPath = path.Replace("/", "\\", StringComparison.OrdinalIgnoreCase).ToLowerInvariant().Replace(_ipcManager.PenumbraModDirectory!.ToLowerInvariant(), "", StringComparison.OrdinalIgnoreCase);
|
||||||
var entry = _fileCaches.Values.FirstOrDefault(f => f.ResolvedFilepath.EndsWith(cleanedPath, StringComparison.OrdinalIgnoreCase));
|
var entry = _fileCaches.SelectMany(v => v.Value).FirstOrDefault(f => f.ResolvedFilepath.EndsWith(cleanedPath, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
if (entry == null)
|
if (entry == null)
|
||||||
{
|
{
|
||||||
@@ -130,22 +136,26 @@ public sealed class FileCacheManager : IDisposable
|
|||||||
return validatedCacheEntry;
|
return validatedCacheEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveHash(FileCacheEntity? entity)
|
public void RemoveHashedFile(FileCacheEntity? fileCache)
|
||||||
{
|
{
|
||||||
if (entity != null)
|
if (fileCache == null) return;
|
||||||
|
if (_fileCaches.TryGetValue(fileCache.Hash, out var caches))
|
||||||
{
|
{
|
||||||
_logger.LogTrace("Removing {path}", entity.ResolvedFilepath);
|
caches?.RemoveAll(c => string.Equals(c.PrefixedFilePath, fileCache.PrefixedFilePath, StringComparison.Ordinal));
|
||||||
_fileCaches.Remove(entity.PrefixedFilePath, out _);
|
if (_fileCaches.Count == 0)
|
||||||
|
{
|
||||||
|
_fileCaches.Remove(fileCache.Hash, out _);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateHash(FileCacheEntity fileCache)
|
public void UpdateHashedFile(FileCacheEntity fileCache)
|
||||||
{
|
{
|
||||||
_logger.LogTrace("Updating hash for {path}", fileCache.ResolvedFilepath);
|
_logger.LogTrace("Updating hash for {path}", fileCache.ResolvedFilepath);
|
||||||
fileCache.Hash = Crypto.GetFileHash(fileCache.ResolvedFilepath);
|
fileCache.Hash = Crypto.GetFileHash(fileCache.ResolvedFilepath);
|
||||||
fileCache.LastModifiedDateTicks = new FileInfo(fileCache.ResolvedFilepath).LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture);
|
fileCache.LastModifiedDateTicks = new FileInfo(fileCache.ResolvedFilepath).LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture);
|
||||||
_fileCaches.Remove(fileCache.PrefixedFilePath, out _);
|
RemoveHashedFile(fileCache);
|
||||||
_fileCaches[fileCache.PrefixedFilePath] = fileCache;
|
AddHashedFile(fileCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
public (FileState, FileCacheEntity) ValidateFileCacheEntity(FileCacheEntity fileCache)
|
public (FileState, FileCacheEntity) ValidateFileCacheEntity(FileCacheEntity fileCache)
|
||||||
@@ -167,9 +177,9 @@ public sealed class FileCacheManager : IDisposable
|
|||||||
public void WriteOutFullCsv()
|
public void WriteOutFullCsv()
|
||||||
{
|
{
|
||||||
StringBuilder sb = new();
|
StringBuilder sb = new();
|
||||||
foreach (var entry in _fileCaches.OrderBy(f => f.Value.PrefixedFilePath, StringComparer.OrdinalIgnoreCase))
|
foreach (var entry in _fileCaches.SelectMany(k => k.Value).OrderBy(f => f.PrefixedFilePath, StringComparer.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
sb.AppendLine(entry.Value.CsvEntry);
|
sb.AppendLine(entry.CsvEntry);
|
||||||
}
|
}
|
||||||
if (File.Exists(_csvPath))
|
if (File.Exists(_csvPath))
|
||||||
{
|
{
|
||||||
@@ -189,12 +199,22 @@ public sealed class FileCacheManager : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void AddHashedFile(FileCacheEntity fileCache)
|
||||||
|
{
|
||||||
|
if (!_fileCaches.TryGetValue(fileCache.Hash, out var entries))
|
||||||
|
{
|
||||||
|
_fileCaches[fileCache.Hash] = entries = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.Add(fileCache);
|
||||||
|
}
|
||||||
|
|
||||||
private FileCacheEntity? CreateFileCacheEntity(FileInfo fileInfo, string prefixedPath, string? hash = null)
|
private FileCacheEntity? CreateFileCacheEntity(FileInfo fileInfo, string prefixedPath, string? hash = null)
|
||||||
{
|
{
|
||||||
hash ??= Crypto.GetFileHash(fileInfo.FullName);
|
hash ??= Crypto.GetFileHash(fileInfo.FullName);
|
||||||
var entity = new FileCacheEntity(hash, prefixedPath, fileInfo.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture));
|
var entity = new FileCacheEntity(hash, prefixedPath, fileInfo.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture));
|
||||||
entity = ReplacePathPrefixes(entity);
|
entity = ReplacePathPrefixes(entity);
|
||||||
_fileCaches[prefixedPath] = entity;
|
AddHashedFile(entity);
|
||||||
lock (_fileWriteLock)
|
lock (_fileWriteLock)
|
||||||
{
|
{
|
||||||
File.AppendAllLines(_csvPath, new[] { entity.CsvEntry });
|
File.AppendAllLines(_csvPath, new[] { entity.CsvEntry });
|
||||||
@@ -230,13 +250,13 @@ public sealed class FileCacheManager : IDisposable
|
|||||||
var file = new FileInfo(fileCache.ResolvedFilepath);
|
var file = new FileInfo(fileCache.ResolvedFilepath);
|
||||||
if (!file.Exists)
|
if (!file.Exists)
|
||||||
{
|
{
|
||||||
_fileCaches.Remove(fileCache.PrefixedFilePath, out _);
|
RemoveHashedFile(fileCache);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.Equals(file.LastWriteTimeUtc.Ticks.ToString(), fileCache.LastModifiedDateTicks, StringComparison.Ordinal))
|
if (!string.Equals(file.LastWriteTimeUtc.Ticks.ToString(), fileCache.LastModifiedDateTicks, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
UpdateHash(fileCache);
|
UpdateHashedFile(fileCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
return fileCache;
|
return fileCache;
|
||||||
|
|||||||
@@ -258,12 +258,12 @@ public sealed class PeriodicFileScanner : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
foreach (var entity in entitiesToUpdate)
|
foreach (var entity in entitiesToUpdate)
|
||||||
{
|
{
|
||||||
_fileDbManager.UpdateHash(entity);
|
_fileDbManager.UpdateHashedFile(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var entity in entitiesToRemove)
|
foreach (var entity in entitiesToRemove)
|
||||||
{
|
{
|
||||||
_fileDbManager.RemoveHash(entity);
|
_fileDbManager.RemoveHashedFile(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
_fileDbManager.WriteOutFullCsv();
|
_fileDbManager.WriteOutFullCsv();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors></Authors>
|
<Authors></Authors>
|
||||||
<Company></Company>
|
<Company></Company>
|
||||||
<Version>0.8.22</Version>
|
<Version>0.8.23</Version>
|
||||||
<Description></Description>
|
<Description></Description>
|
||||||
<Copyright></Copyright>
|
<Copyright></Copyright>
|
||||||
<PackageProjectUrl>https://github.com/Penumbra-Sync/client</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/Penumbra-Sync/client</PackageProjectUrl>
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ using MareSynchronos.Utils;
|
|||||||
using MareSynchronos.WebAPI.Files;
|
using MareSynchronos.WebAPI.Files;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Diagnostics;
|
||||||
using ObjectKind = MareSynchronos.API.Data.Enum.ObjectKind;
|
using ObjectKind = MareSynchronos.API.Data.Enum.ObjectKind;
|
||||||
|
|
||||||
namespace MareSynchronos.PlayerData.Pairs;
|
namespace MareSynchronos.PlayerData.Pairs;
|
||||||
@@ -422,6 +424,8 @@ public sealed class CachedPlayer : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
downloadToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var appToken = _applicationCancellationTokenSource?.Token;
|
var appToken = _applicationCancellationTokenSource?.Token;
|
||||||
while ((!_applicationTask?.IsCompleted ?? false)
|
while ((!_applicationTask?.IsCompleted ?? false)
|
||||||
&& !downloadToken.IsCancellationRequested
|
&& !downloadToken.IsCancellationRequested
|
||||||
@@ -577,12 +581,19 @@ public sealed class CachedPlayer : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
private List<FileReplacementData> TryCalculateModdedDictionary(CharacterData charaData, out Dictionary<string, string> moddedDictionary, CancellationToken token)
|
private List<FileReplacementData> TryCalculateModdedDictionary(CharacterData charaData, out Dictionary<string, string> moddedDictionary, CancellationToken token)
|
||||||
{
|
{
|
||||||
|
Stopwatch st = Stopwatch.StartNew();
|
||||||
List<FileReplacementData> missingFiles = new();
|
List<FileReplacementData> missingFiles = new();
|
||||||
moddedDictionary = new Dictionary<string, string>(StringComparer.Ordinal);
|
moddedDictionary = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
|
ConcurrentDictionary<string, string> outputDict = new ConcurrentDictionary<string, string>(StringComparer.Ordinal);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var replacementList = charaData.FileReplacements.SelectMany(k => k.Value.Where(v => string.IsNullOrEmpty(v.FileSwapPath))).DistinctBy(p => p.Hash).ToList();
|
var replacementList = charaData.FileReplacements.SelectMany(k => k.Value.Where(v => string.IsNullOrEmpty(v.FileSwapPath))).DistinctBy(p => p.Hash).ToList();
|
||||||
foreach (var item in replacementList)
|
Parallel.ForEach(replacementList, new ParallelOptions()
|
||||||
|
{
|
||||||
|
CancellationToken = token,
|
||||||
|
MaxDegreeOfParallelism = 4
|
||||||
|
},
|
||||||
|
(item) =>
|
||||||
{
|
{
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
var fileCache = _fileDbManager.GetFileCacheByHash(item.Hash);
|
var fileCache = _fileDbManager.GetFileCacheByHash(item.Hash);
|
||||||
@@ -590,7 +601,7 @@ public sealed class CachedPlayer : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
foreach (var gamePath in item.GamePaths)
|
foreach (var gamePath in item.GamePaths)
|
||||||
{
|
{
|
||||||
moddedDictionary[gamePath] = fileCache.ResolvedFilepath;
|
outputDict[gamePath] = fileCache.ResolvedFilepath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -598,7 +609,9 @@ public sealed class CachedPlayer : DisposableMediatorSubscriberBase
|
|||||||
Logger.LogTrace("Missing file: {hash}", item.Hash);
|
Logger.LogTrace("Missing file: {hash}", item.Hash);
|
||||||
missingFiles.Add(item);
|
missingFiles.Add(item);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
|
moddedDictionary = outputDict.ToDictionary(k => k.Key, k => k.Value, StringComparer.Ordinal);
|
||||||
|
|
||||||
foreach (var item in charaData.FileReplacements.SelectMany(k => k.Value.Where(v => !string.IsNullOrEmpty(v.FileSwapPath))).ToList())
|
foreach (var item in charaData.FileReplacements.SelectMany(k => k.Value.Where(v => !string.IsNullOrEmpty(v.FileSwapPath))).ToList())
|
||||||
{
|
{
|
||||||
@@ -613,7 +626,8 @@ public sealed class CachedPlayer : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
PluginLog.Error(ex, "Something went wrong during calculation replacements");
|
PluginLog.Error(ex, "Something went wrong during calculation replacements");
|
||||||
}
|
}
|
||||||
Logger.LogDebug("ModdedPaths calculated, missing files: {count}", missingFiles.Count);
|
st.Stop();
|
||||||
|
Logger.LogDebug("ModdedPaths calculated in {time}ms, missing files: {count}", st.ElapsedMilliseconds, missingFiles.Count);
|
||||||
return missingFiles;
|
return missingFiles;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,7 +259,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
Logger.LogError("Hash mismatch after extracting, got {hash}, expected {expectedHash}, deleting file", entry?.Hash, file.Hash);
|
Logger.LogError("Hash mismatch after extracting, got {hash}, expected {expectedHash}, deleting file", entry?.Hash, file.Hash);
|
||||||
File.Delete(filePath);
|
File.Delete(filePath);
|
||||||
_fileDbManager.RemoveHash(entry);
|
_fileDbManager.RemoveHashedFile(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
Reference in New Issue
Block a user