Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8dd8204
feat: add `maui profile` command for Android startup tracing
simonrozsival Apr 2, 2026
777bf3f
fix: drop net11.0 from StartupProfiling targets (CI only has .NET 10 …
simonrozsival Apr 4, 2026
73b478e
fix: suppress NU5118 duplicate README in StartupProfiling package
simonrozsival Apr 4, 2026
d41cb00
Merge branch 'main' of github.com:dotnet/maui-labs into feature/maui-…
simonrozsival Apr 10, 2026
b4b2d5c
Improve maui profile startup tracing
simonrozsival Apr 10, 2026
a3b58ef
Simplify maui profile cleanup
simonrozsival Apr 10, 2026
3960b2c
Address startup profiling review feedback
simonrozsival Apr 10, 2026
3933e8c
Polish profile progress and cancellation UX
simonrozsival Apr 10, 2026
d762c51
Add iOS simulator support to maui profile
simonrozsival Apr 11, 2026
81fe732
Fix iOS simulator Release profiling
simonrozsival Apr 11, 2026
d0b12d2
Handle mixed diagnostics tool installs
simonrozsival Apr 11, 2026
745e838
Refactor ProfileCommand into partial files
simonrozsival Apr 11, 2026
4080a26
Replace partial ProfileCommand split
simonrozsival Apr 11, 2026
fc4e9ce
Simplify profile command structure
simonrozsival Apr 12, 2026
60ac9e4
Add startup subcommand for maui profile
simonrozsival Apr 14, 2026
a692cf9
Fix profile trace stop race
simonrozsival Apr 14, 2026
3d6a45e
Improve Android startup PGO tracing
simonrozsival Apr 15, 2026
fcdf59f
Merge origin/main into feature/maui-profile-command
simonrozsival Apr 15, 2026
c5f0c3a
Restore manual stop for Android startup tracing
simonrozsival Apr 15, 2026
59081e5
Simplify startup profiling cleanup
simonrozsival Apr 15, 2026
12865c8
Refine startup trace finalization
simonrozsival Apr 15, 2026
6e434f5
Harden startup trace review fixes
simonrozsival Apr 15, 2026
f656f2c
Merge origin/main into feature/maui-profile-command
simonrozsival Apr 16, 2026
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
1 change: 1 addition & 0 deletions MauiLabs.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<Folder Name="/src/Cli/">
<Project Path="src/Cli/Microsoft.Maui.Cli.UnitTests/Microsoft.Maui.Cli.UnitTests.csproj" />
<Project Path="src/Cli/Microsoft.Maui.Cli/Microsoft.Maui.Cli.csproj" />
<Project Path="src/Cli/Microsoft.Maui.StartupProfiling/Microsoft.Maui.StartupProfiling.csproj" />
</Folder>
<Folder Name="/src/DevFlow/">
<Project Path="src/DevFlow/Microsoft.Maui.DevFlow.Agent.Core/Microsoft.Maui.DevFlow.Agent.Core.csproj" />
Expand Down
1 change: 1 addition & 0 deletions src/Cli/Cli.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"projects": [
"src\\Cli\\Microsoft.Maui.Cli\\Microsoft.Maui.Cli.csproj",
"src\\Cli\\Microsoft.Maui.Cli.UnitTests\\Microsoft.Maui.Cli.UnitTests.csproj",
"src\\Cli\\Microsoft.Maui.StartupProfiling\\Microsoft.Maui.StartupProfiling.csproj",
"src\\DevFlow\\Microsoft.Maui.DevFlow.Driver\\Microsoft.Maui.DevFlow.Driver.csproj"
]
}
Expand Down
65 changes: 58 additions & 7 deletions src/Cli/Microsoft.Maui.Cli.UnitTests/DeviceManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ namespace Microsoft.Maui.Cli.UnitTests;

public class DeviceManagerTests
{
static DeviceManager CreateManager(FakeAndroidProvider? androidProvider = null) =>
new(androidProvider, _ => Task.FromResult<IReadOnlyList<Device>>([]));

[Fact]
public async Task GetAllDevicesAsync_ReturnsAndroidDevices()
{
Expand All @@ -23,7 +26,7 @@ public async Task GetAllDevicesAsync_ReturnsAndroidDevices()
}
};

