Merge pull request #1160 from Mbucari/master

Fixed stack overflow crash when movifying large databases
This commit is contained in:
rmcrackan 2025-02-27 21:41:33 -05:00 committed by GitHub
commit 5f5c9f65ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 169 additions and 148 deletions

View File

@ -51,13 +51,8 @@ jobs:
with: with:
name: Libation ${{ needs.prerelease.outputs.version }} name: Libation ${{ needs.prerelease.outputs.version }}
body: <Put a body here> body: <Put a body here>
token: ${{ secrets.GITHUB_TOKEN }}
draft: true draft: true
prerelease: false prerelease: false
files: |
- name: Upload release assets artifacts/*/*
uses: dwenegar/upload-release-assets@v2
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
with:
release_id: "${{ steps.release.outputs.id }}"
assets_path: ./artifacts

View File

@ -279,8 +279,11 @@ namespace AppScaffolding
private static void wireUpSystemEvents(Configuration configuration) private static void wireUpSystemEvents(Configuration configuration)
{ {
LibraryCommands.LibrarySizeChanged += (_, __) => SearchEngineCommands.FullReIndex(); LibraryCommands.LibrarySizeChanged += (object _, List<DataLayer.LibraryBook> libraryBooks)
LibraryCommands.BookUserDefinedItemCommitted += (_, books) => SearchEngineCommands.UpdateBooks(books); => SearchEngineCommands.FullReIndex(libraryBooks);
LibraryCommands.BookUserDefinedItemCommitted += (_, books)
=> SearchEngineCommands.UpdateBooks(books);
} }
public static UpgradeProperties GetLatestRelease() public static UpgradeProperties GetLatestRelease()

View File

@ -222,7 +222,7 @@ namespace ApplicationServices
{ {
int qtyChanged = await Task.Run(() => SaveContext(context)); int qtyChanged = await Task.Run(() => SaveContext(context));
if (qtyChanged > 0) if (qtyChanged > 0)
await Task.Run(finalizeLibrarySizeChange); await Task.Run(() => finalizeLibrarySizeChange(context));
return qtyChanged; return qtyChanged;
} }
catch (Exception ex) catch (Exception ex)
@ -329,7 +329,7 @@ namespace ApplicationServices
// this is any changes at all to the database, not just new books // this is any changes at all to the database, not just new books
if (qtyChanges > 0) if (qtyChanges > 0)
await Task.Run(() => finalizeLibrarySizeChange()); await Task.Run(() => finalizeLibrarySizeChange(context));
logTime("importIntoDbAsync -- post finalizeLibrarySizeChange"); logTime("importIntoDbAsync -- post finalizeLibrarySizeChange");
return newCount; return newCount;
@ -369,16 +369,16 @@ namespace ApplicationServices
using var context = DbContexts.GetContext(); using var context = DbContexts.GetContext();
// Attach() NoTracking entities before SaveChanges() // Entry() NoTracking entities before SaveChanges()
foreach (var lb in removeLibraryBooks) foreach (var lb in removeLibraryBooks)
{ {
lb.IsDeleted = true; lb.IsDeleted = true;
context.Attach(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified; context.Entry(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
} }
var qtyChanges = context.SaveChanges(); var qtyChanges = context.SaveChanges();
if (qtyChanges > 0) if (qtyChanges > 0)
finalizeLibrarySizeChange(); finalizeLibrarySizeChange(context);
return qtyChanges; return qtyChanges;
} }
@ -398,16 +398,16 @@ namespace ApplicationServices
using var context = DbContexts.GetContext(); using var context = DbContexts.GetContext();
// Attach() NoTracking entities before SaveChanges() // Entry() NoTracking entities before SaveChanges()
foreach (var lb in libraryBooks) foreach (var lb in libraryBooks)
{ {
lb.IsDeleted = false; lb.IsDeleted = false;
context.Attach(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified; context.Entry(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
} }
var qtyChanges = context.SaveChanges(); var qtyChanges = context.SaveChanges();
if (qtyChanges > 0) if (qtyChanges > 0)
finalizeLibrarySizeChange(); finalizeLibrarySizeChange(context);
return qtyChanges; return qtyChanges;
} }
@ -432,7 +432,7 @@ namespace ApplicationServices
var qtyChanges = context.SaveChanges(); var qtyChanges = context.SaveChanges();
if (qtyChanges > 0) if (qtyChanges > 0)
finalizeLibrarySizeChange(); finalizeLibrarySizeChange(context);
return qtyChanges; return qtyChanges;
} }
@ -445,10 +445,14 @@ namespace ApplicationServices
#endregion #endregion
// call this whenever books are added or removed from library // call this whenever books are added or removed from library
private static void finalizeLibrarySizeChange() => LibrarySizeChanged?.Invoke(null, null); private static void finalizeLibrarySizeChange(LibationContext context)
{
var library = context.GetLibrary_Flat_NoTracking(includeParents: true);
LibrarySizeChanged?.Invoke(null, library);
}
/// <summary>Occurs when the size of the library changes. ie: books are added or removed</summary> /// <summary>Occurs when the size of the library changes. ie: books are added or removed</summary>
public static event EventHandler LibrarySizeChanged; public static event EventHandler<List<LibraryBook>> LibrarySizeChanged;
/// <summary> /// <summary>
/// Occurs when the size of the library does not change but book(s) details do. Especially when <see cref="UserDefinedItem.Tags"/>, <see cref="UserDefinedItem.BookStatus"/>, or <see cref="UserDefinedItem.PdfStatus"/> changed values are successfully persisted. /// Occurs when the size of the library does not change but book(s) details do. Especially when <see cref="UserDefinedItem.Tags"/>, <see cref="UserDefinedItem.BookStatus"/>, or <see cref="UserDefinedItem.PdfStatus"/> changed values are successfully persisted.
@ -518,16 +522,17 @@ namespace ApplicationServices
if (libraryBooks is null || !libraryBooks.Any()) if (libraryBooks is null || !libraryBooks.Any())
return 0; return 0;
foreach (var book in libraryBooks)
action?.Invoke(book.Book.UserDefinedItem);
using var context = DbContexts.GetContext(); using var context = DbContexts.GetContext();
// Attach() NoTracking entities before SaveChanges() // Entry() instead of Attach() due to possible stack overflow with large tables
foreach (var book in libraryBooks) foreach (var book in libraryBooks)
{ {
context.Attach(book.Book.UserDefinedItem).State = Microsoft.EntityFrameworkCore.EntityState.Modified; action?.Invoke(book.Book.UserDefinedItem);
context.Attach(book.Book.UserDefinedItem.Rating).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
var udiEntity = context.Entry(book.Book.UserDefinedItem);
udiEntity.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
udiEntity.Reference(udi => udi.Rating).TargetEntry.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
} }
var qtyChanges = context.SaveChanges(); var qtyChanges = context.SaveChanges();
@ -599,6 +604,7 @@ namespace ApplicationServices
var results = libraryBooks var results = libraryBooks
.AsParallel() .AsParallel()
.WithoutParents()
.Select(lb => new { absent = lb.AbsentFromLastScan, status = Liberated_Status(lb.Book) }) .Select(lb => new { absent = lb.AbsentFromLastScan, status = Liberated_Status(lb.Book) })
.ToList(); .ToList();

View File

@ -48,6 +48,8 @@ namespace ApplicationServices
} }
public static void FullReIndex() => performSafeCommand(fullReIndex); public static void FullReIndex() => performSafeCommand(fullReIndex);
public static void FullReIndex(List<LibraryBook> libraryBooks)
=> performSafeCommand(se => fullReIndex(se, libraryBooks.WithoutParents()));
internal static void UpdateUserDefinedItems(LibraryBook book) => performSafeCommand(e => internal static void UpdateUserDefinedItems(LibraryBook book) => performSafeCommand(e =>
{ {
@ -94,8 +96,11 @@ namespace ApplicationServices
private static void fullReIndex(SearchEngine engine) private static void fullReIndex(SearchEngine engine)
{ {
var library = DbContexts.GetLibrary_Flat_NoTracking(); var library = DbContexts.GetLibrary_Flat_NoTracking();
engine.CreateNewIndex(library); fullReIndex(engine, library);
} }
private static void fullReIndex(SearchEngine engine, IEnumerable<LibraryBook> libraryBooks)
=> engine.CreateNewIndex(libraryBooks);
#endregion #endregion
} }
} }

View File

@ -6,6 +6,7 @@ namespace DataLayer
public class LibationContextFactory : DesignTimeDbContextFactoryBase<LibationContext> public class LibationContextFactory : DesignTimeDbContextFactoryBase<LibationContext>
{ {
protected override LibationContext CreateNewInstance(DbContextOptions<LibationContext> options) => new LibationContext(options); protected override LibationContext CreateNewInstance(DbContextOptions<LibationContext> options) => new LibationContext(options);
protected override void UseDatabaseEngine(DbContextOptionsBuilder optionsBuilder, string connectionString) => optionsBuilder.UseSqlite(connectionString); protected override void UseDatabaseEngine(DbContextOptionsBuilder optionsBuilder, string connectionString)
=> optionsBuilder.UseSqlite(connectionString, ob => ob.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
} }
} }

View File

@ -44,6 +44,10 @@ namespace DataLayer
public static bool IsEpisodeParent(this Book book) public static bool IsEpisodeParent(this Book book)
=> book.ContentType is ContentType.Parent; => book.ContentType is ContentType.Parent;
public static IEnumerable<LibraryBook> WithoutParents(this IEnumerable<LibraryBook> libraryBooks)
=> libraryBooks.Where(lb => !lb.Book.IsEpisodeParent());
public static bool HasLiberated(this Book book) public static bool HasLiberated(this Book book)
=> book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated || => book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated ||
book.UserDefinedItem.PdfStatus is not null and LiberatedStatus.Liberated; book.UserDefinedItem.PdfStatus is not null and LiberatedStatus.Liberated;

View File

@ -21,7 +21,7 @@ namespace DataLayer
.AsNoTrackingWithIdentityResolution() .AsNoTrackingWithIdentityResolution()
.GetLibrary() .GetLibrary()
.AsEnumerable() .AsEnumerable()
.Where(lb => !lb.Book.IsEpisodeParent() || includeParents) .Where(c => !c.Book.IsEpisodeParent() || includeParents)
.ToList(); .ToList();
public static LibraryBook GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId) public static LibraryBook GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId)
@ -91,7 +91,7 @@ namespace DataLayer
} }
#nullable disable #nullable disable
public static IEnumerable<LibraryBook> FindChildren(this IEnumerable<LibraryBook> bookList, LibraryBook parent) public static List<LibraryBook> FindChildren(this IEnumerable<LibraryBook> bookList, LibraryBook parent)
=> bookList => bookList
.Where( .Where(
lb => lb =>

View File

@ -1,8 +1,6 @@
using ApplicationServices; using ApplicationServices;
using Avalonia; using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Input;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
@ -14,14 +12,13 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using ReactiveUI; using Avalonia.Threading;
using DataLayer;
namespace LibationAvalonia namespace LibationAvalonia
{ {
public class App : Application public class App : Application
{ {
public static Window MainWindow { get; private set; } public static MainWindow MainWindow { get; private set; }
public static IBrush ProcessQueueBookFailedBrush { get; private set; } public static IBrush ProcessQueueBookFailedBrush { get; private set; }
public static IBrush ProcessQueueBookCompletedBrush { get; private set; } public static IBrush ProcessQueueBookCompletedBrush { get; private set; }
public static IBrush ProcessQueueBookCancelledBrush { get; private set; } public static IBrush ProcessQueueBookCancelledBrush { get; private set; }
@ -216,11 +213,17 @@ namespace LibationAvalonia
LoadStyles(); LoadStyles();
var mainWindow = new MainWindow(); var mainWindow = new MainWindow();
desktop.MainWindow = MainWindow = mainWindow; desktop.MainWindow = MainWindow = mainWindow;
mainWindow.OnLibraryLoaded(LibraryTask.GetAwaiter().GetResult()); mainWindow.Loaded += MainWindow_Loaded;
mainWindow.RestoreSizeAndLocation(Configuration.Instance); mainWindow.RestoreSizeAndLocation(Configuration.Instance);
mainWindow.Show(); mainWindow.Show();
} }
private static async void MainWindow_Loaded(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var library = await LibraryTask;
await Dispatcher.UIThread.InvokeAsync(() => MainWindow.OnLibraryLoadedAsync(library));
}
private static void LoadStyles() private static void LoadStyles()
{ {
ProcessQueueBookFailedBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookFailedBrush)); ProcessQueueBookFailedBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookFailedBrush));

View File

@ -44,16 +44,18 @@ namespace LibationAvalonia.ViewModels
private void Configure_BackupCounts() private void Configure_BackupCounts()
{ {
MainWindow.LibraryLoaded += (_, e) => setBackupCounts(e.Where(l => !l.Book.IsEpisodeParent())); LibraryCommands.LibrarySizeChanged += async (object _, List<LibraryBook> libraryBooks)
LibraryCommands.LibrarySizeChanged += (_,_) => setBackupCounts(); => await SetBackupCountsAsync(libraryBooks);
LibraryCommands.BookUserDefinedItemCommitted += (_, _) => setBackupCounts();
//Pass null to the setup count to get the whole library.
LibraryCommands.BookUserDefinedItemCommitted += async (_, _)
=> await SetBackupCountsAsync(null);
} }
private async void setBackupCounts(IEnumerable<LibraryBook> libraryBooks = null) public async Task SetBackupCountsAsync(IEnumerable<LibraryBook> libraryBooks)
{ {
if (updateCountsTask?.IsCompleted ?? true) if (updateCountsTask?.IsCompleted ?? true)
{ {
libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking();
updateCountsTask = Task.Run(() => LibraryCommands.GetCounts(libraryBooks)); updateCountsTask = Task.Run(() => LibraryCommands.GetCounts(libraryBooks));
var stats = await updateCountsTask; var stats = await updateCountsTask;
await Dispatcher.UIThread.InvokeAsync(() => LibraryStats = stats); await Dispatcher.UIThread.InvokeAsync(() => LibraryStats = stats);

View File

@ -1,7 +1,9 @@
using ApplicationServices; using ApplicationServices;
using DataLayer;
using LibationAvalonia.Views; using LibationAvalonia.Views;
using LibationFileManager; using LibationFileManager;
using ReactiveUI; using ReactiveUI;
using System.Collections.Generic;
namespace LibationAvalonia.ViewModels namespace LibationAvalonia.ViewModels
{ {
@ -20,7 +22,7 @@ namespace LibationAvalonia.ViewModels
MainWindow = mainWindow; MainWindow = mainWindow;
ProductsDisplay.RemovableCountChanged += (_, removeCount) => RemoveBooksButtonText = removeCount == 1 ? "Remove 1 Book from Libation" : $"Remove {removeCount} Books from Libation"; ProductsDisplay.RemovableCountChanged += (_, removeCount) => RemoveBooksButtonText = removeCount == 1 ? "Remove 1 Book from Libation" : $"Remove {removeCount} Books from Libation";
LibraryCommands.LibrarySizeChanged += async (_, _) => await ProductsDisplay.UpdateGridAsync(DbContexts.GetLibrary_Flat_NoTracking(includeParents: true)); LibraryCommands.LibrarySizeChanged += LibraryCommands_LibrarySizeChanged;
Configure_NonUI(); Configure_NonUI();
Configure_BackupCounts(); Configure_BackupCounts();
@ -34,6 +36,11 @@ namespace LibationAvalonia.ViewModels
Configure_VisibleBooks(); Configure_VisibleBooks();
} }
private async void LibraryCommands_LibrarySizeChanged(object sender, List<LibraryBook> fullLibrary)
{
await ProductsDisplay.UpdateGridAsync(fullLibrary);
}
private static string menufyText(string header) => Configuration.IsMacOs ? header : $"_{header}"; private static string menufyText(string header) => Configuration.IsMacOs ? header : $"_{header}";
} }
} }

View File

@ -28,7 +28,13 @@ namespace LibationAvalonia.ViewModels
/// <summary>Grid entries included in the filter set. If null, all grid entries are shown</summary> /// <summary>Grid entries included in the filter set. If null, all grid entries are shown</summary>
private HashSet<IGridEntry> FilteredInGridEntries; private HashSet<IGridEntry> FilteredInGridEntries;
public string FilterString { get; private set; } public string FilterString { get; private set; }
public DataGridCollectionView GridEntries { get; private set; }
private DataGridCollectionView _gridEntries;
public DataGridCollectionView GridEntries
{
get => _gridEntries;
private set => this.RaiseAndSetIfChanged(ref _gridEntries, value);
}
private bool _removeColumnVisible; private bool _removeColumnVisible;
public bool RemoveColumnVisible { get => _removeColumnVisible; private set => this.RaiseAndSetIfChanged(ref _removeColumnVisible, value); } public bool RemoveColumnVisible { get => _removeColumnVisible; private set => this.RaiseAndSetIfChanged(ref _removeColumnVisible, value); }

View File

@ -13,7 +13,6 @@ namespace LibationAvalonia.Views
{ {
public partial class MainWindow : ReactiveWindow<MainVM> public partial class MainWindow : ReactiveWindow<MainVM>
{ {
public event EventHandler<List<LibraryBook>> LibraryLoaded;
public MainWindow() public MainWindow()
{ {
DataContext = new MainVM(this); DataContext = new MainVM(this);
@ -23,7 +22,6 @@ namespace LibationAvalonia.Views
Opened += MainWindow_Opened; Opened += MainWindow_Opened;
Closing += MainWindow_Closing; Closing += MainWindow_Closing;
LibraryLoaded += MainWindow_LibraryLoaded;
KeyBindings.Add(new KeyBinding { Command = ReactiveCommand.Create(selectAndFocusSearchBox), Gesture = new KeyGesture(Key.F, Configuration.IsMacOs ? KeyModifiers.Meta : KeyModifiers.Control) }); KeyBindings.Add(new KeyBinding { Command = ReactiveCommand.Create(selectAndFocusSearchBox), Gesture = new KeyGesture(Key.F, Configuration.IsMacOs ? KeyModifiers.Meta : KeyModifiers.Control) });
@ -56,21 +54,21 @@ namespace LibationAvalonia.Views
this.SaveSizeAndLocation(Configuration.Instance); this.SaveSizeAndLocation(Configuration.Instance);
} }
private async void MainWindow_LibraryLoaded(object sender, List<LibraryBook> dbBooks)
{
if (QuickFilters.UseDefault)
await ViewModel.PerformFilter(QuickFilters.Filters.FirstOrDefault());
await ViewModel.ProductsDisplay.BindToGridAsync(dbBooks);
}
private void selectAndFocusSearchBox() private void selectAndFocusSearchBox()
{ {
filterSearchTb.SelectAll(); filterSearchTb.SelectAll();
filterSearchTb.Focus(); filterSearchTb.Focus();
} }
public void OnLibraryLoaded(List<LibraryBook> initialLibrary) => LibraryLoaded?.Invoke(this, initialLibrary); public async System.Threading.Tasks.Task OnLibraryLoadedAsync(List<LibraryBook> initialLibrary)
{
if (QuickFilters.UseDefault)
await ViewModel.PerformFilter(QuickFilters.Filters.FirstOrDefault());
await ViewModel.SetBackupCountsAsync(initialLibrary);
await ViewModel.ProductsDisplay.BindToGridAsync(initialLibrary);
}
public void ProductsDisplay_LiberateClicked(object _, LibraryBook libraryBook) => ViewModel.LiberateClicked(libraryBook); public void ProductsDisplay_LiberateClicked(object _, LibraryBook libraryBook) => ViewModel.LiberateClicked(libraryBook);
public void ProductsDisplay_LiberateSeriesClicked(object _, ISeriesEntry series) => ViewModel.LiberateSeriesClicked(series); public void ProductsDisplay_LiberateSeriesClicked(object _, ISeriesEntry series) => ViewModel.LiberateSeriesClicked(series);
public void ProductsDisplay_ConvertToMp3Clicked(object _, LibraryBook libraryBook) => ViewModel.ConvertToMp3Clicked(libraryBook); public void ProductsDisplay_ConvertToMp3Clicked(object _, LibraryBook libraryBook) => ViewModel.ConvertToMp3Clicked(libraryBook);

View File

@ -24,14 +24,14 @@ namespace LibationFileManager
// load json into memory. if file doesn't exist, nothing to do. save() will create if needed // load json into memory. if file doesn't exist, nothing to do. save() will create if needed
public static FilterState InMemoryState { get; set; } = null!; public static FilterState? InMemoryState { get; set; }
public static bool UseDefault public static bool UseDefault
{ {
get => InMemoryState.UseDefault; get => InMemoryState?.UseDefault ?? false;
set set
{ {
if (UseDefault == value) if (InMemoryState is null || UseDefault == value)
return; return;
lock (locker) lock (locker)
@ -52,7 +52,8 @@ namespace LibationFileManager
public string? Name { get; set; } = Name; public string? Name { get; set; } = Name;
} }
public static IEnumerable<NamedFilter> Filters => InMemoryState.Filters.AsReadOnly(); public static IEnumerable<NamedFilter> Filters
=> InMemoryState?.Filters.AsReadOnly() ?? Enumerable.Empty<NamedFilter>();
public static void Add(NamedFilter namedFilter) public static void Add(NamedFilter namedFilter)
{ {
@ -66,6 +67,7 @@ namespace LibationFileManager
lock (locker) lock (locker)
{ {
InMemoryState ??= new();
// check for duplicates // check for duplicates
if (InMemoryState.Filters.Select(x => x.Filter).ContainsInsensative(namedFilter.Filter)) if (InMemoryState.Filters.Select(x => x.Filter).ContainsInsensative(namedFilter.Filter))
return; return;
@ -79,6 +81,8 @@ namespace LibationFileManager
{ {
lock (locker) lock (locker)
{ {
if (InMemoryState is null)
return;
InMemoryState.Filters.Remove(filter); InMemoryState.Filters.Remove(filter);
save(); save();
} }
@ -88,8 +92,7 @@ namespace LibationFileManager
{ {
lock (locker) lock (locker)
{ {
var index = InMemoryState.Filters.IndexOf(oldFilter); if (InMemoryState is null || InMemoryState.Filters.IndexOf(oldFilter) < 0)
if (index < 0)
return; return;
InMemoryState.Filters = InMemoryState.Filters.Select(f => f == oldFilter ? newFilter : f).ToList(); InMemoryState.Filters = InMemoryState.Filters.Select(f => f == oldFilter ? newFilter : f).ToList();
@ -107,6 +110,7 @@ namespace LibationFileManager
filter.Filter = filter.Filter.Trim(); filter.Filter = filter.Filter.Trim();
lock (locker) lock (locker)
{ {
InMemoryState ??= new();
InMemoryState.Filters = new List<NamedFilter>(filters); InMemoryState.Filters = new List<NamedFilter>(filters);
save(); save();
} }

View File

@ -33,37 +33,25 @@ namespace LibationUiBase.GridView
LoadCover(); LoadCover();
} }
public static async Task<List<IGridEntry>> GetAllProductsAsync(IEnumerable<LibraryBook> libraryBooks) public static async Task<List<IGridEntry>> GetAllProductsAsync(IEnumerable<LibraryBook> libraryBooks)
{ {
var products = libraryBooks.Where(lb => lb.Book.IsProduct()).ToArray(); var products = libraryBooks.Where(lb => lb.Book.IsProduct()).ToArray();
if (products.Length == 0)
return [];
int parallelism = int.Max(1, Environment.ProcessorCount - 1); int parallelism = int.Max(1, Environment.ProcessorCount - 1);
(int numPer, int rem) = int.DivRem(products.Length, parallelism); (int batchSize, int rem) = int.DivRem(products.Length, parallelism);
if (rem != 0) numPer++; if (rem != 0) batchSize++;
var tasks = new Task<IGridEntry[]>[parallelism];
var syncContext = SynchronizationContext.Current; var syncContext = SynchronizationContext.Current;
for (int i = 0; i < parallelism; i++) //Asynchronously create an ILibraryBookEntry for every book in the library
{ var tasks = products.Chunk(batchSize).Select(batch => Task.Run(() =>
int start = i * numPer;
tasks[i] = Task.Run(() =>
{ {
SynchronizationContext.SetSynchronizationContext(syncContext); SynchronizationContext.SetSynchronizationContext(syncContext);
return batch.Select(lb => new LibraryBookEntry<TStatus>(lb) as IGridEntry);
int length = int.Min(numPer, products.Length - start); }));
if (length < 1) return Array.Empty<IGridEntry>();
var result = new IGridEntry[length];
for (int j = 0; j < length; j++)
result[j] = new LibraryBookEntry<TStatus>(products[start + j]);
return result;
});
}
return (await Task.WhenAll(tasks)).SelectMany(a => a).ToList(); return (await Task.WhenAll(tasks)).SelectMany(a => a).ToList();
} }

View File

@ -1,6 +1,5 @@
using DataLayer; using DataLayer;
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@ -62,53 +61,54 @@ namespace LibationUiBase.GridView
var seriesBooks = libraryBooks.Where(lb => lb.Book.IsEpisodeParent()).ToArray(); var seriesBooks = libraryBooks.Where(lb => lb.Book.IsEpisodeParent()).ToArray();
var allEpisodes = libraryBooks.Where(lb => lb.Book.IsEpisodeChild()).ToArray(); var allEpisodes = libraryBooks.Where(lb => lb.Book.IsEpisodeChild()).ToArray();
int parallelism = int.Max(1, Environment.ProcessorCount - 1);
var tasks = new Task[parallelism];
var syncContext = SynchronizationContext.Current;
var q = new BlockingCollection<(int, LibraryBook episode)>();
var seriesEntries = new ISeriesEntry[seriesBooks.Length]; var seriesEntries = new ISeriesEntry[seriesBooks.Length];
var seriesEpisodes = new ConcurrentBag<ILibraryBookEntry>[seriesBooks.Length]; var seriesEpisodes = new ILibraryBookEntry[seriesBooks.Length][];
for (int i = 0; i < parallelism; i++) var syncContext = SynchronizationContext.Current;
{ var options = new ParallelOptions { MaxDegreeOfParallelism = int.Max(1, Environment.ProcessorCount - 1) };
tasks[i] = Task.Run(() =>
{
SynchronizationContext.SetSynchronizationContext(syncContext);
while (q.TryTake(out var entry, -1)) //Asynchronously create an ILibraryBookEntry for every episode in the library
{ await Parallel.ForEachAsync(getAllEpisodes(), options, createEpisodeEntry);
var parent = seriesEntries[entry.Item1];
var episodeBag = seriesEpisodes[entry.Item1];
episodeBag.Add(new LibraryBookEntry<TStatus>(entry.episode, parent));
}
});
}
for (int i = 0; i <seriesBooks.Length; i++) //Match all episode entries to their corresponding parents
{ for (int i = seriesEntries.Length - 1; i >= 0; i--)
var series = seriesBooks[i];
seriesEntries[i] = new SeriesEntry<TStatus>(series, Enumerable.Empty<LibraryBook>());
seriesEpisodes[i] = new ConcurrentBag<ILibraryBookEntry>();
foreach (var ep in allEpisodes.FindChildren(series))
q.Add((i, ep));
}
q.CompleteAdding();
await Task.WhenAll(tasks);
for (int i = 0; i < seriesBooks.Length; i++)
{ {
var series = seriesEntries[i]; var series = seriesEntries[i];
series.Children.AddRange(seriesEpisodes[i].OrderByDescending(c => c.SeriesOrder));
//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.UpdateLibraryBook(series.LibraryBook); series.UpdateLibraryBook(series.LibraryBook);
} }
return seriesEntries.Where(s => s.Children.Count != 0).ToList(); return seriesEntries.Where(s => s.Children.Count != 0).Cast<ISeriesEntry>().ToList();
//Create a LibraryBookEntry for a single episode
ValueTask createEpisodeEntry((int seriesIndex, int episodeIndex, LibraryBook episode) data, CancellationToken cancellationToken)
{
SynchronizationContext.SetSynchronizationContext(syncContext);
var parent = seriesEntries[data.seriesIndex];
seriesEpisodes[data.seriesIndex][data.episodeIndex] = new LibraryBookEntry<TStatus>(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++)
{
var series = seriesBooks[i];
var childEpisodes = allEpisodes.FindChildren(series);
SynchronizationContext.SetSynchronizationContext(syncContext);
seriesEntries[i] = new SeriesEntry<TStatus>(series, []);
seriesEpisodes[i] = new ILibraryBookEntry[childEpisodes.Count];
for (int j = 0; j < childEpisodes.Count; j++)
yield return (i, j, childEpisodes[j]);
}
}
} }
public void RemoveChild(ILibraryBookEntry lbe) public void RemoveChild(ILibraryBookEntry lbe)

View File

@ -17,7 +17,9 @@ namespace LibationWinForms
beginPdfBackupsToolStripMenuItem.Format(0); beginPdfBackupsToolStripMenuItem.Format(0);
LibraryCommands.LibrarySizeChanged += setBackupCounts; LibraryCommands.LibrarySizeChanged += setBackupCounts;
LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts; //Pass null to the runner to get the whole library.
LibraryCommands.BookUserDefinedItemCommitted += (_, _)
=> setBackupCounts(null, null);
updateCountsBw.DoWork += UpdateCountsBw_DoWork; updateCountsBw.DoWork += UpdateCountsBw_DoWork;
updateCountsBw.RunWorkerCompleted += exportMenuEnable; updateCountsBw.RunWorkerCompleted += exportMenuEnable;
@ -28,12 +30,12 @@ namespace LibationWinForms
private bool runBackupCountsAgain; private bool runBackupCountsAgain;
private void setBackupCounts(object _, object __) private void setBackupCounts(object _, List<LibraryBook> libraryBooks)
{ {
runBackupCountsAgain = true; runBackupCountsAgain = true;
if (!updateCountsBw.IsBusy) if (!updateCountsBw.IsBusy)
updateCountsBw.RunWorkerAsync(); updateCountsBw.RunWorkerAsync(libraryBooks);
} }
private void UpdateCountsBw_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e) private void UpdateCountsBw_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
@ -41,11 +43,7 @@ namespace LibationWinForms
while (runBackupCountsAgain) while (runBackupCountsAgain)
{ {
runBackupCountsAgain = false; runBackupCountsAgain = false;
e.Result = LibraryCommands.GetCounts(e.Argument as IEnumerable<LibraryBook>);
if (e.Argument is not IEnumerable<LibraryBook> lbs)
lbs = DbContexts.GetLibrary_Flat_NoTracking();
e.Result = LibraryCommands.GetCounts(lbs);
} }
} }

View File

@ -52,7 +52,8 @@ namespace LibationWinForms
// Configure_Grid(); // since it's just this, can keep here. If it needs more, then give grid it's own 'partial class Form1' // Configure_Grid(); // since it's just this, can keep here. If it needs more, then give grid it's own 'partial class Form1'
{ {
LibraryCommands.LibrarySizeChanged += (_, __) => Invoke(() => productsDisplay.DisplayAsync()); LibraryCommands.LibrarySizeChanged += (object _, List<LibraryBook> fullLibrary)
=> Invoke(() => productsDisplay.DisplayAsync(fullLibrary));
} }
Shown += Form1_Shown; Shown += Form1_Shown;
} }
@ -75,7 +76,7 @@ namespace LibationWinForms
public async Task InitLibraryAsync(List<LibraryBook> libraryBooks) public async Task InitLibraryAsync(List<LibraryBook> libraryBooks)
{ {
runBackupCountsAgain = true; runBackupCountsAgain = true;
updateCountsBw.RunWorkerAsync(libraryBooks.Where(b => !b.Book.IsEpisodeParent())); updateCountsBw.RunWorkerAsync(libraryBooks);
await productsDisplay.DisplayAsync(libraryBooks); await productsDisplay.DisplayAsync(libraryBooks);
} }