Public Access
Merge claude/windows-startup-performance-a615bc: start the Windows client in under a second by precompiling the publish and taking the cache off the UI thread
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
3e607f57b9
3 files changed
+46
-8
No files matched your search
@@ -266,6 +266,11 @@ internal sealed partial class DodoSshApp : Application
|
|||||||
var viewModel = BuildShell(desktop, paths, caches, workspace, knownHosts, deviceKeys, connections,
|
var viewModel = BuildShell(desktop, paths, caches, workspace, knownHosts, deviceKeys, connections,
|
||||||
browser, updates);
|
browser, updates);
|
||||||
|
|
||||||
|
// Started rather than awaited, and before the window rather than after it: the migration runs on the
|
||||||
|
// thread pool, so it overlaps building the window instead of following it — see StartAsync. The view
|
||||||
|
// model shows its own progress and handles its own failures, so discarding the task here is safe.
|
||||||
|
_ = viewModel.StartAsync(CancellationToken.None);
|
||||||
|
|
||||||
desktop.MainWindow = new MainWindow { DataContext = viewModel };
|
desktop.MainWindow = new MainWindow { DataContext = viewModel };
|
||||||
|
|
||||||
// The other half of Program's claim on this profile: a second launch was refused, and this is what
|
// The other half of Program's claim on this profile: a second launch was refused, and this is what
|
||||||
@@ -286,11 +291,6 @@ internal sealed partial class DodoSshApp : Application
|
|||||||
viewModel.OpenLink(first);
|
viewModel.OpenLink(first);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Started rather than awaited: the framework's initialisation must not block on a schema
|
|
||||||
// migration. The view model shows its own progress and handles its own failures, which is why
|
|
||||||
// discarding the task here is safe rather than merely convenient.
|
|
||||||
_ = viewModel.StartAsync(CancellationToken.None);
|
|
||||||
|
|
||||||
WireShutdown(desktop, viewModel, workspace, caches, others);
|
WireShutdown(desktop, viewModel, workspace, caches, others);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,26 @@
|
|||||||
that is correct for nobody.
|
that is correct for nobody.
|
||||||
-->
|
-->
|
||||||
<InvariantGlobalization>false</InvariantGlobalization>
|
<InvariantGlobalization>false</InvariantGlobalization>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Precompiled, on a Windows publish, because a cold start here is mostly the JIT. The runtime pack's
|
||||||
|
own assemblies already ship precompiled; ours and every package's — Avalonia, Skia's bindings, EF
|
||||||
|
Core, SSH.NET — ship as IL, and before this every launch compiled the toolkit, the XAML and EF's
|
||||||
|
model building method by method before anything reached the screen.
|
||||||
|
|
||||||
|
Measured on a self-contained win-x64 publish, warm disk, median of six launches: the splash from
|
||||||
|
735 ms to 536 ms after the process starts, and the main window idle on its first screen from 2.6 s
|
||||||
|
to 1.4 s. The cost is size, because each method is carried as IL and as code: the publish grows from
|
||||||
|
146 MB to 175 MB and the Setup executable from 70 MB to 89 MB. That is the trade worth making for an
|
||||||
|
application opened many times a day and installed once — and it is a fifth of what dropping the
|
||||||
|
native symbols below saves, so the first install is still far smaller than it once was.
|
||||||
|
|
||||||
|
Conditioned on the RID rather than set outright, so a plain `dotnet build`, `dotnet run` and every
|
||||||
|
test project referencing this one are untouched: ReadyToRun is a publish step and needs a RID to
|
||||||
|
target. Windows only for now, because Windows is where it was measured; macOS and Linux publish the
|
||||||
|
same way and would very likely gain the same, but nothing here has timed them.
|
||||||
|
-->
|
||||||
|
<PublishReadyToRun Condition="$(RuntimeIdentifier.StartsWith('win-'))">true</PublishReadyToRun>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -3213,6 +3213,15 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
|||||||
/// Migrating happens before unlock and touches no encrypted content — only the shape of the tables.
|
/// Migrating happens before unlock and touches no encrypted content — only the shape of the tables.
|
||||||
/// That is the point of migrating rather than recreating: a user who upgrades while offline must still
|
/// That is the point of migrating rather than recreating: a user who upgrades while offline must still
|
||||||
/// be able to open their vault.
|
/// be able to open their vault.
|
||||||
|
/// <para>
|
||||||
|
/// <b>The cache is read on the thread pool, and that is the whole of what makes this asynchronous.</b>
|
||||||
|
/// EF Core over SQLite has no real asynchronous I/O: its async methods complete synchronously, so
|
||||||
|
/// awaiting them on the UI thread ran the migration and the profile read inline, inside the caller's
|
||||||
|
/// dispatcher turn. Measured on a release build, that was a quarter to half a second of a cold start —
|
||||||
|
/// EF building its model and SQLite loading — during which the window could not be built or drawn.
|
||||||
|
/// Off the UI thread it overlaps the window's construction instead, which is why both heads call this
|
||||||
|
/// before building their view rather than after.
|
||||||
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
internal async Task StartAsync(CancellationToken cancellationToken)
|
internal async Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@@ -3224,10 +3233,19 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
paths.EnsureCreated();
|
// Taken here rather than inside the delegate, so that nothing on the thread pool reads this view
|
||||||
await caches.MigrateAsync(cancellationToken).ConfigureAwait(true);
|
// model's state.
|
||||||
|
var opener = Opener();
|
||||||
|
|
||||||
var profile = await Opener().ReadProfileAsync(cancellationToken).ConfigureAwait(true);
|
var profile = await Task.Run(
|
||||||
|
async () =>
|
||||||
|
{
|
||||||
|
paths.EnsureCreated();
|
||||||
|
await caches.MigrateAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return await opener.ReadProfileAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
},
|
||||||
|
cancellationToken).ConfigureAwait(true);
|
||||||
|
|
||||||
if (profile is null)
|
if (profile is null)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in new issue
Block a user