var manager = new DeviceManager(fakeAndroid);
var manager = CreateManager(fakeAndroid);

// Act
var devices = await manager.GetAllDevicesAsync();
Expand All @@ -45,7 +48,7 @@ public async Task GetDevicesByPlatformAsync_FiltersCorrectly()
}
};

var manager = new DeviceManager(fakeAndroid);
var manager = CreateManager(fakeAndroid);

// Act
var androidOnly = await manager.GetDevicesByPlatformAsync("android");
Expand All @@ -68,7 +71,7 @@ public async Task GetDeviceByIdAsync_FindsCorrectDevice()
}
};

var manager = new DeviceManager(fakeAndroid);
var manager = CreateManager(fakeAndroid);

// Act
var device = await manager.GetDeviceByIdAsync("device-2");
Expand All @@ -84,7 +87,7 @@ public async Task GetDeviceByIdAsync_ReturnsNull_WhenNotFound()
{
// Arrange
var fakeAndroid = new FakeAndroidProvider();
var manager = new DeviceManager(fakeAndroid);
var manager = CreateManager(fakeAndroid);

// Act
var device = await manager.GetDeviceByIdAsync("nonexistent");
Expand All @@ -105,7 +108,7 @@ public async Task GetAllDevicesAsync_IncludesShutdownAvds()
}
};

var manager = new DeviceManager(fakeAndroid);
var manager = CreateManager(fakeAndroid);

// Act
var devices = await manager.GetAllDevicesAsync();
Expand Down Expand Up @@ -144,7 +147,7 @@ public async Task GetAllDevicesAsync_MergesRunningEmulatorWithAvd()
}
};

var manager = new DeviceManager(fakeAndroid);
var manager = CreateManager(fakeAndroid);

// Act
var devices = await manager.GetAllDevicesAsync();
Expand Down Expand Up @@ -183,7 +186,7 @@ public async Task GetAllDevicesAsync_MergesRunningEmulatorWithAvd_ByEmulatorId()
}
};

var manager = new DeviceManager(fakeAndroid);
var manager = CreateManager(fakeAndroid);

// Act
var devices = await manager.GetAllDevicesAsync();
Expand All @@ -194,4 +197,52 @@ public async Task GetAllDevicesAsync_MergesRunningEmulatorWithAvd_ByEmulatorId()
Assert.Equal("Pixel_6_API_35", devices[0].EmulatorId);
Assert.True(devices[0].IsRunning);
}

[Fact]
public void ParseAppleSimulatorDevices_ReturnsBootedAndShutdownIosSimulators()
{
const string json =
"""
{
"devices": {
"com.apple.CoreSimulator.SimRuntime.iOS-26-2": [
{
"udid": "BOOTED-SIM",
"name": "iPhone 17 Pro",
"state": "Booted",
"isAvailable": true,
"deviceTypeIdentifier": "com.apple.CoreSimulator.SimDeviceType.iPhone-17-Pro"
},
{
"udid": "SHUTDOWN-SIM",
"name": "iPad Air 11-inch (M3)",
"state": "Shutdown",
"isAvailable": true,
"deviceTypeIdentifier": "com.apple.CoreSimulator.SimDeviceType.iPad-Air-11-inch-M3"
}
],
"com.apple.CoreSimulator.SimRuntime.tvOS-26-2": [
{
"udid": "TV-SIM",
"name": "Apple TV",
"state": "Booted",
"isAvailable": true
}
]
}
}
""";

var devices = DeviceManager.ParseAppleSimulatorDevices(json);

Assert.Equal(2, devices.Count);
Assert.Equal("BOOTED-SIM", devices[0].Id);
Assert.True(devices[0].IsRunning);
Assert.Equal(Platforms.iOS, devices[0].Platform);
Assert.Equal("iOS 26.2", devices[0].VersionName);

Assert.Equal("SHUTDOWN-SIM", devices[1].Id);
Assert.False(devices[1].IsRunning);
Assert.Equal(DeviceIdiom.Tablet, devices[1].Idiom);
}
}
93 changes: 93 additions & 0 deletions src/Cli/Microsoft.Maui.Cli.UnitTests/OutputFormatterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text;
using System.Diagnostics;
using Microsoft.Maui.Cli.Errors;
using Microsoft.Maui.Cli.Commands;
using Microsoft.Maui.Cli.Models;
using Microsoft.Maui.Cli.Output;
using Spectre.Console;
Expand Down Expand Up @@ -78,6 +80,27 @@ private static (SpectreOutputFormatter formatter, TestConsole console) CreateTes
return (formatter, console);
}

