Extract startup tracing behind IStartupTracer abstraction

Add Microsoft.Extensions.DependencyInjection package and introduce an
IStartupTracer abstraction with NoOpStartupTracer (zero-overhead default)
and FileStartupTracer (renamed from WinUiStartupTrace). A StartupTracer
factory selects the implementation from environment configuration.

Consumers (App, MainWindow, MainWindowViewModel, SensorColumnMeasurer) now
depend on the non-nullable interface, removing null-conditional trace calls
and decoupling startup instrumentation from construction logic. No behavior
change: tracing still writes the same log when enabled.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-06-02 14:57:48 -05:00
co-authored by Copilot
parent 73a81138a7
commit 9b576ea633
9 changed files with 237 additions and 104 deletions
+11 -20
View File
@@ -5,7 +5,7 @@
using System;
using System.IO;
using System.Threading.Tasks;
using LibreHardwareMonitor.Windows.WinUI.Services;
using LibreHardwareMonitor.Windows.WinUI.Services.Tracing;
using Microsoft.UI.Xaml;
using IOPath = System.IO.Path;
@@ -14,13 +14,13 @@ namespace LibreHardwareMonitor.Windows.WinUI;
public partial class App : Application
{
private bool _launchStarted;
private readonly WinUiStartupTrace? _startupTrace;
private readonly IStartupTracer _startupTrace;
private Window? _window;
public App()
{
_startupTrace = WinUiStartupTrace.Create();
_startupTrace?.Mark("App.Constructor.Begin");
_startupTrace = StartupTracer.Create();
_startupTrace.Mark("App.Constructor.Begin");
MeasureStartup("App.WireExceptionHandlers", () =>
{
UnhandledException += App_UnhandledException;
@@ -28,8 +28,8 @@ public partial class App : Application
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
});
MeasureStartup("App.InitializeComponent", InitializeComponent);
_startupTrace?.Mark("App.Constructor.Complete");
_startupTrace?.Flush();
_startupTrace.Mark("App.Constructor.Complete");
_startupTrace.Flush();
}
protected override void OnLaunched(LaunchActivatedEventArgs args)
@@ -45,18 +45,18 @@ public partial class App : Application
_launchStarted = true;
try
{
_startupTrace?.Mark("App.LaunchMainWindow.Begin");
_startupTrace.Mark("App.LaunchMainWindow.Begin");
MainWindow mainWindow = MeasureStartup("App.CreateMainWindow", () => new MainWindow(_startupTrace));
_window = mainWindow;
MeasureStartup("App.ActivateWindow", mainWindow.Activate);
MeasureStartup("App.StartMonitoringAfterActivation", mainWindow.StartMonitoringAfterActivation);
_startupTrace?.Mark("App.LaunchMainWindow.Complete");
_startupTrace?.Flush();
_startupTrace.Mark("App.LaunchMainWindow.Complete");
_startupTrace.Flush();
}
catch (Exception ex)
{
_startupTrace?.Mark("App.LaunchMainWindow.Exception", $"{ex.GetType().FullName}: {ex.Message}");
_startupTrace?.Flush();
_startupTrace.Mark("App.LaunchMainWindow.Exception", $"{ex.GetType().FullName}: {ex.Message}");
_startupTrace.Flush();
WriteExceptionLog("Window launch failed", ex);
throw;
}
@@ -64,20 +64,11 @@ public partial class App : Application
private void MeasureStartup(string phase, Action action)
{
if (_startupTrace == null)
{
action();
return;
}
_startupTrace.Measure(phase, action);
}
private T MeasureStartup<T>(string phase, Func<T> action)
{
if (_startupTrace == null)
return action();
return _startupTrace.Measure(phase, action);
}
@@ -21,6 +21,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="2.0.1" />
</ItemGroup>
@@ -12,6 +12,7 @@ using System.Threading.Tasks;
using LibreHardwareMonitor.Hardware;
using LibreHardwareMonitor.Windows.WinUI.Controls;
using LibreHardwareMonitor.Windows.WinUI.Services;
using LibreHardwareMonitor.Windows.WinUI.Services.Tracing;
using LibreHardwareMonitor.Windows.WinUI.Utilities;
using LibreHardwareMonitor.Windows.WinUI.ViewModels;
using Microsoft.UI;
@@ -33,7 +34,7 @@ public sealed partial class MainWindow : Window
private readonly WindowChromeManager _chromeManager;
private readonly WindowPlacementService _placementService;
private readonly DispatcherQueueTimer _timer;
private readonly WinUiStartupTrace? _startupTrace;
private readonly IStartupTracer _startupTrace;
private readonly TrayIconService _trayIconService;
private readonly DialogService _dialogService;
private readonly SensorColumnMeasurer _columnMeasurer;
@@ -50,14 +51,14 @@ public sealed partial class MainWindow : Window
private bool _sensorTreeRebuildQueued;
private bool _sensorColumnWidthUpdateQueued;
public MainWindow() : this(null)
public MainWindow() : this(NoOpStartupTracer.Instance)
{
}
internal MainWindow(WinUiStartupTrace? startupTrace)
internal MainWindow(IStartupTracer startupTrace)
{
_startupTrace = startupTrace;
_startupTrace?.Mark("MainWindow.Constructor.Begin");
_startupTrace.Mark("MainWindow.Constructor.Begin");
AppSettings settings = MeasureStartup("MainWindow.LoadSettings", AppSettings.LoadDefault);
ViewModel = MeasureStartup("MainWindow.CreateViewModel", () => new MainWindowViewModel(settings, _startupTrace));
@@ -159,14 +160,14 @@ public sealed partial class MainWindow : Window
Closed += MainWindow_Closed;
});
_startupTrace?.Mark("MainWindow.Constructor.Complete");
_startupTrace.Mark("MainWindow.Constructor.Complete");
}
public MainWindowViewModel ViewModel { get; }
public void StartMonitoringAfterActivation()
{
_startupTrace?.Mark("MainWindow.StartMonitoringAfterActivation.Queued");
_startupTrace.Mark("MainWindow.StartMonitoringAfterActivation.Queued");
if (!DispatcherQueue.TryEnqueue(async () =>
{
if (_isMonitoringStarted)
@@ -175,27 +176,27 @@ public sealed partial class MainWindow : Window
_isMonitoringStarted = true;
try
{
_startupTrace?.Mark("MainWindow.StartMonitoringAfterActivation.Begin");
_startupTrace.Mark("MainWindow.StartMonitoringAfterActivation.Begin");
ApplyInitialWindowState();
await MeasureStartupAsync("MainWindowViewModel.StartAsync", ViewModel.StartAsync);
SyncTraySensors();
SyncGadgetSensors();
UpdateGadgetVisibility();
MeasureStartup("MainWindow.StartTimer", _timer.Start);
_startupTrace?.Mark("MainWindow.StartMonitoringAfterActivation.Complete");
_startupTrace.Mark("MainWindow.StartMonitoringAfterActivation.Complete");
RequestStartupTraceComplete();
_startupTrace?.Flush();
_startupTrace.Flush();
}
catch (Exception ex)
{
_startupTrace?.Mark("MainWindow.StartMonitoringAfterActivation.Exception", $"{ex.GetType().FullName}: {ex.Message}");
_startupTrace?.Flush();
_startupTrace.Mark("MainWindow.StartMonitoringAfterActivation.Exception", $"{ex.GetType().FullName}: {ex.Message}");
_startupTrace.Flush();
RecordRuntimeError("Hardware initialization failed", ex);
}
}))
{
_startupTrace?.Mark("MainWindow.StartMonitoringAfterActivation.EnqueueFailed");
_startupTrace?.Flush();
_startupTrace.Mark("MainWindow.StartMonitoringAfterActivation.EnqueueFailed");
_startupTrace.Flush();
}
}
@@ -207,8 +208,8 @@ public sealed partial class MainWindow : Window
private void RootGrid_Loaded(object sender, RoutedEventArgs e)
{
RootGrid.Loaded -= RootGrid_Loaded;
_startupTrace?.Mark("MainWindow.RootLoaded", GetRootSizeDetail());
_startupTrace?.Flush();
_startupTrace.Mark("MainWindow.RootLoaded", GetRootSizeDetail());
_startupTrace.Flush();
}
private void RootGrid_LayoutUpdated(object? sender, object e)
@@ -218,13 +219,13 @@ public sealed partial class MainWindow : Window
_firstLayoutRecorded = true;
RootGrid.LayoutUpdated -= RootGrid_LayoutUpdated;
_startupTrace?.Mark("MainWindow.FirstLayoutUpdated", GetRootSizeDetail());
_startupTrace?.Flush();
_startupTrace.Mark("MainWindow.FirstLayoutUpdated", GetRootSizeDetail());
_startupTrace.Flush();
}
private void MeasureStartup(string phase, Action action)
{
if (_startupTrace is not { IsComplete: false })
if (_startupTrace.IsComplete)
{
action();
return;
@@ -235,7 +236,7 @@ public sealed partial class MainWindow : Window
private void MeasureStartup(string phase, Action action, Func<string> getDetail)
{
if (_startupTrace is not { IsComplete: false })
if (_startupTrace.IsComplete)
{
action();
return;
@@ -246,7 +247,7 @@ public sealed partial class MainWindow : Window
private T MeasureStartup<T>(string phase, Func<T> action)
{
if (_startupTrace is not { IsComplete: false })
if (_startupTrace.IsComplete)
return action();
return _startupTrace.Measure(phase, action);
@@ -254,7 +255,7 @@ public sealed partial class MainWindow : Window
private async Task MeasureStartupAsync(string phase, Func<Task> action)
{
if (_startupTrace is not { IsComplete: false })
if (_startupTrace.IsComplete)
{
await action();
return;
@@ -427,8 +428,8 @@ public sealed partial class MainWindow : Window
private void RecordRuntimeError(string message, Exception exception)
{
_startupTrace?.Mark("MainWindow.RuntimeError", $"{message}: {exception.GetType().FullName}: {exception.Message}");
_startupTrace?.Flush();
_startupTrace.Mark("MainWindow.RuntimeError", $"{message}: {exception.GetType().FullName}: {exception.Message}");
_startupTrace.Flush();
// Use a dedicated runtime log (not the startup log that App.xaml.cs appends to) and write it only once, so a
// recurring per-tick failure neither clobbers the startup diagnostics nor grows the file without bound.
@@ -466,7 +467,7 @@ public sealed partial class MainWindow : Window
_secondaryWindows.CloseAll();
_placementService.Save();
ViewModel.Dispose();
_startupTrace?.Dispose();
_startupTrace.Dispose();
}
private void AppWindow_Changed(AppWindow sender, AppWindowChangedEventArgs args)
@@ -566,7 +567,7 @@ public sealed partial class MainWindow : Window
private void CompleteStartupTraceIfReady()
{
if (_startupCompleteRecorded || _startupTrace is not { IsComplete: false } || _columnMeasurer.RowCount == 0)
if (_startupCompleteRecorded || _startupTrace.IsComplete || _columnMeasurer.RowCount == 0)
return;
_startupCompleteRecorded = true;
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using LibreHardwareMonitor.Windows.WinUI.Services.Tracing;
using LibreHardwareMonitor.Windows.WinUI.ViewModels;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
@@ -24,7 +25,7 @@ internal sealed class SensorColumnMeasurer
private static readonly TimeSpan SettleDelay = TimeSpan.FromSeconds(5);
private readonly MainWindowViewModel _viewModel;
private readonly WinUiStartupTrace? _startupTrace;
private readonly IStartupTracer _startupTrace;
private readonly Dictionary<(string Text, bool Bold), double> _cache = new();
private readonly List<Grid> _rowGrids = [];
private readonly DispatcherQueueTimer _settleTimer;
@@ -34,7 +35,7 @@ internal sealed class SensorColumnMeasurer
private double _stableDeviceColumnWidth = DefaultDeviceColumnWidth;
private bool _settled;
public SensorColumnMeasurer(DispatcherQueue dispatcherQueue, MainWindowViewModel viewModel, WinUiStartupTrace? startupTrace)
public SensorColumnMeasurer(DispatcherQueue dispatcherQueue, MainWindowViewModel viewModel, IStartupTracer startupTrace)
{
_viewModel = viewModel ?? throw new ArgumentNullException(nameof(viewModel));
_startupTrace = startupTrace;
@@ -215,6 +216,6 @@ internal sealed class SensorColumnMeasurer
return;
_viewModel.Settings.SetValue(DeviceColumnWidthSetting, measuredWidth);
_startupTrace?.Mark("MainWindow.RecordDeviceColumnWidth", FormattableString.Invariant($"width={measuredWidth:F0}, rows={_rowGrids.Count}"));
_startupTrace.Mark("MainWindow.RecordDeviceColumnWidth", FormattableString.Invariant($"width={measuredWidth:F0}, rows={_rowGrids.Count}"));
}
}
@@ -11,13 +11,14 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LibreHardwareMonitor.Windows.WinUI.Services;
namespace LibreHardwareMonitor.Windows.WinUI.Services.Tracing;
internal sealed class WinUiStartupTrace : IDisposable
/// <summary>
/// File-writing startup tracer. Records phase timings and flushes them to a CSV-style log.
/// Created via <see cref="StartupTracer.Create"/> when startup timing is enabled.
/// </summary>
internal sealed class FileStartupTracer : IStartupTracer
{
private const string EnabledEnvironmentVariable = "LHM_WINUI_STARTUP_TIMING";
private const string PathEnvironmentVariable = "LHM_WINUI_STARTUP_TIMING_PATH";
private readonly List<Entry> _entries = new();
private readonly string _fileName;
private readonly object _lock = new();
@@ -25,9 +26,9 @@ internal sealed class WinUiStartupTrace : IDisposable
private bool _completed;
private bool _disposed;
private WinUiStartupTrace()
internal FileStartupTracer(string fileName)
{
_fileName = GetLogFileName();
_fileName = fileName;
_totalStopwatch = Stopwatch.StartNew();
Mark("WinUIStartupTrace.Begin");
}
@@ -41,11 +42,6 @@ internal sealed class WinUiStartupTrace : IDisposable
}
}
public static WinUiStartupTrace? Create()
{
return IsEnabled() ? new WinUiStartupTrace() : null;
}
public void Complete(string phase, string detail = "")
{
bool shouldWrite;
@@ -231,38 +227,6 @@ internal sealed class WinUiStartupTrace : IDisposable
}
}
private static string GetLogFileName()
{
string? configuredPath = Environment.GetEnvironmentVariable(PathEnvironmentVariable);
string fileName = $"LibreHardwareMonitor.WinUIStartupTiming-{DateTime.Now:yyyyMMdd-HHmmss-fff}.log";
if (string.IsNullOrWhiteSpace(configuredPath))
return Path.Combine(AppContext.BaseDirectory, fileName);
if (configuredPath.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal)
|| configuredPath.EndsWith(Path.AltDirectorySeparatorChar.ToString(), StringComparison.Ordinal)
|| Directory.Exists(configuredPath))
{
return Path.Combine(configuredPath, fileName);
}
return configuredPath;
}
private static bool IsEnabled()
{
string environmentValue = Environment.GetEnvironmentVariable(EnabledEnvironmentVariable) ?? "";
return IsTruthy(environmentValue);
}
private static bool IsTruthy(string value)
{
return value.Equals("1", StringComparison.OrdinalIgnoreCase)
|| value.Equals("true", StringComparison.OrdinalIgnoreCase)
|| value.Equals("yes", StringComparison.OrdinalIgnoreCase)
|| value.Equals("on", StringComparison.OrdinalIgnoreCase);
}
private void AddEntry(string phase, TimeSpan elapsed, string status, string detail)
{
AddEntry(phase, _totalStopwatch.Elapsed, elapsed, status, detail);
@@ -0,0 +1,39 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
using System;
using System.Threading.Tasks;
namespace LibreHardwareMonitor.Windows.WinUI.Services.Tracing;
/// <summary>
/// Optional startup instrumentation consumed by startup operations. A no-op implementation
/// is used when tracing is disabled, so callers never need null checks.
/// </summary>
internal interface IStartupTracer : IDisposable
{
bool IsComplete { get; }
void Mark(string phase, string detail = "");
void Complete(string phase, string detail = "");
void Flush();
void Measure(string phase, Action action);
void Measure(string phase, Action action, Func<string>? getDetail);
T Measure<T>(string phase, Func<T> action);
T Measure<T>(string phase, Func<T> action, Func<T, string>? getDetail);
Task MeasureAsync(string phase, Func<Task> action);
Task MeasureAsync(string phase, Func<Task> action, Func<string>? getDetail);
Task<T> MeasureAsync<T>(string phase, Func<Task<T>> action);
Task<T> MeasureAsync<T>(string phase, Func<Task<T>> action, Func<T, string>? getDetail);
}
@@ -0,0 +1,79 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
using System;
using System.Threading.Tasks;
namespace LibreHardwareMonitor.Windows.WinUI.Services.Tracing;
/// <summary>
/// Zero-overhead tracer used when startup tracing is disabled. Measure overloads simply
/// invoke the supplied delegate; everything else is a no-op.
/// </summary>
internal sealed class NoOpStartupTracer : IStartupTracer
{
public static readonly NoOpStartupTracer Instance = new();
private NoOpStartupTracer()
{
}
public bool IsComplete => false;
public void Mark(string phase, string detail = "")
{
}
public void Complete(string phase, string detail = "")
{
}
public void Flush()
{
}
public void Measure(string phase, Action action)
{
action();
}
public void Measure(string phase, Action action, Func<string>? getDetail)
{
action();
}
public T Measure<T>(string phase, Func<T> action)
{
return action();
}
public T Measure<T>(string phase, Func<T> action, Func<T, string>? getDetail)
{
return action();
}
public Task MeasureAsync(string phase, Func<Task> action)
{
return action();
}
public Task MeasureAsync(string phase, Func<Task> action, Func<string>? getDetail)
{
return action();
}
public Task<T> MeasureAsync<T>(string phase, Func<Task<T>> action)
{
return action();
}
public Task<T> MeasureAsync<T>(string phase, Func<Task<T>> action, Func<T, string>? getDetail)
{
return action();
}
public void Dispose()
{
}
}
@@ -0,0 +1,56 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
using System;
using System.IO;
namespace LibreHardwareMonitor.Windows.WinUI.Services.Tracing;
/// <summary>
/// Factory that selects the appropriate <see cref="IStartupTracer"/> based on environment
/// configuration. Returns a <see cref="FileStartupTracer"/> when timing is enabled, otherwise
/// the shared <see cref="NoOpStartupTracer"/>.
/// </summary>
internal static class StartupTracer
{
private const string EnabledEnvironmentVariable = "LHM_WINUI_STARTUP_TIMING";
private const string PathEnvironmentVariable = "LHM_WINUI_STARTUP_TIMING_PATH";
public static IStartupTracer Create()
{
return IsEnabled() ? new FileStartupTracer(GetLogFileName()) : NoOpStartupTracer.Instance;
}
private static string GetLogFileName()
{
string? configuredPath = Environment.GetEnvironmentVariable(PathEnvironmentVariable);
string fileName = $"LibreHardwareMonitor.WinUIStartupTiming-{DateTime.Now:yyyyMMdd-HHmmss-fff}.log";
if (string.IsNullOrWhiteSpace(configuredPath))
return Path.Combine(AppContext.BaseDirectory, fileName);
if (configuredPath.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal)
|| configuredPath.EndsWith(Path.AltDirectorySeparatorChar.ToString(), StringComparison.Ordinal)
|| Directory.Exists(configuredPath))
{
return Path.Combine(configuredPath, fileName);
}
return configuredPath;
}
private static bool IsEnabled()
{
string environmentValue = Environment.GetEnvironmentVariable(EnabledEnvironmentVariable) ?? "";
return IsTruthy(environmentValue);
}
private static bool IsTruthy(string value)
{
return value.Equals("1", StringComparison.OrdinalIgnoreCase)
|| value.Equals("true", StringComparison.OrdinalIgnoreCase)
|| value.Equals("yes", StringComparison.OrdinalIgnoreCase)
|| value.Equals("on", StringComparison.OrdinalIgnoreCase);
}
}
@@ -11,6 +11,7 @@ using System.Threading.Tasks;
using LibreHardwareMonitor.Hardware;
using LibreHardwareMonitor.Hardware.Storage;
using LibreHardwareMonitor.Windows.WinUI.Services;
using LibreHardwareMonitor.Windows.WinUI.Services.Tracing;
using LibreHardwareMonitor.Windows.WinUI.Utilities;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
@@ -88,7 +89,7 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable
private readonly SensorSelectionService _sensorSelection;
private readonly RemoteWebServer _remoteWebServer;
private readonly StartupService _startupService = new();
private readonly WinUiStartupTrace? _startupTrace;
private readonly IStartupTracer _startupTrace;
private AppThemeMode _themeMode;
private int _loggingIntervalIndex;
private PlotLocation _plotLocation;
@@ -115,15 +116,15 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable
private TemperatureUnit _temperatureUnit;
private int _updateIntervalIndex;
public MainWindowViewModel(AppSettings settings) : this(settings, null)
public MainWindowViewModel(AppSettings settings) : this(settings, NoOpStartupTracer.Instance)
{
}
internal MainWindowViewModel(AppSettings settings, WinUiStartupTrace? startupTrace)
internal MainWindowViewModel(AppSettings settings, IStartupTracer startupTrace)
{
Settings = settings;
_startupTrace = startupTrace;
_startupTrace?.Mark("MainWindowViewModel.Constructor.Begin");
_startupTrace.Mark("MainWindowViewModel.Constructor.Begin");
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
_hardwareMonitor = new HardwareMonitorService(settings);
_logger = new Logger(_hardwareMonitor.Computer);
@@ -162,7 +163,7 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable
StorageDevice.ThrottleInterval = _throttleAtaUpdate ? TimeSpan.FromSeconds(30) : TimeSpan.Zero;
_logger.LoggingInterval = LoggingIntervals[_loggingIntervalIndex];
_logger.FileRotationMethod = (LoggerFileRotation)Math.Clamp(settings.GetValue("logger.fileRotation", 0), 0, 1);
_startupTrace?.Mark("MainWindowViewModel.Constructor.Complete");
_startupTrace.Mark("MainWindowViewModel.Constructor.Complete");
}
public event EventHandler? PlotInvalidated;
@@ -864,7 +865,7 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable
try
{
IsHardwareLoading = true;
_startupTrace?.Mark("MainWindowViewModel.StartAsync.Begin");
_startupTrace.Mark("MainWindowViewModel.StartAsync.Begin");
StatusText = "Initializing hardware sensors...";
await MeasureStartupAsync("MainWindowViewModel.HardwareMonitor.OpenAsync", () => _hardwareMonitor.OpenAsync(raiseTreeRebuilt: false));
MeasureStartup("MainWindowViewModel.UpdateRoot", UpdateRoot, GetRootDetail);
@@ -873,12 +874,12 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable
MeasureStartup("MainWindowViewModel.StartWebServerFromSettings", StartWebServerFromSettings);
MeasureStartup("MainWindowViewModel.UpdateStatus", UpdateStatus, GetRootDetail);
_isStarted = true;
_startupTrace?.Mark("MainWindowViewModel.StartAsync.Complete", GetRootDetail());
_startupTrace.Mark("MainWindowViewModel.StartAsync.Complete", GetRootDetail());
}
finally
{
IsHardwareLoading = false;
_startupTrace?.Mark("MainWindowViewModel.HardwareLoadingComplete", GetRootDetail());
_startupTrace.Mark("MainWindowViewModel.HardwareLoadingComplete", GetRootDetail());
_isStarting = false;
}
}
@@ -951,7 +952,7 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable
private void MeasureStartup(string phase, Action action)
{
if (_startupTrace is not { IsComplete: false })
if (_startupTrace.IsComplete)
{
action();
return;
@@ -962,7 +963,7 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable
private void MeasureStartup(string phase, Action action, Func<string> getDetail)
{
if (_startupTrace is not { IsComplete: false })
if (_startupTrace.IsComplete)
{
action();
return;
@@ -973,7 +974,7 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable
private T MeasureStartup<T>(string phase, Func<T> action)
{
if (_startupTrace is not { IsComplete: false })
if (_startupTrace.IsComplete)
return action();
return _startupTrace.Measure(phase, action);
@@ -981,7 +982,7 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable
private async Task MeasureStartupAsync(string phase, Func<Task> action)
{
if (_startupTrace is not { IsComplete: false })
if (_startupTrace.IsComplete)
{
await action();
return;