Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions docs/linux-port/investigations/startup-time.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Startup time investigation

Reported baseline: host start to first window about 1.0 s warm; fresh Debian 13
container about 3.5 s warm and 6.8 s first run. These are inputs, not measurements
made in this sandbox. A mapped window, first backdrop render and usable folder
listing are different endpoints; record all three when comparing changes.

## Phases and tracing

Set `FILES_TRACE=1` for opt-in, bounded asynchronous stderr records. Existing
`PerformanceTrace` scopes report duration, parent id and thread, without paths.
New scopes cover:

| Phase | Trace operation | Work and ordering |
| --- | --- | --- |
| Root environment | startup.root-environment | Security checks before any host setup; must stay synchronous. |
| Instance ownership | startup.single-instance | D-Bus ownership/forwarding, socket fallback; must precede window creation. Existing 3 s connect and 5 s forwarding budgets retained to avoid duplicate windows. |
| Cursor | startup.cursor | X resources, then up to two read-only gsettings processes before host creation. |
| App resources | startup.app-resources | App XAML and exception hook setup; stale archive cleanup already runs on a worker. |
| Activation | startup.activation | DI, eager managers, placement, backdrop, navigation and component scheduling. |
| DI registration | startup.configure-services | File logger, registrations, provider build. Most singleton registrations are lazy. |
| User settings | startup.user-settings | Synchronous JSON settings initialization when first resolved; theme, placement and restored tabs depend on settings. |
| Window resources | startup.window-resources | MainWindow constructor/XAML/title setup (base constructor outside scope). |
| Theme discovery | startup.theme-detection | Portal read followed by GTK environment, KDE file and gsettings fallback. |
| First backdrop | startup.first-backdrop | Frame/theme/backdrop preparation, compositor commit, activation and first Rendered event with loaded root. |
| Navigation/components | startup.navigation-and-components | Command-line/session navigation and background component scheduling after backdrop render. |

Scopes overlap: do not sum nested durations. Program/native runtime loading before
App construction is not included in activation. Existing `dbus-connect`,
`dbus.theme`, `process-start` and file-operation traces provide further detail.
First-backdrop is a framework render acknowledgement, not proof that the display
server presented pixels. External window timestamps are still needed.

## Changes and expected savings

Desktop startup previously called Activate directly and immediately constructed
main content. It now uses the existing ActivateWithBackdropAsync helper, which
awaits a lightweight backdrop render before navigation. Terminal discovery and
Sentry configuration follow that render. Main content, drives and startup tasks
therefore cannot monopolize the UI thread before the backdrop acknowledgement.
Settings, placement, eager manager resolution and initial theme stay before it;
moving those blindly could break dependencies or flash the wrong theme.

Theme portal discovery now has a 250 ms synchronous wait, bounded connect and
read waits, and returns each read's result directly instead of shared `_lastRead`.
Late worker failures are caught. Theme gsettings waits shrink from 1500 to 250 ms;
cursor fallback waits shrink from 1500 to 250 ms per missing setting. Slow portal
sessions can initially use the local fallback; the existing portal watcher still
updates theme on change. User-action portal and mount timeouts remain unchanged.
The theme watcher may finish after the foreground wait; this is not cancellation
of a remote D-Bus method. Existing shared connection lifecycle is retained.

Expected savings, **unmeasured**:

- Unresponsive theme portal: up to 1.75 s less foreground waiting per detection.
- Slow theme gsettings fallback: up to 1.25 s less waiting per detection.
- Two slow cursor fallback processes: up to 2.5 s less waiting total.
- Deferring navigation, terminal discovery and Sentry: moves their pre-yield CPU
and I/O cost after backdrop render; amount depends on session and first-run data.
- Healthy services: timeout reductions alone save approximately zero; process
startup, XAML parsing, fontconfig and renderer initialization remain.

These are wait-budget differences, not additive predictions for the supplied
3.5/6.8 s timings. Process creation and termination, local file reads and native
calls are not hard-bounded by these waits. Container renderer choice, font caches,
filesystem caches and portal availability can explain substantial differences.

