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 LibationWinForms.AvaloniaUI.ViewModels;
using LibationFileManager;
using DataLayer;
using System.Collections.Generic;
namespace LibationWinForms.AvaloniaUI.Views
{
public partial class MainWindow : ReactiveWindow<MainWindowViewModel>
{
public event EventHandler Load;
public event EventHandler<List<LibraryBook>> 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<LibraryBook> initialLibrary) => LibraryLoaded?.Invoke(this, initialLibrary);
private void FindAllControls()
{

View File

@ -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<LibraryBook> 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();
});
}
}
catch (Exception ex)

View File

@ -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<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
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<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);
}
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);
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,9 +160,20 @@ namespace LibationWinForms
// logging is init'd here
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)
{
DisplayStartupErrorMessage(ex);
return false;
}
}
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.";
@ -113,12 +185,6 @@ namespace LibationWinForms
{
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 RunInstaller(Configuration config)