Improve AvaloniaUI startup times
This commit is contained in:
parent
2b6d1201b6
commit
428ea5e864
@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user