Improve AvaloniaUI startup times

This commit is contained in:
Michael Bucari-Tovo 2022-07-14 17:57:46 -06:00
parent 2b6d1201b6
commit 428ea5e864
3 changed files with 120 additions and 60 deletions

View File

@ -8,11 +8,16 @@ using LibationWinForms.AvaloniaUI.Views.ProductsGrid;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using LibationWinForms.AvaloniaUI.ViewModels; using LibationWinForms.AvaloniaUI.ViewModels;
using LibationFileManager; using LibationFileManager;
using DataLayer;
using System.Collections.Generic;
namespace LibationWinForms.AvaloniaUI.Views namespace LibationWinForms.AvaloniaUI.Views
{ {
public partial class MainWindow : ReactiveWindow<MainWindowViewModel> public partial class MainWindow : ReactiveWindow<MainWindowViewModel>
{ {
public event EventHandler Load;
public event EventHandler<List<LibraryBook>> LibraryLoaded;
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
@ -37,13 +42,9 @@ namespace LibationWinForms.AvaloniaUI.Views
// misc which belongs in winforms app but doesn't have a UI element // misc which belongs in winforms app but doesn't have a UI element
Configure_NonUI(); Configure_NonUI();
async void DoDisplay(object _, EventArgs __)
{ {
await productsDisplay.Display(); this.LibraryLoaded += (_, dbBooks) => productsDisplay.Display(dbBooks);
} LibraryCommands.LibrarySizeChanged += (_, _) => productsDisplay.Display(DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
{
this.Load += DoDisplay;
LibraryCommands.LibrarySizeChanged += DoDisplay;
this.Closing += (_,_) => this.SaveSizeAndLocation(Configuration.Instance); this.Closing += (_,_) => this.SaveSizeAndLocation(Configuration.Instance);
} }
} }
@ -53,9 +54,8 @@ namespace LibationWinForms.AvaloniaUI.Views
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
public event EventHandler Load;
public void OnLoad() => Load?.Invoke(this, EventArgs.Empty); public void OnLoad() => Load?.Invoke(this, EventArgs.Empty);
public void OnLibraryLoaded(List<LibraryBook> initialLibrary) => LibraryLoaded?.Invoke(this, initialLibrary);
private void FindAllControls() private void FindAllControls()
{ {

View File

@ -1,12 +1,11 @@
using ApplicationServices; using Avalonia.Controls;
using Avalonia.Controls; using DataLayer;
using LibationWinForms.AvaloniaUI.ViewModels; using LibationWinForms.AvaloniaUI.ViewModels;
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using Avalonia.Threading;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
{ {
@ -14,19 +13,17 @@ namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
{ {
private void Configure_Display() { } private void Configure_Display() { }
public async Task Display() public void Display(List<LibraryBook> dbBooks)
{ {
try try
{ {
var dbBooks = DbContexts.GetLibrary_Flat_NoTracking(includeParents: true);
if (_viewModel is null) if (_viewModel is null)
{ {
_viewModel = new ProductsDisplayViewModel(dbBooks); _viewModel = new ProductsDisplayViewModel(dbBooks);
await Dispatcher.UIThread.InvokeAsync(() => InitialLoaded?.Invoke(this, EventArgs.Empty)); InitialLoaded?.Invoke(this, EventArgs.Empty);
int bookEntryCount = bindingList.BookEntries().Count(); 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 //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 //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 //List is already displayed. Replace all items with new ones, refilter, and re-sort
string existingFilter = _viewModel?.GridEntries?.Filter; string existingFilter = _viewModel?.GridEntries?.Filter;
var newEntries = ProductsDisplayViewModel.CreateGridEntries(dbBooks); var newEntries = ProductsDisplayViewModel.CreateGridEntries(dbBooks);
await Dispatcher.UIThread.InvokeAsync(() => bindingList.ReplaceList(newEntries);
{ bindingList.Filter = existingFilter;
bindingList.ReplaceList(newEntries); ReSort();
bindingList.Filter = existingFilter;
ReSort();
});
} }
} }
catch (Exception ex) catch (Exception ex)

View File

@ -1,16 +1,14 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using ApplicationServices;
using Avalonia; using Avalonia;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using Dinah.Core;
using LibationFileManager; using LibationFileManager;
using LibationWinForms.Dialogs; using LibationWinForms.Dialogs;
using Serilog;
namespace LibationWinForms namespace LibationWinForms
{ {
@ -23,33 +21,70 @@ namespace LibationWinForms
[STAThread] [STAThread]
static async Task Main() static async Task Main()
{ {
//Start as much work in parallel as possible. var config = LoadLibationConfig();
var startupTask = Task.Run(RunStartupStuff);
var appBuilderTask = Task.Run(BuildAvaloniaApp);
var classicLifetimeTask = Task.Run(() => new ClassicDesktopStyleApplicationLifetime());
List<Task> 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 The first run is substantially slower than all subsequent runs for both serial
if (!startupTask.Result.success) and parallel. This is most likely due to file system caching speeding up
return; 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. All times are in milliseconds.
//Otherwise we just ignore all the Avalonia app build stuff and continue with winforms.
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. //For debug purposes, always run AvaloniaUI.
if (true) //(startupTask.Result.useBeta) if (true) //(config.GetNonString<bool>("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); classicLifetimeTask.Result.Start(null);
} }
else else
{ {
if (!RunDbMigrations(config) || !RunOtherMigrations(config))
return;
System.Windows.Forms.Application.Run(new Form1()); System.Windows.Forms.Application.Run(new Form1());
} }
} }
@ -60,9 +95,8 @@ namespace LibationWinForms
.LogToTrace() .LogToTrace()
.UseReactiveUI(); .UseReactiveUI();
private static (bool success, bool useBeta) RunStartupStuff() private static Configuration LoadLibationConfig()
{ {
bool useBeta = false;
try try
{ {
//// Uncomment to see Console. Must be called before anything writes to Console. //// 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 // Migrations which must occur before configuration is loaded for the first time. Usually ones which alter the Configuration
var config = AppScaffolding.LibationScaffolding.RunPreConfigMigrations(); 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) // do this as soon as possible (post-config)
RunInstaller(config); RunInstaller(config);
// most migrations go in here // 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 // migrations which require Forms or are long-running
RunWindowsOnlyMigrations(config); RunWindowsOnlyMigrations(config);
@ -99,26 +160,31 @@ namespace LibationWinForms
// logging is init'd here // logging is init'd here
AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding(config); AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding(config);
useBeta = config.GetNonString<bool>("BetaOptIn"); // global exception handling (ShowAdminAlert) attempts to use logging. only call it after logging has been init'd
postLoggingGlobalExceptionHandling();
return true;
} }
catch (Exception ex) catch (Exception ex)
{ {
var title = "Fatal error, pre-logging"; DisplayStartupErrorMessage(ex);
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."; return false;
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);
} }
// 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) private static void RunInstaller(Configuration config)