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
3 changes: 1 addition & 2 deletions src/MauiSherpa.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,4 @@
CliOptions.Agent,
};

var config = new CommandLineConfiguration(root);
return await config.InvokeAsync(args);
return await root.Parse(args).InvokeAsync();

This file was deleted.

This file was deleted.

104 changes: 28 additions & 76 deletions src/MauiSherpa.Core/Interfaces.cs
Original file line number Diff line number Diff line change
Expand Up @@ -357,27 +357,27 @@ ProfilingSessionValidationResult ValidateSessionDefinition(
ProfilingPlatformCapabilities capabilities);
}

public interface IProfilingCapabilityProvider
public interface IMauiCliToolService
{
ProfilingTargetPlatform Platform { get; }
Task<ProfilingPlatformCapabilities?> GetCapabilitiesAsync(CancellationToken ct = default);
Task<MauiCliToolStatus> GetStatusAsync(CancellationToken ct = default);
Task<MauiCliToolUpdateInfo> GetUpdateInfoAsync(MauiCliToolStatus status, CancellationToken ct = default);
Task<ProcessResult> InstallAsync(string? version = null, CancellationToken ct = default);
Task<ProcessResult> UpdateAsync(string? version = null, CancellationToken ct = default);
Task<IReadOnlyList<MauiCliDevice>> GetDevicesAsync(CancellationToken ct = default);
}

public interface IProfilingPrerequisitesService
public interface IMauiProfilingCliService : IDisposable
{
Task<ProfilingPrerequisiteReport> GetPrerequisitesAsync(
ProfilingTargetPlatform platform,
IReadOnlyList<ProfilingCaptureKind>? captureKinds = null,
string? workingDirectory = null,
CancellationToken ct = default);
}
MauiProfileRunState State { get; }
event EventHandler<MauiProfileStateChangedEventArgs>? StateChanged;
event EventHandler<MauiCliMessageEventArgs>? MessageReceived;

public interface IProfilingCaptureOrchestrationService
{
Task<ProfilingCapturePlan> PlanCaptureAsync(
ProfilingSessionDefinition definition,
ProfilingCapturePlanOptions? options = null,
Task<MauiProfileExecutionResult> RunAsync(
MauiProfileRequest request,
CancellationToken ct = default);
Task BeginRecordingAsync(CancellationToken ct = default);
Task StopRecordingAsync(CancellationToken ct = default);
void Cancel();
}

public interface IProfilingArtifactLibraryService
Expand All @@ -403,67 +403,6 @@ Task<IReadOnlyList<ProfilingArtifactAnalysis>> AnalyzeArtifactsAsync(
CancellationToken ct = default);
}

/// <summary>
/// Executes a profiling capture plan as a coordinated multi-process pipeline.
/// Handles dependency ordering, parallel execution, long-running processes,
/// graceful stop, and artifact collection.
/// </summary>
public interface IProfilingSessionRunner : IDisposable
{
/// <summary>Current pipeline state</summary>
ProfilingPipelineState State { get; }

/// <summary>Status of each step in the pipeline</summary>
IReadOnlyList<ProfilingStepStatus> Steps { get; }

/// <summary>Fired when pipeline state changes</summary>
event EventHandler<ProfilingPipelineStateChangedEventArgs>? PipelineStateChanged;

/// <summary>Fired when a step's state changes</summary>
event EventHandler<ProfilingStepStateChangedEventArgs>? StepStateChanged;

/// <summary>Fired when a step produces output</summary>
event EventHandler<ProfilingStepOutputEventArgs>? StepOutputReceived;

/// <summary>
/// Execute the full pipeline. Returns when all steps complete (or fail/cancel).
/// </summary>
Task<ProfilingPipelineResult> RunAsync(ProfilingCapturePlan plan, CancellationToken ct = default);

/// <summary>
/// Gracefully stop capture — sends SIGINT to ManualStop steps, waits for processes
/// to flush output files and exit before returning.
/// </summary>
Task StopCaptureAsync();

/// <summary>
/// Abort everything immediately — kills all running processes.
/// </summary>
void Cancel();

/// <summary>
/// Collect a GC dump on demand while the pipeline is running.
/// Returns the path to the .gcdump file, or null if collection failed.
/// </summary>
Task<string?> CollectGcDumpAsync(CancellationToken ct = default);

/// <summary>
/// Whether a trace capture is currently active.
/// </summary>
bool IsTraceActive { get; }

/// <summary>
/// Start an on-demand trace capture. Returns the step ID or null if it cannot start.
/// The trace runs until StopTraceAsync() is called.
/// </summary>
string? StartTraceAsync();

/// <summary>
/// Stop the currently running on-demand trace.
/// </summary>
Task StopTraceAsync();
}

/// <summary>
/// Parses and reports on GC dump (.gcdump) files by shelling out to dotnet-gcdump.
/// </summary>
Expand Down Expand Up @@ -508,6 +447,19 @@ public interface IProfilingSessionStorageService
/// <summary>Delete a session and its folder.</summary>
Task DeleteSessionAsync(string sessionId, CancellationToken ct = default);

/// <summary>Create and persist a completed session from a MAUI CLI profile result.</summary>
Task<ProfilingSessionManifest> SaveMauiProfileSessionAsync(
string sessionId,
MauiProfileRequest request,
MauiProfileResult result,
string? cliVersion = null,
CancellationToken ct = default);

/// <summary>Import a standalone supported profiling artifact as a managed session.</summary>
Task<ProfilingSessionManifest> ImportArtifactAsync(
string artifactPath,
CancellationToken ct = default);