## Services, storage and icons

App.xaml.cs initializes XAML/handlers, not the entire service graph. App.Desktop.cs
registers services then eagerly resolves QuickAccessManager, StorageHistoryWrapper,
FileTagsManager, LibraryManager, logger and AppModel. AppModel is constructed on
the UI thread for Clipboard.ContentChanged. Preserve that affinity.

UserSettingsService/BaseJsonSettings and serializers use JSON files. The desktop
FileTagsDatabase.Linux implementation lazily loads JSON; StorageHistoryWrapper is
in memory. A SQLite package reference does not establish a startup DB open.
InitializeAppComponentsAsync already schedules pinned items, libraries, cloud
sections and tag updates on workers after navigation. DrivesViewModel constructs
a watcher and enumerates via GetDrivesAsync when updated; this work is downstream
of navigation. Keep mount/polkit operation budgets unchanged.

LinuxIconThemeProvider discovers theme via environment and GTK/KDE configuration
and indexes icon files; it does not spawn a portal/gsettings process for theme
selection. MainWindow already schedules its window icon after the next render.
PortalWallpaperService is invoked for wallpaper actions, not eager startup
wallpaper discovery; DesktopWallpaperService is a stub. Its 5 s call/10 minute
confirmation budgets should not be shortened for startup.

## Publishing, JIT and globalization

Files.App.csproj already sets PublishReadyToRun=true for non-Debug builds with a
RID, with composite R2R disabled. scripts/linux/publish.sh supplies Release and a
Linux RID, so its tarball publish already requests R2R. The script now explicitly
passes PublishReadyToRun=true so this packaging intent is visible. The analysis
publish profile also enables it. No published tarball was available for binary
verification here. The previous project comment's 25–30% claim is not a measured
result for the supplied Debian environment.

R2R does not eliminate assembly loading, XAML initialization, native library
loading or all JIT compilation. No explicit tiered compilation/TieredPGO or
runtime ReadyToRun overrides were found in the app/publishing paths examined;
retain runtime defaults rather than trading later responsiveness for speculative
startup gains. Benchmark the same Release RID publication with R2R on/off, and
inspect Files.runtimeconfig.json plus DOTNET_/COMPlus_ overrides. Compare file
size and page faults as well as latency; a larger R2R payload can cost cold I/O.

Do not enable InvariantGlobalization for Files.App. AppLanguageHelper constructs
named cultures and the date formatter uses the selected language's CultureInfo;
localized formatting and sorting need real globalization. Only the isolated
ElevationHelper explicitly uses invariant mode. Preserve app ICU availability;
measure its loading cost rather than disabling culture support.

## Validation and next measurements

The sandbox cannot build/run the Uno desktop app. Platform tests validate the
Linux changes, but do not compile App.Desktop/MainWindow. Review the app changes
by reference searches and run desktop build and UI checks on a supported host.
Use scripts/linux/headless-run.sh with its private display/throwaway home; never
use the user's display or real settings. Capture FILES_TRACE stderr for repeated
warm runs and fresh-home first runs of the same published artifact. Record runtime
version, RID, renderer, portal availability and cache state. Compare backdrop and
listing readiness separately, including closing the window during startup.

Validation in this sandbox: single-node platform build passed with
`-p:NuGetAudit=false` (network auditing failed on the initial attempt); direct
runner: 1208 passed, 41 skipped, 13 failed (X11 display setup and native-directory
fixtures). Focused CursorSettingsResolverTests: 9 passed. `git diff --check` and
`bash -n scripts/linux/publish.sh` passed. Desktop build/run and published binary
verification were not performed; no measured latency improvement is claimed.
2 changes: 1 addition & 1 deletion scripts/linux/publish.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ rm -rf "$out"
MSBUILDDISABLENODEREUSE=1 nice -n 19 dotnet publish "$root/src/Files.App/Files.App.csproj" \
-f net10.0-desktop -c Release -r "$rid" \
--self-contained "$self_contained" \
-p:PublishTrimmed=false -p:PublishAot=false \
-p:PublishTrimmed=false -p:PublishAot=false -p:PublishReadyToRun=true \
-nodeReuse:false -m:2 -o "$out"

