From 657a7bb6bcd5028002bed3021e81085a9986e593 Mon Sep 17 00:00:00 2001 From: Mbucari <37587114+Mbucari@users.noreply.github.com> Date: Sun, 20 Jul 2025 17:22:05 -0600 Subject: [PATCH] Improve podcast episode GridEntry creation performance. Tested on a library with ~5000 podcast episodes on an AMD Ryzen 7700X. Startup time decreases by ~400 ms in Release mode. --- .../GridView/GridEntry[TStatus].cs | 33 ++++++++++- .../GridView/LibraryBookEntry[TStatus].cs | 26 +------- .../GridView/SeriesEntry[TStatus].cs | 59 ++++++------------- 3 files changed, 51 insertions(+), 67 deletions(-) diff --git a/Source/LibationUiBase/GridView/GridEntry[TStatus].cs b/Source/LibationUiBase/GridView/GridEntry[TStatus].cs index a57b52db..e00991cd 100644 --- a/Source/LibationUiBase/GridView/GridEntry[TStatus].cs +++ b/Source/LibationUiBase/GridView/GridEntry[TStatus].cs @@ -1,7 +1,6 @@ using ApplicationServices; using DataLayer; using Dinah.Core; -using Dinah.Core.Threading; using FileLiberator; using LibationFileManager; using System; @@ -9,7 +8,7 @@ using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; namespace LibationUiBase.GridView @@ -311,6 +310,36 @@ namespace LibationUiBase.GridView #endregion + + /// + /// Creates for all non-episode books in an enumeration of . + /// + /// Can be called from any thread, but requires the calling thread's to be valid. + public static async Task> GetAllProductsAsync(IEnumerable libraryBooks, Func includeIf, Func factory) + where TEntry : IGridEntry + { + var products = libraryBooks.Where(includeIf).ToArray(); + if (products.Length == 0) + return []; + + int parallelism = int.Max(1, Environment.ProcessorCount - 1); + + (int batchSize, int rem) = int.DivRem(products.Length, parallelism); + if (rem != 0) batchSize++; + + var syncContext = SynchronizationContext.Current; + + //Asynchronously create a GridEntry for every book in the library + var tasks = products.Chunk(batchSize).Select(batch => Task.Run(() => + { + SynchronizationContext.SetSynchronizationContext(syncContext); + return batch.Select(factory).OfType().ToArray(); + })); + + return (await Task.WhenAll(tasks)).SelectMany(a => a).ToList(); + } + + ~GridEntry() { PictureStorage.PictureCached -= PictureStorage_PictureCached; diff --git a/Source/LibationUiBase/GridView/LibraryBookEntry[TStatus].cs b/Source/LibationUiBase/GridView/LibraryBookEntry[TStatus].cs index 2d118e36..0dbff282 100644 --- a/Source/LibationUiBase/GridView/LibraryBookEntry[TStatus].cs +++ b/Source/LibationUiBase/GridView/LibraryBookEntry[TStatus].cs @@ -3,8 +3,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Threading.Tasks; -using System.Threading; -using System.Linq; namespace LibationUiBase.GridView { @@ -36,29 +34,9 @@ namespace LibationUiBase.GridView /// /// Creates for all non-episode books in an enumeration of . /// - /// Can be called from any thread, but requires the calling thread's to be valid. + /// Can be called from any thread, but requires the calling thread's to be valid. public static async Task> GetAllProductsAsync(IEnumerable libraryBooks) - { - var products = libraryBooks.Where(lb => lb.Book.IsProduct()).ToArray(); - if (products.Length == 0) - return []; - - int parallelism = int.Max(1, Environment.ProcessorCount - 1); - - (int batchSize, int rem) = int.DivRem(products.Length, parallelism); - if (rem != 0) batchSize++; - - var syncContext = SynchronizationContext.Current; - - //Asynchronously create an ILibraryBookEntry for every book in the library - var tasks = products.Chunk(batchSize).Select(batch => Task.Run(() => - { - SynchronizationContext.SetSynchronizationContext(syncContext); - return batch.Select(lb => new LibraryBookEntry(lb) as IGridEntry); - })); - - return (await Task.WhenAll(tasks)).SelectMany(a => a).ToList(); - } + => await GetAllProductsAsync(libraryBooks, lb => lb.Book.IsProduct(), lb => new LibraryBookEntry(lb) as IGridEntry); protected override string GetBookTags() => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated); } diff --git a/Source/LibationUiBase/GridView/SeriesEntry[TStatus].cs b/Source/LibationUiBase/GridView/SeriesEntry[TStatus].cs index 17eeff93..80627eac 100644 --- a/Source/LibationUiBase/GridView/SeriesEntry[TStatus].cs +++ b/Source/LibationUiBase/GridView/SeriesEntry[TStatus].cs @@ -1,4 +1,5 @@ using DataLayer; +using Dinah.Core.Collections.Generic; using System; using System.Collections.Generic; using System.Linq; @@ -62,56 +63,32 @@ namespace LibationUiBase.GridView /// Can be called from any thread, but requires the calling thread's to be valid. public static async Task> GetAllSeriesEntriesAsync(IEnumerable libraryBooks) { - var seriesBooks = libraryBooks.Where(lb => lb.Book.IsEpisodeParent()).ToArray(); - var allEpisodes = libraryBooks.Where(lb => lb.Book.IsEpisodeChild()).ToArray(); + var seriesEntries = await GetAllProductsAsync(libraryBooks, lb => lb.Book.IsEpisodeParent(), lb => new SeriesEntry(lb, []) as ISeriesEntry); + var seriesDict = seriesEntries.ToDictionarySafe(s => s.AudibleProductId); + await GetAllProductsAsync(libraryBooks, lb => lb.Book.IsEpisodeChild(), CreateAndLinkEpisodeEntry); - var seriesEntries = new ISeriesEntry[seriesBooks.Length]; - var seriesEpisodes = new ILibraryBookEntry[seriesBooks.Length][]; - - var syncContext = SynchronizationContext.Current; - var options = new ParallelOptions { MaxDegreeOfParallelism = int.Max(1, Environment.ProcessorCount - 1) }; - - //Asynchronously create an ILibraryBookEntry for every episode in the library - await Parallel.ForEachAsync(getAllEpisodes(), options, createEpisodeEntry); - - //Match all episode entries to their corresponding parents - for (int i = seriesEntries.Length - 1; i >= 0; i--) + //sort episodes by series order descending and update SeriesEntry + foreach (var series in seriesEntries) { - var series = seriesEntries[i]; - - //Sort episodes by series order descending, then add them to their parent's entry - Array.Sort(seriesEpisodes[i], (a, b) => -a.SeriesOrder.CompareTo(b.SeriesOrder)); - series.Children.AddRange(seriesEpisodes[i]); + series.Children.Sort((a, b) => -a.SeriesOrder.CompareTo(b.SeriesOrder)); series.UpdateLibraryBook(series.LibraryBook); } - return seriesEntries.Where(s => s.Children.Count != 0).Cast().ToList(); + return seriesEntries.Where(s => s.Children.Count != 0).ToList(); - //Create a LibraryBookEntry for a single episode - ValueTask createEpisodeEntry((int seriesIndex, int episodeIndex, LibraryBook episode) data, CancellationToken cancellationToken) + //Create a LibraryBookEntry for an episode and link it to its series parent + LibraryBookEntry CreateAndLinkEpisodeEntry(LibraryBook episode) { - SynchronizationContext.SetSynchronizationContext(syncContext); - var parent = seriesEntries[data.seriesIndex]; - seriesEpisodes[data.seriesIndex][data.episodeIndex] = new LibraryBookEntry(data.episode, parent); - return ValueTask.CompletedTask; - } - - //Enumeration all series episodes, along with the index to its seriesEntries entry - //and an index to its seriesEpisodes entry - IEnumerable<(int seriesIndex, int episodeIndex, LibraryBook episode)> getAllEpisodes() - { - for (int i = 0; i < seriesBooks.Length; i++) + foreach (var s in episode.Book.SeriesLink) { - var series = seriesBooks[i]; - var childEpisodes = allEpisodes.FindChildren(series); - - SynchronizationContext.SetSynchronizationContext(syncContext); - seriesEntries[i] = new SeriesEntry(series, []); - seriesEpisodes[i] = new ILibraryBookEntry[childEpisodes.Count]; - - for (int j = 0; j < childEpisodes.Count; j++) - yield return (i, j, childEpisodes[j]); + if (seriesDict.TryGetValue(s.Series.AudibleSeriesId, out var seriesParent)) + { + var entry = new LibraryBookEntry(episode, seriesParent); + seriesParent.Children.Add(entry); + return entry; + } } + return null; } }