private static Process StartTestProcessThatWritesStderr()
{
var startInfo = OperatingSystem.IsWindows()
? new ProcessStartInfo("cmd", "/c echo boom 1>&2")
: new ProcessStartInfo("/bin/bash", "-lc \"echo boom >&2\"");

startInfo.UseShellExecute = false;
startInfo.RedirectStandardOutput = true;
startInfo.RedirectStandardError = true;
startInfo.RedirectStandardInput = true;
startInfo.CreateNoWindow = true;

var process = new Process
{
StartInfo = startInfo
};

Assert.True(process.Start());
return process;
}

[Fact]
public void SpectreOutputFormatter_WriteSuccess_OutputsMessage()
{
Expand Down Expand Up @@ -114,6 +137,44 @@ public void SpectreOutputFormatter_WriteInfo_OutputsMessage()
Assert.Contains("ℹ", output);
}

[Fact]
public void Program_HandleCommandException_ForCancellation_WritesCancelledInsteadOfError()
{
var (formatter, console) = CreateTestFormatter();

var exitCode = Program.HandleCommandException(formatter, new OperationCanceledException());

Assert.Equal(130, exitCode);
Assert.Contains("Cancelled.", console.Output);
Assert.DoesNotContain("Error", console.Output);
}

[Fact]
public async Task MonitoredProcess_Attach_DoesNotEchoStderr_WhenNotVerbose()
{
var (formatter, console) = CreateTestFormatter();
using var process = StartTestProcessThatWritesStderr();
using var monitored = MonitoredProcess.Attach(process, formatter, useJson: false, verbose: false, prefix: "trace", CancellationToken.None);

await monitored.WaitForExitAsync();

Assert.DoesNotContain("boom", console.Output);
Assert.Contains("boom", monitored.StandardError.ToString());
}

[Fact]
public async Task MonitoredProcess_Attach_EchoesStderr_AsProgress_WhenVerbose()
{
var (formatter, console) = CreateTestFormatter(verbose: true);
using var process = StartTestProcessThatWritesStderr();
using var monitored = MonitoredProcess.Attach(process, formatter, useJson: false, verbose: true, prefix: "trace", CancellationToken.None);

await monitored.WaitForExitAsync();

Assert.Contains("[trace:stderr] boom", console.Output);
Assert.DoesNotContain("⚠", console.Output);
}

[Fact]
public void SpectreOutputFormatter_WriteError_IncludesErrorCode()
{
Expand Down Expand Up @@ -217,4 +278,36 @@ public void SpectreOutputFormatter_WriteTable_FormatsColumns()
Assert.Contains("Apple", output);
Assert.Contains("Carrot", output);
}

[Theory]
[InlineData(0.4, "0.4s")]
[InlineData(5.4, "5.4s")]
[InlineData(36, "36.0s")]
[InlineData(62.3, "1:02.3s")]
public void SpectreOutputFormatter_FormatElapsed_UsesReadableDurations(double seconds, string expected)
{
var actual = SpectreOutputFormatter.FormatElapsed(TimeSpan.FromSeconds(seconds));
Assert.Equal(expected, actual);
}

[Fact]
public void SpectreOutputFormatter_FormatTimedStatusMarkup_AppendsElapsedToFirstLine()
{
var actual = SpectreOutputFormatter.FormatTimedStatusMarkup(
"Publishing dotnet-pgo...\n[grey] Restored packages[/]",
TimeSpan.FromSeconds(36));

Assert.StartsWith("Publishing dotnet-pgo... [grey](36.0s)[/]", actual);
Assert.Contains("\n[grey] Restored packages[/]", actual);
}

[Fact]
public void SpectreOutputFormatter_FormatCompletedStatusMessage_StripsTrailingEllipsis()
{
var actual = SpectreOutputFormatter.FormatCompletedStatusMessage(
"Building the app...",
TimeSpan.FromSeconds(36));

Assert.Equal("Building the app (36.0s)", actual);
}
}
Loading