# This AOT executable is installed separately, root-owned, by native packages only.
Expand Down
1 change: 1 addition & 0 deletions src/Files.App/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public static FlyoutBase? LastOpenedFlyout
/// </summary>
public App()
{
using var trace = Files.Platform.Abstractions.Diagnostics.PerformanceTrace.Begin("startup.app-resources", true);
InitializeComponent();

// Configure exception handlers
Expand Down
2 changes: 1 addition & 1 deletion src/Files.App/Files.App.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
<!-- XAML cannot see generator output on Uno; ICommandManager is checked in under Platforms/Desktop/Generated -->
<FilesPregeneratedICommandManager Condition="'$(FilesPregeneratedICommandManager)' == ''">true</FilesPregeneratedICommandManager>
<DefineConstants>$(DefineConstants);DESKTOP</DefineConstants>
<!-- Precompile (ReadyToRun) published Linux builds: roughly 25-30% faster cold start. Composite stays off (self-contained Uno assemblies) -->
<!-- Precompile (ReadyToRun) published Linux builds to reduce startup JIT work. Composite stays off (self-contained Uno assemblies) -->
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug' and '$(RuntimeIdentifier)' != ''">true</PublishReadyToRun>
<PublishReadyToRunComposite>false</PublishReadyToRunComposite>
</PropertyGroup>
Expand Down
7 changes: 7 additions & 0 deletions src/Files.App/Helpers/Application/AppLifecycleHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ public static void ConfigureSentry()
/// <param name="appModel">Constructed on the UI thread by the caller (its ctor is UI-thread-only).</param>
public static IServiceProvider ConfigureHost(AppModel appModel)
{
using var trace = Files.Platform.Abstractions.Diagnostics.PerformanceTrace.Begin("startup.configure-services", true);
var services = new ServiceCollection();
var fileLoggerProvider = new FileLoggerProvider(Path.Combine(ApplicationData.Current.LocalFolder.Path, "debug.log"));

Expand Down Expand Up @@ -414,6 +415,12 @@ public static IServiceProvider ConfigureHost(AppModel appModel)
/// </summary>
public static void SaveSessionTabs()
{
#if DESKTOP
// Navigation can still be restoring tabs while the window closes.
if (!MainPageViewModel.StartupTabsLoadedTask.IsCompletedSuccessfully)
return;
#endif

var userSettingsService = Ioc.Default.GetRequiredService<IUserSettingsService>();

userSettingsService.GeneralSettingsService.LastSessionTabList = MainPageViewModel.AppInstances.DefaultIfEmpty().Select(tab =>
Expand Down
1 change: 1 addition & 0 deletions src/Files.App/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public sealed partial class MainWindow : WindowEx

public MainWindow() : base(416, 316)
{
using var trace = Files.Platform.Abstractions.Diagnostics.PerformanceTrace.Begin("startup.window-resources", true);
InitializeComponent();

ExtendsContentIntoTitleBar = UseClientSideDecorations;
Expand Down
22 changes: 14 additions & 8 deletions src/Files.App/Platforms/Desktop/App.Desktop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,9 @@ protected override void OnLaunched(LaunchActivatedEventArgs e)

async Task ActivateAsync()
{
using var startupTrace = Files.Platform.Abstractions.Diagnostics.PerformanceTrace.Begin("startup.activation", true);
var serviceProvider = AppLifecycleHelper.ConfigureHost(AppModel);
Ioc.Default.ConfigureServices(serviceProvider);
RootActionsHelper.InitializeTerminalAvailability();

if (AppLifecycleHelper.AppEnvironment is not AppEnvironment.Dev)
AppLifecycleHelper.ConfigureSentry();

// TODO: Replace with DI
QuickAccessManager = Ioc.Default.GetRequiredService<QuickAccessManager>();
Expand All @@ -46,14 +43,23 @@ async Task ActivateAsync()
AppModel = Ioc.Default.GetRequiredService<AppModel>();

MainWindow.Instance.RestorePlacement(Ioc.Default.GetRequiredService<Files.Platform.Abstractions.ILocalSettingsStore>());
MainWindow.Instance.Activate();
UiResponsivenessProbe.Start(UiDispatcher!);

MainWindow.Instance.Closed += Window_Closed;
MainWindow.Instance.Activated += Window_Activated;

using (var trace = Files.Platform.Abstractions.Diagnostics.PerformanceTrace.Begin("startup.first-backdrop", true))
{
if (!await MainWindow.Instance.ActivateWithBackdropAsync())
return;
}

RootActionsHelper.InitializeTerminalAvailability();
if (AppLifecycleHelper.AppEnvironment is not AppEnvironment.Dev)
AppLifecycleHelper.ConfigureSentry();
UiResponsivenessProbe.Start(UiDispatcher!);

Logger.LogInformation("App launched (desktop).");

using var navigationTrace = Files.Platform.Abstractions.Diagnostics.PerformanceTrace.Begin("startup.navigation-and-components", true);
// Command line (paths, --select, -t, -n) of this process; later launches arrive through ISingleInstanceService
var launchOptions = Files.App.Utils.CommandLine.DesktopCommandLine.Parse(Program.LaunchArguments, Environment.CurrentDirectory);
if (launchOptions.IsEmpty)
Expand Down Expand Up @@ -144,7 +150,7 @@ private async void Window_Closed(object sender, WindowEventArgs args)

if (userSettingsService.GeneralSettingsService.ContinueLastSessionOnStartUp || userSettingsService.AppSettingsService.RestoreTabsOnStartup)
AppLifecycleHelper.SaveSessionTabs();
else
else if (MainPageViewModel.StartupTabsLoadedTask.IsCompletedSuccessfully)
await commandManager.CloseAllTabs.ExecuteAsync();

// TODO: "leave app running" (background parking) is not supported on desktop yet
Expand Down
5 changes: 4 additions & 1 deletion src/Files.App/Platforms/Desktop/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public static int Main(string[] args)
{
try
{
using var trace = Files.Platform.Abstractions.Diagnostics.PerformanceTrace.Begin("startup.root-environment");
RootStartupEnvironment.Apply();
}
catch (System.IO.IOException)
Expand Down Expand Up @@ -76,6 +77,7 @@ public static int Main(string[] args)
bool isPrimary;
try
{
using var trace = Files.Platform.Abstractions.Diagnostics.PerformanceTrace.Begin("startup.single-instance");
isPrimary = singleInstance.TryBecomePrimaryAsync(request).GetAwaiter().GetResult();
}
catch (Exception ex) when (ex is System.IO.IOException or InvalidOperationException or System.Net.Sockets.SocketException or UnauthorizedAccessException)
Expand All @@ -97,7 +99,8 @@ public static int Main(string[] args)
var (renderingBackend, reason) = RenderingBackendSelector.Resolve(Environment.GetEnvironmentVariable, RenderingBackendSelector.HasGpuDevice);
Console.Error.WriteLine($"[Files] Rendering backend: {renderingBackend} ({reason ?? "default"})");

CursorSettingsResolver.ApplyToProcess();
using (var trace = Files.Platform.Abstractions.Diagnostics.PerformanceTrace.Begin("startup.cursor"))
CursorSettingsResolver.ApplyToProcess();

var host = UnoPlatformHostBuilder.Create()
.App(() => new App())
Expand Down
1 change: 1 addition & 0 deletions src/Files.App/Services/Settings/UserSettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ public IAppSettingsService AppSettingsService

public UserSettingsService()
{
using var trace = Files.Platform.Abstractions.Diagnostics.PerformanceTrace.Begin("startup.user-settings", true);
var settingsSerializer = new DefaultSettingsSerializer(message => App.Logger.LogWarning(message));
SettingsSerializer = settingsSerializer;

Expand Down
Loading
Loading