/// <summary>
/// Get the directory path for a session. Creates the directory if it doesn't exist.
/// </summary>
Expand Down
189 changes: 189 additions & 0 deletions src/MauiSherpa.Core/Models/Profiling/MauiProfilingCliModels.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using MauiSherpa.Core.Interfaces;

namespace MauiSherpa.Core.Models.Profiling;

public enum MauiProfileMode
{
Startup,
Interaction
}

public enum MauiProfileOutputFormat
{
NetTrace,
Speedscope,
Mibc
}

public enum MauiProfileRunState
{
Idle,
Starting,
AwaitingRecording,
Recording,
Finalizing,
Completed,
Failed,
Cancelled
}

public enum MauiCliToolState
{
Missing,
Available,
UpdateRequired
}

public sealed record MauiCliToolStatus(
MauiCliToolState State,
string? ExecutablePath = null,
string? Version = null,
string? Message = null)
{
public bool IsAvailable => State == MauiCliToolState.Available;
}

public sealed record MauiCliToolUpdateInfo(
string? InstalledVersion = null,
string? LatestVersion = null,
bool UpdateAvailable = false,
string? Message = null);

public sealed record MauiCliDevice
{
[JsonPropertyName("name")]
public required string Name { get; init; }

[JsonPropertyName("identifier")]
public required string Identifier { get; init; }

[JsonPropertyName("emulator_id")]
public string? EmulatorId { get; init; }

[JsonPropertyName("platforms")]
public required string[] Platforms { get; init; }

[JsonPropertyName("version")]
public string? Version { get; init; }

[JsonPropertyName("version_name")]
public string? VersionName { get; init; }

[JsonPropertyName("model")]
public string? Model { get; init; }

[JsonPropertyName("is_emulator")]
public bool IsEmulator { get; init; }

[JsonPropertyName("is_running")]
public bool IsRunning { get; init; }

[JsonPropertyName("connection_type")]
public string? ConnectionType { get; init; }

public string Platform => Platforms.FirstOrDefault() ?? string.Empty;
}

public sealed record MauiProfileRequest
{
public required string ProjectPath { get; init; }
public required ProfilingTargetPlatform Platform { get; init; }
public required string DeviceId { get; init; }
public string? DeviceName { get; init; }
public bool IsEmulator { get; init; }
public required MauiProfileMode Mode { get; init; }
public MauiProfileOutputFormat Format { get; init; } = MauiProfileOutputFormat.Speedscope;
public required string OutputPath { get; init; }
public string Configuration { get; init; } = "Release";
public TimeSpan? Duration { get; init; }
public string? TraceProfile { get; init; }
public bool NoBuild { get; init; }
}

public sealed record MauiProfileResult
{
public required string ProjectPath { get; init; }
public required string ProjectName { get; init; }
public required string Framework { get; init; }
public required string Platform { get; init; }
public required string DeviceId { get; init; }
public required string DeviceName { get; init; }
public required string Configuration { get; init; }
public required string Format { get; init; }
public required string OutputPath { get; init; }
public string? RawTracePath { get; init; }
public string? DsrouterKind { get; init; }
public string? DiagnosticAddress { get; init; }
public int? DiagnosticPort { get; init; }
public bool UsedStoppingEvent { get; init; }
public DateTimeOffset? StartedAtUtc { get; init; }
public DateTimeOffset? CompletedAtUtc { get; init; }

/// <summary>
/// True when Sherpa reconstructed this result from the artifacts on disk because the
/// CLI captured a profile but failed to report it.
/// </summary>
public bool RecoveredFromDisk { get; init; }
}

public sealed record MauiCliRemediation(
string Type,
string? Command,
IReadOnlyList<string> ManualSteps);

public abstract record MauiCliMessage;

public sealed record MauiCliStatusMessage(
string Status,
string Message,
int? Percentage = null) : MauiCliMessage;

public sealed record MauiCliErrorMessage(
string Code,
string Category,
string Severity,
string Message,
string? NativeError = null,
MauiCliRemediation? Remediation = null,
string? DocsUrl = null,
string? CorrelationId = null,
JsonElement? Context = null) : MauiCliMessage;

public sealed record MauiProfileResultMessage(MauiProfileResult Result) : MauiCliMessage;

public sealed record MauiCliDeviceListMessage(
IReadOnlyList<MauiCliDevice> Devices) : MauiCliMessage;

public sealed record MauiCliVersionMessage(
string Version,
string? Runtime = null,
string? OperatingSystem = null) : MauiCliMessage;

public sealed record MauiCliUnknownMessage(JsonElement Payload) : MauiCliMessage;

public sealed record MauiProfileExecutionResult(
ProcessResult Process,
MauiProfileResult? Profile,
MauiCliErrorMessage? Error,
IReadOnlyList<MauiCliStatusMessage> StatusMessages)
{
// A profile is only present when a usable artifact exists, so it is a better success
// signal than the CLI exit code, which preview builds set even after a good capture.
public bool Success => Profile is not null && Error is null && !WasCancelled;
public bool WasCancelled => Process.WasCancelled || Process.ExitCode == 130;
}

public sealed class MauiCliMessageEventArgs(MauiCliMessage message) : EventArgs
{
public MauiCliMessage Message { get; } = message;
}

public sealed class MauiProfileStateChangedEventArgs(
MauiProfileRunState oldState,
MauiProfileRunState newState) : EventArgs
{
public MauiProfileRunState OldState { get; } = oldState;
public MauiProfileRunState NewState { get; } = newState;
}
Loading
Loading