From 736fbbf82f0aa7819b64c118d59e80cd354ebaa8 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Fri, 15 Aug 2025 10:50:37 -0600 Subject: [PATCH] Improve FilePathCache performance --- Source/LibationFileManager/FilePathCache.cs | 73 +++++++++++++++++---- 1 file changed, 59 insertions(+), 14 deletions(-) diff --git a/Source/LibationFileManager/FilePathCache.cs b/Source/LibationFileManager/FilePathCache.cs index a6c99ef6..3cf6c528 100644 --- a/Source/LibationFileManager/FilePathCache.cs +++ b/Source/LibationFileManager/FilePathCache.cs @@ -1,10 +1,11 @@ -using System; +using FileManager; +using Newtonsoft.Json; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; -using FileManager; -using Newtonsoft.Json; +using System.Threading.Tasks; #nullable enable namespace LibationFileManager @@ -32,6 +33,10 @@ namespace LibationFileManager { Cache = JsonConvert.DeserializeObject>(File.ReadAllText(jsonFileV2)) ?? throw new NullReferenceException("File exists but deserialize is null. This will never happen when file is healthy."); + + //Once per startup, launch a task to validate existence of files in the cache. + //This is fire-and-forget. Since it is never awaited, it will no exceptions will be thrown to the caller. + Task.Run(ValidateAllFiles); } catch (Exception ex) { @@ -42,6 +47,23 @@ namespace LibationFileManager } } + private static void ValidateAllFiles() + { + bool cacheChanged = false; + foreach (var id in Cache.GetIDs()) + { + foreach (var entry in Cache.GetIdEntries(id)) + { + if (!File.Exists(entry.Path)) + { + cacheChanged |= Remove(entry); + } + } + } + if (cacheChanged) + save(); + } + public static bool Exists(string id, FileType type) => GetFirstPath(id, type) is not null; public static List<(FileType fileType, LongPath path)> GetFiles(string id) @@ -111,10 +133,20 @@ namespace LibationFileManager return false; } - public static void Insert(string id, string path) + public static void Insert(string id, params string[] paths) { - var type = FileTypes.GetFileTypeFromPath(path); - Insert(new CacheEntry(id, type, path)); + var newEntries + = paths + .Select(path => new CacheEntry(id, FileTypes.GetFileTypeFromPath(path), path)) + .ToList(); + + lock (locker) + Cache.AddRange(id, newEntries); + + if (Inserted is not null) + newEntries.ForEach(e => Inserted?.Invoke(null, e)); + + save(); } public static void Insert(CacheEntry entry) @@ -150,9 +182,11 @@ namespace LibationFileManager private class FileCacheV2 { [JsonProperty] - private readonly ConcurrentDictionary> Dictionary = new(); + private readonly ConcurrentDictionary> Dictionary = new(); private static object lockObject = new(); + public List GetIDs() => Dictionary.Keys.ToList(); + public List GetIdEntries(string id) { static List empty() => new(); @@ -162,23 +196,34 @@ namespace LibationFileManager public void Add(string id, TEntry entry) { - Dictionary.AddOrUpdate(id, [entry], (id, entries) => { entries.Add(entry); return entries; }); + Dictionary.AddOrUpdate(id, + (_, e) => [e], //Add new Dictionary Value + (id, existingEntries, newEntry) => //Update existing Dictionary Value + { + existingEntries.Add(entry); + return existingEntries; + }, + entry); } public void AddRange(string id, IEnumerable entries) { - Dictionary.AddOrUpdate(id, entries.ToList(), (id, entries) => - { - entries.AddRange(entries); - return entries; - }); + Dictionary.AddOrUpdate>(id, + (_, e) => e.ToHashSet(), //Add new Dictionary Value + (id, existingEntries, newEntries) => //Update existing Dictionary Value + { + foreach (var entry in newEntries) + existingEntries.Add(entry); + return existingEntries; + }, + entries); } public bool Remove(string id, TEntry entry) { lock (lockObject) { - if (Dictionary.TryGetValue(id, out List? entries)) + if (Dictionary.TryGetValue(id, out HashSet? entries)) { var removed = entries?.Remove(entry) ?? false; if (removed && entries?.Count == 0)