diff --git a/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.axaml.cs b/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.axaml.cs index af644b21..e0842120 100644 --- a/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.axaml.cs +++ b/Source/LibationWinForms/AvaloniaUI/Views/MainWindow/MainWindow.axaml.cs @@ -8,11 +8,16 @@ using LibationWinForms.AvaloniaUI.Views.ProductsGrid; using Avalonia.ReactiveUI; using LibationWinForms.AvaloniaUI.ViewModels; using LibationFileManager; +using DataLayer; +using System.Collections.Generic; namespace LibationWinForms.AvaloniaUI.Views { public partial class MainWindow : ReactiveWindow { + public event EventHandler Load; + public event EventHandler> LibraryLoaded; + public MainWindow() { InitializeComponent(); @@ -37,13 +42,9 @@ namespace LibationWinForms.AvaloniaUI.Views // misc which belongs in winforms app but doesn't have a UI element Configure_NonUI(); - async void DoDisplay(object _, EventArgs __) { - await productsDisplay.Display(); - } - { - this.Load += DoDisplay; - LibraryCommands.LibrarySizeChanged += DoDisplay; + this.LibraryLoaded += (_, dbBooks) => productsDisplay.Display(dbBooks); + LibraryCommands.LibrarySizeChanged += (_, _) => productsDisplay.Display(DbContexts.GetLibrary_Flat_NoTracking(includeParents: true)); this.Closing += (_,_) => this.SaveSizeAndLocation(Configuration.Instance); } } @@ -53,9 +54,8 @@ namespace LibationWinForms.AvaloniaUI.Views AvaloniaXamlLoader.Load(this); } - public event EventHandler Load; - public void OnLoad() => Load?.Invoke(this, EventArgs.Empty); + public void OnLibraryLoaded(List initialLibrary) => LibraryLoaded?.Invoke(this, initialLibrary); private void FindAllControls() { diff --git a/Source/LibationWinForms/AvaloniaUI/Views/ProductsGrid/ProductsDisplay2.Display.xaml.cs b/Source/LibationWinForms/AvaloniaUI/Views/ProductsGrid/ProductsDisplay2.Display.xaml.cs index 824f26b6..001c21bf 100644 --- a/Source/LibationWinForms/AvaloniaUI/Views/ProductsGrid/ProductsDisplay2.Display.xaml.cs +++ b/Source/LibationWinForms/AvaloniaUI/Views/ProductsGrid/ProductsDisplay2.Display.xaml.cs @@ -1,12 +1,11 @@ -using ApplicationServices; -using Avalonia.Controls; +using Avalonia.Controls; +using DataLayer; using LibationWinForms.AvaloniaUI.ViewModels; using System; using System.Collections; +using System.Collections.Generic; using System.Linq; using System.Reflection; -using Avalonia.Threading; -using System.Threading.Tasks; namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid { @@ -14,19 +13,17 @@ namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid { private void Configure_Display() { } - public async Task Display() + public void Display(List dbBooks) { try - { - var dbBooks = DbContexts.GetLibrary_Flat_NoTracking(includeParents: true); - + { if (_viewModel is null) { _viewModel = new ProductsDisplayViewModel(dbBooks); - await Dispatcher.UIThread.InvokeAsync(() => InitialLoaded?.Invoke(this, EventArgs.Empty)); + InitialLoaded?.Invoke(this, EventArgs.Empty); int bookEntryCount = bindingList.BookEntries().Count(); - await Dispatcher.UIThread.InvokeAsync(() => VisibleCountChanged?.Invoke(this, bookEntryCount)); + VisibleCountChanged?.Invoke(this, bookEntryCount); //Avalonia displays items in the DataConncetion from an internal copy of //the bound list, not the actual bound list. So we need to reflect to get @@ -53,12 +50,9 @@ namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid //List is already displayed. Replace all items with new ones, refilter, and re-sort string existingFilter = _viewModel?.GridEntries?.Filter; var newEntries = ProductsDisplayViewModel.CreateGridEntries(dbBooks); - await Dispatcher.UIThread.InvokeAsync(() => - { - bindingList.ReplaceList(newEntries); - bindingList.Filter = existingFilter; - ReSort(); - }); + bindingList.ReplaceList(newEntries); + bindingList.Filter = existingFilter; + ReSort(); } } catch (Exception ex) diff --git a/Source/LibationWinForms/Program.cs b/Source/LibationWinForms/Program.cs index 42441865..627c1f63 100644 --- a/Source/LibationWinForms/Program.cs +++ b/Source/LibationWinForms/Program.cs @@ -1,16 +1,14 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Windows.Forms; +using ApplicationServices; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.ReactiveUI; -using Dinah.Core; using LibationFileManager; using LibationWinForms.Dialogs; -using Serilog; namespace LibationWinForms { @@ -23,33 +21,70 @@ namespace LibationWinForms [STAThread] static async Task Main() { - //Start as much work in parallel as possible. - var startupTask = Task.Run(RunStartupStuff); - var appBuilderTask = Task.Run(BuildAvaloniaApp); - var classicLifetimeTask = Task.Run(() => new ClassicDesktopStyleApplicationLifetime()); + var config = LoadLibationConfig(); - List tasks = new() { startupTask, appBuilderTask, classicLifetimeTask }; + if (config is null) return; - while ((await Task.WhenAny(tasks)) is Task t && t != startupTask) - tasks.Remove(t); + /* + Results below compare startup times when parallelizing startup tasks vs when + running everything sequentially, from the entry point until after the call to + OnLoadedLibrary() returns. Tests were run on a ReadyToRun enables release build. - //When RunStartupStuff completes, check success and return if fail - if (!startupTask.Result.success) - return; + The first run is substantially slower than all subsequent runs for both serial + and parallel. This is most likely due to file system caching speeding up + subsequent runs, and it's significant because in the wild, most runs are "cold" + and will not benefit from caching. - //When RunStartupStuff completes, check if user has opted into beta and run Avalonia UI if they did. - //Otherwise we just ignore all the Avalonia app build stuff and continue with winforms. + All times are in milliseconds. + + Run Parallel Serial + 1 2837 5835 + 2 1566 2774 + 3 1562 2316 + 4 1642 2388 + 5 1596 2391 + 6 1591 2358 + 7 1492 2363 + 8 1542 2335 + 9 1600 2418 + 10 1564 2359 + 11 1567 2379 + + Min 1492 2316 + Q1 1562 2358 + Med 1567 2379 + Q2 1567 2379 + Max 2837 5835 + */ //For debug purposes, always run AvaloniaUI. - if (true) //(startupTask.Result.useBeta) + if (true) //(config.GetNonString("BetaOptIn")) { - await Task.WhenAll(appBuilderTask, classicLifetimeTask, startupTask); + //Start as much work in parallel as possible. + var runPreStartTasksTask = Task.Run(() => RunDbMigrations(config)); + var classicLifetimeTask = Task.Run(() => new ClassicDesktopStyleApplicationLifetime()); + var appBuilderTask = Task.Run(BuildAvaloniaApp); + + if (!await runPreStartTasksTask) + return; + + var runOtherMigrationsTask = Task.Run(() => RunOtherMigrations(config)); + var dbLibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true)); + + (await appBuilderTask).SetupWithLifetime(await classicLifetimeTask); + + if (!await runOtherMigrationsTask) + return; + + ((AvaloniaUI.Views.MainWindow)classicLifetimeTask.Result.MainWindow).OnLibraryLoaded(await dbLibraryTask); - appBuilderTask.Result.SetupWithLifetime(classicLifetimeTask.Result); classicLifetimeTask.Result.Start(null); } else { + if (!RunDbMigrations(config) || !RunOtherMigrations(config)) + return; + System.Windows.Forms.Application.Run(new Form1()); } } @@ -60,9 +95,8 @@ namespace LibationWinForms .LogToTrace() .UseReactiveUI(); - private static (bool success, bool useBeta) RunStartupStuff() + private static Configuration LoadLibationConfig() { - bool useBeta = false; try { //// Uncomment to see Console. Must be called before anything writes to Console. @@ -81,13 +115,40 @@ namespace LibationWinForms //***********************************************// // Migrations which must occur before configuration is loaded for the first time. Usually ones which alter the Configuration var config = AppScaffolding.LibationScaffolding.RunPreConfigMigrations(); + AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists(); + return config; + } + catch (Exception ex) + { + DisplayStartupErrorMessage(ex); + return null; + } + } + + private static bool RunDbMigrations(Configuration config) + { + try + { // do this as soon as possible (post-config) RunInstaller(config); // most migrations go in here - AppScaffolding.LibationScaffolding.RunPostConfigMigrations(config); + AppScaffolding.LibationScaffolding.RunPostConfigMigrations(config); + return true; + } + catch (Exception ex) + { + DisplayStartupErrorMessage(ex); + return false; + } + } + + private static bool RunOtherMigrations(Configuration config) + { + try + { // migrations which require Forms or are long-running RunWindowsOnlyMigrations(config); @@ -99,26 +160,31 @@ namespace LibationWinForms // logging is init'd here AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding(config); - useBeta = config.GetNonString("BetaOptIn"); + // global exception handling (ShowAdminAlert) attempts to use logging. only call it after logging has been init'd + postLoggingGlobalExceptionHandling(); + + return true; } catch (Exception ex) { - var title = "Fatal error, pre-logging"; - var body = "An unrecoverable error occurred. Since this error happened before logging could be initialized, this error can not be written to the log file."; - try - { - MessageBoxLib.ShowAdminAlert(null, body, title, ex); - } - catch - { - MessageBox.Show($"{body}\r\n\r\n{ex.Message}\r\n\r\n{ex.StackTrace}", title, MessageBoxButtons.OK, MessageBoxIcon.Error); - } - return (false, false); + DisplayStartupErrorMessage(ex); + return false; } - // global exception handling (ShowAdminAlert) attempts to use logging. only call it after logging has been init'd - postLoggingGlobalExceptionHandling(); + } - return (true, useBeta); + + private static void DisplayStartupErrorMessage(Exception ex) + { + var title = "Fatal error, pre-logging"; + var body = "An unrecoverable error occurred. Since this error happened before logging could be initialized, this error can not be written to the log file."; + try + { + MessageBoxLib.ShowAdminAlert(null, body, title, ex); + } + catch + { + MessageBox.Show($"{body}\r\n\r\n{ex.Message}\r\n\r\n{ex.StackTrace}", title, MessageBoxButtons.OK, MessageBoxIcon.Error); + } } private static void RunInstaller(Configuration config)