fix: startup speed
CI / build-and-test (push) Canceled after 0s

This commit is contained in:
2026-09-13 22:04:07 +02:00
parent 2c791258a1
commit 0962845ead
7 changed files with 86 additions and 22 deletions
+45 -13
View File
@@ -26,6 +26,8 @@ public class App : Application
public static IServiceProvider Services { get; private set; } = null!;
private static ServiceProvider? _serviceProvider;
private static bool _exitHandlerAttached;
private static Task _initialPolls = Task.CompletedTask;
private static readonly CancellationTokenSource StartupCancellation = new();
public override void Initialize() => AvaloniaXamlLoader.Load(this);
@@ -65,7 +67,11 @@ public class App : Application
promptVm.OnUnlocked = async password =>
{
AppBootstrapper.DbPassword = password;
await StartMainAppAsync(desktop, promptWindow);
var unlockedSplash = new SplashWindow();
desktop.MainWindow = unlockedSplash;
unlockedSplash.Show();
promptWindow.Close();
await StartMainAppAsync(desktop, unlockedSplash);
};
desktop.MainWindow = promptWindow;
promptWindow.Show();
@@ -79,14 +85,21 @@ public class App : Application
private static async Task StartMainAppAsync(
IClassicDesktopStyleApplicationLifetime desktop, Window? windowToClose = null)
{
_serviceProvider = AppBootstrapper.BuildServices();
var splash = windowToClose as SplashWindow;
var timer = System.Diagnostics.Stopwatch.StartNew();
var progress = new Progress<(int Value, string Text)>(step =>
splash?.SetProgress(step.Value, step.Text));
_serviceProvider = await Task.Run(() => AppBootstrapper.BuildServices(
(value, text) => ((IProgress<(int, string)>)progress).Report((value, text))));
Services = _serviceProvider;
Services.GetRequiredService<AppLogger>().Info("Anwendung gestartet.");
GlobalExceptionHandler.AttachNotifications(Services.GetRequiredService<NotificationService>());
// Papierkorb (14.3): Einträge älter als 30 Tage endgültig entfernen. Beim Start statt per
// Timer - reicht für ein Werkzeug, das ohnehin nur "Fehlklick eben rückgängig machen" sein
// soll, kein dauerhaftes Archiv.
Services.GetRequiredService<ITrashRepository>().PurgeOlderThan(DateTime.UtcNow.AddDays(-30));
splash?.SetProgress(60, "Papierkorb aufräumen …");
await Task.Run(() => Services.GetRequiredService<ITrashRepository>()
.PurgeOlderThan(DateTime.UtcNow.AddDays(-30)));
if (!_exitHandlerAttached)
{
@@ -99,19 +112,13 @@ public class App : Application
// Opt-in - ein Neustart der App richtet den Pipe-Listener neu ein.
Services.GetRequiredService<Services.Mcp.McpServerHostedService>().Start();
splash?.SetProgress(75, "Übersicht vorbereiten …");
await Dispatcher.UIThread.InvokeAsync(() => { }, DispatcherPriority.Background);
var mainVm = Services.GetRequiredService<MainWindowViewModel>();
WireCallbacks(mainVm);
// Beide optionalen Erstabgleiche noch unter dem Splashscreen abschließen. Ihre
// CPU-/Datenbankarbeit läuft innerhalb der Dienste im Threadpool; dadurch öffnet das
// Hauptfenster mit fertigem Datenstand und friert nicht kurz danach ein. Das Auflösen
// aktiviert zugleich die periodischen Timer.
var initialPolls = new List<Task>();
if (Services.GetService<UntisSyncService>() is { } untisSync)
initialPolls.Add(untisSync.PollAsync());
if (Services.GetService<AnnualPlanSyncService>() is { } annualPlanSync)
initialPolls.Add(annualPlanSync.PollAsync());
await Task.WhenAll(initialPolls);
splash?.SetProgress(90, "Hauptfenster öffnen …");
await Dispatcher.UIThread.InvokeAsync(() => { }, DispatcherPriority.Background);
var main = new MainWindow { DataContext = mainVm };
main.EnableWindowSizePersistence(Services.GetRequiredService<WindowSettingsService>());
@@ -119,7 +126,30 @@ public class App : Application
main.EnableFinalSync(syncEngine);
desktop.MainWindow = main;
main.Show();
splash?.SetProgress(100, "Bereit");
windowToClose?.Close();
AppBootstrapper.Logger.Info($"Start: Hauptfenster nach {timer.ElapsedMilliseconds} ms geöffnet.");
// Vorhandene lokale Daten sind sofort nutzbar; Netzwerkzugriffe blockieren den Start nicht.
var untis = Services.GetService<UntisSyncService>();
var annualPlan = Services.GetService<AnnualPlanSyncService>();
if (untis is not null)
untis.DataChanged += () => Dispatcher.UIThread.Post(() =>
{
Services.GetRequiredService<TimetableViewModel>().Load();
Services.GetRequiredService<DashboardViewModel>().RefreshCommand.Execute(null);
});
_initialPolls = Task.Run(async () =>
{
try
{
await Task.WhenAll(
untis?.PollAsync(StartupCancellation.Token) ?? Task.CompletedTask,
annualPlan?.PollAsync(StartupCancellation.Token) ?? Task.CompletedTask);
}
catch (OperationCanceledException) when (StartupCancellation.IsCancellationRequested) { }
catch (Exception ex) { AppBootstrapper.Logger.Error("Erstabgleich fehlgeschlagen.", ex); }
});
}
/// <summary>Wechselt die Darstellung sofort, ohne Neustart (12.4) — Avalonia stylt den
@@ -140,6 +170,8 @@ public class App : Application
try
{
StartupCancellation.Cancel();
_initialPolls.GetAwaiter().GetResult();
serviceProvider.GetService<LiteDbContext>()?.Checkpoint();
}
catch (Exception ex)