diff --git a/MauiSherpa.sln b/MauiSherpa.sln index 99e512bf..3df4556c 100644 --- a/MauiSherpa.sln +++ b/MauiSherpa.sln @@ -23,6 +23,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MauiSherpa.LinuxGtk", "src\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MauiSherpa.Cli", "src\MauiSherpa.Cli\MauiSherpa.Cli.csproj", "{5BD8CEB8-EC58-47DE-9C52-1E0BB078B268}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MauiSherpa.ProfilingSample", "src\MauiSherpa.ProfilingSample\MauiSherpa.ProfilingSample.csproj", "{465A3EFF-F52C-42AB-A4FE-3C6C7600DE9A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -129,6 +131,18 @@ Global {5BD8CEB8-EC58-47DE-9C52-1E0BB078B268}.Release|x64.Build.0 = Release|Any CPU {5BD8CEB8-EC58-47DE-9C52-1E0BB078B268}.Release|x86.ActiveCfg = Release|Any CPU {5BD8CEB8-EC58-47DE-9C52-1E0BB078B268}.Release|x86.Build.0 = Release|Any CPU + {465A3EFF-F52C-42AB-A4FE-3C6C7600DE9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {465A3EFF-F52C-42AB-A4FE-3C6C7600DE9A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {465A3EFF-F52C-42AB-A4FE-3C6C7600DE9A}.Debug|x64.ActiveCfg = Debug|Any CPU + {465A3EFF-F52C-42AB-A4FE-3C6C7600DE9A}.Debug|x64.Build.0 = Debug|Any CPU + {465A3EFF-F52C-42AB-A4FE-3C6C7600DE9A}.Debug|x86.ActiveCfg = Debug|Any CPU + {465A3EFF-F52C-42AB-A4FE-3C6C7600DE9A}.Debug|x86.Build.0 = Debug|Any CPU + {465A3EFF-F52C-42AB-A4FE-3C6C7600DE9A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {465A3EFF-F52C-42AB-A4FE-3C6C7600DE9A}.Release|Any CPU.Build.0 = Release|Any CPU + {465A3EFF-F52C-42AB-A4FE-3C6C7600DE9A}.Release|x64.ActiveCfg = Release|Any CPU + {465A3EFF-F52C-42AB-A4FE-3C6C7600DE9A}.Release|x64.Build.0 = Release|Any CPU + {465A3EFF-F52C-42AB-A4FE-3C6C7600DE9A}.Release|x86.ActiveCfg = Release|Any CPU + {465A3EFF-F52C-42AB-A4FE-3C6C7600DE9A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -142,5 +156,6 @@ Global {7F4B08C8-EC1E-4C00-9F0E-6CFF4169F36B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {985FC27D-16D3-4858-9F6C-4F8E1B76893F} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {5BD8CEB8-EC58-47DE-9C52-1E0BB078B268} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {465A3EFF-F52C-42AB-A4FE-3C6C7600DE9A} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} EndGlobalSection EndGlobal diff --git a/src/MauiSherpa.Core/Handlers/Profiling/AnalyzeProfilingArtifactHandler.cs b/src/MauiSherpa.Core/Handlers/Profiling/AnalyzeProfilingArtifactHandler.cs new file mode 100644 index 00000000..ffb5e696 --- /dev/null +++ b/src/MauiSherpa.Core/Handlers/Profiling/AnalyzeProfilingArtifactHandler.cs @@ -0,0 +1,24 @@ +using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Models.Profiling; +using MauiSherpa.Core.Requests.Profiling; +using Shiny.Mediator; + +namespace MauiSherpa.Core.Handlers.Profiling; + +public partial class AnalyzeProfilingArtifactHandler : IRequestHandler +{ + private readonly IProfilingArtifactAnalysisService _profilingArtifactAnalysisService; + + public AnalyzeProfilingArtifactHandler(IProfilingArtifactAnalysisService profilingArtifactAnalysisService) + { + _profilingArtifactAnalysisService = profilingArtifactAnalysisService; + } + + public async Task Handle( + AnalyzeProfilingArtifactRequest request, + IMediatorContext context, + CancellationToken ct) + { + return await _profilingArtifactAnalysisService.AnalyzeArtifactAsync(request.ArtifactId, ct); + } +} diff --git a/src/MauiSherpa.Core/Handlers/Profiling/GetProfilingCapabilitiesHandler.cs b/src/MauiSherpa.Core/Handlers/Profiling/GetProfilingCapabilitiesHandler.cs new file mode 100644 index 00000000..38b05e8b --- /dev/null +++ b/src/MauiSherpa.Core/Handlers/Profiling/GetProfilingCapabilitiesHandler.cs @@ -0,0 +1,30 @@ +using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Models.Profiling; +using MauiSherpa.Core.Requests.Profiling; +using Shiny.Mediator; +using Shiny.Mediator.Caching; + +namespace MauiSherpa.Core.Handlers.Profiling; + +/// +/// Handler for retrieving platform-specific profiling capabilities with medium-lived caching. +/// +public partial class GetProfilingCapabilitiesHandler : IRequestHandler +{ + private readonly IProfilingCatalogService _profilingCatalogService; + + public GetProfilingCapabilitiesHandler(IProfilingCatalogService profilingCatalogService) + { + _profilingCatalogService = profilingCatalogService; + } + + [Cache(AbsoluteExpirationSeconds = 300)] + [OfflineAvailable] + public async Task Handle( + GetProfilingCapabilitiesRequest request, + IMediatorContext context, + CancellationToken ct) + { + return await _profilingCatalogService.GetCapabilitiesAsync(request.Platform, ct); + } +} diff --git a/src/MauiSherpa.Core/Handlers/Profiling/GetProfilingCatalogHandler.cs b/src/MauiSherpa.Core/Handlers/Profiling/GetProfilingCatalogHandler.cs new file mode 100644 index 00000000..251b4d95 --- /dev/null +++ b/src/MauiSherpa.Core/Handlers/Profiling/GetProfilingCatalogHandler.cs @@ -0,0 +1,30 @@ +using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Models.Profiling; +using MauiSherpa.Core.Requests.Profiling; +using Shiny.Mediator; +using Shiny.Mediator.Caching; + +namespace MauiSherpa.Core.Handlers.Profiling; + +/// +/// Handler for retrieving the built-in profiling catalog with medium-lived caching. +/// +public partial class GetProfilingCatalogHandler : IRequestHandler +{ + private readonly IProfilingCatalogService _profilingCatalogService; + + public GetProfilingCatalogHandler(IProfilingCatalogService profilingCatalogService) + { + _profilingCatalogService = profilingCatalogService; + } + + [Cache(AbsoluteExpirationSeconds = 300)] + [OfflineAvailable] + public async Task Handle( + GetProfilingCatalogRequest request, + IMediatorContext context, + CancellationToken ct) + { + return await _profilingCatalogService.GetCatalogAsync(ct); + } +} diff --git a/src/MauiSherpa.Core/Handlers/Profiling/GetProfilingPrerequisitesHandler.cs b/src/MauiSherpa.Core/Handlers/Profiling/GetProfilingPrerequisitesHandler.cs new file mode 100644 index 00000000..0e9b112f --- /dev/null +++ b/src/MauiSherpa.Core/Handlers/Profiling/GetProfilingPrerequisitesHandler.cs @@ -0,0 +1,30 @@ +using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Models.Profiling; +using MauiSherpa.Core.Requests.Profiling; +using Shiny.Mediator; +using Shiny.Mediator.Caching; + +namespace MauiSherpa.Core.Handlers.Profiling; + +public partial class GetProfilingPrerequisitesHandler : IRequestHandler +{ + private readonly IProfilingPrerequisitesService _profilingPrerequisitesService; + + public GetProfilingPrerequisitesHandler(IProfilingPrerequisitesService profilingPrerequisitesService) + { + _profilingPrerequisitesService = profilingPrerequisitesService; + } + + [Cache(AbsoluteExpirationSeconds = 120)] + [OfflineAvailable] + public async Task Handle( + GetProfilingPrerequisitesRequest request, + IMediatorContext context, + CancellationToken ct) + { + return await _profilingPrerequisitesService.GetPrerequisitesAsync( + request.Platform, + request.CaptureKinds, + ct: ct); + } +} diff --git a/src/MauiSherpa.Core/Handlers/Profiling/PlanProfilingCaptureHandler.cs b/src/MauiSherpa.Core/Handlers/Profiling/PlanProfilingCaptureHandler.cs new file mode 100644 index 00000000..b1a88216 --- /dev/null +++ b/src/MauiSherpa.Core/Handlers/Profiling/PlanProfilingCaptureHandler.cs @@ -0,0 +1,27 @@ +using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Models.Profiling; +using MauiSherpa.Core.Requests.Profiling; +using Shiny.Mediator; + +namespace MauiSherpa.Core.Handlers.Profiling; + +/// +/// Handler for building validation-friendly profiling capture command plans. +/// +public partial class PlanProfilingCaptureHandler : IRequestHandler +{ + private readonly IProfilingCaptureOrchestrationService _profilingCaptureOrchestrationService; + + public PlanProfilingCaptureHandler(IProfilingCaptureOrchestrationService profilingCaptureOrchestrationService) + { + _profilingCaptureOrchestrationService = profilingCaptureOrchestrationService; + } + + public async Task Handle( + PlanProfilingCaptureRequest request, + IMediatorContext context, + CancellationToken ct) + { + return await _profilingCaptureOrchestrationService.PlanCaptureAsync(request.Definition, request.Options, ct); + } +} diff --git a/src/MauiSherpa.Core/Interfaces.cs b/src/MauiSherpa.Core/Interfaces.cs index d52cbe30..57769316 100644 --- a/src/MauiSherpa.Core/Interfaces.cs +++ b/src/MauiSherpa.Core/Interfaces.cs @@ -1,3 +1,4 @@ +using MauiSherpa.Core.Models.Profiling; using MauiSherpa.Core.Services; namespace MauiSherpa.Core.Interfaces; @@ -217,7 +218,194 @@ public interface IScreenCaptureService Task StopRecordingAsync(CancellationToken ct = default); } +// ============================================================================ +// Profiling +// ============================================================================ +public interface IProfilingCatalogService +{ + Task GetCatalogAsync(CancellationToken ct = default); + Task GetCapabilitiesAsync(ProfilingTargetPlatform platform, CancellationToken ct = default); + ProfilingSessionDefinition CreateSessionDefinition( + ProfilingTarget target, + ProfilingScenarioKind scenario, + string? name = null, + IReadOnlyList? captureKinds = null, + string? appId = null, + TimeSpan? duration = null, + IReadOnlyDictionary? tags = null); + ProfilingSessionValidationResult ValidateSessionDefinition( + ProfilingSessionDefinition definition, + ProfilingPlatformCapabilities capabilities); +} + +public interface IProfilingCapabilityProvider +{ + ProfilingTargetPlatform Platform { get; } + Task GetCapabilitiesAsync(CancellationToken ct = default); +} + +public interface IProfilingPrerequisitesService +{ + Task GetPrerequisitesAsync( + ProfilingTargetPlatform platform, + IReadOnlyList? captureKinds = null, + string? workingDirectory = null, + CancellationToken ct = default); +} + +public interface IProfilingCaptureOrchestrationService +{ + Task PlanCaptureAsync( + ProfilingSessionDefinition definition, + ProfilingCapturePlanOptions? options = null, + CancellationToken ct = default); +} + +public interface IProfilingArtifactLibraryService +{ + Task> GetArtifactsAsync( + ProfilingArtifactLibraryQuery? query = null, + CancellationToken ct = default); + Task GetArtifactAsync(string artifactId, CancellationToken ct = default); + Task SaveArtifactAsync( + ProfilingArtifactLibrarySaveRequest request, + CancellationToken ct = default); + Task DeleteArtifactAsync(string artifactId, bool deleteFile = false, CancellationToken ct = default); + Task GetArtifactPathAsync(string artifactId, CancellationToken ct = default); + string GetDefaultArtifactDirectory(string sessionId); + event Action? OnArtifactsChanged; +} + +public interface IProfilingArtifactAnalysisService +{ + Task AnalyzeArtifactAsync(string artifactId, CancellationToken ct = default); + Task> AnalyzeArtifactsAsync( + ProfilingArtifactLibraryQuery? query = null, + CancellationToken ct = default); +} + +/// +/// Executes a profiling capture plan as a coordinated multi-process pipeline. +/// Handles dependency ordering, parallel execution, long-running processes, +/// graceful stop, and artifact collection. +/// +public interface IProfilingSessionRunner : IDisposable +{ + /// Current pipeline state + ProfilingPipelineState State { get; } + + /// Status of each step in the pipeline + IReadOnlyList Steps { get; } + + /// Fired when pipeline state changes + event EventHandler? PipelineStateChanged; + + /// Fired when a step's state changes + event EventHandler? StepStateChanged; + + /// Fired when a step produces output + event EventHandler? StepOutputReceived; + + /// + /// Execute the full pipeline. Returns when all steps complete (or fail/cancel). + /// + Task RunAsync(ProfilingCapturePlan plan, CancellationToken ct = default); + + /// + /// Gracefully stop capture — sends SIGINT to ManualStop steps, waits for processes + /// to flush output files and exit before returning. + /// + Task StopCaptureAsync(); + + /// + /// Abort everything immediately — kills all running processes. + /// + void Cancel(); + + /// + /// Collect a GC dump on demand while the pipeline is running. + /// Returns the path to the .gcdump file, or null if collection failed. + /// + Task CollectGcDumpAsync(CancellationToken ct = default); + + /// + /// Whether a trace capture is currently active. + /// + bool IsTraceActive { get; } + + /// + /// Start an on-demand trace capture. Returns the step ID or null if it cannot start. + /// The trace runs until StopTraceAsync() is called. + /// + string? StartTraceAsync(); + + /// + /// Stop the currently running on-demand trace. + /// + Task StopTraceAsync(); +} + +/// +/// Parses and reports on GC dump (.gcdump) files by shelling out to dotnet-gcdump. +/// +public interface IGcDumpReportService +{ + /// + /// Parse a .gcdump file and return structured heap statistics. + /// + Task GetReportAsync(string gcdumpPath, CancellationToken ct = default); +} + +/// +/// Converts profiling artifacts between formats (e.g., .nettrace → .speedscope.json). +/// +public interface IProfilingArtifactConverterService +{ + /// + /// Convert a .nettrace file to speedscope JSON format. + /// Returns the path to the converted file, or null on failure. + /// + Task ConvertToSpeedscopeAsync(string nettracePath, CancellationToken ct = default); +} + +/// +/// Manages persistent profiling session storage. Each session is a folder +/// containing a session.json manifest and artifact files. +/// +public interface IProfilingSessionStorageService +{ + /// List all sessions, ordered by most recent first. + Task> GetSessionsAsync(CancellationToken ct = default); + + /// Get a single session by ID. + Task GetSessionAsync(string sessionId, CancellationToken ct = default); + + /// + /// Save (create or update) a session manifest. + /// If the session folder doesn't exist, it is created. + /// + Task SaveSessionAsync(ProfilingSessionManifest manifest, CancellationToken ct = default); + + /// Delete a session and its folder. + Task DeleteSessionAsync(string sessionId, CancellationToken ct = default); + + /// + /// Get the directory path for a session. Creates the directory if it doesn't exist. + /// + string GetSessionDirectoryPath(string sessionId); + + /// Generate a new unique session ID based on project name and date. + string GenerateSessionId(string? projectName = null); + + /// Export a session as a .zip file. + Task ExportSessionAsync(string sessionId, string outputZipPath, CancellationToken ct = default); + + /// + /// Import a session from a .zip file. Returns the imported manifest, or null on failure. + /// + Task ImportSessionAsync(string zipPath, CancellationToken ct = default); +} public interface IAndroidSdkSettingsService { @@ -2115,6 +2303,22 @@ public interface ICopilotToolsService IReadOnlyList ReadOnlyToolNames { get; } } +/// +/// Builds lightweight, structured profiling context for Copilot without requiring raw trace uploads. +/// +public interface IProfilingContextService +{ + /// + /// Gets the currently available local profiling targets discovered from MauiDevFlow. + /// + Task> GetAvailableTargetsAsync(CancellationToken ct = default); + + /// + /// Builds a lightweight profiling snapshot for the requested target. + /// + Task GetSnapshotAsync(ProfilingSnapshotOptions options, CancellationToken ct = default); +} + /// /// Service for coordinating splash screen visibility between MAUI and Blazor /// @@ -2846,6 +3050,7 @@ public record MauiSherpaSettings public PushTestingSettings PushTesting { get; init; } = new(); public List PushProjects { get; init; } = new(); public List PublishProfiles { get; init; } = new(); + public List ProfilingArtifacts { get; init; } = new(); public DateTime LastModified { get; init; } = DateTime.UtcNow; } @@ -3105,7 +3310,7 @@ public interface IKeystoreSyncService Task UploadKeystorePasswordAsync(string keystoreId, string password, CancellationToken ct = default); Task UploadKeystoreMetadataAsync(string keystoreId, CancellationToken ct = default); Task DownloadKeystoreFromCloudAsync(string cloudKey, CancellationToken ct = default); - Task DeleteKeystoreFromCloudAsync(string cloudKey, CancellationToken ct = default); + Task DeleteKeystoreFromCloudAsync(string alias, CancellationToken ct = default); } public record KeystoreSyncStatus( diff --git a/src/MauiSherpa.Core/Models/DevFlow/DevFlowModels.cs b/src/MauiSherpa.Core/Models/DevFlow/DevFlowModels.cs index 225eab37..70604f54 100644 --- a/src/MauiSherpa.Core/Models/DevFlow/DevFlowModels.cs +++ b/src/MauiSherpa.Core/Models/DevFlow/DevFlowModels.cs @@ -158,6 +158,9 @@ public class DevFlowProfilerSample [JsonPropertyName("nativeMemoryBytes")] public long? NativeMemoryBytes { get; set; } + [JsonPropertyName("nativeMemoryKind")] + public string? NativeMemoryKind { get; set; } + [JsonPropertyName("gc0")] public int Gc0 { get; set; } @@ -598,3 +601,133 @@ public class CdpTarget [JsonPropertyName("ready")] public bool Ready { get; set; } } + +// ── Platform Info DTOs ── + +public class DevFlowAppInfo +{ + [JsonPropertyName("name")] public string? Name { get; set; } + [JsonPropertyName("packageName")] public string? PackageName { get; set; } + [JsonPropertyName("version")] public string? Version { get; set; } + [JsonPropertyName("buildNumber")] public string? BuildNumber { get; set; } + [JsonPropertyName("requestedTheme")] public string? RequestedTheme { get; set; } + [JsonPropertyName("requestedLayoutDirection")] public string? RequestedLayoutDirection { get; set; } +} + +public class DevFlowDeviceInfo +{ + [JsonPropertyName("manufacturer")] public string? Manufacturer { get; set; } + [JsonPropertyName("model")] public string? Model { get; set; } + [JsonPropertyName("name")] public string? Name { get; set; } + [JsonPropertyName("platform")] public string? Platform { get; set; } + [JsonPropertyName("idiom")] public string? Idiom { get; set; } + [JsonPropertyName("deviceType")] public string? DeviceType { get; set; } + [JsonPropertyName("osVersion")] public string? OsVersion { get; set; } +} + +public class DevFlowDisplayInfo +{ + [JsonPropertyName("width")] public double Width { get; set; } + [JsonPropertyName("height")] public double Height { get; set; } + [JsonPropertyName("density")] public double Density { get; set; } + [JsonPropertyName("orientation")] public string? Orientation { get; set; } + [JsonPropertyName("rotation")] public string? Rotation { get; set; } + [JsonPropertyName("refreshRate")] public double RefreshRate { get; set; } +} + +public class DevFlowBatteryInfo +{ + [JsonPropertyName("chargeLevel")] public double ChargeLevel { get; set; } + [JsonPropertyName("state")] public string? State { get; set; } + [JsonPropertyName("powerSource")] public string? PowerSource { get; set; } + [JsonPropertyName("energySaverStatus")] public string? EnergySaverStatus { get; set; } +} + +public class DevFlowConnectivityInfo +{ + [JsonPropertyName("networkAccess")] public string? NetworkAccess { get; set; } + [JsonPropertyName("connectionProfiles")] public List? ConnectionProfiles { get; set; } +} + +public class DevFlowVersionTracking +{ + [JsonPropertyName("currentVersion")] public string? CurrentVersion { get; set; } + [JsonPropertyName("currentBuild")] public string? CurrentBuild { get; set; } + [JsonPropertyName("previousVersion")] public string? PreviousVersion { get; set; } + [JsonPropertyName("previousBuild")] public string? PreviousBuild { get; set; } + [JsonPropertyName("firstInstalledVersion")] public string? FirstInstalledVersion { get; set; } + [JsonPropertyName("firstInstalledBuild")] public string? FirstInstalledBuild { get; set; } + [JsonPropertyName("isFirstLaunchEver")] public bool IsFirstLaunchEver { get; set; } + [JsonPropertyName("isFirstLaunchForCurrentVersion")] public bool IsFirstLaunchForCurrentVersion { get; set; } + [JsonPropertyName("isFirstLaunchForCurrentBuild")] public bool IsFirstLaunchForCurrentBuild { get; set; } + [JsonPropertyName("versionHistory")] public List? VersionHistory { get; set; } + [JsonPropertyName("buildHistory")] public List? BuildHistory { get; set; } +} + +public class DevFlowPermissionStatus +{ + [JsonPropertyName("permission")] public string Permission { get; set; } = string.Empty; + [JsonPropertyName("status")] public string Status { get; set; } = string.Empty; +} + +public class DevFlowPermissionsResponse +{ + [JsonPropertyName("permissions")] public List? Permissions { get; set; } +} + +public class DevFlowGeolocation +{ + [JsonPropertyName("latitude")] public double Latitude { get; set; } + [JsonPropertyName("longitude")] public double Longitude { get; set; } + [JsonPropertyName("altitude")] public double? Altitude { get; set; } + [JsonPropertyName("accuracy")] public double? Accuracy { get; set; } + [JsonPropertyName("speed")] public double? Speed { get; set; } + [JsonPropertyName("course")] public double? Course { get; set; } + [JsonPropertyName("timestamp")] public DateTimeOffset Timestamp { get; set; } + [JsonPropertyName("isFromMockProvider")] public bool IsFromMockProvider { get; set; } +} + +// ── Sensor DTOs ── + +public class DevFlowSensorStatus +{ + [JsonPropertyName("sensor")] public string Sensor { get; set; } = string.Empty; + [JsonPropertyName("active")] public bool Active { get; set; } + [JsonPropertyName("supported")] public bool Supported { get; set; } + [JsonPropertyName("subscribers")] public int Subscribers { get; set; } +} + +public class DevFlowSensorReading +{ + [JsonPropertyName("sensor")] public string Sensor { get; set; } = string.Empty; + [JsonPropertyName("timestamp")] public string? Timestamp { get; set; } + [JsonPropertyName("data")] public JsonElement Data { get; set; } +} + +// ── Storage DTOs ── + +public class DevFlowPreferenceEntry +{ + [JsonPropertyName("key")] public string Key { get; set; } = string.Empty; + [JsonPropertyName("value")] public string? Value { get; set; } + [JsonPropertyName("sharedName")] public string? SharedName { get; set; } +} + +public class DevFlowPreferencesListResponse +{ + [JsonPropertyName("keys")] public List? Keys { get; set; } +} + +public class DevFlowPreferenceSetRequest +{ + [JsonPropertyName("value")] public object? Value { get; set; } + [JsonPropertyName("type")] public string? Type { get; set; } + [JsonPropertyName("sharedName")] public string? SharedName { get; set; } +} + +public class DevFlowSecureStorageEntry +{ + [JsonPropertyName("key")] public string Key { get; set; } = string.Empty; + [JsonPropertyName("value")] public string? Value { get; set; } + [JsonPropertyName("exists")] public bool Exists { get; set; } +} diff --git a/src/MauiSherpa.Core/Models/Profiling/ProfilingAnalysisModels.cs b/src/MauiSherpa.Core/Models/Profiling/ProfilingAnalysisModels.cs new file mode 100644 index 00000000..91284565 --- /dev/null +++ b/src/MauiSherpa.Core/Models/Profiling/ProfilingAnalysisModels.cs @@ -0,0 +1,69 @@ +namespace MauiSherpa.Core.Models.Profiling; + +public enum ProfilingAnalysisKind +{ + Metadata, + Speedscope, + Logs, + Json, + GcDump +} + +public enum ProfilingAnalysisInsightSeverity +{ + Info, + Warning, + Critical +} + +public record ProfilingArtifactAnalysisResult( + ProfilingArtifactAnalysis? Analysis, + string? Message = null +); + +public record ProfilingArtifactAnalysis( + ProfilingArtifactMetadata Artifact, + string? ArtifactPath, + bool ArtifactExists, + ProfilingAnalysisKind Kind, + string Summary, + IReadOnlyList Metrics, + IReadOnlyList Hotspots, + IReadOnlyList Insights, + IReadOnlyList Notes +); + +public record ProfilingAnalysisMetric( + string Key, + string Label, + string Value, + double? NumericValue = null, + string? Unit = null +); + +public record ProfilingAnalysisHotspot( + string Name, + string Value, + double PercentOfTrace, + string? Description = null +); + +public record ProfilingAnalysisInsight( + ProfilingAnalysisInsightSeverity Severity, + string Title, + string Description +); + +// GC dump report data +public record GcDumpReport( + IReadOnlyList Types, + long TotalSize, + long TotalCount, + string? RawOutput = null +); + +public record GcDumpTypeEntry( + string TypeName, + long Count, + long Size +); diff --git a/src/MauiSherpa.Core/Models/Profiling/ProfilingArtifactLibraryModels.cs b/src/MauiSherpa.Core/Models/Profiling/ProfilingArtifactLibraryModels.cs new file mode 100644 index 00000000..1ed2dc77 --- /dev/null +++ b/src/MauiSherpa.Core/Models/Profiling/ProfilingArtifactLibraryModels.cs @@ -0,0 +1,21 @@ +namespace MauiSherpa.Core.Models.Profiling; + +public record ProfilingArtifactLibraryEntry( + ProfilingArtifactMetadata Metadata, + bool IsManagedPath, + string? SourcePath, + DateTimeOffset AddedAt, + DateTimeOffset UpdatedAt +); + +public record ProfilingArtifactLibraryQuery( + string? SessionId = null, + ProfilingArtifactKind? Kind = null, + bool IncludeMissing = true +); + +public record ProfilingArtifactLibrarySaveRequest( + ProfilingArtifactMetadata Metadata, + string? ArtifactPath = null, + bool CopyToLibrary = false +); diff --git a/src/MauiSherpa.Core/Models/Profiling/ProfilingCaptureOrchestrationModels.cs b/src/MauiSherpa.Core/Models/Profiling/ProfilingCaptureOrchestrationModels.cs new file mode 100644 index 00000000..e0315748 --- /dev/null +++ b/src/MauiSherpa.Core/Models/Profiling/ProfilingCaptureOrchestrationModels.cs @@ -0,0 +1,134 @@ +using MauiSherpa.Core.Interfaces; + +namespace MauiSherpa.Core.Models.Profiling; + +public enum ProfilingCaptureLaunchMode +{ + Launch, + Attach +} + +public enum ProfilingCommandStepKind +{ + Prepare, + Build, + Launch, + DiscoverProcess, + Connect, + Capture, + CollectArtifacts, + Cleanup +} + +public enum ProfilingDiagnosticListenMode +{ + Connect, + Listen +} + +public enum ProfilingDsRouterMode +{ + None, + ServerServer, + ServerClient +} + +public enum ProfilingStopTrigger +{ + None, // Step exits on its own + ManualStop, // User must explicitly stop it + OnPipelineStop // Stopped when the pipeline stops +} + +public record ProfilingCapturePlanOptions( + string? ProjectPath = null, + string Configuration = "Release", + string? TargetFramework = null, + string? WorkingDirectory = null, + string? OutputDirectory = null, + ProfilingCaptureLaunchMode LaunchMode = ProfilingCaptureLaunchMode.Launch, + int DiagnosticPort = 9000, + bool SuspendAtStartup = false, + int? ProcessId = null, + IReadOnlyDictionary? AdditionalBuildProperties = null +); + +public record ProfilingPlanValidation( + IReadOnlyList Errors, + IReadOnlyList Warnings +) +{ + public bool IsValid => Errors.Count == 0; +} + +public record ProfilingDiagnosticConfiguration( + string Address, + int Port, + ProfilingDiagnosticListenMode ListenMode, + bool SuspendOnStartup, + bool RequiresDsRouter, + ProfilingDsRouterMode DsRouterMode, + string? DsRouterPortForwardPlatform, + string IpcAddress, + string TcpEndpoint +); + +public record ProfilingRuntimeBinding( + string Token, + string Description, + bool IsRequired = true, + string? ExampleValue = null +); + +public record ProfilingCommandStep( + string Id, + ProfilingCommandStepKind Kind, + string DisplayName, + string Description, + string Command, + IReadOnlyList Arguments, + string? WorkingDirectory = null, + IReadOnlyDictionary? Environment = null, + IReadOnlyList? DependsOn = null, + IReadOnlyList? RequiredRuntimeBindings = null, + IReadOnlyDictionary? Metadata = null, + bool IsOptional = false, + bool IsLongRunning = false, + bool RequiresManualStop = false, + bool CanRunParallel = false, + ProfilingStopTrigger StopTrigger = ProfilingStopTrigger.None, + string? ReadyOutputPattern = null) +{ + public string CommandLine => Arguments.Count > 0 + ? $"{Command} {string.Join(" ", Arguments)}" + : Command; + + public ProcessRequest ToProcessRequest(string? title = null, string? description = null) => new( + Command, + Arguments.ToArray(), + WorkingDirectory, + Environment: Environment?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value, StringComparer.OrdinalIgnoreCase), + Title: title ?? DisplayName, + Description: description ?? Description); +} + +public record ProfilingCapturePlan( + ProfilingSessionDefinition Session, + ProfilingPlatformCapabilities Capabilities, + ProfilingCapturePlanOptions Options, + string HostPlatform, + string TargetFramework, + string OutputDirectory, + string? WorkingDirectory, + bool IsTargetCurrentlyAvailable, + ProfilingDiagnosticConfiguration? Diagnostics, + ProfilingPrerequisiteReport? Prerequisites, + ProfilingPlanValidation Validation, + IReadOnlyList RuntimeBindings, + IReadOnlyList Commands, + IReadOnlyList ExpectedArtifacts, + IReadOnlyDictionary Metadata) +{ + public bool RequiresRuntimeInputs => RuntimeBindings.Any(binding => binding.IsRequired); + public bool CanExecute => Validation.IsValid && !RequiresRuntimeInputs; +} diff --git a/src/MauiSherpa.Core/Models/Profiling/ProfilingCatalogModels.cs b/src/MauiSherpa.Core/Models/Profiling/ProfilingCatalogModels.cs new file mode 100644 index 00000000..bb1d7bec --- /dev/null +++ b/src/MauiSherpa.Core/Models/Profiling/ProfilingCatalogModels.cs @@ -0,0 +1,120 @@ +namespace MauiSherpa.Core.Models.Profiling; + +public enum ProfilingTargetPlatform +{ + Android, + iOS, + MacCatalyst, + MacOS, + Windows +} + +public enum ProfilingTargetKind +{ + PhysicalDevice, + Emulator, + Simulator, + Desktop +} + +public enum ProfilingCaptureKind +{ + Startup, + Cpu, + Memory, + Network, + Rendering, + Energy, + SystemTrace, + Logs +} + +public enum ProfilingScenarioKind +{ + Launch, + Interaction, + Scrolling, + BackgroundWork, + MemoryInvestigation +} + +public enum ProfilingArtifactKind +{ + Trace, + Metrics, + Screenshot, + Logs, + Report, + Export, + GcDump, + Log, + Other +} + +public record ProfilingTarget( + ProfilingTargetPlatform Platform, + ProfilingTargetKind Kind, + string Identifier, + string DisplayName, + string? OperatingSystemVersion = null, + string? Model = null +); + +public record ProfilingScenarioDefinition( + ProfilingScenarioKind Kind, + string DisplayName, + string Description, + IReadOnlyList DefaultCaptureKinds, + TimeSpan SuggestedDuration, + bool SupportsContinuousCapture = false +); + +public record ProfilingPlatformCapabilities( + ProfilingTargetPlatform Platform, + string DisplayName, + IReadOnlyList SupportedTargetKinds, + IReadOnlyList SupportedCaptureKinds, + IReadOnlyList SupportedArtifactKinds, + IReadOnlyList SupportedScenarios, + bool SupportsLaunchProfiling, + bool SupportsAttachToProcess, + bool SupportsLiveMetrics, + bool SupportsSymbolication, + string? Notes = null +); + +public record ProfilingSessionDefinition( + string Id, + string Name, + ProfilingTarget Target, + ProfilingScenarioKind Scenario, + IReadOnlyList CaptureKinds, + string? AppId = null, + TimeSpan? Duration = null, + IReadOnlyDictionary? Tags = null, + DateTimeOffset CreatedAt = default +); + +public record ProfilingSessionValidationResult( + bool IsValid, + IReadOnlyList Errors, + IReadOnlyList UnsupportedCaptureKinds +); + +public record ProfilingArtifactMetadata( + string Id, + string SessionId, + ProfilingArtifactKind Kind, + string DisplayName, + string FileName, + string? RelativePath, + string ContentType, + DateTimeOffset CreatedAt, + long? SizeBytes = null, + IReadOnlyDictionary? Properties = null +); + +public record ProfilingCatalog( + IReadOnlyList Platforms, + IReadOnlyList Scenarios +); diff --git a/src/MauiSherpa.Core/Models/Profiling/ProfilingModels.cs b/src/MauiSherpa.Core/Models/Profiling/ProfilingModels.cs new file mode 100644 index 00000000..05c440ba --- /dev/null +++ b/src/MauiSherpa.Core/Models/Profiling/ProfilingModels.cs @@ -0,0 +1,82 @@ +namespace MauiSherpa.Core.Models.Profiling; + +public record ProfilingTargetInfo( + string Id, + string Host, + int Port, + string DisplayName, + string? AppName, + string? Platform, + string? Project, + string? Tfm, + DateTimeOffset? ConnectedAt, + bool Running, + string DiscoveryMethod +); + +public record ProfilingSnapshotOptions( + string? TargetId = null, + int NetworkSampleSize = 40, + bool IncludeNetworkSummary = true, + bool IncludeVisualTreeSummary = true +); + +public record ProfilingSnapshotResult( + ProfilingSnapshot? Snapshot, + string? Message = null +); + +public record ProfilingSnapshot( + ProfilingTargetInfo Target, + ProfilingRuntimeInfo Runtime, + ProfilingNetworkSummary? Network, + ProfilingVisualTreeSummary? VisualTree, + IReadOnlyList Notes +); + +public record ProfilingRuntimeInfo( + string? Agent, + string? Version, + string? Platform, + string? DeviceType, + string? Idiom, + string? AppName, + bool Running, + bool CdpReady, + int CdpWebViewCount +); + +public record ProfilingNetworkSummary( + int SampleSize, + int SuccessCount, + int FailureCount, + double AverageDurationMs, + double P95DurationMs, + long MaxDurationMs, + long TotalRequestBytes, + long TotalResponseBytes, + IReadOnlyList SlowestRequests +); + +public record ProfilingRequestSummary( + string Method, + string Url, + int? StatusCode, + long DurationMs, + string? Error +); + +public record ProfilingVisualTreeSummary( + int RootCount, + int TotalElementCount, + int VisibleElementCount, + int FocusedElementCount, + int InteractiveElementCount, + int MaxDepth, + IReadOnlyList TopElementTypes +); + +public record ProfilingElementTypeCount( + string Type, + int Count +); diff --git a/src/MauiSherpa.Core/Models/Profiling/ProfilingPipelineModels.cs b/src/MauiSherpa.Core/Models/Profiling/ProfilingPipelineModels.cs new file mode 100644 index 00000000..824b4640 --- /dev/null +++ b/src/MauiSherpa.Core/Models/Profiling/ProfilingPipelineModels.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; + +namespace MauiSherpa.Core.Models.Profiling; + +public enum ProfilingPipelineState +{ + NotStarted, + Running, + WaitingForStop, + Completing, + Completed, + Failed, + Cancelled +} + +public enum ProfilingStepState +{ + Pending, + WaitingForDependencies, + Running, + Completed, + Failed, + Stopped, + Skipped, + Cancelled +} + +public class ProfilingStepStatus +{ + public required string StepId { get; init; } + public required string DisplayName { get; init; } + public required ProfilingCommandStepKind Kind { get; init; } + public ProfilingStepState State { get; set; } = ProfilingStepState.Pending; + public List OutputLines { get; } = new(); + public TimeSpan? Duration { get; set; } + public int? ExitCode { get; set; } + public int? ProcessId { get; set; } + public string? ErrorMessage { get; set; } + public DateTime? StartedAt { get; set; } + public DateTime? CompletedAt { get; set; } + public bool IsLongRunning { get; init; } + public bool CanRunParallel { get; init; } + public ProfilingStopTrigger StopTrigger { get; init; } + + /// + /// True when a long-running step has established its connection and is + /// actively capturing. Used to gate dependent non-long-running steps + /// (e.g. gcdump waits for trace to connect before running). + /// + public bool IsReady { get; set; } +} + +public record ProfilingStepOutputLine(string Text, bool IsError, DateTime Timestamp); + +public record ProfilingPipelineResult( + bool Success, + TimeSpan TotalDuration, + ProfilingPipelineState FinalState, + IReadOnlyList StepResults, + IReadOnlyList ArtifactPaths, + IReadOnlyList MissingArtifacts); + +public class ProfilingPipelineStateChangedEventArgs : EventArgs +{ + public ProfilingPipelineState OldState { get; init; } + public ProfilingPipelineState NewState { get; init; } +} + +public class ProfilingStepStateChangedEventArgs : EventArgs +{ + public required string StepId { get; init; } + public ProfilingStepState OldState { get; init; } + public ProfilingStepState NewState { get; init; } +} + +public class ProfilingStepOutputEventArgs : EventArgs +{ + public required string StepId { get; init; } + public required string Text { get; init; } + public bool IsError { get; init; } +} diff --git a/src/MauiSherpa.Core/Models/Profiling/ProfilingPrerequisitesModels.cs b/src/MauiSherpa.Core/Models/Profiling/ProfilingPrerequisitesModels.cs new file mode 100644 index 00000000..90c6b456 --- /dev/null +++ b/src/MauiSherpa.Core/Models/Profiling/ProfilingPrerequisitesModels.cs @@ -0,0 +1,55 @@ +using MauiSherpa.Core.Interfaces; + +namespace MauiSherpa.Core.Models.Profiling; + +public enum ProfilingPrerequisiteKind +{ + HostPlatform, + DotNetSdk, + DotNetTool, + AndroidToolchain, + AppleToolchain, + WindowsToolchain, + Other +} + +public record ProfilingPrerequisiteContext( + ProfilingTargetPlatform Platform, + IReadOnlyList CaptureKinds, + string? WorkingDirectory, + string? DotNetExecutablePath, + DoctorContext DoctorContext +); + +public record ProfilingPrerequisiteStatus( + string Name, + ProfilingPrerequisiteKind Kind, + DependencyStatusType Status, + bool IsRequired, + string? RequiredVersion, + string? RecommendedVersion, + string? InstalledVersion, + string? Message, + string? DiscoveredBy = null, + string? ExecutablePath = null, + bool IsFixable = false, + string? FixAction = null, + string? SuggestedCommand = null +); + +public record ProfilingPrerequisiteReport( + ProfilingPrerequisiteContext Context, + IReadOnlyList Checks, + DateTimeOffset Timestamp +) +{ + public bool IsReady => Checks + .Where(check => check.IsRequired) + .All(check => check.Status != DependencyStatusType.Error); + + public bool HasErrors => Checks.Any(check => check.Status == DependencyStatusType.Error); + public bool HasWarnings => Checks.Any(check => check.Status == DependencyStatusType.Warning); + public int OkCount => Checks.Count(check => check.Status == DependencyStatusType.Ok || check.Status == DependencyStatusType.Info); + public int WarningCount => Checks.Count(check => check.Status == DependencyStatusType.Warning); + public int ErrorCount => Checks.Count(check => check.Status == DependencyStatusType.Error); +} diff --git a/src/MauiSherpa.Core/Models/Profiling/ProfilingSessionManifestModels.cs b/src/MauiSherpa.Core/Models/Profiling/ProfilingSessionManifestModels.cs new file mode 100644 index 00000000..2aa9397e --- /dev/null +++ b/src/MauiSherpa.Core/Models/Profiling/ProfilingSessionManifestModels.cs @@ -0,0 +1,115 @@ +using System.Text.Json.Serialization; + +namespace MauiSherpa.Core.Models.Profiling; + +/// +/// Status of a profiling session. +/// +public enum ProfilingSessionStatus +{ + InProgress, + Completed, + Failed, + Cancelled +} + +/// +/// Persistent manifest for a profiling session, stored as session.json in the session folder. +/// Contains all metadata needed to understand what was captured and how. +/// +public record ProfilingSessionManifest +{ + public required string Id { get; init; } + public required string Name { get; init; } + public ProfilingSessionStatus Status { get; set; } = ProfilingSessionStatus.InProgress; + public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow; + public DateTimeOffset? CompletedAt { get; set; } + + /// Target device/simulator/desktop info. + public required ProfilingSessionTarget Target { get; init; } + + /// Project info (null if attach mode). + public ProfilingSessionProject? Project { get; init; } + + /// Capture kinds selected for this session. + public required IReadOnlyList CaptureKinds { get; init; } + + /// Capture options used. + public required ProfilingSessionOptions Options { get; init; } + + /// Pipeline execution summary (populated after capture). + public ProfilingSessionPipelineSummary? Pipeline { get; set; } + + /// Artifact files produced by this session. + public List Artifacts { get; set; } = new(); + + /// Path to the session folder on disk. + [JsonIgnore] + public string? DirectoryPath { get; set; } +} + +/// +/// Target device/emulator/desktop for the profiling session. +/// +public record ProfilingSessionTarget +{ + public required ProfilingTargetPlatform Platform { get; init; } + public required ProfilingTargetKind Kind { get; init; } + public required string Identifier { get; init; } + public required string DisplayName { get; init; } +} + +/// +/// Project info for the profiling session (when using Launch mode). +/// +public record ProfilingSessionProject +{ + public required string Path { get; init; } + public required string Name { get; init; } + public required string Configuration { get; init; } + public string? TargetFramework { get; init; } +} + +/// +/// Options used for the profiling session. +/// +public record ProfilingSessionOptions +{ + public ProfilingCaptureLaunchMode LaunchMode { get; init; } = ProfilingCaptureLaunchMode.Launch; + public int DiagnosticPort { get; init; } = 9000; + public bool SuspendAtStartup { get; init; } + public int? ProcessId { get; init; } + public ProfilingScenarioKind Scenario { get; init; } = ProfilingScenarioKind.Launch; +} + +/// +/// Summary of the pipeline execution results. +/// +public record ProfilingSessionPipelineSummary +{ + public bool Success { get; init; } + public TimeSpan Duration { get; init; } + public List Steps { get; init; } = new(); +} + +/// +/// Summary of a single pipeline step. +/// +public record ProfilingSessionStepSummary +{ + public required string Id { get; init; } + public required string DisplayName { get; init; } + public required string State { get; init; } + public string? CommandLine { get; init; } +} + +/// +/// An artifact file produced by the session. +/// +public record ProfilingSessionArtifact +{ + public required string FileName { get; init; } + public required ProfilingArtifactKind Kind { get; init; } + public long? SizeBytes { get; init; } + public string? DisplayName { get; init; } +} diff --git a/src/MauiSherpa.Core/Requests/Profiling/AnalyzeProfilingArtifactRequest.cs b/src/MauiSherpa.Core/Requests/Profiling/AnalyzeProfilingArtifactRequest.cs new file mode 100644 index 00000000..ee107b0a --- /dev/null +++ b/src/MauiSherpa.Core/Requests/Profiling/AnalyzeProfilingArtifactRequest.cs @@ -0,0 +1,6 @@ +using MauiSherpa.Core.Models.Profiling; +using Shiny.Mediator; + +namespace MauiSherpa.Core.Requests.Profiling; + +public record AnalyzeProfilingArtifactRequest(string ArtifactId) : IRequest; diff --git a/src/MauiSherpa.Core/Requests/Profiling/GetProfilingCapabilitiesRequest.cs b/src/MauiSherpa.Core/Requests/Profiling/GetProfilingCapabilitiesRequest.cs new file mode 100644 index 00000000..9dade264 --- /dev/null +++ b/src/MauiSherpa.Core/Requests/Profiling/GetProfilingCapabilitiesRequest.cs @@ -0,0 +1,10 @@ +using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Models.Profiling; +using Shiny.Mediator; + +namespace MauiSherpa.Core.Requests.Profiling; + +public record GetProfilingCapabilitiesRequest(ProfilingTargetPlatform Platform) : IRequest, IContractKey +{ + public string GetKey() => $"profiling:capabilities:{Platform}"; +} diff --git a/src/MauiSherpa.Core/Requests/Profiling/GetProfilingCatalogRequest.cs b/src/MauiSherpa.Core/Requests/Profiling/GetProfilingCatalogRequest.cs new file mode 100644 index 00000000..1ada726b --- /dev/null +++ b/src/MauiSherpa.Core/Requests/Profiling/GetProfilingCatalogRequest.cs @@ -0,0 +1,10 @@ +using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Models.Profiling; +using Shiny.Mediator; + +namespace MauiSherpa.Core.Requests.Profiling; + +public record GetProfilingCatalogRequest : IRequest, IContractKey +{ + public string GetKey() => "profiling:catalog"; +} diff --git a/src/MauiSherpa.Core/Requests/Profiling/GetProfilingPrerequisitesRequest.cs b/src/MauiSherpa.Core/Requests/Profiling/GetProfilingPrerequisitesRequest.cs new file mode 100644 index 00000000..30cbf110 --- /dev/null +++ b/src/MauiSherpa.Core/Requests/Profiling/GetProfilingPrerequisitesRequest.cs @@ -0,0 +1,19 @@ +using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Models.Profiling; +using Shiny.Mediator; + +namespace MauiSherpa.Core.Requests.Profiling; + +public record GetProfilingPrerequisitesRequest( + ProfilingTargetPlatform Platform, + IReadOnlyList? CaptureKinds = null) : IRequest, IContractKey +{ + public string GetKey() + { + var normalizedCaptureKinds = CaptureKinds is { Count: > 0 } + ? string.Join(",", CaptureKinds.Distinct().OrderBy(kind => kind)) + : "default"; + + return $"profiling:prerequisites:{Platform}:{normalizedCaptureKinds}"; + } +} diff --git a/src/MauiSherpa.Core/Requests/Profiling/PlanProfilingCaptureRequest.cs b/src/MauiSherpa.Core/Requests/Profiling/PlanProfilingCaptureRequest.cs new file mode 100644 index 00000000..d3a6207b --- /dev/null +++ b/src/MauiSherpa.Core/Requests/Profiling/PlanProfilingCaptureRequest.cs @@ -0,0 +1,8 @@ +using MauiSherpa.Core.Models.Profiling; +using Shiny.Mediator; + +namespace MauiSherpa.Core.Requests.Profiling; + +public record PlanProfilingCaptureRequest( + ProfilingSessionDefinition Definition, + ProfilingCapturePlanOptions? Options = null) : IRequest; diff --git a/src/MauiSherpa.Core/Services/CopilotToolsService.cs b/src/MauiSherpa.Core/Services/CopilotToolsService.cs index c567c531..6b1ae673 100644 --- a/src/MauiSherpa.Core/Services/CopilotToolsService.cs +++ b/src/MauiSherpa.Core/Services/CopilotToolsService.cs @@ -2,11 +2,12 @@ using System.Text.Json; using Microsoft.Extensions.AI; using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Models.Profiling; namespace MauiSherpa.Core.Services; /// -/// Provides Copilot SDK tool definitions for Apple Developer and Android SDK operations +/// Provides Copilot SDK tool definitions for Apple Developer, Android SDK, and profiling operations /// public class CopilotToolsService : ICopilotToolsService { @@ -14,6 +15,10 @@ public class CopilotToolsService : ICopilotToolsService private readonly IAppleIdentityStateService _identityState; private readonly IAppleIdentityService _identityService; private readonly IAndroidSdkService _androidService; + private readonly IProfilingCatalogService _profilingCatalogService; + private readonly IProfilingArtifactLibraryService _profilingArtifactLibraryService; + private readonly IProfilingArtifactAnalysisService _profilingArtifactAnalysisService; + private readonly IProfilingContextService _profilingContextService; private readonly ILoggingService _logger; private readonly List _tools = new(); @@ -24,12 +29,20 @@ public CopilotToolsService( IAppleIdentityStateService identityState, IAppleIdentityService identityService, IAndroidSdkService androidService, + IProfilingCatalogService profilingCatalogService, + IProfilingArtifactLibraryService profilingArtifactLibraryService, + IProfilingArtifactAnalysisService profilingArtifactAnalysisService, + IProfilingContextService profilingContextService, ILoggingService logger) { _appleService = appleService; _identityState = identityState; _identityService = identityService; _androidService = androidService; + _profilingCatalogService = profilingCatalogService; + _profilingArtifactLibraryService = profilingArtifactLibraryService; + _profilingArtifactAnalysisService = profilingArtifactAnalysisService; + _profilingContextService = profilingContextService; _logger = logger; InitializeTools(); @@ -143,6 +156,18 @@ private void InitializeTools() "List available Android system images for creating emulators."), isReadOnly: true); AddTool(AIFunctionFactory.Create(ListDeviceDefinitionsAsync, "list_device_definitions", "List available device definitions for creating emulators."), isReadOnly: true); + + // Profiling Tools + AddTool(AIFunctionFactory.Create(ListProfilingTargetsAsync, "list_profiling_targets", + "List currently available local profiling targets discovered via MauiDevFlow."), isReadOnly: true); + AddTool(AIFunctionFactory.Create(GetProfilingCatalogAsync, "get_profiling_catalog", + "Get the supported profiling scenarios and platform capabilities available in Maui Sherpa. Optionally filter to a single platform."), isReadOnly: true); + AddTool(AIFunctionFactory.Create(ListProfilingArtifactsAsync, "list_profiling_artifacts", + "List profiling artifacts stored in Sherpa's artifact library. Optionally filter by session or artifact kind."), isReadOnly: true); + AddTool(AIFunctionFactory.Create(GetProfilingSnapshotAsync, "get_profiling_snapshot", + "Get a lightweight profiling snapshot for a running MAUI app using local status, network, and visual-tree summaries instead of raw trace uploads."), isReadOnly: true); + AddTool(AIFunctionFactory.Create(AnalyzeProfilingArtifactAsync, "analyze_profiling_artifact", + "Analyze a captured profiling artifact from Sherpa's artifact library and return a portable summary with hotspots, metrics, and insights."), isReadOnly: true); } private string? CheckIdentitySelected() @@ -1516,4 +1541,142 @@ private async Task ListDeviceDefinitionsAsync( } #endregion + + #region Profiling Tools + + [Description("List currently available local profiling targets discovered via MauiDevFlow")] + private async Task ListProfilingTargetsAsync() + { + _logger.LogDebug("Tool: list_profiling_targets called"); + + var targets = await _profilingContextService.GetAvailableTargetsAsync(); + if (targets.Count == 0) + { + return "No local profiling targets are available. Start a MAUI app with MauiDevFlow enabled, then try again."; + } + + return JsonSerializer.Serialize(targets, new JsonSerializerOptions { WriteIndented = true }); + } + + [Description("Get supported profiling scenarios and platform capabilities")] + private async Task GetProfilingCatalogAsync( + [Description("Optional platform name to filter to: Android, iOS, MacCatalyst, MacOS, or Windows")] string? platform = null) + { + _logger.LogDebug($"Tool: get_profiling_catalog called with platform '{platform ?? ""}'"); + + var catalog = await _profilingCatalogService.GetCatalogAsync(); + if (string.IsNullOrWhiteSpace(platform)) + { + return JsonSerializer.Serialize(catalog, new JsonSerializerOptions { WriteIndented = true }); + } + + if (!Enum.TryParse(platform, ignoreCase: true, out var parsedPlatform)) + { + return $"Unknown platform '{platform}'. Valid values: {string.Join(", ", Enum.GetNames())}."; + } + + var capabilities = await _profilingCatalogService.GetCapabilitiesAsync(parsedPlatform); + var result = new + { + Platform = capabilities, + Scenarios = catalog.Scenarios + .Where(scenario => capabilities.SupportedScenarios.Contains(scenario.Kind)) + .ToArray() + }; + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } + + [Description("Get a lightweight profiling snapshot for a running MAUI app")] + private async Task GetProfilingSnapshotAsync( + [Description("Optional target ID or app name from list_profiling_targets")] string? targetId = null, + [Description("Maximum number of recent network requests to summarize (5-200)")] int networkSampleSize = 40, + [Description("Include a summary of current captured network traffic")] bool includeNetworkSummary = true, + [Description("Include a summary of the current MAUI visual tree")] bool includeVisualTreeSummary = true) + { + _logger.LogDebug($"Tool: get_profiling_snapshot called for target '{targetId ?? ""}'"); + + var result = await _profilingContextService.GetSnapshotAsync( + new ProfilingSnapshotOptions( + TargetId: targetId, + NetworkSampleSize: networkSampleSize, + IncludeNetworkSummary: includeNetworkSummary, + IncludeVisualTreeSummary: includeVisualTreeSummary)); + + if (result.Snapshot == null) + { + return result.Message ?? "Unable to build a profiling snapshot."; + } + + return JsonSerializer.Serialize(result.Snapshot, new JsonSerializerOptions { WriteIndented = true }); + } + + [Description("List captured profiling artifacts stored in Sherpa's artifact library")] + private async Task ListProfilingArtifactsAsync( + [Description("Optional profiling session id to filter to")] string? sessionId = null, + [Description("Optional artifact kind filter: Trace, Metrics, Screenshot, Logs, Report, or Export")] string? kind = null, + [Description("Include artifacts whose backing file is missing")] bool includeMissing = false) + { + _logger.LogDebug($"Tool: list_profiling_artifacts called for session '{sessionId ?? ""}' and kind '{kind ?? ""}'"); + + ProfilingArtifactKind? parsedKind = null; + if (!string.IsNullOrWhiteSpace(kind)) + { + if (!Enum.TryParse(kind, ignoreCase: true, out var parsed)) + { + return $"Unknown artifact kind '{kind}'. Valid values: {string.Join(", ", Enum.GetNames())}."; + } + + parsedKind = parsed; + } + + var artifacts = await _profilingArtifactLibraryService.GetArtifactsAsync( + new ProfilingArtifactLibraryQuery( + SessionId: sessionId, + Kind: parsedKind, + IncludeMissing: includeMissing)); + + if (artifacts.Count == 0) + { + return "No profiling artifacts matched the requested filters."; + } + + var result = new List(artifacts.Count); + foreach (var artifact in artifacts) + { + var artifactPath = await _profilingArtifactLibraryService.GetArtifactPathAsync(artifact.Metadata.Id); + result.Add(new + { + artifact.Metadata.Id, + artifact.Metadata.SessionId, + artifact.Metadata.Kind, + artifact.Metadata.DisplayName, + artifact.Metadata.FileName, + artifact.Metadata.CreatedAt, + artifact.Metadata.SizeBytes, + artifact.Metadata.Properties, + artifact.IsManagedPath, + ArtifactExists = !string.IsNullOrWhiteSpace(artifactPath) && File.Exists(artifactPath) + }); + } + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } + + [Description("Analyze a captured profiling artifact from Sherpa's artifact library")] + private async Task AnalyzeProfilingArtifactAsync( + [Description("Artifact id returned by list_profiling_artifacts")] string artifactId) + { + _logger.LogDebug($"Tool: analyze_profiling_artifact called for artifact '{artifactId}'"); + + var result = await _profilingArtifactAnalysisService.AnalyzeArtifactAsync(artifactId); + if (result.Analysis is null) + { + return result.Message ?? $"Profiling artifact '{artifactId}' could not be analyzed."; + } + + return JsonSerializer.Serialize(result.Analysis, new JsonSerializerOptions { WriteIndented = true }); + } + + #endregion } diff --git a/src/MauiSherpa.Core/Services/DevFlowAgentClient.cs b/src/MauiSherpa.Core/Services/DevFlowAgentClient.cs index c76cd06d..18a174eb 100644 --- a/src/MauiSherpa.Core/Services/DevFlowAgentClient.cs +++ b/src/MauiSherpa.Core/Services/DevFlowAgentClient.cs @@ -15,6 +15,7 @@ public class DevFlowAgentClient : IDisposable private CancellationTokenSource? _networkWsCts; private ClientWebSocket? _logsWs; private CancellationTokenSource? _logsWsCts; + private readonly Dictionary _sensorStreams = new(); private bool _disposed; public string AgentHost { get; } @@ -427,6 +428,213 @@ public void StopLogStream() _logsWsCts?.Cancel(); } + // --- Platform Info --- + + public async Task GetAppInfoAsync(CancellationToken ct = default) + => await GetAsync("/api/platform/app-info", ct); + + public async Task GetDeviceInfoAsync(CancellationToken ct = default) + => await GetAsync("/api/platform/device-info", ct); + + public async Task GetDisplayInfoAsync(CancellationToken ct = default) + => await GetAsync("/api/platform/device-display", ct); + + public async Task GetBatteryInfoAsync(CancellationToken ct = default) + => await GetAsync("/api/platform/battery", ct); + + public async Task GetConnectivityAsync(CancellationToken ct = default) + => await GetAsync("/api/platform/connectivity", ct); + + public async Task GetVersionTrackingAsync(CancellationToken ct = default) + => await GetAsync("/api/platform/version-tracking", ct); + + public async Task> GetPermissionsAsync(CancellationToken ct = default) + { + var result = await GetAsync("/api/platform/permissions", ct); + return result?.Permissions ?? new(); + } + + public async Task CheckPermissionAsync(string permission, CancellationToken ct = default) + => await GetAsync($"/api/platform/permissions/{Uri.EscapeDataString(permission)}", ct); + + public async Task GetGeolocationAsync(string accuracy = "Medium", int timeoutSeconds = 10, CancellationToken ct = default) + => await GetAsync($"/api/platform/geolocation?accuracy={Uri.EscapeDataString(accuracy)}&timeout={timeoutSeconds}", ct); + + // --- Preferences --- + + public async Task> GetPreferencesAsync(string? sharedName = null, CancellationToken ct = default) + { + var query = sharedName != null ? $"?sharedName={Uri.EscapeDataString(sharedName)}" : ""; + var result = await GetAsync($"/api/preferences{query}", ct); + return result?.Keys ?? new(); + } + + public async Task GetPreferenceAsync(string key, string type = "string", string? sharedName = null, CancellationToken ct = default) + { + var query = $"?type={Uri.EscapeDataString(type)}"; + if (sharedName != null) query += $"&sharedName={Uri.EscapeDataString(sharedName)}"; + return await GetAsync($"/api/preferences/{Uri.EscapeDataString(key)}{query}", ct); + } + + public async Task SetPreferenceAsync(string key, object? value, string? type = null, string? sharedName = null, CancellationToken ct = default) + { + var body = new DevFlowPreferenceSetRequest { Value = value, Type = type ?? "string", SharedName = sharedName }; + var result = await PostAsync($"/api/preferences/{Uri.EscapeDataString(key)}", body, ct); + return result != null; + } + + public async Task DeletePreferenceAsync(string key, string? sharedName = null, CancellationToken ct = default) + { + var query = sharedName != null ? $"?sharedName={Uri.EscapeDataString(sharedName)}" : ""; + return await DeleteAsync($"/api/preferences/{Uri.EscapeDataString(key)}{query}", ct); + } + + public async Task ClearPreferencesAsync(string? sharedName = null, CancellationToken ct = default) + { + var query = sharedName != null ? $"?sharedName={Uri.EscapeDataString(sharedName)}" : ""; + try + { + var response = await _http.PostAsync($"{BaseUrl}/api/preferences/clear{query}", null, ct); + return response.IsSuccessStatusCode; + } + catch { return false; } + } + + // --- Secure Storage --- + + public async Task GetSecureStorageAsync(string key, CancellationToken ct = default) + => await GetAsync($"/api/secure-storage/{Uri.EscapeDataString(key)}", ct); + + public async Task SetSecureStorageAsync(string key, string value, CancellationToken ct = default) + { + var body = new { value }; + var result = await PostAsync($"/api/secure-storage/{Uri.EscapeDataString(key)}", body, ct); + return result != null; + } + + public async Task DeleteSecureStorageAsync(string key, CancellationToken ct = default) + => await DeleteAsync($"/api/secure-storage/{Uri.EscapeDataString(key)}", ct); + + public async Task ClearSecureStorageAsync(CancellationToken ct = default) + { + try + { + var response = await _http.PostAsync($"{BaseUrl}/api/secure-storage/clear", null, ct); + return response.IsSuccessStatusCode; + } + catch { return false; } + } + + // --- Sensors --- + + public async Task> GetSensorsAsync(CancellationToken ct = default) + => await GetAsync>("/api/sensors", ct) ?? new(); + + public async Task StartSensorAsync(string sensor, string speed = "UI", CancellationToken ct = default) + { + try + { + var response = await _http.PostAsync($"{BaseUrl}/api/sensors/{Uri.EscapeDataString(sensor)}/start?speed={Uri.EscapeDataString(speed)}", null, ct); + return response.IsSuccessStatusCode; + } + catch { return false; } + } + + public async Task StopSensorAsync(string sensor, CancellationToken ct = default) + { + try + { + var response = await _http.PostAsync($"{BaseUrl}/api/sensors/{Uri.EscapeDataString(sensor)}/stop", null, ct); + return response.IsSuccessStatusCode; + } + catch { return false; } + } + + public async Task StreamSensorAsync(string sensor, Action onReading, string speed = "UI", int throttleMs = 100, CancellationToken ct = default) + { + StopSensorStream(sensor); + + var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); + var ws = new ClientWebSocket(); + lock (_sensorStreams) { _sensorStreams[sensor] = (ws, cts); } + + var token = cts.Token; + try + { + var wsUrl = $"ws://{AgentHost}:{AgentPort}/ws/sensors?sensor={Uri.EscapeDataString(sensor)}&speed={Uri.EscapeDataString(speed)}&throttleMs={throttleMs}"; + await ws.ConnectAsync(new Uri(wsUrl), token); + + var buffer = new byte[16 * 1024]; + var sb = new StringBuilder(); + + while (ws.State == WebSocketState.Open && !token.IsCancellationRequested) + { + var result = await ws.ReceiveAsync(buffer, token); + if (result.MessageType == WebSocketMessageType.Close) + break; + + sb.Append(Encoding.UTF8.GetString(buffer, 0, result.Count)); + if (result.EndOfMessage) + { + var json = sb.ToString(); + sb.Clear(); + try + { + var reading = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + if (reading != null) onReading(reading); + } + catch { } + } + } + } + catch (OperationCanceledException) { } + catch (WebSocketException) { } + finally + { + if (ws.State == WebSocketState.Open) + { + try { await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None); } + catch { } + } + lock (_sensorStreams) { _sensorStreams.Remove(sensor); } + } + } + + public void StopSensorStream(string sensor) + { + lock (_sensorStreams) + { + if (_sensorStreams.Remove(sensor, out var entry)) + { + entry.Cts.Cancel(); + entry.Ws.Dispose(); + } + } + } + + public void StopAllSensorStreams() + { + lock (_sensorStreams) + { + foreach (var entry in _sensorStreams.Values) + { + entry.Cts.Cancel(); + entry.Ws.Dispose(); + } + _sensorStreams.Clear(); + } + } + + public bool IsSensorStreaming(string sensor) + { + lock (_sensorStreams) { return _sensorStreams.ContainsKey(sensor); } + } + + public int StreamingSensorCount + { + get { lock (_sensorStreams) { return _sensorStreams.Count; } } + } + // --- Helpers --- private async Task GetAsync(string path, CancellationToken ct = default) where T : class @@ -470,6 +678,16 @@ private async Task PostActionAsync(string path, object body, CancellationT catch { return false; } } + private async Task DeleteAsync(string path, CancellationToken ct = default) + { + try + { + var response = await _http.DeleteAsync($"{BaseUrl}{path}", ct); + return response.IsSuccessStatusCode; + } + catch { return false; } + } + public void Dispose() { if (_disposed) return; @@ -478,6 +696,7 @@ public void Dispose() _networkWs?.Dispose(); _logsWsCts?.Cancel(); _logsWs?.Dispose(); + StopAllSensorStreams(); _http.Dispose(); } } diff --git a/src/MauiSherpa.Core/Services/GcDumpReportParser.cs b/src/MauiSherpa.Core/Services/GcDumpReportParser.cs new file mode 100644 index 00000000..74d056ce --- /dev/null +++ b/src/MauiSherpa.Core/Services/GcDumpReportParser.cs @@ -0,0 +1,105 @@ +using System.Globalization; +using System.Text.RegularExpressions; +using MauiSherpa.Core.Models.Profiling; + +namespace MauiSherpa.Core.Services; + +/// +/// Parses the text output from dotnet-gcdump report into structured data. +/// +public static partial class GcDumpReportParser +{ + /// + /// Parse heapstat output from dotnet-gcdump report. + /// Expected format: + /// + /// MT Count TotalSize Class Name + /// 00007ffa... 12345 6789012 System.String + /// + /// + public static GcDumpReport? ParseHeapStatOutput(string output) + { + if (string.IsNullOrWhiteSpace(output)) + return null; + + var types = new List(); + var lines = output.Split('\n', StringSplitOptions.TrimEntries); + var inTable = false; + + foreach (var line in lines) + { + if (string.IsNullOrWhiteSpace(line)) + continue; + + // Detect header line — formats vary by dotnet-gcdump version: + // "MT Count TotalSize Class Name" + // "Object Bytes Count Type" + if ((line.Contains("Count") && (line.Contains("TotalSize") || line.Contains("Object Bytes")) + && (line.Contains("Class Name") || line.Contains("Type")))) + { + inTable = true; + continue; + } + + // Detect summary/total lines + if (inTable && line.StartsWith("Total", StringComparison.OrdinalIgnoreCase)) + continue; + + if (!inTable) + continue; + + // Try the modern format first: " 46,376 1 System.String (Bytes > 1K) [Module(...)]" + var match = ModernHeapStatLineRegex().Match(line); + if (match.Success) + { + var sizeStr = match.Groups["size"].Value.Replace(",", ""); + var countStr = match.Groups["count"].Value.Replace(",", ""); + var size = long.Parse(sizeStr, CultureInfo.InvariantCulture); + var count = long.Parse(countStr, CultureInfo.InvariantCulture); + var typeName = match.Groups["name"].Value.Trim(); + // Strip trailing annotations like "(Bytes > 1K) [Module(...)]" + var annoIdx = typeName.IndexOf(" [Module(", StringComparison.Ordinal); + if (annoIdx > 0) typeName = typeName[..annoIdx]; + annoIdx = typeName.IndexOf(" (Bytes ", StringComparison.Ordinal); + if (annoIdx > 0) typeName = typeName[..annoIdx]; + + if (!string.IsNullOrWhiteSpace(typeName)) + types.Add(new GcDumpTypeEntry(typeName, count, size)); + continue; + } + + // Legacy format: "00007ffa12345678 12345 6789012 System.String" + var legacyMatch = LegacyHeapStatLineRegex().Match(line); + if (legacyMatch.Success) + { + var count = long.Parse(legacyMatch.Groups["count"].Value, CultureInfo.InvariantCulture); + var size = long.Parse(legacyMatch.Groups["size"].Value, CultureInfo.InvariantCulture); + var typeName = legacyMatch.Groups["name"].Value.Trim(); + + if (!string.IsNullOrWhiteSpace(typeName)) + types.Add(new GcDumpTypeEntry(typeName, count, size)); + } + } + + if (types.Count == 0) + return null; + + // Sort by size descending (largest allocations first) + types.Sort((a, b) => b.Size.CompareTo(a.Size)); + + return new GcDumpReport( + Types: types, + TotalSize: types.Sum(t => t.Size), + TotalCount: types.Sum(t => t.Count), + RawOutput: output); + } + + // Modern format: " 46,376 1 System.String (Bytes > 1K) [Module(...)]" + // Size and count may have commas as thousand separators + [GeneratedRegex(@"^\s*(?[\d,]+)\s+(?[\d,]+)\s+(?.+)$")] + private static partial Regex ModernHeapStatLineRegex(); + + // Legacy format: "00007ffa12345678 12345 6789012 System.String" + [GeneratedRegex(@"^[0-9a-fA-F]+\s+(?\d+)\s+(?\d+)\s+(?.+)$")] + private static partial Regex LegacyHeapStatLineRegex(); +} diff --git a/src/MauiSherpa.Core/Services/InfisicalProvider.cs b/src/MauiSherpa.Core/Services/InfisicalProvider.cs index ad2b2390..199bf757 100644 --- a/src/MauiSherpa.Core/Services/InfisicalProvider.cs +++ b/src/MauiSherpa.Core/Services/InfisicalProvider.cs @@ -223,19 +223,22 @@ public async Task DeleteSecretAsync(string key, CancellationToken cancella _logger.LogInformation($"Deleted secret: {key}"); return true; } - catch (InfisicalException ex) when (ex.Message.Contains("not found", StringComparison.OrdinalIgnoreCase)) + catch (InfisicalException ex) when ( + ex.Message.Contains("not found", StringComparison.OrdinalIgnoreCase) || + ex.InnerException?.Message?.Contains("not found", StringComparison.OrdinalIgnoreCase) == true) { _logger.LogInformation($"Secret already deleted or not found: {key}"); return true; } catch (InfisicalException ex) { - _logger.LogError($"Infisical delete secret failed: {ex.Message}", ex); + var innerMsg = ex.InnerException?.Message; + _logger.LogError($"Infisical delete secret failed for '{key}': {ex.Message}{(innerMsg != null ? $" → {innerMsg}" : "")}", ex); return false; } catch (Exception ex) { - _logger.LogError($"Infisical delete secret error: {ex.Message}", ex); + _logger.LogError($"Infisical delete secret error for '{key}': {ex.Message}", ex); return false; } } @@ -299,10 +302,21 @@ public async Task> ListSecretsAsync(string? prefix = null, if (secrets == null) return Array.Empty(); + _logger.LogDebug($"Infisical ListSecrets returned {secrets.Length} secrets (path={SecretPath}, env={Environment})"); var sanitizedPrefix = !string.IsNullOrEmpty(prefix) ? SanitizeSecretName(prefix) : null; var result = new List(); foreach (var secret in secrets) { + _logger.LogDebug($" Secret: {secret.SecretKey} path={secret.SecretPath} env={secret.Environment}"); + + // Skip imported secrets from other paths — they can't be deleted from our path + if (!string.IsNullOrEmpty(secret.SecretPath) && + !string.Equals(secret.SecretPath, SecretPath, StringComparison.OrdinalIgnoreCase)) + { + _logger.LogDebug($" → Skipped (path mismatch: '{secret.SecretPath}' != '{SecretPath}')"); + continue; + } + var secretKey = secret.SecretKey; // Filter by sanitized prefix if specified diff --git a/src/MauiSherpa.Core/Services/KeystoreSyncService.cs b/src/MauiSherpa.Core/Services/KeystoreSyncService.cs index 4156c304..e9e875ac 100644 --- a/src/MauiSherpa.Core/Services/KeystoreSyncService.cs +++ b/src/MauiSherpa.Core/Services/KeystoreSyncService.cs @@ -170,14 +170,22 @@ public async Task DownloadKeystoreFromCloudAsync(string cloudKey, CancellationTo _logger.LogInformation($"Keystore downloaded from cloud: {alias} → {filePath}"); } - public async Task DeleteKeystoreFromCloudAsync(string cloudKey, CancellationToken ct = default) + public async Task DeleteKeystoreFromCloudAsync(string alias, CancellationToken ct = default) { - var alias = ExtractAliasFromKey(cloudKey); _logger.LogInformation($"Deleting keystore from cloud: {alias}"); - await _cloudService.DeleteSecretAsync(GetCloudKey(alias, "JKS"), ct); - await _cloudService.DeleteSecretAsync(GetCloudKey(alias, "PWD"), ct); - await _cloudService.DeleteSecretAsync(GetCloudKey(alias, "META"), ct); + var failures = new List(); + + if (!await _cloudService.DeleteSecretAsync(GetCloudKey(alias, "JKS"), ct)) + failures.Add("keystore file (JKS)"); + if (!await _cloudService.DeleteSecretAsync(GetCloudKey(alias, "PWD"), ct)) + failures.Add("password (PWD)"); + if (!await _cloudService.DeleteSecretAsync(GetCloudKey(alias, "META"), ct)) + failures.Add("metadata (META)"); + + if (failures.Count > 0) + throw new InvalidOperationException( + $"Failed to delete keystore '{alias}' from cloud. Could not remove: {string.Join(", ", failures)}"); } private static string GetCloudKey(string alias, string suffix) => $"{CloudKeyPrefix}{alias}_{suffix}"; diff --git a/src/MauiSherpa.Core/Services/ProfilingArtifactAnalysisService.cs b/src/MauiSherpa.Core/Services/ProfilingArtifactAnalysisService.cs new file mode 100644 index 00000000..4b9e5d1a --- /dev/null +++ b/src/MauiSherpa.Core/Services/ProfilingArtifactAnalysisService.cs @@ -0,0 +1,1003 @@ +using System.Globalization; +using System.Text.Json; +using System.Text.RegularExpressions; +using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Models.Profiling; + +namespace MauiSherpa.Core.Services; + +public class ProfilingArtifactAnalysisService : IProfilingArtifactAnalysisService +{ + private static readonly Regex HexValueRegex = new(@"0x[0-9a-fA-F]+", RegexOptions.Compiled); + private static readonly Regex IntegerRegex = new(@"\b\d+\b", RegexOptions.Compiled); + private static readonly Regex MultiWhitespaceRegex = new(@"\s+", RegexOptions.Compiled); + private static readonly Regex TimestampPrefixRegex = new( + @"^\s*(?:\[[^\]]+\]\s*)?(?\d{4}-\d{2}-\d{2}[T ][0-9:\.,+\-Z]+|\d{2}:\d{2}:\d{2}(?:[\.,]\d+)?)\s*", + RegexOptions.Compiled); + + private readonly IProfilingArtifactLibraryService _profilingArtifactLibraryService; + private readonly ILoggingService _loggingService; + + public ProfilingArtifactAnalysisService( + IProfilingArtifactLibraryService profilingArtifactLibraryService, + ILoggingService loggingService) + { + _profilingArtifactLibraryService = profilingArtifactLibraryService; + _loggingService = loggingService; + } + + public async Task AnalyzeArtifactAsync(string artifactId, CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(artifactId)) + { + throw new ArgumentException("A profiling artifact id is required.", nameof(artifactId)); + } + + var entry = await _profilingArtifactLibraryService.GetArtifactAsync(artifactId, ct); + if (entry is null) + { + return new ProfilingArtifactAnalysisResult( + null, + $"Profiling artifact '{artifactId}' was not found in the artifact library."); + } + + var analysis = await AnalyzeEntryAsync(entry, ct); + return new ProfilingArtifactAnalysisResult(analysis); + } + + public async Task> AnalyzeArtifactsAsync( + ProfilingArtifactLibraryQuery? query = null, + CancellationToken ct = default) + { + var artifacts = await _profilingArtifactLibraryService.GetArtifactsAsync(query, ct); + var analyses = new List(artifacts.Count); + + foreach (var artifact in artifacts) + { + ct.ThrowIfCancellationRequested(); + analyses.Add(await AnalyzeEntryAsync(artifact, ct)); + } + + return analyses; + } + + private async Task AnalyzeEntryAsync( + ProfilingArtifactLibraryEntry artifact, + CancellationToken ct) + { + var artifactPath = await _profilingArtifactLibraryService.GetArtifactPathAsync(artifact.Metadata.Id, ct); + var artifactExists = !string.IsNullOrWhiteSpace(artifactPath) && File.Exists(artifactPath); + + if (!artifactExists) + { + return CreateMetadataAnalysis( + artifact.Metadata, + artifactPath, + artifactExists: false, + summary: $"The artifact file for {artifact.Metadata.DisplayName} is no longer available on disk.", + insights: + [ + new ProfilingAnalysisInsight( + ProfilingAnalysisInsightSeverity.Warning, + "Artifact file is missing", + "The artifact metadata is still available, but the referenced file could not be found.") + ]); + } + + try + { + if (IsJsonArtifact(artifact.Metadata, artifactPath!)) + { + using var stream = File.OpenRead(artifactPath!); + using var document = await JsonDocument.ParseAsync(stream, cancellationToken: ct); + + if (LooksLikeSpeedscope(document.RootElement, artifact.Metadata)) + { + var speedscopeAnalysis = AnalyzeSpeedscopeArtifact(artifact.Metadata, artifactPath!, document.RootElement); + if (speedscopeAnalysis is not null) + { + return speedscopeAnalysis; + } + } + + return AnalyzeJsonArtifact(artifact.Metadata, artifactPath!, document.RootElement); + } + + if (IsLogArtifact(artifact.Metadata, artifactPath!)) + { + return AnalyzeLogArtifact(artifact.Metadata, artifactPath!); + } + } + catch (JsonException ex) + { + _loggingService.LogDebug($"Portable profiling analysis could not parse JSON for '{artifact.Metadata.Id}': {ex.Message}"); + return CreateMetadataAnalysis( + artifact.Metadata, + artifactPath, + artifactExists: true, + summary: $"Sherpa found {artifact.Metadata.DisplayName}, but the JSON payload could not be analyzed as a portable summary.", + notes: ["The artifact may require a specialized viewer or exporter to inspect in detail."], + insights: + [ + new ProfilingAnalysisInsight( + ProfilingAnalysisInsightSeverity.Warning, + "JSON artifact could not be parsed", + ex.Message) + ]); + } + catch (Exception ex) + { + _loggingService.LogError($"Failed to analyze profiling artifact '{artifact.Metadata.Id}': {ex.Message}", ex); + return CreateMetadataAnalysis( + artifact.Metadata, + artifactPath, + artifactExists: true, + summary: $"Sherpa found {artifact.Metadata.DisplayName}, but portable analysis fell back to metadata because the artifact could not be parsed.", + insights: + [ + new ProfilingAnalysisInsight( + ProfilingAnalysisInsightSeverity.Warning, + "Portable analysis failed", + ex.Message) + ]); + } + + return CreateMetadataAnalysis( + artifact.Metadata, + artifactPath, + artifactExists: true, + summary: CreateMetadataSummary(artifact.Metadata), + notes: [CreateMetadataNote(artifact.Metadata)]); + } + + private static ProfilingArtifactAnalysis? AnalyzeSpeedscopeArtifact( + ProfilingArtifactMetadata metadata, + string artifactPath, + JsonElement root) + { + if (!TryGetProperty(root, "profiles", out var profilesElement) || profilesElement.ValueKind != JsonValueKind.Array) + { + return null; + } + + if (!TryGetProperty(root, "shared", out var sharedElement) || + !TryGetProperty(sharedElement, "frames", out var framesElement) || + framesElement.ValueKind != JsonValueKind.Array) + { + return null; + } + + var frames = framesElement + .EnumerateArray() + .Select(ParseFrame) + .ToArray(); + + var aggregate = new SpeedscopeAggregate(); + foreach (var profile in profilesElement.EnumerateArray()) + { + var type = GetString(profile, "type"); + if (string.Equals(type, "sampled", StringComparison.OrdinalIgnoreCase)) + { + AnalyzeSampledProfile(profile, aggregate); + } + else if (string.Equals(type, "evented", StringComparison.OrdinalIgnoreCase)) + { + AnalyzeEventedProfile(profile, aggregate); + } + else if (!string.IsNullOrWhiteSpace(type)) + { + aggregate.Notes.Add($"Skipped unsupported speedscope profile type '{type}'."); + } + } + + if (aggregate.ProfileCount == 0) + { + return null; + } + + var hotspots = aggregate.InclusiveWeights + .OrderByDescending(pair => pair.Value) + .ThenBy(pair => frames[pair.Key].Name, StringComparer.OrdinalIgnoreCase) + .Take(5) + .Select(pair => + { + var frame = frames[pair.Key]; + var percent = aggregate.TotalReferenceWeight <= 0 + ? 0 + : Math.Round((pair.Value / aggregate.TotalReferenceWeight) * 100, 1); + var location = frame.File is null + ? null + : frame.Line.HasValue + ? $"{frame.File}:{frame.Line.Value}" + : frame.File; + + return new ProfilingAnalysisHotspot( + frame.Name, + FormatReferenceValue(pair.Value, aggregate), + percent, + location); + }) + .ToArray(); + + var metrics = CreateBaseMetrics(metadata); + metrics.Add(new ProfilingAnalysisMetric("profileCount", "Profiles", aggregate.ProfileCount.ToString(CultureInfo.InvariantCulture), aggregate.ProfileCount)); + metrics.Add(new ProfilingAnalysisMetric( + "samples", + aggregate.EventedProfileCount > 0 && aggregate.SampledProfileCount == 0 ? "Events" : "Samples", + aggregate.ReferenceEventCount.ToString(CultureInfo.InvariantCulture), + aggregate.ReferenceEventCount)); + metrics.Add(new ProfilingAnalysisMetric("frames", "Observed frames", aggregate.ObservedFrames.Count.ToString(CultureInfo.InvariantCulture), aggregate.ObservedFrames.Count)); + + if (aggregate.TotalDurationMs > 0) + { + metrics.Add(new ProfilingAnalysisMetric( + "durationMs", + "Duration", + $"{aggregate.TotalDurationMs:0.##} ms", + aggregate.TotalDurationMs, + "ms")); + } + + if (hotspots.Length > 0) + { + metrics.Add(new ProfilingAnalysisMetric( + "topHotspotPercent", + "Top hotspot share", + $"{hotspots[0].PercentOfTrace:0.#}%", + hotspots[0].PercentOfTrace, + "%")); + } + + var insights = new List(); + if (hotspots.Length > 0 && hotspots[0].PercentOfTrace >= 45) + { + insights.Add(new ProfilingAnalysisInsight( + ProfilingAnalysisInsightSeverity.Warning, + "Single hotspot dominates the trace", + $"{hotspots[0].Name} accounts for {hotspots[0].PercentOfTrace:0.#}% of the analyzed trace.")); + } + + if (aggregate.SampledProfileCount > 0 && aggregate.ReferenceEventCount < 25) + { + insights.Add(new ProfilingAnalysisInsight( + ProfilingAnalysisInsightSeverity.Info, + "Low sample count", + "The sampled trace has limited coverage, so hotspot rankings may shift with a longer capture.")); + } + + if (aggregate.ProfileCount > 1) + { + insights.Add(new ProfilingAnalysisInsight( + ProfilingAnalysisInsightSeverity.Info, + "Multiple profiles analyzed", + $"The artifact contains {aggregate.ProfileCount} profiles. Hotspots are aggregated across them.")); + } + + if (aggregate.UsesSampleWeights) + { + aggregate.Notes.Add("Hotspot percentages are based on inclusive weighted samples exported in the speedscope trace."); + } + else if (aggregate.SampledProfileCount > 0) + { + aggregate.Notes.Add("Hotspot percentages are based on how often a frame appears in sampled stacks, not exact CPU time."); + } + + var summary = hotspots.Length > 0 + ? $"Trace capture spans about {FormatDurationSummary(aggregate.TotalDurationMs)} with {hotspots[0].Name} as the top hotspot at {hotspots[0].PercentOfTrace:0.#}% of the analyzed trace." + : $"Trace capture includes {aggregate.ProfileCount} profile(s) and {aggregate.ObservedFrames.Count} observed frame(s)."; + + return new ProfilingArtifactAnalysis( + metadata, + artifactPath, + ArtifactExists: true, + Kind: ProfilingAnalysisKind.Speedscope, + Summary: summary, + Metrics: metrics, + Hotspots: hotspots, + Insights: insights, + Notes: aggregate.Notes); + } + + private static ProfilingArtifactAnalysis AnalyzeLogArtifact( + ProfilingArtifactMetadata metadata, + string artifactPath) + { + var lineCount = 0; + var warningCount = 0; + var errorCount = 0; + var exceptionCount = 0; + DateTimeOffset? firstTimestamp = null; + DateTimeOffset? lastTimestamp = null; + var frequentLines = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var line in File.ReadLines(artifactPath)) + { + lineCount++; + var normalized = NormalizeLogMessage(line); + if (!string.IsNullOrWhiteSpace(normalized)) + { + frequentLines[normalized] = frequentLines.TryGetValue(normalized, out var count) ? count + 1 : 1; + } + + var lowerLine = line.ToLowerInvariant(); + if (lowerLine.Contains("exception", StringComparison.Ordinal)) + { + exceptionCount++; + } + + if (lowerLine.Contains("error", StringComparison.Ordinal) || + lowerLine.Contains("fatal", StringComparison.Ordinal)) + { + errorCount++; + } + else if (lowerLine.Contains("warn", StringComparison.Ordinal)) + { + warningCount++; + } + + var timestamp = TryParseLogTimestamp(line); + if (timestamp is not null) + { + firstTimestamp ??= timestamp; + lastTimestamp = timestamp; + } + } + + var hotspots = frequentLines + .Where(pair => pair.Value > 1) + .OrderByDescending(pair => pair.Value) + .ThenBy(pair => pair.Key, StringComparer.OrdinalIgnoreCase) + .Take(5) + .Select(pair => new ProfilingAnalysisHotspot( + pair.Key, + $"{pair.Value} occurrence{(pair.Value == 1 ? string.Empty : "s")}", + lineCount == 0 ? 0 : Math.Round((pair.Value / (double)lineCount) * 100, 1))) + .ToArray(); + + var metrics = CreateBaseMetrics(metadata); + metrics.Add(new ProfilingAnalysisMetric("lineCount", "Log lines", lineCount.ToString(CultureInfo.InvariantCulture), lineCount)); + metrics.Add(new ProfilingAnalysisMetric("warningCount", "Warnings", warningCount.ToString(CultureInfo.InvariantCulture), warningCount)); + metrics.Add(new ProfilingAnalysisMetric("errorCount", "Errors", errorCount.ToString(CultureInfo.InvariantCulture), errorCount)); + metrics.Add(new ProfilingAnalysisMetric("exceptionCount", "Exception mentions", exceptionCount.ToString(CultureInfo.InvariantCulture), exceptionCount)); + + var notes = new List(); + if (firstTimestamp is not null && lastTimestamp is not null && lastTimestamp >= firstTimestamp) + { + var duration = lastTimestamp.Value - firstTimestamp.Value; + metrics.Add(new ProfilingAnalysisMetric( + "durationMs", + "Duration", + $"{duration.TotalMilliseconds:0.##} ms", + duration.TotalMilliseconds, + "ms")); + } + else + { + notes.Add("No parseable timestamps were found, so Sherpa could not estimate log duration."); + } + + var insights = new List(); + if (errorCount > 0) + { + insights.Add(new ProfilingAnalysisInsight( + exceptionCount > 0 ? ProfilingAnalysisInsightSeverity.Critical : ProfilingAnalysisInsightSeverity.Warning, + "Errors detected in captured logs", + $"The artifact contains {errorCount} error-level line(s) and {exceptionCount} exception mention(s).")); + } + else if (warningCount > 0) + { + insights.Add(new ProfilingAnalysisInsight( + ProfilingAnalysisInsightSeverity.Info, + "Warnings detected in captured logs", + $"The artifact contains {warningCount} warning-level line(s).")); + } + + if (hotspots.Length > 0) + { + insights.Add(new ProfilingAnalysisInsight( + ProfilingAnalysisInsightSeverity.Info, + "Recurring log lines found", + $"The most frequent recurring line appears {hotspots[0].Value} in the captured logs.")); + } + + var summary = $"Parsed {lineCount} log line(s) with {warningCount} warning(s), {errorCount} error(s), and {exceptionCount} exception mention(s)."; + + return new ProfilingArtifactAnalysis( + metadata, + artifactPath, + ArtifactExists: true, + Kind: ProfilingAnalysisKind.Logs, + Summary: summary, + Metrics: metrics, + Hotspots: hotspots, + Insights: insights, + Notes: notes); + } + + private static ProfilingArtifactAnalysis AnalyzeJsonArtifact( + ProfilingArtifactMetadata metadata, + string artifactPath, + JsonElement root) + { + var metrics = CreateBaseMetrics(metadata); + var hotspots = new List(); + var insights = new List(); + var notes = new List(); + string summary; + + switch (root.ValueKind) + { + case JsonValueKind.Object: + { + var properties = root.EnumerateObject().ToArray(); + metrics.Add(new ProfilingAnalysisMetric( + "propertyCount", + "Top-level properties", + properties.Length.ToString(CultureInfo.InvariantCulture), + properties.Length)); + + var arrayCount = 0; + foreach (var property in properties) + { + switch (property.Value.ValueKind) + { + case JsonValueKind.Array: + arrayCount++; + hotspots.Add(new ProfilingAnalysisHotspot( + ToDisplayLabel(property.Name), + $"{property.Value.GetArrayLength()} item(s)", + 0)); + break; + case JsonValueKind.Number when property.Value.TryGetDouble(out var numericValue): + metrics.Add(new ProfilingAnalysisMetric( + property.Name, + ToDisplayLabel(property.Name), + numericValue.ToString("0.##", CultureInfo.InvariantCulture), + numericValue)); + if (IsAlertMetric(property.Name, numericValue)) + { + insights.Add(new ProfilingAnalysisInsight( + ProfilingAnalysisInsightSeverity.Warning, + $"{ToDisplayLabel(property.Name)} is elevated", + $"The JSON artifact reports {numericValue:0.##} for {ToDisplayLabel(property.Name)}.")); + } + break; + case JsonValueKind.True: + case JsonValueKind.False: + metrics.Add(new ProfilingAnalysisMetric( + property.Name, + ToDisplayLabel(property.Name), + property.Value.GetBoolean() ? "True" : "False")); + break; + } + } + + summary = arrayCount > 0 + ? $"Parsed a structured JSON artifact with {properties.Length} top-level properties and {arrayCount} collection(s)." + : $"Parsed a structured JSON artifact with {properties.Length} top-level properties."; + break; + } + case JsonValueKind.Array: + { + var count = root.GetArrayLength(); + metrics.Add(new ProfilingAnalysisMetric("itemCount", "Items", count.ToString(CultureInfo.InvariantCulture), count)); + summary = $"Parsed a JSON artifact containing {count} top-level item(s)."; + break; + } + default: + summary = "Parsed a JSON artifact, but it did not contain an object or array that Sherpa could summarize in a structured way."; + notes.Add("Consider exporting the artifact in a richer report format for deeper analysis."); + break; + } + + if (hotspots.Count > 5) + { + hotspots = hotspots.Take(5).ToList(); + } + + return new ProfilingArtifactAnalysis( + metadata, + artifactPath, + ArtifactExists: true, + Kind: ProfilingAnalysisKind.Json, + Summary: summary, + Metrics: metrics, + Hotspots: hotspots, + Insights: insights, + Notes: notes); + } + + private static void AnalyzeSampledProfile(JsonElement profile, SpeedscopeAggregate aggregate) + { + aggregate.ProfileCount++; + aggregate.SampledProfileCount++; + + var unit = GetString(profile, "unit"); + var startValue = TryGetDouble(profile, "startValue"); + var endValue = TryGetDouble(profile, "endValue"); + if (startValue.HasValue && endValue.HasValue && endValue.Value >= startValue.Value) + { + aggregate.TotalDurationMs += ConvertToMilliseconds(endValue.Value - startValue.Value, unit); + } + + if (!TryGetProperty(profile, "samples", out var samplesElement) || samplesElement.ValueKind != JsonValueKind.Array) + { + aggregate.Notes.Add("A sampled profile was missing the samples array."); + return; + } + + var weights = Array.Empty(); + if (TryGetProperty(profile, "weights", out var weightsElement) && weightsElement.ValueKind == JsonValueKind.Array) + { + weights = weightsElement.EnumerateArray() + .Select(weight => weight.ValueKind == JsonValueKind.Number && weight.TryGetDouble(out var value) ? value : 1d) + .ToArray(); + aggregate.UsesSampleWeights = true; + } + + var referenceMode = weights.Length > 0 && CanConvertToMilliseconds(unit) + ? SpeedscopeReferenceMode.DurationMs + : SpeedscopeReferenceMode.SampleWeight; + aggregate.SetReferenceMode(referenceMode); + + var index = 0; + foreach (var sample in samplesElement.EnumerateArray()) + { + var weight = index < weights.Length ? weights[index] : 1d; + var normalizedWeight = referenceMode == SpeedscopeReferenceMode.DurationMs + ? ConvertToMilliseconds(weight, unit) + : weight; + + aggregate.TotalReferenceWeight += normalizedWeight; + aggregate.ReferenceEventCount++; + + var uniqueFrames = new HashSet(); + foreach (var frameValue in sample.EnumerateArray()) + { + var frameIndex = TryGetInt32(frameValue); + if (!frameIndex.HasValue) + { + continue; + } + + aggregate.ObservedFrames.Add(frameIndex.Value); + uniqueFrames.Add(frameIndex.Value); + } + + foreach (var frameIndex in uniqueFrames) + { + aggregate.InclusiveWeights[frameIndex] = aggregate.InclusiveWeights.TryGetValue(frameIndex, out var existing) + ? existing + normalizedWeight + : normalizedWeight; + } + + index++; + } + } + + private static void AnalyzeEventedProfile(JsonElement profile, SpeedscopeAggregate aggregate) + { + aggregate.ProfileCount++; + aggregate.EventedProfileCount++; + + var unit = GetString(profile, "unit"); + var startValue = TryGetDouble(profile, "startValue"); + var endValue = TryGetDouble(profile, "endValue"); + var durationMs = startValue.HasValue && endValue.HasValue && endValue.Value >= startValue.Value + ? ConvertToMilliseconds(endValue.Value - startValue.Value, unit) + : 0d; + if (durationMs > 0) + { + aggregate.TotalDurationMs += durationMs; + } + + aggregate.SetReferenceMode(SpeedscopeReferenceMode.DurationMs); + + if (!TryGetProperty(profile, "events", out var eventsElement) || eventsElement.ValueKind != JsonValueKind.Array) + { + aggregate.Notes.Add("An evented profile was missing the events array."); + return; + } + + var stack = new Stack<(int FrameIndex, double StartedAt)>(); + var minTimestamp = double.MaxValue; + var maxTimestamp = double.MinValue; + + foreach (var @event in eventsElement.EnumerateArray()) + { + aggregate.ReferenceEventCount++; + + var eventType = GetString(@event, "type"); + var eventAt = TryGetDouble(@event, "at"); + var frameIndex = TryGetInt32(@event, "frame"); + if (eventAt is null || frameIndex is null) + { + continue; + } + + minTimestamp = Math.Min(minTimestamp, eventAt.Value); + maxTimestamp = Math.Max(maxTimestamp, eventAt.Value); + aggregate.ObservedFrames.Add(frameIndex.Value); + + if (string.Equals(eventType, "O", StringComparison.OrdinalIgnoreCase)) + { + stack.Push((frameIndex.Value, eventAt.Value)); + continue; + } + + if (!string.Equals(eventType, "C", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (!TryPopMatchingFrame(stack, frameIndex.Value, out var openedFrame)) + { + continue; + } + + var duration = Math.Max(0, eventAt.Value - openedFrame.StartedAt); + var normalizedDuration = ConvertToMilliseconds(duration, unit); + aggregate.InclusiveWeights[frameIndex.Value] = aggregate.InclusiveWeights.TryGetValue(frameIndex.Value, out var existing) + ? existing + normalizedDuration + : normalizedDuration; + } + + if (durationMs <= 0 && minTimestamp != double.MaxValue && maxTimestamp >= minTimestamp) + { + aggregate.TotalDurationMs += ConvertToMilliseconds(maxTimestamp - minTimestamp, unit); + } + + aggregate.TotalReferenceWeight += durationMs > 0 + ? durationMs + : minTimestamp != double.MaxValue && maxTimestamp >= minTimestamp + ? ConvertToMilliseconds(maxTimestamp - minTimestamp, unit) + : 0; + } + + private static ProfilingArtifactAnalysis CreateMetadataAnalysis( + ProfilingArtifactMetadata metadata, + string? artifactPath, + bool artifactExists, + string summary, + IReadOnlyList? notes = null, + IReadOnlyList? insights = null) + { + return new ProfilingArtifactAnalysis( + metadata, + artifactPath, + artifactExists, + ProfilingAnalysisKind.Metadata, + summary, + CreateBaseMetrics(metadata), + Array.Empty(), + insights ?? Array.Empty(), + notes ?? Array.Empty()); + } + + private static List CreateBaseMetrics(ProfilingArtifactMetadata metadata) + { + var metrics = new List(); + + if (metadata.SizeBytes.HasValue) + { + metrics.Add(new ProfilingAnalysisMetric( + "sizeBytes", + "Size", + FormatBytes(metadata.SizeBytes.Value), + metadata.SizeBytes.Value, + "bytes")); + } + + if (metadata.Properties?.TryGetValue("targetPlatform", out var targetPlatform) == true) + { + metrics.Add(new ProfilingAnalysisMetric("targetPlatform", "Platform", targetPlatform)); + } + + if (metadata.Properties?.TryGetValue("scenario", out var scenario) == true) + { + metrics.Add(new ProfilingAnalysisMetric("scenario", "Scenario", scenario)); + } + + if (metadata.Properties?.TryGetValue("category", out var category) == true) + { + metrics.Add(new ProfilingAnalysisMetric("category", "Category", category)); + } + + return metrics; + } + + private static bool LooksLikeSpeedscope(JsonElement root, ProfilingArtifactMetadata metadata) + { + if (metadata.Kind == ProfilingArtifactKind.Trace) + { + return true; + } + + return TryGetProperty(root, "$schema", out var schemaElement) && + schemaElement.ValueKind == JsonValueKind.String && + schemaElement.GetString()?.Contains("speedscope", StringComparison.OrdinalIgnoreCase) == true; + } + + private static bool IsJsonArtifact(ProfilingArtifactMetadata metadata, string artifactPath) + => metadata.ContentType.Contains("json", StringComparison.OrdinalIgnoreCase) || + artifactPath.EndsWith(".json", StringComparison.OrdinalIgnoreCase); + + private static bool IsLogArtifact(ProfilingArtifactMetadata metadata, string artifactPath) + => metadata.Kind == ProfilingArtifactKind.Logs || + metadata.ContentType.Contains("text", StringComparison.OrdinalIgnoreCase) || + artifactPath.EndsWith(".log", StringComparison.OrdinalIgnoreCase) || + artifactPath.EndsWith(".txt", StringComparison.OrdinalIgnoreCase); + + private static bool TryGetProperty(JsonElement element, string name, out JsonElement value) + { + if (element.ValueKind == JsonValueKind.Object && element.TryGetProperty(name, out value)) + { + return true; + } + + value = default; + return false; + } + + private static string? GetString(JsonElement element, string propertyName) + => TryGetProperty(element, propertyName, out var value) && value.ValueKind == JsonValueKind.String + ? value.GetString() + : null; + + private static double? TryGetDouble(JsonElement element, string propertyName) + => TryGetProperty(element, propertyName, out var value) && value.ValueKind == JsonValueKind.Number && value.TryGetDouble(out var result) + ? result + : null; + + private static int? TryGetInt32(JsonElement element, string propertyName) + => TryGetProperty(element, propertyName, out var value) ? TryGetInt32(value) : null; + + private static int? TryGetInt32(JsonElement element) + { + if (element.ValueKind == JsonValueKind.Number && element.TryGetInt32(out var value)) + { + return value; + } + + return null; + } + + private static ParsedSpeedscopeFrame ParseFrame(JsonElement frame) + { + var name = GetString(frame, "name"); + return new ParsedSpeedscopeFrame( + string.IsNullOrWhiteSpace(name) ? "Unknown frame" : name, + GetString(frame, "file"), + TryGetInt32(frame, "line")); + } + + private static string FormatReferenceValue(double value, SpeedscopeAggregate aggregate) + { + return aggregate.ReferenceMode switch + { + SpeedscopeReferenceMode.DurationMs => $"{value:0.##} ms", + SpeedscopeReferenceMode.SampleWeight => $"{value:0.#} weighted samples", + _ => $"{value:0.##}" + }; + } + + private static string FormatDurationSummary(double durationMs) + => durationMs > 0 ? $"{durationMs:0.##} ms" : "an unknown duration"; + + private static bool CanConvertToMilliseconds(string? unit) + => NormalizeUnit(unit) is "nanoseconds" or "microseconds" or "milliseconds" or "seconds"; + + private static double ConvertToMilliseconds(double value, string? unit) + { + return NormalizeUnit(unit) switch + { + "nanoseconds" => value / 1_000_000d, + "microseconds" => value / 1_000d, + "milliseconds" => value, + "seconds" => value * 1_000d, + _ => value + }; + } + + private static string NormalizeUnit(string? unit) + { + return unit?.Trim().ToLowerInvariant() switch + { + "ns" => "nanoseconds", + "nanosecond" => "nanoseconds", + "nanoseconds" => "nanoseconds", + "us" => "microseconds", + "microsecond" => "microseconds", + "microseconds" => "microseconds", + "ms" => "milliseconds", + "millisecond" => "milliseconds", + "milliseconds" => "milliseconds", + "s" => "seconds", + "sec" => "seconds", + "second" => "seconds", + "seconds" => "seconds", + _ => unit?.Trim().ToLowerInvariant() ?? string.Empty + }; + } + + private static bool TryPopMatchingFrame( + Stack<(int FrameIndex, double StartedAt)> stack, + int expectedFrameIndex, + out (int FrameIndex, double StartedAt) openedFrame) + { + if (stack.Count == 0) + { + openedFrame = default; + return false; + } + + if (stack.Peek().FrameIndex == expectedFrameIndex) + { + openedFrame = stack.Pop(); + return true; + } + + var buffer = new Stack<(int FrameIndex, double StartedAt)>(); + while (stack.Count > 0) + { + var current = stack.Pop(); + if (current.FrameIndex == expectedFrameIndex) + { + while (buffer.Count > 0) + { + stack.Push(buffer.Pop()); + } + + openedFrame = current; + return true; + } + + buffer.Push(current); + } + + while (buffer.Count > 0) + { + stack.Push(buffer.Pop()); + } + + openedFrame = default; + return false; + } + + private static string NormalizeLogMessage(string line) + { + if (string.IsNullOrWhiteSpace(line)) + { + return string.Empty; + } + + var withoutTimestamp = TimestampPrefixRegex.Replace(line, string.Empty); + var withoutHexValues = HexValueRegex.Replace(withoutTimestamp, "0x*"); + var withoutIntegers = IntegerRegex.Replace(withoutHexValues, "#"); + return MultiWhitespaceRegex.Replace(withoutIntegers.Trim(), " "); + } + + private static DateTimeOffset? TryParseLogTimestamp(string line) + { + if (string.IsNullOrWhiteSpace(line)) + { + return null; + } + + var match = TimestampPrefixRegex.Match(line); + if (!match.Success) + { + return null; + } + + var stamp = match.Groups["stamp"].Value; + return DateTimeOffset.TryParse(stamp, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var parsed) + ? parsed + : null; + } + + private static bool IsAlertMetric(string key, double value) + { + var normalized = key.Trim().ToLowerInvariant(); + return value > 0 && (normalized.Contains("error", StringComparison.Ordinal) || + normalized.Contains("exception", StringComparison.Ordinal) || + normalized.Contains("warning", StringComparison.Ordinal) || + normalized.Contains("failure", StringComparison.Ordinal)); + } + + private static string ToDisplayLabel(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return string.Empty; + } + + var builder = new List(value.Length + 4); + for (var i = 0; i < value.Length; i++) + { + var current = value[i]; + if (i > 0 && char.IsUpper(current) && char.IsLower(value[i - 1])) + { + builder.Add(' '); + } + + builder.Add(current == '_' || current == '-' ? ' ' : current); + } + + return CultureInfo.InvariantCulture.TextInfo.ToTitleCase(new string(builder.ToArray())); + } + + private static string CreateMetadataSummary(ProfilingArtifactMetadata metadata) + { + return metadata.Kind switch + { + ProfilingArtifactKind.Export => $"{metadata.DisplayName} is available in the artifact library, but Sherpa currently provides metadata-only analysis for binary exports such as GC dumps.", + ProfilingArtifactKind.Screenshot => $"{metadata.DisplayName} is stored in the artifact library and can be used as supporting evidence for a profiling session.", + _ => $"{metadata.DisplayName} is stored in the artifact library and has a portable metadata summary ready for later UI or Copilot analysis." + }; + } + + private static string CreateMetadataNote(ProfilingArtifactMetadata metadata) + { + return metadata.Kind switch + { + ProfilingArtifactKind.Export => "Open the export in a specialized tool for heap or object graph analysis.", + ProfilingArtifactKind.Screenshot => "Screenshots complement trace and log artifacts but do not currently receive deeper profiling analysis.", + _ => "Portable analysis for this artifact kind can be expanded incrementally without changing the artifact library contract." + }; + } + + private static string FormatBytes(long bytes) + { + string[] suffixes = ["B", "KB", "MB", "GB", "TB"]; + var value = (double)bytes; + var index = 0; + + while (value >= 1024 && index < suffixes.Length - 1) + { + value /= 1024; + index++; + } + + return $"{value:0.##} {suffixes[index]}"; + } + + private sealed record ParsedSpeedscopeFrame(string Name, string? File, int? Line); + + private sealed class SpeedscopeAggregate + { + public int ProfileCount { get; set; } + public int SampledProfileCount { get; set; } + public int EventedProfileCount { get; set; } + public int ReferenceEventCount { get; set; } + public bool UsesSampleWeights { get; set; } + public double TotalReferenceWeight { get; set; } + public double TotalDurationMs { get; set; } + public SpeedscopeReferenceMode ReferenceMode { get; private set; } + public Dictionary InclusiveWeights { get; } = new(); + public HashSet ObservedFrames { get; } = new(); + public List Notes { get; } = new(); + + public void SetReferenceMode(SpeedscopeReferenceMode mode) + { + if (ReferenceMode == SpeedscopeReferenceMode.None) + { + ReferenceMode = mode; + return; + } + + if (ReferenceMode != mode) + { + ReferenceMode = SpeedscopeReferenceMode.Mixed; + } + } + } + + private enum SpeedscopeReferenceMode + { + None, + DurationMs, + SampleWeight, + Mixed + } +} diff --git a/src/MauiSherpa.Core/Services/ProfilingArtifactLibraryService.cs b/src/MauiSherpa.Core/Services/ProfilingArtifactLibraryService.cs new file mode 100644 index 00000000..1fdaaef6 --- /dev/null +++ b/src/MauiSherpa.Core/Services/ProfilingArtifactLibraryService.cs @@ -0,0 +1,371 @@ +using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Models.Profiling; + +namespace MauiSherpa.Core.Services; + +public class ProfilingArtifactLibraryService : IProfilingArtifactLibraryService +{ + private readonly IEncryptedSettingsService _settingsService; + private readonly ILoggingService _loggingService; + private readonly string _artifactLibraryRoot; + private readonly SemaphoreSlim _gate = new(1, 1); + + public event Action? OnArtifactsChanged; + + public ProfilingArtifactLibraryService( + IEncryptedSettingsService settingsService, + ILoggingService loggingService) + : this( + settingsService, + loggingService, + Path.Combine(AppDataPath.GetAppDataDirectory(), "profiling-artifacts")) + { + } + + internal ProfilingArtifactLibraryService( + IEncryptedSettingsService settingsService, + ILoggingService loggingService, + string artifactLibraryRoot) + { + _settingsService = settingsService; + _loggingService = loggingService; + _artifactLibraryRoot = Path.GetFullPath(artifactLibraryRoot); + } + + public async Task> GetArtifactsAsync( + ProfilingArtifactLibraryQuery? query = null, + CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); + + var settings = await _settingsService.GetSettingsAsync(); + IEnumerable artifacts = settings.ProfilingArtifacts; + + if (!string.IsNullOrWhiteSpace(query?.SessionId)) + { + artifacts = artifacts.Where(a => + a.Metadata.SessionId.Equals(query.SessionId, StringComparison.OrdinalIgnoreCase)); + } + + if (query?.Kind is ProfilingArtifactKind kind) + { + artifacts = artifacts.Where(a => a.Metadata.Kind == kind); + } + + if (query is { IncludeMissing: false }) + { + artifacts = artifacts.Where(a => + { + var path = ResolveArtifactPath(a); + return !string.IsNullOrWhiteSpace(path) && File.Exists(path); + }); + } + + return artifacts + .OrderByDescending(a => a.UpdatedAt) + .ThenByDescending(a => a.Metadata.CreatedAt) + .ToArray(); + } + + public async Task GetArtifactAsync(string artifactId, CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); + ValidateRequiredText(artifactId, nameof(artifactId)); + + var settings = await _settingsService.GetSettingsAsync(); + return settings.ProfilingArtifacts.FirstOrDefault(a => + a.Metadata.Id.Equals(artifactId, StringComparison.OrdinalIgnoreCase)); + } + + public async Task SaveArtifactAsync( + ProfilingArtifactLibrarySaveRequest request, + CancellationToken ct = default) + { + ArgumentNullException.ThrowIfNull(request); + ValidateMetadata(request.Metadata); + + await _gate.WaitAsync(ct); + try + { + ct.ThrowIfCancellationRequested(); + + var storage = await PrepareStorageAsync(request, ct); + var now = DateTimeOffset.UtcNow; + var metadata = request.Metadata with + { + RelativePath = storage.StoredPath, + SizeBytes = storage.SizeBytes ?? request.Metadata.SizeBytes, + CreatedAt = request.Metadata.CreatedAt == default ? now : request.Metadata.CreatedAt + }; + + ProfilingArtifactLibraryEntry? savedEntry = null; + await _settingsService.UpdateSettingsAsync(settings => + { + var artifacts = settings.ProfilingArtifacts.ToList(); + var existingIndex = artifacts.FindIndex(a => + a.Metadata.Id.Equals(metadata.Id, StringComparison.OrdinalIgnoreCase)); + + if (existingIndex >= 0) + { + var existing = artifacts[existingIndex]; + savedEntry = existing with + { + Metadata = metadata, + IsManagedPath = storage.IsManagedPath, + SourcePath = storage.SourcePath ?? existing.SourcePath, + UpdatedAt = now + }; + artifacts[existingIndex] = savedEntry; + } + else + { + savedEntry = new ProfilingArtifactLibraryEntry( + Metadata: metadata, + IsManagedPath: storage.IsManagedPath, + SourcePath: storage.SourcePath, + AddedAt: now, + UpdatedAt: now); + artifacts.Add(savedEntry); + } + + return settings with { ProfilingArtifacts = artifacts }; + }); + + OnArtifactsChanged?.Invoke(); + return savedEntry!; + } + catch (Exception ex) + { + _loggingService.LogError($"Failed to save profiling artifact '{request.Metadata.Id}': {ex.Message}", ex); + throw; + } + finally + { + _gate.Release(); + } + } + + public async Task DeleteArtifactAsync(string artifactId, bool deleteFile = false, CancellationToken ct = default) + { + ValidateRequiredText(artifactId, nameof(artifactId)); + + await _gate.WaitAsync(ct); + try + { + ct.ThrowIfCancellationRequested(); + + var entry = await GetArtifactAsync(artifactId, ct); + if (entry is null) + { + return; + } + + await _settingsService.UpdateSettingsAsync(settings => settings with + { + ProfilingArtifacts = settings.ProfilingArtifacts + .Where(a => !a.Metadata.Id.Equals(artifactId, StringComparison.OrdinalIgnoreCase)) + .ToList() + }); + + if (deleteFile) + { + var path = ResolveArtifactPath(entry); + if (!string.IsNullOrWhiteSpace(path) && File.Exists(path)) + { + File.Delete(path); + DeleteEmptyManagedDirectories(path, entry.IsManagedPath); + } + } + + OnArtifactsChanged?.Invoke(); + } + catch (Exception ex) + { + _loggingService.LogError($"Failed to delete profiling artifact '{artifactId}': {ex.Message}", ex); + throw; + } + finally + { + _gate.Release(); + } + } + + public async Task GetArtifactPathAsync(string artifactId, CancellationToken ct = default) + { + var artifact = await GetArtifactAsync(artifactId, ct); + return artifact is null ? null : ResolveArtifactPath(artifact); + } + + public string GetDefaultArtifactDirectory(string sessionId) + { + ValidateRequiredText(sessionId, nameof(sessionId)); + return Path.Combine(_artifactLibraryRoot, SanitizePathSegment(sessionId)); + } + + private async Task PrepareStorageAsync( + ProfilingArtifactLibrarySaveRequest request, + CancellationToken ct) + { + var candidatePath = request.ArtifactPath ?? request.Metadata.RelativePath; + var normalizedCandidatePath = string.IsNullOrWhiteSpace(candidatePath) + ? null + : Path.GetFullPath(candidatePath.Trim()); + + if (request.CopyToLibrary) + { + if (string.IsNullOrWhiteSpace(normalizedCandidatePath)) + { + throw new InvalidOperationException("A source path is required when copying a profiling artifact into the library."); + } + + if (!File.Exists(normalizedCandidatePath)) + { + throw new FileNotFoundException("The profiling artifact source file could not be found.", normalizedCandidatePath); + } + + var managedRelativePath = BuildManagedRelativePath(request.Metadata); + var managedAbsolutePath = Path.Combine(_artifactLibraryRoot, managedRelativePath); + var managedDirectory = Path.GetDirectoryName(managedAbsolutePath); + if (!string.IsNullOrWhiteSpace(managedDirectory)) + { + Directory.CreateDirectory(managedDirectory); + } + + if (!string.Equals( + Path.GetFullPath(normalizedCandidatePath), + Path.GetFullPath(managedAbsolutePath), + GetPathComparison())) + { + File.Copy(normalizedCandidatePath, managedAbsolutePath, overwrite: true); + } + + ct.ThrowIfCancellationRequested(); + return new ResolvedArtifactStorage( + managedRelativePath, + IsManagedPath: true, + SourcePath: normalizedCandidatePath, + SizeBytes: new FileInfo(managedAbsolutePath).Length); + } + + if (string.IsNullOrWhiteSpace(normalizedCandidatePath)) + { + return new ResolvedArtifactStorage( + BuildManagedRelativePath(request.Metadata), + IsManagedPath: true, + SourcePath: null, + SizeBytes: null); + } + + if (IsPathUnderRoot(normalizedCandidatePath, _artifactLibraryRoot)) + { + return new ResolvedArtifactStorage( + Path.GetRelativePath(_artifactLibraryRoot, normalizedCandidatePath), + IsManagedPath: true, + SourcePath: null, + SizeBytes: TryGetFileSize(normalizedCandidatePath)); + } + + return new ResolvedArtifactStorage( + normalizedCandidatePath, + IsManagedPath: false, + SourcePath: null, + SizeBytes: TryGetFileSize(normalizedCandidatePath)); + } + + private string? ResolveArtifactPath(ProfilingArtifactLibraryEntry entry) + { + var storedPath = entry.Metadata.RelativePath; + if (string.IsNullOrWhiteSpace(storedPath)) + { + return null; + } + + return entry.IsManagedPath + ? Path.GetFullPath(Path.Combine(_artifactLibraryRoot, storedPath)) + : Path.GetFullPath(storedPath); + } + + private void DeleteEmptyManagedDirectories(string deletedPath, bool isManagedPath) + { + if (!isManagedPath) + { + return; + } + + var current = Path.GetDirectoryName(Path.GetFullPath(deletedPath)); + while (!string.IsNullOrWhiteSpace(current) && + IsPathUnderRoot(current, _artifactLibraryRoot) && + !string.Equals(current, _artifactLibraryRoot, GetPathComparison())) + { + if (Directory.EnumerateFileSystemEntries(current).Any()) + { + return; + } + + Directory.Delete(current); + current = Path.GetDirectoryName(current); + } + } + + private static string BuildManagedRelativePath(ProfilingArtifactMetadata metadata) + { + var sessionSegment = SanitizePathSegment(metadata.SessionId); + var idSegment = SanitizePathSegment(metadata.Id); + var fileName = Path.GetFileName(string.IsNullOrWhiteSpace(metadata.FileName) ? $"{idSegment}.bin" : metadata.FileName); + var safeFileName = SanitizeFileName(fileName); + return Path.Combine(sessionSegment, $"{idSegment}-{safeFileName}"); + } + + private static string SanitizePathSegment(string value) + { + ValidateRequiredText(value, nameof(value)); + return new string(value.Trim().Select(ch => Path.GetInvalidFileNameChars().Contains(ch) ? '-' : ch).ToArray()); + } + + private static string SanitizeFileName(string value) + { + var fileName = new string(value.Select(ch => Path.GetInvalidFileNameChars().Contains(ch) ? '-' : ch).ToArray()); + return string.IsNullOrWhiteSpace(fileName) ? "artifact.bin" : fileName; + } + + private static long? TryGetFileSize(string path) => File.Exists(path) ? new FileInfo(path).Length : null; + + private static bool IsPathUnderRoot(string path, string rootPath) + { + var normalizedPath = EnsureTrailingSeparator(Path.GetFullPath(path)); + var normalizedRoot = EnsureTrailingSeparator(Path.GetFullPath(rootPath)); + return normalizedPath.StartsWith(normalizedRoot, GetPathComparison()); + } + + private static string EnsureTrailingSeparator(string path) => + path.EndsWith(Path.DirectorySeparatorChar) || path.EndsWith(Path.AltDirectorySeparatorChar) + ? path + : path + Path.DirectorySeparatorChar; + + private static StringComparison GetPathComparison() => + OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + + private static void ValidateMetadata(ProfilingArtifactMetadata metadata) + { + ArgumentNullException.ThrowIfNull(metadata); + ValidateRequiredText(metadata.Id, nameof(metadata.Id)); + ValidateRequiredText(metadata.SessionId, nameof(metadata.SessionId)); + ValidateRequiredText(metadata.DisplayName, nameof(metadata.DisplayName)); + ValidateRequiredText(metadata.FileName, nameof(metadata.FileName)); + ValidateRequiredText(metadata.ContentType, nameof(metadata.ContentType)); + } + + private static void ValidateRequiredText(string value, string paramName) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException("A value is required.", paramName); + } + } + + private sealed record ResolvedArtifactStorage( + string StoredPath, + bool IsManagedPath, + string? SourcePath, + long? SizeBytes); +} diff --git a/src/MauiSherpa.Core/Services/ProfilingCaptureOrchestrationService.cs b/src/MauiSherpa.Core/Services/ProfilingCaptureOrchestrationService.cs new file mode 100644 index 00000000..1694cc58 --- /dev/null +++ b/src/MauiSherpa.Core/Services/ProfilingCaptureOrchestrationService.cs @@ -0,0 +1,965 @@ +using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Models.Profiling; + +namespace MauiSherpa.Core.Services; + +public class ProfilingCaptureOrchestrationService : IProfilingCaptureOrchestrationService +{ + private const string ProcessIdToken = "{{PROCESS_ID}}"; + private static readonly IReadOnlySet TraceCaptureKinds = new HashSet + { + ProfilingCaptureKind.Startup, + ProfilingCaptureKind.Cpu, + ProfilingCaptureKind.Network, + ProfilingCaptureKind.Rendering, + ProfilingCaptureKind.Energy, + ProfilingCaptureKind.SystemTrace + }; + + private readonly IProfilingCatalogService _profilingCatalogService; + private readonly IProfilingPrerequisitesService _profilingPrerequisitesService; + private readonly IDeviceMonitorService _deviceMonitorService; + private readonly IPlatformService _platformService; + private readonly IAndroidSdkSettingsService _androidSdkSettingsService; + private readonly ILoggingService _loggingService; + + public ProfilingCaptureOrchestrationService( + IProfilingCatalogService profilingCatalogService, + IProfilingPrerequisitesService profilingPrerequisitesService, + IDeviceMonitorService deviceMonitorService, + IPlatformService platformService, + IAndroidSdkSettingsService androidSdkSettingsService, + ILoggingService loggingService) + { + _profilingCatalogService = profilingCatalogService; + _profilingPrerequisitesService = profilingPrerequisitesService; + _deviceMonitorService = deviceMonitorService; + _platformService = platformService; + _androidSdkSettingsService = androidSdkSettingsService; + _loggingService = loggingService; + } + + public async Task PlanCaptureAsync( + ProfilingSessionDefinition definition, + ProfilingCapturePlanOptions? options = null, + CancellationToken ct = default) + { + ArgumentNullException.ThrowIfNull(definition); + + var normalizedOptions = NormalizeOptions(definition, options); + var targetFramework = ResolveTargetFramework(definition.Target.Platform, normalizedOptions.TargetFramework); + var workingDirectory = ResolveWorkingDirectory(normalizedOptions); + var capabilities = await _profilingCatalogService.GetCapabilitiesAsync(definition.Target.Platform, ct); + var definitionValidation = _profilingCatalogService.ValidateSessionDefinition(definition, capabilities); + var prerequisites = await _profilingPrerequisitesService.GetPrerequisitesAsync( + definition.Target.Platform, + definition.CaptureKinds, + workingDirectory, + ct); + + var errors = new List(definitionValidation.Errors); + var warnings = new List(); + var commands = new List(); + var runtimeBindings = new List(); + var expectedArtifacts = new List(); + var metadata = CreatePlanMetadata(definition, normalizedOptions, targetFramework); + + AppendPrerequisiteFindings(prerequisites, errors, warnings); + + if (normalizedOptions.LaunchMode == ProfilingCaptureLaunchMode.Launch && + string.IsNullOrWhiteSpace(normalizedOptions.ProjectPath)) + { + errors.Add("A project path is required to plan build and launch steps."); + } + + var isTargetCurrentlyAvailable = IsTargetCurrentlyAvailable(definition.Target); + if (!isTargetCurrentlyAvailable && RequiresConnectedTarget(definition.Target)) + { + warnings.Add($"Target '{definition.Target.Identifier}' is not currently present in the connected device snapshot."); + } + + if (normalizedOptions.LaunchMode == ProfilingCaptureLaunchMode.Attach && + !capabilities.SupportsAttachToProcess) + { + errors.Add($"{capabilities.DisplayName} capabilities do not support attach flows."); + } + + var diagnostics = BuildDiagnosticsConfiguration(definition.Target, normalizedOptions, _platformService.IsWindows); + var traceArtifactPath = Path.Combine(normalizedOptions.OutputDirectory!, "trace.nettrace"); + var gcdumpArtifactPath = Path.Combine(normalizedOptions.OutputDirectory!, "memory.gcdump"); + var logsArtifactPath = Path.Combine(normalizedOptions.OutputDirectory!, "logs.txt"); + + var androidSdkPath = definition.Target.Platform == ProfilingTargetPlatform.Android + ? await TryGetAndroidSdkPathAsync() + : null; + + // Modern dotnet-trace/dotnet-gcdump support --dsrouter natively, so we no longer + // need a standalone dotnet-dsrouter process. However, only ONE tool can use --dsrouter + // at a time because each starts its own dsrouter instance. When both trace and gcdump + // are requested on a mobile target, we fall back to a standalone dsrouter process and + // have both tools connect via --diagnostic-port using the IPC address instead. + var hasTraceCapture = definition.CaptureKinds.Any(kind => TraceCaptureKinds.Contains(kind)); + var hasMemoryCapture = definition.CaptureKinds.Contains(ProfilingCaptureKind.Memory); + var hasLogCapture = definition.CaptureKinds.Contains(ProfilingCaptureKind.Logs); + var dsrouterPlatformArg = GetDsRouterPlatformArg(definition.Target); + var isMobileTarget = dsrouterPlatformArg is not null; + // Both trace and gcdump are now on-demand, so always use standalone dsrouter + // on mobile when either is requested — they need to share the diagnostic port. + var needsStandaloneDsRouter = isMobileTarget && (hasTraceCapture || hasMemoryCapture); + + // If we need standalone dsrouter, clear the inline arg so capture steps use --diagnostic-port instead + if (needsStandaloneDsRouter) + dsrouterPlatformArg = null; + + var preLaunchCaptureSteps = new List(); + var postLaunchCaptureSteps = new List(); + + // When both trace and gcdump target a mobile platform, start a standalone dsrouter + // and have both tools connect to it via --diagnostic-port using the IPC socket. + if (needsStandaloneDsRouter && diagnostics is not null) + { + commands.Add(CreateDsRouterStep(definition, diagnostics, normalizedOptions, androidSdkPath)); + } + + if (hasTraceCapture) + { + // Don't add traceStep to pipeline — trace is an on-demand action + // triggered by the user via Start Trace / Stop Trace buttons. + // This prevents trace from auto-starting and competing with gcdump + // for the diagnostic port. + var (_, traceArtifact) = CreateTraceCaptureStep( + definition, + normalizedOptions, + dsrouterPlatformArg, + traceArtifactPath, + runtimeBindings, + needsStandaloneDsRouter ? diagnostics?.IpcAddress : null, + androidSdkPath); + + expectedArtifacts.Add(traceArtifact); + } + + if (normalizedOptions.LaunchMode == ProfilingCaptureLaunchMode.Launch) + { + commands.AddRange(preLaunchCaptureSteps); + + // Android requires adb setup steps before the app launches: + // - Physical devices need adb reverse for port forwarding + // - All Android targets need debug.mono.profile system property set + if (definition.Target.Platform == ProfilingTargetPlatform.Android && diagnostics is not null) + { + commands.AddRange(CreateAndroidDiagnosticSetupSteps( + definition.Target, diagnostics, normalizedOptions, androidSdkPath)); + } + + commands.Add(CreateLaunchStep(definition, normalizedOptions, targetFramework, workingDirectory, diagnostics, androidSdkPath)); + } + + if (!isMobileTarget && + normalizedOptions.ProcessId is null && + (hasTraceCapture || hasMemoryCapture)) + { + commands.Add(CreateProcessDiscoveryStep(definition, normalizedOptions)); + if (runtimeBindings.All(binding => binding.Token != ProcessIdToken)) + { + runtimeBindings.Add(new ProfilingRuntimeBinding( + ProcessIdToken, + "Resolve the local desktop process id after the app is running.", + ExampleValue: "12345")); + } + } + + if (hasMemoryCapture) + { + var (_, memoryArtifact) = CreateMemoryCaptureStep( + definition, + normalizedOptions, + dsrouterPlatformArg, + gcdumpArtifactPath, + runtimeBindings, + needsStandaloneDsRouter ? diagnostics?.IpcAddress : null, + androidSdkPath, + hasTraceCapture); + + // Don't add memoryStep to pipeline — GC dump is a point-in-time snapshot + // that users trigger on demand via the capture UI, not auto-run. + expectedArtifacts.Add(memoryArtifact); + } + + if (hasLogCapture) + { + var logStep = CreateLogCaptureStep(definition, normalizedOptions, logsArtifactPath, androidSdkPath); + if (logStep is not null) + { + postLaunchCaptureSteps.Add(logStep); + expectedArtifacts.Add(new ProfilingArtifactMetadata( + Id: $"{definition.Id}-logs", + SessionId: definition.Id, + Kind: ProfilingArtifactKind.Logs, + DisplayName: "Streaming logs", + FileName: Path.GetFileName(logsArtifactPath), + RelativePath: logsArtifactPath, + ContentType: "text/plain", + CreatedAt: DateTimeOffset.UtcNow, + Properties: CreateArtifactProperties(definition, "logs"))); + } + else + { + warnings.Add($"Logs capture planning is not yet modeled for {capabilities.DisplayName} {definition.Target.Kind} targets."); + } + } + + commands.AddRange(postLaunchCaptureSteps); + + var validation = new ProfilingPlanValidation( + Errors: errors + .Where(error => !string.IsNullOrWhiteSpace(error)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(), + Warnings: warnings + .Where(warning => !string.IsNullOrWhiteSpace(warning)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray()); + + if (!validation.IsValid) + { + _loggingService.LogDebug( + $"Profiling capture plan for session '{definition.Id}' contains validation issues: {string.Join(" | ", validation.Errors)}"); + } + + return new ProfilingCapturePlan( + definition, + capabilities, + normalizedOptions, + _platformService.PlatformName, + targetFramework, + normalizedOptions.OutputDirectory!, + workingDirectory, + isTargetCurrentlyAvailable, + diagnostics, + prerequisites, + validation, + runtimeBindings.ToArray(), + commands.ToArray(), + expectedArtifacts.ToArray(), + metadata); + } + + private static ProfilingCapturePlanOptions NormalizeOptions( + ProfilingSessionDefinition definition, + ProfilingCapturePlanOptions? options) + { + var normalized = options ?? new ProfilingCapturePlanOptions(); + var configuration = string.IsNullOrWhiteSpace(normalized.Configuration) ? "Release" : normalized.Configuration.Trim(); + var projectPath = string.IsNullOrWhiteSpace(normalized.ProjectPath) ? null : normalized.ProjectPath.Trim(); + var workingDirectory = string.IsNullOrWhiteSpace(normalized.WorkingDirectory) ? null : normalized.WorkingDirectory.Trim(); + var effectiveWorkingDirectory = workingDirectory ?? (string.IsNullOrWhiteSpace(projectPath) ? null : Path.GetDirectoryName(projectPath)); + var outputDirectory = string.IsNullOrWhiteSpace(normalized.OutputDirectory) + ? BuildDefaultOutputDirectory(normalized.ProjectPath, definition.CreatedAt) + : normalized.OutputDirectory.Trim(); + + // Make the output directory absolute so that artifact collection in the runner + // (which may run with a different CWD) can always find the files. + if (!Path.IsPathRooted(outputDirectory)) + { + var resolveBase = effectiveWorkingDirectory ?? Directory.GetCurrentDirectory(); + outputDirectory = Path.GetFullPath(Path.Combine(resolveBase, outputDirectory)); + } + var additionalBuildProperties = normalized.AdditionalBuildProperties is null + ? null + : new Dictionary(normalized.AdditionalBuildProperties, StringComparer.OrdinalIgnoreCase); + + return normalized with + { + ProjectPath = projectPath, + Configuration = configuration, + WorkingDirectory = effectiveWorkingDirectory, + OutputDirectory = outputDirectory, + AdditionalBuildProperties = additionalBuildProperties + }; + } + + private static string BuildDefaultOutputDirectory(string? projectPath, DateTimeOffset createdAt) + { + var projectName = "session"; + if (!string.IsNullOrWhiteSpace(projectPath)) + { + projectName = Path.GetFileNameWithoutExtension(projectPath); + } + + var dateStr = createdAt == default + ? DateTime.Now.ToString("yyyy-MM-dd") + : createdAt.LocalDateTime.ToString("yyyy-MM-dd"); + var baseDir = Path.Combine("artifacts", "profiling", projectName); + + var runNumber = 1; + if (Directory.Exists(baseDir)) + { + var prefix = $"{dateStr}-"; + var existingRuns = Directory.GetDirectories(baseDir) + .Select(d => Path.GetFileName(d)) + .Where(name => name!.StartsWith(prefix, StringComparison.Ordinal)) + .Select(name => { + var suffix = name!.Substring(prefix.Length); + return int.TryParse(suffix, out var n) ? n : 0; + }) + .Where(n => n > 0) + .ToList(); + + if (existingRuns.Count > 0) + runNumber = existingRuns.Max() + 1; + } + + return Path.Combine(baseDir, $"{dateStr}-{runNumber}"); + } + + private static string ResolveTargetFramework(ProfilingTargetPlatform platform, string? targetFrameworkOverride) => + string.IsNullOrWhiteSpace(targetFrameworkOverride) + ? platform switch + { + ProfilingTargetPlatform.Android => "net10.0-android", + ProfilingTargetPlatform.iOS => "net10.0-ios", + ProfilingTargetPlatform.MacCatalyst => "net10.0-maccatalyst", + ProfilingTargetPlatform.MacOS => "net10.0-macos", + ProfilingTargetPlatform.Windows => "net10.0-windows10.0.19041.0", + _ => throw new ArgumentOutOfRangeException(nameof(platform), platform, "Unsupported profiling platform.") + } + : targetFrameworkOverride.Trim(); + + private static string? ResolveWorkingDirectory(ProfilingCapturePlanOptions options) + { + if (!string.IsNullOrWhiteSpace(options.WorkingDirectory)) + return options.WorkingDirectory; + + return string.IsNullOrWhiteSpace(options.ProjectPath) + ? null + : Path.GetDirectoryName(options.ProjectPath); + } + + private static IReadOnlyDictionary CreatePlanMetadata( + ProfilingSessionDefinition definition, + ProfilingCapturePlanOptions options, + string targetFramework) + { + var metadata = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["sessionId"] = definition.Id, + ["targetPlatform"] = definition.Target.Platform.ToString(), + ["targetKind"] = definition.Target.Kind.ToString(), + ["targetIdentifier"] = definition.Target.Identifier, + ["configuration"] = options.Configuration, + ["launchMode"] = options.LaunchMode.ToString(), + ["targetFramework"] = targetFramework, + ["outputDirectory"] = options.OutputDirectory ?? string.Empty + }; + + if (!string.IsNullOrWhiteSpace(options.ProjectPath)) + metadata["projectPath"] = options.ProjectPath; + + return metadata; + } + + private static void AppendPrerequisiteFindings( + ProfilingPrerequisiteReport prerequisites, + List errors, + List warnings) + { + foreach (var check in prerequisites.Checks.Where(check => check.IsRequired && check.Status == DependencyStatusType.Error)) + { + errors.Add(check.Message ?? $"{check.Name} is required for profiling orchestration."); + } + } + + private bool IsTargetCurrentlyAvailable(ProfilingTarget target) + { + var snapshot = _deviceMonitorService.Current; + + return target.Platform switch + { + ProfilingTargetPlatform.Android when target.Kind == ProfilingTargetKind.PhysicalDevice + => snapshot.AndroidDevices.Any(device => string.Equals(device.Serial, target.Identifier, StringComparison.OrdinalIgnoreCase)), + ProfilingTargetPlatform.Android when target.Kind == ProfilingTargetKind.Emulator + => snapshot.AndroidEmulators.Any(device => string.Equals(device.Serial, target.Identifier, StringComparison.OrdinalIgnoreCase)), + ProfilingTargetPlatform.iOS when target.Kind == ProfilingTargetKind.PhysicalDevice + => snapshot.ApplePhysicalDevices.Any(device => string.Equals(device.Identifier, target.Identifier, StringComparison.OrdinalIgnoreCase)), + ProfilingTargetPlatform.iOS when target.Kind == ProfilingTargetKind.Simulator + => snapshot.BootedSimulators.Any(device => string.Equals(device.Identifier, target.Identifier, StringComparison.OrdinalIgnoreCase)), + _ => true + }; + } + + private static bool RequiresConnectedTarget(ProfilingTarget target) => + (target.Platform == ProfilingTargetPlatform.Android && + target.Kind is ProfilingTargetKind.PhysicalDevice or ProfilingTargetKind.Emulator) + || (target.Platform == ProfilingTargetPlatform.iOS && + target.Kind is ProfilingTargetKind.PhysicalDevice or ProfilingTargetKind.Simulator); + + private static ProfilingDiagnosticConfiguration? BuildDiagnosticsConfiguration( + ProfilingTarget target, + ProfilingCapturePlanOptions options, + bool isWindowsHost) + { + if (target.Platform is not ProfilingTargetPlatform.Android and not ProfilingTargetPlatform.iOS) + return null; + + var ipcAddress = isWindowsHost + ? $@"\\.\pipe\maui-sherpa-profile-{Guid.NewGuid():N}" + : Path.Combine(Path.GetTempPath(), $"ms-prof-{Guid.NewGuid().ToString("N")[..8]}.sock"); + var tcpEndpoint = $"127.0.0.1:{options.DiagnosticPort}"; + + return target.Platform switch + { + ProfilingTargetPlatform.Android => new ProfilingDiagnosticConfiguration( + Address: target.Kind == ProfilingTargetKind.Emulator ? "10.0.2.2" : "127.0.0.1", + Port: options.DiagnosticPort, + ListenMode: ProfilingDiagnosticListenMode.Connect, + SuspendOnStartup: options.SuspendAtStartup, + RequiresDsRouter: true, + DsRouterMode: ProfilingDsRouterMode.ServerServer, + DsRouterPortForwardPlatform: "Android", + IpcAddress: ipcAddress, + TcpEndpoint: tcpEndpoint), + ProfilingTargetPlatform.iOS => new ProfilingDiagnosticConfiguration( + Address: "127.0.0.1", + Port: options.DiagnosticPort, + ListenMode: ProfilingDiagnosticListenMode.Listen, + SuspendOnStartup: options.SuspendAtStartup, + RequiresDsRouter: true, + DsRouterMode: ProfilingDsRouterMode.ServerClient, + DsRouterPortForwardPlatform: target.Kind == ProfilingTargetKind.PhysicalDevice ? "iOS" : null, + IpcAddress: ipcAddress, + TcpEndpoint: tcpEndpoint), + _ => null + }; + } + + /// + /// Creates a standalone dotnet-dsrouter process step. Kept as a fallback for environments + /// where the integrated --dsrouter flag in dotnet-trace/dotnet-gcdump is not available. + /// In the normal flow, --dsrouter is passed directly to the capture tools instead. + /// + private static ProfilingCommandStep CreateDsRouterStep( + ProfilingSessionDefinition definition, + ProfilingDiagnosticConfiguration diagnostics, + ProfilingCapturePlanOptions options, + string? androidSdkPath) + { + var arguments = new List + { + diagnostics.DsRouterMode == ProfilingDsRouterMode.ServerServer ? "server-server" : "server-client", + "-ipcs", + diagnostics.IpcAddress, + diagnostics.DsRouterMode == ProfilingDsRouterMode.ServerServer ? "-tcps" : "-tcpc", + diagnostics.TcpEndpoint, + "-rt", + Math.Max(30, (int)Math.Ceiling((definition.Duration ?? TimeSpan.FromMinutes(5)).TotalSeconds)).ToString() + }; + + if (!string.IsNullOrWhiteSpace(diagnostics.DsRouterPortForwardPlatform)) + { + arguments.Add("--forward-port"); + arguments.Add(diagnostics.DsRouterPortForwardPlatform); + } + + var environment = BuildAndroidEnvironment(definition.Target, androidSdkPath); + + return new ProfilingCommandStep( + Id: "start-dsrouter", + Kind: ProfilingCommandStepKind.Prepare, + DisplayName: "Start diagnostics router", + Description: "Start dotnet-dsrouter so local diagnostic tools can talk to the remote mobile runtime.", + Command: "dotnet-dsrouter", + Arguments: arguments, + WorkingDirectory: options.WorkingDirectory, + Environment: environment, + Metadata: new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["tool"] = "dotnet-dsrouter", + ["mode"] = diagnostics.DsRouterMode.ToString(), + ["ipcAddress"] = diagnostics.IpcAddress, + ["tcpEndpoint"] = diagnostics.TcpEndpoint, + ["portForward"] = diagnostics.DsRouterPortForwardPlatform ?? string.Empty + }, + IsLongRunning: true, + RequiresManualStop: true, + ReadyOutputPattern: "Starting IPC server"); + } + + private static ProfilingCommandStep CreateLaunchStep( + ProfilingSessionDefinition definition, + ProfilingCapturePlanOptions options, + string targetFramework, + string? workingDirectory, + ProfilingDiagnosticConfiguration? diagnostics, + string? androidSdkPath = null) + { + var arguments = new List + { + "build" + }; + + if (!string.IsNullOrWhiteSpace(options.ProjectPath)) + arguments.Add(options.ProjectPath); + + arguments.Add("-t:Run"); + arguments.Add("-c"); + arguments.Add(options.Configuration); + arguments.Add("-f"); + arguments.Add(targetFramework); + + // Android requires AndroidEnableProfiler=true to include the Mono diagnostic component. + // The runtime diagnostic port is configured via adb system properties, not MSBuild properties. + if (diagnostics is not null && definition.Target.Platform == ProfilingTargetPlatform.Android) + { + arguments.Add("-p:AndroidEnableProfiler=true"); + } + + if (options.AdditionalBuildProperties is not null) + { + foreach (var buildProperty in options.AdditionalBuildProperties.OrderBy(kvp => kvp.Key, StringComparer.OrdinalIgnoreCase)) + { + arguments.Add($"-p:{buildProperty.Key}={buildProperty.Value}"); + } + } + + var environment = definition.Target.Platform == ProfilingTargetPlatform.Android + ? BuildAndroidEnvironment(definition.Target, androidSdkPath) + : null; + + return new ProfilingCommandStep( + Id: "build-and-run", + Kind: ProfilingCommandStepKind.Launch, + DisplayName: "Build and run target app", + Description: $"Build and launch {definition.Target.DisplayName} using {targetFramework}.", + Command: "dotnet", + Arguments: arguments, + WorkingDirectory: workingDirectory, + Environment: environment, + Metadata: new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["tool"] = "dotnet", + ["targetFramework"] = targetFramework, + ["configuration"] = options.Configuration, + ["targetIdentifier"] = definition.Target.Identifier, + ["launchMode"] = options.LaunchMode.ToString() + }, + IsLongRunning: true, + RequiresManualStop: definition.Target.Platform is ProfilingTargetPlatform.MacCatalyst or ProfilingTargetPlatform.MacOS or ProfilingTargetPlatform.Windows, + CanRunParallel: true, + StopTrigger: ProfilingStopTrigger.OnPipelineStop, + ReadyOutputPattern: "Build succeeded"); + } + + /// + /// Creates Android-specific setup steps that must run before the app launches: + /// 1. For physical devices: adb reverse to forward the diagnostic TCP port + /// 2. adb shell setprop to configure the Mono diagnostic port on the device/emulator + /// + private static List CreateAndroidDiagnosticSetupSteps( + ProfilingTarget target, + ProfilingDiagnosticConfiguration diagnostics, + ProfilingCapturePlanOptions options, + string? androidSdkPath) + { + var steps = new List(); + var environment = BuildAndroidEnvironment(target, androidSdkPath); + var suspendMode = diagnostics.SuspendOnStartup ? "suspend" : "nosuspend"; + + // Physical devices need adb reverse to forward the TCP port from device to host + if (target.Kind == ProfilingTargetKind.PhysicalDevice) + { + steps.Add(new ProfilingCommandStep( + Id: "setup-adb-reverse", + Kind: ProfilingCommandStepKind.Prepare, + DisplayName: "Forward diagnostic port", + Description: $"Set up adb reverse port forwarding so the device can reach the host diagnostic router on port {diagnostics.Port}.", + Command: "adb", + Arguments: ["reverse", $"tcp:{diagnostics.Port}", $"tcp:{diagnostics.Port + 1}"], + WorkingDirectory: options.WorkingDirectory, + Environment: environment, + Metadata: new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["tool"] = "adb", + ["port"] = diagnostics.Port.ToString() + })); + } + + // Set the debug.mono.profile system property so the app runtime connects to the diagnostic router + var diagnosticAddress = $"{diagnostics.Address}:{diagnostics.Port},{suspendMode},connect"; + steps.Add(new ProfilingCommandStep( + Id: "setup-diagnostic-port", + Kind: ProfilingCommandStepKind.Prepare, + DisplayName: "Configure diagnostic port", + Description: $"Set Android system property debug.mono.profile to '{diagnosticAddress}' so the app runtime connects to the diagnostic router.", + Command: "adb", + Arguments: ["shell", "setprop", "debug.mono.profile", diagnosticAddress], + WorkingDirectory: options.WorkingDirectory, + Environment: environment, + Metadata: new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["tool"] = "adb", + ["diagnosticAddress"] = diagnosticAddress, + ["suspendMode"] = suspendMode + })); + + return steps; + } + + private static ProfilingCommandStep CreateProcessDiscoveryStep( + ProfilingSessionDefinition definition, + ProfilingCapturePlanOptions options) + { + return new ProfilingCommandStep( + Id: "discover-process-id", + Kind: ProfilingCommandStepKind.DiscoverProcess, + DisplayName: "Discover target process id", + Description: $"List local .NET processes and bind {ProcessIdToken} to the running {definition.Target.DisplayName} process before attaching.", + Command: "dotnet-trace", + Arguments: ["ps"], + WorkingDirectory: options.WorkingDirectory, + RequiredRuntimeBindings: [ProcessIdToken], + Metadata: new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["tool"] = "dotnet-trace", + ["runtimeBinding"] = ProcessIdToken + }); + } + + private static (ProfilingCommandStep Step, ProfilingArtifactMetadata Artifact) CreateTraceCaptureStep( + ProfilingSessionDefinition definition, + ProfilingCapturePlanOptions options, + string? dsrouterPlatformArg, + string traceArtifactPath, + List runtimeBindings, + string? diagnosticPortAddress = null, + string? androidSdkPath = null) + { + var traceKinds = definition.CaptureKinds + .Where(kind => TraceCaptureKinds.Contains(kind)) + .Select(kind => kind.ToString()) + .ToArray(); + var arguments = new List + { + "collect" + }; + + if (dsrouterPlatformArg is not null) + { + arguments.Add("--dsrouter"); + arguments.Add(dsrouterPlatformArg); + } + else if (diagnosticPortAddress is not null) + { + // Connect to a standalone dsrouter via its IPC address (connect mode, not listen) + arguments.Add("--diagnostic-port"); + arguments.Add($"{diagnosticPortAddress},connect"); + } + else + { + arguments.Add("--process-id"); + arguments.Add(options.ProcessId?.ToString() ?? ProcessIdToken); + if (options.ProcessId is null) + { + runtimeBindings.Add(new ProfilingRuntimeBinding( + ProcessIdToken, + "Resolve the process id before starting dotnet-trace.", + ExampleValue: "12345")); + } + } + + arguments.Add("--output"); + arguments.Add(traceArtifactPath); + + // Map capture kinds to dotnet-trace profiles for meaningful data. + // "dotnet-sampled-thread-time" samples managed stacks at ~100Hz (works on all platforms). + // "cpu-sampling" and "thread-time" are Linux-only (collect-linux). + var profiles = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var kind in definition.CaptureKinds.Where(k => TraceCaptureKinds.Contains(k))) + { + switch (kind) + { + case ProfilingCaptureKind.Cpu: + case ProfilingCaptureKind.Startup: + profiles.Add("dotnet-sampled-thread-time"); + break; + case ProfilingCaptureKind.Rendering: + case ProfilingCaptureKind.Network: + case ProfilingCaptureKind.Energy: + case ProfilingCaptureKind.SystemTrace: + profiles.Add("dotnet-common"); + break; + } + } + if (profiles.Count == 0) + profiles.Add("dotnet-sampled-thread-time"); + arguments.Add("--profile"); + arguments.Add(string.Join(",", profiles)); + + // Add JIT/Loader provider flags for managed symbol resolution in speedscope. + // 0x10000018 = JitTracing | NGenTracing | Loader keywords, Verbose level (5). + arguments.Add("--providers"); + arguments.Add("Microsoft-Windows-DotNETRuntime:0x10000018:5"); + + var dependsOn = new List(); + if (diagnosticPortAddress is not null) + dependsOn.Add("start-dsrouter"); + if (dsrouterPlatformArg is null && diagnosticPortAddress is null && options.ProcessId is null) + dependsOn.Add("discover-process-id"); + + return ( + new ProfilingCommandStep( + Id: "capture-trace", + Kind: ProfilingCommandStepKind.Capture, + DisplayName: "Collect trace", + Description: $"Collect a trace for {string.Join(", ", traceKinds)} captures.", + Command: "dotnet-trace", + Arguments: arguments, + WorkingDirectory: options.WorkingDirectory, + Environment: (dsrouterPlatformArg is not null || diagnosticPortAddress is not null) + ? BuildAndroidEnvironment(definition.Target, androidSdkPath) : null, + DependsOn: dependsOn.Count > 0 ? dependsOn : null, + RequiredRuntimeBindings: dsrouterPlatformArg is null && diagnosticPortAddress is null && options.ProcessId is null ? [ProcessIdToken] : null, + Metadata: new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["tool"] = "dotnet-trace", + ["captureKinds"] = string.Join(",", traceKinds), + ["output"] = traceArtifactPath + }, + IsLongRunning: true, + RequiresManualStop: true, + CanRunParallel: true, + StopTrigger: ProfilingStopTrigger.ManualStop, + ReadyOutputPattern: "Process"), + new ProfilingArtifactMetadata( + Id: $"{definition.Id}-trace", + SessionId: definition.Id, + Kind: ProfilingArtifactKind.Trace, + DisplayName: "Trace capture", + FileName: Path.GetFileName(traceArtifactPath), + RelativePath: traceArtifactPath, + ContentType: "application/json", + CreatedAt: DateTimeOffset.UtcNow, + Properties: CreateArtifactProperties(definition, "trace"))); + } + + private static (ProfilingCommandStep Step, ProfilingArtifactMetadata Artifact) CreateMemoryCaptureStep( + ProfilingSessionDefinition definition, + ProfilingCapturePlanOptions options, + string? dsrouterPlatformArg, + string gcdumpArtifactPath, + List runtimeBindings, + string? diagnosticPortAddress = null, + string? androidSdkPath = null, + bool hasTraceCapture = false) + { + var arguments = new List + { + "collect" + }; + + if (dsrouterPlatformArg is not null) + { + arguments.Add("--dsrouter"); + arguments.Add(dsrouterPlatformArg); + } + else if (diagnosticPortAddress is not null) + { + // Connect to a standalone dsrouter via its IPC address (connect mode, not listen) + arguments.Add("--diagnostic-port"); + arguments.Add($"{diagnosticPortAddress},connect"); + } + else + { + arguments.Add("--process-id"); + arguments.Add(options.ProcessId?.ToString() ?? ProcessIdToken); + if (options.ProcessId is null && runtimeBindings.All(binding => binding.Token != ProcessIdToken)) + { + runtimeBindings.Add(new ProfilingRuntimeBinding( + ProcessIdToken, + "Resolve the process id before collecting a GC dump.", + ExampleValue: "12345")); + } + } + + arguments.Add("-o"); + arguments.Add(gcdumpArtifactPath); + + var dependsOn = new List(); + if (options.LaunchMode == ProfilingCaptureLaunchMode.Launch) + dependsOn.Add("build-and-run"); + if (diagnosticPortAddress is not null) + dependsOn.Add("start-dsrouter"); + if (dsrouterPlatformArg is null && diagnosticPortAddress is null && options.ProcessId is null) + dependsOn.Add("discover-process-id"); + // When both trace and memory are requested, wait for the trace step to + // establish its diagnostic port connection before collecting the GC dump. + if (hasTraceCapture) + dependsOn.Add("capture-trace"); + + return ( + new ProfilingCommandStep( + Id: "capture-memory", + Kind: ProfilingCommandStepKind.CollectArtifacts, + DisplayName: "Collect GC dump", + Description: "Collect a managed memory dump using dotnet-gcdump.", + Command: "dotnet-gcdump", + Arguments: arguments, + WorkingDirectory: options.WorkingDirectory, + Environment: (dsrouterPlatformArg is not null || diagnosticPortAddress is not null) + ? BuildAndroidEnvironment(definition.Target, androidSdkPath) : null, + DependsOn: dependsOn.Count > 0 ? dependsOn : null, + RequiredRuntimeBindings: dsrouterPlatformArg is null && diagnosticPortAddress is null && options.ProcessId is null ? [ProcessIdToken] : null, + Metadata: new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["tool"] = "dotnet-gcdump", + ["output"] = gcdumpArtifactPath + }), + new ProfilingArtifactMetadata( + Id: $"{definition.Id}-memory", + SessionId: definition.Id, + Kind: ProfilingArtifactKind.Export, + DisplayName: "GC dump", + FileName: Path.GetFileName(gcdumpArtifactPath), + RelativePath: gcdumpArtifactPath, + ContentType: "application/octet-stream", + CreatedAt: DateTimeOffset.UtcNow, + Properties: CreateArtifactProperties(definition, "memory"))); + } + + private static ProfilingCommandStep? CreateLogCaptureStep( + ProfilingSessionDefinition definition, + ProfilingCapturePlanOptions options, + string logsArtifactPath, + string? androidSdkPath) + { + switch (definition.Target.Platform, definition.Target.Kind) + { + case (ProfilingTargetPlatform.Android, ProfilingTargetKind.PhysicalDevice): + case (ProfilingTargetPlatform.Android, ProfilingTargetKind.Emulator): + Dictionary? environment = null; + if (!string.IsNullOrWhiteSpace(androidSdkPath)) + { + environment = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["ANDROID_HOME"] = androidSdkPath + }; + } + + return new ProfilingCommandStep( + Id: "capture-logs", + Kind: ProfilingCommandStepKind.Capture, + DisplayName: "Stream Android logs", + Description: $"Stream adb logcat output for {definition.Target.DisplayName}. Redirect output to {logsArtifactPath}.", + Command: "adb", + Arguments: ["-s", definition.Target.Identifier, "logcat", "-v", "threadtime"], + WorkingDirectory: options.WorkingDirectory, + Environment: environment, + Metadata: new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["tool"] = "adb", + ["outputHint"] = logsArtifactPath + }, + IsLongRunning: true, + RequiresManualStop: true, + CanRunParallel: true, + StopTrigger: ProfilingStopTrigger.OnPipelineStop); + + case (ProfilingTargetPlatform.iOS, ProfilingTargetKind.Simulator): + return new ProfilingCommandStep( + Id: "capture-logs", + Kind: ProfilingCommandStepKind.Capture, + DisplayName: "Stream simulator logs", + Description: $"Stream simulator logs for {definition.Target.DisplayName}. Redirect output to {logsArtifactPath}.", + Command: "xcrun", + Arguments: ["simctl", "spawn", definition.Target.Identifier, "log", "stream", "--style", "ndjson", "--level", "debug"], + WorkingDirectory: options.WorkingDirectory, + Metadata: new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["tool"] = "xcrun", + ["outputHint"] = logsArtifactPath + }, + IsLongRunning: true, + RequiresManualStop: true, + CanRunParallel: true, + StopTrigger: ProfilingStopTrigger.OnPipelineStop); + + case (ProfilingTargetPlatform.iOS, ProfilingTargetKind.PhysicalDevice): + return new ProfilingCommandStep( + Id: "capture-logs", + Kind: ProfilingCommandStepKind.Capture, + DisplayName: "Stream device logs", + Description: $"Stream physical device logs for {definition.Target.DisplayName}. Redirect output to {logsArtifactPath}.", + Command: "idevicesyslog", + Arguments: ["-u", definition.Target.Identifier], + WorkingDirectory: options.WorkingDirectory, + Metadata: new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["tool"] = "idevicesyslog", + ["outputHint"] = logsArtifactPath, + ["alternativeTool"] = "pymobiledevice3 syslog live --udid " + }, + IsLongRunning: true, + RequiresManualStop: true, + CanRunParallel: true, + StopTrigger: ProfilingStopTrigger.OnPipelineStop); + + default: + return null; + } + } + + private async Task TryGetAndroidSdkPathAsync() + { + try + { + return await _androidSdkSettingsService.GetEffectiveSdkPathAsync(); + } + catch (Exception ex) + { + _loggingService.LogDebug($"Failed to resolve Android SDK path for profiling orchestration: {ex.Message}"); + return null; + } + } + + private static string? GetDsRouterPlatformArg(ProfilingTarget target) => + (target.Platform, target.Kind) switch + { + (ProfilingTargetPlatform.Android, ProfilingTargetKind.Emulator) => "android-emu", + (ProfilingTargetPlatform.Android, ProfilingTargetKind.PhysicalDevice) => "android", + (ProfilingTargetPlatform.iOS, ProfilingTargetKind.Simulator) => "ios-sim", + (ProfilingTargetPlatform.iOS, ProfilingTargetKind.PhysicalDevice) => "ios", + _ => null + }; + + /// + /// Build environment variables for Android targets so that adb/dsrouter target + /// the correct device when multiple devices or emulators are connected. + /// + private static Dictionary? BuildAndroidEnvironment( + ProfilingTarget target, + string? androidSdkPath) + { + if (target.Platform != ProfilingTargetPlatform.Android) + return null; + + var env = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (!string.IsNullOrWhiteSpace(androidSdkPath)) + env["ANDROID_HOME"] = androidSdkPath; + if (!string.IsNullOrWhiteSpace(target.Identifier)) + env["ANDROID_SERIAL"] = target.Identifier; + return env.Count > 0 ? env : null; + } + + private static IReadOnlyDictionary CreateArtifactProperties( + ProfilingSessionDefinition definition, + string category) + { + return new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["targetPlatform"] = definition.Target.Platform.ToString(), + ["targetIdentifier"] = definition.Target.Identifier, + ["scenario"] = definition.Scenario.ToString(), + ["category"] = category + }; + } +} diff --git a/src/MauiSherpa.Core/Services/ProfilingCatalogService.cs b/src/MauiSherpa.Core/Services/ProfilingCatalogService.cs new file mode 100644 index 00000000..29b0139d --- /dev/null +++ b/src/MauiSherpa.Core/Services/ProfilingCatalogService.cs @@ -0,0 +1,218 @@ +using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Models.Profiling; + +namespace MauiSherpa.Core.Services; + +public class ProfilingCatalogService : IProfilingCatalogService +{ + private static readonly IReadOnlyDictionary BuiltInScenarios = + new Dictionary + { + [ProfilingScenarioKind.Launch] = new( + ProfilingScenarioKind.Launch, + "Launch & startup", + "Capture cold or warm start behavior with startup, CPU, and memory signals.", + [ProfilingCaptureKind.Startup, ProfilingCaptureKind.Cpu, ProfilingCaptureKind.Memory], + TimeSpan.FromMinutes(2)), + [ProfilingScenarioKind.Interaction] = new( + ProfilingScenarioKind.Interaction, + "Interaction trace", + "Focus on a bounded interaction such as tapping through a flow or completing a task.", + [ProfilingCaptureKind.Cpu, ProfilingCaptureKind.Rendering, ProfilingCaptureKind.Memory], + TimeSpan.FromMinutes(5), + SupportsContinuousCapture: true), + [ProfilingScenarioKind.Scrolling] = new( + ProfilingScenarioKind.Scrolling, + "Scrolling & rendering", + "Measure rendering smoothness and CPU pressure during scrolling-heavy experiences.", + [ProfilingCaptureKind.Rendering, ProfilingCaptureKind.Cpu, ProfilingCaptureKind.Memory], + TimeSpan.FromMinutes(3), + SupportsContinuousCapture: true), + [ProfilingScenarioKind.BackgroundWork] = new( + ProfilingScenarioKind.BackgroundWork, + "Background work", + "Profile sync, notifications, or other longer-running work that happens away from the main UI.", + [ProfilingCaptureKind.Cpu, ProfilingCaptureKind.Network, ProfilingCaptureKind.Energy], + TimeSpan.FromMinutes(10), + SupportsContinuousCapture: true), + [ProfilingScenarioKind.MemoryInvestigation] = new( + ProfilingScenarioKind.MemoryInvestigation, + "Memory investigation", + "Use memory-oriented captures to investigate leaks, spikes, and long-lived allocations.", + [ProfilingCaptureKind.Memory, ProfilingCaptureKind.Cpu, ProfilingCaptureKind.Logs], + TimeSpan.FromMinutes(15), + SupportsContinuousCapture: true) + }; + + private static readonly IReadOnlyDictionary BuiltInCapabilities = + new Dictionary + { + [ProfilingTargetPlatform.Android] = new( + ProfilingTargetPlatform.Android, + "Android", + [ProfilingTargetKind.PhysicalDevice, ProfilingTargetKind.Emulator], + [ProfilingCaptureKind.Startup, ProfilingCaptureKind.Cpu, ProfilingCaptureKind.Memory, ProfilingCaptureKind.Network, ProfilingCaptureKind.Rendering, ProfilingCaptureKind.Energy, ProfilingCaptureKind.SystemTrace, ProfilingCaptureKind.Logs], + [ProfilingArtifactKind.Trace, ProfilingArtifactKind.Metrics, ProfilingArtifactKind.Screenshot, ProfilingArtifactKind.Logs, ProfilingArtifactKind.Export, ProfilingArtifactKind.Report], + BuiltInScenarios.Keys.ToArray(), + SupportsLaunchProfiling: true, + SupportsAttachToProcess: true, + SupportsLiveMetrics: true, + SupportsSymbolication: false, + Notes: "Initial Android abstractions assume adb-backed devices and emulators."), + [ProfilingTargetPlatform.iOS] = new( + ProfilingTargetPlatform.iOS, + "iOS", + [ProfilingTargetKind.PhysicalDevice, ProfilingTargetKind.Simulator], + [ProfilingCaptureKind.Startup, ProfilingCaptureKind.Cpu, ProfilingCaptureKind.Memory, ProfilingCaptureKind.Network, ProfilingCaptureKind.Rendering, ProfilingCaptureKind.Energy, ProfilingCaptureKind.SystemTrace, ProfilingCaptureKind.Logs], + [ProfilingArtifactKind.Trace, ProfilingArtifactKind.Metrics, ProfilingArtifactKind.Screenshot, ProfilingArtifactKind.Logs, ProfilingArtifactKind.Export, ProfilingArtifactKind.Report], + BuiltInScenarios.Keys.ToArray(), + SupportsLaunchProfiling: true, + SupportsAttachToProcess: true, + SupportsLiveMetrics: true, + SupportsSymbolication: true, + Notes: "Initial iOS abstractions cover both physical devices and simulators."), + [ProfilingTargetPlatform.MacCatalyst] = new( + ProfilingTargetPlatform.MacCatalyst, + "Mac Catalyst", + [ProfilingTargetKind.Desktop], + [ProfilingCaptureKind.Startup, ProfilingCaptureKind.Cpu, ProfilingCaptureKind.Memory, ProfilingCaptureKind.Network, ProfilingCaptureKind.Rendering, ProfilingCaptureKind.Energy, ProfilingCaptureKind.SystemTrace, ProfilingCaptureKind.Logs], + [ProfilingArtifactKind.Trace, ProfilingArtifactKind.Metrics, ProfilingArtifactKind.Logs, ProfilingArtifactKind.Export, ProfilingArtifactKind.Report], + BuiltInScenarios.Keys.ToArray(), + SupportsLaunchProfiling: true, + SupportsAttachToProcess: true, + SupportsLiveMetrics: true, + SupportsSymbolication: true, + Notes: "Mac Catalyst is treated as a desktop target with Apple tooling semantics."), + [ProfilingTargetPlatform.MacOS] = new( + ProfilingTargetPlatform.MacOS, + "macOS", + [ProfilingTargetKind.Desktop], + [ProfilingCaptureKind.Startup, ProfilingCaptureKind.Cpu, ProfilingCaptureKind.Memory, ProfilingCaptureKind.Network, ProfilingCaptureKind.Rendering, ProfilingCaptureKind.Energy, ProfilingCaptureKind.SystemTrace, ProfilingCaptureKind.Logs], + [ProfilingArtifactKind.Trace, ProfilingArtifactKind.Metrics, ProfilingArtifactKind.Logs, ProfilingArtifactKind.Export, ProfilingArtifactKind.Report], + BuiltInScenarios.Keys.ToArray(), + SupportsLaunchProfiling: true, + SupportsAttachToProcess: true, + SupportsLiveMetrics: true, + SupportsSymbolication: true, + Notes: "macOS profiling is modeled as a desktop target that can evolve beyond MAUI-specific flows."), + [ProfilingTargetPlatform.Windows] = new( + ProfilingTargetPlatform.Windows, + "Windows", + [ProfilingTargetKind.Desktop], + [ProfilingCaptureKind.Startup, ProfilingCaptureKind.Cpu, ProfilingCaptureKind.Memory, ProfilingCaptureKind.Network, ProfilingCaptureKind.Rendering, ProfilingCaptureKind.Energy, ProfilingCaptureKind.SystemTrace, ProfilingCaptureKind.Logs], + [ProfilingArtifactKind.Trace, ProfilingArtifactKind.Metrics, ProfilingArtifactKind.Logs, ProfilingArtifactKind.Export, ProfilingArtifactKind.Report], + BuiltInScenarios.Keys.ToArray(), + SupportsLaunchProfiling: true, + SupportsAttachToProcess: true, + SupportsLiveMetrics: true, + SupportsSymbolication: false, + Notes: "Windows support is modeled for desktop processes and future tooling adapters.") + }; + + private readonly IReadOnlyDictionary _capabilityProviders; + + public ProfilingCatalogService(IEnumerable capabilityProviders) + { + _capabilityProviders = capabilityProviders + .GroupBy(provider => provider.Platform) + .ToDictionary(group => group.Key, group => group.Last()); + } + + public async Task GetCatalogAsync(CancellationToken ct = default) + { + var platforms = new List(); + + foreach (var platform in Enum.GetValues()) + platforms.Add(await GetCapabilitiesAsync(platform, ct)); + + return new ProfilingCatalog(platforms, BuiltInScenarios.Values.ToArray()); + } + + public async Task GetCapabilitiesAsync(ProfilingTargetPlatform platform, CancellationToken ct = default) + { + if (!BuiltInCapabilities.TryGetValue(platform, out var builtInCapabilities)) + throw new ArgumentOutOfRangeException(nameof(platform), platform, "Unsupported profiling platform."); + + if (_capabilityProviders.TryGetValue(platform, out var provider)) + { + var providerCapabilities = await provider.GetCapabilitiesAsync(ct); + if (providerCapabilities is not null) + return providerCapabilities; + } + + return builtInCapabilities; + } + + public ProfilingSessionDefinition CreateSessionDefinition( + ProfilingTarget target, + ProfilingScenarioKind scenario, + string? name = null, + IReadOnlyList? captureKinds = null, + string? appId = null, + TimeSpan? duration = null, + IReadOnlyDictionary? tags = null) + { + if (!BuiltInScenarios.TryGetValue(scenario, out var scenarioDefinition)) + throw new ArgumentOutOfRangeException(nameof(scenario), scenario, "Unknown profiling scenario."); + + var normalizedName = string.IsNullOrWhiteSpace(name) + ? $"{target.DisplayName} - {scenarioDefinition.DisplayName}" + : name.Trim(); + + var normalizedCaptureKinds = captureKinds is { Count: > 0 } + ? captureKinds.Distinct().ToArray() + : scenarioDefinition.DefaultCaptureKinds.ToArray(); + + return new ProfilingSessionDefinition( + Guid.NewGuid().ToString("N"), + normalizedName, + target, + scenario, + normalizedCaptureKinds, + appId, + duration ?? scenarioDefinition.SuggestedDuration, + tags is null ? new Dictionary() : new Dictionary(tags), + DateTimeOffset.UtcNow); + } + + public ProfilingSessionValidationResult ValidateSessionDefinition( + ProfilingSessionDefinition definition, + ProfilingPlatformCapabilities capabilities) + { + var errors = new List(); + + if (string.IsNullOrWhiteSpace(definition.Name)) + errors.Add("A profiling session name is required."); + + if (string.IsNullOrWhiteSpace(definition.Target.Identifier)) + errors.Add("A profiling target identifier is required."); + + if (definition.Target.Platform != capabilities.Platform) + errors.Add($"Target platform {definition.Target.Platform} does not match {capabilities.DisplayName} capabilities."); + + if (!capabilities.SupportedTargetKinds.Contains(definition.Target.Kind)) + errors.Add($"{definition.Target.Kind} targets are not supported on {capabilities.DisplayName}."); + + if (!capabilities.SupportedScenarios.Contains(definition.Scenario)) + errors.Add($"{definition.Scenario} is not supported on {capabilities.DisplayName}."); + + if (definition.CaptureKinds.Count == 0) + errors.Add("At least one capture kind is required."); + + if (definition.Duration is { } duration && duration <= TimeSpan.Zero) + errors.Add("Duration must be greater than zero."); + + var unsupportedCaptureKinds = definition.CaptureKinds + .Where(kind => !capabilities.SupportedCaptureKinds.Contains(kind)) + .Distinct() + .ToArray(); + + if (unsupportedCaptureKinds.Length > 0) + errors.Add($"Unsupported capture kinds for {capabilities.DisplayName}: {string.Join(", ", unsupportedCaptureKinds)}."); + + return new ProfilingSessionValidationResult( + errors.Count == 0, + errors, + unsupportedCaptureKinds); + } +} diff --git a/src/MauiSherpa.Core/Services/ProfilingContextService.cs b/src/MauiSherpa.Core/Services/ProfilingContextService.cs new file mode 100644 index 00000000..db59b4a5 --- /dev/null +++ b/src/MauiSherpa.Core/Services/ProfilingContextService.cs @@ -0,0 +1,322 @@ +using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Models.DevFlow; +using MauiSherpa.Core.Models.Profiling; + +namespace MauiSherpa.Core.Services; + +/// +/// Builds lightweight profiling summaries from live MauiDevFlow data. +/// +public class ProfilingContextService : IProfilingContextService +{ + private const string DefaultHost = "localhost"; + private const int DefaultAgentPort = 9223; + private const int DefaultBrokerPort = 19223; + + private readonly ILoggingService _logger; + private readonly Func _clientFactory; + + public ProfilingContextService(ILoggingService logger) + : this(logger, (host, port) => new DevFlowAgentClient(host, port)) + { + } + + internal ProfilingContextService( + ILoggingService logger, + Func clientFactory) + { + _logger = logger; + _clientFactory = clientFactory; + } + + public async Task> GetAvailableTargetsAsync(CancellationToken ct = default) + { + try + { + var agents = await DevFlowAgentClient.GetBrokerAgentsAsync(DefaultHost, DefaultBrokerPort, ct); + if (agents.Count > 0) + { + return agents + .OrderByDescending(a => a.ConnectedAt ?? DateTimeOffset.MinValue) + .ThenBy(a => a.AppName ?? a.Project ?? a.Id, StringComparer.OrdinalIgnoreCase) + .Select(a => new ProfilingTargetInfo( + a.Id, + DefaultHost, + a.Port, + a.AppName ?? a.Project ?? a.Id, + a.AppName, + a.Platform, + a.Project, + a.Tfm, + a.ConnectedAt, + true, + "broker")) + .ToList(); + } + } + catch (Exception ex) + { + _logger.LogDebug($"Profiling target discovery via broker failed: {ex.Message}"); + } + + var directTarget = await TryGetDirectTargetAsync(ct); + return directTarget is null ? Array.Empty() : new[] { directTarget }; + } + + public async Task GetSnapshotAsync(ProfilingSnapshotOptions options, CancellationToken ct = default) + { + var targets = await GetAvailableTargetsAsync(ct); + if (targets.Count == 0) + { + return new ProfilingSnapshotResult( + null, + "No local profiling targets are available. Start a MAUI app with MauiDevFlow enabled, then try again."); + } + + var resolvedTarget = ResolveTarget(targets, options.TargetId); + if (resolvedTarget.Target is null) + { + return new ProfilingSnapshotResult( + null, + options.TargetId is null + ? "No profiling target could be resolved." + : $"Profiling target '{options.TargetId}' was not found. Use list_profiling_targets to see available targets."); + } + + using var client = _clientFactory(resolvedTarget.Target.Host, resolvedTarget.Target.Port); + var status = await client.GetStatusAsync(ct); + if (status == null) + { + return new ProfilingSnapshotResult( + null, + $"Could not connect to profiling target '{resolvedTarget.Target.DisplayName}'."); + } + + var notes = new List(); + if (!string.IsNullOrWhiteSpace(resolvedTarget.Note)) + { + notes.Add(resolvedTarget.Note); + } + + var networkSampleSize = Math.Clamp(options.NetworkSampleSize, 5, 200); + ProfilingNetworkSummary? networkSummary = null; + if (options.IncludeNetworkSummary) + { + var requests = await client.GetNetworkRequestsAsync(ct); + networkSummary = BuildNetworkSummary(requests, networkSampleSize); + if (networkSummary.SampleSize == 0) + { + notes.Add("No network requests have been captured yet for this target."); + } + } + + ProfilingVisualTreeSummary? visualTreeSummary = null; + if (options.IncludeVisualTreeSummary) + { + var roots = await client.GetTreeAsync(ct: ct); + visualTreeSummary = BuildVisualTreeSummary(roots); + if (visualTreeSummary.TotalElementCount == 0) + { + notes.Add("The visual tree is currently empty or could not be enumerated."); + } + } + + var runtime = new ProfilingRuntimeInfo( + status.Agent, + status.Version, + status.Platform, + status.DeviceType, + status.Idiom, + status.AppName, + status.Running, + status.CdpReady, + status.CdpWebViewCount); + + return new ProfilingSnapshotResult( + new ProfilingSnapshot( + resolvedTarget.Target, + runtime, + networkSummary, + visualTreeSummary, + notes)); + } + + internal static ProfilingNetworkSummary BuildNetworkSummary( + IEnumerable requests, + int sampleSize) + { + var sample = requests + .OrderByDescending(r => r.Timestamp) + .Take(Math.Max(sampleSize, 0)) + .ToList(); + + if (sample.Count == 0) + { + return new ProfilingNetworkSummary(0, 0, 0, 0, 0, 0, 0, 0, Array.Empty()); + } + + var durations = sample + .Select(r => r.DurationMs) + .OrderBy(v => v) + .ToArray(); + + var successCount = sample.Count(r => string.IsNullOrWhiteSpace(r.Error) && (!r.StatusCode.HasValue || r.StatusCode.Value < 400)); + var failureCount = sample.Count - successCount; + + var slowestRequests = sample + .OrderByDescending(r => r.DurationMs) + .ThenBy(r => r.Url, StringComparer.OrdinalIgnoreCase) + .Take(5) + .Select(r => new ProfilingRequestSummary( + r.Method, + r.Url, + r.StatusCode, + r.DurationMs, + r.Error)) + .ToList(); + + return new ProfilingNetworkSummary( + sample.Count, + successCount, + failureCount, + sample.Average(r => r.DurationMs), + CalculatePercentile(durations, 0.95), + durations[^1], + sample.Sum(r => r.RequestSize ?? 0), + sample.Sum(r => r.ResponseSize ?? 0), + slowestRequests); + } + + internal static ProfilingVisualTreeSummary BuildVisualTreeSummary(IEnumerable roots) + { + var rootList = roots.ToList(); + var flattened = FlattenTree(rootList).ToList(); + + if (flattened.Count == 0) + { + return new ProfilingVisualTreeSummary(0, 0, 0, 0, 0, 0, Array.Empty()); + } + + var topElementTypes = flattened + .GroupBy(item => string.IsNullOrWhiteSpace(item.Element.Type) ? "Unknown" : item.Element.Type) + .Select(group => new ProfilingElementTypeCount(group.Key, group.Count())) + .OrderByDescending(group => group.Count) + .ThenBy(group => group.Type, StringComparer.OrdinalIgnoreCase) + .Take(10) + .ToList(); + + return new ProfilingVisualTreeSummary( + rootList.Count, + flattened.Count, + flattened.Count(item => item.Element.IsVisible), + flattened.Count(item => item.Element.IsFocused), + flattened.Count(item => item.Element.Gestures?.Count > 0), + flattened.Max(item => item.Depth), + topElementTypes); + } + + private async Task TryGetDirectTargetAsync(CancellationToken ct) + { + try + { + using var client = _clientFactory(DefaultHost, DefaultAgentPort); + var status = await client.GetStatusAsync(ct); + if (status == null) + { + return null; + } + + return new ProfilingTargetInfo( + "localhost", + DefaultHost, + DefaultAgentPort, + status.AppName ?? "Local MauiDevFlow agent", + status.AppName, + status.Platform, + null, + null, + null, + status.Running, + "direct"); + } + catch (Exception ex) + { + _logger.LogDebug($"Profiling direct target discovery failed: {ex.Message}"); + return null; + } + } + + private static (ProfilingTargetInfo? Target, string? Note) ResolveTarget( + IReadOnlyList targets, + string? targetId) + { + if (!string.IsNullOrWhiteSpace(targetId)) + { + var match = targets.FirstOrDefault(t => + t.Id.Equals(targetId, StringComparison.OrdinalIgnoreCase) || + t.DisplayName.Equals(targetId, StringComparison.OrdinalIgnoreCase) || + (t.AppName?.Equals(targetId, StringComparison.OrdinalIgnoreCase) ?? false)); + + return (match, null); + } + + if (targets.Count == 1) + { + return (targets[0], null); + } + + var selected = targets + .OrderByDescending(t => t.ConnectedAt ?? DateTimeOffset.MinValue) + .ThenBy(t => t.DisplayName, StringComparer.OrdinalIgnoreCase) + .First(); + + return (selected, $"Multiple profiling targets were available. Auto-selected '{selected.DisplayName}'."); + } + + private static IEnumerable<(DevFlowElementInfo Element, int Depth)> FlattenTree(IEnumerable roots) + { + var stack = new Stack<(DevFlowElementInfo Element, int Depth)>( + roots.Reverse().Select(root => (root, 1))); + + while (stack.Count > 0) + { + var current = stack.Pop(); + yield return current; + + if (current.Element.Children == null) + { + continue; + } + + for (var i = current.Element.Children.Count - 1; i >= 0; i--) + { + stack.Push((current.Element.Children[i], current.Depth + 1)); + } + } + } + + private static double CalculatePercentile(IReadOnlyList orderedValues, double percentile) + { + if (orderedValues.Count == 0) + { + return 0; + } + + if (orderedValues.Count == 1) + { + return orderedValues[0]; + } + + var position = (orderedValues.Count - 1) * percentile; + var lowerIndex = (int)Math.Floor(position); + var upperIndex = (int)Math.Ceiling(position); + + if (lowerIndex == upperIndex) + { + return orderedValues[lowerIndex]; + } + + var fraction = position - lowerIndex; + return orderedValues[lowerIndex] + ((orderedValues[upperIndex] - orderedValues[lowerIndex]) * fraction); + } +} diff --git a/src/MauiSherpa.Core/Services/ProfilingPrerequisitesService.cs b/src/MauiSherpa.Core/Services/ProfilingPrerequisitesService.cs new file mode 100644 index 00000000..6f604369 --- /dev/null +++ b/src/MauiSherpa.Core/Services/ProfilingPrerequisitesService.cs @@ -0,0 +1,492 @@ +using System.Diagnostics; +using System.Text.RegularExpressions; +using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Models.Profiling; +using MauiSherpa.Workloads.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace MauiSherpa.Core.Services; + +public class ProfilingPrerequisitesService : IProfilingPrerequisitesService +{ + private static readonly StringComparer ToolComparer = StringComparer.OrdinalIgnoreCase; + private static readonly Regex ToolListLineRegex = new( + @"^(?\S+)\s+(?\S+)\s+(?.+?)\s*$", + RegexOptions.Compiled); + private static readonly Regex VersionRegex = new(@"(?\d+(?:\.\d+)+(?:[-+][^\s]+)?)", RegexOptions.Compiled); + + private readonly IDoctorService _doctorService; + private readonly IPlatformService _platformService; + private readonly ILoggingService _loggingService; + private readonly ILogger _logger; + private readonly Func> _executeProcessAsync; + + public ProfilingPrerequisitesService( + IDoctorService doctorService, + IPlatformService platformService, + ILoggingService loggingService, + ILoggerFactory? loggerFactory = null) + : this(doctorService, platformService, loggingService, ExecuteProcessAsync, loggerFactory) + { + } + + internal ProfilingPrerequisitesService( + IDoctorService doctorService, + IPlatformService platformService, + ILoggingService loggingService, + Func> executeProcessAsync, + ILoggerFactory? loggerFactory = null) + { + _doctorService = doctorService; + _platformService = platformService; + _loggingService = loggingService; + _executeProcessAsync = executeProcessAsync; + _logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger(); + } + + public async Task GetPrerequisitesAsync( + ProfilingTargetPlatform platform, + IReadOnlyList? captureKinds = null, + string? workingDirectory = null, + CancellationToken ct = default) + { + var normalizedCaptureKinds = captureKinds? + .Distinct() + .OrderBy(kind => kind) + .ToArray() ?? Array.Empty(); + + var doctorContext = await _doctorService.GetContextAsync(workingDirectory); + var doctorReport = await _doctorService.RunDoctorAsync(doctorContext); + var dotNetExecutablePath = _doctorService.GetDotNetExecutablePath(); + var effectiveWorkingDirectory = workingDirectory ?? doctorContext.WorkingDirectory; + + var statuses = new List(); + AddHostPlatformStatus(platform, statuses); + AddDoctorDependencyStatus( + doctorReport, + statuses, + ".NET SDK", + ProfilingPrerequisiteKind.DotNetSdk, + isRequired: true, + requiredVersion: doctorContext.ResolvedSdkVersion ?? doctorContext.PinnedSdkVersion, + recommendedVersion: doctorContext.ActiveSdkVersion); + AddPlatformDependencyStatuses(platform, doctorReport, statuses); + + var discoveredTools = await DiscoverDotNetToolsAsync(dotNetExecutablePath, effectiveWorkingDirectory, ct); + AddDotNetToolStatus( + statuses, + discoveredTools, + "dotnet-trace", + "dotnet-trace", + isRequired: RequiresTraceTool(normalizedCaptureKinds), + missingMessage: "Install dotnet-trace to capture EventPipe traces for profiling workflows."); + + AddDotNetToolStatus( + statuses, + discoveredTools, + "dotnet-gcdump", + "dotnet-gcdump", + isRequired: RequiresGcDumpTool(normalizedCaptureKinds), + missingMessage: "Install dotnet-gcdump to collect memory dumps for profiling sessions."); + + AddDotNetToolStatus( + statuses, + discoveredTools, + "dotnet-dsrouter", + "dotnet-dsrouter", + isRequired: RequiresDiagnosticsRouter(platform, normalizedCaptureKinds), + missingMessage: "Install dotnet-dsrouter to bridge diagnostics traffic for mobile profiling."); + + return new ProfilingPrerequisiteReport( + new ProfilingPrerequisiteContext( + platform, + normalizedCaptureKinds, + effectiveWorkingDirectory, + dotNetExecutablePath, + doctorContext), + statuses, + DateTimeOffset.UtcNow); + } + + private void AddHostPlatformStatus( + ProfilingTargetPlatform platform, + List statuses) + { + var hostPlatform = _platformService.PlatformName; + var (supported, message) = platform switch + { + ProfilingTargetPlatform.iOS or ProfilingTargetPlatform.MacCatalyst or ProfilingTargetPlatform.MacOS + when !_platformService.IsMacOS && !_platformService.IsMacCatalyst + => (false, $"{platform} profiling requires a macOS host."), + ProfilingTargetPlatform.Windows when !_platformService.IsWindows + => (false, "Windows profiling requires a Windows host."), + _ => (true, $"Host platform '{hostPlatform}' supports {platform} profiling prerequisites.") + }; + + statuses.Add(new ProfilingPrerequisiteStatus( + "Host Platform", + ProfilingPrerequisiteKind.HostPlatform, + supported ? DependencyStatusType.Ok : DependencyStatusType.Error, + IsRequired: true, + RequiredVersion: null, + RecommendedVersion: null, + InstalledVersion: hostPlatform, + Message: message)); + } + + private void AddPlatformDependencyStatuses( + ProfilingTargetPlatform platform, + DoctorReport doctorReport, + List statuses) + { + switch (platform) + { + case ProfilingTargetPlatform.Android: + AddDoctorDependencyStatus( + doctorReport, + statuses, + "Android SDK", + ProfilingPrerequisiteKind.AndroidToolchain, + isRequired: true); + AddDoctorDependencyStatus( + doctorReport, + statuses, + "Platform Tools", + ProfilingPrerequisiteKind.AndroidToolchain, + isRequired: true, + upgradeWarningToError: true, + missingMessage: "Android platform-tools (adb) are required to profile Android targets."); + break; + + case ProfilingTargetPlatform.iOS: + AddDoctorDependencyStatus( + doctorReport, + statuses, + "Xcode", + ProfilingPrerequisiteKind.AppleToolchain, + isRequired: true); + AddDoctorDependencyStatus( + doctorReport, + statuses, + "iOS Simulators", + ProfilingPrerequisiteKind.AppleToolchain, + isRequired: false); + break; + + case ProfilingTargetPlatform.MacCatalyst: + AddDoctorDependencyStatus( + doctorReport, + statuses, + "Xcode", + ProfilingPrerequisiteKind.AppleToolchain, + isRequired: true); + break; + + case ProfilingTargetPlatform.Windows: + statuses.Add(new ProfilingPrerequisiteStatus( + "Windows Toolchain", + ProfilingPrerequisiteKind.WindowsToolchain, + DependencyStatusType.Info, + IsRequired: false, + RequiredVersion: null, + RecommendedVersion: null, + InstalledVersion: _platformService.IsWindows ? _platformService.PlatformName : null, + Message: "Windows-specific toolchain validation is not implemented yet for profiling prerequisites.")); + break; + } + } + + private static bool RequiresTraceTool(IReadOnlyList captureKinds) => + captureKinds.Count == 0 || + captureKinds.Any(kind => kind is ProfilingCaptureKind.Startup + or ProfilingCaptureKind.Cpu + or ProfilingCaptureKind.Memory + or ProfilingCaptureKind.Rendering + or ProfilingCaptureKind.Energy + or ProfilingCaptureKind.SystemTrace); + + private static bool RequiresGcDumpTool(IReadOnlyList captureKinds) => + captureKinds.Contains(ProfilingCaptureKind.Memory); + + private static bool RequiresDiagnosticsRouter( + ProfilingTargetPlatform platform, + IReadOnlyList captureKinds) => + platform is ProfilingTargetPlatform.Android or ProfilingTargetPlatform.iOS && + RequiresTraceTool(captureKinds); + + private void AddDoctorDependencyStatus( + DoctorReport doctorReport, + List statuses, + string dependencyName, + ProfilingPrerequisiteKind kind, + bool isRequired, + string? requiredVersion = null, + string? recommendedVersion = null, + bool upgradeWarningToError = false, + string? missingMessage = null) + { + var dependency = doctorReport.Dependencies + .FirstOrDefault(item => item.Name.Equals(dependencyName, StringComparison.OrdinalIgnoreCase)); + + if (dependency is null) + { + statuses.Add(new ProfilingPrerequisiteStatus( + dependencyName, + kind, + isRequired ? DependencyStatusType.Error : DependencyStatusType.Warning, + isRequired, + requiredVersion, + recommendedVersion, + InstalledVersion: null, + Message: missingMessage ?? $"{dependencyName} could not be validated from the doctor report.")); + return; + } + + var status = dependency.Status; + if (upgradeWarningToError && status == DependencyStatusType.Warning) + status = DependencyStatusType.Error; + + var message = dependency.Message; + if (upgradeWarningToError && dependency.Status == DependencyStatusType.Warning && !string.IsNullOrWhiteSpace(message)) + message = $"{message} This is required for profiling readiness."; + + statuses.Add(new ProfilingPrerequisiteStatus( + dependency.Name, + kind, + status, + isRequired, + requiredVersion ?? dependency.RequiredVersion, + recommendedVersion ?? dependency.RecommendedVersion, + dependency.InstalledVersion, + message, + IsFixable: dependency.IsFixable, + FixAction: dependency.FixAction)); + } + + private void AddDotNetToolStatus( + List statuses, + IReadOnlyDictionary discoveredTools, + string commandName, + string packageId, + bool isRequired, + string missingMessage) + { + if (!isRequired && !discoveredTools.ContainsKey(commandName)) + return; + + if (!discoveredTools.TryGetValue(commandName, out var tool)) + { + statuses.Add(new ProfilingPrerequisiteStatus( + commandName, + ProfilingPrerequisiteKind.DotNetTool, + isRequired ? DependencyStatusType.Error : DependencyStatusType.Warning, + isRequired, + RequiredVersion: null, + RecommendedVersion: null, + InstalledVersion: null, + Message: missingMessage, + DiscoveredBy: null, + ExecutablePath: null, + IsFixable: true, + FixAction: $"install-dotnet-tool:{packageId}", + SuggestedCommand: $"dotnet tool install --global {packageId}")); + return; + } + + var message = $"{commandName} {tool.Version} discovered via {tool.Source}."; + + statuses.Add(new ProfilingPrerequisiteStatus( + commandName, + ProfilingPrerequisiteKind.DotNetTool, + DependencyStatusType.Ok, + isRequired, + RequiredVersion: null, + RecommendedVersion: null, + InstalledVersion: tool.Version, + Message: message, + DiscoveredBy: tool.Source, + ExecutablePath: tool.ExecutablePath, + IsFixable: false, + FixAction: null, + SuggestedCommand: null)); + } + + private async Task> DiscoverDotNetToolsAsync( + string dotNetExecutablePath, + string? workingDirectory, + CancellationToken ct) + { + var discoveredTools = new Dictionary(ToolComparer); + + if (!string.IsNullOrWhiteSpace(workingDirectory)) + { + foreach (var tool in await TryListDotNetToolsAsync(dotNetExecutablePath, ["tool", "list", "--local"], "local-manifest", workingDirectory, ct)) + discoveredTools[tool.Command] = tool; + } + + foreach (var tool in await TryListDotNetToolsAsync(dotNetExecutablePath, ["tool", "list", "--global"], "global-tool", null, ct)) + discoveredTools[tool.Command] = tool; + + foreach (var command in new[] { "dotnet-trace", "dotnet-gcdump", "dotnet-dsrouter" }) + { + if (discoveredTools.ContainsKey(command)) + continue; + + var shimPath = ResolveGlobalToolShim(command); + if (shimPath is null) + continue; + + var result = await _executeProcessAsync( + new ProcessRequest(shimPath, ["--version"], workingDirectory), + ct); + + if (!result.Success) + continue; + + var version = TryExtractVersion(result.Output) ?? TryExtractVersion(result.Error); + if (version is null) + continue; + + discoveredTools[command] = new DiscoveredDotNetTool(command, version, "global-shim", shimPath); + } + + return discoveredTools; + } + + private async Task> TryListDotNetToolsAsync( + string dotNetExecutablePath, + string[] arguments, + string source, + string? workingDirectory, + CancellationToken ct) + { + try + { + var result = await _executeProcessAsync( + new ProcessRequest(dotNetExecutablePath, arguments, workingDirectory), + ct); + + if (!result.Success) + { + _logger.LogDebug("Failed to list {Source} dotnet tools: {Error}", source, result.Error); + return Array.Empty(); + } + + return ParseDotNetToolList(result.Output, source); + } + catch (Exception ex) + { + _loggingService.LogDebug($"Failed to discover {source} dotnet tools: {ex.Message}"); + return Array.Empty(); + } + } + + internal static IReadOnlyList ParseDotNetToolList(string output, string source) + { + var results = new List(); + var lines = output + .Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries) + .Select(line => line.Trim()) + .ToArray(); + + var separatorIndex = Array.FindIndex(lines, line => line.StartsWith("---", StringComparison.Ordinal)); + if (separatorIndex < 0) + return results; + + foreach (var line in lines[(separatorIndex + 1)..]) + { + var match = ToolListLineRegex.Match(line); + if (!match.Success) + continue; + + var version = match.Groups["version"].Value; + var commands = match.Groups["commands"].Value + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + foreach (var command in commands) + results.Add(new DiscoveredDotNetTool(command, version, source, ResolveGlobalToolShim(command))); + } + + return results; + } + + private static int? GetSdkMajorVersion(string? version) + { + if (string.IsNullOrWhiteSpace(version)) + return null; + + if (SdkVersion.TryParse(version, out var sdkVersion) && sdkVersion is not null) + return sdkVersion.Major; + + var match = Regex.Match(version, @"^(?\d+)"); + return match.Success && int.TryParse(match.Groups["major"].Value, out var major) ? major : null; + } + + private static string? TryExtractVersion(string? output) + { + if (string.IsNullOrWhiteSpace(output)) + return null; + + var match = VersionRegex.Match(output); + return match.Success ? match.Groups["version"].Value : null; + } + + private static string? ResolveGlobalToolShim(string command) + { + var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + if (string.IsNullOrWhiteSpace(userProfile)) + return null; + + var executableName = OperatingSystem.IsWindows() ? $"{command}.exe" : command; + var candidate = Path.Combine(userProfile, ".dotnet", "tools", executableName); + return File.Exists(candidate) ? candidate : null; + } + + private static async Task ExecuteProcessAsync(ProcessRequest request, CancellationToken ct) + { + try + { + var startInfo = new ProcessStartInfo + { + FileName = request.Command, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + WorkingDirectory = request.WorkingDirectory ?? Environment.CurrentDirectory + }; + + foreach (var arg in request.Arguments) + startInfo.ArgumentList.Add(arg); + + if (request.Environment is not null) + { + foreach (var (key, value) in request.Environment) + startInfo.Environment[key] = value; + } + + using var process = new Process { StartInfo = startInfo }; + process.Start(); + var outputTask = process.StandardOutput.ReadToEndAsync(ct); + var errorTask = process.StandardError.ReadToEndAsync(ct); + await process.WaitForExitAsync(ct); + + var output = await outputTask; + var error = await errorTask; + var finalState = process.ExitCode == 0 ? ProcessState.Completed : ProcessState.Failed; + + return new ProcessResult(process.ExitCode, output, error, TimeSpan.Zero, finalState); + } + catch (Exception ex) + { + return new ProcessResult(-1, string.Empty, ex.Message, TimeSpan.Zero, ProcessState.Failed); + } + } + + internal sealed record DiscoveredDotNetTool( + string Command, + string Version, + string Source, + string? ExecutablePath); +} diff --git a/src/MauiSherpa.Core/Skills/maui-profiling/SKILL.md b/src/MauiSherpa.Core/Skills/maui-profiling/SKILL.md new file mode 100644 index 00000000..778be7bf --- /dev/null +++ b/src/MauiSherpa.Core/Skills/maui-profiling/SKILL.md @@ -0,0 +1,26 @@ +--- +name: maui-profiling +description: Build lightweight profiling context for a running .NET MAUI app using local MauiDevFlow status, network, and visual-tree summaries instead of uploading raw trace artifacts. Use when investigating slow screens, performance regressions, excessive network chatter, or deciding whether deeper trace capture is needed. +--- + +# MAUI Profiling Context + +Use structured, local summaries first. Avoid asking for raw `.nettrace`, `gcdump`, or other large artifacts unless the lightweight snapshot is insufficient. + +## Workflow + +1. Run `get_profiling_catalog` to understand supported scenarios and platform capabilities. +2. Run `list_profiling_targets` to discover locally running MauiDevFlow-enabled apps. +3. Run `get_profiling_snapshot` for the relevant target. +4. Review: + - runtime status (platform, device type, WebView readiness) + - recent network behavior (sample size, failures, latency, slowest requests) + - visual tree complexity (element counts, depth, top element types) +5. Use the summary to narrow the investigation before requesting any deeper traces. + +## Guidance + +- Prefer the default lightweight snapshot first. +- Increase `networkSampleSize` only when recent request volume is too low to explain the issue. +- If multiple targets are connected, specify `targetId` from `list_profiling_targets`. +- Treat the snapshot as context for follow-up recommendations, not as a full profiler replacement. diff --git a/src/MauiSherpa.LinuxGtk/MauiSherpa.LinuxGtk.csproj b/src/MauiSherpa.LinuxGtk/MauiSherpa.LinuxGtk.csproj index 3bf191d5..174f6a7d 100644 --- a/src/MauiSherpa.LinuxGtk/MauiSherpa.LinuxGtk.csproj +++ b/src/MauiSherpa.LinuxGtk/MauiSherpa.LinuxGtk.csproj @@ -39,8 +39,8 @@ - - + + diff --git a/src/MauiSherpa.MacOS/MacOSApp.cs b/src/MauiSherpa.MacOS/MacOSApp.cs index c8a21608..fb1b38b3 100644 --- a/src/MauiSherpa.MacOS/MacOSApp.cs +++ b/src/MauiSherpa.MacOS/MacOSApp.cs @@ -146,6 +146,12 @@ void AddAppMenuItems(BlazorContentPage blazorPage) doctorItem.Target = doctorHandler; appMenu.InsertItem(doctorItem, insertIndex++); + var profilingHandler = new MenuActionHandler(() => blazorPage.NavigateToRoute("/profiling")); + _menuHandlers.Add(profilingHandler); + var profilingItem = new NSMenuItem("Profiling", new ObjCRuntime.Selector("menuAction:"), ""); + profilingItem.Target = profilingHandler; + appMenu.InsertItem(profilingItem, insertIndex++); + var sep2 = NSMenuItem.SeparatorItem; appMenu.InsertItem(sep2, insertIndex); } @@ -202,25 +208,18 @@ FlyoutPage CreateFlyoutPage(BlazorContentPage blazorPage) }, new MacOSSidebarItem { - Title = "DevFlow", + Title = "Tools", Children = new List { + new() { Title = "Profiling", SystemImage = "chart.bar.xaxis", Tag = "/profiling" }, new() { Title = "App Inspector", SystemImage = "wand.and.stars", Tag = "/devflow" }, +#if DEBUG + new() { Title = "Debug UI", SystemImage = "ant", Tag = "/debug" }, +#endif } }, }; -#if DEBUG - sidebarItems.Add(new MacOSSidebarItem - { - Title = "Development", - Children = new List - { - new() { Title = "Debug UI", SystemImage = "ant", Tag = "/debug" }, - } - }); -#endif - MacOSFlyoutPage.SetSidebarItems(flyoutPage, sidebarItems); _sidebarItems = sidebarItems; MacOSFlyoutPage.SetSidebarSelectionChanged(flyoutPage, item => diff --git a/src/MauiSherpa.MacOS/MacOSMauiProgram.cs b/src/MauiSherpa.MacOS/MacOSMauiProgram.cs index 4a63db4c..42424c98 100644 --- a/src/MauiSherpa.MacOS/MacOSMauiProgram.cs +++ b/src/MauiSherpa.MacOS/MacOSMauiProgram.cs @@ -73,7 +73,11 @@ public static MauiApp CreateMauiApp() builder.Services.AddSingleton(); // Process execution services - builder.Services.AddSingleton(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(sp => sp.GetRequiredService()); @@ -94,12 +98,19 @@ public static MauiApp CreateMauiApp() builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -199,6 +210,11 @@ public static MauiApp CreateMauiApp() builder.Services.AddSingletonAsImplementedInterfaces(); builder.Services.AddSingletonAsImplementedInterfaces(); builder.Services.AddSingletonAsImplementedInterfaces(); + builder.Services.AddSingletonAsImplementedInterfaces(); + builder.Services.AddSingletonAsImplementedInterfaces(); + builder.Services.AddSingletonAsImplementedInterfaces(); + builder.Services.AddSingletonAsImplementedInterfaces(); + builder.Services.AddSingletonAsImplementedInterfaces(); #if DEBUG builder.Services.AddBlazorWebViewDeveloperTools(); diff --git a/src/MauiSherpa.MacOS/MauiSherpa.MacOS.csproj b/src/MauiSherpa.MacOS/MauiSherpa.MacOS.csproj index d132ae80..21ec64c3 100644 --- a/src/MauiSherpa.MacOS/MauiSherpa.MacOS.csproj +++ b/src/MauiSherpa.MacOS/MauiSherpa.MacOS.csproj @@ -38,8 +38,8 @@ - - + + diff --git a/src/MauiSherpa.ProfilingSample/App.cs b/src/MauiSherpa.ProfilingSample/App.cs new file mode 100644 index 00000000..1d603c24 --- /dev/null +++ b/src/MauiSherpa.ProfilingSample/App.cs @@ -0,0 +1,21 @@ +namespace MauiSherpa.ProfilingSample; + +public sealed class App : Application +{ + protected override Window CreateWindow(IActivationState? activationState) + { + return new Window + { + Title = "Sherpa Profiling Sample", + Width = 1440, + Height = 960, + MinimumWidth = 960, + MinimumHeight = 720, + Page = new NavigationPage(new NativeMainPage()) + { + BarBackgroundColor = Color.FromArgb("#0f172a"), + BarTextColor = Colors.White + } + }; + } +} diff --git a/src/MauiSherpa.ProfilingSample/BlazorMainPage.cs b/src/MauiSherpa.ProfilingSample/BlazorMainPage.cs new file mode 100644 index 00000000..16bd93c7 --- /dev/null +++ b/src/MauiSherpa.ProfilingSample/BlazorMainPage.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Components.WebView.Maui; + +namespace MauiSherpa.ProfilingSample; + +public sealed class BlazorMainPage : ContentPage +{ + public BlazorMainPage() + { + var blazorWebView = new BlazorWebView + { + HostPage = "wwwroot/index.html" + }; + + blazorWebView.RootComponents.Add(new RootComponent + { + Selector = "#app", + ComponentType = typeof(Components.App) + }); + + Content = blazorWebView; + } +} diff --git a/src/MauiSherpa.ProfilingSample/Components/App.razor b/src/MauiSherpa.ProfilingSample/Components/App.razor new file mode 100644 index 00000000..c52170b7 --- /dev/null +++ b/src/MauiSherpa.ProfilingSample/Components/App.razor @@ -0,0 +1,14 @@ + + + + + + + +
+

Route not found

+

The profiling sample only exposes the main dashboard.

+
+
+
+
diff --git a/src/MauiSherpa.ProfilingSample/Components/MainLayout.razor b/src/MauiSherpa.ProfilingSample/Components/MainLayout.razor new file mode 100644 index 00000000..9646dd6e --- /dev/null +++ b/src/MauiSherpa.ProfilingSample/Components/MainLayout.razor @@ -0,0 +1,36 @@ +@inherits LayoutComponentBase + +
+
+
+
Sherpa diagnostics playground
+
+

Sherpa Profiling Sample

+ Multiple manual workloads +
+

A standalone MAUI Blazor sample with CPU, memory, rendering, scrolling, and network-heavy paths designed to make performance investigations obvious.

+
+ +
+
Debug builds expose MauiDevFlow for local profiling snapshots.
+ +
+
+ +
+ @Body +
+
+ +@code { + private bool IsDarkMode { get; set; } = true; + + private string ThemeClass => IsDarkMode ? "theme-dark" : "theme-light"; + + private void ToggleTheme() + { + IsDarkMode = !IsDarkMode; + } +} diff --git a/src/MauiSherpa.ProfilingSample/MauiProgram.cs b/src/MauiSherpa.ProfilingSample/MauiProgram.cs new file mode 100644 index 00000000..81dd53b3 --- /dev/null +++ b/src/MauiSherpa.ProfilingSample/MauiProgram.cs @@ -0,0 +1,41 @@ +using Microsoft.Extensions.Logging; +using MauiSherpa.ProfilingSample.Services; +#if DEBUG +using MauiDevFlow.Agent; +using MauiDevFlow.Blazor; +#endif + +namespace MauiSherpa.ProfilingSample; + +public static class MauiProgram +{ + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder(); + + builder + .UseMauiApp() + .ConfigureFonts(fonts => + { + fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); + fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); + }); + + builder.Services.AddMauiBlazorWebView(); + builder.Services.AddSingleton(); + +#if DEBUG + builder.Services.AddBlazorWebViewDeveloperTools(); + builder.Logging.AddDebug(); + builder.AddMauiDevFlowAgent(options => + { + options.EnableProfiler = true; + options.EnableHighLevelUiHooks = true; + options.EnableDetailedUiHooks = true; + }); + builder.AddMauiBlazorDevFlowTools(); +#endif + + return builder.Build(); + } +} diff --git a/src/MauiSherpa.ProfilingSample/MauiSherpa.ProfilingSample.csproj b/src/MauiSherpa.ProfilingSample/MauiSherpa.ProfilingSample.csproj new file mode 100644 index 00000000..fcaa66be --- /dev/null +++ b/src/MauiSherpa.ProfilingSample/MauiSherpa.ProfilingSample.csproj @@ -0,0 +1,39 @@ + + + + net10.0-android;net10.0-ios;net10.0-maccatalyst + + Exe + MauiSherpa.ProfilingSample + true + true + enable + enable + false + 9241 + + Sherpa Profiling Sample + codes.redth.mauisherpa.profilingsample + 1.0.0 + 1 + + 24.0 + 15.0 + 15.0 + + + + + + + + + + + + + + + + + diff --git a/src/MauiSherpa.ProfilingSample/Models/ProfilingScenarioModels.cs b/src/MauiSherpa.ProfilingSample/Models/ProfilingScenarioModels.cs new file mode 100644 index 00000000..7921fe96 --- /dev/null +++ b/src/MauiSherpa.ProfilingSample/Models/ProfilingScenarioModels.cs @@ -0,0 +1,93 @@ +namespace MauiSherpa.ProfilingSample.Models; + +public sealed record RenderTile(int Id, string Label, int Hue, double Scale, int LoadValue, bool IsHot); + +public sealed record FeedItem(int Id, string Title, string Category, int ActiveUsers, int DurationMs, int Score, string Summary); + +public sealed record MemorySnapshot(string Id, DateTimeOffset Timestamp, long RetainedBytes, long ManagedHeapBytes, int ChunkCount); + +public sealed class NetworkRunResult +{ + public string Url { get; set; } = string.Empty; + + public int? StatusCode { get; set; } + + public bool Success { get; set; } + + public long DurationMs { get; set; } + + public string? Error { get; set; } + + public long Bytes { get; set; } +} + +public sealed class NetworkBurstOptions +{ + public string Mode { get; set; } = "local"; + + public int RequestCount { get; set; } = 12; +} + +public sealed class CpuScenarioState +{ + public bool IsRunning { get; set; } + + public int WorkerCount { get; set; } + + public int DurationSeconds { get; set; } + + public long IterationsCompleted { get; set; } + + public double LastBatchMilliseconds { get; set; } + + public string Status { get; set; } = "Idle"; + + public DateTimeOffset? StartedAt { get; set; } +} + +public sealed class MemoryScenarioState +{ + public bool IsChurning { get; set; } + + public int ChunkCount { get; set; } + + public long RetainedBytes { get; set; } + + public long ManagedHeapBytes { get; set; } + + public string Status { get; set; } = "Idle"; + + public IReadOnlyList Snapshots { get; set; } = Array.Empty(); +} + +public sealed class RenderingScenarioState +{ + public bool IsAnimating { get; set; } + + public int Frame { get; set; } + + public IReadOnlyList Tiles { get; set; } = Array.Empty(); + + public IReadOnlyList FeedItems { get; set; } = Array.Empty(); + + public string Status { get; set; } = "Ready"; +} + +public sealed class NetworkScenarioState +{ + public bool IsRunning { get; set; } + + public string LastMode { get; set; } = "Idle"; + + public int SuccessCount { get; set; } + + public int FailureCount { get; set; } + + public double AverageDurationMs { get; set; } + + public long TotalBytes { get; set; } + + public string Status { get; set; } = "Idle"; + + public IReadOnlyList RecentRequests { get; set; } = Array.Empty(); +} diff --git a/src/MauiSherpa.ProfilingSample/NativeMainPage.cs b/src/MauiSherpa.ProfilingSample/NativeMainPage.cs new file mode 100644 index 00000000..f8097c57 --- /dev/null +++ b/src/MauiSherpa.ProfilingSample/NativeMainPage.cs @@ -0,0 +1,642 @@ +using System.Diagnostics; +using MauiSherpa.ProfilingSample.Models; +using MauiSherpa.ProfilingSample.Services; +using Microsoft.Maui.Controls.Shapes; +using Microsoft.Maui.Layouts; + +namespace MauiSherpa.ProfilingSample; + +public sealed class NativeMainPage : ContentPage, IDisposable +{ + private readonly ProfilingScenarioService _service; + private IDispatcherTimer? _metricsTimer; + private IDispatcherTimer? _animationTimer; + + // Metrics labels + private readonly Label _workingSetLabel = new() { FontSize = 13, FontAttributes = FontAttributes.Bold }; + private readonly Label _privateBytesLabel = new() { FontSize = 13, FontAttributes = FontAttributes.Bold }; + private readonly Label _managedHeapLabel = new() { FontSize = 13, FontAttributes = FontAttributes.Bold }; + private readonly Label _gcGen0Label = new() { FontSize = 13, FontAttributes = FontAttributes.Bold }; + private readonly Label _gcGen1Label = new() { FontSize = 13, FontAttributes = FontAttributes.Bold }; + private readonly Label _gcGen2Label = new() { FontSize = 13, FontAttributes = FontAttributes.Bold }; + + // CPU controls + private readonly Slider _cpuWorkerSlider = new() { Minimum = 1, Maximum = 16, Value = 4 }; + private readonly Slider _cpuDurationSlider = new() { Minimum = 5, Maximum = 180, Value = 30 }; + private readonly Label _cpuWorkerLabel = new() { Text = "4 workers", FontSize = 12 }; + private readonly Label _cpuDurationLabel = new() { Text = "30s", FontSize = 12 }; + private readonly Label _cpuStatusLabel = new() { Text = "Idle", FontSize = 12 }; + private readonly Label _cpuIterationsLabel = new() { Text = "0 iterations", FontSize = 12 }; + private readonly Button _cpuToggleButton = new() { Text = "Start CPU Stress" }; + + // Memory controls + private readonly Label _memoryStatusLabel = new() { FontSize = 12 }; + private readonly Label _memoryRetainedLabel = new() { Text = "Retained: 0 bytes", FontSize = 12 }; + private readonly Label _memoryHeapLabel = new() { Text = "Managed heap: 0 bytes", FontSize = 12 }; + private readonly Label _memoryChunksLabel = new() { Text = "0 chunks", FontSize = 12 }; + private readonly Button _memoryChurnButton = new() { Text = "Start Churn" }; + + // Rendering controls + private readonly Label _renderStatusLabel = new() { Text = "Ready", FontSize = 12 }; + private readonly Label _renderTileCountLabel = new() { Text = "0 tiles", FontSize = 12 }; + private readonly Label _renderFeedCountLabel = new() { Text = "0 feed items", FontSize = 12 }; + private readonly FlexLayout _tileWall = new() { Wrap = FlexWrap.Wrap, AlignContent = FlexAlignContent.Start }; + private readonly VerticalStackLayout _feedList = new() { Spacing = 4 }; + private readonly Button _renderAnimateButton = new() { Text = "Start Animation" }; + + // Network controls + private readonly Slider _networkCountSlider = new() { Minimum = 4, Maximum = 40, Value = 12 }; + private readonly Label _networkCountLabel = new() { Text = "12 requests", FontSize = 12 }; + private readonly Label _networkStatusLabel = new() { Text = "Idle", FontSize = 12 }; + private readonly Label _networkStatsLabel = new() { Text = "", FontSize = 12 }; + private readonly VerticalStackLayout _networkResultsList = new() { Spacing = 2 }; + + public NativeMainPage() + { + Title = "Profiling Sample (Native)"; + _service = Application.Current!.Handler!.MauiContext!.Services.GetRequiredService(); + + var switchButton = new ToolbarItem + { + Text = "Blazor View", + Command = new Command(async () => await Navigation.PushAsync(new BlazorMainPage())) + }; + ToolbarItems.Add(switchButton); + + BackgroundColor = Color.FromArgb("#0f172a"); + Content = BuildLayout(); + + _service.Changed += OnServiceChanged; + StartMetricsTimer(); + } + + private View BuildLayout() + { + return new ScrollView + { + Content = new VerticalStackLayout + { + Spacing = 16, + Padding = new Thickness(16, 24, 16, 16), + Children = + { + BuildHeader(), + BuildMetricsStrip(), + BuildCpuCard(), + BuildMemoryCard(), + BuildRenderingCard(), + BuildNetworkCard() + } + } + }; + } + + private View BuildHeader() + { + return new VerticalStackLayout + { + Spacing = 4, + Children = + { + new Label + { + Text = "SHERPA DIAGNOSTICS PLAYGROUND", + FontSize = 11, + TextColor = Color.FromArgb("#818cf8"), + CharacterSpacing = 1.5 + }, + new Label + { + Text = "Profiling Sample — Native MAUI", + FontSize = 22, + FontAttributes = FontAttributes.Bold, + TextColor = Colors.White + }, + new Label + { + Text = "CPU, memory, rendering, scrolling, and network-heavy paths using native MAUI controls.", + FontSize = 13, + TextColor = Color.FromArgb("#94a3b8") + } + } + }; + } + + private View BuildMetricsStrip() + { + var grid = new Grid + { + ColumnDefinitions = + { + new ColumnDefinition(GridLength.Star), + new ColumnDefinition(GridLength.Star), + new ColumnDefinition(GridLength.Star), + new ColumnDefinition(GridLength.Star), + new ColumnDefinition(GridLength.Star), + new ColumnDefinition(GridLength.Star), + }, + ColumnSpacing = 8, + }; + + grid.Add(MetricCard("Working Set", _workingSetLabel), 0); + grid.Add(MetricCard("Private Bytes", _privateBytesLabel), 1); + grid.Add(MetricCard("Managed Heap", _managedHeapLabel), 2); + grid.Add(MetricCard("GC Gen 0", _gcGen0Label), 3); + grid.Add(MetricCard("GC Gen 1", _gcGen1Label), 4); + grid.Add(MetricCard("GC Gen 2", _gcGen2Label), 5); + + return grid; + } + + private static View MetricCard(string title, Label valueLabel) + { + valueLabel.TextColor = Colors.White; + return new Border + { + Stroke = Color.FromArgb("#1e293b"), + StrokeShape = new RoundRectangle { CornerRadius = 8 }, + BackgroundColor = Color.FromArgb("#111827"), + Padding = new Thickness(10, 8), + Content = new VerticalStackLayout + { + Spacing = 2, + Children = + { + new Label { Text = title, FontSize = 10, TextColor = Color.FromArgb("#64748b") }, + valueLabel + } + } + }; + } + + private View BuildCpuCard() + { + _cpuWorkerSlider.ValueChanged += (_, e) => + { + var v = (int)e.NewValue; + _cpuWorkerLabel.Text = $"{v} worker{(v > 1 ? "s" : "")}"; + }; + _cpuDurationSlider.ValueChanged += (_, e) => _cpuDurationLabel.Text = $"{(int)e.NewValue}s"; + _cpuToggleButton.Clicked += OnCpuToggle; + + return ScenarioCard("CPU Stress", "fa-microchip", new View[] + { + Row("Workers:", _cpuWorkerSlider, _cpuWorkerLabel), + Row("Duration:", _cpuDurationSlider, _cpuDurationLabel), + _cpuToggleButton, + _cpuStatusLabel, + _cpuIterationsLabel, + }); + } + + private View BuildMemoryCard() + { + var allocRow = new FlexLayout + { + Wrap = FlexWrap.Wrap, + JustifyContent = FlexJustify.Start, + Children = + { + SmallButton("8 MB", () => _service.AllocateRetainedMemory(8)), + SmallButton("64 MB", () => _service.AllocateRetainedMemory(64)), + SmallButton("128 MB", () => _service.AllocateRetainedMemory(128)), + SmallButton("512 MB", () => _service.AllocateRetainedMemory(512)), + } + }; + + var actionRow = new FlexLayout + { + Wrap = FlexWrap.Wrap, + JustifyContent = FlexJustify.Start, + Children = + { + SmallButton("Release Half", () => _service.ReleaseHalfMemory()), + SmallButton("Clear All", () => _service.ClearRetainedMemory()), + SmallButton("Snapshot", () => _service.CaptureMemorySnapshot()), + } + }; + + _memoryChurnButton.Clicked += (_, _) => + { + _service.ToggleMemoryChurn(); + }; + + return ScenarioCard("Memory", "fa-memory", new View[] + { + new Label { Text = "Allocate retained memory:", FontSize = 12, TextColor = Color.FromArgb("#94a3b8") }, + allocRow, + actionRow, + _memoryChurnButton, + _memoryRetainedLabel, + _memoryHeapLabel, + _memoryChunksLabel, + _memoryStatusLabel, + }); + } + + private View BuildRenderingCard() + { + var tileRow = new FlexLayout + { + Wrap = FlexWrap.Wrap, + JustifyContent = FlexJustify.Start, + Children = + { + SmallButton("+60 Tiles", () => _service.AddTiles(60)), + SmallButton("+180 Tiles", () => _service.AddTiles(180)), + SmallButton("+480 Tiles", () => _service.AddTiles(480)), + } + }; + + var feedRow = new FlexLayout + { + Wrap = FlexWrap.Wrap, + JustifyContent = FlexJustify.Start, + Children = + { + SmallButton("+180 Feed", () => _service.AddFeedItems(180)), + SmallButton("+500 Feed", () => _service.AddFeedItems(500)), + SmallButton("+1000 Feed", () => _service.AddFeedItems(1000)), + } + }; + + _renderAnimateButton.Clicked += (_, _) => + { + _service.ToggleRenderingAnimation(); + UpdateRenderingAnimation(); + }; + + var resetButton = SmallButton("Reset Scene", () => + { + _service.ResetRenderingScene(); + RebuildTileWall(); + RebuildFeedList(); + }); + + var tileScroll = new ScrollView + { + HeightRequest = 200, + Content = _tileWall + }; + + var feedScroll = new ScrollView + { + HeightRequest = 300, + Content = _feedList + }; + + return ScenarioCard("Rendering", "fa-paint-brush", new View[] + { + tileRow, + feedRow, + new FlexLayout + { + Wrap = FlexWrap.Wrap, + JustifyContent = FlexJustify.Start, + Children = { _renderAnimateButton, resetButton } + }, + _renderStatusLabel, + _renderTileCountLabel, + _renderFeedCountLabel, + new Label { Text = "Tile Wall", FontSize = 11, TextColor = Color.FromArgb("#64748b"), Margin = new Thickness(0, 8, 0, 0) }, + tileScroll, + new Label { Text = "Feed List", FontSize = 11, TextColor = Color.FromArgb("#64748b"), Margin = new Thickness(0, 8, 0, 0) }, + feedScroll, + }); + } + + private View BuildNetworkCard() + { + _networkCountSlider.ValueChanged += (_, e) => _networkCountLabel.Text = $"{(int)e.NewValue} requests"; + + var localButton = SmallButton("Local Burst", async () => + await _service.RunNativeNetworkBurstAsync("local", (int)_networkCountSlider.Value)); + var remoteButton = SmallButton("Remote Mixed", async () => + await _service.RunNativeNetworkBurstAsync("remote-mixed", (int)_networkCountSlider.Value)); + + var resultsScroll = new ScrollView + { + HeightRequest = 200, + Content = _networkResultsList + }; + + return ScenarioCard("Network", "fa-wifi", new View[] + { + Row("Requests:", _networkCountSlider, _networkCountLabel), + new FlexLayout + { + Wrap = FlexWrap.Wrap, + JustifyContent = FlexJustify.Start, + Children = { localButton, remoteButton } + }, + _networkStatusLabel, + _networkStatsLabel, + new Label { Text = "Recent Requests", FontSize = 11, TextColor = Color.FromArgb("#64748b"), Margin = new Thickness(0, 8, 0, 0) }, + resultsScroll, + }); + } + + // --- Helpers --- + + private static View ScenarioCard(string title, string icon, View[] children) + { + var stack = new VerticalStackLayout { Spacing = 8 }; + stack.Add(new Label + { + Text = title, + FontSize = 16, + FontAttributes = FontAttributes.Bold, + TextColor = Colors.White, + Margin = new Thickness(0, 0, 0, 4) + }); + + foreach (var child in children) + { + if (child is Label lbl) + lbl.TextColor ??= Color.FromArgb("#cbd5e1"); + stack.Add(child); + } + + return new Border + { + Stroke = Color.FromArgb("#1e293b"), + StrokeShape = new RoundRectangle { CornerRadius = 10 }, + BackgroundColor = Color.FromArgb("#111827"), + Padding = new Thickness(16), + Content = stack + }; + } + + private static View Row(string label, Slider slider, Label valueLabel) + { + valueLabel.TextColor = Color.FromArgb("#cbd5e1"); + valueLabel.WidthRequest = 90; + valueLabel.HorizontalTextAlignment = TextAlignment.End; + + return new Grid + { + ColumnDefinitions = + { + new ColumnDefinition(new GridLength(80)), + new ColumnDefinition(GridLength.Star), + new ColumnDefinition(new GridLength(90)), + }, + ColumnSpacing = 8, + Children = + { + new Label { Text = label, FontSize = 12, TextColor = Color.FromArgb("#94a3b8"), VerticalTextAlignment = TextAlignment.Center }.Column(0), + slider.Column(1), + valueLabel.Column(2), + } + }; + } + + private static Button SmallButton(string text, Action action) + { + var btn = new Button + { + Text = text, + FontSize = 12, + Padding = new Thickness(12, 6), + Margin = new Thickness(0, 0, 6, 6), + BackgroundColor = Color.FromArgb("#334155"), + TextColor = Colors.White, + CornerRadius = 6, + HeightRequest = 34, + }; + btn.Clicked += (_, _) => action(); + return btn; + } + + private static Button SmallButton(string text, Func action) + { + var btn = new Button + { + Text = text, + FontSize = 12, + Padding = new Thickness(12, 6), + Margin = new Thickness(0, 0, 6, 6), + BackgroundColor = Color.FromArgb("#334155"), + TextColor = Colors.White, + CornerRadius = 6, + HeightRequest = 34, + }; + btn.Clicked += async (_, _) => await action(); + return btn; + } + + // --- State Updates --- + + private void OnServiceChanged() + { + Dispatcher.Dispatch(UpdateUi); + } + + private void UpdateUi() + { + // CPU + _cpuStatusLabel.Text = _service.Cpu.Status; + _cpuIterationsLabel.Text = $"{_service.Cpu.IterationsCompleted:N0} iterations — {_service.Cpu.LastBatchMilliseconds:F1} ms/batch"; + _cpuToggleButton.Text = _service.Cpu.IsRunning ? "Stop CPU Stress" : "Start CPU Stress"; + _cpuToggleButton.BackgroundColor = _service.Cpu.IsRunning + ? Color.FromArgb("#ef4444") : Color.FromArgb("#818cf8"); + + // Memory + _memoryRetainedLabel.Text = $"Retained: {FormatBytes(_service.Memory.RetainedBytes)}"; + _memoryHeapLabel.Text = $"Managed heap: {FormatBytes(_service.Memory.ManagedHeapBytes)}"; + _memoryChunksLabel.Text = $"{_service.Memory.ChunkCount} chunk(s)"; + _memoryStatusLabel.Text = _service.Memory.Status; + _memoryChurnButton.Text = _service.Memory.IsChurning ? "Stop Churn" : "Start Churn"; + _memoryChurnButton.BackgroundColor = _service.Memory.IsChurning + ? Color.FromArgb("#ef4444") : Color.FromArgb("#334155"); + + // Rendering + _renderStatusLabel.Text = _service.Rendering.Status; + _renderTileCountLabel.Text = $"{_service.Rendering.Tiles.Count} tiles"; + _renderFeedCountLabel.Text = $"{_service.Rendering.FeedItems.Count} feed items"; + _renderAnimateButton.Text = _service.Rendering.IsAnimating ? "Stop Animation" : "Start Animation"; + _renderAnimateButton.BackgroundColor = _service.Rendering.IsAnimating + ? Color.FromArgb("#ef4444") : Color.FromArgb("#334155"); + + // Network + _networkStatusLabel.Text = _service.Network.Status; + if (_service.Network.SuccessCount > 0 || _service.Network.FailureCount > 0) + { + _networkStatsLabel.Text = $"✓ {_service.Network.SuccessCount} ✗ {_service.Network.FailureCount} " + + $"Avg: {_service.Network.AverageDurationMs:F0}ms Total: {FormatBytes(_service.Network.TotalBytes)}"; + } + + RebuildNetworkResults(); + } + + private void UpdateMetrics() + { + try + { + var process = Process.GetCurrentProcess(); + _workingSetLabel.Text = FormatBytes(process.WorkingSet64); + _privateBytesLabel.Text = FormatBytes(process.PrivateMemorySize64); + _managedHeapLabel.Text = FormatBytes(GC.GetTotalMemory(false)); + _gcGen0Label.Text = GC.CollectionCount(0).ToString("N0"); + _gcGen1Label.Text = GC.CollectionCount(1).ToString("N0"); + _gcGen2Label.Text = GC.CollectionCount(2).ToString("N0"); + } + catch + { + // Process info may not be available on all platforms + } + } + + private async void OnCpuToggle(object? sender, EventArgs e) + { + if (_service.Cpu.IsRunning) + { + _service.StopCpuStress(); + } + else + { + await _service.StartCpuStressAsync( + (int)_cpuWorkerSlider.Value, + TimeSpan.FromSeconds((int)_cpuDurationSlider.Value)); + } + } + + private void UpdateRenderingAnimation() + { + if (_service.Rendering.IsAnimating) + { + _animationTimer ??= Dispatcher.CreateTimer(); + _animationTimer.Interval = TimeSpan.FromMilliseconds(250); + _animationTimer.Tick += (_, _) => + { + RebuildTileWall(); + if (_service.Rendering.Frame % 6 == 0) + RebuildFeedList(); + }; + _animationTimer.Start(); + } + else + { + _animationTimer?.Stop(); + } + } + + private void RebuildTileWall() + { + var tiles = _service.Rendering.Tiles; + // Only rebuild if count changed significantly to avoid excessive layout + if (Math.Abs(_tileWall.Children.Count - tiles.Count) > 0 || _service.Rendering.IsAnimating) + { + _tileWall.Children.Clear(); + foreach (var tile in tiles.Take(480)) + { + var box = new BoxView + { + WidthRequest = 28, + HeightRequest = 28, + Margin = new Thickness(1), + Color = Color.FromHsla(tile.Hue / 360.0, 0.7, 0.5), + CornerRadius = 4, + }; + _tileWall.Add(box); + } + } + } + + private void RebuildFeedList() + { + var items = _service.Rendering.FeedItems; + _feedList.Children.Clear(); + + foreach (var item in items.Take(200)) + { + var row = new Grid + { + ColumnDefinitions = + { + new ColumnDefinition(GridLength.Star), + new ColumnDefinition(new GridLength(60)), + new ColumnDefinition(new GridLength(80)), + }, + ColumnSpacing = 8, + Padding = new Thickness(8, 4), + BackgroundColor = Color.FromArgb("#1e293b"), + }; + + row.Add(new Label { Text = item.Title, FontSize = 11, TextColor = Colors.White, LineBreakMode = LineBreakMode.TailTruncation }.Column(0)); + row.Add(new Label { Text = item.Category, FontSize = 10, TextColor = Color.FromArgb("#818cf8"), HorizontalTextAlignment = TextAlignment.Center }.Column(1)); + + var progress = new ProgressBar { Progress = item.Score / 100.0, ProgressColor = Color.FromArgb("#818cf8") }; + row.Add(progress.Column(2)); + + _feedList.Add(row); + } + } + + private void RebuildNetworkResults() + { + var results = _service.Network.RecentRequests; + _networkResultsList.Children.Clear(); + + foreach (var result in results) + { + var shortUrl = result.Url.Length > 50 ? "..." + result.Url[^47..] : result.Url; + var row = new Grid + { + ColumnDefinitions = + { + new ColumnDefinition(new GridLength(24)), + new ColumnDefinition(GridLength.Star), + new ColumnDefinition(new GridLength(60)), + new ColumnDefinition(new GridLength(60)), + }, + ColumnSpacing = 6, + Padding = new Thickness(6, 3), + BackgroundColor = Color.FromArgb("#1e293b"), + }; + + var statusColor = result.Success ? Color.FromArgb("#22c55e") : Color.FromArgb("#ef4444"); + row.Add(new Label { Text = result.Success ? "✓" : "✗", TextColor = statusColor, FontSize = 12, HorizontalTextAlignment = TextAlignment.Center }.Column(0)); + row.Add(new Label { Text = shortUrl, FontSize = 10, TextColor = Color.FromArgb("#94a3b8"), LineBreakMode = LineBreakMode.TailTruncation }.Column(1)); + row.Add(new Label { Text = $"{result.DurationMs}ms", FontSize = 10, TextColor = Colors.White, HorizontalTextAlignment = TextAlignment.End }.Column(2)); + row.Add(new Label { Text = FormatBytes(result.Bytes), FontSize = 10, TextColor = Color.FromArgb("#64748b"), HorizontalTextAlignment = TextAlignment.End }.Column(3)); + + _networkResultsList.Add(row); + } + } + + // --- Timers --- + + private void StartMetricsTimer() + { + _metricsTimer = Dispatcher.CreateTimer(); + _metricsTimer.Interval = TimeSpan.FromSeconds(1); + _metricsTimer.Tick += (_, _) => UpdateMetrics(); + _metricsTimer.Start(); + UpdateMetrics(); + } + + private static string FormatBytes(long bytes) + { + if (bytes >= 1024 * 1024 * 1024) return $"{bytes / (1024.0 * 1024 * 1024):F1} GB"; + if (bytes >= 1024 * 1024) return $"{bytes / (1024.0 * 1024):F1} MB"; + if (bytes >= 1024) return $"{bytes / 1024.0:F1} KB"; + return $"{bytes} B"; + } + + public void Dispose() + { + _service.Changed -= OnServiceChanged; + _metricsTimer?.Stop(); + _animationTimer?.Stop(); + } +} + +internal static class GridExtensions +{ + public static T Column(this T view, int column) where T : View + { + Grid.SetColumn(view, column); + return view; + } +} diff --git a/src/MauiSherpa.ProfilingSample/Pages/Home.razor b/src/MauiSherpa.ProfilingSample/Pages/Home.razor new file mode 100644 index 00000000..f72c626f --- /dev/null +++ b/src/MauiSherpa.ProfilingSample/Pages/Home.razor @@ -0,0 +1,415 @@ +@page "/" +@implements IDisposable +@inject ProfilingScenarioService ScenarioService +@inject IJSRuntime JS + +Sherpa Profiling Sample + +
+
+
+
Profiling starter kit
+

Trigger realistic hotspots in a few clicks

+

Use the controls below to spin up CPU pressure, retain large heaps, churn the visual tree, and generate browser-visible fetch traffic from the WebView.

+
+ +
+ + + + +
+
+ +
+
+ Working set + @FormatBytes(_process.WorkingSetBytes) + Private: @FormatBytes(_process.PrivateBytes) +
+
+ Managed heap + @FormatBytes(_process.ManagedHeapBytes) + GC counts: @_process.Gen0Collections / @_process.Gen1Collections / @_process.Gen2Collections +
+
+ CPU loops + @ScenarioService.Cpu.IterationsCompleted.ToString("N0") + Last batch: @FormatDuration(ScenarioService.Cpu.LastBatchMilliseconds) +
+
+ Retained memory + @FormatBytes(ScenarioService.Memory.RetainedBytes) + Chunks: @ScenarioService.Memory.ChunkCount +
+
+ Visual surface + @ScenarioService.Rendering.Tiles.Count tile(s) + Feed rows: @ScenarioService.Rendering.FeedItems.Count +
+
+ Network summary + @ScenarioService.Network.SuccessCount success / @ScenarioService.Network.FailureCount fail + Avg: @FormatDuration(ScenarioService.Network.AverageDurationMs) +
+
+ +
+
+
+
CPU
+

Prime + math worker loop

+
+ @ScenarioService.Cpu.Status +
+ +

Starts multiple background workers that repeatedly scan for primes and sort floating point batches. It creates an obvious CPU ramp without needing external dependencies.

+ +
+ + +
+ +
+ + +
+ +
+
+ Started + @(ScenarioService.Cpu.StartedAt?.ToLocalTime().ToString("T") ?? "Idle") +
+
+ Worker count + @ScenarioService.Cpu.WorkerCount +
+
+ Target duration + @ScenarioService.Cpu.DurationSeconds s +
+
+ Iterations + @ScenarioService.Cpu.IterationsCompleted.ToString("N0") +
+
+
+ +
+
+
+
Memory
+

Retained allocations + background churn

+
+ @ScenarioService.Memory.Status +
+ +

Retain object graphs backed by 1 MB buffers, or enable churn to create repeated allocation and trim cycles that are easy to spot in heap or GC diagnostics.

+ +
+ +
+ Managed heap now + @FormatBytes(ScenarioService.Memory.ManagedHeapBytes) +
+
+ +
+ + + + + +
+ +
+ @if (ScenarioService.Memory.Snapshots.Count == 0) + { +
No memory markers yet.
+ } + else + { + @foreach (var snapshot in ScenarioService.Memory.Snapshots) + { +
+ @snapshot.Timestamp.ToString("T") + @FormatBytes(snapshot.RetainedBytes) retained + @FormatBytes(snapshot.ManagedHeapBytes) heap + @snapshot.ChunkCount chunk(s) +
+ } + } +
+
+ +
+
+
+
Rendering & scrolling
+

Dense tile wall and long feed

+
+ @ScenarioService.Rendering.Status +
+ +

The tile wall creates a broad visual tree that can animate, while the feed creates a tall scroll surface packed with text, metadata, and progress bars.

+ +
+ + + + +
+ +
+
+ @foreach (var tile in ScenarioService.Rendering.Tiles) + { +
+ @tile.Label + @tile.LoadValue% +
+ } +
+ +
+
Frame @ScenarioService.Rendering.Frame • @ScenarioService.Rendering.FeedItems.Count scroll row(s)
+
+ @foreach (var item in ScenarioService.Rendering.FeedItems) + { +
+
+
+ @item.Title + @item.Category +
+ @item.DurationMs ms +
+

@item.Summary

+
+ @item.ActiveUsers active users +
+ @item.Score score +
+
+ } +
+
+
+
+ +
+
+
+
Network-ish
+

WebView-visible fetch bursts

+
+ @ScenarioService.Network.Status +
+ +

These runs fire fetches from JavaScript inside the Blazor WebView so MauiDevFlow and Sherpa can observe them. Local mode always works; remote mixed mode intentionally includes slow and failing requests when available.

+ +
+ +
+ Last mode + @ScenarioService.Network.LastMode +
+
+ +
+ + +
+ +
+
+ Success + @ScenarioService.Network.SuccessCount +
+
+ Failure + @ScenarioService.Network.FailureCount +
+
+ Average + @FormatDuration(ScenarioService.Network.AverageDurationMs) +
+
+ Payload + @FormatBytes(ScenarioService.Network.TotalBytes) +
+
+ +
+ @if (ScenarioService.Network.RecentRequests.Count == 0) + { +
No request history yet.
+ } + else + { + @foreach (var request in ScenarioService.Network.RecentRequests) + { +
+
+ @request.StatusCode?.ToString() ?? "ERR" + @request.Url +
+
+ @FormatDuration(request.DurationMs) + @(request.Success ? "ok" : request.Error ?? "failed") +
+
+ } + } +
+
+
+ +@code { + private int _cpuWorkers = Math.Clamp(Environment.ProcessorCount / 2, 1, 4); + private int _cpuDurationSeconds = 20; + private int _memoryMegabytes = 64; + private int _networkRequestCount = 12; + private ProcessSnapshot _process = ProcessSnapshot.Capture(); + private CancellationTokenSource _metricsCts = new(); + + protected override void OnInitialized() + { + ScenarioService.Changed += OnScenarioChanged; + _ = RefreshProcessMetricsAsync(_metricsCts.Token); + } + + private Task StartCpuScenarioAsync() + { + return ScenarioService.StartCpuStressAsync(_cpuWorkers, TimeSpan.FromSeconds(_cpuDurationSeconds)); + } + + private Task RunLocalNetworkScenarioAsync() + { + return RunNetworkScenarioAsync("local"); + } + + private Task RunRemoteNetworkScenarioAsync() + { + return RunNetworkScenarioAsync("remote-mixed"); + } + + private async Task RunNetworkScenarioAsync(string mode) + { + ScenarioService.BeginNetworkRun(mode, _networkRequestCount); + + try + { + var results = await JS.InvokeAsync( + "profilingSample.runNetworkBurst", + new NetworkBurstOptions + { + Mode = mode, + RequestCount = _networkRequestCount + }); + + ScenarioService.CompleteNetworkRun(mode, results); + } + catch (Exception ex) + { + ScenarioService.FailNetworkRun(mode, ex.Message); + } + } + + private async Task RefreshProcessMetricsAsync(CancellationToken cancellationToken) + { + using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1)); + + while (!cancellationToken.IsCancellationRequested) + { + _process = ProcessSnapshot.Capture(); + await InvokeAsync(StateHasChanged); + + try + { + await timer.WaitForNextTickAsync(cancellationToken); + } + catch (OperationCanceledException) + { + break; + } + } + } + + private void OnScenarioChanged() + { + _ = InvokeAsync(StateHasChanged); + } + + private static string FormatBytes(long bytes) + { + string[] units = ["B", "KB", "MB", "GB"]; + double value = bytes; + var unit = 0; + while (value >= 1024 && unit < units.Length - 1) + { + value /= 1024; + unit++; + } + + return $"{value:0.#} {units[unit]}"; + } + + private static string FormatDuration(double durationMs) + { + if (durationMs >= 1000) + { + return $"{durationMs / 1000:0.00}s"; + } + + return $"{durationMs:0.#} ms"; + } + + public void Dispose() + { + ScenarioService.Changed -= OnScenarioChanged; + _metricsCts.Cancel(); + _metricsCts.Dispose(); + } + + private sealed record ProcessSnapshot( + long WorkingSetBytes, + long PrivateBytes, + long ManagedHeapBytes, + int Gen0Collections, + int Gen1Collections, + int Gen2Collections) + { + public static ProcessSnapshot Capture() + { + try + { + var process = Process.GetCurrentProcess(); + process.Refresh(); + return new ProcessSnapshot( + process.WorkingSet64, + process.PrivateMemorySize64, + GC.GetTotalMemory(false), + GC.CollectionCount(0), + GC.CollectionCount(1), + GC.CollectionCount(2)); + } + catch + { + return new ProcessSnapshot(0, 0, GC.GetTotalMemory(false), GC.CollectionCount(0), GC.CollectionCount(1), GC.CollectionCount(2)); + } + } + } +} diff --git a/src/MauiSherpa.ProfilingSample/Platforms/Android/AndroidManifest.xml b/src/MauiSherpa.ProfilingSample/Platforms/Android/AndroidManifest.xml new file mode 100644 index 00000000..ecf6baa5 --- /dev/null +++ b/src/MauiSherpa.ProfilingSample/Platforms/Android/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/MauiSherpa.ProfilingSample/Platforms/Android/MainActivity.cs b/src/MauiSherpa.ProfilingSample/Platforms/Android/MainActivity.cs new file mode 100644 index 00000000..768c2fb9 --- /dev/null +++ b/src/MauiSherpa.ProfilingSample/Platforms/Android/MainActivity.cs @@ -0,0 +1,18 @@ +using Android.App; +using Android.Content.PM; +using Android.OS; + +namespace MauiSherpa.ProfilingSample; + +[Activity( + Theme = "@style/Maui.SplashTheme", + MainLauncher = true, + ConfigurationChanges = ConfigChanges.ScreenSize + | ConfigChanges.Orientation + | ConfigChanges.UiMode + | ConfigChanges.ScreenLayout + | ConfigChanges.SmallestScreenSize + | ConfigChanges.Density)] +public sealed class MainActivity : MauiAppCompatActivity +{ +} diff --git a/src/MauiSherpa.ProfilingSample/Platforms/Android/MainApplication.cs b/src/MauiSherpa.ProfilingSample/Platforms/Android/MainApplication.cs new file mode 100644 index 00000000..63c63288 --- /dev/null +++ b/src/MauiSherpa.ProfilingSample/Platforms/Android/MainApplication.cs @@ -0,0 +1,15 @@ +using Android.App; +using Android.Runtime; + +namespace MauiSherpa.ProfilingSample; + +[Application] +public sealed class MainApplication : MauiApplication +{ + public MainApplication(IntPtr handle, JniHandleOwnership ownership) + : base(handle, ownership) + { + } + + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} diff --git a/src/MauiSherpa.ProfilingSample/Platforms/Android/Resources/values/colors.xml b/src/MauiSherpa.ProfilingSample/Platforms/Android/Resources/values/colors.xml new file mode 100644 index 00000000..892ae109 --- /dev/null +++ b/src/MauiSherpa.ProfilingSample/Platforms/Android/Resources/values/colors.xml @@ -0,0 +1,6 @@ + + + #4F46E5 + #3730A3 + #3730A3 + diff --git a/src/MauiSherpa.ProfilingSample/Platforms/MacCatalyst/AppDelegate.cs b/src/MauiSherpa.ProfilingSample/Platforms/MacCatalyst/AppDelegate.cs new file mode 100644 index 00000000..3e66b3db --- /dev/null +++ b/src/MauiSherpa.ProfilingSample/Platforms/MacCatalyst/AppDelegate.cs @@ -0,0 +1,9 @@ +using Foundation; + +namespace MauiSherpa.ProfilingSample; + +[Register("AppDelegate")] +public sealed class AppDelegate : MauiUIApplicationDelegate +{ + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} diff --git a/src/MauiSherpa.ProfilingSample/Platforms/MacCatalyst/Program.cs b/src/MauiSherpa.ProfilingSample/Platforms/MacCatalyst/Program.cs new file mode 100644 index 00000000..19a401f6 --- /dev/null +++ b/src/MauiSherpa.ProfilingSample/Platforms/MacCatalyst/Program.cs @@ -0,0 +1,11 @@ +using UIKit; + +namespace MauiSherpa.ProfilingSample; + +public static class Program +{ + private static void Main(string[] args) + { + UIApplication.Main(args, null, typeof(AppDelegate)); + } +} diff --git a/src/MauiSherpa.ProfilingSample/Platforms/iOS/AppDelegate.cs b/src/MauiSherpa.ProfilingSample/Platforms/iOS/AppDelegate.cs new file mode 100644 index 00000000..3e66b3db --- /dev/null +++ b/src/MauiSherpa.ProfilingSample/Platforms/iOS/AppDelegate.cs @@ -0,0 +1,9 @@ +using Foundation; + +namespace MauiSherpa.ProfilingSample; + +[Register("AppDelegate")] +public sealed class AppDelegate : MauiUIApplicationDelegate +{ + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} diff --git a/src/MauiSherpa.ProfilingSample/Platforms/iOS/Info.plist b/src/MauiSherpa.ProfilingSample/Platforms/iOS/Info.plist new file mode 100644 index 00000000..ecb7f719 --- /dev/null +++ b/src/MauiSherpa.ProfilingSample/Platforms/iOS/Info.plist @@ -0,0 +1,32 @@ + + + + + LSRequiresIPhoneOS + + UIDeviceFamily + + 1 + 2 + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/appicon.appiconset + + diff --git a/src/MauiSherpa.ProfilingSample/Platforms/iOS/Program.cs b/src/MauiSherpa.ProfilingSample/Platforms/iOS/Program.cs new file mode 100644 index 00000000..19a401f6 --- /dev/null +++ b/src/MauiSherpa.ProfilingSample/Platforms/iOS/Program.cs @@ -0,0 +1,11 @@ +using UIKit; + +namespace MauiSherpa.ProfilingSample; + +public static class Program +{ + private static void Main(string[] args) + { + UIApplication.Main(args, null, typeof(AppDelegate)); + } +} diff --git a/src/MauiSherpa.ProfilingSample/Platforms/iOS/Resources/PrivacyInfo.xcprivacy b/src/MauiSherpa.ProfilingSample/Platforms/iOS/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 00000000..9c604a59 --- /dev/null +++ b/src/MauiSherpa.ProfilingSample/Platforms/iOS/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,41 @@ + + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategorySystemBootTime + NSPrivacyAccessedAPITypeReasons + + 35F9.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryDiskSpace + NSPrivacyAccessedAPITypeReasons + + E174.1 + + + + + diff --git a/src/MauiSherpa.ProfilingSample/Services/ProfilingScenarioService.cs b/src/MauiSherpa.ProfilingSample/Services/ProfilingScenarioService.cs new file mode 100644 index 00000000..82b63db5 --- /dev/null +++ b/src/MauiSherpa.ProfilingSample/Services/ProfilingScenarioService.cs @@ -0,0 +1,592 @@ +using System.Diagnostics; +using MauiSherpa.ProfilingSample.Models; + +namespace MauiSherpa.ProfilingSample.Services; + +public sealed class ProfilingScenarioService : IDisposable +{ + private readonly object _memoryGate = new(); + private readonly object _renderGate = new(); + private readonly List _retainedMemory = []; + private readonly Random _random = new(4172); + + private CancellationTokenSource? _cpuCts; + private CancellationTokenSource? _memoryChurnCts; + private Timer? _animationTimer; + private long _cpuIterations; + private int _cpuRunId; + private int _nextFeedId = 1; + private int _nextChunkId = 1; + + public event Action? Changed; + + public CpuScenarioState Cpu { get; } = new(); + + public MemoryScenarioState Memory { get; } = new(); + + public RenderingScenarioState Rendering { get; } = new(); + + public NetworkScenarioState Network { get; } = new(); + + public ProfilingScenarioService() + { + ResetRenderingScene(); + RefreshMemoryState("Ready to retain or churn allocations."); + Network.Status = "Run a local or remote burst to generate visible request traffic."; + } + + public Task StartCpuStressAsync(int workerCount, TimeSpan duration) + { + StopCpuStress(); + + var runId = Interlocked.Increment(ref _cpuRunId); + var tokenSource = new CancellationTokenSource(); + _cpuCts = tokenSource; + Interlocked.Exchange(ref _cpuIterations, 0); + + Cpu.IsRunning = true; + Cpu.WorkerCount = Math.Clamp(workerCount, 1, Math.Max(1, Environment.ProcessorCount)); + Cpu.DurationSeconds = Math.Clamp((int)Math.Ceiling(duration.TotalSeconds), 5, 180); + Cpu.StartedAt = DateTimeOffset.UtcNow; + Cpu.Status = $"Crunching prime checks with {Cpu.WorkerCount} worker(s)."; + Cpu.LastBatchMilliseconds = 0; + Cpu.IterationsCompleted = 0; + NotifyChanged(); + + var tasks = Enumerable.Range(0, Cpu.WorkerCount) + .Select(worker => Task.Run(() => RunCpuWorker(worker, tokenSource.Token), tokenSource.Token)) + .ToArray(); + + _ = Task.Run(async () => + { + var completedNaturally = false; + + try + { + await Task.Delay(TimeSpan.FromSeconds(Cpu.DurationSeconds), tokenSource.Token); + completedNaturally = true; + } + catch (OperationCanceledException) + { + } + finally + { + tokenSource.Cancel(); + } + + try + { + await Task.WhenAll(tasks); + } + catch (OperationCanceledException) + { + } + + if (runId != Volatile.Read(ref _cpuRunId)) + { + return; + } + + Cpu.IsRunning = false; + Cpu.IterationsCompleted = Interlocked.Read(ref _cpuIterations); + Cpu.Status = completedNaturally + ? "Scheduled CPU run completed." + : "CPU run stopped."; + NotifyChanged(); + tokenSource.Dispose(); + }); + + return Task.CompletedTask; + } + + public void StopCpuStress() + { + _cpuCts?.Cancel(); + _cpuCts = null; + + if (Cpu.IsRunning) + { + Cpu.IsRunning = false; + Cpu.Status = "CPU run stopped."; + NotifyChanged(); + } + } + + public void AllocateRetainedMemory(int megabytes) + { + var clampedMegabytes = Math.Clamp(megabytes, 8, 512); + + lock (_memoryGate) + { + _retainedMemory.Add(CreateChunk(clampedMegabytes)); + RefreshMemoryState($"Retained another {clampedMegabytes} MB across object graphs and byte buffers."); + } + + NotifyChanged(); + } + + public void ReleaseHalfMemory() + { + lock (_memoryGate) + { + var removeCount = _retainedMemory.Count / 2; + if (removeCount > 0) + { + _retainedMemory.RemoveRange(0, removeCount); + } + + RefreshMemoryState(removeCount == 0 + ? "Nothing to release yet." + : $"Released {removeCount} retained chunk(s)."); + } + + NotifyChanged(); + } + + public void ClearRetainedMemory() + { + lock (_memoryGate) + { + _retainedMemory.Clear(); + RefreshMemoryState("Cleared retained allocations."); + } + + NotifyChanged(); + } + + public void CaptureMemorySnapshot() + { + lock (_memoryGate) + { + var snapshot = new MemorySnapshot( + $"snap-{DateTimeOffset.UtcNow:HHmmssfff}", + DateTimeOffset.Now, + Memory.RetainedBytes, + GC.GetTotalMemory(false), + Memory.ChunkCount); + + Memory.Snapshots = Memory.Snapshots + .Prepend(snapshot) + .Take(6) + .ToArray(); + + Memory.Status = "Captured a point-in-time heap snapshot marker."; + } + + NotifyChanged(); + } + + public void ToggleMemoryChurn() + { + if (Memory.IsChurning) + { + StopMemoryChurn(); + return; + } + + _memoryChurnCts?.Cancel(); + var churnCts = new CancellationTokenSource(); + _memoryChurnCts = churnCts; + Memory.IsChurning = true; + Memory.Status = "Background churn is allocating and trimming every 350 ms."; + NotifyChanged(); + + _ = Task.Run(async () => + { + while (!churnCts.IsCancellationRequested) + { + lock (_memoryGate) + { + _retainedMemory.Add(CreateChunk(8)); + if (_retainedMemory.Count > 18) + { + _retainedMemory.RemoveAt(0); + } + + RefreshMemoryState("Background churn is allocating and trimming every 350 ms."); + } + + NotifyChanged(); + + try + { + await Task.Delay(350, churnCts.Token); + } + catch (OperationCanceledException) + { + break; + } + } + + churnCts.Dispose(); + }); + } + + public void StopMemoryChurn() + { + _memoryChurnCts?.Cancel(); + _memoryChurnCts = null; + Memory.IsChurning = false; + RefreshMemoryState("Memory churn stopped."); + NotifyChanged(); + } + + public void AddTiles(int count) + { + lock (_renderGate) + { + var targetCount = Math.Clamp(Rendering.Tiles.Count + count, 0, 480); + Rendering.Tiles = BuildTiles(targetCount, Rendering.Frame); + Rendering.Status = $"Visual tile wall now contains {targetCount} animated nodes."; + } + + NotifyChanged(); + } + + public void AddFeedItems(int count) + { + lock (_renderGate) + { + var updated = Rendering.FeedItems.ToList(); + updated.AddRange(BuildFeedItems(count)); + Rendering.FeedItems = updated; + Rendering.Status = $"Scrollable feed expanded to {updated.Count} rows."; + } + + NotifyChanged(); + } + + public void ResetRenderingScene() + { + lock (_renderGate) + { + Rendering.Frame = 0; + Rendering.Tiles = BuildTiles(12, 0); + Rendering.FeedItems = BuildFeedItems(6); + Rendering.Status = "Scene ready — use the buttons to add tiles and feed items."; + } + + NotifyChanged(); + } + + public void ToggleRenderingAnimation() + { + if (Rendering.IsAnimating) + { + _animationTimer?.Dispose(); + _animationTimer = null; + Rendering.IsAnimating = false; + Rendering.Status = "Animation paused. Scroll the feed to inspect the dense visual tree."; + NotifyChanged(); + return; + } + + Rendering.IsAnimating = true; + Rendering.Status = "Animating the tile wall at ~14 FPS for render churn."; + _animationTimer?.Dispose(); + _animationTimer = new Timer(_ => AdvanceAnimationFrame(), null, TimeSpan.Zero, TimeSpan.FromMilliseconds(70)); + NotifyChanged(); + } + + public void BeginNetworkRun(string mode, int requestCount) + { + Network.IsRunning = true; + Network.LastMode = mode; + Network.Status = $"Running {requestCount} {mode} request(s) from the Blazor WebView..."; + NotifyChanged(); + } + + public void CompleteNetworkRun(string mode, IReadOnlyList results) + { + var recent = results + .OrderByDescending(r => r.DurationMs) + .Take(12) + .ToArray(); + + Network.IsRunning = false; + Network.LastMode = mode; + Network.SuccessCount = results.Count(r => r.Success); + Network.FailureCount = results.Count - Network.SuccessCount; + Network.AverageDurationMs = results.Count == 0 ? 0 : results.Average(r => r.DurationMs); + Network.TotalBytes = results.Sum(r => r.Bytes); + Network.RecentRequests = recent; + Network.Status = results.Count == 0 + ? "No requests were recorded." + : $"Captured {results.Count} request(s) with {Network.FailureCount} failure(s)."; + NotifyChanged(); + } + + public void FailNetworkRun(string mode, string error) + { + Network.IsRunning = false; + Network.LastMode = mode; + Network.Status = $"Network run failed before completion: {error}"; + NotifyChanged(); + } + + /// + /// Runs a network burst using HttpClient (for native MAUI UI, no JS/WebView needed). + /// + public async Task RunNativeNetworkBurstAsync(string mode, int requestCount) + { + requestCount = Math.Clamp(requestCount, 1, 40); + BeginNetworkRun(mode, requestCount); + + try + { + using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(15) }; + var tasks = Enumerable.Range(0, requestCount) + .Select(i => ExecuteNativeRequest(httpClient, mode, i)) + .ToArray(); + + var results = await Task.WhenAll(tasks); + CompleteNetworkRun(mode, results); + } + catch (Exception ex) + { + FailNetworkRun(mode, ex.Message); + } + } + + private static async Task ExecuteNativeRequest(HttpClient client, string mode, int index) + { + var url = mode == "remote-mixed" + ? (index % 4) switch + { + 0 => $"https://jsonplaceholder.typicode.com/todos/{(index % 10) + 1}", + 1 => $"https://httpbin.org/delay/1", + 2 => $"https://jsonplaceholder.typicode.com/invalid-route-{index}", + _ => $"https://httpbin.org/status/503" + } + : $"https://jsonplaceholder.typicode.com/posts/{(index % 50) + 1}"; + + var sw = Stopwatch.StartNew(); + try + { + using var response = await client.GetAsync(url); + var body = await response.Content.ReadAsStringAsync(); + sw.Stop(); + return new NetworkRunResult + { + Url = url, + StatusCode = (int)response.StatusCode, + Success = response.IsSuccessStatusCode, + DurationMs = (int)sw.ElapsedMilliseconds, + Error = response.IsSuccessStatusCode ? null : $"HTTP {(int)response.StatusCode}", + Bytes = body.Length + }; + } + catch (Exception ex) + { + sw.Stop(); + return new NetworkRunResult + { + Url = url, + StatusCode = 0, + Success = false, + DurationMs = (int)sw.ElapsedMilliseconds, + Error = ex.Message, + Bytes = 0 + }; + } + } + + private void RunCpuWorker(int worker, CancellationToken cancellationToken) + { + var stopwatch = Stopwatch.StartNew(); + + while (!cancellationToken.IsCancellationRequested) + { + var score = 0d; + var primeHits = 0; + + for (var candidate = 10_000 + (worker * 137); candidate < 14_500 + (worker * 137); candidate++) + { + if (!IsPrime(candidate)) + { + continue; + } + + primeHits++; + score += Math.Sqrt(candidate) * Math.Sin(candidate / 13d); + } + + var accumulator = new double[128]; + for (var index = 0; index < accumulator.Length; index++) + { + accumulator[index] = Math.Log10(index + 2 + score) * Math.Cos(index + worker); + } + + accumulator.Sort(); + + Interlocked.Add(ref _cpuIterations, primeHits + accumulator.Length); + Cpu.IterationsCompleted = Interlocked.Read(ref _cpuIterations); + Cpu.LastBatchMilliseconds = stopwatch.Elapsed.TotalMilliseconds; + Cpu.Status = $"Latest worker batch finished in {Cpu.LastBatchMilliseconds:F1} ms."; + stopwatch.Restart(); + + if (worker == 0) + { + NotifyChanged(); + } + } + } + + private bool IsPrime(int value) + { + if (value < 2) + { + return false; + } + + if (value % 2 == 0) + { + return value == 2; + } + + var limit = (int)Math.Sqrt(value); + for (var divisor = 3; divisor <= limit; divisor += 2) + { + if (value % divisor == 0) + { + return false; + } + } + + return true; + } + + private void AdvanceAnimationFrame() + { + lock (_renderGate) + { + var nextFrame = Rendering.Frame + 1; + Rendering.Frame = nextFrame; + Rendering.Tiles = BuildTiles(Rendering.Tiles.Count, nextFrame); + + if (nextFrame % 6 == 0) + { + Rendering.FeedItems = Rendering.FeedItems + .Select((item, index) => index < 16 + ? item with + { + Score = ((item.Score + 7 + index) % 100), + ActiveUsers = item.ActiveUsers + (index % 3) + } + : item) + .ToArray(); + } + } + + NotifyChanged(); + } + + private IReadOnlyList BuildTiles(int count, int frame) + { + var tiles = new List(count); + for (var index = 0; index < count; index++) + { + var hue = (index * 17 + frame * 5) % 360; + var scale = 0.92 + ((Math.Sin((frame + index) / 5d) + 1) * 0.18); + tiles.Add(new RenderTile( + index + 1, + $"Node {index + 1}", + hue, + scale, + 35 + ((frame * 3 + index * 11) % 65), + (index + frame) % 9 == 0)); + } + + return tiles; + } + + private IReadOnlyList BuildFeedItems(int count) + { + var items = new List(count); + for (var index = 0; index < count; index++) + { + var id = _nextFeedId++; + items.Add(new FeedItem( + id, + $"Rendering lane {id}", + id % 3 == 0 ? "Hot path" : id % 3 == 1 ? "Scroll" : "Layout", + 12 + ((id * 5) % 140), + 18 + ((id * 11) % 240), + 30 + ((id * 9) % 70), + $"Card {id} intentionally adds depth, text, and progress indicators for visual-tree analysis.")); + } + + return items; + } + + private MemoryChunk CreateChunk(int megabytes) + { + var blockCount = Math.Max(1, megabytes); + var buffers = new byte[blockCount][]; + for (var index = 0; index < blockCount; index++) + { + var buffer = new byte[1024 * 1024]; + for (var offset = 0; offset < buffer.Length; offset += 4096) + { + buffer[offset] = (byte)((index + offset) % 255); + } + + buffers[index] = buffer; + } + + var records = Enumerable.Range(0, Math.Max(12, megabytes / 2)) + .Select(index => new RetainedRecord( + $"payload-{_nextChunkId}-{index}", + string.Join(' ', Enumerable.Repeat("profiling-sample", 10 + (index % 4))), + Enumerable.Range(0, 12).Select(number => (number + index + _random.Next(0, 5)) * 1.37d).ToArray())) + .ToArray(); + + var chunk = new MemoryChunk($"chunk-{_nextChunkId++}", buffers, records); + return chunk; + } + + private void RefreshMemoryState(string status) + { + lock (_memoryGate) + { + Memory.ChunkCount = _retainedMemory.Count; + Memory.RetainedBytes = _retainedMemory.Sum(chunk => chunk.ApproximateBytes); + Memory.ManagedHeapBytes = GC.GetTotalMemory(false); + Memory.Status = status; + } + } + + private void NotifyChanged() + { + Changed?.Invoke(); + } + + public void Dispose() + { + StopCpuStress(); + StopMemoryChurn(); + _animationTimer?.Dispose(); + } + + private sealed record RetainedRecord(string Name, string Description, double[] Scores); + + private sealed class MemoryChunk + { + public MemoryChunk(string id, byte[][] buffers, RetainedRecord[] records) + { + Id = id; + Buffers = buffers; + Records = records; + ApproximateBytes = buffers.Sum(buffer => (long)buffer.Length) + + records.Sum(record => sizeof(double) * record.Scores.Length + (record.Name.Length + record.Description.Length) * sizeof(char)); + } + + public string Id { get; } + + public byte[][] Buffers { get; } + + public RetainedRecord[] Records { get; } + + public long ApproximateBytes { get; } + } +} diff --git a/src/MauiSherpa.ProfilingSample/_Imports.razor b/src/MauiSherpa.ProfilingSample/_Imports.razor new file mode 100644 index 00000000..43809f94 --- /dev/null +++ b/src/MauiSherpa.ProfilingSample/_Imports.razor @@ -0,0 +1,10 @@ +@using System.Diagnostics +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.JSInterop +@using MauiSherpa.ProfilingSample +@using MauiSherpa.ProfilingSample.Components +@using MauiSherpa.ProfilingSample.Models +@using MauiSherpa.ProfilingSample.Pages +@using MauiSherpa.ProfilingSample.Services diff --git a/src/MauiSherpa.ProfilingSample/wwwroot/css/app.css b/src/MauiSherpa.ProfilingSample/wwwroot/css/app.css new file mode 100644 index 00000000..65a1b678 --- /dev/null +++ b/src/MauiSherpa.ProfilingSample/wwwroot/css/app.css @@ -0,0 +1,527 @@ +:root { + color-scheme: light; + --safe-area-top: env(safe-area-inset-top, 0px); + --safe-area-right: env(safe-area-inset-right, 0px); + --safe-area-bottom: env(safe-area-inset-bottom, 0px); + --safe-area-left: env(safe-area-inset-left, 0px); + --bg-primary: #f4f7fb; + --bg-secondary: #ffffff; + --bg-tertiary: #e8edf5; + --border-color: rgba(15, 23, 42, 0.08); + --text-primary: #0f172a; + --text-secondary: #334155; + --text-muted: #64748b; + --accent-primary: #4f46e5; + --accent-secondary: #7c3aed; + --accent-success: #0f766e; + --accent-warning: #b45309; + --accent-info: #1d4ed8; + --accent-danger: #b91c1c; + --card-shadow: 0 22px 45px rgba(15, 23, 42, 0.08); +} + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + min-height: 100%; + font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + background: #0f172a; +} + +body { + -webkit-tap-highlight-color: transparent; +} + +#app { + min-height: 100vh; +} + +button, +input { + font: inherit; +} + +.app-shell { + min-height: 100vh; + padding: + calc(28px + var(--safe-area-top)) + calc(28px + var(--safe-area-right)) + calc(28px + var(--safe-area-bottom)) + calc(28px + var(--safe-area-left)); + background: var(--bg-primary); + color: var(--text-primary); +} + +.theme-dark { + color-scheme: dark; + --bg-primary: #0f172a; + --bg-secondary: #111827; + --bg-tertiary: #1f2937; + --border-color: rgba(148, 163, 184, 0.18); + --text-primary: #e5eef8; + --text-secondary: #cbd5e1; + --text-muted: #94a3b8; + --accent-primary: #818cf8; + --accent-secondary: #c084fc; + --accent-success: #2dd4bf; + --accent-warning: #fbbf24; + --accent-info: #60a5fa; + --accent-danger: #f87171; + --card-shadow: 0 24px 60px rgba(2, 6, 23, 0.45); +} + +.topbar { + display: flex; + justify-content: space-between; + gap: 24px; + align-items: flex-start; + margin-bottom: 24px; +} + +.topbar h1, +.hero-card h2, +.scenario-card h2 { + margin: 0; + font-size: 2rem; + line-height: 1.1; +} + +.title-row { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.topbar-copy, +.section-copy { + margin: 10px 0 0; + color: var(--text-secondary); + max-width: 860px; + line-height: 1.6; +} + +.topbar-actions { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 12px; + min-width: 280px; +} + +.build-note { + color: var(--text-muted); + text-align: right; + max-width: 320px; + line-height: 1.5; +} + +.eyebrow { + text-transform: uppercase; + letter-spacing: 0.16em; + font-size: 0.72rem; + color: var(--accent-secondary); + font-weight: 700; +} + +.layout-body { + display: flex; + flex-direction: column; + gap: 20px; +} + +.dashboard-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 20px; +} + +.hero-card, +.scenario-card, +.metric-strip { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 24px; + box-shadow: var(--card-shadow); +} + +.hero-card { + grid-column: 1 / -1; + display: flex; + justify-content: space-between; + gap: 24px; + padding: 28px; +} + +.metric-strip { + grid-column: 1 / -1; + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 14px; + padding: 18px; +} + +.metric-card { + background: var(--bg-tertiary); + border-radius: 18px; + padding: 16px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.metric-label, +.detail-label, +.stat-callout span, +.feed-summary { + color: var(--text-muted); + font-size: 0.82rem; +} + +.metric-card strong, +.detail-grid strong, +.stat-callout strong { + font-size: 1.2rem; +} + +.metric-detail { + color: var(--text-secondary); + font-size: 0.88rem; + line-height: 1.4; +} + +.quick-actions, +.button-row { + display: flex; + flex-wrap: wrap; + gap: 12px; +} + +.btn { + border: 0; + border-radius: 999px; + padding: 12px 18px; + cursor: pointer; + transition: transform 0.15s ease, opacity 0.15s ease, background 0.15s ease; +} + +.btn:hover:not(:disabled) { + transform: translateY(-1px); +} + +.btn:disabled { + opacity: 0.55; + cursor: not-allowed; +} + +.btn-primary { + background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); + color: white; +} + +.btn-secondary { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.scenario-card { + padding: 24px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.scenario-card-wide { + grid-column: 1 / -1; +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 16px; +} + +.status-chip { + border-radius: 999px; + padding: 8px 12px; + font-size: 0.86rem; + color: var(--text-primary); + background: var(--bg-tertiary); +} + +.status-chip-success { + background: rgba(16, 185, 129, 0.18); + color: var(--accent-success); +} + +.status-chip-warning { + background: rgba(251, 191, 36, 0.18); + color: var(--accent-warning); +} + +.status-chip-info { + background: rgba(96, 165, 250, 0.18); + color: var(--accent-info); +} + +.status-chip-muted { + background: var(--bg-tertiary); + color: var(--text-secondary); +} + +.control-row, +.detail-grid { + display: grid; + gap: 14px; +} + +.compact-grid, +.detail-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +label { + display: flex; + flex-direction: column; + gap: 8px; + color: var(--text-secondary); +} + +input { + padding: 12px 14px; + border-radius: 16px; + border: 1px solid var(--border-color); + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.stat-callout { + border-radius: 18px; + background: var(--bg-tertiary); + padding: 14px 16px; + display: flex; + flex-direction: column; + gap: 8px; + justify-content: center; +} + +.snapshot-list, +.request-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.snapshot-row, +.request-row { + display: flex; + justify-content: space-between; + gap: 12px; + padding: 12px 14px; + border-radius: 16px; + background: var(--bg-tertiary); + color: var(--text-secondary); +} + +.request-row { + align-items: center; +} + +.request-url { + display: block; + color: var(--text-muted); + margin-top: 2px; + word-break: break-all; +} + +.request-meta { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 4px; +} + +.render-layout { + display: grid; + grid-template-columns: minmax(0, 1.2fr) minmax(320px, 0.8fr); + gap: 18px; +} + +.tile-wall { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); + gap: 10px; + align-content: start; +} + +.tile { + min-height: 92px; + border-radius: 18px; + padding: 12px; + background: linear-gradient(180deg, hsla(var(--tile-hue), 82%, 62%, 0.95), hsla(calc(var(--tile-hue) + 22), 70%, 42%, 0.95)); + color: white; + transform: scale(var(--tile-scale)); + transition: transform 80ms linear; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.tile-hot { + outline: 2px solid rgba(255, 255, 255, 0.55); +} + +.tile-label { + font-size: 0.8rem; + opacity: 0.88; +} + +.feed-column { + display: flex; + flex-direction: column; + gap: 10px; +} + +.feed-list { + max-height: 520px; + overflow: auto; + padding-right: 4px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.feed-item { + background: var(--bg-tertiary); + border-radius: 18px; + padding: 14px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.feed-item-header { + display: flex; + justify-content: space-between; + gap: 12px; + align-items: flex-start; +} + +.feed-tag { + display: inline-flex; + margin-left: 8px; + padding: 4px 8px; + border-radius: 999px; + background: rgba(124, 58, 237, 0.12); + color: var(--accent-secondary); + font-size: 0.76rem; +} + +.feed-item p { + margin: 0; + color: var(--text-secondary); + line-height: 1.5; +} + +.feed-metrics { + display: grid; + grid-template-columns: auto 1fr auto; + gap: 10px; + align-items: center; + color: var(--text-secondary); +} + +.progress-bar { + height: 10px; + background: rgba(148, 163, 184, 0.2); + border-radius: 999px; + overflow: hidden; +} + +.progress-bar span { + display: block; + height: 100%; + background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); + border-radius: inherit; +} + +.empty-state { + display: grid; + place-items: center; + min-height: 200px; + color: var(--text-secondary); + text-align: center; +} + +.empty-state.small { + min-height: 80px; + background: var(--bg-tertiary); + border-radius: 16px; +} + +#blazor-error-ui { + display: none; + position: fixed; + left: 16px; + right: 16px; + bottom: 16px; + background: #7f1d1d; + color: white; + padding: 12px 16px; + border-radius: 16px; +} + +#blazor-error-ui .reload, +#blazor-error-ui .dismiss { + color: white; + margin-left: 12px; +} + +@media (max-width: 1200px) { + .metric-strip { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .render-layout { + grid-template-columns: 1fr; + } +} + +@media (max-width: 900px) { + .app-shell { + padding: + calc(18px + var(--safe-area-top)) + calc(18px + var(--safe-area-right)) + calc(18px + var(--safe-area-bottom)) + calc(18px + var(--safe-area-left)); + } + + .dashboard-grid { + grid-template-columns: 1fr; + } + + .hero-card, + .topbar, + .section-header { + flex-direction: column; + } + + .metric-strip, + .compact-grid, + .detail-grid { + grid-template-columns: 1fr; + } + + .topbar-actions { + align-items: flex-start; + } + + .build-note { + text-align: left; + } +} diff --git a/src/MauiSherpa.ProfilingSample/wwwroot/index.html b/src/MauiSherpa.ProfilingSample/wwwroot/index.html new file mode 100644 index 00000000..84c1abdd --- /dev/null +++ b/src/MauiSherpa.ProfilingSample/wwwroot/index.html @@ -0,0 +1,23 @@ + + + + + + Sherpa Profiling Sample + + + + + +
+ +
+ An unhandled error has occurred. + Reload + × +
+ + + + + diff --git a/src/MauiSherpa.ProfilingSample/wwwroot/js/profilingSample.js b/src/MauiSherpa.ProfilingSample/wwwroot/js/profilingSample.js new file mode 100644 index 00000000..646f8cf3 --- /dev/null +++ b/src/MauiSherpa.ProfilingSample/wwwroot/js/profilingSample.js @@ -0,0 +1,64 @@ +window.profilingSample = { + async runNetworkBurst(options) { + const mode = options?.mode ?? 'local'; + const requestCount = Math.max(1, Math.min(40, options?.requestCount ?? 12)); + const timestamp = Date.now(); + const requests = Array.from({ length: requestCount }, (_, index) => createRequest(mode, index, timestamp)); + const results = await Promise.all(requests.map(executeRequest)); + return results; + } +}; + +function createRequest(mode, index, timestamp) { + if (mode === 'remote-mixed') { + const remotePattern = index % 4; + if (remotePattern === 0) { + return { url: `https://jsonplaceholder.typicode.com/todos/${(index % 10) + 1}?cb=${timestamp}_${index}` }; + } + + if (remotePattern === 1) { + return { url: `https://httpbin.org/delay/1?cb=${timestamp}_${index}` }; + } + + if (remotePattern === 2) { + return { url: `https://jsonplaceholder.typicode.com/invalid-route-${index}?cb=${timestamp}_${index}` }; + } + + return { url: `https://httpbin.org/status/503?cb=${timestamp}_${index}` }; + } + + const assetPath = index % 2 === 0 ? 'css/app.css' : 'js/profilingSample.js'; + return { url: `${assetPath}?cb=${timestamp}_${index}` }; +} + +async function executeRequest(request) { + const started = performance.now(); + + try { + const response = await fetch(request.url, { + cache: 'no-store', + headers: { + 'x-sherpa-profile': 'network-burst' + } + }); + + const text = await response.text(); + return { + url: request.url, + statusCode: response.status, + success: response.ok, + durationMs: Math.round(performance.now() - started), + error: response.ok ? null : `HTTP ${response.status}`, + bytes: text.length + }; + } catch (error) { + return { + url: request.url, + statusCode: null, + success: false, + durationMs: Math.round(performance.now() - started), + error: error instanceof Error ? error.message : String(error), + bytes: 0 + }; + } +} diff --git a/src/MauiSherpa/Components/MainLayout.razor b/src/MauiSherpa/Components/MainLayout.razor index a2a14f43..0333cdfd 100644 --- a/src/MauiSherpa/Components/MainLayout.razor +++ b/src/MauiSherpa/Components/MainLayout.razor @@ -106,7 +106,11 @@ Publish - + + + + Profiling + App Inspector @@ -114,7 +118,6 @@ @if (isDebugBuild) { - Debug UI diff --git a/src/MauiSherpa/MauiProgram.cs b/src/MauiSherpa/MauiProgram.cs index df0f87b3..ba07fb88 100644 --- a/src/MauiSherpa/MauiProgram.cs +++ b/src/MauiSherpa/MauiProgram.cs @@ -92,7 +92,7 @@ public static MauiApp CreateMauiApp() builder.Services.AddSingleton(); // Process execution services - builder.Services.AddSingleton(); + builder.Services.AddTransient(); builder.Services.AddSingleton(); builder.Services.AddSingleton(sp => sp.GetRequiredService()); builder.Services.AddSingleton(); @@ -112,12 +112,22 @@ public static MauiApp CreateMauiApp() builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddTransient(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -223,6 +233,11 @@ public static MauiApp CreateMauiApp() builder.Services.AddSingletonAsImplementedInterfaces(); builder.Services.AddSingletonAsImplementedInterfaces(); builder.Services.AddSingletonAsImplementedInterfaces(); + builder.Services.AddSingletonAsImplementedInterfaces(); + builder.Services.AddSingletonAsImplementedInterfaces(); + builder.Services.AddSingletonAsImplementedInterfaces(); + builder.Services.AddSingletonAsImplementedInterfaces(); + builder.Services.AddSingletonAsImplementedInterfaces(); #if DEBUG #if !LINUXGTK diff --git a/src/MauiSherpa/MauiSherpa.csproj b/src/MauiSherpa/MauiSherpa.csproj index 1e64f07b..fbea19ba 100644 --- a/src/MauiSherpa/MauiSherpa.csproj +++ b/src/MauiSherpa/MauiSherpa.csproj @@ -53,8 +53,8 @@ - - + +
diff --git a/src/MauiSherpa/Pages/Forms/HybridFormBridge.cs b/src/MauiSherpa/Pages/Forms/HybridFormBridge.cs index 5bab4963..418d6b50 100644 --- a/src/MauiSherpa/Pages/Forms/HybridFormBridge.cs +++ b/src/MauiSherpa/Pages/Forms/HybridFormBridge.cs @@ -42,6 +42,12 @@ public class HybridFormBridge public bool IsSubmitting { get; private set; } public string? SubmitText { get; private set; } + /// When true, native Cancel does not close the modal (Blazor handles it). + public bool PreventClose { get; set; } + + /// When true, native Submit/Primary does not close the modal after the handler completes. + public bool PreventSubmitClose { get; set; } + /// Called by Blazor component to update form validity. public void SetValid(bool valid) { @@ -78,9 +84,15 @@ public async Task RequestActionAsync(string actionId) await ActionRequested.Invoke(actionId); } + /// Fired by the Blazor component to programmatically close the modal with a result. + public event Action? CloseRequested; + /// Called by MAUI page when native cancel button is pressed. public void RequestCancel() => CancelRequested?.Invoke(); + /// Called by Blazor component to close the modal with the current Result. + public void RequestClose() => CloseRequested?.Invoke(); + /// Called by MAUI page when native back button is pressed. public async Task RequestBackAsync() { diff --git a/src/MauiSherpa/Pages/Forms/WizardFormPage.cs b/src/MauiSherpa/Pages/Forms/WizardFormPage.cs index 84905451..051a2f38 100644 --- a/src/MauiSherpa/Pages/Forms/WizardFormPage.cs +++ b/src/MauiSherpa/Pages/Forms/WizardFormPage.cs @@ -61,6 +61,7 @@ private void BuildPage() // Listen for wizard state changes from Blazor _bridge.WizardStateChanged += OnWizardStateChanged; + _bridge.CloseRequested += OnCloseRequested; // Title var titleLabel = new Label @@ -266,6 +267,7 @@ private async void OnPrimaryClicked(object? sender, EventArgs e) try { await _bridge.RequestSubmitAsync(); + if (_bridge.PreventSubmitClose) return; var result = (TResult?)_bridge.Result; _bridgeHolder.Pop(); _tcs.TrySetResult(result); @@ -284,10 +286,21 @@ private async void OnPrimaryClicked(object? sender, EventArgs e) private void OnCancelClicked(object? sender, EventArgs e) { _bridge.RequestCancel(); + if (_bridge.PreventClose) return; _bridgeHolder.Pop(); _tcs.TrySetResult(default); } + private void OnCloseRequested() + { + Dispatcher.Dispatch(() => + { + var result = (TResult?)_bridge.Result; + _bridgeHolder.Pop(); + _tcs.TrySetResult(result); + }); + } + private void OnBlazorReady(double contentHeight) { ModalWebViewReadySignal.Ready -= OnBlazorReady; diff --git a/src/MauiSherpa/Pages/GcDumpViewer.razor b/src/MauiSherpa/Pages/GcDumpViewer.razor new file mode 100644 index 00000000..54acfcb9 --- /dev/null +++ b/src/MauiSherpa/Pages/GcDumpViewer.razor @@ -0,0 +1,331 @@ +@page "/profiling/gcdump" +@using MauiSherpa.Core.Interfaces +@using MauiSherpa.Core.Models.Profiling +@inject IGcDumpReportService GcDumpReportService + +
+
+ + GC Dump — Heap Statistics + @if (!string.IsNullOrEmpty(fileName)) + { + @fileName + } +
+ +
+ @if (loading) + { +
+ +
Analyzing GC dump...
+
+ } + else if (errorMessage is not null) + { +
+ + @errorMessage +
+ } + else if (report is not null) + { +
+
+ @report.TotalCount.ToString("N0") objects + @FormatBytes(report.TotalSize) + @report.Types.Count types +
+
+ +
+
+ +
+ + + + + + + + + + + @foreach (var entry in GetFilteredTypes()) + { + var pct = report.TotalSize > 0 + ? (double)entry.Size / report.TotalSize * 100 + : 0; + + + + + + + } + +
+ Type Name + @if (sortColumn == "name") { } + + Count + @if (sortColumn == "count") { } + + Total Size + @if (sortColumn == "size") { } + %
+ @entry.TypeName + @entry.Count.ToString("N0")@FormatBytes(entry.Size) +
+
+ @pct.ToString("F1")% +
+
+
+ } +
+
+ +@code { + [SupplyParameterFromQuery] public string? file { get; set; } + + private string? filePath; + private string? fileName; + private bool loading = true; + private string? errorMessage; + private GcDumpReport? report; + private string filterText = ""; + private string sortColumn = "size"; + private bool sortAsc = false; + + protected override async Task OnInitializedAsync() + { + if (string.IsNullOrEmpty(file)) + { + loading = false; + errorMessage = "No file path specified."; + return; + } + + filePath = Uri.UnescapeDataString(file); + fileName = Path.GetFileName(filePath); + + try + { + report = await GcDumpReportService.GetReportAsync(filePath); + if (report is null) + errorMessage = "Failed to parse GC dump. Make sure dotnet-gcdump is installed."; + } + catch (Exception ex) + { + errorMessage = $"Error analyzing GC dump: {ex.Message}"; + } + finally + { + loading = false; + } + } + + private IEnumerable GetFilteredTypes() + { + if (report is null) return Enumerable.Empty(); + + var types = report.Types.AsEnumerable(); + + if (!string.IsNullOrWhiteSpace(filterText)) + types = types.Where(t => t.TypeName.Contains(filterText, StringComparison.OrdinalIgnoreCase)); + + types = sortColumn switch + { + "name" => sortAsc ? types.OrderBy(t => t.TypeName) : types.OrderByDescending(t => t.TypeName), + "count" => sortAsc ? types.OrderBy(t => t.Count) : types.OrderByDescending(t => t.Count), + "size" => sortAsc ? types.OrderBy(t => t.Size) : types.OrderByDescending(t => t.Size), + _ => types + }; + + return types.Take(500); + } + + private void Sort(string column) + { + if (sortColumn == column) + sortAsc = !sortAsc; + else + { + sortColumn = column; + sortAsc = column == "name"; + } + } + + private static string FormatBytes(long bytes) + { + if (bytes >= 1024 * 1024) return $"{bytes / (1024.0 * 1024.0):F1} MB"; + if (bytes >= 1024) return $"{bytes / 1024.0:F1} KB"; + return $"{bytes:N0} B"; + } +} + + diff --git a/src/MauiSherpa/Pages/Inspector/DevFlowInspector.razor b/src/MauiSherpa/Pages/Inspector/DevFlowInspector.razor index d279c75a..4768a0e0 100644 --- a/src/MauiSherpa/Pages/Inspector/DevFlowInspector.razor +++ b/src/MauiSherpa/Pages/Inspector/DevFlowInspector.razor @@ -38,6 +38,9 @@ + @@ -63,7 +66,7 @@ break; case "profiling": - + break; case "webview": @@ -71,6 +74,9 @@ case "logs": break; + case "platform": + + break; } } @@ -93,7 +99,7 @@ private string activeTab = "tree"; private static readonly HashSet ValidTabs = new(StringComparer.OrdinalIgnoreCase) { - "tree", "network", "profiling", "webview", "logs" + "tree", "network", "profiling", "webview", "logs", "platform" }; private bool isConnected; private bool isReconnecting; diff --git a/src/MauiSherpa/Pages/Inspector/DevFlowPlatformTab.razor b/src/MauiSherpa/Pages/Inspector/DevFlowPlatformTab.razor new file mode 100644 index 00000000..c13dc7f2 --- /dev/null +++ b/src/MauiSherpa/Pages/Inspector/DevFlowPlatformTab.razor @@ -0,0 +1,1453 @@ +@using MauiSherpa.Core.Models.DevFlow +@using MauiSherpa.Core.Services +@using System.Text.Json +@implements IDisposable + +
+
+ + + + + +
+ +
+ @switch (activeSubTab) + { + case "info": + @RenderInfoTab() + break; + case "storage": + @RenderStorageTab() + break; + case "permissions": + @RenderPermissionsTab() + break; + case "sensors": + @RenderSensorsTab() + break; + case "location": + @RenderLocationTab() + break; + } +
+
+ +@code { + [Parameter] public DevFlowAgentClient Client { get; set; } = null!; + [Parameter] public string? AgentVersion { get; set; } + + private string activeSubTab = "info"; + private bool loading; + private string? error; + + // Info state + private DevFlowAppInfo? appInfo; + private DevFlowDeviceInfo? deviceInfo; + private DevFlowDisplayInfo? displayInfo; + private DevFlowBatteryInfo? batteryInfo; + private DevFlowConnectivityInfo? connectivityInfo; + private DevFlowVersionTracking? versionTracking; + private bool infoLoaded; + private Dictionary cardErrors = new(); + + // Storage state + private List preferences = new(); + private bool prefsLoaded; + private bool showAddPref; + private string newPrefKey = ""; + private string newPrefValue = ""; + private string newPrefType = "string"; + private string newPrefSharedName = ""; + private string? editingPrefKey; + private string editingPrefValue = ""; + private string secureStorageKey = ""; + private string secureStorageValue = ""; + private DevFlowSecureStorageEntry? secureStorageResult; + private bool showAddSecure; + private string newSecureKey = ""; + private string newSecureValue = ""; + + // Permissions state + private List permissions = new(); + private bool permissionsLoaded; + + // Sensors state + private List sensors = new(); + private bool sensorsLoaded; + private Dictionary sensorReadings = new(); + private string sensorSpeed = "UI"; + + // Location state + private DevFlowGeolocation? location; + private string locationAccuracy = "Medium"; + private int locationTimeout = 10; + private bool locationLoading; + + protected override async Task OnInitializedAsync() + { + await LoadInfoAsync(); + } + + private void SwitchSubTab(string tab) + { + activeSubTab = tab; + _ = tab switch + { + "info" when !infoLoaded => LoadInfoAsync(), + "storage" when !prefsLoaded => LoadPreferencesAsync(), + "permissions" when !permissionsLoaded => LoadPermissionsAsync(), + "sensors" when !sensorsLoaded => LoadSensorsAsync(), + _ => Task.CompletedTask + }; + } + + // ── Info Tab ── + + private async Task LoadInfoAsync() + { + loading = true; + error = null; + cardErrors.Clear(); + StateHasChanged(); + + try + { + await Task.WhenAll( + LoadInfoCardAsync("App Info", "/api/platform/app-info", r => appInfo = r), + LoadInfoCardAsync("Device", "/api/platform/device-info", r => deviceInfo = r), + LoadInfoCardAsync("Display", "/api/platform/device-display", r => displayInfo = r), + LoadInfoCardAsync("Battery", "/api/platform/battery", r => batteryInfo = r), + LoadInfoCardAsync("Connectivity", "/api/platform/connectivity", r => connectivityInfo = r), + LoadInfoCardAsync("Version Tracking", "/api/platform/version-tracking", r => versionTracking = r) + ); + infoLoaded = true; + } + catch (Exception ex) { error = ex.Message; } + finally { loading = false; StateHasChanged(); } + } + + private async Task LoadInfoCardAsync(string cardName, string endpoint, Action setter) where T : class + { + try + { + using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(10) }; + var httpResponse = await httpClient.GetAsync($"{Client.BaseUrl}{endpoint}"); + var body = await httpResponse.Content.ReadAsStringAsync(); + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + + // Check if the response is an error (non-2xx or {"success": false, "error": "..."}) + if (!httpResponse.IsSuccessStatusCode || body.Contains("\"success\":false", StringComparison.OrdinalIgnoreCase)) + { + try + { + var errorCheck = JsonSerializer.Deserialize(body, options); + if (errorCheck.TryGetProperty("error", out var errProp)) + { + cardErrors[cardName] = FormatApiError(errProp.GetString()); + setter(null); + return; + } + } + catch { } + + cardErrors[cardName] = $"HTTP {(int)httpResponse.StatusCode}: {httpResponse.ReasonPhrase}"; + setter(null); + return; + } + + setter(JsonSerializer.Deserialize(body, options)); + } + catch (Exception ex) + { + cardErrors[cardName] = FormatApiError(ex.Message); + setter(null); + } + } + + private static string FormatApiError(string? error) + { + if (string.IsNullOrEmpty(error)) return "Unknown error"; + + // Make permission errors more user-friendly + if (error.Contains("permission", StringComparison.OrdinalIgnoreCase) && + error.Contains("AndroidManifest", StringComparison.OrdinalIgnoreCase)) + { + // Extract the permission name + var tickStart = error.IndexOf('`'); + var tickEnd = tickStart >= 0 ? error.IndexOf('`', tickStart + 1) : -1; + var permName = tickStart >= 0 && tickEnd > tickStart + ? error[(tickStart + 1)..tickEnd] + : "required permission"; + return $"Missing Android permission: {permName}. Add it to your app's AndroidManifest.xml."; + } + + if (error.Contains("UIKit Consistency", StringComparison.OrdinalIgnoreCase)) + { + return "This API requires the UI thread. Update MauiDevFlow to fix this (PR #35)."; + } + + return error; + } + + private RenderFragment RenderInfoTab() => __builder => + { +
+ +
+ + @if (error != null) + { +
@error
+ } + + @if (!infoLoaded && loading) + { +
Loading platform info...
+ } + else + { +
+ @RenderInfoCard("App Info", "fas fa-mobile-alt", new Dictionary + { + ["Name"] = appInfo?.Name, + ["Package"] = appInfo?.PackageName, + ["Version"] = appInfo?.Version, + ["Build"] = appInfo?.BuildNumber, + ["Theme"] = appInfo?.RequestedTheme, + ["Layout Direction"] = appInfo?.RequestedLayoutDirection + }) + + @RenderInfoCard("Device", "fas fa-microchip", new Dictionary + { + ["Manufacturer"] = deviceInfo?.Manufacturer, + ["Model"] = deviceInfo?.Model, + ["Name"] = deviceInfo?.Name, + ["Platform"] = deviceInfo?.Platform, + ["Idiom"] = deviceInfo?.Idiom, + ["Type"] = deviceInfo?.DeviceType, + ["OS Version"] = deviceInfo?.OsVersion + }) + + @RenderInfoCard("Display", "fas fa-desktop", new Dictionary + { + ["Resolution"] = displayInfo != null ? $"{displayInfo.Width} × {displayInfo.Height}" : null, + ["Density"] = displayInfo?.Density.ToString("F2"), + ["Orientation"] = displayInfo?.Orientation, + ["Rotation"] = displayInfo?.Rotation, + ["Refresh Rate"] = displayInfo?.RefreshRate > 0 ? $"{displayInfo.RefreshRate:F0} Hz" : null + }) + + @RenderInfoCard("Battery", "fas fa-battery-half", new Dictionary + { + ["Charge"] = batteryInfo != null ? (batteryInfo.ChargeLevel >= 0 ? $"{batteryInfo.ChargeLevel * 100:F0}%" : "N/A") : null, + ["State"] = batteryInfo?.State, + ["Power Source"] = batteryInfo?.PowerSource, + ["Energy Saver"] = batteryInfo?.EnergySaverStatus + }) + + @RenderInfoCard("Connectivity", "fas fa-wifi", new Dictionary + { + ["Network Access"] = connectivityInfo?.NetworkAccess, + ["Profiles"] = connectivityInfo?.ConnectionProfiles != null ? string.Join(", ", connectivityInfo.ConnectionProfiles) : null + }) + + @RenderInfoCard("Version Tracking", "fas fa-code-branch", new Dictionary + { + ["Current"] = versionTracking != null ? $"{versionTracking.CurrentVersion} ({versionTracking.CurrentBuild})" : null, + ["Previous"] = versionTracking?.PreviousVersion != null ? $"{versionTracking.PreviousVersion} ({versionTracking.PreviousBuild})" : null, + ["First Installed"] = versionTracking?.FirstInstalledVersion != null ? $"{versionTracking.FirstInstalledVersion} ({versionTracking.FirstInstalledBuild})" : null, + ["First Launch Ever"] = versionTracking?.IsFirstLaunchEver.ToString(), + ["First Launch Version"] = versionTracking?.IsFirstLaunchForCurrentVersion.ToString(), + ["First Launch Build"] = versionTracking?.IsFirstLaunchForCurrentBuild.ToString() + }) +
+ } + }; + + private RenderFragment RenderInfoCard(string title, string icon, Dictionary fields) => __builder => + { + var hasError = cardErrors.TryGetValue(title, out var cardError); + var isPermissionError = hasError && cardError!.Contains("Missing", StringComparison.OrdinalIgnoreCase); + var cardClass = hasError ? (isPermissionError ? "info-card-warning" : "info-card-error") : ""; +
+
+ @title +
+
+ @if (hasError) + { +
+ + @cardError +
+ } + else + { + @foreach (var (key, value) in fields) + { +
+ @key + @(value ?? "—") +
+ } + } +
+
+ }; + + // ── Storage Tab ── + + private async Task LoadPreferencesAsync() + { + loading = true; + StateHasChanged(); + try + { + preferences = await Client.GetPreferencesAsync(); + prefsLoaded = true; + } + catch (Exception ex) { error = ex.Message; } + finally { loading = false; StateHasChanged(); } + } + + private async Task AddPreferenceAsync() + { + if (string.IsNullOrWhiteSpace(newPrefKey)) return; + var sharedName = string.IsNullOrWhiteSpace(newPrefSharedName) ? null : newPrefSharedName; + await Client.SetPreferenceAsync(newPrefKey, newPrefValue, newPrefType, sharedName); + newPrefKey = ""; + newPrefValue = ""; + newPrefType = "string"; + newPrefSharedName = ""; + showAddPref = false; + await LoadPreferencesAsync(); + } + + private async Task DeletePreferenceAsync(string key, string? sharedName) + { + await Client.DeletePreferenceAsync(key, sharedName); + await LoadPreferencesAsync(); + } + + private void StartEditPref(string key, string? value) + { + editingPrefKey = key; + editingPrefValue = value ?? ""; + } + + private async Task SaveEditPrefAsync(string key, string? sharedName) + { + await Client.SetPreferenceAsync(key, editingPrefValue, "string", sharedName); + editingPrefKey = null; + await LoadPreferencesAsync(); + } + + private async Task ClearAllPreferencesAsync() + { + await Client.ClearPreferencesAsync(); + await LoadPreferencesAsync(); + } + + private async Task LookupSecureStorageAsync() + { + if (string.IsNullOrWhiteSpace(secureStorageKey)) return; + secureStorageResult = await Client.GetSecureStorageAsync(secureStorageKey); + StateHasChanged(); + } + + private async Task AddSecureStorageAsync() + { + if (string.IsNullOrWhiteSpace(newSecureKey)) return; + await Client.SetSecureStorageAsync(newSecureKey, newSecureValue); + newSecureKey = ""; + newSecureValue = ""; + showAddSecure = false; + StateHasChanged(); + } + + private async Task DeleteSecureStorageAsync(string key) + { + await Client.DeleteSecureStorageAsync(key); + secureStorageResult = null; + StateHasChanged(); + } + + private async Task ClearAllSecureStorageAsync() + { + await Client.ClearSecureStorageAsync(); + secureStorageResult = null; + StateHasChanged(); + } + + private RenderFragment RenderStorageTab() => __builder => + { +
+
+
+ Preferences +
+ + + +
+
+ + @if (showAddPref) + { +
+ + + + + + +
+ } + + @if (!prefsLoaded && loading) + { +
Loading...
+ } + else if (preferences.Count == 0) + { +
No preferences found. Add one or use the app to create preferences.
+ } + else + { +
+ + + + + + + + + + + @foreach (var pref in preferences) + { + + + + + + + } + +
KeyValueShared Name
@pref.Key + @if (editingPrefKey == pref.Key) + { +
+ + + +
+ } + else + { + @(pref.Value ?? "null") + } +
@(pref.SharedName ?? "—") + +
+
+ } +
+ +
+
+ Secure Storage +
+ + +
+
+ + @if (showAddSecure) + { +
+ + + + +
+ } + +
+ + +
+ + @if (secureStorageResult != null) + { +
+
+ Key + @secureStorageResult.Key +
+
+ Exists + + @if (secureStorageResult.Exists) + { + Yes + } + else + { + No + } + +
+ @if (secureStorageResult.Exists) + { +
+ Value + @secureStorageResult.Value +
+
+ +
+ } +
+ } + else + { +
+ Secure Storage doesn't support listing keys. Enter a key above to look up its value. +
+ } +
+
+ }; + + // ── Permissions Tab ── + + private async Task LoadPermissionsAsync() + { + loading = true; + StateHasChanged(); + try + { + permissions = await Client.GetPermissionsAsync(); + permissionsLoaded = true; + } + catch (Exception ex) { error = ex.Message; } + finally { loading = false; StateHasChanged(); } + } + + private async Task RefreshPermissionAsync(string name) + { + var result = await Client.CheckPermissionAsync(name); + if (result != null) + { + var idx = permissions.FindIndex(p => p.Permission == name); + if (idx >= 0) permissions[idx] = result; + StateHasChanged(); + } + } + + private string GetPermissionStatusClass(string status) => status.ToLowerInvariant() switch + { + "granted" => "status-granted", + "denied" => "status-denied", + "restricted" => "status-restricted", + "disabled" => "status-denied", + "unavailable" => "status-unavailable", + _ => "status-unknown" + }; + + private string GetPermissionIcon(string status) => status.ToLowerInvariant() switch + { + "granted" => "fa-check-circle", + "denied" => "fa-times-circle", + "restricted" => "fa-exclamation-triangle", + "disabled" => "fa-ban", + "unavailable" => "fa-minus-circle", + _ => "fa-question-circle" + }; + + private RenderFragment RenderPermissionsTab() => __builder => + { +
+ +
+ + @if (!permissionsLoaded && loading) + { +
Checking permissions...
+ } + else if (permissions.Count == 0) + { +
No permission data available.
+ } + else + { +
+ @foreach (var perm in permissions) + { +
+ + @FormatPermissionName(perm.Permission) + @perm.Status +
+ } +
+ } + }; + + private string FormatPermissionName(string name) + { + // Convert camelCase or PascalCase to spaced words + var result = new System.Text.StringBuilder(); + foreach (var c in name) + { + if (char.IsUpper(c) && result.Length > 0) result.Append(' '); + result.Append(c); + } + return result.ToString(); + } + + // ── Sensors Tab ── + + private async Task LoadSensorsAsync() + { + loading = true; + StateHasChanged(); + try + { + sensors = await Client.GetSensorsAsync(); + sensorsLoaded = true; + } + catch (Exception ex) { error = ex.Message; } + finally { loading = false; StateHasChanged(); } + } + + private async Task ToggleSensorAsync(string sensorName, bool currentlyActive) + { + if (currentlyActive) + { + await Client.StopSensorAsync(sensorName); + StopSensorStreaming(sensorName); + } + else + { + await Client.StartSensorAsync(sensorName, sensorSpeed); + } + await LoadSensorsAsync(); + } + + private void StartSensorStreaming(string sensorName) + { + if (Client.IsSensorStreaming(sensorName)) return; + + _ = Task.Run(async () => + { + await Client.StreamSensorAsync(sensorName, reading => + { + sensorReadings[sensorName] = reading; + InvokeAsync(StateHasChanged); + }, sensorSpeed, 100); + }); + } + + private void StopSensorStreaming(string sensorName) + { + Client.StopSensorStream(sensorName); + sensorReadings.Remove(sensorName); + } + + private void StopAllSensorStreaming() + { + Client.StopAllSensorStreams(); + sensorReadings.Clear(); + StateHasChanged(); + } + + private string GetSensorIcon(string sensor) => sensor.ToLowerInvariant() switch + { + "accelerometer" => "fa-arrows-alt", + "barometer" => "fa-tachometer-alt", + "compass" => "fa-compass", + "gyroscope" => "fa-sync", + "magnetometer" => "fa-magnet", + "orientation" => "fa-cube", + _ => "fa-wave-square" + }; + + private bool TryGetDataProperty(DevFlowSensorReading reading, string name, out JsonElement value) + { + // Try exact name, then PascalCase, then camelCase + if (reading.Data.TryGetProperty(name, out value)) return true; + var pascal = char.ToUpperInvariant(name[0]) + name[1..]; + if (reading.Data.TryGetProperty(pascal, out value)) return true; + var camel = char.ToLowerInvariant(name[0]) + name[1..]; + if (reading.Data.TryGetProperty(camel, out value)) return true; + return false; + } + + private double GetDataDouble(DevFlowSensorReading reading, string name) + { + if (TryGetDataProperty(reading, name, out var val)) return val.GetDouble(); + return 0; + } + + private RenderFragment RenderSensorReadingData(DevFlowSensorReading reading) => __builder => + { + var sensor = reading.Sensor.ToLowerInvariant(); + try + { + switch (sensor) + { + case "accelerometer": + case "gyroscope": + case "magnetometer": + if (TryGetDataProperty(reading, "x", out var ax)) + { +
+
X @ax.GetDouble().ToString("F4")
+
Y @GetDataDouble(reading, "y").ToString("F4")
+
Z @GetDataDouble(reading, "z").ToString("F4")
+
+ } + else + { +
@reading.Data.ToString()
+ } + break; + case "barometer": + if (TryGetDataProperty(reading, "pressureInHectopascals", out var pressure)) + { +
+
Pressure @pressure.GetDouble().ToString("F2") hPa
+
+ } + else + { +
@reading.Data.ToString()
+ } + break; + case "compass": + if (TryGetDataProperty(reading, "headingMagneticNorth", out var heading)) + { +
+
Heading @heading.GetDouble().ToString("F1")°
+
+ } + else + { +
@reading.Data.ToString()
+ } + break; + case "orientation": + if (TryGetDataProperty(reading, "x", out var ox)) + { +
+
X @ox.GetDouble().ToString("F4")
+
Y @GetDataDouble(reading, "y").ToString("F4")
+
Z @GetDataDouble(reading, "z").ToString("F4")
+
W @GetDataDouble(reading, "w").ToString("F4")
+
+ } + else + { +
@reading.Data.ToString()
+ } + break; + default: +
@reading.Data.ToString()
+ break; + } + } + catch + { +
@reading.Data.ToString()
+ } + }; + + private RenderFragment RenderSensorsTab() => __builder => + { +
+ + @if (Client.StreamingSensorCount > 0) + { + + } + +
+ + @if (!sensorsLoaded && loading) + { +
Loading sensors...
+ } + else + { +
+ @foreach (var sensor in sensors) + { +
+
+ +
+ @sensor.Sensor + + @if (!sensor.Supported) + { + Not Supported + } + else if (sensor.Active) + { + Active + @if (sensor.Subscribers > 0) + { + @sensor.Subscribers subscriber(s) + } + } + else + { + Stopped + } + +
+
+ @if (sensorReadings.TryGetValue(sensor.Sensor, out var reading)) + { +
+ @RenderSensorReadingData(reading) +
+ } +
+ @if (sensor.Supported) + { + + @if (sensor.Active) + { + @if (Client.IsSensorStreaming(sensor.Sensor)) + { + + } + else + { + + } + } + } +
+
+ } +
+ } + }; + + // ── Location Tab ── + + private async Task GetLocationAsync() + { + locationLoading = true; + error = null; + StateHasChanged(); + try + { + location = await Client.GetGeolocationAsync(locationAccuracy, locationTimeout); + } + catch (Exception ex) { error = ex.Message; } + finally { locationLoading = false; StateHasChanged(); } + } + + private RenderFragment RenderLocationTab() => __builder => + { +
+ +
+ + s +
+ +
+ + @if (error != null) + { +
@error
+ } + + @if (locationLoading) + { +
Getting location...
+ } + else if (location != null) + { +
+ @RenderInfoCard("Coordinates", "fas fa-crosshairs", new Dictionary + { + ["Latitude"] = location.Latitude.ToString("F6"), + ["Longitude"] = location.Longitude.ToString("F6"), + ["Altitude"] = location.Altitude?.ToString("F1"), + ["Accuracy"] = location.Accuracy?.ToString("F1"), + }) + + @RenderInfoCard("Movement", "fas fa-tachometer-alt", new Dictionary + { + ["Speed"] = location.Speed?.ToString("F1"), + ["Course"] = location.Course?.ToString("F1"), + ["Timestamp"] = location.Timestamp.ToString("G"), + ["Mock Provider"] = location.IsFromMockProvider ? "⚠️ Yes" : "No" + }) +
+ } + else + { +
Click "Get Location" to request the device's current position.
+ } + }; + + public void Dispose() + { + Client.StopAllSensorStreams(); + sensorReadings.Clear(); + } +} + + diff --git a/src/MauiSherpa/Pages/Inspector/DevFlowProfilingTab.razor b/src/MauiSherpa/Pages/Inspector/DevFlowProfilingTab.razor index d1ca9998..cc69e9e0 100644 --- a/src/MauiSherpa/Pages/Inspector/DevFlowProfilingTab.razor +++ b/src/MauiSherpa/Pages/Inspector/DevFlowProfilingTab.razor @@ -56,7 +56,16 @@ } - @if (capabilities == null) + @if (!MeetsMinVersion) + { +
+ +
MauiDevFlow agent version @(AgentVersion ?? "unknown") does not support profiling.
+
Minimum required version: @MinRequiredVersion
+
Update the Redth.MauiDevFlow.Agent NuGet package in your app and rebuild.
+
+ } + else if (capabilities == null) {
@@ -99,8 +108,8 @@
Managed Heap
@FormatBytes(LatestSample?.ManagedBytes)
-
-
Native Heap
+
+
@NativeMemoryLabel
@FormatBytes(LatestSample?.NativeMemoryBytes)
@@ -167,7 +176,7 @@
- Native heap + @NativeMemoryLabel @FormatBytes(LatestSample?.NativeMemoryBytes)
@@ -300,7 +309,11 @@
Frame source: @(LatestSample?.FrameSource ?? "—")
Frame quality: @FormatFrameQuality(LatestSample?.FrameQuality)
Frame quality detail: @(LatestSample?.FrameQuality ?? "—")
-
Native heap: @FormatBytes(LatestSample?.NativeMemoryBytes)
+
@NativeMemoryLabel: @FormatBytes(LatestSample?.NativeMemoryBytes)
+ @if (!string.IsNullOrEmpty(LatestSample?.NativeMemoryKind)) + { +
Memory kind: @LatestSample.NativeMemoryKind
+ }
@@ -346,7 +359,9 @@ @code { [Parameter] public DevFlowAgentClient Client { get; set; } = null!; + [Parameter] public string? AgentVersion { get; set; } + private static readonly Version MinRequiredVersion = new(0, 20, 0); private const int MaxSampleHistory = 600; private const int MaxMarkerHistory = 300; private const int MaxSpanHistory = 600; @@ -374,7 +389,9 @@ private bool CanStart => !isBusy && capabilities?.Available == true && session?.IsActive != true; private bool CanStop => !isBusy && session?.IsActive == true; + private bool MeetsMinVersion => Version.TryParse(AgentVersion, out var v) && v >= MinRequiredVersion; private DevFlowProfilerSample? LatestSample => samples.Count > 0 ? samples[^1] : null; + private string NativeMemoryLabel => FormatNativeMemoryKind(LatestSample?.NativeMemoryKind); private (int Gc0, int Gc1, int Gc2) gcDelta => GetGcDelta(); private string fpsPoints => GetSparklinePoints(s => s.Fps); private string frameP95Points => GetSparklinePoints(s => s.FrameTimeMsP95); @@ -438,6 +455,9 @@ protected override async Task OnInitializedAsync() { + if (!MeetsMinVersion) + return; + await RefreshCapabilitiesAsync(); if (capabilities?.Available == true) { @@ -821,6 +841,15 @@ return frameQuality; } + private static string FormatNativeMemoryKind(string? kind) => kind switch + { + "apple.phys-footprint" => "Phys Footprint", + "android.native-heap-allocated" => "Native Heap", + "windows.working-set" => "Working Set", + "process.working-set-minus-managed" => "Native Memory", + _ => "Native Memory", + }; + private static string? BuildSpanDetail(DevFlowProfilerSpan span) { var details = new List(); diff --git a/src/MauiSherpa/Pages/Keystores.razor b/src/MauiSherpa/Pages/Keystores.razor index 7aaf416b..d0c355ce 100644 --- a/src/MauiSherpa/Pages/Keystores.razor +++ b/src/MauiSherpa/Pages/Keystores.razor @@ -599,9 +599,13 @@ else try { - var cloudKey = $"KEYSTORE_{alias}"; - await SyncService.DeleteKeystoreFromCloudAsync(cloudKey, default); - await LoadSyncStatuses(); + await SyncService.DeleteKeystoreFromCloudAsync(alias, default); + + // Update local collections directly — cloud provider may lag on ListSecrets after delete + cloudOnlyStatuses.RemoveAll(s => s.Alias == alias); + if (localId != null) + syncStatuses.Remove(localId); + StateHasChanged(); ToastService.ShowSuccess($"Keystore '{alias}' deleted from cloud."); } diff --git a/src/MauiSherpa/Pages/Modals/ProfilingCaptureWizardModal.razor b/src/MauiSherpa/Pages/Modals/ProfilingCaptureWizardModal.razor new file mode 100644 index 00000000..518a522f --- /dev/null +++ b/src/MauiSherpa/Pages/Modals/ProfilingCaptureWizardModal.razor @@ -0,0 +1,2312 @@ +@page "/modal/profiling-wizard" +@using MauiSherpa.Core.Interfaces +@using MauiSherpa.Core.Models.Profiling +@using MauiSherpa.Core.Requests.Android +@using MauiSherpa.Core.Requests.Apple +@using MauiSherpa.Core.Requests.Profiling +@using MauiSherpa.Pages.Forms +@using Shiny.Mediator +@inject HybridFormBridgeHolder BridgeHolder +@inject IMediator Mediator +@inject IProfilingCatalogService ProfilingCatalogService +@inject IDeviceMonitorService DeviceMonitor +@inject IDialogService DialogService +@inject IAlertService AlertService +@inject IPlatformService Platform +@inject IProfilingSessionRunner PipelineRunner +@inject IProfilingSessionStorageService SessionStorage +@inject IProfilingArtifactConverterService ArtifactConverter +@inject IJSRuntime JS +@implements IDisposable + +@{ + var bridge = BridgeHolder.Current; +} + +@if (bridge != null) +{ +
+ @if (catalog is null) + { +
+ + Loading profiling catalog... +
+ } + else + { + +
+ @for (int i = 0; i < WizardStepLabels.Length; i++) + { + var stepIndex = i; + var label = WizardStepLabels[i]; +
stepIndex ? "completed" : "")"> +
@(currentStep > stepIndex ? "✓" : (stepIndex + 1).ToString())
+
@label
+
+ @if (i < WizardStepLabels.Length - 1) + { +
+ } + } +
+ +
+ @if (currentStep == 0) + { +
+
+
+ +
+ + +
+
+ Launch planning needs a project path. Connect-to-running-app flows can omit it if you provide a process id. +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + Advanced settings +
+ @if (showAdvanced) + { +
+ + +
+ +
+ + +
+ +
+ + +
+ + @if (launchMode == ProfilingCaptureLaunchMode.Attach) + { +
+ + +
+ } + } +
+
+ } + + @if (currentStep == 1) + { +
+
+ + + @if (CurrentScenarioDefinition is not null) + { +
@CurrentScenarioDefinition.Description
+ } +
+ +
+ @foreach (var captureKind in SupportedCaptureKinds) + { + + } +
+ +
+ +
+ Pauses the app at startup until diagnostic tools connect. Useful for capturing startup performance. +
+
+
+ } + + @if (currentStep == 2) + { +
+
+ Targets + +
+ @if (TargetOptions.Count == 0) + { +
+ +
No targets are currently selectable for @GetPlatformDisplayName(selectedPlatform).
+ @if (selectedPlatform == ProfilingTargetPlatform.Android && knownAndroidEmulatorCount > 0) + { +
You have @knownAndroidEmulatorCount Android emulator definition(s). Start one from the Emulators page to profile it here.
+ } + @if (selectedPlatform == ProfilingTargetPlatform.iOS && knownAppleSimulatorCount > 0) + { +
You have @knownAppleSimulatorCount available Apple simulator(s). Select one after refreshing if it does not appear yet.
+ } + +
+ } + else + { +
+ @foreach (var option in TargetOptions) + { + + } +
+ } +
+ } + + @if (currentStep == 3) + { +
+
+ Prerequisites + @if (isCheckingPrerequisites) + { + Checking... + } + else if (prerequisiteReport is not null) + { +
+ @if (!prerequisiteReport.HasErrors && !prerequisiteReport.HasWarnings) + { + All OK + } + else + { + @if (prerequisiteReport.WarningCount > 0) + { + @prerequisiteReport.WarningCount warning + } + @if (prerequisiteReport.ErrorCount > 0) + { + @prerequisiteReport.ErrorCount error + } + } + +
+ } +
+ @if (prerequisiteReport is not null && (prerequisiteReport.HasErrors || prerequisiteReport.HasWarnings || showPrereqDetails)) + { +
+ @foreach (var check in prerequisiteReport.Checks) + { +
+ + @check.Name + @if (!string.IsNullOrWhiteSpace(check.InstalledVersion)) + { + @check.InstalledVersion + } + @if (check.Status != DependencyStatusType.Ok && check.Status != DependencyStatusType.Info) + { + @check.Message + } + @if (!string.IsNullOrWhiteSpace(check.SuggestedCommand)) + { + @check.SuggestedCommand + } +
+ } +
+ } +
+ +
+
+ Capture plan + @if (isCheckingPrerequisites) + { + Waiting... + } + else if (isPlanningCapture) + { + Generating... + } + else if (capturePlan is not null) + { +
+ + @(capturePlan.Validation.IsValid ? "Valid" : "Needs attention") + + + @(capturePlan.CanExecute ? "Executable" : "Preview only") + +
+ } +
+ @if (capturePlan is not null && !isPlanningCapture && !isCheckingPrerequisites) + { +
+
+ Target framework + @capturePlan.TargetFramework +
+
+ Output directory + @if (capturePlan.ExpectedArtifacts.Count > 0) + { + + @foreach (var artifact in capturePlan.ExpectedArtifacts) + { + @System.IO.Path.GetExtension(artifact.RelativePath) + } + + } +
+ @capturePlan.OutputDirectory +
+ + @if (capturePlan.Validation.Errors.Count > 0) + { +
+
Errors
+
    + @foreach (var error in capturePlan.Validation.Errors) + { +
  • @error
  • + } +
+
+ } + + @if (capturePlan.Validation.Warnings.Count > 0) + { +
+
Warnings
+
    + @foreach (var warning in capturePlan.Validation.Warnings) + { +
  • @warning
  • + } +
+
+ } + + @if (capturePlan.RuntimeBindings.Count > 0) + { +
+
Runtime bindings
+
    + @foreach (var binding in capturePlan.RuntimeBindings) + { +
  • + @binding.Token — @binding.Description + @if (!string.IsNullOrWhiteSpace(binding.ExampleValue)) + { + (example: @binding.ExampleValue) + } +
  • + } +
+
+ } + +
Commands @capturePlan.Commands.Count
+
+ @foreach (var step in capturePlan.Commands) + { + var stepId = step.DisplayName.Replace(" ", "-").ToLowerInvariant(); +
+
+ @step.DisplayName + + + + @if (step.IsLongRunning) + { + Long-running + } + + + +
+ @if (expandedCommands.Contains(stepId)) + { + @BuildCommandLine(step) + } +
+ } +
+ } +
+ } + + @if (currentStep == 4) + { +
+
+ Capture + @if (pipelineState is ProfilingPipelineState.Running or ProfilingPipelineState.WaitingForStop) + { + @FormatElapsed(pipelineElapsed) + } +
+ @if (pipelineState == ProfilingPipelineState.NotStarted) + { +
+ +
Starting capture pipeline...
+
+ } + else + { +
+ + @GetPipelineStateText() +
+ +
+ @foreach (var stepStatus in PipelineRunner.Steps) + { +
+
+
+ @switch (stepStatus.State) + { + case ProfilingStepState.Running: + + break; + case ProfilingStepState.Completed: + + break; + case ProfilingStepState.Failed: + + break; + case ProfilingStepState.Stopped: + + break; + case ProfilingStepState.Skipped: + + break; + case ProfilingStepState.Cancelled: + + break; + default: + + break; + } +
+
+
@stepStatus.DisplayName
+
+ @stepStatus.Kind + @if (stepStatus.IsLongRunning) + { + Long-running + } + @if (stepStatus.Duration.HasValue) + { + @FormatElapsed(stepStatus.Duration.Value) + } + else if (stepStatus.State == ProfilingStepState.Running && stepStatus.StartedAt.HasValue) + { + @FormatElapsed(DateTime.Now - stepStatus.StartedAt.Value) + } +
+
+
+ +
+
+ @if (stepStatus.ErrorMessage is not null && stepStatus.State == ProfilingStepState.Failed) + { +
+ @stepStatus.ErrorMessage +
+ } + @if (expandedStepLogs.Contains(stepStatus.StepId)) + { +
+ @if (stepStatus.OutputLines.Count == 0) + { +
No output yet
+ } + else + { + @foreach (var line in stepStatus.OutputLines.TakeLast(200)) + { +
@line.Text
+ } + } +
+ } +
+ } +
+ } +
+ + @if (pipelineState == ProfilingPipelineState.WaitingForStop) + { + var hasTraceCapture = selectedCaptureKinds.Any(k => + k is ProfilingCaptureKind.Cpu or ProfilingCaptureKind.Startup + or ProfilingCaptureKind.Network or ProfilingCaptureKind.Rendering + or ProfilingCaptureKind.Energy or ProfilingCaptureKind.SystemTrace); + var hasMemoryCapture = selectedCaptureKinds.Contains(ProfilingCaptureKind.Memory); + + @if (hasTraceCapture || hasMemoryCapture) + { +
+
+ On-demand Actions + (only one tool can use the diagnostic port at a time) +
+
+ @if (hasTraceCapture) + { + @if (PipelineRunner.IsTraceActive) + { + + } + else + { + + } + @if (traceCount > 0) + { + @traceCount trace@(traceCount != 1 ? "s" : "") captured + } + } + + @if (hasMemoryCapture) + { + + @if (gcDumpCount > 0) + { + @gcDumpCount snapshot@(gcDumpCount != 1 ? "s" : "") collected + } + } +
+
+ } + } + + @if (pipelineState == ProfilingPipelineState.Running) + { +
+ + Launching capture pipeline... +
+ } + } +
+ } +
+} + +@code { + private ProfilingCatalog? catalog; + private ConnectedDevicesSnapshot snapshot = ConnectedDevicesSnapshot.Empty; + private List androidEmulators = new(); + private List appleSimulators = new(); + private List targetOptions = new(); + private HashSet selectedCaptureKinds = new(); + private ProfilingPrerequisiteReport? prerequisiteReport; + private ProfilingCapturePlan? capturePlan; + private string? selectedTargetKey; + private string? projectPath; + private string? targetFrameworkOverride; + private string? outputDirectory; + private string? processIdText; + private string configuration = "Release"; + private int diagnosticPort = 9000; + private bool suspendAtStartup = false; + private bool isRefreshingTargets; + private bool isCheckingPrerequisites; + private bool isPlanningCapture; + private int knownAndroidEmulatorCount; + private int knownAppleSimulatorCount; + private ProfilingTargetPlatform selectedPlatform = ProfilingTargetPlatform.Android; + private ProfilingScenarioKind selectedScenario = ProfilingScenarioKind.Launch; + private ProfilingCaptureLaunchMode launchMode = ProfilingCaptureLaunchMode.Launch; + + private int currentStep = 0; + private bool showAdvanced = false; + private bool showPrereqDetails = false; + + // Pipeline state + private ProfilingPipelineState pipelineState = ProfilingPipelineState.NotStarted; + private ProfilingPipelineResult? pipelineResult; + private TimeSpan pipelineElapsed; + private System.Threading.Timer? pipelineTimer; + private DateTime pipelineStartTime; + private HashSet expandedStepLogs = new(); + private HashSet expandedCommands = new(); + private bool isCollectingGcDump; + private int gcDumpCount; + private int traceCount; + + private string? activeSessionId; + + private static readonly string[] WizardStepLabels = new[] + { + "Project", + "Capture Kinds", + "Target", + "Review & Plan", + "Capture" + }; + + private bool CanProceedToNext => currentStep switch + { + 0 => launchMode == ProfilingCaptureLaunchMode.Attach || !string.IsNullOrWhiteSpace(projectPath), + 1 => selectedCaptureKinds.Count > 0, + 2 => SelectedTarget is not null, + 3 => capturePlan is not null && capturePlan.Validation.IsValid && !isCheckingPrerequisites && !isPlanningCapture, + _ => false + }; + + private IReadOnlyList TargetOptions => targetOptions; + private TargetOption? SelectedTarget => targetOptions.FirstOrDefault(option => option.Key == selectedTargetKey); + private ProfilingPlatformCapabilities? CurrentPlatformCapabilities => catalog?.Platforms.FirstOrDefault(platform => platform.Platform == selectedPlatform); + private ProfilingScenarioDefinition? CurrentScenarioDefinition => catalog?.Scenarios.FirstOrDefault(scenario => scenario.Kind == selectedScenario); + private IReadOnlyList SupportedCaptureKinds => CurrentPlatformCapabilities?.SupportedCaptureKinds ?? Array.Empty(); + private string? CurrentPlatformNotes => CurrentPlatformCapabilities?.Notes; + + protected override async Task OnInitializedAsync() + { + var bridge = BridgeHolder.Current; + if (bridge == null) return; + + bridge.SubmitRequested += OnSubmitRequested; + bridge.BackRequested += OnBackRequested; + bridge.NextRequested += OnNextRequested; + bridge.CancelRequested += OnCancelRequested; + + DeviceMonitor.Changed += OnDeviceMonitorChanged; + + catalog = await ProfilingCatalogService.GetCatalogAsync(); + selectedPlatform = catalog.Platforms.FirstOrDefault(platform => platform.Platform == ProfilingTargetPlatform.Android)?.Platform + ?? catalog.Platforms.First().Platform; + selectedScenario = catalog.Scenarios.FirstOrDefault(scenario => scenario.Kind == ProfilingScenarioKind.Launch)?.Kind + ?? catalog.Scenarios.First().Kind; + ResetCaptureKindsToScenarioDefaults(); + suspendAtStartup = selectedCaptureKinds.Contains(ProfilingCaptureKind.Startup); + + await RefreshTargetsAsync(); + UpdateWizardState(); + } + + private void UpdateWizardState() + { + var bridge = BridgeHolder.Current; + if (bridge == null) return; + + var isCapturing = currentStep == 4 && pipelineState is ProfilingPipelineState.Running or ProfilingPipelineState.WaitingForStop or ProfilingPipelineState.Completing; + var canStop = currentStep == 4 && pipelineState == ProfilingPipelineState.WaitingForStop; + + bridge.PreventClose = isCapturing; + bridge.PreventSubmitClose = isCapturing; + + bridge.SetWizardState( + showBack: currentStep > 0 && currentStep != 4, + showNext: currentStep < 3, + showSubmit: currentStep == 3 || canStop, + canProceed: canStop || (CanProceedToNext && !isCapturing), + submitText: canStop ? "Stop Capture" : "Start Capture" + ); + } + + private async Task OnNextRequested() + { + if (!CanProceedToNext) return; + + if (currentStep == 2) + { + currentStep = 3; + await InvokeAsync(StateHasChanged); + await CheckPrerequisitesAsync(); + if (SelectedTarget is not null) + await GeneratePlanAsync(); + } + else + { + currentStep = Math.Min(currentStep + 1, 4); + } + + UpdateWizardState(); + await InvokeAsync(StateHasChanged); + } + + private Task OnBackRequested() + { + if (currentStep > 0 && currentStep != 4) + { + currentStep--; + UpdateWizardState(); + InvokeAsync(StateHasChanged); + } + return Task.CompletedTask; + } + + private async Task OnSubmitRequested() + { + if (currentStep == 4 && pipelineState == ProfilingPipelineState.WaitingForStop) + { + await HandleStopCapture(); + return; + } + + if (currentStep == 3 && CanProceedToNext) + { + // Start Capture — advance to step 4 and run the pipeline + currentStep = 4; + UpdateWizardState(); + await InvokeAsync(StateHasChanged); + await StartPipelineAsync(); + } + } + + private void OnDeviceMonitorChanged(ConnectedDevicesSnapshot updatedSnapshot) + { + snapshot = updatedSnapshot; + BuildTargetOptions(); + InvokeAsync(StateHasChanged); + } + + private async Task RefreshTargetsAsync(bool forceRefresh = false) + { + isRefreshingTargets = true; + StateHasChanged(); + + try + { + if (forceRefresh) + { + await Mediator.FlushStores("android:emulators"); + await Mediator.FlushStores("apple:simulators"); + } + + var emuTask = Mediator.Request(new GetEmulatorsRequest()); + var simTask = Mediator.Request(new GetSimulatorsRequest()); + + await Task.WhenAll(emuTask, simTask); + + var (_, emuResult) = await emuTask; + androidEmulators = emuResult?.ToList() ?? new(); + knownAndroidEmulatorCount = androidEmulators.Count; + + var (_, simResult) = await simTask; + appleSimulators = simResult?.Where(simulator => simulator.IsAvailable).ToList() ?? new(); + knownAppleSimulatorCount = appleSimulators.Count; + + snapshot = DeviceMonitor.Current; + BuildTargetOptions(); + } + catch (Exception ex) + { + await AlertService.ShowToastAsync($"Error refreshing profiling targets: {ex.Message}"); + } + finally + { + isRefreshingTargets = false; + StateHasChanged(); + } + } + + private Task OnPlatformChangedAsync() + { + ResetCaptureKindsToScenarioDefaults(); + BuildTargetOptions(); + prerequisiteReport = null; + capturePlan = null; + UpdateWizardState(); + return Task.CompletedTask; + } + + private Task OnScenarioChangedAsync() + { + ResetCaptureKindsToScenarioDefaults(); + // Auto-toggle suspend based on whether Startup capture kind is selected + suspendAtStartup = selectedCaptureKinds.Contains(ProfilingCaptureKind.Startup); + prerequisiteReport = null; + capturePlan = null; + UpdateWizardState(); + return Task.CompletedTask; + } + + private Task OnLaunchModeChangedAsync() + { + capturePlan = null; + UpdateWizardState(); + return Task.CompletedTask; + } + + private void ResetCaptureKindsToScenarioDefaults() + { + var defaults = CurrentScenarioDefinition?.DefaultCaptureKinds + .Where(kind => SupportedCaptureKinds.Contains(kind)) + .ToArray(); + + selectedCaptureKinds = defaults is { Length: > 0 } + ? defaults.ToHashSet() + : SupportedCaptureKinds.ToHashSet(); + } + + private void SelectTarget(string key) + { + selectedTargetKey = key; + capturePlan = null; + } + + private void OnCaptureKindChanged(ProfilingCaptureKind kind, ChangeEventArgs args) + { + var isSelected = args.Value as bool? == true; + if (isSelected) + selectedCaptureKinds.Add(kind); + else if (selectedCaptureKinds.Count > 1) + selectedCaptureKinds.Remove(kind); + + // Auto-toggle suspend when Startup capture kind changes + if (kind == ProfilingCaptureKind.Startup) + suspendAtStartup = isSelected; + + capturePlan = null; + } + + private async Task BrowseProjectAsync() + { + var selectedPath = await DialogService.PickOpenFileAsync("Select project", new[] { ".csproj" }); + if (!string.IsNullOrWhiteSpace(selectedPath)) + { + projectPath = selectedPath; + capturePlan = null; + UpdateWizardState(); + StateHasChanged(); + } + } + + private async Task CheckPrerequisitesAsync() + { + isCheckingPrerequisites = true; + UpdateWizardState(); + StateHasChanged(); + + try + { + var captureKinds = GetSelectedCaptureKinds(); + var (_, report) = await Mediator.Request(new GetProfilingPrerequisitesRequest(selectedPlatform, captureKinds)); + prerequisiteReport = report; + } + catch (Exception ex) + { + await AlertService.ShowToastAsync($"Unable to load profiling prerequisites: {ex.Message}"); + } + finally + { + isCheckingPrerequisites = false; + UpdateWizardState(); + StateHasChanged(); + } + } + + private async Task GeneratePlanAsync() + { + if (SelectedTarget is null) + { + await AlertService.ShowToastAsync("Select a profiling target first."); + return; + } + + isPlanningCapture = true; + UpdateWizardState(); + StateHasChanged(); + + try + { + var definition = ProfilingCatalogService.CreateSessionDefinition( + SelectedTarget.ToProfilingTarget(), + selectedScenario, + captureKinds: GetSelectedCaptureKinds()); + + var (_, plan) = await Mediator.Request(new PlanProfilingCaptureRequest(definition, BuildPlanOptions(SelectedTarget))); + + capturePlan = plan; + } + catch (Exception ex) + { + await AlertService.ShowToastAsync($"Unable to generate profiling plan: {ex.Message}"); + } + finally + { + isPlanningCapture = false; + UpdateWizardState(); + StateHasChanged(); + } + } + + private ProfilingCapturePlanOptions BuildPlanOptions(TargetOption selectedTarget) + { + var additionalBuildProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (selectedTarget.Platform == ProfilingTargetPlatform.Android && + selectedTarget.Kind is ProfilingTargetKind.PhysicalDevice or ProfilingTargetKind.Emulator) + { + additionalBuildProperties["AdbTarget"] = $"-s {selectedTarget.Identifier}"; + } + + if (selectedTarget.Platform == ProfilingTargetPlatform.iOS && + selectedTarget.Kind is ProfilingTargetKind.PhysicalDevice or ProfilingTargetKind.Simulator) + { + additionalBuildProperties["_DeviceName"] = $":v2:udid={selectedTarget.Identifier}"; + } + + int? processId = null; + if (int.TryParse(processIdText, out var parsedProcessId) && parsedProcessId > 0) + processId = parsedProcessId; + + // Pre-compute session storage path so commands are built with correct output dir + var effectiveOutputDir = outputDirectory; + if (string.IsNullOrWhiteSpace(effectiveOutputDir)) + { + var projectName = projectPath is not null + ? Path.GetFileNameWithoutExtension(projectPath) + : null; + var previewSessionId = SessionStorage.GenerateSessionId(projectName); + effectiveOutputDir = SessionStorage.GetSessionDirectoryPath(previewSessionId); + } + + return new ProfilingCapturePlanOptions( + ProjectPath: string.IsNullOrWhiteSpace(projectPath) ? null : projectPath, + Configuration: configuration, + TargetFramework: string.IsNullOrWhiteSpace(targetFrameworkOverride) ? null : targetFrameworkOverride, + OutputDirectory: effectiveOutputDir, + LaunchMode: launchMode, + DiagnosticPort: diagnosticPort, + SuspendAtStartup: suspendAtStartup, + ProcessId: processId, + AdditionalBuildProperties: additionalBuildProperties.Count == 0 ? null : additionalBuildProperties); + } + + private void BuildTargetOptions() + { + var options = new List(); + + switch (selectedPlatform) + { + case ProfilingTargetPlatform.Android: + options.AddRange(snapshot.AndroidDevices.Select(device => new TargetOption( + Key: $"android-device:{device.Serial}", + Platform: ProfilingTargetPlatform.Android, + Kind: ProfilingTargetKind.PhysicalDevice, + Identifier: device.Serial, + DisplayName: device.Model ?? device.Serial, + Subtitle: "Connected Android device", + KindLabel: "Device", + IsAvailable: true, + ExtraHint: device.State))); + + options.AddRange(snapshot.AndroidEmulators.Select(device => new TargetOption( + Key: $"android-emulator:{device.Serial}", + Platform: ProfilingTargetPlatform.Android, + Kind: ProfilingTargetKind.Emulator, + Identifier: device.Serial, + DisplayName: device.Model ?? device.Serial, + Subtitle: "Running Android emulator", + KindLabel: "Emulator", + IsAvailable: true, + ExtraHint: device.State))); + break; + + case ProfilingTargetPlatform.iOS: + options.AddRange(snapshot.ApplePhysicalDevices.Select(device => new TargetOption( + Key: $"ios-device:{device.Identifier}", + Platform: ProfilingTargetPlatform.iOS, + Kind: ProfilingTargetKind.PhysicalDevice, + Identifier: device.Identifier, + DisplayName: device.Name, + Subtitle: $"{device.ModelName} · iOS {device.OsVersion}", + KindLabel: "Device", + IsAvailable: device.IsAvailable, + ExtraHint: device.Interface))); + + options.AddRange(appleSimulators.Select(simulator => new TargetOption( + Key: $"ios-simulator:{simulator.Udid}", + Platform: ProfilingTargetPlatform.iOS, + Kind: ProfilingTargetKind.Simulator, + Identifier: simulator.Udid, + DisplayName: simulator.Name, + Subtitle: $"{simulator.Runtime ?? "Unknown runtime"} · {simulator.ProductFamily ?? "Simulator"}", + KindLabel: "Simulator", + IsAvailable: string.Equals(simulator.State, "Booted", StringComparison.OrdinalIgnoreCase), + ExtraHint: simulator.State))); + break; + + case ProfilingTargetPlatform.MacCatalyst: + if (Platform.IsMacCatalyst || Platform.IsMacOS) + { + options.Add(new TargetOption( + Key: "desktop:maccatalyst", + Platform: ProfilingTargetPlatform.MacCatalyst, + Kind: ProfilingTargetKind.Desktop, + Identifier: "local-maccatalyst", + DisplayName: "This Mac", + Subtitle: "Local Mac Catalyst target", + KindLabel: "Desktop", + IsAvailable: true)); + } + break; + + case ProfilingTargetPlatform.MacOS: + if (Platform.IsMacOS) + { + options.Add(new TargetOption( + Key: "desktop:macos", + Platform: ProfilingTargetPlatform.MacOS, + Kind: ProfilingTargetKind.Desktop, + Identifier: "local-macos", + DisplayName: "This Mac", + Subtitle: "Local macOS target", + KindLabel: "Desktop", + IsAvailable: true)); + } + break; + + case ProfilingTargetPlatform.Windows: + if (Platform.IsWindows) + { + options.Add(new TargetOption( + Key: "desktop:windows", + Platform: ProfilingTargetPlatform.Windows, + Kind: ProfilingTargetKind.Desktop, + Identifier: "local-windows", + DisplayName: "This PC", + Subtitle: "Local Windows target", + KindLabel: "Desktop", + IsAvailable: true)); + } + break; + } + + targetOptions = options; + if (selectedTargetKey is null || targetOptions.All(option => option.Key != selectedTargetKey)) + selectedTargetKey = targetOptions.FirstOrDefault()?.Key; + } + + private IReadOnlyList GetSelectedCaptureKinds() + => selectedCaptureKinds.Count > 0 + ? selectedCaptureKinds.OrderBy(kind => kind).ToArray() + : SupportedCaptureKinds; + + private async Task StartPipelineAsync() + { + if (capturePlan is null) return; + + // The plan already has the correct output directory from BuildPlanOptions. + // Derive the session ID from the output directory name. + activeSessionId = Path.GetFileName(capturePlan.OutputDirectory); + + pipelineState = ProfilingPipelineState.NotStarted; + pipelineResult = null; + expandedStepLogs.Clear(); + pipelineStartTime = DateTime.Now; + StateHasChanged(); + + PipelineRunner.PipelineStateChanged += OnPipelineStateChanged; + PipelineRunner.StepStateChanged += OnStepStateChanged; + PipelineRunner.StepOutputReceived += OnStepOutputReceived; + + pipelineTimer = new System.Threading.Timer(_ => + { + pipelineElapsed = DateTime.Now - pipelineStartTime; + InvokeAsync(() => + { + UpdateWizardState(); + StateHasChanged(); + }); + }, null, TimeSpan.Zero, TimeSpan.FromSeconds(1)); + + try + { + pipelineResult = await PipelineRunner.RunAsync(capturePlan); + pipelineState = pipelineResult.FinalState; + + // Save session and set bridge result + var manifest = await SaveSessionAsync(); + if (manifest is not null) + { + var bridge = BridgeHolder.Current; + if (bridge != null) + { + bridge.Result = manifest; + bridge.RequestClose(); + return; + } + } + } + catch (Exception ex) + { + pipelineState = ProfilingPipelineState.Failed; + await AlertService.ShowToastAsync($"Pipeline failed: {ex.Message}"); + } + finally + { + pipelineTimer?.Dispose(); + pipelineTimer = null; + PipelineRunner.PipelineStateChanged -= OnPipelineStateChanged; + PipelineRunner.StepStateChanged -= OnStepStateChanged; + PipelineRunner.StepOutputReceived -= OnStepOutputReceived; + UpdateWizardState(); + StateHasChanged(); + } + } + + private async Task SaveSessionAsync() + { + if (pipelineResult is null || capturePlan is null) return null; + + try + { + var projectName = projectPath is not null + ? Path.GetFileNameWithoutExtension(projectPath) + : null; + var sessionId = activeSessionId ?? SessionStorage.GenerateSessionId(projectName); + activeSessionId = sessionId; + + var target = SelectedTarget; + var manifest = new ProfilingSessionManifest + { + Id = sessionId, + Name = $"{projectName ?? "Session"} — {selectedPlatform} {target?.DisplayName ?? "Unknown"}", + Status = pipelineResult.Success ? ProfilingSessionStatus.Completed + : (pipelineState == ProfilingPipelineState.Cancelled ? ProfilingSessionStatus.Cancelled + : ProfilingSessionStatus.Failed), + CreatedAt = pipelineStartTime, + CompletedAt = DateTimeOffset.UtcNow, + Target = new ProfilingSessionTarget + { + Platform = selectedPlatform, + Kind = target?.Kind ?? ProfilingTargetKind.Desktop, + Identifier = target?.Identifier ?? "", + DisplayName = target?.DisplayName ?? selectedPlatform.ToString() + }, + Project = projectPath is not null ? new ProfilingSessionProject + { + Path = projectPath, + Name = projectName!, + Configuration = configuration, + TargetFramework = targetFrameworkOverride + } : null, + CaptureKinds = selectedCaptureKinds.ToList(), + Options = new ProfilingSessionOptions + { + LaunchMode = launchMode, + DiagnosticPort = diagnosticPort, + SuspendAtStartup = suspendAtStartup, + ProcessId = int.TryParse(processIdText, out var pid) ? pid : null, + Scenario = selectedScenario + }, + Pipeline = new ProfilingSessionPipelineSummary + { + Success = pipelineResult.Success, + Duration = pipelineResult.TotalDuration, + Steps = pipelineResult.StepResults.Select(step => new ProfilingSessionStepSummary + { + Id = step.StepId, + DisplayName = step.DisplayName, + State = step.State.ToString(), + CommandLine = capturePlan.Commands.FirstOrDefault(c => c.Id == step.StepId)?.CommandLine + }).ToList() + }, + Artifacts = new List() + }; + + var sessionDir = SessionStorage.GetSessionDirectoryPath(sessionId); + foreach (var artifactPath in pipelineResult.ArtifactPaths) + { + var fileName = Path.GetFileName(artifactPath); + var extension = GetArtifactExtension(fileName); + var kind = extension switch + { + ".nettrace" => ProfilingArtifactKind.Trace, + ".speedscope.json" => ProfilingArtifactKind.Export, + ".gcdump" => ProfilingArtifactKind.GcDump, + ".log" => ProfilingArtifactKind.Log, + _ => ProfilingArtifactKind.Other + }; + + long? size = null; + try { if (File.Exists(artifactPath)) size = new FileInfo(artifactPath).Length; } catch { } + + manifest.Artifacts.Add(new ProfilingSessionArtifact + { + FileName = fileName, + Kind = kind, + SizeBytes = size + }); + } + + if (Directory.Exists(sessionDir)) + { + // Scan for files not already in the artifact list (on-demand gcdumps, + // traces, and log files may have numbered names not in pipeline results) + foreach (var pattern in new[] { "*.log", "*.gcdump", "*.nettrace", "*.speedscope.json" }) + { + foreach (var file in Directory.GetFiles(sessionDir, pattern)) + { + var name = Path.GetFileName(file); + if (manifest.Artifacts.Any(a => a.FileName == name)) continue; + long? size = null; + try { size = new FileInfo(file).Length; } catch { } + + var ext = GetArtifactExtension(name); + var kind = ext switch + { + ".gcdump" => ProfilingArtifactKind.GcDump, + ".nettrace" => ProfilingArtifactKind.Trace, + ".speedscope.json" => ProfilingArtifactKind.Export, + _ => ProfilingArtifactKind.Log + }; + + manifest.Artifacts.Add(new ProfilingSessionArtifact + { + FileName = name, + Kind = kind, + SizeBytes = size + }); + } + } + } + + await SessionStorage.SaveSessionAsync(manifest); + return manifest; + } + catch (Exception ex) + { + await AlertService.ShowToastAsync($"Failed to save session: {ex.Message}"); + return null; + } + } + + private void OnPipelineStateChanged(object? sender, ProfilingPipelineStateChangedEventArgs e) + { + pipelineState = e.NewState; + InvokeAsync(() => + { + UpdateWizardState(); + StateHasChanged(); + }); + } + + private void OnStepStateChanged(object? sender, ProfilingStepStateChangedEventArgs e) + { + if (e.NewState == ProfilingStepState.Running) + { + expandedStepLogs.Add(e.StepId); + InvokeAsync(async () => + { + StateHasChanged(); + await Task.Yield(); + try { await JS.InvokeVoidAsync("logScrollInterop.track", $"step-log-{e.StepId}"); } catch { } + }); + return; + } + InvokeAsync(StateHasChanged); + } + + private void OnStepOutputReceived(object? sender, ProfilingStepOutputEventArgs e) + { + InvokeAsync(async () => + { + StateHasChanged(); + await Task.Yield(); + try { await JS.InvokeVoidAsync("logScrollInterop.scrollToBottom", $"step-log-{e.StepId}"); } catch { } + }); + } + + private async Task HandleStopCapture() + { + // Stop any active trace before stopping the pipeline + if (PipelineRunner.IsTraceActive) + { + await PipelineRunner.StopTraceAsync(); + } + await PipelineRunner.StopCaptureAsync(); + } + + private async Task CollectGcDumpOnDemandAsync() + { + if (isCollectingGcDump || PipelineRunner.IsTraceActive) return; + isCollectingGcDump = true; + StateHasChanged(); + + try + { + var path = await PipelineRunner.CollectGcDumpAsync(); + if (path is not null) + { + gcDumpCount++; + await AlertService.ShowToastAsync($"GC dump #{gcDumpCount} collected"); + } + else + { + await AlertService.ShowToastAsync("GC dump collection failed"); + } + } + catch (Exception ex) + { + await AlertService.ShowToastAsync($"GC dump failed: {ex.Message}"); + } + finally + { + isCollectingGcDump = false; + StateHasChanged(); + } + } + + private void StartTraceOnDemand() + { + if (PipelineRunner.IsTraceActive || isCollectingGcDump) return; + + var stepId = PipelineRunner.StartTraceAsync(); + if (stepId is not null) + { + traceCount++; + StateHasChanged(); + } + } + + private async Task StopTraceOnDemandAsync() + { + if (!PipelineRunner.IsTraceActive) return; + + await PipelineRunner.StopTraceAsync(); + await AlertService.ShowToastAsync($"Trace #{traceCount} captured"); + StateHasChanged(); + } + + private void HandleCancelPipeline() + { + PipelineRunner.Cancel(); + pipelineState = ProfilingPipelineState.Cancelled; + currentStep = 3; + UpdateWizardState(); + StateHasChanged(); + } + + private void OnCancelRequested() + { + if (currentStep == 4 && pipelineState is ProfilingPipelineState.Running or ProfilingPipelineState.WaitingForStop) + { + InvokeAsync(() => + { + HandleCancelPipeline(); + }); + } + } + + private async void ToggleStepLog(string stepId) + { + if (!expandedStepLogs.Remove(stepId)) + { + expandedStepLogs.Add(stepId); + StateHasChanged(); + await Task.Yield(); + try + { + await JS.InvokeVoidAsync("logScrollInterop.track", $"step-log-{stepId}"); + await JS.InvokeVoidAsync("logScrollInterop.scrollToBottom", $"step-log-{stepId}"); + } + catch { } + } + else + { + try { await JS.InvokeVoidAsync("logScrollInterop.untrack", $"step-log-{stepId}"); } catch { } + } + } + + private void ToggleCommandExpand(string commandId) + { + if (!expandedCommands.Remove(commandId)) + expandedCommands.Add(commandId); + } + + private string GetPipelineStateIcon() => pipelineState switch + { ProfilingPipelineState.Running => "fa-spinner fa-spin", + ProfilingPipelineState.WaitingForStop => "fa-circle-dot fa-beat-fade", + ProfilingPipelineState.Completing => "fa-spinner fa-spin", + ProfilingPipelineState.Completed => "fa-check-circle", + ProfilingPipelineState.Failed => "fa-times-circle", + ProfilingPipelineState.Cancelled => "fa-ban", + _ => "fa-circle" + }; + + private string GetPipelineStateText() => pipelineState switch + { + ProfilingPipelineState.Running => "Launching capture pipeline...", + ProfilingPipelineState.WaitingForStop => "Recording — click Stop Capture when ready", + ProfilingPipelineState.Completing => "Stopping capture and collecting artifacts...", + ProfilingPipelineState.Completed => "Capture completed successfully", + ProfilingPipelineState.Failed => "Capture failed — check step details below", + ProfilingPipelineState.Cancelled => "Capture cancelled", + _ => "Initializing..." + }; + + private static string FormatElapsed(TimeSpan ts) => + ts.TotalMinutes >= 1 ? $"{(int)ts.TotalMinutes}m {ts.Seconds}s" : $"{ts.Seconds}s"; + + private static string GetPlatformDisplayName(ProfilingTargetPlatform platform) => platform switch + { + ProfilingTargetPlatform.iOS => "iOS", + ProfilingTargetPlatform.MacCatalyst => "Mac Catalyst", + ProfilingTargetPlatform.MacOS => "macOS", + _ => platform.ToString() + }; + + private static string GetCaptureKindDisplayName(ProfilingCaptureKind captureKind) => captureKind switch + { + ProfilingCaptureKind.Cpu => "CPU trace", + ProfilingCaptureKind.Memory => "Memory / GC dump", + ProfilingCaptureKind.Network => "Network", + ProfilingCaptureKind.Rendering => "Rendering", + ProfilingCaptureKind.Energy => "Energy", + ProfilingCaptureKind.SystemTrace => "System trace", + ProfilingCaptureKind.Logs => "Logs", + ProfilingCaptureKind.Startup => "Startup", + _ => captureKind.ToString() + }; + + private static string GetDependencyCss(DependencyStatusType status) => status switch + { + DependencyStatusType.Ok => "ok", + DependencyStatusType.Info => "ok", + DependencyStatusType.Warning => "warning", + DependencyStatusType.Error => "error", + _ => "neutral" + }; + + private async Task CopyCommandAsync(ProfilingCommandStep step) + { + await DialogService.CopyToClipboardAsync(BuildCommandLine(step)); + await AlertService.ShowToastAsync("Copied command to clipboard."); + } + + private string BuildCommandLine(ProfilingCommandStep step) + { + var resolved = ResolveCommandStep(step); + return resolved?.CommandLine ?? step.CommandLine; + } + + private ProcessRequest? ResolveCommandStep(ProfilingCommandStep step) + { + if (step.RequiredRuntimeBindings is not null) + { + foreach (var binding in step.RequiredRuntimeBindings) + { + if (!TryResolveRuntimeBinding(binding, out _)) + return null; + } + } + + var resolvedArguments = step.Arguments.Select(ResolveRuntimeTokens).ToArray(); + var resolvedEnvironment = step.Environment?.ToDictionary( + kvp => kvp.Key, + kvp => ResolveRuntimeTokens(kvp.Value), + StringComparer.OrdinalIgnoreCase); + + return new ProcessRequest( + step.Command, + resolvedArguments, + step.WorkingDirectory, + Environment: resolvedEnvironment, + Title: step.DisplayName, + Description: step.Description); + } + + private string ResolveRuntimeTokens(string value) + { + foreach (var binding in capturePlan?.RuntimeBindings ?? Array.Empty()) + { + if (TryResolveRuntimeBinding(binding.Token, out var resolvedValue)) + value = value.Replace(binding.Token, resolvedValue, StringComparison.Ordinal); + } + + return value; + } + + private bool TryResolveRuntimeBinding(string token, out string value) + { + if (token == "{{PROCESS_ID}}" && int.TryParse(processIdText, out var parsedProcessId) && parsedProcessId > 0) + { + value = parsedProcessId.ToString(); + return true; + } + + value = string.Empty; + return false; + } + + private static string GetArtifactExtension(string path) + { + if (path.EndsWith(".speedscope.json", StringComparison.OrdinalIgnoreCase)) + return ".speedscope.json"; + return Path.GetExtension(path).ToLowerInvariant(); + } + + public void Dispose() + { + var bridge = BridgeHolder.Current; + if (bridge != null) + { + bridge.SubmitRequested -= OnSubmitRequested; + bridge.BackRequested -= OnBackRequested; + bridge.NextRequested -= OnNextRequested; + bridge.CancelRequested -= OnCancelRequested; + } + + DeviceMonitor.Changed -= OnDeviceMonitorChanged; + pipelineTimer?.Dispose(); + } + + private sealed record TargetOption( + string Key, + ProfilingTargetPlatform Platform, + ProfilingTargetKind Kind, + string Identifier, + string DisplayName, + string Subtitle, + string KindLabel, + bool IsAvailable, + string? ExtraHint = null) + { + public ProfilingTarget ToProfilingTarget() => new( + Platform, + Kind, + Identifier, + DisplayName); + } +} + + diff --git a/src/MauiSherpa/Pages/Modals/ProfilingCaptureWizardPage.cs b/src/MauiSherpa/Pages/Modals/ProfilingCaptureWizardPage.cs new file mode 100644 index 00000000..47493222 --- /dev/null +++ b/src/MauiSherpa/Pages/Modals/ProfilingCaptureWizardPage.cs @@ -0,0 +1,31 @@ +using MauiSherpa.Core.Models.Profiling; +using MauiSherpa.Pages.Forms; +#if MACOSAPP +using Microsoft.Maui.Platform.MacOS; +#endif +#if LINUXGTK +using Platform.Maui.Linux.Gtk4.Platform; +#endif + +namespace MauiSherpa.Pages.Modals; + +public class ProfilingCaptureWizardPage : WizardFormPage +{ + protected override string FormTitle => "New Profiling Session"; + protected override string DefaultSubmitText => "Start Capture"; + protected override string BlazorRoute => "/modal/profiling-wizard"; + + public ProfilingCaptureWizardPage(HybridFormBridgeHolder bridgeHolder) + : base(bridgeHolder) + { +#if MACOSAPP + MacOSPage.SetModalSheetSizesToContent(this, false); + MacOSPage.SetModalSheetWidth(this, 750); + MacOSPage.SetModalSheetHeight(this, 700); +#elif LINUXGTK + GtkPage.SetModalSizesToContent(this, false); + GtkPage.SetModalWidth(this, 750); + GtkPage.SetModalHeight(this, 700); +#endif + } +} diff --git a/src/MauiSherpa/Pages/Profiling.razor b/src/MauiSherpa/Pages/Profiling.razor new file mode 100644 index 00000000..ce5308f4 --- /dev/null +++ b/src/MauiSherpa/Pages/Profiling.razor @@ -0,0 +1,894 @@ +@page "/profiling" +@using MauiSherpa.Core.Interfaces +@using MauiSherpa.Core.Models.Profiling +@using MauiSherpa.Pages.Forms +@inject IAlertService AlertService +@inject IToolbarService ToolbarService +@inject IDialogService DialogService +@inject IPlatformService Platform +@inject IProfilingArtifactConverterService ArtifactConverter +@inject IProfilingSessionStorageService SessionStorage +@inject IFormModalService FormModal +@inject HybridFormBridgeHolder BridgeHolder +@inject MauiSherpa.Services.ProfilingViewerService ViewerService +@implements IDisposable + + + + +@if (isLoadingSessions) + { +
+ + Loading sessions... +
+ } + else if (filteredSessions.Count == 0 && sessions.Count == 0) + { +
+ +

No profiling sessions yet

+

Capture CPU traces, memory dumps, and more for your .NET MAUI apps.

+
+ } + else + { + @if (filteredSessions.Count == 0 && !string.IsNullOrEmpty(sessionSearchText)) + { +
+ +

No matching sessions

+

Try a different search term.

+
+ } + else + { +
+ @foreach (var session in filteredSessions) + { + var isExpanded = expandedSessionId == session.Id; +
+
+
+ +
+
+
@session.Name
+
+ @session.CreatedAt.LocalDateTime.ToString("MMM d, yyyy h:mm tt") + @session.Target.DisplayName + @if (session.Pipeline is not null) + { + @FormatElapsed(session.Pipeline.Duration) + } + @session.Artifacts.Count artifact(s) +
+
+
+ @foreach (var kind in session.CaptureKinds) + { + @kind + } + + @session.Status + +
+
+ +
+
+ + @if (isExpanded) + { +
+ @if (session.Project is not null) + { +
+ Project + @session.Project.Name +
+ } +
+ Platform + @session.Target.Platform — @session.Target.Kind +
+ @if (session.Pipeline is not null) + { +
+ Pipeline + + @(session.Pipeline.Success ? "✓ Succeeded" : "✗ Failed") — + @session.Pipeline.Steps.Count step(s) + +
+ } + + @{ + var profilingArtifacts = session.Artifacts.Where(a => !a.FileName.EndsWith(".log", StringComparison.OrdinalIgnoreCase)).ToList(); + var logArtifacts = session.Artifacts.Where(a => a.FileName.EndsWith(".log", StringComparison.OrdinalIgnoreCase)).ToList(); + } + + @if (profilingArtifacts.Count > 0) + { +
Artifacts
+
+ @foreach (var artifact in profilingArtifacts) + { + @RenderArtifactCard(session, artifact) + } +
+ } + + @if (logArtifacts.Count > 0) + { +
+ + + Log Files + @logArtifacts.Count + +
+ @foreach (var artifact in logArtifacts) + { + @RenderArtifactCard(session, artifact) + } +
+
+ } + +
+ + + +
+
+ } +
+ } +
+ } + } + +@code { + private List sessions = new(); + private List filteredSessions = new(); + private bool isLoadingSessions; + private string sessionSearchText = ""; + private string? expandedSessionId; + + // Per-artifact error state (keyed by file path) + private readonly Dictionary artifactErrors = new(); + + protected override async Task OnInitializedAsync() + { + SetupToolbar(); + ToolbarService.ToolbarItemClicked += OnToolbarItemClicked; + ToolbarService.SearchTextChanged += OnSearchTextChanged; + await LoadSessionsAsync(); + } + + private void OnToolbarItemClicked(string actionId) + { + if (actionId == "create") + _ = InvokeAsync(StartNewWizardAsync); + else if (actionId == "import") + _ = ImportSessionFromZipAsync(); + else if (actionId == "refresh") + _ = LoadSessionsAsync(); + } + + private void OnSearchTextChanged(string text) + { + sessionSearchText = text; + ApplySessionFilter(); + InvokeAsync(StateHasChanged); + } + + private async Task StartNewWizardAsync() + { + var page = new MauiSherpa.Pages.Modals.ProfilingCaptureWizardPage(BridgeHolder); + var result = await FormModal.ShowAsync(page); + + if (result is not null) + { + await LoadSessionsAsync(); + expandedSessionId = result.Id; + StateHasChanged(); + } + } + + private void SetupToolbar() + { + ToolbarService.ClearItems(); + ToolbarService.SetItems( + new ToolbarAction("create", "New Session", "plus"), + new ToolbarAction("import", "Import", "square.and.arrow.down"), + new ToolbarAction("refresh", "Refresh", "arrow.clockwise") + ); + ToolbarService.SetSearch("Search sessions..."); + } + + private async Task LoadSessionsAsync() + { + isLoadingSessions = true; + StateHasChanged(); + + try + { + sessions = (await SessionStorage.GetSessionsAsync()).ToList(); + ApplySessionFilter(); + } + catch (Exception ex) + { + await AlertService.ShowToastAsync($"Failed to load sessions: {ex.Message}"); + } + finally + { + isLoadingSessions = false; + StateHasChanged(); + } + } + + private void ApplySessionFilter() + { + if (string.IsNullOrWhiteSpace(sessionSearchText)) + { + filteredSessions = sessions; + } + else + { + filteredSessions = sessions + .Where(s => + s.Name.Contains(sessionSearchText, StringComparison.OrdinalIgnoreCase) || + s.Target.DisplayName.Contains(sessionSearchText, StringComparison.OrdinalIgnoreCase) || + s.Target.Platform.ToString().Contains(sessionSearchText, StringComparison.OrdinalIgnoreCase) || + (s.Project?.Name?.Contains(sessionSearchText, StringComparison.OrdinalIgnoreCase) ?? false)) + .ToList(); + } + } + + private void ToggleSessionExpand(string sessionId) + { + expandedSessionId = expandedSessionId == sessionId ? null : sessionId; + } + + private async Task DeleteSessionAsync(ProfilingSessionManifest session) + { + var confirmed = await AlertService.ShowConfirmAsync( + "Delete Session", + $"Delete \"{session.Name}\" and all its artifacts? This cannot be undone."); + if (!confirmed) return; + + try + { + await SessionStorage.DeleteSessionAsync(session.Id); + sessions.Remove(session); + ApplySessionFilter(); + if (expandedSessionId == session.Id) + expandedSessionId = null; + await AlertService.ShowToastAsync("Session deleted"); + } + catch (Exception ex) + { + await AlertService.ShowToastAsync($"Failed to delete session: {ex.Message}"); + } + } + + private async Task ExportSessionAsync(ProfilingSessionManifest session) + { + try + { + var outputPath = await DialogService.PickSaveFileAsync( + "Export Profiling Session", + $"{session.Id}", + ".zip"); + if (string.IsNullOrEmpty(outputPath)) return; + + // PickSaveFileAsync creates an empty file; delete so ZipFile can create + if (File.Exists(outputPath)) File.Delete(outputPath); + + await SessionStorage.ExportSessionAsync(session.Id, outputPath); + await AlertService.ShowToastAsync("Session exported"); + } + catch (Exception ex) + { + await AlertService.ShowToastAsync($"Export failed: {ex.Message}"); + } + } + + private async Task ImportSessionFromZipAsync() + { + try + { + var filePath = await DialogService.PickOpenFileAsync( + "Import Profiling Session", + new[] { ".zip" }); + if (string.IsNullOrEmpty(filePath)) return; + + var imported = await SessionStorage.ImportSessionAsync(filePath); + if (imported is null) + { + await AlertService.ShowToastAsync("Invalid session archive — missing session.json"); + return; + } + + await LoadSessionsAsync(); + await AlertService.ShowToastAsync($"Session \"{imported.Name}\" imported"); + } + catch (Exception ex) + { + await AlertService.ShowToastAsync($"Import failed: {ex.Message}"); + } + } + + private void RevealSessionFolder(ProfilingSessionManifest session) + { + if (session.DirectoryPath is null) return; + RevealInFinder(session.DirectoryPath); + } + + private static string GetPlatformIcon(ProfilingTargetPlatform platform) => platform switch + { + ProfilingTargetPlatform.Android => "fa-brands fa-android", + ProfilingTargetPlatform.iOS => "fa-brands fa-apple", + ProfilingTargetPlatform.MacCatalyst => "fa-laptop", + ProfilingTargetPlatform.MacOS => "fa-laptop", + ProfilingTargetPlatform.Windows => "fa-brands fa-windows", + _ => "fa-desktop" + }; + + private static string FormatElapsed(TimeSpan ts) => + ts.TotalMinutes >= 1 ? $"{(int)ts.TotalMinutes}m {ts.Seconds}s" : $"{ts.Seconds}s"; + + private static string? GetFileSize(string path) + { + try + { + if (!File.Exists(path)) return null; + var bytes = new FileInfo(path).Length; + if (bytes >= 1024 * 1024) return $"{bytes / (1024.0 * 1024.0):F1} MB"; + if (bytes >= 1024) return $"{bytes / 1024.0:F1} KB"; + return $"{bytes} B"; + } + catch { return null; } + } + + private RenderFragment RenderArtifactCard(ProfilingSessionManifest session, ProfilingSessionArtifact artifact) => __builder => + { + var filePath = session.DirectoryPath is not null + ? Path.Combine(session.DirectoryPath, artifact.FileName) + : artifact.FileName; + var extension = GetArtifactExtension(artifact.FileName); + var isSpeedscopeFile = extension is ".speedscope.json"; + var isNettraceFile = extension is ".nettrace"; + // On macOS, .nettrace View is redundant (speedscope.json is alongside it). + // Only show View for .nettrace on Windows where PerfView can open it. + var isTraceFile = isSpeedscopeFile || (isNettraceFile && OperatingSystem.IsWindows()); + var isGcDump = extension is ".gcdump"; + +
+
+ +
+
+ @artifact.FileName + @if (artifact.SizeBytes is not null) + { + @FormatBytes(artifact.SizeBytes.Value) + } +
+
+ @if (isTraceFile) + { + + } + @if (isGcDump) + { + + } + + +
+
+ @if (artifactErrors.TryGetValue(filePath, out var artifactError)) + { +
+ + @artifactError + + +
+ } + }; + + private static string GetArtifactExtension(string path) + { + if (path.EndsWith(".speedscope.json", StringComparison.OrdinalIgnoreCase)) + return ".speedscope.json"; + return Path.GetExtension(path).ToLowerInvariant(); + } + + private static string GetArtifactIcon(string extension) => extension switch + { + ".nettrace" => "fa-wave-square", + ".speedscope.json" => "fa-fire", + ".gcdump" => "fa-memory", + ".txt" or ".log" => "fa-file-lines", + ".json" => "fa-code", + _ => "fa-file" + }; + + private static string FormatBytes(long bytes) + { + if (bytes >= 1024 * 1024) return $"{bytes / (1024.0 * 1024.0):F1} MB"; + if (bytes >= 1024) return $"{bytes / 1024.0:F1} KB"; + return $"{bytes:N0} B"; + } + + // Speedscope viewer — opens in a new window + private async Task ViewInSpeedscope(string path) + { + try + { + artifactErrors.Remove(path); + string? speedscopePath = path; + + if (path.EndsWith(".nettrace", StringComparison.OrdinalIgnoreCase)) + { + var baseName = path[..^".nettrace".Length]; + var existingSpeedscope = baseName + ".speedscope.json"; + if (File.Exists(existingSpeedscope)) + { + speedscopePath = existingSpeedscope; + } + else + { + speedscopePath = await ArtifactConverter.ConvertToSpeedscopeAsync(path); + if (speedscopePath is null) + { + artifactErrors[path] = "Failed to convert trace to speedscope format. Make sure dotnet-trace is installed."; + StateHasChanged(); + return; + } + } + } + + if (!File.Exists(speedscopePath)) + { + artifactErrors[path] = "Conversion produced no output — the .speedscope.json file is missing."; + StateHasChanged(); + return; + } + + ViewerService.OpenSpeedscope(speedscopePath); + } + catch (Exception ex) + { + artifactErrors[path] = $"Failed to open trace viewer: {ex.Message}"; + StateHasChanged(); + } + } + + private void DismissArtifactError(string path) + { + artifactErrors.Remove(path); + StateHasChanged(); + } + + // GC dump viewer — opens in a new window + private void ViewGcDump(string path) + { + ViewerService.OpenGcDump(path); + } + + private void RevealInFinder(string path) + { + try + { + var dir = Path.GetDirectoryName(path) ?? path; + if (Platform.IsMacCatalyst || Platform.IsMacOS) + { + System.Diagnostics.Process.Start("open", $"-R \"{path}\""); + } + else + { + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(dir) { UseShellExecute = true }); + } + } + catch { } + } + + private async Task CopyPathAsync(string path) + { + await DialogService.CopyToClipboardAsync(path); + await AlertService.ShowToastAsync("Path copied to clipboard"); + } + + public void Dispose() + { + ToolbarService.ToolbarItemClicked -= OnToolbarItemClicked; + ToolbarService.SearchTextChanged -= OnSearchTextChanged; + ToolbarService.ClearItems(); + } +} + + + diff --git a/src/MauiSherpa/Pages/SpeedscopeViewer.razor b/src/MauiSherpa/Pages/SpeedscopeViewer.razor new file mode 100644 index 00000000..d89c5da3 --- /dev/null +++ b/src/MauiSherpa/Pages/SpeedscopeViewer.razor @@ -0,0 +1,150 @@ +@page "/profiling/speedscope" +@using Microsoft.AspNetCore.Components +@using Microsoft.JSInterop +@using MauiSherpa.Core.Interfaces +@inject IJSRuntime JS + +
+
+ + @(fileName ?? "Trace Viewer") + @if (!string.IsNullOrEmpty(fileName)) + { + @fileName + } +
+ +
+ @if (loading) + { +
+ +
Loading trace viewer...
+
+ } + @if (errorMessage is not null) + { +
+ + @errorMessage +
+ } + +
+
+ +@code { + [SupplyParameterFromQuery] public string? file { get; set; } + + private string? filePath; + private string? fileName; + private bool loading = true; + private string? errorMessage; + private IJSObjectReference? speedscopeModule; + + protected override void OnInitialized() + { + if (!string.IsNullOrEmpty(file)) + { + filePath = Uri.UnescapeDataString(file); + fileName = Path.GetFileName(filePath); + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender || filePath is null) + return; + + if (!File.Exists(filePath)) + { + errorMessage = $"File not found: {filePath}"; + loading = false; + StateHasChanged(); + return; + } + + try + { + var fileBytes = await File.ReadAllBytesAsync(filePath); + var base64 = Convert.ToBase64String(fileBytes); + + speedscopeModule ??= await JS.InvokeAsync( + "import", "./js/speedscopeInterop.js"); + + await speedscopeModule.InvokeVoidAsync( + "openSpeedscopeWithFile", "speedscope-frame", base64, fileName); + } + catch (Exception ex) + { + errorMessage = $"Failed to load trace: {ex.Message}"; + } + finally + { + loading = false; + StateHasChanged(); + } + } +} + + diff --git a/src/MauiSherpa/Resources/Raw/Skills/maui-profiling/SKILL.md b/src/MauiSherpa/Resources/Raw/Skills/maui-profiling/SKILL.md new file mode 100644 index 00000000..778be7bf --- /dev/null +++ b/src/MauiSherpa/Resources/Raw/Skills/maui-profiling/SKILL.md @@ -0,0 +1,26 @@ +--- +name: maui-profiling +description: Build lightweight profiling context for a running .NET MAUI app using local MauiDevFlow status, network, and visual-tree summaries instead of uploading raw trace artifacts. Use when investigating slow screens, performance regressions, excessive network chatter, or deciding whether deeper trace capture is needed. +--- + +# MAUI Profiling Context + +Use structured, local summaries first. Avoid asking for raw `.nettrace`, `gcdump`, or other large artifacts unless the lightweight snapshot is insufficient. + +## Workflow + +1. Run `get_profiling_catalog` to understand supported scenarios and platform capabilities. +2. Run `list_profiling_targets` to discover locally running MauiDevFlow-enabled apps. +3. Run `get_profiling_snapshot` for the relevant target. +4. Review: + - runtime status (platform, device type, WebView readiness) + - recent network behavior (sample size, failures, latency, slowest requests) + - visual tree complexity (element counts, depth, top element types) +5. Use the summary to narrow the investigation before requesting any deeper traces. + +## Guidance + +- Prefer the default lightweight snapshot first. +- Increase `networkSampleSize` only when recent request volume is too low to explain the issue. +- If multiple targets are connected, specify `targetId` from `list_profiling_targets`. +- Treat the snapshot as context for follow-up recommendations, not as a full profiler replacement. diff --git a/src/MauiSherpa/Services/DialogService.cs b/src/MauiSherpa/Services/DialogService.cs index 48086a76..b084ca9f 100644 --- a/src/MauiSherpa/Services/DialogService.cs +++ b/src/MauiSherpa/Services/DialogService.cs @@ -63,6 +63,42 @@ await Dispatcher.DispatchAsync(() => viewController?.PresentViewController(alertController, true, null); }); + return await tcs.Task; +#elif MACOSAPP + var tcs = new TaskCompletionSource(); + + await Dispatcher.DispatchAsync(() => + { + var alert = new NSAlert + { + AlertStyle = NSAlertStyle.Informational, + MessageText = title, + InformativeText = message + }; + + var isPassword = title.Contains("Password", StringComparison.OrdinalIgnoreCase); + NSTextField input; + if (isPassword) + input = NSSecureTextField.CreateTextField(string.Empty); + else + input = NSTextField.CreateTextField(string.Empty); + + input.PlaceholderString = placeholder; + input.Frame = new CoreGraphics.CGRect(0, 0, 260, 24); + alert.AccessoryView = input; + + alert.AddButton("OK"); + alert.AddButton("Cancel"); + + alert.Window.InitialFirstResponder = input; + + var response = alert.RunModal(); + if (response == 1000) // NSAlertFirstButtonReturn (OK) + tcs.TrySetResult(input.StringValue); + else + tcs.TrySetResult(null); + }); + return await tcs.Task; #else // Windows implementation using ContentDialog would go here diff --git a/src/MauiSherpa/Services/GcDumpReportService.cs b/src/MauiSherpa/Services/GcDumpReportService.cs new file mode 100644 index 00000000..9c12b4fb --- /dev/null +++ b/src/MauiSherpa/Services/GcDumpReportService.cs @@ -0,0 +1,67 @@ +using System.Globalization; +using System.Text.RegularExpressions; +using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Models.Profiling; +using MauiSherpa.Core.Services; + +namespace MauiSherpa.Services; + +/// +/// Parses .gcdump files by running dotnet-gcdump report and parsing the heapstat output. +/// +public class GcDumpReportService : IGcDumpReportService +{ + private readonly ILoggingService _logger; + + public GcDumpReportService(ILoggingService logger) + { + _logger = logger; + } + + public async Task GetReportAsync(string gcdumpPath, CancellationToken ct = default) + { + if (!File.Exists(gcdumpPath)) + { + _logger.LogWarning($"GC dump file not found: {gcdumpPath}"); + return null; + } + + try + { + var startInfo = new System.Diagnostics.ProcessStartInfo + { + FileName = "dotnet-gcdump", + ArgumentList = { "report", gcdumpPath }, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = System.Diagnostics.Process.Start(startInfo); + if (process is null) + { + _logger.LogError("Failed to start dotnet-gcdump process"); + return null; + } + + var stdout = await process.StandardOutput.ReadToEndAsync(ct); + var stderr = await process.StandardError.ReadToEndAsync(ct); + + await process.WaitForExitAsync(ct); + + if (process.ExitCode != 0) + { + _logger.LogError($"dotnet-gcdump report failed (exit {process.ExitCode}): {stderr}"); + return null; + } + + return GcDumpReportParser.ParseHeapStatOutput(stdout); + } + catch (Exception ex) + { + _logger.LogError($"Failed to run dotnet-gcdump report: {ex.Message}", ex); + return null; + } + } +} diff --git a/src/MauiSherpa/Services/ProcessExecutionService.cs b/src/MauiSherpa/Services/ProcessExecutionService.cs index 7952eef9..299668cf 100644 --- a/src/MauiSherpa/Services/ProcessExecutionService.cs +++ b/src/MauiSherpa/Services/ProcessExecutionService.cs @@ -386,7 +386,9 @@ public void Cancel() { if (_platform.IsMacCatalyst || _platform.IsMacOS) { - // Send SIGINT on Unix + // Send SIGINT on Unix — let the process flush and exit on its own. + // WaitForExitAsync (called by the pipeline runner) will complete naturally + // once the process finishes writing output and exits. SendSignal(_currentProcess.Id, 2); // SIGINT } else @@ -396,21 +398,24 @@ public void Cancel() _currentProcess.StandardInput.Close(); } - // Give it a moment to gracefully stop - Task.Delay(500).ContinueWith(_ => + // Start a safety timeout: if the process hasn't exited after 30s, + // force-cancel so the pipeline doesn't hang forever. + _ = Task.Run(async () => { - if (_currentProcess != null && !_currentProcess.HasExited) + await Task.Delay(30_000); + if (_currentProcess is { HasExited: false }) { - OnOutput("Process did not stop gracefully. Use Force Kill if needed.", isError: true); + OnOutput("Process did not exit within 30s after SIGINT — forcing cancellation.", isError: true); + _linkedCts?.Cancel(); } }); } catch (Exception ex) { _logger.LogWarning($"Failed to send cancel signal: {ex.Message}"); + // If we can't signal, force cancel so we don't hang + _linkedCts?.Cancel(); } - - _linkedCts?.Cancel(); } public void Kill() diff --git a/src/MauiSherpa/Services/ProfilingArtifactConverterService.cs b/src/MauiSherpa/Services/ProfilingArtifactConverterService.cs new file mode 100644 index 00000000..a1a366a0 --- /dev/null +++ b/src/MauiSherpa/Services/ProfilingArtifactConverterService.cs @@ -0,0 +1,106 @@ +using MauiSherpa.Core.Interfaces; + +namespace MauiSherpa.Services; + +/// +/// Converts profiling artifacts between formats using dotnet CLI tools. +/// +public class ProfilingArtifactConverterService : IProfilingArtifactConverterService +{ + private readonly ILoggingService _logger; + + public ProfilingArtifactConverterService(ILoggingService logger) + { + _logger = logger; + } + + public async Task ConvertToSpeedscopeAsync(string nettracePath, CancellationToken ct = default) + { + if (!File.Exists(nettracePath)) + { + _logger.LogWarning($"Trace file not found: {nettracePath}"); + return null; + } + + // dotnet-trace convert appends ".speedscope.json" to the -o path, + // so we strip .nettrace and set the base output name + var dir = Path.GetDirectoryName(nettracePath) ?? "."; + var baseName = Path.GetFileNameWithoutExtension(nettracePath); + var outputPath = Path.Combine(dir, baseName); + + // The actual output file will be "{baseName}.speedscope.json" + var expectedOutputPath = Path.Combine(dir, $"{baseName}.speedscope.json"); + + // If already converted, return existing + if (File.Exists(expectedOutputPath)) + { + _logger.LogInformation($"Speedscope file already exists: {expectedOutputPath}"); + return expectedOutputPath; + } + + try + { + _logger.LogInformation($"Converting {nettracePath} to speedscope format..."); + + var startInfo = new System.Diagnostics.ProcessStartInfo + { + FileName = "dotnet-trace", + ArgumentList = { "convert", nettracePath, "--format", "speedscope", "-o", outputPath }, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = System.Diagnostics.Process.Start(startInfo); + if (process is null) + { + _logger.LogError("Failed to start dotnet-trace convert process"); + return null; + } + + var stdout = await process.StandardOutput.ReadToEndAsync(ct); + var stderr = await process.StandardError.ReadToEndAsync(ct); + + await process.WaitForExitAsync(ct); + + // dotnet-trace convert returns exit code 1 even on success sometimes, + // so check for the output file instead + if (File.Exists(expectedOutputPath)) + { + _logger.LogInformation($"Conversion complete: {expectedOutputPath}"); + return expectedOutputPath; + } + + // dotnet-trace sometimes appends double extension — check for that too + var doubleExtPath = Path.Combine(dir, $"{baseName}.speedscope.speedscope.json"); + if (File.Exists(doubleExtPath)) + { + // Rename to the expected single-extension name + File.Move(doubleExtPath, expectedOutputPath); + _logger.LogInformation($"Conversion complete (renamed): {expectedOutputPath}"); + return expectedOutputPath; + } + + // Search for any .speedscope.json file in the directory + var speedscopeFiles = Directory.GetFiles(dir, "*.speedscope.json"); + var newestSpeedscope = speedscopeFiles + .OrderByDescending(File.GetLastWriteTimeUtc) + .FirstOrDefault(); + + if (newestSpeedscope is not null) + { + _logger.LogInformation($"Found speedscope output at: {newestSpeedscope}"); + return newestSpeedscope; + } + + _logger.LogError($"dotnet-trace convert produced no output. stdout: {stdout}, stderr: {stderr}"); + return null; + } + catch (Exception ex) + { + _logger.LogError($"Failed to convert trace: {ex.Message}", ex); + return null; + } + } +} diff --git a/src/MauiSherpa/Services/ProfilingSessionRunnerService.cs b/src/MauiSherpa/Services/ProfilingSessionRunnerService.cs new file mode 100644 index 00000000..30319ca9 --- /dev/null +++ b/src/MauiSherpa/Services/ProfilingSessionRunnerService.cs @@ -0,0 +1,927 @@ +using Microsoft.Extensions.DependencyInjection; +using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Models.Profiling; + +namespace MauiSherpa.Services; + +public class ProfilingSessionRunnerService : IProfilingSessionRunner +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILoggingService _logger; + private readonly Dictionary _stepProcesses = new(); + private readonly Dictionary _stepLogWriters = new(); + private readonly List _steps = new(); + private ProfilingPipelineState _state = ProfilingPipelineState.NotStarted; + private CancellationTokenSource? _cts; + private ProfilingCapturePlan? _plan; + private string? _outputDirectory; + private DateTime _startTime; + private volatile bool _stopRequested; + private int _gcDumpCount; + private int _traceCount; + private TaskCompletionSource _stopTcs = new(); + + public ProfilingPipelineState State => _state; + public IReadOnlyList Steps => _steps; + + public event EventHandler? PipelineStateChanged; + public event EventHandler? StepStateChanged; + public event EventHandler? StepOutputReceived; + + public ProfilingSessionRunnerService(IServiceProvider serviceProvider, ILoggingService logger) + { + _serviceProvider = serviceProvider; + _logger = logger; + } + + public async Task RunAsync(ProfilingCapturePlan plan, CancellationToken ct = default) + { + _plan = plan; + _cts = CancellationTokenSource.CreateLinkedTokenSource(ct); + _startTime = DateTime.Now; + _stopRequested = false; + _gcDumpCount = 0; + _traceCount = 0; + _stopTcs = new TaskCompletionSource(); + + if (!string.IsNullOrWhiteSpace(plan.OutputDirectory)) + Directory.CreateDirectory(plan.OutputDirectory); + + _outputDirectory = plan.OutputDirectory; + + _steps.Clear(); + _stepProcesses.Clear(); + foreach (var cmd in plan.Commands) + { + _steps.Add(new ProfilingStepStatus + { + StepId = cmd.Id, + DisplayName = cmd.DisplayName, + Kind = cmd.Kind, + IsLongRunning = cmd.IsLongRunning, + CanRunParallel = cmd.CanRunParallel, + StopTrigger = cmd.StopTrigger + }); + } + + SetPipelineState(ProfilingPipelineState.Running); + + try + { + await ExecutePipelineAsync(plan.Commands, _cts.Token); + + SetPipelineState(ProfilingPipelineState.Completing); + FlushAllStepLogs(); + var (found, missing) = CollectArtifacts(plan); + + // Post-process: convert any .nettrace files to speedscope format + found = await ConvertTraceArtifactsAsync(found, _cts.Token); + + var finalState = _steps.Any(s => s.State == ProfilingStepState.Failed) + ? ProfilingPipelineState.Failed + : ProfilingPipelineState.Completed; + SetPipelineState(finalState); + + return new ProfilingPipelineResult( + Success: finalState == ProfilingPipelineState.Completed, + TotalDuration: DateTime.Now - _startTime, + FinalState: finalState, + StepResults: _steps.ToList(), + ArtifactPaths: found, + MissingArtifacts: missing); + } + catch (OperationCanceledException) + { + SetPipelineState(ProfilingPipelineState.Cancelled); + FlushAllStepLogs(); + KillAllProcesses(); + return new ProfilingPipelineResult( + Success: false, + TotalDuration: DateTime.Now - _startTime, + FinalState: ProfilingPipelineState.Cancelled, + StepResults: _steps.ToList(), + ArtifactPaths: Array.Empty(), + MissingArtifacts: Array.Empty()); + } + catch (Exception ex) + { + if (_stopRequested) + { + // Stop was requested — failures during shutdown are expected + _logger.LogInformation("Pipeline stopped by user. Collecting available artifacts."); + SetPipelineState(ProfilingPipelineState.Completing); + FlushAllStepLogs(); + var (stopFound, stopMissing) = CollectArtifacts(plan); + IReadOnlyList convertedStopFound = await ConvertTraceArtifactsAsync(stopFound, CancellationToken.None); + SetPipelineState(ProfilingPipelineState.Completed); + return new ProfilingPipelineResult( + Success: true, + TotalDuration: DateTime.Now - _startTime, + FinalState: ProfilingPipelineState.Completed, + StepResults: _steps.ToList(), + ArtifactPaths: convertedStopFound, + MissingArtifacts: stopMissing); + } + + _logger.LogError($"Pipeline failed: {ex.Message}", ex); + SetPipelineState(ProfilingPipelineState.Failed); + FlushAllStepLogs(); + KillAllProcesses(); + return new ProfilingPipelineResult( + Success: false, + TotalDuration: DateTime.Now - _startTime, + FinalState: ProfilingPipelineState.Failed, + StepResults: _steps.ToList(), + ArtifactPaths: Array.Empty(), + MissingArtifacts: Array.Empty()); + } + } + + private async Task ExecutePipelineAsync(IReadOnlyList commands, CancellationToken ct) + { + var remaining = new HashSet(commands.Select(c => c.Id)); + var completed = new HashSet(); + var longRunningTasks = new Dictionary(); + + while (remaining.Count > 0) + { + ct.ThrowIfCancellationRequested(); + + // Find steps whose dependencies are all satisfied. + // Long-running steps (like dotnet-trace) can proceed when their dependency + // is merely started, because they themselves wait for the app to connect. + // Non-long-running steps (like dotnet-gcdump) must wait until their + // long-running dependency signals IsReady (app is actually connected). + var ready = commands + .Where(c => remaining.Contains(c.Id)) + .Where(c => c.DependsOn is null || c.DependsOn.All(dep => + completed.Contains(dep) || IsDependencySatisfied(dep, c.IsLongRunning))) + .ToList(); + + if (ready.Count == 0) + { + if (longRunningTasks.Count > 0) + { + // Wait briefly then re-check — a long-running step may signal + // IsReady at any time via its output, unblocking dependent steps. + var completedTask = await Task.WhenAny( + Task.WhenAny(longRunningTasks.Values), + Task.Delay(500, ct)); + + foreach (var (id, task) in longRunningTasks.ToList()) + { + if (task.IsCompleted) + { + completed.Add(id); + remaining.Remove(id); + longRunningTasks.Remove(id); + } + } + continue; + } + throw new InvalidOperationException( + $"Pipeline deadlock: steps {string.Join(", ", remaining)} have unresolved dependencies."); + } + + var parallelSteps = ready.Where(c => c.CanRunParallel).ToList(); + var sequentialSteps = ready.Where(c => !c.CanRunParallel).ToList(); + + // Launch parallel steps + var parallelTasks = new List(); + foreach (var step in parallelSteps) + { + remaining.Remove(step.Id); + var task = LaunchStepAsync(step, ct); + + if (step.IsLongRunning) + { + longRunningTasks[step.Id] = task; + await Task.Delay(500, ct); + } + else + { + var stepId = step.Id; + parallelTasks.Add(task.ContinueWith(_ => + { + completed.Add(stepId); + }, TaskContinuationOptions.OnlyOnRanToCompletion)); + } + } + + // Run sequential steps one at a time + foreach (var step in sequentialSteps) + { + remaining.Remove(step.Id); + var task = LaunchStepAsync(step, ct); + + if (step.IsLongRunning) + { + longRunningTasks[step.Id] = task; + await Task.Delay(500, ct); + } + else + { + await task; + completed.Add(step.Id); + } + } + + if (parallelTasks.Count > 0) + await Task.WhenAll(parallelTasks); + } + + // If long-running ManualStop steps remain, wait for StopCapture() + var manualStopSteps = longRunningTasks.Keys + .Select(id => commands.First(c => c.Id == id)) + .Where(c => c.StopTrigger == ProfilingStopTrigger.ManualStop) + .ToList(); + + if (manualStopSteps.Count > 0) + { + SetPipelineState(ProfilingPipelineState.WaitingForStop); + await Task.WhenAll(longRunningTasks.Values); + } + else if (longRunningTasks.Count > 0) + { + // No ManualStop steps, but long-running infrastructure steps (dsrouter, build-and-run) + // are still running. Enter WaitingForStop so the user can perform on-demand actions + // (Start Trace, Collect GC Dump) before choosing to stop. + SetPipelineState(ProfilingPipelineState.WaitingForStop); + await _stopTcs.Task; + } + + // Stop any OnPipelineStop steps + foreach (var (id, task) in longRunningTasks.ToList()) + { + var step = commands.First(c => c.Id == id); + if (step.StopTrigger == ProfilingStopTrigger.OnPipelineStop + && _stepProcesses.TryGetValue(id, out var proc)) + { + proc.Cancel(); + await task; + } + } + } + + /// + /// Checks if a dependency is satisfied for a dependent step. + /// Long-running dependents (e.g. dotnet-trace) can proceed when the dependency + /// is just started — they themselves wait for the app. Non-long-running dependents + /// (e.g. dotnet-gcdump) must wait for IsReady, meaning the dependency has + /// established its connection and the app is actually available. + /// + private bool IsDependencySatisfied(string depStepId, bool dependentIsLongRunning) + { + var status = _steps.FirstOrDefault(s => s.StepId == depStepId); + if (status is null) return false; + if (!status.IsLongRunning) return false; + if (status.State != ProfilingStepState.Running) return false; + + // Long-running dependents can proceed as soon as the dep is started + if (dependentIsLongRunning) return true; + + // Non-long-running dependents must wait for the dep to signal readiness + return status.IsReady; + } + + private async Task LaunchStepAsync(ProfilingCommandStep step, CancellationToken ct) + { + var status = _steps.First(s => s.StepId == step.Id); + + if (step.IsOptional && step.RequiredRuntimeBindings?.Count > 0) + { + SetStepState(status, ProfilingStepState.Skipped); + return; + } + + SetStepState(status, ProfilingStepState.Running); + status.StartedAt = DateTime.Now; + + var processService = _serviceProvider.GetRequiredService(); + _stepProcesses[step.Id] = processService; + + // Open a log file for this step's output + var logWriter = OpenStepLogWriter(step.Id, step.DisplayName, step.Command, step.Arguments); + + processService.OutputReceived += (_, e) => + { + var line = new ProfilingStepOutputLine(e.Data, e.IsError, DateTime.Now); + status.OutputLines.Add(line); + WriteToStepLog(logWriter, e.Data, e.IsError); + StepOutputReceived?.Invoke(this, new ProfilingStepOutputEventArgs + { + StepId = step.Id, + Text = e.Data, + IsError = e.IsError + }); + + // Detect readiness for long-running steps by matching output patterns + if (step.IsLongRunning && !status.IsReady && !e.IsError + && step.ReadyOutputPattern is not null + && e.Data.Contains(step.ReadyOutputPattern, StringComparison.OrdinalIgnoreCase)) + { + status.IsReady = true; + _logger.LogDebug($"Step '{step.Id}' is ready (matched: {step.ReadyOutputPattern})"); + } + }; + + try + { + var request = step.ToProcessRequest(); + var result = await processService.ExecuteAsync(request, ct); + + status.ExitCode = result.ExitCode; + status.ProcessId = processService.ProcessId; + status.CompletedAt = DateTime.Now; + status.Duration = status.CompletedAt.Value - status.StartedAt.Value; + + if (result.Success) + { + SetStepState(status, ProfilingStepState.Completed); + } + else if (result.WasCancelled || _stopRequested) + { + // Step was cancelled or stop was requested — treat as stopped, not failed + if (status.State != ProfilingStepState.Stopped) + SetStepState(status, ProfilingStepState.Stopped); + } + else if (step.IsLongRunning && status.State == ProfilingStepState.Stopped) + { + // Long-running step was manually stopped — non-zero exit is expected + } + else if (step.IsOptional) + { + SetStepState(status, ProfilingStepState.Skipped); + status.ErrorMessage = result.Error; + } + else + { + SetStepState(status, ProfilingStepState.Failed); + status.ErrorMessage = result.Error; + throw new InvalidOperationException( + $"Required step '{step.DisplayName}' failed with exit code {result.ExitCode}: {result.Error}"); + } + } + catch (OperationCanceledException) + { + status.CompletedAt = DateTime.Now; + status.Duration = status.CompletedAt.Value - (status.StartedAt ?? DateTime.Now); + SetStepState(status, ProfilingStepState.Cancelled); + throw; + } + catch (Exception ex) when (ex is not InvalidOperationException) + { + status.CompletedAt = DateTime.Now; + status.Duration = status.CompletedAt.Value - (status.StartedAt ?? DateTime.Now); + status.ErrorMessage = ex.Message; + SetStepState(status, ProfilingStepState.Failed); + if (!step.IsOptional) + throw; + } + } + + public async Task StopCaptureAsync() + { + _stopRequested = true; + _logger.LogInformation("StopCapture requested — sending SIGINT to all running processes"); + + // Signal the WaitingForStop await so the pipeline can proceed to Completing + _stopTcs.TrySetResult(); + + // Send SIGINT to all running steps. Cancel() sends SIGINT but does NOT + // immediately cancel the CTS, so WaitForExitAsync in LaunchStepAsync will + // block until the process actually exits and flushes its output files. + foreach (var status in _steps.Where(s => s.State == ProfilingStepState.Running)) + { + SetStepState(status, ProfilingStepState.Stopped); + if (_stepProcesses.TryGetValue(status.StepId, out var proc)) + { + proc.Cancel(); + } + } + + // The pipeline's ExecutePipelineAsync is awaiting Task.WhenAll on long-running + // tasks. Those tasks will complete once processes exit after SIGINT. We just + // need to return and let the pipeline flow continue naturally. + await Task.CompletedTask; + } + + public void Cancel() + { + _logger.LogInformation("Pipeline cancel requested — killing all processes"); + _cts?.Cancel(); + KillAllProcesses(); + } + + private void KillAllProcesses() + { + foreach (var (stepId, proc) in _stepProcesses) + { + try + { + if (proc.CurrentState == ProcessState.Running) + proc.Kill(); + } + catch (Exception ex) + { + _logger.LogWarning($"Failed to kill process for step {stepId}: {ex.Message}"); + } + } + } + + private (IReadOnlyList found, IReadOnlyList missing) CollectArtifacts(ProfilingCapturePlan plan) + { + var found = new List(); + var missing = new List(); + + foreach (var artifact in plan.ExpectedArtifacts) + { + // RelativePath already includes the output directory (e.g., "artifacts/profiling/proj/date-1/trace.nettrace") + // FileName is just the basename (e.g., "trace.nettrace") + // Use RelativePath as-is, fall back to combining FileName with OutputDirectory + string path; + if (!string.IsNullOrWhiteSpace(artifact.RelativePath)) + { + path = Path.IsPathRooted(artifact.RelativePath) + ? artifact.RelativePath + : Path.GetFullPath(artifact.RelativePath); + } + else if (!string.IsNullOrWhiteSpace(artifact.FileName)) + { + path = Path.GetFullPath(Path.Combine(plan.OutputDirectory, artifact.FileName)); + } + else + { + continue; + } + + if (File.Exists(path)) + found.Add(path); + else + missing.Add(path); + } + + // Scan for on-demand artifacts that aren't in the expected list. + // On-demand gcdumps (memory-1.gcdump) and traces (trace-1.nettrace) use numbered + // filenames that don't match the plan's expected artifact names. + if (Directory.Exists(plan.OutputDirectory)) + { + var foundSet = new HashSet(found, StringComparer.OrdinalIgnoreCase); + foreach (var pattern in new[] { "*.gcdump", "*.nettrace", "*.speedscope.json" }) + { + foreach (var file in Directory.GetFiles(plan.OutputDirectory, pattern)) + { + var fullPath = Path.GetFullPath(file); + if (!foundSet.Contains(fullPath)) + { + found.Add(fullPath); + foundSet.Add(fullPath); + } + } + } + } + + return (found, missing); + } + + private void SetPipelineState(ProfilingPipelineState newState) + { + var old = _state; + _state = newState; + _logger.LogInformation($"Pipeline state: {old} → {newState}"); + PipelineStateChanged?.Invoke(this, new ProfilingPipelineStateChangedEventArgs + { + OldState = old, + NewState = newState + }); + } + + private void SetStepState(ProfilingStepStatus status, ProfilingStepState newState) + { + var old = status.State; + status.State = newState; + StepStateChanged?.Invoke(this, new ProfilingStepStateChangedEventArgs + { + StepId = status.StepId, + OldState = old, + NewState = newState + }); + } + + public async Task CollectGcDumpAsync(CancellationToken ct = default) + { + if (_plan is null || _outputDirectory is null || _state != ProfilingPipelineState.WaitingForStop) + { + _logger.LogWarning("Cannot collect GC dump: pipeline is not in WaitingForStop state."); + return null; + } + + var dumpNumber = Interlocked.Increment(ref _gcDumpCount); + var fileName = $"memory-{dumpNumber}.gcdump"; + var outputPath = Path.Combine(_outputDirectory, fileName); + + // Build arguments: reuse the diagnostic connection info from the running plan + var arguments = new List { "collect" }; + var diagnostics = _plan.Diagnostics; + var options = _plan.Options; + + if (diagnostics?.IpcAddress is not null) + { + // Standalone dsrouter mode — connect via IPC + arguments.Add("--diagnostic-port"); + arguments.Add($"{diagnostics.IpcAddress},connect"); + } + else + { + // Desktop mode — use process ID discovered at runtime + var discoveredPid = _steps + .FirstOrDefault(s => s.StepId == "discover-process-id") + ?.OutputLines + .LastOrDefault(l => !l.IsError) + ?.Text; + + // Try to get PID from the build-and-run step's process + var buildStep = _stepProcesses.GetValueOrDefault("build-and-run"); + var pid = options.ProcessId + ?? (discoveredPid is not null && int.TryParse(discoveredPid.Trim(), out var parsed) ? parsed : (int?)null) + ?? buildStep?.ProcessId; + + if (pid is null) + { + _logger.LogWarning("Cannot collect GC dump: no process ID available."); + Interlocked.Decrement(ref _gcDumpCount); + return null; + } + + arguments.Add("--process-id"); + arguments.Add(pid.ToString()!); + } + + arguments.Add("-o"); + arguments.Add(outputPath); + + var stepId = $"gcdump-{dumpNumber}"; + var step = new ProfilingCommandStep( + Id: stepId, + Kind: ProfilingCommandStepKind.CollectArtifacts, + DisplayName: $"GC dump #{dumpNumber}", + Description: $"On-demand heap snapshot #{dumpNumber}.", + Command: "dotnet-gcdump", + Arguments: arguments, + WorkingDirectory: options.WorkingDirectory, + IsLongRunning: false, + RequiresManualStop: false, + Metadata: new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["tool"] = "dotnet-gcdump", + ["output"] = outputPath + }); + + var status = new ProfilingStepStatus + { + StepId = stepId, + DisplayName = step.DisplayName, + Kind = step.Kind, + IsLongRunning = false, + CanRunParallel = false, + StopTrigger = ProfilingStopTrigger.None + }; + _steps.Add(status); + StepStateChanged?.Invoke(this, new ProfilingStepStateChangedEventArgs + { + StepId = stepId, + OldState = ProfilingStepState.Pending, + NewState = ProfilingStepState.Pending + }); + + try + { + await LaunchStepAsync(step, ct); + return File.Exists(outputPath) ? outputPath : null; + } + catch (Exception ex) + { + _logger.LogWarning($"On-demand GC dump failed: {ex.Message}"); + return null; + } + } + + private string? _activeTraceStepId; + private Task? _activeTraceTask; + + /// + /// Whether a trace capture is currently active. + /// + public bool IsTraceActive => _activeTraceStepId is not null + && _steps.Any(s => s.StepId == _activeTraceStepId && s.State == ProfilingStepState.Running); + + /// + /// Start an on-demand trace capture. Returns the step ID or null if it cannot start. + /// The trace runs until StopTraceAsync() is called. + /// + public string? StartTraceAsync() + { + if (_plan is null || _outputDirectory is null || _state != ProfilingPipelineState.WaitingForStop) + { + _logger.LogWarning("Cannot start trace: pipeline is not in WaitingForStop state."); + return null; + } + + if (IsTraceActive) + { + _logger.LogWarning("Cannot start trace: a trace is already running."); + return null; + } + + var traceNumber = Interlocked.Increment(ref _traceCount); + var fileName = traceNumber == 1 ? "trace.nettrace" : $"trace-{traceNumber}.nettrace"; + var outputPath = Path.Combine(_outputDirectory, fileName); + + var arguments = new List { "collect" }; + var diagnostics = _plan.Diagnostics; + var options = _plan.Options; + + if (diagnostics?.IpcAddress is not null) + { + arguments.Add("--diagnostic-port"); + arguments.Add($"{diagnostics.IpcAddress},connect"); + } + else + { + var discoveredPid = _steps + .FirstOrDefault(s => s.StepId == "discover-process-id") + ?.OutputLines + .LastOrDefault(l => !l.IsError) + ?.Text; + + var buildStep = _stepProcesses.GetValueOrDefault("build-and-run"); + var pid = options.ProcessId + ?? (discoveredPid is not null && int.TryParse(discoveredPid.Trim(), out var parsed) ? parsed : (int?)null) + ?? buildStep?.ProcessId; + + if (pid is null) + { + _logger.LogWarning("Cannot start trace: no process ID available."); + Interlocked.Decrement(ref _traceCount); + return null; + } + + arguments.Add("--process-id"); + arguments.Add(pid.ToString()!); + } + + arguments.Add("--output"); + arguments.Add(outputPath); + + // Add profiling profiles based on capture kinds + var profiles = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var kind in _plan.Session.CaptureKinds) + { + switch (kind) + { + case ProfilingCaptureKind.Cpu: + case ProfilingCaptureKind.Startup: + profiles.Add("dotnet-sampled-thread-time"); + break; + case ProfilingCaptureKind.Rendering: + case ProfilingCaptureKind.Network: + case ProfilingCaptureKind.Energy: + case ProfilingCaptureKind.SystemTrace: + profiles.Add("dotnet-common"); + break; + } + } + if (profiles.Count == 0) + profiles.Add("dotnet-sampled-thread-time"); + arguments.Add("--profile"); + arguments.Add(string.Join(",", profiles)); + + // Add JIT/Loader provider flags for managed symbol resolution in speedscope. + // 0x10000018 = JitTracing | NGenTracing | Loader keywords, Verbose level (5). + arguments.Add("--providers"); + arguments.Add("Microsoft-Windows-DotNETRuntime:0x10000018:5"); + + var stepId = $"capture-trace-{traceNumber}"; + _activeTraceStepId = stepId; + var step = new ProfilingCommandStep( + Id: stepId, + Kind: ProfilingCommandStepKind.Capture, + DisplayName: traceNumber == 1 ? "Collect trace" : $"Collect trace #{traceNumber}", + Description: "On-demand trace capture.", + Command: "dotnet-trace", + Arguments: arguments, + WorkingDirectory: options.WorkingDirectory, + IsLongRunning: true, + RequiresManualStop: true, + CanRunParallel: false, + StopTrigger: ProfilingStopTrigger.ManualStop, + ReadyOutputPattern: "Process", + Metadata: new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["tool"] = "dotnet-trace", + ["output"] = outputPath + }); + + var status = new ProfilingStepStatus + { + StepId = stepId, + DisplayName = step.DisplayName, + Kind = step.Kind, + IsLongRunning = true, + CanRunParallel = false, + StopTrigger = ProfilingStopTrigger.ManualStop + }; + _steps.Add(status); + StepStateChanged?.Invoke(this, new ProfilingStepStateChangedEventArgs + { + StepId = stepId, + OldState = ProfilingStepState.Pending, + NewState = ProfilingStepState.Pending + }); + + _activeTraceTask = Task.Run(async () => + { + try + { + await LaunchStepAsync(step, _cts?.Token ?? CancellationToken.None); + } + catch (Exception ex) + { + _logger.LogWarning($"Trace capture failed: {ex.Message}"); + } + }); + + return stepId; + } + + /// + /// Stop the currently running on-demand trace. + /// + public async Task StopTraceAsync() + { + if (_activeTraceStepId is null) + { + _logger.LogWarning("No active trace to stop."); + return; + } + + var stepId = _activeTraceStepId; + _logger.LogInformation("Stopping on-demand trace capture..."); + + var status = _steps.FirstOrDefault(s => s.StepId == stepId); + if (status is not null) + SetStepState(status, ProfilingStepState.Stopped); + + if (_stepProcesses.TryGetValue(stepId, out var proc)) + { + _logger.LogInformation("Sending cancel signal to process"); + proc.Cancel(); + } + + if (_activeTraceTask is not null) + { + try + { + await _activeTraceTask.WaitAsync(TimeSpan.FromSeconds(30)); + } + catch (TimeoutException) + { + _logger.LogWarning("Trace process did not exit within timeout."); + } + } + + _activeTraceStepId = null; + _activeTraceTask = null; + } + + public void Dispose() + { + _cts?.Cancel(); + _cts?.Dispose(); + FlushAllStepLogs(); + KillAllProcesses(); + _stepProcesses.Clear(); + } + + private StreamWriter? OpenStepLogWriter(string stepId, string displayName, string command, IReadOnlyList? arguments) + { + if (string.IsNullOrWhiteSpace(_outputDirectory)) + return null; + + try + { + var logPath = Path.Combine(_outputDirectory, $"{stepId}.log"); + var writer = new StreamWriter(logPath, append: false) { AutoFlush = true }; + writer.WriteLine($"# {displayName}"); + writer.WriteLine($"# Command: {command} {(arguments is not null ? string.Join(" ", arguments) : "")}"); + writer.WriteLine($"# Started: {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); + writer.WriteLine(); + _stepLogWriters[stepId] = writer; + return writer; + } + catch (Exception ex) + { + _logger.LogWarning($"Failed to create log file for step '{stepId}': {ex.Message}"); + return null; + } + } + + private static void WriteToStepLog(StreamWriter? writer, string text, bool isError) + { + if (writer is null) + return; + + try + { + var prefix = isError ? "[ERR] " : ""; + writer.WriteLine($"{prefix}{text}"); + } + catch + { + // Don't let log writing failures break the pipeline + } + } + + private void FlushAllStepLogs() + { + foreach (var (stepId, writer) in _stepLogWriters) + { + try + { + var status = _steps.FirstOrDefault(s => s.StepId == stepId); + if (status is not null) + { + writer.WriteLine(); + writer.WriteLine($"# Finished: {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); + writer.WriteLine($"# State: {status.State}"); + writer.WriteLine($"# Exit code: {status.ExitCode?.ToString() ?? "N/A"}"); + if (status.Duration.HasValue) + writer.WriteLine($"# Duration: {status.Duration.Value.TotalSeconds:F1}s"); + if (!string.IsNullOrWhiteSpace(status.ErrorMessage)) + writer.WriteLine($"# Error: {status.ErrorMessage}"); + } + + writer.Flush(); + writer.Dispose(); + } + catch + { + // Best effort + } + } + + _stepLogWriters.Clear(); + } + + /// + /// Post-processes trace artifacts: converts any .nettrace files to .speedscope.json + /// and adds the converted files to the artifact list. + /// + private async Task> ConvertTraceArtifactsAsync( + IReadOnlyList artifacts, CancellationToken ct) + { + var converter = _serviceProvider.GetService(); + if (converter is null) + { + _logger.LogWarning("No IProfilingArtifactConverterService registered — skipping trace conversion"); + return artifacts; + } + + var result = new List(artifacts); + + foreach (var artifact in artifacts) + { + if (!artifact.EndsWith(".nettrace", StringComparison.OrdinalIgnoreCase)) + continue; + + // dotnet-trace collect --format Speedscope already emits a .speedscope.json + // alongside the .nettrace — skip conversion if it already exists. + var baseName = artifact[..^".nettrace".Length]; + var existingSpeedscope = $"{baseName}.speedscope.json"; + if (File.Exists(existingSpeedscope)) + { + _logger.LogInformation($"Speedscope file already exists from capture: {Path.GetFileName(existingSpeedscope)}"); + if (!result.Contains(existingSpeedscope)) + result.Add(existingSpeedscope); + continue; + } + + try + { + _logger.LogInformation($"Converting {Path.GetFileName(artifact)} to speedscope format..."); + var converted = await converter.ConvertToSpeedscopeAsync(artifact, ct); + if (converted is not null && !result.Contains(converted)) + { + result.Add(converted); + _logger.LogInformation($"Speedscope file added: {Path.GetFileName(converted)}"); + } + } + catch (Exception ex) + { + _logger.LogWarning($"Failed to convert {artifact} to speedscope: {ex.Message}"); + } + } + + return result; + } +} diff --git a/src/MauiSherpa/Services/ProfilingSessionStorageService.cs b/src/MauiSherpa/Services/ProfilingSessionStorageService.cs new file mode 100644 index 00000000..723ca17b --- /dev/null +++ b/src/MauiSherpa/Services/ProfilingSessionStorageService.cs @@ -0,0 +1,242 @@ +using System.IO.Compression; +using System.Text.Json; +using System.Text.Json.Serialization; +using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Models.Profiling; +using MauiSherpa.Core.Services; + +namespace MauiSherpa.Services; + +/// +/// Manages persistent profiling sessions stored under AppDataPath/profiling/. +/// Each session is a folder containing session.json + artifact files. +/// +public class ProfilingSessionStorageService : IProfilingSessionStorageService +{ + private readonly string _profilingRoot; + private readonly ILoggingService _logger; + + private static readonly JsonSerializerOptions JsonOptions = new() + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) } + }; + + private const string ManifestFileName = "session.json"; + + public ProfilingSessionStorageService(ILoggingService logger) + { + _logger = logger; + _profilingRoot = Path.Combine(AppDataPath.GetAppDataDirectory(), "profiling"); + Directory.CreateDirectory(_profilingRoot); + } + + public async Task> GetSessionsAsync(CancellationToken ct = default) + { + var sessions = new List(); + + if (!Directory.Exists(_profilingRoot)) + return sessions; + + foreach (var dir in Directory.GetDirectories(_profilingRoot)) + { + ct.ThrowIfCancellationRequested(); + var manifestPath = Path.Combine(dir, ManifestFileName); + if (!File.Exists(manifestPath)) + continue; + + try + { + var manifest = await ReadManifestAsync(manifestPath, ct); + if (manifest is not null) + { + manifest.DirectoryPath = dir; + sessions.Add(manifest); + } + } + catch (Exception ex) + { + _logger.LogWarning($"Failed to read session manifest at {manifestPath}: {ex.Message}"); + } + } + + // Most recent first + sessions.Sort((a, b) => b.CreatedAt.CompareTo(a.CreatedAt)); + return sessions; + } + + public async Task GetSessionAsync(string sessionId, CancellationToken ct = default) + { + var dir = Path.Combine(_profilingRoot, SanitizePath(sessionId)); + var manifestPath = Path.Combine(dir, ManifestFileName); + + if (!File.Exists(manifestPath)) + return null; + + var manifest = await ReadManifestAsync(manifestPath, ct); + if (manifest is not null) + manifest.DirectoryPath = dir; + return manifest; + } + + public async Task SaveSessionAsync(ProfilingSessionManifest manifest, CancellationToken ct = default) + { + var dir = GetSessionDirectoryPath(manifest.Id); + var manifestPath = Path.Combine(dir, ManifestFileName); + + // Update artifact sizes from disk + foreach (var artifact in manifest.Artifacts) + { + var artifactPath = Path.Combine(dir, artifact.FileName); + if (File.Exists(artifactPath)) + { + var info = new FileInfo(artifactPath); + // Use reflection-free approach: create new record with updated size + if (artifact.SizeBytes is null || artifact.SizeBytes == 0) + { + var idx = manifest.Artifacts.IndexOf(artifact); + if (idx >= 0) + { + manifest.Artifacts[idx] = artifact with { SizeBytes = info.Length }; + } + } + } + } + + manifest.DirectoryPath = dir; + + var json = JsonSerializer.Serialize(manifest, JsonOptions); + await File.WriteAllTextAsync(manifestPath, json, ct); + + _logger.LogInformation($"Session manifest saved: {manifest.Id}"); + } + + public Task DeleteSessionAsync(string sessionId, CancellationToken ct = default) + { + var dir = Path.Combine(_profilingRoot, SanitizePath(sessionId)); + + if (Directory.Exists(dir)) + { + Directory.Delete(dir, recursive: true); + _logger.LogInformation($"Session deleted: {sessionId}"); + } + + return Task.CompletedTask; + } + + public string GetSessionDirectoryPath(string sessionId) + { + var dir = Path.Combine(_profilingRoot, SanitizePath(sessionId)); + Directory.CreateDirectory(dir); + return dir; + } + + public string GenerateSessionId(string? projectName = null) + { + var datePart = DateTime.Now.ToString("yyyy-MM-dd"); + var namePart = SanitizePath(projectName ?? "session"); + var baseName = $"{datePart}_{namePart}"; + + // Find next available run number + var runNumber = 1; + while (Directory.Exists(Path.Combine(_profilingRoot, $"{baseName}_{runNumber}"))) + { + runNumber++; + } + + return $"{baseName}_{runNumber}"; + } + + public async Task ExportSessionAsync(string sessionId, string outputZipPath, CancellationToken ct = default) + { + var dir = Path.Combine(_profilingRoot, SanitizePath(sessionId)); + + if (!Directory.Exists(dir)) + throw new DirectoryNotFoundException($"Session directory not found: {dir}"); + + // Delete existing zip if present (save dialog may have created empty file) + if (File.Exists(outputZipPath)) + File.Delete(outputZipPath); + + await Task.Run(() => ZipFile.CreateFromDirectory(dir, outputZipPath), ct); + _logger.LogInformation($"Session exported: {sessionId} → {outputZipPath}"); + } + + public async Task ImportSessionAsync(string zipPath, CancellationToken ct = default) + { + if (!File.Exists(zipPath)) + return null; + + // Extract to a temp directory first to read manifest + var tempDir = Path.Combine(Path.GetTempPath(), $"sherpa-import-{Guid.NewGuid():N}"); + try + { + await Task.Run(() => ZipFile.ExtractToDirectory(zipPath, tempDir), ct); + + var manifestPath = Path.Combine(tempDir, ManifestFileName); + if (!File.Exists(manifestPath)) + { + _logger.LogWarning($"Imported zip has no {ManifestFileName}"); + return null; + } + + var manifest = await ReadManifestAsync(manifestPath, ct); + if (manifest is null) + return null; + + // Move to managed location (use a new ID if collision) + var targetDir = Path.Combine(_profilingRoot, SanitizePath(manifest.Id)); + if (Directory.Exists(targetDir)) + { + // Generate new ID to avoid collision + var newId = GenerateSessionId(manifest.Name); + targetDir = Path.Combine(_profilingRoot, SanitizePath(newId)); + // We don't change manifest.Id since the record is immutable — just store under new folder + } + + Directory.CreateDirectory(Path.GetDirectoryName(targetDir)!); + + // Move the extracted folder to the managed location + if (Directory.Exists(targetDir)) + Directory.Delete(targetDir, true); + Directory.Move(tempDir, targetDir); + + manifest.DirectoryPath = targetDir; + _logger.LogInformation($"Session imported: {manifest.Id} from {zipPath}"); + return manifest; + } + catch (Exception ex) + { + _logger.LogError($"Failed to import session from {zipPath}: {ex.Message}", ex); + return null; + } + finally + { + // Clean up temp directory if it still exists + if (Directory.Exists(tempDir)) + { + try { Directory.Delete(tempDir, true); } + catch { /* best effort */ } + } + } + } + + private static async Task ReadManifestAsync(string path, CancellationToken ct) + { + await using var stream = File.OpenRead(path); + return await JsonSerializer.DeserializeAsync(stream, JsonOptions, ct); + } + + private static string SanitizePath(string input) + { + var invalid = Path.GetInvalidFileNameChars(); + var sanitized = new char[input.Length]; + for (int i = 0; i < input.Length; i++) + { + sanitized[i] = Array.IndexOf(invalid, input[i]) >= 0 ? '_' : input[i]; + } + return new string(sanitized).Trim('.'); + } +} diff --git a/src/MauiSherpa/Services/ProfilingViewerService.cs b/src/MauiSherpa/Services/ProfilingViewerService.cs new file mode 100644 index 00000000..a10e5ff7 --- /dev/null +++ b/src/MauiSherpa/Services/ProfilingViewerService.cs @@ -0,0 +1,45 @@ +namespace MauiSherpa.Services; + +/// +/// Opens profiling artifact viewers in separate windows. +/// Each viewer type gets its own window; reopening the same type +/// closes the existing window and opens a fresh one. +/// +public class ProfilingViewerService +{ + private readonly Dictionary _windows = new(); + + public void OpenSpeedscope(string filePath) + { + var encodedPath = Uri.EscapeDataString(filePath); + OpenViewer("speedscope", $"/profiling/speedscope?file={encodedPath}", + $"Trace — {Path.GetFileName(filePath)}", 1200, 700); + } + + public void OpenGcDump(string filePath) + { + var encodedPath = Uri.EscapeDataString(filePath); + OpenViewer("gcdump", $"/profiling/gcdump?file={encodedPath}", + $"GC Dump — {Path.GetFileName(filePath)}", 900, 600); + } + + private void OpenViewer(string viewerKey, string route, string title, int width, int height) + { + if (_windows.TryGetValue(viewerKey, out var existing)) + { + Application.Current?.CloseWindow(existing); + _windows.Remove(viewerKey); + } + + var page = new InspectorPage(route, title); + var window = new Window(page) + { + Title = title, + Width = width, + Height = height, + }; + window.Destroying += (_, _) => _windows.Remove(viewerKey); + _windows[viewerKey] = window; + Application.Current?.OpenWindow(window); + } +} diff --git a/src/MauiSherpa/wwwroot/index.html b/src/MauiSherpa/wwwroot/index.html index 88c17c57..90761be9 100644 --- a/src/MauiSherpa/wwwroot/index.html +++ b/src/MauiSherpa/wwwroot/index.html @@ -96,6 +96,7 @@ + + + + + diff --git a/src/MauiSherpa/wwwroot/speedscope/speedscope-GHPHNKXC.css b/src/MauiSherpa/wwwroot/speedscope/speedscope-GHPHNKXC.css new file mode 100644 index 00000000..9bd68f7c --- /dev/null +++ b/src/MauiSherpa/wwwroot/speedscope/speedscope-GHPHNKXC.css @@ -0,0 +1,2 @@ +html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:"";content:none}table{border-collapse:collapse;border-spacing:0}html{overflow:hidden;height:100%}body{height:100%;overflow:auto}@font-face{font-family:Source Code Pro;font-weight:400;font-style:normal;font-stretch:normal;src:url("./SourceCodePro-Regular.ttf-ILST5JV6.woff2") format("woff2")} +/*# sourceMappingURL=speedscope-GHPHNKXC.css.map */ diff --git a/src/MauiSherpa/wwwroot/speedscope/speedscope-Y2522XSH.js b/src/MauiSherpa/wwwroot/speedscope/speedscope-Y2522XSH.js new file mode 100644 index 00000000..2211bbef --- /dev/null +++ b/src/MauiSherpa/wwwroot/speedscope/speedscope-Y2522XSH.js @@ -0,0 +1,189 @@ +"use strict";(()=>{var Tf=Object.create;var ho=Object.defineProperty;var Hf=Object.getOwnPropertyDescriptor;var Mf=Object.getOwnPropertyNames;var Kf=Object.getPrototypeOf,Jf=Object.prototype.hasOwnProperty;var re=(t,e)=>()=>(t&&(e=t(t=0)),e);var k=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports),$l=(t,e)=>{for(var n in e)ho(t,n,{get:e[n],enumerable:!0})},Pf=(t,e,n,a)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of Mf(e))!Jf.call(t,r)&&r!==n&&ho(t,r,{get:()=>e[r],enumerable:!(a=Hf(e,r))||a.enumerable});return t};var he=(t,e,n)=>(n=t!=null?Tf(Kf(t)):{},Pf(e||!t||!t.__esModule?ho(n,"default",{value:t,enumerable:!0}):n,t));function Ae(t){return t[t.length-1]||null}function Fe(t,e){function n(a,r){let A=e(a),o=e(r);return Ao?1:0}t.sort(n)}function De(t,e,n){return t.has(e)||t.set(e,n(e)),t.get(e)}function ja(t,e,n){return t.has(e)?t.get(e):n(e)}function et(t,e){if(!t.has(e))throw new Error(`Expected key ${e}`);return t.get(e)}function*os(t,e){for(let n of t)yield e(n)}function is(t,e){for(let n of t)e(n)}function ln(t,e){return new Array(Math.max(e-t.length,0)+1).join("0")+t}function xt(t){let e=`${t.toFixed(0)}%`;return t===100?e="100%":t>99?e=">99%":t<.01?e="<0.01%":t<1?e=`${t.toFixed(2)}%`:t<10&&(e=`${t.toFixed(1)}%`),e}function Vf(t){return t-Math.floor(t)}function va(t){return 2*Math.abs(Vf(t)-.5)-1}function ls(t,e,n,a,r=1){for(console.assert(!isNaN(r)&&!isNaN(a));;){if(e-t<=r)return[t,e];let A=(e+t)/2;n(A){let a;return e==null?(a=t(n),e={args:n,result:a},a):(Qr(e.args,n)||(e.args=n,e.result=t(n)),e.result)}}function qn(t){let e=null;return n=>{let a;return e==null?(a=t(n),e={args:n,result:a},a):(e.args===n||(e.args=n,e.result=t(n)),e.result)}}function Yf(t){let e=null;return()=>(e==null&&(e={result:t()}),e.result)}function cs(t){let e=Wf();if(t.length%4!==0)throw new Error(`Invalid length for base64 encoded string. Expected length % 4 = 0, got length = ${t.length}`);let n=t.length/4,a;t.length>=4&&t.charAt(t.length-1)==="="?t.charAt(t.length-2)==="="?a=n*3-2:a=n*3-1:a=n*3;let r=new Uint8Array(a),A=0;for(let o=0;o>4,s!=="="&&(r[A++]=(_&15)<<4|f>>2),c!=="="&&(r[A++]=(f&7)<<6|m)}if(A!==a)throw new Error(`Expected to decode ${a} bytes, but only decoded ${A})`);return r}var bt,Wf,V=re(()=>{"use strict";bt=class{constructor(){this.map=new Map}getOrInsert(e){let n=e.key,a=this.map.get(n);return a||(this.map.set(n,e),e)}forEach(e){this.map.forEach(e)}[Symbol.iterator](){return this.map.values()}};Wf=Yf(()=>{let t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",e=new Map;for(let n=0;n{"use strict";Object.defineProperty(Ga,"__esModule",{value:!0});Ga.default=xp;var Bp=Bn(),Qp=wp(Bp);function wp(t){return t&&t.__esModule?t:{default:t}}var bp=["-webkit-","-moz-",""];function xp(t,e){if(typeof e=="string"&&!(0,Qp.default)(e)&&e.indexOf("calc(")>-1)return bp.map(function(n){return e.replace(/calc\(/g,n+"calc(")})}zs.exports=Ga.default});var Ys=k((Ua,Vs)=>{"use strict";Object.defineProperty(Ua,"__esModule",{value:!0});Ua.default=Dp;var kp=Bn(),Sp=Np(kp);function Np(t){return t&&t.__esModule?t:{default:t}}var Fp=["-webkit-",""];function Dp(t,e){if(typeof e=="string"&&!(0,Sp.default)(e)&&e.indexOf("cross-fade(")>-1)return Fp.map(function(n){return e.replace(/cross-fade\(/g,n+"cross-fade(")})}Vs.exports=Ua.default});var Zs=k((Oa,Ws)=>{"use strict";Object.defineProperty(Oa,"__esModule",{value:!0});Oa.default=Tp;var Rp=["-webkit-","-moz-",""],Lp={"zoom-in":!0,"zoom-out":!0,grab:!0,grabbing:!0};function Tp(t,e){if(t==="cursor"&&Lp.hasOwnProperty(e))return Rp.map(function(n){return n+e})}Ws.exports=Oa.default});var ec=k((qa,Xs)=>{"use strict";Object.defineProperty(qa,"__esModule",{value:!0});qa.default=Pp;var Hp=Bn(),Mp=Kp(Hp);function Kp(t){return t&&t.__esModule?t:{default:t}}var Jp=["-webkit-",""];function Pp(t,e){if(typeof e=="string"&&!(0,Mp.default)(e)&&e.indexOf("filter(")>-1)return Jp.map(function(n){return e.replace(/filter\(/g,n+"filter(")})}Xs.exports=qa.default});var rc=k((za,nc)=>{"use strict";Object.defineProperty(za,"__esModule",{value:!0});za.default=Gp;var tc={flex:["-webkit-box","-moz-box","-ms-flexbox","-webkit-flex","flex"],"inline-flex":["-webkit-inline-box","-moz-inline-box","-ms-inline-flexbox","-webkit-inline-flex","inline-flex"]};function Gp(t,e){if(t==="display"&&tc.hasOwnProperty(e))return tc[e]}nc.exports=za.default});var oc=k(($a,Ac)=>{"use strict";Object.defineProperty($a,"__esModule",{value:!0});$a.default=Op;var Up={"space-around":"distribute","space-between":"justify","flex-start":"start","flex-end":"end"},ac={alignContent:"msFlexLinePack",alignSelf:"msFlexItemAlign",alignItems:"msFlexAlign",justifyContent:"msFlexPack",order:"msFlexOrder",flexGrow:"msFlexPositive",flexShrink:"msFlexNegative",flexBasis:"msFlexPreferredSize"};function Op(t,e,n){ac.hasOwnProperty(t)&&(n[ac[t]]=Up[e]||e)}Ac.exports=$a.default});var sc=k((Va,lc)=>{"use strict";Object.defineProperty(Va,"__esModule",{value:!0});Va.default=zp;var qp={"space-around":"justify","space-between":"justify","flex-start":"start","flex-end":"end","wrap-reverse":"multiple",wrap:"multiple"},ic={alignItems:"WebkitBoxAlign",justifyContent:"WebkitBoxPack",flexWrap:"WebkitBoxLines"};function zp(t,e,n){t==="flexDirection"&&typeof e=="string"&&(e.indexOf("column")>-1?n.WebkitBoxOrient="vertical":n.WebkitBoxOrient="horizontal",e.indexOf("reverse")>-1?n.WebkitBoxDirection="reverse":n.WebkitBoxDirection="normal"),ic.hasOwnProperty(t)&&(n[ic[t]]=qp[e]||e)}lc.exports=Va.default});var _c=k((Ya,cc)=>{"use strict";Object.defineProperty(Ya,"__esModule",{value:!0});Ya.default=Xp;var $p=Bn(),Vp=Yp($p);function Yp(t){return t&&t.__esModule?t:{default:t}}var Wp=["-webkit-","-moz-",""],Zp=/linear-gradient|radial-gradient|repeating-linear-gradient|repeating-radial-gradient/;function Xp(t,e){if(typeof e=="string"&&!(0,Vp.default)(e)&&Zp.test(e))return Wp.map(function(n){return n+e})}cc.exports=Ya.default});var gc=k((Wa,hc)=>{"use strict";Object.defineProperty(Wa,"__esModule",{value:!0});Wa.default=aC;var eC=Bn(),tC=nC(eC);function nC(t){return t&&t.__esModule?t:{default:t}}var rC=["-webkit-",""];function aC(t,e){if(typeof e=="string"&&!(0,tC.default)(e)&&e.indexOf("image-set(")>-1)return rC.map(function(n){return e.replace(/image-set\(/g,n+"image-set(")})}hc.exports=Wa.default});var uc=k((Za,dc)=>{"use strict";Object.defineProperty(Za,"__esModule",{value:!0});Za.default=AC;function AC(t,e){if(t==="position"&&e==="sticky")return["-webkit-sticky","sticky"]}dc.exports=Za.default});var pc=k((Xa,fc)=>{"use strict";Object.defineProperty(Xa,"__esModule",{value:!0});Xa.default=sC;var oC=["-webkit-","-moz-",""],iC={maxHeight:!0,maxWidth:!0,width:!0,height:!0,columnWidth:!0,minWidth:!0,minHeight:!0},lC={"min-content":!0,"max-content":!0,"fill-available":!0,"fit-content":!0,"contain-floats":!0};function sC(t,e){if(iC.hasOwnProperty(t)&&lC.hasOwnProperty(e))return oC.map(function(n){return n+e})}fc.exports=Xa.default});var mc=k((qB,Cc)=>{"use strict";var cC=/[A-Z]/g,_C=/^ms-/,To={};function hC(t){return t in To?To[t]:To[t]=t.replace(cC,"-$&").toLowerCase().replace(_C,"-ms-")}Cc.exports=hC});var jc=k((eA,Ic)=>{"use strict";Object.defineProperty(eA,"__esModule",{value:!0});eA.default=fC;var gC=mc(),dC=uC(gC);function uC(t){return t&&t.__esModule?t:{default:t}}function fC(t){return(0,dC.default)(t)}Ic.exports=eA.default});var Ho=k((tA,vc)=>{"use strict";Object.defineProperty(tA,"__esModule",{value:!0});tA.default=pC;function pC(t){return t.charAt(0).toUpperCase()+t.slice(1)}vc.exports=tA.default});var Bc=k((nA,Ec)=>{"use strict";Object.defineProperty(nA,"__esModule",{value:!0});nA.default=QC;var CC=jc(),mC=Mo(CC),IC=Bn(),jC=Mo(IC),vC=Ho(),yc=Mo(vC);function Mo(t){return t&&t.__esModule?t:{default:t}}var yC={transition:!0,transitionProperty:!0,WebkitTransition:!0,WebkitTransitionProperty:!0,MozTransition:!0,MozTransitionProperty:!0},EC={Webkit:"-webkit-",Moz:"-moz-",ms:"-ms-"};function BC(t,e){if((0,jC.default)(t))return t;for(var n=t.split(/,(?![^()]*(?:\([^()]*\))?\))/g),a=0,r=n.length;a-1&&l!=="order")for(var s=e[i],c=0,h=s.length;c-1)return A;var o=r.split(/,(?![^()]*(?:\([^()]*\))?\))/g).filter(function(i){return!/-webkit-|-ms-/.test(i)}).join(",");return t.indexOf("Moz")>-1?o:(n["Webkit"+(0,yc.default)(t)]=A,n["Moz"+(0,yc.default)(t)]=o,r)}}Ec.exports=nA.default});var wc=k((zB,Qc)=>{"use strict";function wC(t){for(var e=5381,n=t.length;n;)e=e*33^t.charCodeAt(--n);return e>>>0}Qc.exports=wC});var xc=k((rA,bc)=>{"use strict";Object.defineProperty(rA,"__esModule",{value:!0});rA.default=SC;var bC=Ho(),xC=kC(bC);function kC(t){return t&&t.__esModule?t:{default:t}}function SC(t,e,n){if(t.hasOwnProperty(e)){for(var a={},r=t[e],A=(0,xC.default)(e),o=Object.keys(n),i=0;i{"use strict";Object.defineProperty(aA,"__esModule",{value:!0});aA.default=NC;function NC(t,e,n,a,r){for(var A=0,o=t.length;A{"use strict";Object.defineProperty(AA,"__esModule",{value:!0});AA.default=FC;function Nc(t,e){t.indexOf(e)===-1&&t.push(e)}function FC(t,e){if(Array.isArray(e))for(var n=0,a=e.length;n{"use strict";Object.defineProperty(oA,"__esModule",{value:!0});oA.default=DC;function DC(t){return t instanceof Object&&!Array.isArray(t)}Rc.exports=oA.default});var Mc=k((lA,Hc)=>{"use strict";Object.defineProperty(lA,"__esModule",{value:!0});lA.default=PC;var RC=xc(),LC=iA(RC),TC=Sc(),Tc=iA(TC),HC=Dc(),MC=iA(HC),KC=Lc(),JC=iA(KC);function iA(t){return t&&t.__esModule?t:{default:t}}function PC(t){var e=t.prefixMap,n=t.plugins;function a(r){for(var A in r){var o=r[A];if((0,JC.default)(o))r[A]=a(o);else if(Array.isArray(o)){for(var i=[],l=0,s=o.length;l0&&(r[A]=i)}else{var h=(0,Tc.default)(n,A,o,r,e);h&&(r[A]=h),r=(0,LC.default)(e,A,r)}}return r}return a}Hc.exports=lA.default});var qc=k(($B,Oc)=>{"use strict";Oc.exports=Ko;function Ko(t){Nt.length||(sA(),Pc=!0),Nt[Nt.length]=t}var Nt=[],Pc=!1,sA,St=0,GC=1024;function Kc(){for(;StGC){for(var e=0,n=Nt.length-St;e{"use strict";var zc=qc(),cA=[],Jo=[],OC=zc.makeRequestCallFromTimer(qC);function qC(){if(Jo.length)throw Jo.shift()}Vc.exports=Po;function Po(t){var e;cA.length?e=cA.pop():e=new $c,e.task=t,zc(e)}function $c(){this.task=null}$c.prototype.call=function(){try{this.task.call()}catch(t){Po.onerror?Po.onerror(t):(Jo.push(t),OC())}finally{this.task=null,cA[cA.length]=this}}});var Vt,ee,$e,Ke=re(()=>{"use strict";V();Vt=class{constructor(){this.unit="none"}format(e){return e.toLocaleString()}},ee=class{constructor(e){this.unit=e;e==="nanoseconds"?this.multiplier=1e-9:e==="microseconds"?this.multiplier=1e-6:e==="milliseconds"?this.multiplier=.001:this.multiplier=1}formatUnsigned(e){let n=e*this.multiplier;if(n/60>=1){let a=Math.floor(n/60),r=Math.floor(n-a*60).toString();return`${a}:${ln(r,2)}`}return n/1>=1?`${n.toFixed(2)}s`:n/.001>=1?`${(n/.001).toFixed(2)}ms`:n/1e-6>=1?`${(n/1e-6).toFixed(2)}\xB5s`:`${(n/1e-9).toFixed(2)}ns`}format(e){return`${e<0?"-":""}${this.formatUnsigned(Math.abs(e))}`}},$e=class{constructor(){this.unit="bytes"}format(e){return e<1024?`${e.toFixed(0)} B`:(e/=1024,e<1024?`${e.toFixed(2)} KB`:(e/=1024,e<1024?`${e.toFixed(2)} MB`:(e/=1024,`${e.toFixed(2)} GB`)))}}});var Q_=k((yQ,zo)=>{"use strict";var B_=async function(t={}){var e,n=t,a,r,A=new Promise((M,K)=>{a=M,r=K});function o(){a(n)}function i(M){throw M}for(var l,s,c,h,_,f,m,u,g,p,I,j=new Uint8Array(123),E=25;E>=0;--E)j[48+E]=52+E,j[65+E]=E,j[97+E]=26+E;j[43]=62,j[47]=63;function y(M){for(var K,G,we=0,Xe=0,Pe=M.length,_e=new Uint8Array((Pe*3>>2)-(M[Pe-2]=="=")-(M[Pe-1]=="="));we>4,_e[Xe+1]=K<<4|G>>2,_e[Xe+2]=G<<6|j[M.charCodeAt(we+3)];return _e}function b(){var M=I.buffer;l=new Int8Array(M),s=new Int16Array(M),h=new Uint8Array(M),_=new Uint16Array(M),c=new Int32Array(M),f=new Uint32Array(M),m=new Float32Array(M),u=new Float64Array(M),g=new BigInt64Array(M),p=new BigUint64Array(M)}var F=n.noExitRuntime||!0,T=M=>{i("OOM")},Q=M=>{var K=h.length;M>>>=0,T(M)},w=M=>{for(var K=0,G=0;G=55296&&we<=57343?(K+=4,++G):K+=3}return K},S=(M,K,G,we)=>{if(!(we>0))return 0;for(var Xe=G,Pe=G+we-1,_e=0;_e=55296&&ue<=57343){var Ca=M.charCodeAt(++_e);ue=65536+((ue&1023)<<10)|Ca&1023}if(ue<=127){if(G>=Pe)break;K[G++]=ue}else if(ue<=2047){if(G+1>=Pe)break;K[G++]=192|ue>>6,K[G++]=128|ue&63}else if(ue<=65535){if(G+2>=Pe)break;K[G++]=224|ue>>12,K[G++]=128|ue>>6&63,K[G++]=128|ue&63}else{if(G+3>=Pe)break;K[G++]=240|ue>>18,K[G++]=128|ue>>12&63,K[G++]=128|ue>>6&63,K[G++]=128|ue&63}}return K[G]=0,G-Xe},te=(M,K,G)=>S(M,h,K,G),at=M=>ql(M),At=M=>{var K=w(M)+1,G=at(K);return te(M,G,K),G},Ct=typeof TextDecoder<"u"?new TextDecoder:void 0,on=(M,K=0,G=NaN)=>{for(var we=K+G,Xe=K;M[Xe]&&!(Xe>=we);)++Xe;if(Xe-K>16&&M.buffer&&Ct)return Ct.decode(M.subarray(K,Xe));for(var Pe="";K>10,56320|zl&1023)}}return Pe},Qt=(M,K)=>M?on(h,M,K):"",Pt={a:Q};n.UTF8ToString=Qt,n.stringToUTF8OnStack=At;function pa(M){M.c()}var vr,Ol,ql,Lf={a:Pt};return n.wasm=y("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Amo2AgwgACABEEciA0UNAyAAKAIMIgItAABBxQBHDQMgAkEBaiEBQdwACyECIAAgATYCDAsgACACIANBABAbIQMLIAMPCyABQQE2AgBBAAt6AQJ/An9BASAAKAIMIgEtAABB3wBHDQAaIAAgAUEBajYCDCABLQABIgJB3wBGBEAgACABQQJqNgIMC0EAIAAQDiIBQQBIDQAaQQEgAkHfAEcgAUEKSXINABpBACAAKAIMIgEtAABB3wBHDQAaIAAgAUEBajYCDEEBCwuWAgEDfwJAIAFFDQADQCABKAIIIgJBAUoNASAAKAKcAiIEQYAISg0BIAEgAkEBajYCCEHEAiECQRAhAwJAAkACQAJAAkAgASgCAEEBaw5eAgICAQYGBAQCAgICAgICAgICAgICAgIGAgICAgICAgICAgIAAAICBgICAgIGAgICAgIGBAICAgICAgICAgICAgICBgYCAwMDAwYCAgICAgIDAgIGBgYDBgYGBgYGAgYLIAEoAgwoAgBBBUcNAUG4AiECCyAAIAJqIgIgAigCAEEBajYCAAsgACAEQQFqNgKcAiAAIAEoAgwQLiAAIAEoAhAQLiAAIAAoApwCQQFrNgKcAgwDC0EMIQMLIAEgA2ooAgAiAQ0ACwsLqwIBBX8jAEEQayIDJAAgA0EANgIMIANBDGohAgJAAkADQAJAAkAgACgCDCIELQAAIgFBzgBNBEAgAUUgAUEuRnINAiABQcUARw0BDAILAkAgAUHPAGsOBAABAgABCyAELQABQcUARg0BCyAAEAQiBUUNAiAAKAIUIgQgACgCGE4EQEEAIQEgAkEANgIADAQFIAAoAhAgBEEUbGoiAUIANwIEIAAgBEEBajYCFCABQQA2AhAgASAFNgIMIAFBLzYCACACIAE2AgAgAUEQaiECDAILAAsLIAMoAgwiAUUNACABKAIQDQEgASgCDCICKAIAQShHDQEgAigCDCICKAIQQQlHDQEgACAAKAIsIAIoAgRrNgIsIAFBADYCDAwBC0EAIQELIANBEGokACABC2cBA38CQCAAKAIMIgItAABB1ABHDQAgACACQQFqNgIMIAAQHyIDQQBIDQAgACgCFCICIAAoAhhODQAgACgCECACQRRsaiIBQgA3AgQgACACQQFqNgIUIAEgAzYCDCABQQU2AgALIAELbgEBfwJAAkACQCAA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NwMoIABCARAmIAFCAXwiASACUg0ACyAAKAIUDQAgACgCGA0AQfgbQQIgACgCCCAAKAIMEQAACwslAQF/IABB4QBrQf8BcSIAQRlNBH8gAEECdEGkHGooAgAFIAELC8oBAQN/AkAgACgCDA0AIAAoAggiBCAAKAIEayIDIAJJBEAgAiADayIDIARqIgUgA0kEQCAAQQE2AgwPCyAEQQQgBBshAwJAA0AgAyAFTw0BIANBAXQiAyAETw0ACyAAQQE2AgwPCyAAKAIAIAMQJyIERQRAIAAoAgAQFSAAQoCAgIAQNwIIIABCADcCAA8LIAAgAzYCCCAAIAQ2AgAgACgCDA0BCyACBEAgACgCACAAKAIEaiABIAL8CgAACyAAIAAoAgQgAmo2AgQLCxAAIwAgAGtBcHEiACQAIAALpgEBBX8gACgCVCIDKAIAIQUgAygCBCIEIAAoAhQgACgCHCIHayIGIAQgBkkbIgYEQCAFIAcgBhAhIAMgAygCACAGaiIFNgIAIAMgAygCBCAGayIENgIECyAEIAIgAiAESxsiBARAIAUgASAEECEgAyADKAIAIARqIgU2AgAgAyADKAIEIARrNgIECyAFQQA6AAAgACAAKAIsIgE2AhwgACABNgIUIAILnAUCBn4EfyABIAEoAgBBB2pBeHEiAUEQajYCACAAIAEpAwAhAyABKQMIIQcjAEEgayIAJAAgB0L///////8/gyEFAn4gB0IwiEL//wGDIgSnIglBgfgAa0H9D00EQCAFQgSGIANCPIiEIQIgCUGA+ABrrSEEAkAgA0L//////////w+DIgNCgYCAgICAgIAIWgRAIAJCAXwhAgwBCyADQoCAgICAgICACFINACACQgGDIAJ8IQILQgAgAiACQv////////8HViIBGyECIAGtIAR8DAELIAMgBYRQIARC//8BUnJFBEAgBUIEhiADQjyIhEKAgICAgICABIQhAkL/DwwBCyAJQf6HAUsEQEL/DwwBC0GA+ABBgfgAIARQIggbIgogCWsiAUHwAEoEQEIADAELIAMhAiAFIAVCgICAgICAwACEIAgbIgQhBgJAQYABIAFrIghBwABxBEAgAiAIQUBqrYYhBkIAIQIMAQsgCEUNACAGIAitIgWGIAJBwAAgCGutiIQhBiACIAWGIQILIAAgAjcDECAAIAY3AxgCQCAB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ADAAMAAUARQBFAEUARQBFAEUARQBFAEUATAAMAAwADAAMAAwADAAkAOQA5ADkAOQA5ADkAKQApACkAKQApACkAKQApACkAKQApACkAKQApACkAKQApACkAKQAjAAMAAwADAAMAIwABgDGAMYAxgDGAMYAxgCGAIYAhgCGAIYAhgCGAIYAhgCGAIYAhgCGAIYAhgCGAIYAhgCGAIwADAAMAAwAAIAQYAMC+IQfgB9AHx8AFx1ewA6OnsAc3oAYXoAIF9JbWFnaW5hcnkASkFycmF5AG54ACVsbHgAaXgAIF9Db21wbGV4AGR4AC0rICAgMFgweAAtMFgrMFggMFgtMHgrMHggMHgAdHcAIHRocm93AG53AG5ldwBhdwBkdgBxdQAlbGx1ACBjb25zdABjb25zdF9jYXN0AHJlaW50ZXJwcmV0X2Nhc3QAc3RhdGljX2Nhc3QAZHluYW1pY19jYXN0AHVuc2lnbmVkIHNob3J0ACBub2V4Y2VwdAB1bnNpZ25lZCBpbnQAbHQAZ3QAZHQAIHJlc3RyaWN0AGZsb2F0AF9GbG9hdAB3Y2hhcl90AGNoYXI4X3QAc3RkOjpiZmxvYXQxNl90AGNoYXIxNl90AGNoYXIzMl90AFx0ACBjbGFzcwBycwBwcwBscwB0aGlzAGdzAGRzAHN0cgBvcGVyYXRvcgBzdGQ6OmFsbG9jYXRvcgBmcgB1bnNpZ25lZCBjaGFyAFxyAGVxAHBwAGF1dG8Ab28AZW8AY28AbmFuAGJvb2xlYW4AXG4Acm0AcG0AbW0Ac2hpbQBjbQBiYXNpY19pb3N0cmVhbQBzdGQ6Omlvc3RyZWFtAGJhc2ljX29zdHJlYW0Ac3RkOjpvc3RyZWFtAGJhc2ljX2lzdHJlYW0Ac3RkOjppc3RyZWFtAHVsAHBsAGJvb2wAbWwAdWxsAGZsAGRsAGNsAHN0cmluZyBsaXRlcmFsAG1pAGxpAGRpADE3aAB1bnNpZ25lZCBsb25nIGxvbmcAdW5zaWduZWQgbG9uZwBzdGQ6OmJhc2ljX3N0cmluZwBzdGQ6OnN0cmluZwBpbmYAaGFsZgB1c2l6ZQBpc2l6ZQB0cnVlAGJ5dGUAdGVtcGxhdGUAZmFsc2UAY2xvc3VyZQBuZQB0eXBlbmFtZQAgdm9sYXRpbGUAbG9uZyBkb3VibGUAZ2UAIHRy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"),WebAssembly.instantiate(n.wasm,Lf).then(M=>{var K=M.instance.exports;vr=K.e,Ol=K.f,ql=K.g,I=K.b,b(),pa(K),o()}),n.wasm_demangle=function(M){let K=At(M),G=Ol(K),we=Qt(G);return G!=null&&vr(G),we},e=A,e};zo.exports=B_;zo.exports.default=B_});async function x_(){let t=await xm;return km(t.wasm_demangle)}function km(t){return e=>{let n=w_.get(e);return n!==void 0||(n=t(e),n=n===""?e:n,w_.set(e,n)),e=n,e}}var b_,xm,w_,k_=re(()=>{"use strict";b_=he(Q_()),xm=(0,b_.default)().then(t=>t),w_=new Map});var $o={};$l($o,{loadDemangling:()=>x_});var Vo=re(()=>{"use strict";k_()});var fA,me,Zn,pA,ae,Ie,ke=re(()=>{"use strict";V();Ke();fA=class{constructor(){this.selfWeight=0;this.totalWeight=0}getSelfWeight(){return this.selfWeight}getTotalWeight(){return this.totalWeight}addToTotalWeight(e){this.totalWeight+=e}addToSelfWeight(e){this.selfWeight+=e}overwriteWeightWith(e){this.selfWeight=e.selfWeight,this.totalWeight=e.totalWeight}},me=class t extends fA{constructor(e){super(),this.key=e.key,this.name=e.name,this.file=e.file,this.line=e.line,this.col=e.col}static{this.root=new t({key:"(speedscope root)",name:"(speedscope root)"})}static getOrInsert(e,n){return e.getOrInsert(new t(n))}},Zn=class extends fA{constructor(n,a){super();this.frame=n;this.parent=a;this.children=[];this.frozen=!1}isRoot(){return this.frame===me.root}isFrozen(){return this.frozen}freeze(){this.frozen=!0}},pA=class t{constructor(e=0){this.name="";this.frames=new bt;this.appendOrderCalltreeRoot=new Zn(me.root,null);this.groupedCalltreeRoot=new Zn(me.root,null);this.samples=[];this.weights=[];this.valueFormatter=new Vt;this.totalNonIdleWeight=null;this.totalWeight=e}getAppendOrderCalltreeRoot(){return this.appendOrderCalltreeRoot}getGroupedCalltreeRoot(){return this.groupedCalltreeRoot}shallowClone(){let e=new t(this.totalWeight);return Object.assign(e,this),e}formatValue(e){return this.valueFormatter.format(e)}setValueFormatter(e){this.valueFormatter=e}getWeightUnit(){return this.valueFormatter.unit}getName(){return this.name}setName(e){this.name=e}getTotalWeight(){return this.totalWeight}getTotalNonIdleWeight(){return this.totalNonIdleWeight===null&&(this.totalNonIdleWeight=this.groupedCalltreeRoot.children.reduce((e,n)=>e+n.getTotalWeight(),0)),this.totalNonIdleWeight}sortGroupedCallTree(){function e(n){n.children.sort((a,r)=>-(a.getTotalWeight()-r.getTotalWeight())),n.children.forEach(e)}e(this.groupedCalltreeRoot)}forEachCallGrouped(e,n){function a(r,A){r.frame!==me.root&&e(r,A);let o=0;r.children.forEach(function(i){a(i,A+o),o+=i.getTotalWeight()}),r.frame!==me.root&&n(r,A+r.getTotalWeight())}a(this.groupedCalltreeRoot,0)}forEachCall(e,n){let a=[],r=0,A=0;for(let o of this.samples){let i=null;for(i=o;i&&i.frame!=me.root&&a.indexOf(i)===-1;i=i.parent);for(;a.length>0&&Ae(a)!=i;){let s=a.pop();n(s,r)}let l=[];for(let s=o;s&&s.frame!=me.root&&s!=i;s=s.parent)l.push(s);l.reverse();for(let s of l)e(s,r);a=a.concat(l),r+=this.weights[A++]}for(let o=a.length-1;o>=0;o--)n(a[o],r)}forEachFrame(e){this.frames.forEach(e)}getProfileWithRecursionFlattened(){let e=new Ie,n=[],a=new Set;function r(i,l){a.has(i.frame)?n.push(null):(a.add(i.frame),n.push(i),e.enterFrame(i.frame,l))}function A(i,l){let s=n.pop();s&&(a.delete(s.frame),e.leaveFrame(s.frame,l))}this.forEachCall(r,A);let o=e.build();return o.name=this.name,o.valueFormatter=this.valueFormatter,this.forEachFrame(i=>{o.frames.getOrInsert(i).overwriteWeightWith(i)}),o}getInvertedProfileForCallersOf(e){let n=me.getOrInsert(this.frames,e),a=new ae,r=[];function A(i){if(i.frame===n)r.push(i);else for(let l of i.children)A(l)}A(this.appendOrderCalltreeRoot);for(let i of r){let l=[];for(let s=i;s!=null&&s.frame!==me.root;s=s.parent)l.push(s.frame);a.appendSampleWithWeight(l,i.getTotalWeight())}let o=a.build();return o.name=this.name,o.valueFormatter=this.valueFormatter,o}getProfileForCalleesOf(e){let n=me.getOrInsert(this.frames,e),a=new ae;function r(i){let l=[];function s(c){l.push(c.frame),a.appendSampleWithWeight(l,c.getSelfWeight());for(let h of c.children)s(h);l.pop()}s(i)}function A(i){if(i.frame===n)r(i);else for(let l of i.children)A(l)}A(this.appendOrderCalltreeRoot);let o=a.build();return o.name=this.name,o.valueFormatter=this.valueFormatter,o}async demangle(){let e=null;for(let n of this.frames)(n.name.startsWith("__Z")||n.name.startsWith("_R")||n.name.startsWith("_Z"))&&(e||(e=await(await Promise.resolve().then(()=>(Vo(),$o))).loadDemangling()),n.name=e(n.name))}remapSymbols(e){for(let n of this.frames){let a=e(n);if(a==null)continue;let{name:r,file:A,line:o,col:i}=a;r!=null&&(n.name=r),A!=null&&(n.file=A),o!=null&&(n.line=o),i!=null&&(n.col=i)}}},ae=class extends pA{constructor(){super(...arguments);this.pendingSample=null}_appendSample(n,a,r){if(isNaN(a))throw new Error("invalid weight");let A=r?this.appendOrderCalltreeRoot:this.groupedCalltreeRoot,o=new Set;for(let i of n){let l=r?Ae(A.children):A.children.find(s=>s.frame===i);if(l&&!l.isFrozen()&&l.frame==i)A=l;else{let s=A;A=new Zn(i,A),s.children.push(A)}A.addToTotalWeight(a),o.add(A.frame)}if(A.addToSelfWeight(a),r)for(let i of A.children)i.freeze();if(r){A.frame.addToSelfWeight(a);for(let i of o)i.addToTotalWeight(a);A===Ae(this.samples)?this.weights[this.weights.length-1]+=a:(this.samples.push(A),this.weights.push(a))}}appendSampleWithWeight(n,a){if(a===0)return;if(a<0)throw new Error("Samples must have positive weights");let r=n.map(A=>me.getOrInsert(this.frames,A));this._appendSample(r,a,!0),this._appendSample(r,a,!1)}appendSampleWithTimestamp(n,a){if(this.pendingSample){if(a0?this.appendSampleWithWeight(this.pendingSample.stack,this.pendingSample.centralTimestamp-this.pendingSample.startTimestamp):(this.appendSampleWithWeight(this.pendingSample.stack,1),this.setValueFormatter(new Vt))),this.totalWeight=Math.max(this.totalWeight,this.weights.reduce((n,a)=>n+a,0)),this.sortGroupedCallTree(),this}},Ie=class extends pA{constructor(){super(...arguments);this.appendOrderStack=[this.appendOrderCalltreeRoot];this.groupedOrderStack=[this.groupedCalltreeRoot];this.framesInStack=new Map;this.stack=[];this.lastValue=0}addWeightsToFrames(n){let a=n-this.lastValue;for(let A of this.framesInStack.keys())A.addToTotalWeight(a);let r=Ae(this.stack);r&&r.addToSelfWeight(a)}addWeightsToNodes(n,a){let r=n-this.lastValue;for(let o of a)o.addToTotalWeight(r);let A=Ae(a);A&&A.addToSelfWeight(r)}_enterFrame(n,a,r){let A=r?this.appendOrderStack:this.groupedOrderStack;this.addWeightsToNodes(a,A);let o=Ae(A);if(o){if(r){let s=a-this.lastValue;if(s>0)this.samples.push(o),this.weights.push(a-this.lastValue);else if(s<0)throw new Error(`Samples must be provided in increasing order of cumulative value. Last sample was ${this.lastValue}, this sample was ${a}`)}let i=r?Ae(o.children):o.children.find(s=>s.frame===n),l;i&&!i.isFrozen()&&i.frame==n?l=i:(l=new Zn(n,o),o.children.push(l)),A.push(l)}}enterFrame(n,a){let r=me.getOrInsert(this.frames,n);this.addWeightsToFrames(a),this._enterFrame(r,a,!0),this._enterFrame(r,a,!1),this.stack.push(r);let A=this.framesInStack.get(r)||0;this.framesInStack.set(r,A+1),this.lastValue=a,this.totalWeight=Math.max(this.totalWeight,this.lastValue)}_leaveFrame(n,a,r){let A=r?this.appendOrderStack:this.groupedOrderStack;if(this.addWeightsToNodes(a,A),r){let o=this.appendOrderStack.pop();if(o==null)throw new Error(`Trying to leave ${n.key} when stack is empty`);if(this.lastValue==null)throw new Error(`Trying to leave a ${n.key} before any have been entered`);if(o.freeze(),o.frame.key!==n.key)throw new Error(`Tried to leave frame "${n.name}" while frame "${o.frame.name}" was at the top at ${a}`);let i=a-this.lastValue;if(i>0)this.samples.push(o),this.weights.push(a-this.lastValue);else if(i<0)throw new Error(`Samples must be provided in increasing order of cumulative value. Last sample was ${this.lastValue}, this sample was ${a}`)}else this.groupedOrderStack.pop()}leaveFrame(n,a){let r=me.getOrInsert(this.frames,n);this.addWeightsToFrames(a),this._leaveFrame(r,a,!0),this._leaveFrame(r,a,!1),this.stack.pop();let A=this.framesInStack.get(r);A!=null&&(A===1?this.framesInStack.delete(r):this.framesInStack.set(r,A-1),this.lastValue=a,this.totalWeight=Math.max(this.totalWeight,this.lastValue))}build(){if(this.appendOrderStack.length>1||this.groupedOrderStack.length>1)throw new Error("Tried to complete profile construction with a non-empty stack");return this.sortGroupedCallTree(),this}}});var Yt,S_=re(()=>{"use strict";(n=>{let t;(A=>(A.EVENTED="evented",A.SAMPLED="sampled"))(t=n.ProfileType||={});let e;(A=>(A.OPEN_FRAME="O",A.CLOSE_FRAME="C"))(e=n.EventType||={})})(Yt||={})});var Yo=k((SQ,Sm)=>{Sm.exports={name:"speedscope",version:"1.25.0",description:"",repository:"jlfwong/speedscope",main:"index.js",bin:{speedscope:"./bin/cli.mjs"},scripts:{deploy:"./scripts/deploy.sh",prepack:'./scripts/prepack.sh --outdir "$(pwd)/dist/release" --protocol file',prettier:"prettier --write 'src/**/*.ts' 'src/**/*.tsx'",lint:"eslint 'src/**/*.ts' 'src/**/*.tsx'",jest:"./scripts/test-setup.sh && jest --runInBand",coverage:"npm run jest -- --coverage",typecheck:"tsc --noEmit",test:"./scripts/ci.sh",serve:"tsx scripts/dev-server.ts"},files:["bin/cli.mjs","dist/release/**","!*.map"],browserslist:["last 2 Chrome versions","last 2 Firefox versions"],author:"",license:"MIT",devDependencies:{"@types/jest":"22.2.3","@types/jszip":"3.1.4","@types/node":"14.0.1","@types/pako":"1.0.0","@typescript-eslint/eslint-plugin":"6.16.0","@typescript-eslint/parser":"6.16.0",acorn:"7.2.0",aphrodite:"2.1.0",esbuild:"0.27.0","esbuild-jest":"0.5.0",eslint:"8.0.0","eslint-plugin-prettier":"5.1.2","eslint-plugin-react-hooks":"4.6.0",jest:"30.2.0","jest-environment-jsdom":"29.7.0",jfrview:"0.2.0",jsverify:"0.8.3",jszip:"3.1.5",pako:"1.0.6",preact:"10.4.1",prettier:"3.1.1",protobufjs:"6.8.8","source-map":"0.6.1","ts-jest":"29.4.6",tsx:"4.19.2",typescript:"5.9.3","typescript-json-schema":"0.67.0","uglify-es":"3.2.2","uint8array-json-parser":"0.0.2"},jest:{testEnvironment:"jsdom",transform:{"^.+\\.tsx?$":"ts-jest","^.+\\.js$":"esbuild-jest"},transformIgnorePatterns:[],setupFilesAfterEnv:["./src/jest-setup.js"],moduleNameMapper:{"\\jfrview_bg.wasm$":"/src/import/java-flight-record.mock.ts"},testRegex:"\\.test\\.tsx?$",collectCoverageFrom:["**/*.{ts,tsx}","!**/*.d.{ts,tsx}"],moduleFileExtensions:["ts","tsx","js","jsx","json"]},dependencies:{open:"10.1.0"}}});function Nm(t){let e=[],n=new Map;function a(A){let o=n.get(A);if(o==null){let i={name:A.name};A.file!=null&&(i.file=A.file),A.line!=null&&(i.line=A.line),A.col!=null&&(i.col=A.col),o=e.length,n.set(A,o),e.push(i)}return o}let r={exporter:`speedscope@${Yo().version}`,name:t.name,activeProfileIndex:t.indexToView,$schema:"https://www.speedscope.app/file-format-schema.json",shared:{frames:e},profiles:[]};for(let A of t.profiles)r.profiles.push(Fm(A,a));return r}function Fm(t,e){let n={type:Yt.ProfileType.EVENTED,name:t.getName(),unit:t.getWeightUnit(),startValue:0,endValue:t.getTotalWeight(),events:[]},a=(A,o)=>{n.events.push({type:Yt.EventType.OPEN_FRAME,frame:e(A.frame),at:o})},r=(A,o)=>{n.events.push({type:Yt.EventType.CLOSE_FRAME,frame:e(A.frame),at:o})};return t.forEachCall(a,r),n}function Dm(t,e){function n(A){let{name:o,unit:i}=t;switch(i){case"nanoseconds":case"microseconds":case"milliseconds":case"seconds":A.setValueFormatter(new ee(i));break;case"bytes":A.setValueFormatter(new $e);break;case"none":A.setValueFormatter(new Vt);break}A.setName(o)}function a(A){let{startValue:o,endValue:i,events:l}=A,s=new Ie(i-o);n(s);let c=e.map((h,_)=>({key:_,...h}));for(let h of l)switch(h.type){case Yt.EventType.OPEN_FRAME:{s.enterFrame(c[h.frame],h.at-o);break}case Yt.EventType.CLOSE_FRAME:{s.leaveFrame(c[h.frame],h.at-o);break}}return s.build()}function r(A){let{startValue:o,endValue:i,samples:l,weights:s}=A,c=new ae(i-o);n(c);let h=e.map((_,f)=>({key:f,..._}));if(l.length!==s.length)throw new Error(`Expected samples.length (${l.length}) to equal weights.length (${s.length})`);for(let _=0;_h[u]),m)}return c.build()}switch(t.type){case Yt.ProfileType.EVENTED:return a(t);case Yt.ProfileType.SAMPLED:return r(t)}}function Wo(t){return{name:t.name||t.profiles[0].name||"profile",indexToView:t.activeProfileIndex||0,profiles:t.profiles.map(e=>Dm(e,t.shared.frames))}}function N_(t){let e=Nm(t),n=new Blob([JSON.stringify(e)],{type:"text/json"}),r=`${(e.name?e.name.split(".")[0]:"profile").replace(/\W+/g,"_")}.speedscope.json`;console.log("Saving",r);let A=document.createElement("a");A.download=r,A.href=window.URL.createObjectURL(n),A.dataset.downloadurl=["text/json",A.download,A.href].join(":"),document.body.appendChild(A),A.click(),document.body.removeChild(A)}var Zo=re(()=>{"use strict";ke();Ke();S_()});var eh=k(_i=>{var X_="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split("");_i.encode=function(t){if(0<=t&&t{var th=eh(),hi=5,nh=1<>1;return e?-n:n}gi.encode=function(e){var n="",a,r=lI(e);do a=r&rh,r>>>=hi,r>0&&(a|=ah),n+=th.encode(a);while(r>0);return n};gi.decode=function(e,n,a){var r=e.length,A=0,o=0,i,l;do{if(n>=r)throw new Error("Expected more digits in base 64 VLQ value.");if(l=th.decode(e.charCodeAt(n++)),l===-1)throw new Error("Invalid base64 digit: "+e.charAt(n-1));i=!!(l&ah),l&=rh,A=A+(l<{function cI(t,e,n){if(e in t)return t[e];if(arguments.length===3)return n;throw new Error('"'+e+'" is a required argument.')}Je.getArg=cI;var Ah=/^(?:([\w+\-.]+):)?\/\/(?:(\w+:\w+)@)?([\w.-]*)(?::(\d+))?(.*)$/,_I=/^data:.+\,.+$/;function Lr(t){var e=t.match(Ah);return e?{scheme:e[1],auth:e[2],host:e[3],port:e[4],path:e[5]}:null}Je.urlParse=Lr;function nr(t){var e="";return t.scheme&&(e+=t.scheme+":"),e+="//",t.auth&&(e+=t.auth+"@"),t.host&&(e+=t.host),t.port&&(e+=":"+t.port),t.path&&(e+=t.path),e}Je.urlGenerate=nr;function ui(t){var e=t,n=Lr(t);if(n){if(!n.path)return t;e=n.path}for(var a=Je.isAbsolute(e),r=e.split(/\/+/),A,o=0,i=r.length-1;i>=0;i--)A=r[i],A==="."?r.splice(i,1):A===".."?o++:o>0&&(A===""?(r.splice(i+1,o),o=0):(r.splice(i,2),o--));return e=r.join("/"),e===""&&(e=a?"/":"."),n?(n.path=e,nr(n)):e}Je.normalize=ui;function oh(t,e){t===""&&(t="."),e===""&&(e=".");var n=Lr(e),a=Lr(t);if(a&&(t=a.path||"/"),n&&!n.scheme)return a&&(n.scheme=a.scheme),nr(n);if(n||e.match(_I))return e;if(a&&!a.host&&!a.path)return a.host=e,nr(a);var r=e.charAt(0)==="/"?e:ui(t.replace(/\/+$/,"")+"/"+e);return a?(a.path=r,nr(a)):r}Je.join=oh;Je.isAbsolute=function(t){return t.charAt(0)==="/"||Ah.test(t)};function hI(t,e){t===""&&(t="."),t=t.replace(/\/$/,"");for(var n=0;e.indexOf(t+"/")!==0;){var a=t.lastIndexOf("/");if(a<0||(t=t.slice(0,a),t.match(/^([^\/]+:\/)?\/*$/)))return e;++n}return Array(n+1).join("../")+e.substr(t.length+1)}Je.relative=hI;var ih=(function(){var t=Object.create(null);return!("__proto__"in t)})();function lh(t){return t}function gI(t){return sh(t)?"$"+t:t}Je.toSetString=ih?lh:gI;function dI(t){return sh(t)?t.slice(1):t}Je.fromSetString=ih?lh:dI;function sh(t){if(!t)return!1;var e=t.length;if(e<9||t.charCodeAt(e-1)!==95||t.charCodeAt(e-2)!==95||t.charCodeAt(e-3)!==111||t.charCodeAt(e-4)!==116||t.charCodeAt(e-5)!==111||t.charCodeAt(e-6)!==114||t.charCodeAt(e-7)!==112||t.charCodeAt(e-8)!==95||t.charCodeAt(e-9)!==95)return!1;for(var n=e-10;n>=0;n--)if(t.charCodeAt(n)!==36)return!1;return!0}function uI(t,e,n){var a=rr(t.source,e.source);return a!==0||(a=t.originalLine-e.originalLine,a!==0)||(a=t.originalColumn-e.originalColumn,a!==0||n)||(a=t.generatedColumn-e.generatedColumn,a!==0)||(a=t.generatedLine-e.generatedLine,a!==0)?a:rr(t.name,e.name)}Je.compareByOriginalPositions=uI;function fI(t,e,n){var a=t.generatedLine-e.generatedLine;return a!==0||(a=t.generatedColumn-e.generatedColumn,a!==0||n)||(a=rr(t.source,e.source),a!==0)||(a=t.originalLine-e.originalLine,a!==0)||(a=t.originalColumn-e.originalColumn,a!==0)?a:rr(t.name,e.name)}Je.compareByGeneratedPositionsDeflated=fI;function rr(t,e){return t===e?0:t===null?1:e===null?-1:t>e?1:-1}function pI(t,e){var n=t.generatedLine-e.generatedLine;return n!==0||(n=t.generatedColumn-e.generatedColumn,n!==0)||(n=rr(t.source,e.source),n!==0)||(n=t.originalLine-e.originalLine,n!==0)||(n=t.originalColumn-e.originalColumn,n!==0)?n:rr(t.name,e.name)}Je.compareByGeneratedPositionsInflated=pI;function CI(t){return JSON.parse(t.replace(/^\)]}'[^\n]*\n/,""))}Je.parseSourceMapInput=CI;function mI(t,e,n){if(e=e||"",t&&(t[t.length-1]!=="/"&&e[0]!=="/"&&(t+="/"),e=t+e),n){var a=Lr(n);if(!a)throw new Error("sourceMapURL could not be parsed");if(a.path){var r=a.path.lastIndexOf("/");r>=0&&(a.path=a.path.substring(0,r+1))}e=oh(nr(a),e)}return ui(e)}Je.computeSourceURL=mI});var Ci=k(ch=>{var fi=ar(),pi=Object.prototype.hasOwnProperty,bn=typeof Map<"u";function Xt(){this._array=[],this._set=bn?new Map:Object.create(null)}Xt.fromArray=function(e,n){for(var a=new Xt,r=0,A=e.length;r=0)return n}else{var a=fi.toSetString(e);if(pi.call(this._set,a))return this._set[a]}throw new Error('"'+e+'" is not in the set.')};Xt.prototype.at=function(e){if(e>=0&&e{var _h=ar();function II(t,e){var n=t.generatedLine,a=e.generatedLine,r=t.generatedColumn,A=e.generatedColumn;return a>n||a==n&&A>=r||_h.compareByGeneratedPositionsInflated(t,e)<=0}function bA(){this._array=[],this._sorted=!0,this._last={generatedLine:-1,generatedColumn:0}}bA.prototype.unsortedForEach=function(e,n){this._array.forEach(e,n)};bA.prototype.add=function(e){II(this._last,e)?(this._last=e,this._array.push(e)):(this._sorted=!1,this._array.push(e))};bA.prototype.toArray=function(){return this._sorted||(this._array.sort(_h.compareByGeneratedPositionsInflated),this._sorted=!0),this._array};hh.MappingList=bA});var mi=k(dh=>{var Tr=di(),je=ar(),xA=Ci().ArraySet,jI=gh().MappingList;function ct(t){t||(t={}),this._file=je.getArg(t,"file",null),this._sourceRoot=je.getArg(t,"sourceRoot",null),this._skipValidation=je.getArg(t,"skipValidation",!1),this._sources=new xA,this._names=new xA,this._mappings=new jI,this._sourcesContents=null}ct.prototype._version=3;ct.fromSourceMap=function(e){var n=e.sourceRoot,a=new ct({file:e.file,sourceRoot:n});return e.eachMapping(function(r){var A={generated:{line:r.generatedLine,column:r.generatedColumn}};r.source!=null&&(A.source=r.source,n!=null&&(A.source=je.relative(n,A.source)),A.original={line:r.originalLine,column:r.originalColumn},r.name!=null&&(A.name=r.name)),a.addMapping(A)}),e.sources.forEach(function(r){var A=r;n!==null&&(A=je.relative(n,r)),a._sources.has(A)||a._sources.add(A);var o=e.sourceContentFor(r);o!=null&&a.setSourceContent(r,o)}),a};ct.prototype.addMapping=function(e){var n=je.getArg(e,"generated"),a=je.getArg(e,"original",null),r=je.getArg(e,"source",null),A=je.getArg(e,"name",null);this._skipValidation||this._validateMapping(n,a,r,A),r!=null&&(r=String(r),this._sources.has(r)||this._sources.add(r)),A!=null&&(A=String(A),this._names.has(A)||this._names.add(A)),this._mappings.add({generatedLine:n.line,generatedColumn:n.column,originalLine:a!=null&&a.line,originalColumn:a!=null&&a.column,source:r,name:A})};ct.prototype.setSourceContent=function(e,n){var a=e;this._sourceRoot!=null&&(a=je.relative(this._sourceRoot,a)),n!=null?(this._sourcesContents||(this._sourcesContents=Object.create(null)),this._sourcesContents[je.toSetString(a)]=n):this._sourcesContents&&(delete this._sourcesContents[je.toSetString(a)],Object.keys(this._sourcesContents).length===0&&(this._sourcesContents=null))};ct.prototype.applySourceMap=function(e,n,a){var r=n;if(n==null){if(e.file==null)throw new Error(`SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, or the source map's "file" property. Both were omitted.`);r=e.file}var A=this._sourceRoot;A!=null&&(r=je.relative(A,r));var o=new xA,i=new xA;this._mappings.unsortedForEach(function(l){if(l.source===r&&l.originalLine!=null){var s=e.originalPositionFor({line:l.originalLine,column:l.originalColumn});s.source!=null&&(l.source=s.source,a!=null&&(l.source=je.join(a,l.source)),A!=null&&(l.source=je.relative(A,l.source)),l.originalLine=s.line,l.originalColumn=s.column,s.name!=null&&(l.name=s.name))}var c=l.source;c!=null&&!o.has(c)&&o.add(c);var h=l.name;h!=null&&!i.has(h)&&i.add(h)},this),this._sources=o,this._names=i,e.sources.forEach(function(l){var s=e.sourceContentFor(l);s!=null&&(a!=null&&(l=je.join(a,l)),A!=null&&(l=je.relative(A,l)),this.setSourceContent(l,s))},this)};ct.prototype._validateMapping=function(e,n,a,r){if(n&&typeof n.line!="number"&&typeof n.column!="number")throw new Error("original.line and original.column are not numbers -- you probably meant to omit the original mapping entirely and only map the generated position. If so, pass null for the original mapping instead of an object with empty or null values.");if(!(e&&"line"in e&&"column"in e&&e.line>0&&e.column>=0&&!n&&!a&&!r)){if(e&&"line"in e&&"column"in e&&n&&"line"in n&&"column"in n&&e.line>0&&e.column>=0&&n.line>0&&n.column>=0&&a)return;throw new Error("Invalid mapping: "+JSON.stringify({generated:e,source:a,original:n,name:r}))}};ct.prototype._serializeMappings=function(){for(var e=0,n=1,a=0,r=0,A=0,o=0,i="",l,s,c,h,_=this._mappings.toArray(),f=0,m=_.length;f0){if(!je.compareByGeneratedPositionsInflated(s,_[f-1]))continue;l+=","}l+=Tr.encode(s.generatedColumn-e),e=s.generatedColumn,s.source!=null&&(h=this._sources.indexOf(s.source),l+=Tr.encode(h-o),o=h,l+=Tr.encode(s.originalLine-1-r),r=s.originalLine-1,l+=Tr.encode(s.originalColumn-a),a=s.originalColumn,s.name!=null&&(c=this._names.indexOf(s.name),l+=Tr.encode(c-A),A=c)),i+=l}return i};ct.prototype._generateSourcesContent=function(e,n){return e.map(function(a){if(!this._sourcesContents)return null;n!=null&&(a=je.relative(n,a));var r=je.toSetString(a);return Object.prototype.hasOwnProperty.call(this._sourcesContents,r)?this._sourcesContents[r]:null},this)};ct.prototype.toJSON=function(){var e={version:this._version,sources:this._sources.toArray(),names:this._names.toArray(),mappings:this._serializeMappings()};return this._file!=null&&(e.file=this._file),this._sourceRoot!=null&&(e.sourceRoot=this._sourceRoot),this._sourcesContents&&(e.sourcesContent=this._generateSourcesContent(e.sources,e.sourceRoot)),e};ct.prototype.toString=function(){return JSON.stringify(this.toJSON())};dh.SourceMapGenerator=ct});var uh=k(xn=>{xn.GREATEST_LOWER_BOUND=1;xn.LEAST_UPPER_BOUND=2;function Ii(t,e,n,a,r,A){var o=Math.floor((e-t)/2)+t,i=r(n,a[o],!0);return i===0?o:i>0?e-o>1?Ii(o,e,n,a,r,A):A==xn.LEAST_UPPER_BOUND?e1?Ii(t,o,n,a,r,A):A==xn.LEAST_UPPER_BOUND?o:t<0?-1:t}xn.search=function(e,n,a,r){if(n.length===0)return-1;var A=Ii(-1,n.length,e,n,a,r||xn.GREATEST_LOWER_BOUND);if(A<0)return-1;for(;A-1>=0&&a(n[A],n[A-1],!0)===0;)--A;return A}});var ph=k(fh=>{function ji(t,e,n){var a=t[e];t[e]=t[n],t[n]=a}function vI(t,e){return Math.round(t+Math.random()*(e-t))}function vi(t,e,n,a){if(n{var D=ar(),yi=uh(),Ar=Ci().ArraySet,yI=di(),Hr=ph().quickSort;function se(t,e){var n=t;return typeof t=="string"&&(n=D.parseSourceMapInput(t)),n.sections!=null?new It(n,e):new Le(n,e)}se.fromSourceMap=function(t,e){return Le.fromSourceMap(t,e)};se.prototype._version=3;se.prototype.__generatedMappings=null;Object.defineProperty(se.prototype,"_generatedMappings",{configurable:!0,enumerable:!0,get:function(){return this.__generatedMappings||this._parseMappings(this._mappings,this.sourceRoot),this.__generatedMappings}});se.prototype.__originalMappings=null;Object.defineProperty(se.prototype,"_originalMappings",{configurable:!0,enumerable:!0,get:function(){return this.__originalMappings||this._parseMappings(this._mappings,this.sourceRoot),this.__originalMappings}});se.prototype._charIsMappingSeparator=function(e,n){var a=e.charAt(n);return a===";"||a===","};se.prototype._parseMappings=function(e,n){throw new Error("Subclasses must implement _parseMappings")};se.GENERATED_ORDER=1;se.ORIGINAL_ORDER=2;se.GREATEST_LOWER_BOUND=1;se.LEAST_UPPER_BOUND=2;se.prototype.eachMapping=function(e,n,a){var r=n||null,A=a||se.GENERATED_ORDER,o;switch(A){case se.GENERATED_ORDER:o=this._generatedMappings;break;case se.ORIGINAL_ORDER:o=this._originalMappings;break;default:throw new Error("Unknown order of iteration.")}var i=this.sourceRoot;o.map(function(l){var s=l.source===null?null:this._sources.at(l.source);return s=D.computeSourceURL(i,s,this._sourceMapURL),{source:s,generatedLine:l.generatedLine,generatedColumn:l.generatedColumn,originalLine:l.originalLine,originalColumn:l.originalColumn,name:l.name===null?null:this._names.at(l.name)}},this).forEach(e,r)};se.prototype.allGeneratedPositionsFor=function(e){var n=D.getArg(e,"line"),a={source:D.getArg(e,"source"),originalLine:n,originalColumn:D.getArg(e,"column",0)};if(a.source=this._findSourceIndex(a.source),a.source<0)return[];var r=[],A=this._findMapping(a,this._originalMappings,"originalLine","originalColumn",D.compareByOriginalPositions,yi.LEAST_UPPER_BOUND);if(A>=0){var o=this._originalMappings[A];if(e.column===void 0)for(var i=o.originalLine;o&&o.originalLine===i;)r.push({line:D.getArg(o,"generatedLine",null),column:D.getArg(o,"generatedColumn",null),lastColumn:D.getArg(o,"lastGeneratedColumn",null)}),o=this._originalMappings[++A];else for(var l=o.originalColumn;o&&o.originalLine===n&&o.originalColumn==l;)r.push({line:D.getArg(o,"generatedLine",null),column:D.getArg(o,"generatedColumn",null),lastColumn:D.getArg(o,"lastGeneratedColumn",null)}),o=this._originalMappings[++A]}return r};kA.SourceMapConsumer=se;function Le(t,e){var n=t;typeof t=="string"&&(n=D.parseSourceMapInput(t));var a=D.getArg(n,"version"),r=D.getArg(n,"sources"),A=D.getArg(n,"names",[]),o=D.getArg(n,"sourceRoot",null),i=D.getArg(n,"sourcesContent",null),l=D.getArg(n,"mappings"),s=D.getArg(n,"file",null);if(a!=this._version)throw new Error("Unsupported version: "+a);o&&(o=D.normalize(o)),r=r.map(String).map(D.normalize).map(function(c){return o&&D.isAbsolute(o)&&D.isAbsolute(c)?D.relative(o,c):c}),this._names=Ar.fromArray(A.map(String),!0),this._sources=Ar.fromArray(r,!0),this._absoluteSources=this._sources.toArray().map(function(c){return D.computeSourceURL(o,c,e)}),this.sourceRoot=o,this.sourcesContent=i,this._mappings=l,this._sourceMapURL=e,this.file=s}Le.prototype=Object.create(se.prototype);Le.prototype.consumer=se;Le.prototype._findSourceIndex=function(t){var e=t;if(this.sourceRoot!=null&&(e=D.relative(this.sourceRoot,e)),this._sources.has(e))return this._sources.indexOf(e);var n;for(n=0;n1&&(u.source=i+p[1],i+=p[1],u.originalLine=A+p[2],A=u.originalLine,u.originalLine+=1,u.originalColumn=o+p[3],o=u.originalColumn,p.length>4&&(u.name=l+p[4],l+=p[4])),m.push(u),typeof u.originalLine=="number"&&f.push(u)}Hr(m,D.compareByGeneratedPositionsDeflated),this.__generatedMappings=m,Hr(f,D.compareByOriginalPositions),this.__originalMappings=f};Le.prototype._findMapping=function(e,n,a,r,A,o){if(e[a]<=0)throw new TypeError("Line must be greater than or equal to 1, got "+e[a]);if(e[r]<0)throw new TypeError("Column must be greater than or equal to 0, got "+e[r]);return yi.search(e,n,A,o)};Le.prototype.computeColumnSpans=function(){for(var e=0;e=0){var r=this._generatedMappings[a];if(r.generatedLine===n.generatedLine){var A=D.getArg(r,"source",null);A!==null&&(A=this._sources.at(A),A=D.computeSourceURL(this.sourceRoot,A,this._sourceMapURL));var o=D.getArg(r,"name",null);return o!==null&&(o=this._names.at(o)),{source:A,line:D.getArg(r,"originalLine",null),column:D.getArg(r,"originalColumn",null),name:o}}}return{source:null,line:null,column:null,name:null}};Le.prototype.hasContentsOfAllSources=function(){return this.sourcesContent?this.sourcesContent.length>=this._sources.size()&&!this.sourcesContent.some(function(e){return e==null}):!1};Le.prototype.sourceContentFor=function(e,n){if(!this.sourcesContent)return null;var a=this._findSourceIndex(e);if(a>=0)return this.sourcesContent[a];var r=e;this.sourceRoot!=null&&(r=D.relative(this.sourceRoot,r));var A;if(this.sourceRoot!=null&&(A=D.urlParse(this.sourceRoot))){var o=r.replace(/^file:\/\//,"");if(A.scheme=="file"&&this._sources.has(o))return this.sourcesContent[this._sources.indexOf(o)];if((!A.path||A.path=="/")&&this._sources.has("/"+r))return this.sourcesContent[this._sources.indexOf("/"+r)]}if(n)return null;throw new Error('"'+r+'" is not in the SourceMap.')};Le.prototype.generatedPositionFor=function(e){var n=D.getArg(e,"source");if(n=this._findSourceIndex(n),n<0)return{line:null,column:null,lastColumn:null};var a={source:n,originalLine:D.getArg(e,"line"),originalColumn:D.getArg(e,"column")},r=this._findMapping(a,this._originalMappings,"originalLine","originalColumn",D.compareByOriginalPositions,D.getArg(e,"bias",se.GREATEST_LOWER_BOUND));if(r>=0){var A=this._originalMappings[r];if(A.source===a.source)return{line:D.getArg(A,"generatedLine",null),column:D.getArg(A,"generatedColumn",null),lastColumn:D.getArg(A,"lastGeneratedColumn",null)}}return{line:null,column:null,lastColumn:null}};kA.BasicSourceMapConsumer=Le;function It(t,e){var n=t;typeof t=="string"&&(n=D.parseSourceMapInput(t));var a=D.getArg(n,"version"),r=D.getArg(n,"sections");if(a!=this._version)throw new Error("Unsupported version: "+a);this._sources=new Ar,this._names=new Ar;var A={line:-1,column:0};this._sections=r.map(function(o){if(o.url)throw new Error("Support for url field in sections not implemented.");var i=D.getArg(o,"offset"),l=D.getArg(i,"line"),s=D.getArg(i,"column");if(l{var EI=mi().SourceMapGenerator,SA=ar(),BI=/(\r?\n)/,QI=10,or="$$$isSourceNode$$$";function tt(t,e,n,a,r){this.children=[],this.sourceContents={},this.line=t??null,this.column=e??null,this.source=n??null,this.name=r??null,this[or]=!0,a!=null&&this.add(a)}tt.fromStringWithSourceMap=function(e,n,a){var r=new tt,A=e.split(BI),o=0,i=function(){var _=m(),f=m()||"";return _+f;function m(){return o=0;n--)this.prepend(e[n]);else if(e[or]||typeof e=="string")this.children.unshift(e);else throw new TypeError("Expected a SourceNode, string, or an array of SourceNodes and strings. Got "+e);return this};tt.prototype.walk=function(e){for(var n,a=0,r=this.children.length;a0){for(n=[],a=0;a{NA.SourceMapGenerator=mi().SourceMapGenerator;NA.SourceMapConsumer=mh().SourceMapConsumer;NA.SourceNode=jh().SourceNode});function PI(t){let e=[];function n(a){e.push({id:a.id,callFrame:{columnNumber:0,functionName:a.functionName,lineNumber:a.lineNumber,scriptId:a.scriptId,url:a.url},hitCount:a.hitCount,children:a.children.map(r=>r.id)}),a.children.forEach(n)}return n(t),e}function GI(t,e){return t.map((n,a)=>{let r=a===0?e*1e6:t[a-1];return n-r})}function kh(t){return{samples:t.samples,startTime:t.startTime*1e6,endTime:t.endTime*1e6,nodes:PI(t.head),timeDeltas:GI(t.timestamps,t.startTime)}}var Sh=re(()=>{"use strict"});function bi(t){if(!Array.isArray(t)||t.length<1)return!1;let e=t[0];return!(!("pid"in e&&"tid"in e&&"ph"in e&&"cat"in e)||!t.find(n=>n.name==="CpuProfile"||n.name==="Profile"||n.name==="ProfileChunk"))}function Fh(t){return"traceEvents"in t?bi(t.traceEvents):!1}function Kr(t,e){let n=new Map,a=new Map,r=new Map;Fe(t,A=>A.ts);for(let A of t){if(A.name==="CpuProfile"){let o=`${A.pid}:${A.tid}`,i=A.id||o;if(A.args.data==null)continue;n.set(i,A.args.data.cpuProfile),a.set(i,o)}if(A.name==="Profile"){let o=`${A.pid}:${A.tid}`;n.set(A.id||o,{startTime:0,endTime:0,nodes:[],samples:[],timeDeltas:[],...A.args.data}),A.id&&a.set(A.id,`${A.pid}:${A.tid}`)}if(A.name==="thread_name"&&r.set(`${A.pid}:${A.tid}`,A.args.name),A.name==="ProfileChunk"){let o=`${A.pid}:${A.tid}`,i=n.get(A.id||o);if(i){let l=A.args.data;if(l==null)continue;l.cpuProfile&&(l.cpuProfile.nodes&&(i.nodes=i.nodes.concat(l.cpuProfile.nodes)),l.cpuProfile.samples&&(i.samples=i.samples.concat(l.cpuProfile.samples))),l.timeDeltas&&(i.timeDeltas=i.timeDeltas.concat(l.timeDeltas)),l.startTime!=null&&(i.startTime=l.startTime),l.endTime!=null&&(i.endTime=l.endTime)}else console.warn(`Ignoring ProfileChunk for undeclared Profile with id ${A.id||o}`)}}if(n.size>0){let A=[],o=0;return is(n.keys(),i=>{let l=null,s=a.get(i);s&&(l=r.get(s)||null);let c=DA(n.get(i));l&&n.size>1?(c.setName(`${e} - ${l}`),l==="CrRendererMain"&&(o=A.length)):c.setName(`${e}`),A.push(c)}),{name:e,indexToView:o,profiles:A}}else throw new Error("Could not find CPU profile in Timeline")}function wi(t){return De(UI,t,e=>{let n=e.url,a=e.lineNumber;a!=null&&a++;let r=e.columnNumber;r!=null&&r++;let A=e.functionName||(n?`(anonymous ${n.split("/").pop()}:${a})`:"(anonymous)");return{key:`${A}:${n}:${a}:${r}`,name:A,file:n,line:a,col:r}})}function OI(t){let{functionName:e,url:n}=t;return n==="native dummy.js"?!0:e==="(root)"||e==="(idle)"}function Nh(t){return t==="(garbage collector)"||t==="(program)"}function DA(t){let e=new Ie(t.endTime-t.startTime),n=new Map;for(let s of t.nodes)n.set(s.id,s);for(let s of t.nodes)if(typeof s.parent=="number"&&(s.parent=n.get(s.parent)),!!s.children)for(let c of s.children){let h=n.get(c);h&&(h.parent=s)}let a=[],r=[],A=t.timeDeltas[0],o=A,i=NaN;for(let s=0;s0&&Ae(l)!=f;){let u=l.pop(),g=wi(u.callFrame);e.leaveFrame(g,c)}let m=[];for(let u=_;u&&u!=f&&!OI(u.callFrame);u=Nh(u.callFrame.functionName)?Ae(l):u.parent||null)m.push(u);m.reverse();for(let u of m)e.enterFrame(wi(u.callFrame),c);l=l.concat(m)}for(let s=l.length-1;s>=0;s--)e.leaveFrame(wi(l[s].callFrame),Ae(r));return e.setValueFormatter(new ee("microseconds")),e.build()}function Dh(t){return DA(kh(t))}var UI,Rh=re(()=>{"use strict";ke();V();Ke();Sh();UI=new Map});function xi(t){let{frames:e,mode:n,raw:a,raw_timestamp_deltas:r,interval:A}=t,o=new ae;o.setValueFormatter(new ee("microseconds"));let i=0,l=[];for(let s=0;s{"use strict";ke();Ke()});var en=k(Oe=>{"use strict";var qI=typeof Uint8Array<"u"&&typeof Uint16Array<"u"&&typeof Int32Array<"u";function zI(t,e){return Object.prototype.hasOwnProperty.call(t,e)}Oe.assign=function(t){for(var e=Array.prototype.slice.call(arguments,1);e.length;){var n=e.shift();if(n){if(typeof n!="object")throw new TypeError(n+"must be non-object");for(var a in n)zI(n,a)&&(t[a]=n[a])}}return t};Oe.shrinkBuf=function(t,e){return t.length===e?t:t.subarray?t.subarray(0,e):(t.length=e,t)};var $I={arraySet:function(t,e,n,a,r){if(e.subarray&&t.subarray){t.set(e.subarray(n,n+a),r);return}for(var A=0;A{"use strict";var YI=en(),WI=4,Th=0,Hh=1,ZI=2;function sr(t){for(var e=t.length;--e>=0;)t[e]=0}var XI=0,Uh=1,ej=2,tj=3,nj=258,Li=29,qr=256,Pr=qr+1+Li,lr=30,Ti=19,Oh=2*Pr+1,kn=15,ki=16,rj=7,Hi=256,qh=16,zh=17,$h=18,Di=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0],RA=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],aj=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7],Vh=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],Aj=512,tn=new Array((Pr+2)*2);sr(tn);var Jr=new Array(lr*2);sr(Jr);var Gr=new Array(Aj);sr(Gr);var Ur=new Array(nj-tj+1);sr(Ur);var Mi=new Array(Li);sr(Mi);var LA=new Array(lr);sr(LA);function Si(t,e,n,a,r){this.static_tree=t,this.extra_bits=e,this.extra_base=n,this.elems=a,this.max_length=r,this.has_stree=t&&t.length}var Yh,Wh,Zh;function Ni(t,e){this.dyn_tree=t,this.max_code=0,this.stat_desc=e}function Xh(t){return t<256?Gr[t]:Gr[256+(t>>>7)]}function Or(t,e){t.pending_buf[t.pending++]=e&255,t.pending_buf[t.pending++]=e>>>8&255}function Ve(t,e,n){t.bi_valid>ki-n?(t.bi_buf|=e<>ki-t.bi_valid,t.bi_valid+=n-ki):(t.bi_buf|=e<>>=1,n<<=1;while(--e>0);return n>>>1}function oj(t){t.bi_valid===16?(Or(t,t.bi_buf),t.bi_buf=0,t.bi_valid=0):t.bi_valid>=8&&(t.pending_buf[t.pending++]=t.bi_buf&255,t.bi_buf>>=8,t.bi_valid-=8)}function ij(t,e){var n=e.dyn_tree,a=e.max_code,r=e.stat_desc.static_tree,A=e.stat_desc.has_stree,o=e.stat_desc.extra_bits,i=e.stat_desc.extra_base,l=e.stat_desc.max_length,s,c,h,_,f,m,u=0;for(_=0;_<=kn;_++)t.bl_count[_]=0;for(n[t.heap[t.heap_max]*2+1]=0,s=t.heap_max+1;sl&&(_=l,u++),n[c*2+1]=_,!(c>a)&&(t.bl_count[_]++,f=0,c>=i&&(f=o[c-i]),m=n[c*2],t.opt_len+=m*(_+f),A&&(t.static_len+=m*(r[c*2+1]+f)));if(u!==0){do{for(_=l-1;t.bl_count[_]===0;)_--;t.bl_count[_]--,t.bl_count[_+1]+=2,t.bl_count[l]--,u-=2}while(u>0);for(_=l;_!==0;_--)for(c=t.bl_count[_];c!==0;)h=t.heap[--s],!(h>a)&&(n[h*2+1]!==_&&(t.opt_len+=(_-n[h*2+1])*n[h*2],n[h*2+1]=_),c--)}}function tg(t,e,n){var a=new Array(kn+1),r=0,A,o;for(A=1;A<=kn;A++)a[A]=r=r+n[A-1]<<1;for(o=0;o<=e;o++){var i=t[o*2+1];i!==0&&(t[o*2]=eg(a[i]++,i))}}function lj(){var t,e,n,a,r,A=new Array(kn+1);for(n=0,a=0;a>=7;a8?Or(t,t.bi_buf):t.bi_valid>0&&(t.pending_buf[t.pending++]=t.bi_buf),t.bi_buf=0,t.bi_valid=0}function sj(t,e,n,a){rg(t),a&&(Or(t,n),Or(t,~n)),YI.arraySet(t.pending_buf,t.window,e,n,t.pending),t.pending+=n}function Mh(t,e,n,a){var r=e*2,A=n*2;return t[r]>1;o>=1;o--)Fi(t,n,o);s=A;do o=t.heap[1],t.heap[1]=t.heap[t.heap_len--],Fi(t,n,1),i=t.heap[1],t.heap[--t.heap_max]=o,t.heap[--t.heap_max]=i,n[s*2]=n[o*2]+n[i*2],t.depth[s]=(t.depth[o]>=t.depth[i]?t.depth[o]:t.depth[i])+1,n[o*2+1]=n[i*2+1]=s,t.heap[1]=s++,Fi(t,n,1);while(t.heap_len>=2);t.heap[--t.heap_max]=t.heap[1],ij(t,e),tg(n,l,t.bl_count)}function Jh(t,e,n){var a,r=-1,A,o=e[1],i=0,l=7,s=4;for(o===0&&(l=138,s=3),e[(n+1)*2+1]=65535,a=0;a<=n;a++)A=o,o=e[(a+1)*2+1],!(++i=3&&t.bl_tree[Vh[e]*2+1]===0;e--);return t.opt_len+=3*(e+1)+5+5+4,e}function _j(t,e,n,a){var r;for(Ve(t,e-257,5),Ve(t,n-1,5),Ve(t,a-4,4),r=0;r>>=1)if(e&1&&t.dyn_ltree[n*2]!==0)return Th;if(t.dyn_ltree[18]!==0||t.dyn_ltree[20]!==0||t.dyn_ltree[26]!==0)return Hh;for(n=32;n0?(t.strm.data_type===ZI&&(t.strm.data_type=hj(t)),Ri(t,t.l_desc),Ri(t,t.d_desc),o=cj(t),r=t.opt_len+3+7>>>3,A=t.static_len+3+7>>>3,A<=r&&(r=A)):r=A=n+5,n+4<=r&&e!==-1?ag(t,e,n,a):t.strategy===WI||A===r?(Ve(t,(Uh<<1)+(a?1:0),3),Kh(t,tn,Jr)):(Ve(t,(ej<<1)+(a?1:0),3),_j(t,t.l_desc.max_code+1,t.d_desc.max_code+1,o+1),Kh(t,t.dyn_ltree,t.dyn_dtree)),ng(t),a&&rg(t)}function fj(t,e,n){return t.pending_buf[t.d_buf+t.last_lit*2]=e>>>8&255,t.pending_buf[t.d_buf+t.last_lit*2+1]=e&255,t.pending_buf[t.l_buf+t.last_lit]=n&255,t.last_lit++,e===0?t.dyn_ltree[n*2]++:(t.matches++,e--,t.dyn_ltree[(Ur[n]+qr+1)*2]++,t.dyn_dtree[Xh(e)*2]++),t.last_lit===t.lit_bufsize-1}cr._tr_init=gj;cr._tr_stored_block=ag;cr._tr_flush_block=uj;cr._tr_tally=fj;cr._tr_align=dj});var Ki=k((_k,og)=>{"use strict";function pj(t,e,n,a){for(var r=t&65535|0,A=t>>>16&65535|0,o=0;n!==0;){o=n>2e3?2e3:n,n-=o;do r=r+e[a++]|0,A=A+r|0;while(--o);r%=65521,A%=65521}return r|A<<16|0}og.exports=pj});var Ji=k((hk,ig)=>{"use strict";function Cj(){for(var t,e=[],n=0;n<256;n++){t=n;for(var a=0;a<8;a++)t=t&1?3988292384^t>>>1:t>>>1;e[n]=t}return e}var mj=Cj();function Ij(t,e,n,a){var r=mj,A=a+n;t^=-1;for(var o=a;o>>8^r[(t^e[o])&255];return t^-1}ig.exports=Ij});var TA=k((gk,lg)=>{"use strict";lg.exports={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"}});var pg=k(Ht=>{"use strict";var qe=en(),_t=Ag(),hg=Ki(),hn=Ji(),jj=TA(),Dn=0,vj=1,yj=3,pn=4,sg=5,Tt=0,cg=1,ht=-2,Ej=-3,Pi=-5,Bj=-1,Qj=1,HA=2,wj=3,bj=4,xj=0,kj=2,PA=8,Sj=9,Nj=15,Fj=8,Dj=29,Rj=256,Ui=Rj+1+Dj,Lj=30,Tj=19,Hj=2*Ui+1,Mj=15,$=3,un=258,jt=un+$+1,Kj=32,GA=42,Oi=69,MA=73,KA=91,JA=103,Sn=113,$r=666,Be=1,Vr=2,Nn=3,gr=4,Jj=3;function fn(t,e){return t.msg=jj[e],e}function _g(t){return(t<<1)-(t>4?9:0)}function dn(t){for(var e=t.length;--e>=0;)t[e]=0}function gn(t){var e=t.state,n=e.pending;n>t.avail_out&&(n=t.avail_out),n!==0&&(qe.arraySet(t.output,e.pending_buf,e.pending_out,n,t.next_out),t.next_out+=n,e.pending_out+=n,t.total_out+=n,t.avail_out-=n,e.pending-=n,e.pending===0&&(e.pending_out=0))}function Te(t,e){_t._tr_flush_block(t,t.block_start>=0?t.block_start:-1,t.strstart-t.block_start,e),t.block_start=t.strstart,gn(t.strm)}function W(t,e){t.pending_buf[t.pending++]=e}function zr(t,e){t.pending_buf[t.pending++]=e>>>8&255,t.pending_buf[t.pending++]=e&255}function Pj(t,e,n,a){var r=t.avail_in;return r>a&&(r=a),r===0?0:(t.avail_in-=r,qe.arraySet(e,t.input,t.next_in,r,n),t.state.wrap===1?t.adler=hg(t.adler,e,r,n):t.state.wrap===2&&(t.adler=hn(t.adler,e,r,n)),t.next_in+=r,t.total_in+=r,r)}function gg(t,e){var n=t.max_chain_length,a=t.strstart,r,A,o=t.prev_length,i=t.nice_match,l=t.strstart>t.w_size-jt?t.strstart-(t.w_size-jt):0,s=t.window,c=t.w_mask,h=t.prev,_=t.strstart+un,f=s[a+o-1],m=s[a+o];t.prev_length>=t.good_match&&(n>>=2),i>t.lookahead&&(i=t.lookahead);do if(r=e,!(s[r+o]!==m||s[r+o-1]!==f||s[r]!==s[a]||s[++r]!==s[a+1])){a+=2,r++;do;while(s[++a]===s[++r]&&s[++a]===s[++r]&&s[++a]===s[++r]&&s[++a]===s[++r]&&s[++a]===s[++r]&&s[++a]===s[++r]&&s[++a]===s[++r]&&s[++a]===s[++r]&&a<_);if(A=un-(_-a),a=_-un,A>o){if(t.match_start=e,o=A,A>=i)break;f=s[a+o-1],m=s[a+o]}}while((e=h[e&c])>l&&--n!==0);return o<=t.lookahead?o:t.lookahead}function Fn(t){var e=t.w_size,n,a,r,A,o;do{if(A=t.window_size-t.lookahead-t.strstart,t.strstart>=e+(e-jt)){qe.arraySet(t.window,t.window,e,e,0),t.match_start-=e,t.strstart-=e,t.block_start-=e,a=t.hash_size,n=a;do r=t.head[--n],t.head[n]=r>=e?r-e:0;while(--a);a=e,n=a;do r=t.prev[--n],t.prev[n]=r>=e?r-e:0;while(--a);A+=e}if(t.strm.avail_in===0)break;if(a=Pj(t.strm,t.window,t.strstart+t.lookahead,A),t.lookahead+=a,t.lookahead+t.insert>=$)for(o=t.strstart-t.insert,t.ins_h=t.window[o],t.ins_h=(t.ins_h<t.pending_buf_size-5&&(n=t.pending_buf_size-5);;){if(t.lookahead<=1){if(Fn(t),t.lookahead===0&&e===Dn)return Be;if(t.lookahead===0)break}t.strstart+=t.lookahead,t.lookahead=0;var a=t.block_start+n;if((t.strstart===0||t.strstart>=a)&&(t.lookahead=t.strstart-a,t.strstart=a,Te(t,!1),t.strm.avail_out===0)||t.strstart-t.block_start>=t.w_size-jt&&(Te(t,!1),t.strm.avail_out===0))return Be}return t.insert=0,e===pn?(Te(t,!0),t.strm.avail_out===0?Nn:gr):(t.strstart>t.block_start&&(Te(t,!1),t.strm.avail_out===0),Be)}function Gi(t,e){for(var n,a;;){if(t.lookahead=$&&(t.ins_h=(t.ins_h<=$)if(a=_t._tr_tally(t,t.strstart-t.match_start,t.match_length-$),t.lookahead-=t.match_length,t.match_length<=t.max_lazy_match&&t.lookahead>=$){t.match_length--;do t.strstart++,t.ins_h=(t.ins_h<=$&&(t.ins_h=(t.ins_h<4096)&&(t.match_length=$-1)),t.prev_length>=$&&t.match_length<=t.prev_length){r=t.strstart+t.lookahead-$,a=_t._tr_tally(t,t.strstart-1-t.prev_match,t.prev_length-$),t.lookahead-=t.prev_length-1,t.prev_length-=2;do++t.strstart<=r&&(t.ins_h=(t.ins_h<=$&&t.strstart>0&&(r=t.strstart-1,a=o[r],a===o[++r]&&a===o[++r]&&a===o[++r])){A=t.strstart+un;do;while(a===o[++r]&&a===o[++r]&&a===o[++r]&&a===o[++r]&&a===o[++r]&&a===o[++r]&&a===o[++r]&&a===o[++r]&&rt.lookahead&&(t.match_length=t.lookahead)}if(t.match_length>=$?(n=_t._tr_tally(t,1,t.match_length-$),t.lookahead-=t.match_length,t.strstart+=t.match_length,t.match_length=0):(n=_t._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++),n&&(Te(t,!1),t.strm.avail_out===0))return Be}return t.insert=0,e===pn?(Te(t,!0),t.strm.avail_out===0?Nn:gr):t.last_lit&&(Te(t,!1),t.strm.avail_out===0)?Be:Vr}function Oj(t,e){for(var n;;){if(t.lookahead===0&&(Fn(t),t.lookahead===0)){if(e===Dn)return Be;break}if(t.match_length=0,n=_t._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++,n&&(Te(t,!1),t.strm.avail_out===0))return Be}return t.insert=0,e===pn?(Te(t,!0),t.strm.avail_out===0?Nn:gr):t.last_lit&&(Te(t,!1),t.strm.avail_out===0)?Be:Vr}function Lt(t,e,n,a,r){this.good_length=t,this.max_lazy=e,this.nice_length=n,this.max_chain=a,this.func=r}var hr;hr=[new Lt(0,0,0,0,Gj),new Lt(4,4,8,4,Gi),new Lt(4,5,16,8,Gi),new Lt(4,6,32,32,Gi),new Lt(4,4,16,16,_r),new Lt(8,16,32,32,_r),new Lt(8,16,128,128,_r),new Lt(8,32,128,256,_r),new Lt(32,128,258,1024,_r),new Lt(32,258,258,4096,_r)];function qj(t){t.window_size=2*t.w_size,dn(t.head),t.max_lazy_match=hr[t.level].max_lazy,t.good_match=hr[t.level].good_length,t.nice_match=hr[t.level].nice_length,t.max_chain_length=hr[t.level].max_chain,t.strstart=0,t.block_start=0,t.lookahead=0,t.insert=0,t.match_length=t.prev_length=$-1,t.match_available=0,t.ins_h=0}function zj(){this.strm=null,this.status=0,this.pending_buf=null,this.pending_buf_size=0,this.pending_out=0,this.pending=0,this.wrap=0,this.gzhead=null,this.gzindex=0,this.method=PA,this.last_flush=-1,this.w_size=0,this.w_bits=0,this.w_mask=0,this.window=null,this.window_size=0,this.prev=null,this.head=null,this.ins_h=0,this.hash_size=0,this.hash_bits=0,this.hash_mask=0,this.hash_shift=0,this.block_start=0,this.match_length=0,this.prev_match=0,this.match_available=0,this.strstart=0,this.match_start=0,this.lookahead=0,this.prev_length=0,this.max_chain_length=0,this.max_lazy_match=0,this.level=0,this.strategy=0,this.good_match=0,this.nice_match=0,this.dyn_ltree=new qe.Buf16(Hj*2),this.dyn_dtree=new qe.Buf16((2*Lj+1)*2),this.bl_tree=new qe.Buf16((2*Tj+1)*2),dn(this.dyn_ltree),dn(this.dyn_dtree),dn(this.bl_tree),this.l_desc=null,this.d_desc=null,this.bl_desc=null,this.bl_count=new qe.Buf16(Mj+1),this.heap=new qe.Buf16(2*Ui+1),dn(this.heap),this.heap_len=0,this.heap_max=0,this.depth=new qe.Buf16(2*Ui+1),dn(this.depth),this.l_buf=0,this.lit_bufsize=0,this.last_lit=0,this.d_buf=0,this.opt_len=0,this.static_len=0,this.matches=0,this.insert=0,this.bi_buf=0,this.bi_valid=0}function dg(t){var e;return!t||!t.state?fn(t,ht):(t.total_in=t.total_out=0,t.data_type=kj,e=t.state,e.pending=0,e.pending_out=0,e.wrap<0&&(e.wrap=-e.wrap),e.status=e.wrap?GA:Sn,t.adler=e.wrap===2?0:1,e.last_flush=Dn,_t._tr_init(e),Tt)}function ug(t){var e=dg(t);return e===Tt&&qj(t.state),e}function $j(t,e){return!t||!t.state||t.state.wrap!==2?ht:(t.state.gzhead=e,Tt)}function fg(t,e,n,a,r,A){if(!t)return ht;var o=1;if(e===Bj&&(e=6),a<0?(o=0,a=-a):a>15&&(o=2,a-=16),r<1||r>Sj||n!==PA||a<8||a>15||e<0||e>9||A<0||A>bj)return fn(t,ht);a===8&&(a=9);var i=new zj;return t.state=i,i.strm=t,i.wrap=o,i.gzhead=null,i.w_bits=a,i.w_size=1<sg||e<0)return t?fn(t,ht):ht;if(a=t.state,!t.output||!t.input&&t.avail_in!==0||a.status===$r&&e!==pn)return fn(t,t.avail_out===0?Pi:ht);if(a.strm=t,n=a.last_flush,a.last_flush=e,a.status===GA)if(a.wrap===2)t.adler=0,W(a,31),W(a,139),W(a,8),a.gzhead?(W(a,(a.gzhead.text?1:0)+(a.gzhead.hcrc?2:0)+(a.gzhead.extra?4:0)+(a.gzhead.name?8:0)+(a.gzhead.comment?16:0)),W(a,a.gzhead.time&255),W(a,a.gzhead.time>>8&255),W(a,a.gzhead.time>>16&255),W(a,a.gzhead.time>>24&255),W(a,a.level===9?2:a.strategy>=HA||a.level<2?4:0),W(a,a.gzhead.os&255),a.gzhead.extra&&a.gzhead.extra.length&&(W(a,a.gzhead.extra.length&255),W(a,a.gzhead.extra.length>>8&255)),a.gzhead.hcrc&&(t.adler=hn(t.adler,a.pending_buf,a.pending,0)),a.gzindex=0,a.status=Oi):(W(a,0),W(a,0),W(a,0),W(a,0),W(a,0),W(a,a.level===9?2:a.strategy>=HA||a.level<2?4:0),W(a,Jj),a.status=Sn);else{var o=PA+(a.w_bits-8<<4)<<8,i=-1;a.strategy>=HA||a.level<2?i=0:a.level<6?i=1:a.level===6?i=2:i=3,o|=i<<6,a.strstart!==0&&(o|=Kj),o+=31-o%31,a.status=Sn,zr(a,o),a.strstart!==0&&(zr(a,t.adler>>>16),zr(a,t.adler&65535)),t.adler=1}if(a.status===Oi)if(a.gzhead.extra){for(r=a.pending;a.gzindex<(a.gzhead.extra.length&65535)&&!(a.pending===a.pending_buf_size&&(a.gzhead.hcrc&&a.pending>r&&(t.adler=hn(t.adler,a.pending_buf,a.pending-r,r)),gn(t),r=a.pending,a.pending===a.pending_buf_size));)W(a,a.gzhead.extra[a.gzindex]&255),a.gzindex++;a.gzhead.hcrc&&a.pending>r&&(t.adler=hn(t.adler,a.pending_buf,a.pending-r,r)),a.gzindex===a.gzhead.extra.length&&(a.gzindex=0,a.status=MA)}else a.status=MA;if(a.status===MA)if(a.gzhead.name){r=a.pending;do{if(a.pending===a.pending_buf_size&&(a.gzhead.hcrc&&a.pending>r&&(t.adler=hn(t.adler,a.pending_buf,a.pending-r,r)),gn(t),r=a.pending,a.pending===a.pending_buf_size)){A=1;break}a.gzindexr&&(t.adler=hn(t.adler,a.pending_buf,a.pending-r,r)),A===0&&(a.gzindex=0,a.status=KA)}else a.status=KA;if(a.status===KA)if(a.gzhead.comment){r=a.pending;do{if(a.pending===a.pending_buf_size&&(a.gzhead.hcrc&&a.pending>r&&(t.adler=hn(t.adler,a.pending_buf,a.pending-r,r)),gn(t),r=a.pending,a.pending===a.pending_buf_size)){A=1;break}a.gzindexr&&(t.adler=hn(t.adler,a.pending_buf,a.pending-r,r)),A===0&&(a.status=JA)}else a.status=JA;if(a.status===JA&&(a.gzhead.hcrc?(a.pending+2>a.pending_buf_size&&gn(t),a.pending+2<=a.pending_buf_size&&(W(a,t.adler&255),W(a,t.adler>>8&255),t.adler=0,a.status=Sn)):a.status=Sn),a.pending!==0){if(gn(t),t.avail_out===0)return a.last_flush=-1,Tt}else if(t.avail_in===0&&_g(e)<=_g(n)&&e!==pn)return fn(t,Pi);if(a.status===$r&&t.avail_in!==0)return fn(t,Pi);if(t.avail_in!==0||a.lookahead!==0||e!==Dn&&a.status!==$r){var l=a.strategy===HA?Oj(a,e):a.strategy===wj?Uj(a,e):hr[a.level].func(a,e);if((l===Nn||l===gr)&&(a.status=$r),l===Be||l===Nn)return t.avail_out===0&&(a.last_flush=-1),Tt;if(l===Vr&&(e===vj?_t._tr_align(a):e!==sg&&(_t._tr_stored_block(a,0,0,!1),e===yj&&(dn(a.head),a.lookahead===0&&(a.strstart=0,a.block_start=0,a.insert=0))),gn(t),t.avail_out===0))return a.last_flush=-1,Tt}return e!==pn?Tt:a.wrap<=0?cg:(a.wrap===2?(W(a,t.adler&255),W(a,t.adler>>8&255),W(a,t.adler>>16&255),W(a,t.adler>>24&255),W(a,t.total_in&255),W(a,t.total_in>>8&255),W(a,t.total_in>>16&255),W(a,t.total_in>>24&255)):(zr(a,t.adler>>>16),zr(a,t.adler&65535)),gn(t),a.wrap>0&&(a.wrap=-a.wrap),a.pending!==0?Tt:cg)}function Wj(t){var e;return!t||!t.state?ht:(e=t.state.status,e!==GA&&e!==Oi&&e!==MA&&e!==KA&&e!==JA&&e!==Sn&&e!==$r?fn(t,ht):(t.state=null,e===Sn?fn(t,Ej):Tt))}function Zj(t,e){var n=e.length,a,r,A,o,i,l,s,c;if(!t||!t.state||(a=t.state,o=a.wrap,o===2||o===1&&a.status!==GA||a.lookahead))return ht;for(o===1&&(t.adler=hg(t.adler,e,n,0)),a.wrap=0,n>=a.w_size&&(o===0&&(dn(a.head),a.strstart=0,a.block_start=0,a.insert=0),c=new qe.Buf8(a.w_size),qe.arraySet(c,e,n-a.w_size,a.w_size,0),e=c,n=a.w_size),i=t.avail_in,l=t.next_in,s=t.input,t.avail_in=n,t.next_in=0,t.input=e,Fn(a);a.lookahead>=$;){r=a.strstart,A=a.lookahead-($-1);do a.ins_h=(a.ins_h<{"use strict";var UA=en(),Cg=!0,mg=!0;try{String.fromCharCode.apply(null,[0])}catch{Cg=!1}try{String.fromCharCode.apply(null,new Uint8Array(1))}catch{mg=!1}var Yr=new UA.Buf8(256);for(nn=0;nn<256;nn++)Yr[nn]=nn>=252?6:nn>=248?5:nn>=240?4:nn>=224?3:nn>=192?2:1;var nn;Yr[254]=Yr[254]=1;dr.string2buf=function(t){var e,n,a,r,A,o=t.length,i=0;for(r=0;r>>6,e[A++]=128|n&63):n<65536?(e[A++]=224|n>>>12,e[A++]=128|n>>>6&63,e[A++]=128|n&63):(e[A++]=240|n>>>18,e[A++]=128|n>>>12&63,e[A++]=128|n>>>6&63,e[A++]=128|n&63);return e};function Ig(t,e){if(e<65537&&(t.subarray&&mg||!t.subarray&&Cg))return String.fromCharCode.apply(null,UA.shrinkBuf(t,e));for(var n="",a=0;a4){i[a++]=65533,n+=A-1;continue}for(r&=A===2?31:A===3?15:7;A>1&&n1){i[a++]=65533;continue}r<65536?i[a++]=r:(r-=65536,i[a++]=55296|r>>10&1023,i[a++]=56320|r&1023)}return Ig(i,a)};dr.utf8border=function(t,e){var n;for(e=e||t.length,e>t.length&&(e=t.length),n=e-1;n>=0&&(t[n]&192)===128;)n--;return n<0||n===0?e:n+Yr[t[n]]>e?n:e}});var zi=k((fk,jg)=>{"use strict";function Xj(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}jg.exports=Xj});var Bg=k(Xr=>{"use strict";var Wr=pg(),Zr=en(),Vi=qi(),Yi=TA(),ev=zi(),Eg=Object.prototype.toString,tv=0,$i=4,ur=0,vg=1,yg=2,nv=-1,rv=0,av=8;function Rn(t){if(!(this instanceof Rn))return new Rn(t);this.options=Zr.assign({level:nv,method:av,chunkSize:16384,windowBits:15,memLevel:8,strategy:rv,to:""},t||{});var e=this.options;e.raw&&e.windowBits>0?e.windowBits=-e.windowBits:e.gzip&&e.windowBits>0&&e.windowBits<16&&(e.windowBits+=16),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new ev,this.strm.avail_out=0;var n=Wr.deflateInit2(this.strm,e.level,e.method,e.windowBits,e.memLevel,e.strategy);if(n!==ur)throw new Error(Yi[n]);if(e.header&&Wr.deflateSetHeader(this.strm,e.header),e.dictionary){var a;if(typeof e.dictionary=="string"?a=Vi.string2buf(e.dictionary):Eg.call(e.dictionary)==="[object ArrayBuffer]"?a=new Uint8Array(e.dictionary):a=e.dictionary,n=Wr.deflateSetDictionary(this.strm,a),n!==ur)throw new Error(Yi[n]);this._dict_set=!0}}Rn.prototype.push=function(t,e){var n=this.strm,a=this.options.chunkSize,r,A;if(this.ended)return!1;A=e===~~e?e:e===!0?$i:tv,typeof t=="string"?n.input=Vi.string2buf(t):Eg.call(t)==="[object ArrayBuffer]"?n.input=new Uint8Array(t):n.input=t,n.next_in=0,n.avail_in=n.input.length;do{if(n.avail_out===0&&(n.output=new Zr.Buf8(a),n.next_out=0,n.avail_out=a),r=Wr.deflate(n,A),r!==vg&&r!==ur)return this.onEnd(r),this.ended=!0,!1;(n.avail_out===0||n.avail_in===0&&(A===$i||A===yg))&&(this.options.to==="string"?this.onData(Vi.buf2binstring(Zr.shrinkBuf(n.output,n.next_out))):this.onData(Zr.shrinkBuf(n.output,n.next_out)))}while((n.avail_in>0||n.avail_out===0)&&r!==vg);return A===$i?(r=Wr.deflateEnd(this.strm),this.onEnd(r),this.ended=!0,r===ur):(A===yg&&(this.onEnd(ur),n.avail_out=0),!0)};Rn.prototype.onData=function(t){this.chunks.push(t)};Rn.prototype.onEnd=function(t){t===ur&&(this.options.to==="string"?this.result=this.chunks.join(""):this.result=Zr.flattenChunks(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg};function Wi(t,e){var n=new Rn(e);if(n.push(t,!0),n.err)throw n.msg||Yi[n.err];return n.result}function Av(t,e){return e=e||{},e.raw=!0,Wi(t,e)}function ov(t,e){return e=e||{},e.gzip=!0,Wi(t,e)}Xr.Deflate=Rn;Xr.deflate=Wi;Xr.deflateRaw=Av;Xr.gzip=ov});var wg=k((Ck,Qg)=>{"use strict";var OA=30,iv=12;Qg.exports=function(e,n){var a,r,A,o,i,l,s,c,h,_,f,m,u,g,p,I,j,E,y,b,F,T,Q,w,S;a=e.state,r=e.next_in,w=e.input,A=r+(e.avail_in-5),o=e.next_out,S=e.output,i=o-(n-e.avail_out),l=o+(e.avail_out-257),s=a.dmax,c=a.wsize,h=a.whave,_=a.wnext,f=a.window,m=a.hold,u=a.bits,g=a.lencode,p=a.distcode,I=(1<>>24,m>>>=y,u-=y,y=E>>>16&255,y===0)S[o++]=E&65535;else if(y&16){b=E&65535,y&=15,y&&(u>>=y,u-=y),u<15&&(m+=w[r++]<>>24,m>>>=y,u-=y,y=E>>>16&255,y&16){if(F=E&65535,y&=15,us){e.msg="invalid distance too far back",a.mode=OA;break e}if(m>>>=y,u-=y,y=o-i,F>y){if(y=F-y,y>h&&a.sane){e.msg="invalid distance too far back",a.mode=OA;break e}if(T=0,Q=f,_===0){if(T+=c-y,y2;)S[o++]=Q[T++],S[o++]=Q[T++],S[o++]=Q[T++],b-=3;b&&(S[o++]=Q[T++],b>1&&(S[o++]=Q[T++]))}else{T=o-F;do S[o++]=S[T++],S[o++]=S[T++],S[o++]=S[T++],b-=3;while(b>2);b&&(S[o++]=S[T++],b>1&&(S[o++]=S[T++]))}}else if((y&64)===0){E=p[(E&65535)+(m&(1<>3,r-=b,u-=b<<3,m&=(1<{"use strict";var bg=en(),fr=15,xg=852,kg=592,Sg=0,Zi=1,Ng=2,lv=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,0,0],sv=[16,16,16,16,16,16,16,16,17,17,17,17,18,18,18,18,19,19,19,19,20,20,20,20,21,21,21,21,16,72,78],cv=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,0,0],_v=[16,16,16,16,17,17,18,18,19,19,20,20,21,21,22,22,23,23,24,24,25,25,26,26,27,27,28,28,29,29,64,64];Fg.exports=function(e,n,a,r,A,o,i,l){var s=l.bits,c=0,h=0,_=0,f=0,m=0,u=0,g=0,p=0,I=0,j=0,E,y,b,F,T,Q=null,w=0,S,te=new bg.Buf16(fr+1),at=new bg.Buf16(fr+1),At=null,Ct=0,on,Qt,Pt;for(c=0;c<=fr;c++)te[c]=0;for(h=0;h=1&&te[f]===0;f--);if(m>f&&(m=f),f===0)return A[o++]=1<<24|64<<16|0,A[o++]=1<<24|64<<16|0,l.bits=1,0;for(_=1;_0&&(e===Sg||f!==1))return-1;for(at[1]=0,c=1;cxg||e===Ng&&I>kg)return 1;for(;;){on=c-g,i[h]S?(Qt=At[Ct+i[h]],Pt=Q[w+i[h]]):(Qt=96,Pt=0),E=1<>g)+y]=on<<24|Qt<<16|Pt|0;while(y!==0);for(E=1<>=1;if(E!==0?(j&=E-1,j+=E):j=0,h++,--te[c]===0){if(c===f)break;c=n[a+i[h]]}if(c>m&&(j&F)!==b){for(g===0&&(g=m),T+=_,u=c-g,p=1<xg||e===Ng&&I>kg)return 1;b=j&F,A[b]=m<<24|u<<16|T-o|0}}return j!==0&&(A[T+j]=c-g<<24|64<<16|0),l.bits=m,0}});var ud=k(vt=>{"use strict";var nt=en(),al=Ki(),Mt=Ji(),hv=wg(),ea=Dg(),gv=0,od=1,id=2,Rg=4,dv=5,qA=6,Ln=0,uv=1,fv=2,gt=-2,ld=-3,Al=-4,pv=-5,Lg=8,sd=1,Tg=2,Hg=3,Mg=4,Kg=5,Jg=6,Pg=7,Gg=8,Ug=9,Og=10,VA=11,rn=12,Xi=13,qg=14,el=15,zg=16,$g=17,Vg=18,Yg=19,zA=20,$A=21,Wg=22,Zg=23,Xg=24,ed=25,td=26,tl=27,nd=28,rd=29,ce=30,ol=31,Cv=32,mv=852,Iv=592,jv=15,vv=jv;function ad(t){return(t>>>24&255)+(t>>>8&65280)+((t&65280)<<8)+((t&255)<<24)}function yv(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new nt.Buf16(320),this.work=new nt.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function cd(t){var e;return!t||!t.state?gt:(e=t.state,t.total_in=t.total_out=e.total=0,t.msg="",e.wrap&&(t.adler=e.wrap&1),e.mode=sd,e.last=0,e.havedict=0,e.dmax=32768,e.head=null,e.hold=0,e.bits=0,e.lencode=e.lendyn=new nt.Buf32(mv),e.distcode=e.distdyn=new nt.Buf32(Iv),e.sane=1,e.back=-1,Ln)}function _d(t){var e;return!t||!t.state?gt:(e=t.state,e.wsize=0,e.whave=0,e.wnext=0,cd(t))}function hd(t,e){var n,a;return!t||!t.state||(a=t.state,e<0?(n=0,e=-e):(n=(e>>4)+1,e<48&&(e&=15)),e&&(e<8||e>15))?gt:(a.window!==null&&a.wbits!==e&&(a.window=null),a.wrap=n,a.wbits=e,_d(t))}function gd(t,e){var n,a;return t?(a=new yv,t.state=a,a.window=null,n=hd(t,e),n!==Ln&&(t.state=null),n):gt}function Ev(t){return gd(t,vv)}var Ad=!0,nl,rl;function Bv(t){if(Ad){var e;for(nl=new nt.Buf32(512),rl=new nt.Buf32(32),e=0;e<144;)t.lens[e++]=8;for(;e<256;)t.lens[e++]=9;for(;e<280;)t.lens[e++]=7;for(;e<288;)t.lens[e++]=8;for(ea(od,t.lens,0,288,nl,0,t.work,{bits:9}),e=0;e<32;)t.lens[e++]=5;ea(id,t.lens,0,32,rl,0,t.work,{bits:5}),Ad=!1}t.lencode=nl,t.lenbits=9,t.distcode=rl,t.distbits=5}function dd(t,e,n,a){var r,A=t.state;return A.window===null&&(A.wsize=1<=A.wsize?(nt.arraySet(A.window,e,n-A.wsize,A.wsize,0),A.wnext=0,A.whave=A.wsize):(r=A.wsize-A.wnext,r>a&&(r=a),nt.arraySet(A.window,e,n-a,r,A.wnext),a-=r,a?(nt.arraySet(A.window,e,n-a,a,0),A.wnext=a,A.whave=A.wsize):(A.wnext+=r,A.wnext===A.wsize&&(A.wnext=0),A.whave>>8&255,n.check=Mt(n.check,Q,2,0),s=0,c=0,n.mode=Tg;break}if(n.flags=0,n.head&&(n.head.done=!1),!(n.wrap&1)||(((s&255)<<8)+(s>>8))%31){t.msg="incorrect header check",n.mode=ce;break}if((s&15)!==Lg){t.msg="unknown compression method",n.mode=ce;break}if(s>>>=4,c-=4,F=(s&15)+8,n.wbits===0)n.wbits=F;else if(F>n.wbits){t.msg="invalid window size",n.mode=ce;break}n.dmax=1<>8&1),n.flags&512&&(Q[0]=s&255,Q[1]=s>>>8&255,n.check=Mt(n.check,Q,2,0)),s=0,c=0,n.mode=Hg;case Hg:for(;c<32;){if(i===0)break e;i--,s+=a[A++]<>>8&255,Q[2]=s>>>16&255,Q[3]=s>>>24&255,n.check=Mt(n.check,Q,4,0)),s=0,c=0,n.mode=Mg;case Mg:for(;c<16;){if(i===0)break e;i--,s+=a[A++]<>8),n.flags&512&&(Q[0]=s&255,Q[1]=s>>>8&255,n.check=Mt(n.check,Q,2,0)),s=0,c=0,n.mode=Kg;case Kg:if(n.flags&1024){for(;c<16;){if(i===0)break e;i--,s+=a[A++]<>>8&255,n.check=Mt(n.check,Q,2,0)),s=0,c=0}else n.head&&(n.head.extra=null);n.mode=Jg;case Jg:if(n.flags&1024&&(f=n.length,f>i&&(f=i),f&&(n.head&&(F=n.head.extra_len-n.length,n.head.extra||(n.head.extra=new Array(n.head.extra_len)),nt.arraySet(n.head.extra,a,A,f,F)),n.flags&512&&(n.check=Mt(n.check,a,f,A)),i-=f,A+=f,n.length-=f),n.length))break e;n.length=0,n.mode=Pg;case Pg:if(n.flags&2048){if(i===0)break e;f=0;do F=a[A+f++],n.head&&F&&n.length<65536&&(n.head.name+=String.fromCharCode(F));while(F&&f>9&1,n.head.done=!0),t.adler=n.check=0,n.mode=rn;break;case Og:for(;c<32;){if(i===0)break e;i--,s+=a[A++]<>>=c&7,c-=c&7,n.mode=tl;break}for(;c<3;){if(i===0)break e;i--,s+=a[A++]<>>=1,c-=1,s&3){case 0:n.mode=qg;break;case 1:if(Bv(n),n.mode=zA,e===qA){s>>>=2,c-=2;break e}break;case 2:n.mode=$g;break;case 3:t.msg="invalid block type",n.mode=ce}s>>>=2,c-=2;break;case qg:for(s>>>=c&7,c-=c&7;c<32;){if(i===0)break e;i--,s+=a[A++]<>>16^65535)){t.msg="invalid stored block lengths",n.mode=ce;break}if(n.length=s&65535,s=0,c=0,n.mode=el,e===qA)break e;case el:n.mode=zg;case zg:if(f=n.length,f){if(f>i&&(f=i),f>l&&(f=l),f===0)break e;nt.arraySet(r,a,A,f,o),i-=f,A+=f,l-=f,o+=f,n.length-=f;break}n.mode=rn;break;case $g:for(;c<14;){if(i===0)break e;i--,s+=a[A++]<>>=5,c-=5,n.ndist=(s&31)+1,s>>>=5,c-=5,n.ncode=(s&15)+4,s>>>=4,c-=4,n.nlen>286||n.ndist>30){t.msg="too many length or distance symbols",n.mode=ce;break}n.have=0,n.mode=Vg;case Vg:for(;n.have>>=3,c-=3}for(;n.have<19;)n.lens[te[n.have++]]=0;if(n.lencode=n.lendyn,n.lenbits=7,w={bits:n.lenbits},T=ea(gv,n.lens,0,19,n.lencode,0,n.work,w),n.lenbits=w.bits,T){t.msg="invalid code lengths set",n.mode=ce;break}n.have=0,n.mode=Yg;case Yg:for(;n.have>>24,I=g>>>16&255,j=g&65535,!(p<=c);){if(i===0)break e;i--,s+=a[A++]<>>=p,c-=p,n.lens[n.have++]=j;else{if(j===16){for(S=p+2;c>>=p,c-=p,n.have===0){t.msg="invalid bit length repeat",n.mode=ce;break}F=n.lens[n.have-1],f=3+(s&3),s>>>=2,c-=2}else if(j===17){for(S=p+3;c>>=p,c-=p,F=0,f=3+(s&7),s>>>=3,c-=3}else{for(S=p+7;c>>=p,c-=p,F=0,f=11+(s&127),s>>>=7,c-=7}if(n.have+f>n.nlen+n.ndist){t.msg="invalid bit length repeat",n.mode=ce;break}for(;f--;)n.lens[n.have++]=F}}if(n.mode===ce)break;if(n.lens[256]===0){t.msg="invalid code -- missing end-of-block",n.mode=ce;break}if(n.lenbits=9,w={bits:n.lenbits},T=ea(od,n.lens,0,n.nlen,n.lencode,0,n.work,w),n.lenbits=w.bits,T){t.msg="invalid literal/lengths set",n.mode=ce;break}if(n.distbits=6,n.distcode=n.distdyn,w={bits:n.distbits},T=ea(id,n.lens,n.nlen,n.ndist,n.distcode,0,n.work,w),n.distbits=w.bits,T){t.msg="invalid distances set",n.mode=ce;break}if(n.mode=zA,e===qA)break e;case zA:n.mode=$A;case $A:if(i>=6&&l>=258){t.next_out=o,t.avail_out=l,t.next_in=A,t.avail_in=i,n.hold=s,n.bits=c,hv(t,_),o=t.next_out,r=t.output,l=t.avail_out,A=t.next_in,a=t.input,i=t.avail_in,s=n.hold,c=n.bits,n.mode===rn&&(n.back=-1);break}for(n.back=0;g=n.lencode[s&(1<>>24,I=g>>>16&255,j=g&65535,!(p<=c);){if(i===0)break e;i--,s+=a[A++]<>E)],p=g>>>24,I=g>>>16&255,j=g&65535,!(E+p<=c);){if(i===0)break e;i--,s+=a[A++]<>>=E,c-=E,n.back+=E}if(s>>>=p,c-=p,n.back+=p,n.length=j,I===0){n.mode=td;break}if(I&32){n.back=-1,n.mode=rn;break}if(I&64){t.msg="invalid literal/length code",n.mode=ce;break}n.extra=I&15,n.mode=Wg;case Wg:if(n.extra){for(S=n.extra;c>>=n.extra,c-=n.extra,n.back+=n.extra}n.was=n.length,n.mode=Zg;case Zg:for(;g=n.distcode[s&(1<>>24,I=g>>>16&255,j=g&65535,!(p<=c);){if(i===0)break e;i--,s+=a[A++]<>E)],p=g>>>24,I=g>>>16&255,j=g&65535,!(E+p<=c);){if(i===0)break e;i--,s+=a[A++]<>>=E,c-=E,n.back+=E}if(s>>>=p,c-=p,n.back+=p,I&64){t.msg="invalid distance code",n.mode=ce;break}n.offset=j,n.extra=I&15,n.mode=Xg;case Xg:if(n.extra){for(S=n.extra;c>>=n.extra,c-=n.extra,n.back+=n.extra}if(n.offset>n.dmax){t.msg="invalid distance too far back",n.mode=ce;break}n.mode=ed;case ed:if(l===0)break e;if(f=_-l,n.offset>f){if(f=n.offset-f,f>n.whave&&n.sane){t.msg="invalid distance too far back",n.mode=ce;break}f>n.wnext?(f-=n.wnext,m=n.wsize-f):m=n.wnext-f,f>n.length&&(f=n.length),u=n.window}else u=r,m=o-n.offset,f=n.length;f>l&&(f=l),l-=f,n.length-=f;do r[o++]=u[m++];while(--f);n.length===0&&(n.mode=$A);break;case td:if(l===0)break e;r[o++]=n.length,l--,n.mode=$A;break;case tl:if(n.wrap){for(;c<32;){if(i===0)break e;i--,s|=a[A++]<{"use strict";fd.exports={Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_TREES:6,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_BUF_ERROR:-5,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,Z_BINARY:0,Z_TEXT:1,Z_UNKNOWN:2,Z_DEFLATED:8}});var Cd=k((vk,pd)=>{"use strict";function kv(){this.text=0,this.time=0,this.xflags=0,this.os=0,this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=!1}pd.exports=kv});var jd=k(ra=>{"use strict";var ta=ud(),na=en(),YA=qi(),ve=il(),Id=TA(),Sv=zi(),Nv=Cd(),md=Object.prototype.toString;function Tn(t){if(!(this instanceof Tn))return new Tn(t);this.options=na.assign({chunkSize:16384,windowBits:0,to:""},t||{});var e=this.options;e.raw&&e.windowBits>=0&&e.windowBits<16&&(e.windowBits=-e.windowBits,e.windowBits===0&&(e.windowBits=-15)),e.windowBits>=0&&e.windowBits<16&&!(t&&t.windowBits)&&(e.windowBits+=32),e.windowBits>15&&e.windowBits<48&&(e.windowBits&15)===0&&(e.windowBits|=15),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new Sv,this.strm.avail_out=0;var n=ta.inflateInit2(this.strm,e.windowBits);if(n!==ve.Z_OK)throw new Error(Id[n]);this.header=new Nv,ta.inflateGetHeader(this.strm,this.header)}Tn.prototype.push=function(t,e){var n=this.strm,a=this.options.chunkSize,r=this.options.dictionary,A,o,i,l,s,c,h=!1;if(this.ended)return!1;o=e===~~e?e:e===!0?ve.Z_FINISH:ve.Z_NO_FLUSH,typeof t=="string"?n.input=YA.binstring2buf(t):md.call(t)==="[object ArrayBuffer]"?n.input=new Uint8Array(t):n.input=t,n.next_in=0,n.avail_in=n.input.length;do{if(n.avail_out===0&&(n.output=new na.Buf8(a),n.next_out=0,n.avail_out=a),A=ta.inflate(n,ve.Z_NO_FLUSH),A===ve.Z_NEED_DICT&&r&&(typeof r=="string"?c=YA.string2buf(r):md.call(r)==="[object ArrayBuffer]"?c=new Uint8Array(r):c=r,A=ta.inflateSetDictionary(this.strm,c)),A===ve.Z_BUF_ERROR&&h===!0&&(A=ve.Z_OK,h=!1),A!==ve.Z_STREAM_END&&A!==ve.Z_OK)return this.onEnd(A),this.ended=!0,!1;n.next_out&&(n.avail_out===0||A===ve.Z_STREAM_END||n.avail_in===0&&(o===ve.Z_FINISH||o===ve.Z_SYNC_FLUSH))&&(this.options.to==="string"?(i=YA.utf8border(n.output,n.next_out),l=n.next_out-i,s=YA.buf2string(n.output,i),n.next_out=l,n.avail_out=a-l,l&&na.arraySet(n.output,n.output,i,l,0),this.onData(s)):this.onData(na.shrinkBuf(n.output,n.next_out))),n.avail_in===0&&n.avail_out===0&&(h=!0)}while((n.avail_in>0||n.avail_out===0)&&A!==ve.Z_STREAM_END);return A===ve.Z_STREAM_END&&(o=ve.Z_FINISH),o===ve.Z_FINISH?(A=ta.inflateEnd(this.strm),this.onEnd(A),this.ended=!0,A===ve.Z_OK):(o===ve.Z_SYNC_FLUSH&&(this.onEnd(ve.Z_OK),n.avail_out=0),!0)};Tn.prototype.onData=function(t){this.chunks.push(t)};Tn.prototype.onEnd=function(t){t===ve.Z_OK&&(this.options.to==="string"?this.result=this.chunks.join(""):this.result=na.flattenChunks(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg};function ll(t,e){var n=new Tn(e);if(n.push(t,!0),n.err)throw n.msg||Id[n.err];return n.result}function Fv(t,e){return e=e||{},e.raw=!0,ll(t,e)}ra.Inflate=Tn;ra.inflate=ll;ra.inflateRaw=Fv;ra.ungzip=ll});var Ed=k((Ek,yd)=>{"use strict";var Dv=en().assign,Rv=Bg(),Lv=jd(),Tv=il(),vd={};Dv(vd,Rv,Lv,Tv);yd.exports=vd});var Bd=k(WA=>{(function(t){var e=String.fromCharCode;function n(a,r,A){for(var o=a[r],i=1,l=0,s=0;s=32&&o<=126?"Unexpected character "+e(o)+" in JSON at position "+r+" (line "+i+", column "+l+")":"Unexpected byte 0x"+o.toString(16)+" in JSON at position "+r+" (line "+i+", column "+l+")"))}t.JSON_parse=function(a){if(!(a instanceof Uint8Array))throw new Error("JSON input must be a Uint8Array");for(var r=[],A=[],o=[],i=a.length,l=null,s=0,c,h=0;h=i&&n(a,i),_=a[h++],_!==34;)if(_===92)switch(a[h++]){case 34:f+='"';break;case 47:f+="/";break;case 92:f+="\\";break;case 98:f+="\b";break;case 102:f+="\f";break;case 110:f+=` +`;break;case 114:f+="\r";break;case 116:f+=" ";break;case 117:{for(var u=0,g=0;g<4;g++)_=a[h++],u<<=4,_>=48&&_<=57?u|=_-48:_>=97&&_<=102?u|=_+-87:_>=65&&_<=70?u|=_+-55:n(a,--h);f+=e(u);break}default:n(a,--h);break}else if(_<=127)f+=e(_);else if((_&224)===192)f+=e((_&31)<<6|a[h++]&63);else if((_&240)===224)f+=e((_&15)<<12|(a[h++]&63)<<6|a[h++]&63);else if((_&248)==240){var p=(_&7)<<18|(a[h++]&63)<<12|(a[h++]&63)<<6|a[h++]&63;p>65535&&(p-=65536,f+=e(p>>10&1023|55296),p=56320|p&1023),f+=e(p)}f[0];break}case 91:{f=[],r.push(l),A.push(c),o.push(s),l=null,c=f,s=1;continue}case 123:{f={},r.push(l),A.push(c),o.push(s),l=null,c=f,s=2;continue}case 93:{s!==1&&n(a,--h),f=c,l=r.pop(),c=A.pop(),s=o.pop();break}case 125:{s!==2&&n(a,--h),f=c,l=r.pop(),c=A.pop(),s=o.pop();break}default:n(a,--h)}for(_=a[h];_<=32;)_=a[++h];switch(s){case 0:{if(h===i)return f;break}case 1:{if(c.push(f),_===44){h++;continue}if(_===93)continue;break}case 2:{if(l===null){if(l=f,_===58){h++;continue}}else{if(c[l]=f,l=null,_===44){h++;continue}if(_===125)continue}break}}break}}n(a,h)}})(WA)});function bd(t){return t=t.trim(),t[0]==="["&&(t=t.replace(/,\s*$/,""),t[t.length-1]!=="]"&&(t+="]")),JSON.parse(t)}function Hv(t){let e=0;for(let n=0;n0&&/\s/.exec(String.fromCharCode(t[n-1]));)n--;if(String.fromCharCode(t[n-1])===","&&n--,String.fromCharCode(t[n-1])!=="]"){let a=new Uint8Array(n+1);a.set(t.subarray(0,n)),a[n]=93,t=a}}return(0,wd.JSON_parse)(t)}var Qd,wd,ZA,sl,cl,XA,an,_l=re(()=>{"use strict";Qd=he(Ed()),wd=he(Bd()),ZA=1<<27;sl=class{constructor(e){this.chunks=[];let n=this.byteArray=new Uint8Array(e),a="utf-8";if(n.length>2&&(n[0]===255&&n[1]===254?a="utf-16le":n[0]===254&&n[1]===255&&(a="utf-16be")),typeof TextDecoder<"u"){let r=new TextDecoder(a);for(let A=0;A=ZA&&this.chunks.push("")}}splitLines(){let e=function*(){let n="";for(let a of this.chunks){let r=a.split(` +`);for(let A=0;A{try{return Qd.inflate(new Uint8Array(a)).buffer}catch{return a}})}async name(){return await this.namePromise}async readAsArrayBuffer(){return await this.uncompressedData}async readAsText(){let e=await this.readAsArrayBuffer();return new sl(e)}static fromFile(e){let n=new Promise(a=>{let r=new FileReader;r.addEventListener("loadend",()=>{if(!(r.result instanceof ArrayBuffer))throw new Error("Expected reader.result to be an instance of ArrayBuffer");a(r.result)}),r.readAsArrayBuffer(e)});return new t(Promise.resolve(e.name),n)}static fromArrayBuffer(e,n){return new t(Promise.resolve(e),Promise.resolve(n))}}});function Mv(t){let e=[...t.splitLines()].map(A=>A.split(" ")),n=e.shift();if(!n)return[];let a=new Map;for(let A=0;A0;){let o=a.pop();r=Math.max(r,o.endValue),e.leaveFrame(o,r)}return"Bytes Used"in n[0]?e.setValueFormatter(new $e):("Weight"in n[0]||"Running Time"in n[0])&&e.setValueFormatter(new ee("milliseconds")),e.build()}async function kd(t){let e={name:t.name,files:new Map,subdirectories:new Map},n=await new Promise((a,r)=>{t.createReader().readEntries(A=>{a(A)},r)});for(let a of n)if(a.isDirectory){let r=await kd(a);e.subdirectories.set(r.name,r)}else{let r=await new Promise((A,o)=>{a.file(A,o)});e.files.set(r.name,r)}return e}function eo(t){return an.fromFile(t).readAsArrayBuffer()}function Jv(t){return an.fromFile(t).readAsText()}function Pv(t,e){let n=et(t.subdirectories,"corespace"),a=et(n.subdirectories,`run${e}`);return et(a.subdirectories,"core")}async function Gv(t){let e=et(t.subdirectories,"stores");for(let n of e.subdirectories.values()){let a=n.files.get("schema.xml");if(!a)continue;let r=await Jv(a);if(!/name="time-profile"/.exec(r.firstChunk()))continue;let A=new aa(await eo(et(n.files,"bulkstore")));A.readUint32(),A.readUint32(),A.readUint32();let o=A.readUint32(),i=A.readUint32();A.seek(o);let l=[];for(;;){let s=A.readUint48();if(s===0)break;let c=A.readUint32();A.skip(i-6-4-4);let h=A.readUint32();l.push({timestamp:s,threadID:c,backtraceID:h})}return l}throw new Error("Could not find sample list")}async function Uv(t,e){let n=et(e.subdirectories,"uniquing"),a=et(n.subdirectories,"arrayUniquer"),r=et(a.files,"integeruniquer.index"),A=et(a.files,"integeruniquer.data"),o=new aa(await eo(r)),i=new aa(await eo(A));o.seek(32);let l=[];for(;o.hasMore();){let s=o.readUint32()+o.readUint32()*1048576;if(s===0)continue;i.seek(s);let c=i.readUint32(),h=[];for(;c--;)h.push(i.readUint64());l.push(h)}return l}async function Ov(t){let e=et(t.files,"form.template"),n=$v(await eo(e)),a=n["com.apple.xray.owner.template.version"],r=1;"com.apple.xray.owner.template"in n&&(r=n["com.apple.xray.owner.template"].get("_selectedRunNumber"));let A=n.$1;"stubInfoByUUID"in n&&(A=Array.from(n.stubInfoByUUID.keys())[0]);let o=n["com.apple.xray.run.data"],i=[];for(let l of o.runNumbers){let s=et(o.runData,l),c=et(s,"symbolsByPid"),h=new Map;for(let _ of c.values()){for(let f of _.symbols){if(!f)continue;let{sourcePath:m,symbolName:u,addressToLine:g}=f;for(let p of g.keys())De(h,p,()=>{let I=u||`0x${ln(p.toString(16),16)}`,j={key:`${m}:${I}`,name:I};return m&&(j.file=m),j})}i.push({number:l,addressToFrameMap:h})}}return{version:a,instrument:A,selectedRunNumber:r,runs:i}}async function Sd(t){let e=await kd(t),{version:n,runs:a,instrument:r,selectedRunNumber:A}=await Ov(e);if(r!=="com.apple.xray.instrument-type.coresampler2")throw new Error(`The only supported instrument from .trace import is "com.apple.xray.instrument-type.coresampler2". Got ${r}`);console.log("version: ",n),console.log("Importing time profile");let o=[],i=0;for(let l of a){let{addressToFrameMap:s,number:c}=l,h=await qv({fileName:t.name,tree:e,addressToFrameMap:s,runNumber:c});l.number===A&&(i=o.length+h.indexToView),o.push(...h.profiles)}return{name:t.name,indexToView:i,profiles:o}}async function qv(t){let{fileName:e,tree:n,addressToFrameMap:a,runNumber:r}=t,A=Pv(n,r),o=await Gv(A),i=await Uv(o,A),l=new Map;for(let h of o)l.set(h.threadID,ja(l,h.threadID,()=>0)+1);let s=Array.from(l.entries());Fe(s,h=>-h[1]);let c=s.map(h=>h[0]);return{name:e,indexToView:0,profiles:c.map(h=>zv({threadID:h,fileName:e,arrays:i,addressToFrameMap:a,samples:o}))}}function zv(t){let{fileName:e,addressToFrameMap:n,arrays:a,threadID:r,samples:A}=t,o=new Map;A=A.filter(c=>c.threadID===r);let i=new ae(Ae(A).timestamp);i.setName(`${e} - thread ${r}`);function l(c,h){let _=n.get(c);if(_)h.push(_);else if(c in a)for(let f of a[c])l(f,h);else{let f={key:c,name:`0x${ln(c.toString(16),16)}`};n.set(c,f),h.push(f)}}let s=null;for(let c of A){let h=De(o,c.backtraceID,_=>{let f=[];return l(_,f),f.reverse(),f});if(s===null&&(i.appendSampleWithWeight([],c.timestamp),s=c.timestamp),c.timestamp{switch(r){case"NSTextStorage":case"NSParagraphStyle":case"NSFont":return null;case"PFTSymbolData":{let o=Object.create(null);o.symbolName=A.$0,o.sourcePath=A.$1,o.addressToLine=new Map;for(let i=3;;i+=2){let l=A["$"+i],s=A["$"+(i+1)];if(l==null||s==null)break;o.addressToLine.set(l,s)}return o}case"PFTOwnerData":{let o=Object.create(null);return o.ownerName=A.$0,o.ownerPath=A.$1,o}case"PFTPersistentSymbols":{let o=Object.create(null),i=A.$4;o.threadNames=A.$3,o.symbols=[];for(let l=1;ln){if(t.$version!==1e5||t.$archiver!=="NSKeyedArchiver"||!hl(t.$top)||!xd(t.$objects))throw new Error("Invalid keyed archive");t.$objects[0]==="$null"&&(t.$objects[0]=null);for(let a=0;a{if(a instanceof Aa)return t.$objects[a.index];if(xd(a))for(let r=0;ra){if(hl(e)&&e.$class){let a=Yv(t,e.$class).$classname;switch(a){case"NSDecimalNumberPlaceholder":{let i=e["NS.length"],l=e["NS.exponent"],s=e["NS.mantissa.bo"],c=e["NS.negative"],h=new Uint16Array(new Uint8Array(e["NS.mantissa"]).buffer),_=0;for(let f=0;f>8|(m&255)<<8),_+=m*Math.pow(65536,f)}return _*=Math.pow(10,l),c?-_:_}case"NSData":case"NSMutableData":return e["NS.bytes"]||e["NS.data"];case"NSString":case"NSMutableString":return e["NS.string"]?e["NS.string"]:e["NS.bytes"]?Vv(e["NS.bytes"]):(console.warn(`Unexpected ${a} format: `,e),null);case"NSArray":case"NSMutableArray":if("NS.objects"in e)return e["NS.objects"];let r=[];for(;;){let i="NS.object."+r.length;if(!(i in e))break;r.push(e[i])}return r;case"_NSKeyedCoderOldStyleArray":{let i=e["NS.count"],l=[];for(let s=0;s{"use strict";ke();V();Ke();_l();aa=class{constructor(e){this.bytePos=0;this.view=new DataView(e)}seek(e){this.bytePos=e}skip(e){this.bytePos+=e}hasMore(){return this.bytePosthis.view.byteLength?0:this.view.getUint8(this.bytePos-1)}readUint32(){return this.bytePos+=4,this.bytePos>this.view.byteLength?0:this.view.getUint32(this.bytePos-4,!0)}readUint48(){return this.bytePos+=6,this.bytePos>this.view.byteLength?0:this.view.getUint32(this.bytePos-6,!0)+this.view.getUint16(this.bytePos-2,!0)*Math.pow(2,32)}readUint64(){return this.bytePos+=8,this.bytePos>this.view.byteLength?0:this.view.getUint32(this.bytePos-8,!0)+this.view.getUint32(this.bytePos-4,!0)*Math.pow(2,32)}};Aa=class{constructor(e){this.index=e}};gl=class{constructor(e){this.view=e;this.referenceSize=0;this.objects=[];this.offsetTable=[]}parseRoot(){let e=this.view.byteLength-32,n=this.view.getUint8(e+6);this.referenceSize=this.view.getUint8(e+7);let a=this.view.getUint32(e+12,!1),r=this.view.getUint32(e+20,!1),A=this.view.getUint32(e+28,!1);for(let o=0;o>4){case 0:return this.parseSingleton(e,a);case 1:return this.parseInteger(e,1<({key:o,name:o})),duration:parseInt(A,10)})}return e}function ul(t){let e=ey(t),n=e.reduce((r,A)=>r+A.duration,0),a=new ae(n);if(e.length===0)return null;for(let r of e)a.appendSampleWithWeight(r.stack,r.duration);return a.build()}var Fd=re(()=>{"use strict";ke()});function Dd(t){let e=t.profile,n=e.threads.length===1?e.threads[0]:e.threads.filter(i=>i.name==="GeckoMain")[0],a=new Map;function r(i){let l=i[0],s=[];for(;l!=null;){let c=n.stackTable.data[l],[h,_]=c;s.push(_),l=h}return s.reverse(),s.map(c=>{let h=n.frameTable.data[c],_=n.stringTable[h[0]],f=/(.*)\s+\((.*?)(?::(\d+))?(?::(\d+))?\)$/.exec(_);return!f||f[2].startsWith("resource:")||f[2]==="self-hosted"||f[2].startsWith("self-hosted:")?null:De(a,_,()=>({key:_,name:f[1],file:f[2],line:f[3]?parseInt(f[3]):void 0,col:f[4]?parseInt(f[4])+1:void 0}))}).filter(c=>c!=null)}let A=new Ie(t.duration),o=[];for(let i of n.samples.data){let l=r(i),s=i[1],c=-1;for(let h=0;hc;h--)A.leaveFrame(o[h],s);for(let h=c+1;h{"use strict";ke();V();Ke()});function ty(t,e){if(!t||!t.type)return{key:"(unknown type)",name:"(unknown type)"};let n=t.name;switch(t.type){case"CPP":{let a=n.match(/[tT] ([^(<]*)/);a&&(n=`(c++) ${a[1]}`);break}case"SHARED_LIB":n="(LIB) "+n;break;case"JS":{let a=n.match(/([a-zA-Z0-9\._\-$]*) ([a-zA-Z0-9\.\-_\/$]*):(\d+):(\d+)/);if(a){let r=a[2],A=parseInt(a[3],10),o=parseInt(a[4],10),i=a[1].length>0?a[1]:r?`(anonymous ${r.split("/").pop()}:${A})`:"(anonymous)";return{key:n,name:i,file:r.length>0?r:"(unknown file)",line:A,col:o}}break}case"CODE":{switch(t.kind){case"LoadIC":case"StoreIC":case"KeyedStoreIC":case"KeyedLoadIC":case"LoadGlobalIC":case"Handler":n="(IC) "+n;break;case"BytecodeHandler":n="(bytecode) ~"+n;break;case"Stub":n="(stub) "+n;break;case"Builtin":n="(builtin) "+n;break;case"RegExp":n="(regexp) "+n;break}break}default:{n=`(${t.type}) ${n}`;break}}return{key:n,name:n}}function fl(t){let e=new ae,n=new Map;function a(A){return De(n,A,o=>{let i=t.code[o];return ty(i,t)})}let r=0;Fe(t.ticks,A=>A.tm);for(let A of t.ticks){let o=[];for(let i=A.s.length-2;i>=0;i-=2){let l=A.s[i];if(l!==-1){if(l>t.code.length){o.push({key:l,name:`0x${l.toString(16)}`});continue}o.push(a(l))}}e.appendSampleWithWeight(o,A.tm-r),r=A.tm}return e.setValueFormatter(new ee("microseconds")),e.build()}var Ld=re(()=>{"use strict";ke();V();Ke()});function*ny(t){let e=[];for(let n of t.splitLines())n===""?(yield Td(e),e=[]):e.push(n);e.length>0&&(yield Td(e))}function Td(t){let e=t.filter(i=>!/^\s*#/.exec(i)),n={command:null,processID:null,threadID:null,time:null,eventType:"",stack:[]},a=e.shift();if(!a)return null;let r=/^(\S.+?)\s+(\d+)(?:\/?(\d+))?\s+/.exec(a);if(!r)return null;n.command=r[1],r[3]?(n.processID=parseInt(r[2],10),n.threadID=parseInt(r[3],10)):n.threadID=parseInt(r[2],10);let A=/\s+(\d+\.\d+):\s+/.exec(a);A&&(n.time=parseFloat(A[1]));let o=/(\S+):\s*$/.exec(a);o&&(n.eventType=o[1]);for(let i of e){let l=/^\s*(\w+)\s*(.+) \((\S*)\)/.exec(i);if(!l)continue;let[,s,c,h]=l;c=c.replace(/\+0x[\da-f]+$/,""),n.stack.push({address:`0x${s}`,symbolName:c,file:h})}return n.stack.reverse(),n}function pl(t){let e=new Map,n=null;for(let a of ny(t)){if(a==null||n!=null&&n!=a.eventType||a.time==null)continue;n=a.eventType;let r=[];a.command&&r.push(a.command),a.processID&&r.push(`pid: ${a.processID}`),a.threadID&&r.push(`tid: ${a.threadID}`);let A=r.join(" ");De(e,A,()=>{let l=new ae;return l.setName(A),l.setValueFormatter(new ee("seconds")),l}).appendSampleWithTimestamp(a.stack.map(({symbolName:l,file:s})=>({key:`${l} (${s})`,name:l==="[unknown]"?`??? (${s})`:l,file:s})),a.time)}return e.size===0?null:{name:e.size===1?Array.from(e.keys())[0]:"",indexToView:0,profiles:Array.from(os(e.values(),a=>a.build()))}}var Hd=re(()=>{"use strict";ke();V();Ke()});function Cl(t,e,n,a,r){if(t.ticks===0&&t.entries===0&&t.alloc===0&&t.children.length===0)return e;let A=e,o=a.get(t.id);n.enterFrame(o,A);for(let i of t.children)A=Cl(i,A,n,a,r);return A+=r(t),n.leaveFrame(o,A),A}function Md(t){let e=new Map;for(let r of t.cost_centres){let A={key:r.id,name:`${r.module}.${r.label}`};r.src_loc.startsWith("<")||(A.file=r.src_loc),e.set(r.id,A)}let n=new Ie(t.total_ticks);Cl(t.profile,0,n,e,r=>r.ticks),n.setValueFormatter(new ee("milliseconds")),n.setName(`${t.program} time`);let a=new Ie(t.total_ticks);return Cl(t.profile,0,a,e,r=>r.alloc),a.setValueFormatter(new $e),a.setName(`${t.program} allocation`),{name:t.program,indexToView:0,profiles:[n.build(),a.build()]}}var Kd=re(()=>{"use strict";ke();Ke()});function ry(t){return t.map(({name:e,url:n,line:a,column:r})=>({key:`${e}:${n}:${a}:${r}`,file:n,line:a,col:r,name:e||(n?`(anonymous ${n.split("/").pop()}:${a})`:"(anonymous)")})).reverse()}function ml(t){t.version!==1&&console.warn(`Unknown Safari profile version ${t.version}... Might be incompatible.`);let{recording:e}=t,{sampleStackTraces:n,sampleDurations:a}=e,r=n.length;if(r<1)return console.warn("Empty profile"),null;let A=n[r-1].timestamp-n[0].timestamp+a[0],o=new ae(A),i=Number.MAX_VALUE;return n.forEach((l,s)=>{let c=l.timestamp,h=a[s],f=c-h-i;f>.002&&o.appendSampleWithWeight([],f),o.appendSampleWithWeight(ry(l.stackFrames),h),i=c}),o.setValueFormatter(new ee("seconds")),o.setName(e.displayName),o.build()}var Jd=re(()=>{"use strict";ke();Ke()});var Gd=k((gS,Pd)=>{"use strict";Pd.exports=ay;function ay(t,e){for(var n=new Array(arguments.length-1),a=0,r=2,A=!0;r{"use strict";var to=qd;to.length=function(e){var n=e.length;if(!n)return 0;for(var a=0;--n%4>1&&e.charAt(n)==="=";)++a;return Math.ceil(e.length*3)/4-a};var pr=new Array(64),Od=new Array(123);for(yt=0;yt<64;)Od[pr[yt]=yt<26?yt+65:yt<52?yt+71:yt<62?yt-4:yt-59|43]=yt++;var yt;to.encode=function(e,n,a){for(var r=null,A=[],o=0,i=0,l;n>2],l=(s&3)<<4,i=1;break;case 1:A[o++]=pr[l|s>>4],l=(s&15)<<2,i=2;break;case 2:A[o++]=pr[l|s>>6],A[o++]=pr[s&63],i=0;break}o>8191&&((r||(r=[])).push(String.fromCharCode.apply(String,A)),o=0)}return i&&(A[o++]=pr[l],A[o++]=61,i===1&&(A[o++]=61)),r?(o&&r.push(String.fromCharCode.apply(String,A.slice(0,o))),r.join("")):String.fromCharCode.apply(String,A.slice(0,o))};var Ud="invalid encoding";to.decode=function(e,n,a){for(var r=a,A=0,o,i=0;i1)break;if((l=Od[l])===void 0)throw Error(Ud);switch(A){case 0:o=l,A=1;break;case 1:n[a++]=o<<2|(l&48)>>4,o=l,A=2;break;case 2:n[a++]=(o&15)<<4|(l&60)>>2,o=l,A=3;break;case 3:n[a++]=(o&3)<<6|l,A=0;break}}if(A===1)throw Error(Ud);return a-r};to.test=function(e){return/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(e)}});var Vd=k((uS,$d)=>{"use strict";$d.exports=no;function no(){this._listeners={}}no.prototype.on=function(e,n,a){return(this._listeners[e]||(this._listeners[e]=[])).push({fn:n,ctx:a||this}),this};no.prototype.off=function(e,n){if(e===void 0)this._listeners={};else if(n===void 0)this._listeners[e]=[];else for(var a=this._listeners[e],r=0;r{"use strict";tu.exports=Yd(Yd);function Yd(t){return typeof Float32Array<"u"?(function(){var e=new Float32Array([-0]),n=new Uint8Array(e.buffer),a=n[3]===128;function r(l,s,c){e[0]=l,s[c]=n[0],s[c+1]=n[1],s[c+2]=n[2],s[c+3]=n[3]}function A(l,s,c){e[0]=l,s[c]=n[3],s[c+1]=n[2],s[c+2]=n[1],s[c+3]=n[0]}t.writeFloatLE=a?r:A,t.writeFloatBE=a?A:r;function o(l,s){return n[0]=l[s],n[1]=l[s+1],n[2]=l[s+2],n[3]=l[s+3],e[0]}function i(l,s){return n[3]=l[s],n[2]=l[s+1],n[1]=l[s+2],n[0]=l[s+3],e[0]}t.readFloatLE=a?o:i,t.readFloatBE=a?i:o})():(function(){function e(a,r,A,o){var i=r<0?1:0;if(i&&(r=-r),r===0)a(1/r>0?0:2147483648,A,o);else if(isNaN(r))a(2143289344,A,o);else if(r>34028234663852886e22)a((i<<31|2139095040)>>>0,A,o);else if(r<11754943508222875e-54)a((i<<31|Math.round(r/1401298464324817e-60))>>>0,A,o);else{var l=Math.floor(Math.log(r)/Math.LN2),s=Math.round(r*Math.pow(2,-l)*8388608)&8388607;a((i<<31|l+127<<23|s)>>>0,A,o)}}t.writeFloatLE=e.bind(null,Wd),t.writeFloatBE=e.bind(null,Zd);function n(a,r,A){var o=a(r,A),i=(o>>31)*2+1,l=o>>>23&255,s=o&8388607;return l===255?s?NaN:i*(1/0):l===0?i*1401298464324817e-60*s:i*Math.pow(2,l-150)*(s+8388608)}t.readFloatLE=n.bind(null,Xd),t.readFloatBE=n.bind(null,eu)})(),typeof Float64Array<"u"?(function(){var e=new Float64Array([-0]),n=new Uint8Array(e.buffer),a=n[7]===128;function r(l,s,c){e[0]=l,s[c]=n[0],s[c+1]=n[1],s[c+2]=n[2],s[c+3]=n[3],s[c+4]=n[4],s[c+5]=n[5],s[c+6]=n[6],s[c+7]=n[7]}function A(l,s,c){e[0]=l,s[c]=n[7],s[c+1]=n[6],s[c+2]=n[5],s[c+3]=n[4],s[c+4]=n[3],s[c+5]=n[2],s[c+6]=n[1],s[c+7]=n[0]}t.writeDoubleLE=a?r:A,t.writeDoubleBE=a?A:r;function o(l,s){return n[0]=l[s],n[1]=l[s+1],n[2]=l[s+2],n[3]=l[s+3],n[4]=l[s+4],n[5]=l[s+5],n[6]=l[s+6],n[7]=l[s+7],e[0]}function i(l,s){return n[7]=l[s],n[6]=l[s+1],n[5]=l[s+2],n[4]=l[s+3],n[3]=l[s+4],n[2]=l[s+5],n[1]=l[s+6],n[0]=l[s+7],e[0]}t.readDoubleLE=a?o:i,t.readDoubleBE=a?i:o})():(function(){function e(a,r,A,o,i,l){var s=o<0?1:0;if(s&&(o=-o),o===0)a(0,i,l+r),a(1/o>0?0:2147483648,i,l+A);else if(isNaN(o))a(0,i,l+r),a(2146959360,i,l+A);else if(o>17976931348623157e292)a(0,i,l+r),a((s<<31|2146435072)>>>0,i,l+A);else{var c;if(o<22250738585072014e-324)c=o/5e-324,a(c>>>0,i,l+r),a((s<<31|c/4294967296)>>>0,i,l+A);else{var h=Math.floor(Math.log(o)/Math.LN2);h===1024&&(h=1023),c=o*Math.pow(2,-h),a(c*4503599627370496>>>0,i,l+r),a((s<<31|h+1023<<20|c*1048576&1048575)>>>0,i,l+A)}}}t.writeDoubleLE=e.bind(null,Wd,0,4),t.writeDoubleBE=e.bind(null,Zd,4,0);function n(a,r,A,o,i){var l=a(o,i+r),s=a(o,i+A),c=(s>>31)*2+1,h=s>>>20&2047,_=4294967296*(s&1048575)+l;return h===2047?_?NaN:c*(1/0):h===0?c*5e-324*_:c*Math.pow(2,h-1075)*(_+4503599627370496)}t.readDoubleLE=n.bind(null,Xd,0,4),t.readDoubleBE=n.bind(null,eu,4,0)})(),t}function Wd(t,e,n){e[n]=t&255,e[n+1]=t>>>8&255,e[n+2]=t>>>16&255,e[n+3]=t>>>24}function Zd(t,e,n){e[n]=t>>>24,e[n+1]=t>>>16&255,e[n+2]=t>>>8&255,e[n+3]=t&255}function Xd(t,e){return(t[e]|t[e+1]<<8|t[e+2]<<16|t[e+3]<<24)>>>0}function eu(t,e){return(t[e]<<24|t[e+1]<<16|t[e+2]<<8|t[e+3])>>>0}});var ru=k((exports,module)=>{"use strict";module.exports=inquire;function inquire(moduleName){try{var mod=eval("quire".replace(/^/,"re"))(moduleName);if(mod&&(mod.length||Object.keys(mod).length))return mod}catch(t){}return null}});var Au=k(au=>{"use strict";var Il=au;Il.length=function(e){for(var n=0,a=0,r=0;r191&&l<224?o[i++]=(l&31)<<6|e[n++]&63:l>239&&l<365?(l=((l&7)<<18|(e[n++]&63)<<12|(e[n++]&63)<<6|e[n++]&63)-65536,o[i++]=55296+(l>>10),o[i++]=56320+(l&1023)):o[i++]=(l&15)<<12|(e[n++]&63)<<6|e[n++]&63,i>8191&&((A||(A=[])).push(String.fromCharCode.apply(String,o)),i=0);return A?(i&&A.push(String.fromCharCode.apply(String,o.slice(0,i))),A.join("")):String.fromCharCode.apply(String,o.slice(0,i))};Il.write=function(e,n,a){for(var r=a,A,o,i=0;i>6|192,n[a++]=A&63|128):(A&64512)===55296&&((o=e.charCodeAt(i+1))&64512)===56320?(A=65536+((A&1023)<<10)+(o&1023),++i,n[a++]=A>>18|240,n[a++]=A>>12&63|128,n[a++]=A>>6&63|128,n[a++]=A&63|128):(n[a++]=A>>12|224,n[a++]=A>>6&63|128,n[a++]=A&63|128);return a-r}});var iu=k((CS,ou)=>{"use strict";ou.exports=Ay;function Ay(t,e,n){var a=n||8192,r=a>>>1,A=null,o=a;return function(l){if(l<1||l>r)return t(l);o+l>a&&(A=t(a),o=0);var s=e.call(A,o,o+=l);return o&7&&(o=(o|7)+1),s}}});var su=k((mS,lu)=>{"use strict";lu.exports=Se;var oa=mn();function Se(t,e){this.lo=t>>>0,this.hi=e>>>0}var Hn=Se.zero=new Se(0,0);Hn.toNumber=function(){return 0};Hn.zzEncode=Hn.zzDecode=function(){return this};Hn.length=function(){return 1};var oy=Se.zeroHash="\0\0\0\0\0\0\0\0";Se.fromNumber=function(e){if(e===0)return Hn;var n=e<0;n&&(e=-e);var a=e>>>0,r=(e-a)/4294967296>>>0;return n&&(r=~r>>>0,a=~a>>>0,++a>4294967295&&(a=0,++r>4294967295&&(r=0))),new Se(a,r)};Se.from=function(e){if(typeof e=="number")return Se.fromNumber(e);if(oa.isString(e))if(oa.Long)e=oa.Long.fromString(e);else return Se.fromNumber(parseInt(e,10));return e.low||e.high?new Se(e.low>>>0,e.high>>>0):Hn};Se.prototype.toNumber=function(e){if(!e&&this.hi>>>31){var n=~this.lo+1>>>0,a=~this.hi>>>0;return n||(a=a+1>>>0),-(n+a*4294967296)}return this.lo+this.hi*4294967296};Se.prototype.toLong=function(e){return oa.Long?new oa.Long(this.lo|0,this.hi|0,!!e):{low:this.lo|0,high:this.hi|0,unsigned:!!e}};var Cn=String.prototype.charCodeAt;Se.fromHash=function(e){return e===oy?Hn:new Se((Cn.call(e,0)|Cn.call(e,1)<<8|Cn.call(e,2)<<16|Cn.call(e,3)<<24)>>>0,(Cn.call(e,4)|Cn.call(e,5)<<8|Cn.call(e,6)<<16|Cn.call(e,7)<<24)>>>0)};Se.prototype.toHash=function(){return String.fromCharCode(this.lo&255,this.lo>>>8&255,this.lo>>>16&255,this.lo>>>24,this.hi&255,this.hi>>>8&255,this.hi>>>16&255,this.hi>>>24)};Se.prototype.zzEncode=function(){var e=this.hi>>31;return this.hi=((this.hi<<1|this.lo>>>31)^e)>>>0,this.lo=(this.lo<<1^e)>>>0,this};Se.prototype.zzDecode=function(){var e=-(this.lo&1);return this.lo=((this.lo>>>1|this.hi<<31)^e)>>>0,this.hi=(this.hi>>>1^e)>>>0,this};Se.prototype.length=function(){var e=this.lo,n=(this.lo>>>28|this.hi<<4)>>>0,a=this.hi>>>24;return a===0?n===0?e<16384?e<128?1:2:e<2097152?3:4:n<16384?n<128?5:6:n<2097152?7:8:a<128?9:10}});var mn=k(jl=>{"use strict";var H=jl;H.asPromise=Gd();H.base64=zd();H.EventEmitter=Vd();H.float=nu();H.inquire=ru();H.utf8=Au();H.pool=iu();H.LongBits=su();H.global=typeof window<"u"&&window||typeof global<"u"&&global||typeof self<"u"&&self||jl;H.emptyArray=Object.freeze?Object.freeze([]):[];H.emptyObject=Object.freeze?Object.freeze({}):{};H.isNode=!!(H.global.process&&H.global.process.versions&&H.global.process.versions.node);H.isInteger=Number.isInteger||function(e){return typeof e=="number"&&isFinite(e)&&Math.floor(e)===e};H.isString=function(e){return typeof e=="string"||e instanceof String};H.isObject=function(e){return e&&typeof e=="object"};H.isset=H.isSet=function(e,n){var a=e[n];return a!=null&&e.hasOwnProperty(n)?typeof a!="object"||(Array.isArray(a)?a.length:Object.keys(a).length)>0:!1};H.Buffer=(function(){try{var t=H.inquire("buffer").Buffer;return t.prototype.utf8Write?t:null}catch{return null}})();H._Buffer_from=null;H._Buffer_allocUnsafe=null;H.newBuffer=function(e){return typeof e=="number"?H.Buffer?H._Buffer_allocUnsafe(e):new H.Array(e):H.Buffer?H._Buffer_from(e):typeof Uint8Array>"u"?e:new Uint8Array(e)};H.Array=typeof Uint8Array<"u"?Uint8Array:Array;H.Long=H.global.dcodeIO&&H.global.dcodeIO.Long||H.global.Long||H.inquire("long");H.key2Re=/^true|false|0|1$/;H.key32Re=/^-?(?:0|[1-9][0-9]*)$/;H.key64Re=/^(?:[\\x00-\\xff]{8}|-?(?:0|[1-9][0-9]*))$/;H.longToHash=function(e){return e?H.LongBits.from(e).toHash():H.LongBits.zeroHash};H.longFromHash=function(e,n){var a=H.LongBits.fromHash(e);return H.Long?H.Long.fromBits(a.lo,a.hi,n):a.toNumber(!!n)};function cu(t,e,n){for(var a=Object.keys(e),r=0;r-1;--A)if(n[r[A]]===1&&this[r[A]]!==void 0&&this[r[A]]!==null)return r[A]}};H.oneOfSetter=function(e){return function(n){for(var a=0;a{"use strict";uu.exports=Z;var dt=mn(),du,ro=dt.LongBits,hu=dt.base64,gu=dt.utf8;function ia(t,e,n){this.fn=t,this.len=e,this.next=void 0,this.val=n}function yl(){}function iy(t){this.head=t.head,this.tail=t.tail,this.len=t.len,this.next=t.states}function Z(){this.len=0,this.head=new ia(yl,0,0),this.tail=this.head,this.states=null}Z.create=dt.Buffer?function(){return(Z.create=function(){return new du})()}:function(){return new Z};Z.alloc=function(e){return new dt.Array(e)};dt.Array!==Array&&(Z.alloc=dt.pool(Z.alloc,dt.Array.prototype.subarray));Z.prototype._push=function(e,n,a){return this.tail=this.tail.next=new ia(e,n,a),this.len+=n,this};function El(t,e,n){e[n]=t&255}function ly(t,e,n){for(;t>127;)e[n++]=t&127|128,t>>>=7;e[n]=t}function Bl(t,e){this.len=t,this.next=void 0,this.val=e}Bl.prototype=Object.create(ia.prototype);Bl.prototype.fn=ly;Z.prototype.uint32=function(e){return this.len+=(this.tail=this.tail.next=new Bl((e=e>>>0)<128?1:e<16384?2:e<2097152?3:e<268435456?4:5,e)).len,this};Z.prototype.int32=function(e){return e<0?this._push(Ql,10,ro.fromNumber(e)):this.uint32(e)};Z.prototype.sint32=function(e){return this.uint32((e<<1^e>>31)>>>0)};function Ql(t,e,n){for(;t.hi;)e[n++]=t.lo&127|128,t.lo=(t.lo>>>7|t.hi<<25)>>>0,t.hi>>>=7;for(;t.lo>127;)e[n++]=t.lo&127|128,t.lo=t.lo>>>7;e[n++]=t.lo}Z.prototype.uint64=function(e){var n=ro.from(e);return this._push(Ql,n.length(),n)};Z.prototype.int64=Z.prototype.uint64;Z.prototype.sint64=function(e){var n=ro.from(e).zzEncode();return this._push(Ql,n.length(),n)};Z.prototype.bool=function(e){return this._push(El,1,e?1:0)};function vl(t,e,n){e[n]=t&255,e[n+1]=t>>>8&255,e[n+2]=t>>>16&255,e[n+3]=t>>>24}Z.prototype.fixed32=function(e){return this._push(vl,4,e>>>0)};Z.prototype.sfixed32=Z.prototype.fixed32;Z.prototype.fixed64=function(e){var n=ro.from(e);return this._push(vl,4,n.lo)._push(vl,4,n.hi)};Z.prototype.sfixed64=Z.prototype.fixed64;Z.prototype.float=function(e){return this._push(dt.float.writeFloatLE,4,e)};Z.prototype.double=function(e){return this._push(dt.float.writeDoubleLE,8,e)};var sy=dt.Array.prototype.set?function(e,n,a){n.set(e,a)}:function(e,n,a){for(var r=0;r>>0;if(!n)return this._push(El,1,0);if(dt.isString(e)){var a=Z.alloc(n=hu.length(e));hu.decode(e,a,0),e=a}return this.uint32(n)._push(sy,n,e)};Z.prototype.string=function(e){var n=gu.length(e);return n?this.uint32(n)._push(gu.write,n,e):this._push(El,1,0)};Z.prototype.fork=function(){return this.states=new iy(this),this.head=this.tail=new ia(yl,0,0),this.len=0,this};Z.prototype.reset=function(){return this.states?(this.head=this.states.head,this.tail=this.states.tail,this.len=this.states.len,this.states=this.states.next):(this.head=this.tail=new ia(yl,0,0),this.len=0),this};Z.prototype.ldelim=function(){var e=this.head,n=this.tail,a=this.len;return this.reset().uint32(a),a&&(this.tail.next=e.next,this.tail=n,this.len+=a),this};Z.prototype.finish=function(){for(var e=this.head.next,n=this.constructor.alloc(this.len),a=0;e;)e.fn(e.val,n,a),a+=e.len,e=e.next;return n};Z._configure=function(t){du=t}});var Cu=k((vS,pu)=>{"use strict";pu.exports=Mn;var fu=wl();(Mn.prototype=Object.create(fu.prototype)).constructor=Mn;var la=mn(),ao=la.Buffer;function Mn(){fu.call(this)}Mn.alloc=function(e){return(Mn.alloc=la._Buffer_allocUnsafe)(e)};var cy=ao&&ao.prototype instanceof Uint8Array&&ao.prototype.set.name==="set"?function(e,n,a){n.set(e,a)}:function(e,n,a){if(e.copy)e.copy(n,a,0,e.length);else for(var r=0;r>>0;return this.uint32(n),n&&this._push(cy,n,e),this};function _y(t,e,n){t.length<40?la.utf8.write(t,e,n):e.utf8Write(t,n)}Mn.prototype.string=function(e){var n=ao.byteLength(e);return this.uint32(n),n&&this._push(_y,n,e),this}});var xl=k((yS,yu)=>{"use strict";yu.exports=ye;var Kt=mn(),ju,vu=Kt.LongBits,hy=Kt.utf8;function Et(t,e){return RangeError("index out of range: "+t.pos+" + "+(e||1)+" > "+t.len)}function ye(t){this.buf=t,this.pos=0,this.len=t.length}var mu=typeof Uint8Array<"u"?function(e){if(e instanceof Uint8Array||Array.isArray(e))return new ye(e);throw Error("illegal buffer")}:function(e){if(Array.isArray(e))return new ye(e);throw Error("illegal buffer")};ye.create=Kt.Buffer?function(e){return(ye.create=function(a){return Kt.Buffer.isBuffer(a)?new ju(a):mu(a)})(e)}:mu;ye.prototype._slice=Kt.Array.prototype.subarray||Kt.Array.prototype.slice;ye.prototype.uint32=(function(){var e=4294967295;return function(){if(e=(this.buf[this.pos]&127)>>>0,this.buf[this.pos++]<128||(e=(e|(this.buf[this.pos]&127)<<7)>>>0,this.buf[this.pos++]<128)||(e=(e|(this.buf[this.pos]&127)<<14)>>>0,this.buf[this.pos++]<128)||(e=(e|(this.buf[this.pos]&127)<<21)>>>0,this.buf[this.pos++]<128)||(e=(e|(this.buf[this.pos]&15)<<28)>>>0,this.buf[this.pos++]<128))return e;if((this.pos+=5)>this.len)throw this.pos=this.len,Et(this,10);return e}})();ye.prototype.int32=function(){return this.uint32()|0};ye.prototype.sint32=function(){var e=this.uint32();return e>>>1^-(e&1)|0};function bl(){var t=new vu(0,0),e=0;if(this.len-this.pos>4){for(;e<4;++e)if(t.lo=(t.lo|(this.buf[this.pos]&127)<>>0,this.buf[this.pos++]<128)return t;if(t.lo=(t.lo|(this.buf[this.pos]&127)<<28)>>>0,t.hi=(t.hi|(this.buf[this.pos]&127)>>4)>>>0,this.buf[this.pos++]<128)return t;e=0}else{for(;e<3;++e){if(this.pos>=this.len)throw Et(this);if(t.lo=(t.lo|(this.buf[this.pos]&127)<>>0,this.buf[this.pos++]<128)return t}return t.lo=(t.lo|(this.buf[this.pos++]&127)<>>0,t}if(this.len-this.pos>4){for(;e<5;++e)if(t.hi=(t.hi|(this.buf[this.pos]&127)<>>0,this.buf[this.pos++]<128)return t}else for(;e<5;++e){if(this.pos>=this.len)throw Et(this);if(t.hi=(t.hi|(this.buf[this.pos]&127)<>>0,this.buf[this.pos++]<128)return t}throw Error("invalid varint encoding")}ye.prototype.bool=function(){return this.uint32()!==0};function Ao(t,e){return(t[e-4]|t[e-3]<<8|t[e-2]<<16|t[e-1]<<24)>>>0}ye.prototype.fixed32=function(){if(this.pos+4>this.len)throw Et(this,4);return Ao(this.buf,this.pos+=4)};ye.prototype.sfixed32=function(){if(this.pos+4>this.len)throw Et(this,4);return Ao(this.buf,this.pos+=4)|0};function Iu(){if(this.pos+8>this.len)throw Et(this,8);return new vu(Ao(this.buf,this.pos+=4),Ao(this.buf,this.pos+=4))}ye.prototype.float=function(){if(this.pos+4>this.len)throw Et(this,4);var e=Kt.float.readFloatLE(this.buf,this.pos);return this.pos+=4,e};ye.prototype.double=function(){if(this.pos+8>this.len)throw Et(this,4);var e=Kt.float.readDoubleLE(this.buf,this.pos);return this.pos+=8,e};ye.prototype.bytes=function(){var e=this.uint32(),n=this.pos,a=this.pos+e;if(a>this.len)throw Et(this,e);return this.pos+=e,Array.isArray(this.buf)?this.buf.slice(n,a):n===a?new this.buf.constructor(0):this._slice.call(this.buf,n,a)};ye.prototype.string=function(){var e=this.bytes();return hy.read(e,0,e.length)};ye.prototype.skip=function(e){if(typeof e=="number"){if(this.pos+e>this.len)throw Et(this,e);this.pos+=e}else do if(this.pos>=this.len)throw Et(this);while(this.buf[this.pos++]&128);return this};ye.prototype.skipType=function(t){switch(t){case 0:this.skip();break;case 1:this.skip(8);break;case 2:this.skip(this.uint32());break;case 3:for(;(t=this.uint32()&7)!==4;)this.skipType(t);break;case 5:this.skip(4);break;default:throw Error("invalid wire type "+t+" at offset "+this.pos)}return this};ye._configure=function(t){ju=t;var e=Kt.Long?"toLong":"toNumber";Kt.merge(ye.prototype,{int64:function(){return bl.call(this)[e](!1)},uint64:function(){return bl.call(this)[e](!0)},sint64:function(){return bl.call(this).zzDecode()[e](!1)},fixed64:function(){return Iu.call(this)[e](!0)},sfixed64:function(){return Iu.call(this)[e](!1)}})}});var wu=k((ES,Qu)=>{"use strict";Qu.exports=sa;var Bu=xl();(sa.prototype=Object.create(Bu.prototype)).constructor=sa;var Eu=mn();function sa(t){Bu.call(this,t)}Eu.Buffer&&(sa.prototype._slice=Eu.Buffer.prototype.slice);sa.prototype.string=function(){var e=this.uint32();return this.buf.utf8Slice(this.pos,this.pos=Math.min(this.pos+e,this.len))}});var xu=k((BS,bu)=>{"use strict";bu.exports=ca;var kl=mn();(ca.prototype=Object.create(kl.EventEmitter.prototype)).constructor=ca;function ca(t,e,n){if(typeof t!="function")throw TypeError("rpcImpl must be a function");kl.EventEmitter.call(this),this.rpcImpl=t,this.requestDelimited=!!e,this.responseDelimited=!!n}ca.prototype.rpcCall=function t(e,n,a,r,A){if(!r)throw TypeError("request must be specified");var o=this;if(!A)return kl.asPromise(t,o,e,n,a,r);if(!o.rpcImpl){setTimeout(function(){A(Error("already ended"))},0);return}try{return o.rpcImpl(e,n[o.requestDelimited?"encodeDelimited":"encode"](r).finish(),function(l,s){if(l)return o.emit("error",l,e),A(l);if(s===null){o.end(!0);return}if(!(s instanceof a))try{s=a[o.responseDelimited?"decodeDelimited":"decode"](s)}catch(c){return o.emit("error",c,e),A(c)}return o.emit("data",s,e),A(null,s)})}catch(i){o.emit("error",i,e),setTimeout(function(){A(i)},0);return}};ca.prototype.end=function(e){return this.rpcImpl&&(e||this.rpcImpl(null,null,null),this.rpcImpl=null,this.emit("end").off()),this}});var Su=k(ku=>{"use strict";var gy=ku;gy.Service=xu()});var Fu=k((wS,Nu)=>{"use strict";Nu.exports={}});var Lu=k(Ru=>{"use strict";var Ye=Ru;Ye.build="minimal";Ye.Writer=wl();Ye.BufferWriter=Cu();Ye.Reader=xl();Ye.BufferReader=wu();Ye.util=mn();Ye.rpc=Su();Ye.roots=Fu();Ye.configure=Du;function Du(){Ye.Reader._configure(Ye.BufferReader),Ye.util._configure()}Ye.Writer._configure(Ye.BufferWriter);Du()});var Hu=k((xS,Tu)=>{"use strict";Tu.exports=Lu()});var Ku=k((kS,Mu)=>{"use strict";var rt=Hu(),X=rt.Reader,In=rt.Writer,d=rt.util,N=rt.roots.default||(rt.roots.default={});N.perftools=(function(){var t={};return t.profiles=(function(){var e={};return e.Profile=(function(){function n(a){if(this.sampleType=[],this.sample=[],this.mapping=[],this.location=[],this.function=[],this.stringTable=[],this.comment=[],a)for(var r=Object.keys(a),A=0;A>>3){case 1:i.sampleType&&i.sampleType.length||(i.sampleType=[]),i.sampleType.push(N.perftools.profiles.ValueType.decode(r,r.uint32()));break;case 2:i.sample&&i.sample.length||(i.sample=[]),i.sample.push(N.perftools.profiles.Sample.decode(r,r.uint32()));break;case 3:i.mapping&&i.mapping.length||(i.mapping=[]),i.mapping.push(N.perftools.profiles.Mapping.decode(r,r.uint32()));break;case 4:i.location&&i.location.length||(i.location=[]),i.location.push(N.perftools.profiles.Location.decode(r,r.uint32()));break;case 5:i.function&&i.function.length||(i.function=[]),i.function.push(N.perftools.profiles.Function.decode(r,r.uint32()));break;case 6:i.stringTable&&i.stringTable.length||(i.stringTable=[]),i.stringTable.push(r.string());break;case 7:i.dropFrames=r.int64();break;case 8:i.keepFrames=r.int64();break;case 9:i.timeNanos=r.int64();break;case 10:i.durationNanos=r.int64();break;case 11:i.periodType=N.perftools.profiles.ValueType.decode(r,r.uint32());break;case 12:i.period=r.int64();break;case 13:if(i.comment&&i.comment.length||(i.comment=[]),(l&7)===2)for(var s=r.uint32()+r.pos;r.pos>>0,r.dropFrames.high>>>0).toNumber())),r.keepFrames!=null&&(d.Long?(A.keepFrames=d.Long.fromValue(r.keepFrames)).unsigned=!1:typeof r.keepFrames=="string"?A.keepFrames=parseInt(r.keepFrames,10):typeof r.keepFrames=="number"?A.keepFrames=r.keepFrames:typeof r.keepFrames=="object"&&(A.keepFrames=new d.LongBits(r.keepFrames.low>>>0,r.keepFrames.high>>>0).toNumber())),r.timeNanos!=null&&(d.Long?(A.timeNanos=d.Long.fromValue(r.timeNanos)).unsigned=!1:typeof r.timeNanos=="string"?A.timeNanos=parseInt(r.timeNanos,10):typeof r.timeNanos=="number"?A.timeNanos=r.timeNanos:typeof r.timeNanos=="object"&&(A.timeNanos=new d.LongBits(r.timeNanos.low>>>0,r.timeNanos.high>>>0).toNumber())),r.durationNanos!=null&&(d.Long?(A.durationNanos=d.Long.fromValue(r.durationNanos)).unsigned=!1:typeof r.durationNanos=="string"?A.durationNanos=parseInt(r.durationNanos,10):typeof r.durationNanos=="number"?A.durationNanos=r.durationNanos:typeof r.durationNanos=="object"&&(A.durationNanos=new d.LongBits(r.durationNanos.low>>>0,r.durationNanos.high>>>0).toNumber())),r.periodType!=null){if(typeof r.periodType!="object")throw TypeError(".perftools.profiles.Profile.periodType: object expected");A.periodType=N.perftools.profiles.ValueType.fromObject(r.periodType)}if(r.period!=null&&(d.Long?(A.period=d.Long.fromValue(r.period)).unsigned=!1:typeof r.period=="string"?A.period=parseInt(r.period,10):typeof r.period=="number"?A.period=r.period:typeof r.period=="object"&&(A.period=new d.LongBits(r.period.low>>>0,r.period.high>>>0).toNumber())),r.comment){if(!Array.isArray(r.comment))throw TypeError(".perftools.profiles.Profile.comment: array expected");A.comment=[];for(var o=0;o>>0,r.comment[o].high>>>0).toNumber())}return r.defaultSampleType!=null&&(d.Long?(A.defaultSampleType=d.Long.fromValue(r.defaultSampleType)).unsigned=!1:typeof r.defaultSampleType=="string"?A.defaultSampleType=parseInt(r.defaultSampleType,10):typeof r.defaultSampleType=="number"?A.defaultSampleType=r.defaultSampleType:typeof r.defaultSampleType=="object"&&(A.defaultSampleType=new d.LongBits(r.defaultSampleType.low>>>0,r.defaultSampleType.high>>>0).toNumber())),A},n.toObject=function(r,A){A||(A={});var o={};if((A.arrays||A.defaults)&&(o.sampleType=[],o.sample=[],o.mapping=[],o.location=[],o.function=[],o.stringTable=[],o.comment=[]),A.defaults){if(d.Long){var i=new d.Long(0,0,!1);o.dropFrames=A.longs===String?i.toString():A.longs===Number?i.toNumber():i}else o.dropFrames=A.longs===String?"0":0;if(d.Long){var i=new d.Long(0,0,!1);o.keepFrames=A.longs===String?i.toString():A.longs===Number?i.toNumber():i}else o.keepFrames=A.longs===String?"0":0;if(d.Long){var i=new d.Long(0,0,!1);o.timeNanos=A.longs===String?i.toString():A.longs===Number?i.toNumber():i}else o.timeNanos=A.longs===String?"0":0;if(d.Long){var i=new d.Long(0,0,!1);o.durationNanos=A.longs===String?i.toString():A.longs===Number?i.toNumber():i}else o.durationNanos=A.longs===String?"0":0;if(o.periodType=null,d.Long){var i=new d.Long(0,0,!1);o.period=A.longs===String?i.toString():A.longs===Number?i.toNumber():i}else o.period=A.longs===String?"0":0;if(d.Long){var i=new d.Long(0,0,!1);o.defaultSampleType=A.longs===String?i.toString():A.longs===Number?i.toNumber():i}else o.defaultSampleType=A.longs===String?"0":0}if(r.sampleType&&r.sampleType.length){o.sampleType=[];for(var l=0;l>>0,r.dropFrames.high>>>0).toNumber():r.dropFrames),r.keepFrames!=null&&r.hasOwnProperty("keepFrames")&&(typeof r.keepFrames=="number"?o.keepFrames=A.longs===String?String(r.keepFrames):r.keepFrames:o.keepFrames=A.longs===String?d.Long.prototype.toString.call(r.keepFrames):A.longs===Number?new d.LongBits(r.keepFrames.low>>>0,r.keepFrames.high>>>0).toNumber():r.keepFrames),r.timeNanos!=null&&r.hasOwnProperty("timeNanos")&&(typeof r.timeNanos=="number"?o.timeNanos=A.longs===String?String(r.timeNanos):r.timeNanos:o.timeNanos=A.longs===String?d.Long.prototype.toString.call(r.timeNanos):A.longs===Number?new d.LongBits(r.timeNanos.low>>>0,r.timeNanos.high>>>0).toNumber():r.timeNanos),r.durationNanos!=null&&r.hasOwnProperty("durationNanos")&&(typeof r.durationNanos=="number"?o.durationNanos=A.longs===String?String(r.durationNanos):r.durationNanos:o.durationNanos=A.longs===String?d.Long.prototype.toString.call(r.durationNanos):A.longs===Number?new d.LongBits(r.durationNanos.low>>>0,r.durationNanos.high>>>0).toNumber():r.durationNanos),r.periodType!=null&&r.hasOwnProperty("periodType")&&(o.periodType=N.perftools.profiles.ValueType.toObject(r.periodType,A)),r.period!=null&&r.hasOwnProperty("period")&&(typeof r.period=="number"?o.period=A.longs===String?String(r.period):r.period:o.period=A.longs===String?d.Long.prototype.toString.call(r.period):A.longs===Number?new d.LongBits(r.period.low>>>0,r.period.high>>>0).toNumber():r.period),r.comment&&r.comment.length){o.comment=[];for(var l=0;l>>0,r.comment[l].high>>>0).toNumber():r.comment[l]}return r.defaultSampleType!=null&&r.hasOwnProperty("defaultSampleType")&&(typeof r.defaultSampleType=="number"?o.defaultSampleType=A.longs===String?String(r.defaultSampleType):r.defaultSampleType:o.defaultSampleType=A.longs===String?d.Long.prototype.toString.call(r.defaultSampleType):A.longs===Number?new d.LongBits(r.defaultSampleType.low>>>0,r.defaultSampleType.high>>>0).toNumber():r.defaultSampleType),o},n.prototype.toJSON=function(){return this.constructor.toObject(this,rt.util.toJSONOptions)},n})(),e.ValueType=(function(){function n(a){if(a)for(var r=Object.keys(a),A=0;A>>3){case 1:i.type=r.int64();break;case 2:i.unit=r.int64();break;default:r.skipType(l&7);break}}return i},n.decodeDelimited=function(r){return r instanceof X||(r=new X(r)),this.decode(r,r.uint32())},n.verify=function(r){return typeof r!="object"||r===null?"object expected":r.type!=null&&r.hasOwnProperty("type")&&!d.isInteger(r.type)&&!(r.type&&d.isInteger(r.type.low)&&d.isInteger(r.type.high))?"type: integer|Long expected":r.unit!=null&&r.hasOwnProperty("unit")&&!d.isInteger(r.unit)&&!(r.unit&&d.isInteger(r.unit.low)&&d.isInteger(r.unit.high))?"unit: integer|Long expected":null},n.fromObject=function(r){if(r instanceof N.perftools.profiles.ValueType)return r;var A=new N.perftools.profiles.ValueType;return r.type!=null&&(d.Long?(A.type=d.Long.fromValue(r.type)).unsigned=!1:typeof r.type=="string"?A.type=parseInt(r.type,10):typeof r.type=="number"?A.type=r.type:typeof r.type=="object"&&(A.type=new d.LongBits(r.type.low>>>0,r.type.high>>>0).toNumber())),r.unit!=null&&(d.Long?(A.unit=d.Long.fromValue(r.unit)).unsigned=!1:typeof r.unit=="string"?A.unit=parseInt(r.unit,10):typeof r.unit=="number"?A.unit=r.unit:typeof r.unit=="object"&&(A.unit=new d.LongBits(r.unit.low>>>0,r.unit.high>>>0).toNumber())),A},n.toObject=function(r,A){A||(A={});var o={};if(A.defaults){if(d.Long){var i=new d.Long(0,0,!1);o.type=A.longs===String?i.toString():A.longs===Number?i.toNumber():i}else o.type=A.longs===String?"0":0;if(d.Long){var i=new d.Long(0,0,!1);o.unit=A.longs===String?i.toString():A.longs===Number?i.toNumber():i}else o.unit=A.longs===String?"0":0}return r.type!=null&&r.hasOwnProperty("type")&&(typeof r.type=="number"?o.type=A.longs===String?String(r.type):r.type:o.type=A.longs===String?d.Long.prototype.toString.call(r.type):A.longs===Number?new d.LongBits(r.type.low>>>0,r.type.high>>>0).toNumber():r.type),r.unit!=null&&r.hasOwnProperty("unit")&&(typeof r.unit=="number"?o.unit=A.longs===String?String(r.unit):r.unit:o.unit=A.longs===String?d.Long.prototype.toString.call(r.unit):A.longs===Number?new d.LongBits(r.unit.low>>>0,r.unit.high>>>0).toNumber():r.unit),o},n.prototype.toJSON=function(){return this.constructor.toObject(this,rt.util.toJSONOptions)},n})(),e.Sample=(function(){function n(a){if(this.locationId=[],this.value=[],this.label=[],a)for(var r=Object.keys(a),A=0;A>>3){case 1:if(i.locationId&&i.locationId.length||(i.locationId=[]),(l&7)===2)for(var s=r.uint32()+r.pos;r.pos>>0,r.locationId[o].high>>>0).toNumber(!0))}if(r.value){if(!Array.isArray(r.value))throw TypeError(".perftools.profiles.Sample.value: array expected");A.value=[];for(var o=0;o>>0,r.value[o].high>>>0).toNumber())}if(r.label){if(!Array.isArray(r.label))throw TypeError(".perftools.profiles.Sample.label: array expected");A.label=[];for(var o=0;o>>0,r.locationId[i].high>>>0).toNumber(!0):r.locationId[i]}if(r.value&&r.value.length){o.value=[];for(var i=0;i>>0,r.value[i].high>>>0).toNumber():r.value[i]}if(r.label&&r.label.length){o.label=[];for(var i=0;i>>3){case 1:i.key=r.int64();break;case 2:i.str=r.int64();break;case 3:i.num=r.int64();break;case 4:i.numUnit=r.int64();break;default:r.skipType(l&7);break}}return i},n.decodeDelimited=function(r){return r instanceof X||(r=new X(r)),this.decode(r,r.uint32())},n.verify=function(r){return typeof r!="object"||r===null?"object expected":r.key!=null&&r.hasOwnProperty("key")&&!d.isInteger(r.key)&&!(r.key&&d.isInteger(r.key.low)&&d.isInteger(r.key.high))?"key: integer|Long expected":r.str!=null&&r.hasOwnProperty("str")&&!d.isInteger(r.str)&&!(r.str&&d.isInteger(r.str.low)&&d.isInteger(r.str.high))?"str: integer|Long expected":r.num!=null&&r.hasOwnProperty("num")&&!d.isInteger(r.num)&&!(r.num&&d.isInteger(r.num.low)&&d.isInteger(r.num.high))?"num: integer|Long expected":r.numUnit!=null&&r.hasOwnProperty("numUnit")&&!d.isInteger(r.numUnit)&&!(r.numUnit&&d.isInteger(r.numUnit.low)&&d.isInteger(r.numUnit.high))?"numUnit: integer|Long expected":null},n.fromObject=function(r){if(r instanceof N.perftools.profiles.Label)return r;var A=new N.perftools.profiles.Label;return r.key!=null&&(d.Long?(A.key=d.Long.fromValue(r.key)).unsigned=!1:typeof r.key=="string"?A.key=parseInt(r.key,10):typeof r.key=="number"?A.key=r.key:typeof r.key=="object"&&(A.key=new d.LongBits(r.key.low>>>0,r.key.high>>>0).toNumber())),r.str!=null&&(d.Long?(A.str=d.Long.fromValue(r.str)).unsigned=!1:typeof r.str=="string"?A.str=parseInt(r.str,10):typeof r.str=="number"?A.str=r.str:typeof r.str=="object"&&(A.str=new d.LongBits(r.str.low>>>0,r.str.high>>>0).toNumber())),r.num!=null&&(d.Long?(A.num=d.Long.fromValue(r.num)).unsigned=!1:typeof r.num=="string"?A.num=parseInt(r.num,10):typeof r.num=="number"?A.num=r.num:typeof r.num=="object"&&(A.num=new d.LongBits(r.num.low>>>0,r.num.high>>>0).toNumber())),r.numUnit!=null&&(d.Long?(A.numUnit=d.Long.fromValue(r.numUnit)).unsigned=!1:typeof r.numUnit=="string"?A.numUnit=parseInt(r.numUnit,10):typeof r.numUnit=="number"?A.numUnit=r.numUnit:typeof r.numUnit=="object"&&(A.numUnit=new d.LongBits(r.numUnit.low>>>0,r.numUnit.high>>>0).toNumber())),A},n.toObject=function(r,A){A||(A={});var o={};if(A.defaults){if(d.Long){var i=new d.Long(0,0,!1);o.key=A.longs===String?i.toString():A.longs===Number?i.toNumber():i}else o.key=A.longs===String?"0":0;if(d.Long){var i=new d.Long(0,0,!1);o.str=A.longs===String?i.toString():A.longs===Number?i.toNumber():i}else o.str=A.longs===String?"0":0;if(d.Long){var i=new d.Long(0,0,!1);o.num=A.longs===String?i.toString():A.longs===Number?i.toNumber():i}else o.num=A.longs===String?"0":0;if(d.Long){var i=new d.Long(0,0,!1);o.numUnit=A.longs===String?i.toString():A.longs===Number?i.toNumber():i}else o.numUnit=A.longs===String?"0":0}return r.key!=null&&r.hasOwnProperty("key")&&(typeof r.key=="number"?o.key=A.longs===String?String(r.key):r.key:o.key=A.longs===String?d.Long.prototype.toString.call(r.key):A.longs===Number?new d.LongBits(r.key.low>>>0,r.key.high>>>0).toNumber():r.key),r.str!=null&&r.hasOwnProperty("str")&&(typeof r.str=="number"?o.str=A.longs===String?String(r.str):r.str:o.str=A.longs===String?d.Long.prototype.toString.call(r.str):A.longs===Number?new d.LongBits(r.str.low>>>0,r.str.high>>>0).toNumber():r.str),r.num!=null&&r.hasOwnProperty("num")&&(typeof r.num=="number"?o.num=A.longs===String?String(r.num):r.num:o.num=A.longs===String?d.Long.prototype.toString.call(r.num):A.longs===Number?new d.LongBits(r.num.low>>>0,r.num.high>>>0).toNumber():r.num),r.numUnit!=null&&r.hasOwnProperty("numUnit")&&(typeof r.numUnit=="number"?o.numUnit=A.longs===String?String(r.numUnit):r.numUnit:o.numUnit=A.longs===String?d.Long.prototype.toString.call(r.numUnit):A.longs===Number?new d.LongBits(r.numUnit.low>>>0,r.numUnit.high>>>0).toNumber():r.numUnit),o},n.prototype.toJSON=function(){return this.constructor.toObject(this,rt.util.toJSONOptions)},n})(),e.Mapping=(function(){function n(a){if(a)for(var r=Object.keys(a),A=0;A>>3){case 1:i.id=r.uint64();break;case 2:i.memoryStart=r.uint64();break;case 3:i.memoryLimit=r.uint64();break;case 4:i.fileOffset=r.uint64();break;case 5:i.filename=r.int64();break;case 6:i.buildId=r.int64();break;case 7:i.hasFunctions=r.bool();break;case 8:i.hasFilenames=r.bool();break;case 9:i.hasLineNumbers=r.bool();break;case 10:i.hasInlineFrames=r.bool();break;default:r.skipType(l&7);break}}return i},n.decodeDelimited=function(r){return r instanceof X||(r=new X(r)),this.decode(r,r.uint32())},n.verify=function(r){return typeof r!="object"||r===null?"object expected":r.id!=null&&r.hasOwnProperty("id")&&!d.isInteger(r.id)&&!(r.id&&d.isInteger(r.id.low)&&d.isInteger(r.id.high))?"id: integer|Long expected":r.memoryStart!=null&&r.hasOwnProperty("memoryStart")&&!d.isInteger(r.memoryStart)&&!(r.memoryStart&&d.isInteger(r.memoryStart.low)&&d.isInteger(r.memoryStart.high))?"memoryStart: integer|Long expected":r.memoryLimit!=null&&r.hasOwnProperty("memoryLimit")&&!d.isInteger(r.memoryLimit)&&!(r.memoryLimit&&d.isInteger(r.memoryLimit.low)&&d.isInteger(r.memoryLimit.high))?"memoryLimit: integer|Long expected":r.fileOffset!=null&&r.hasOwnProperty("fileOffset")&&!d.isInteger(r.fileOffset)&&!(r.fileOffset&&d.isInteger(r.fileOffset.low)&&d.isInteger(r.fileOffset.high))?"fileOffset: integer|Long expected":r.filename!=null&&r.hasOwnProperty("filename")&&!d.isInteger(r.filename)&&!(r.filename&&d.isInteger(r.filename.low)&&d.isInteger(r.filename.high))?"filename: integer|Long expected":r.buildId!=null&&r.hasOwnProperty("buildId")&&!d.isInteger(r.buildId)&&!(r.buildId&&d.isInteger(r.buildId.low)&&d.isInteger(r.buildId.high))?"buildId: integer|Long expected":r.hasFunctions!=null&&r.hasOwnProperty("hasFunctions")&&typeof r.hasFunctions!="boolean"?"hasFunctions: boolean expected":r.hasFilenames!=null&&r.hasOwnProperty("hasFilenames")&&typeof r.hasFilenames!="boolean"?"hasFilenames: boolean expected":r.hasLineNumbers!=null&&r.hasOwnProperty("hasLineNumbers")&&typeof r.hasLineNumbers!="boolean"?"hasLineNumbers: boolean expected":r.hasInlineFrames!=null&&r.hasOwnProperty("hasInlineFrames")&&typeof r.hasInlineFrames!="boolean"?"hasInlineFrames: boolean expected":null},n.fromObject=function(r){if(r instanceof N.perftools.profiles.Mapping)return r;var A=new N.perftools.profiles.Mapping;return r.id!=null&&(d.Long?(A.id=d.Long.fromValue(r.id)).unsigned=!0:typeof r.id=="string"?A.id=parseInt(r.id,10):typeof r.id=="number"?A.id=r.id:typeof r.id=="object"&&(A.id=new d.LongBits(r.id.low>>>0,r.id.high>>>0).toNumber(!0))),r.memoryStart!=null&&(d.Long?(A.memoryStart=d.Long.fromValue(r.memoryStart)).unsigned=!0:typeof r.memoryStart=="string"?A.memoryStart=parseInt(r.memoryStart,10):typeof r.memoryStart=="number"?A.memoryStart=r.memoryStart:typeof r.memoryStart=="object"&&(A.memoryStart=new d.LongBits(r.memoryStart.low>>>0,r.memoryStart.high>>>0).toNumber(!0))),r.memoryLimit!=null&&(d.Long?(A.memoryLimit=d.Long.fromValue(r.memoryLimit)).unsigned=!0:typeof r.memoryLimit=="string"?A.memoryLimit=parseInt(r.memoryLimit,10):typeof r.memoryLimit=="number"?A.memoryLimit=r.memoryLimit:typeof r.memoryLimit=="object"&&(A.memoryLimit=new d.LongBits(r.memoryLimit.low>>>0,r.memoryLimit.high>>>0).toNumber(!0))),r.fileOffset!=null&&(d.Long?(A.fileOffset=d.Long.fromValue(r.fileOffset)).unsigned=!0:typeof r.fileOffset=="string"?A.fileOffset=parseInt(r.fileOffset,10):typeof r.fileOffset=="number"?A.fileOffset=r.fileOffset:typeof r.fileOffset=="object"&&(A.fileOffset=new d.LongBits(r.fileOffset.low>>>0,r.fileOffset.high>>>0).toNumber(!0))),r.filename!=null&&(d.Long?(A.filename=d.Long.fromValue(r.filename)).unsigned=!1:typeof r.filename=="string"?A.filename=parseInt(r.filename,10):typeof r.filename=="number"?A.filename=r.filename:typeof r.filename=="object"&&(A.filename=new d.LongBits(r.filename.low>>>0,r.filename.high>>>0).toNumber())),r.buildId!=null&&(d.Long?(A.buildId=d.Long.fromValue(r.buildId)).unsigned=!1:typeof r.buildId=="string"?A.buildId=parseInt(r.buildId,10):typeof r.buildId=="number"?A.buildId=r.buildId:typeof r.buildId=="object"&&(A.buildId=new d.LongBits(r.buildId.low>>>0,r.buildId.high>>>0).toNumber())),r.hasFunctions!=null&&(A.hasFunctions=!!r.hasFunctions),r.hasFilenames!=null&&(A.hasFilenames=!!r.hasFilenames),r.hasLineNumbers!=null&&(A.hasLineNumbers=!!r.hasLineNumbers),r.hasInlineFrames!=null&&(A.hasInlineFrames=!!r.hasInlineFrames),A},n.toObject=function(r,A){A||(A={});var o={};if(A.defaults){if(d.Long){var i=new d.Long(0,0,!0);o.id=A.longs===String?i.toString():A.longs===Number?i.toNumber():i}else o.id=A.longs===String?"0":0;if(d.Long){var i=new d.Long(0,0,!0);o.memoryStart=A.longs===String?i.toString():A.longs===Number?i.toNumber():i}else o.memoryStart=A.longs===String?"0":0;if(d.Long){var i=new d.Long(0,0,!0);o.memoryLimit=A.longs===String?i.toString():A.longs===Number?i.toNumber():i}else o.memoryLimit=A.longs===String?"0":0;if(d.Long){var i=new d.Long(0,0,!0);o.fileOffset=A.longs===String?i.toString():A.longs===Number?i.toNumber():i}else o.fileOffset=A.longs===String?"0":0;if(d.Long){var i=new d.Long(0,0,!1);o.filename=A.longs===String?i.toString():A.longs===Number?i.toNumber():i}else o.filename=A.longs===String?"0":0;if(d.Long){var i=new d.Long(0,0,!1);o.buildId=A.longs===String?i.toString():A.longs===Number?i.toNumber():i}else o.buildId=A.longs===String?"0":0;o.hasFunctions=!1,o.hasFilenames=!1,o.hasLineNumbers=!1,o.hasInlineFrames=!1}return r.id!=null&&r.hasOwnProperty("id")&&(typeof r.id=="number"?o.id=A.longs===String?String(r.id):r.id:o.id=A.longs===String?d.Long.prototype.toString.call(r.id):A.longs===Number?new d.LongBits(r.id.low>>>0,r.id.high>>>0).toNumber(!0):r.id),r.memoryStart!=null&&r.hasOwnProperty("memoryStart")&&(typeof r.memoryStart=="number"?o.memoryStart=A.longs===String?String(r.memoryStart):r.memoryStart:o.memoryStart=A.longs===String?d.Long.prototype.toString.call(r.memoryStart):A.longs===Number?new d.LongBits(r.memoryStart.low>>>0,r.memoryStart.high>>>0).toNumber(!0):r.memoryStart),r.memoryLimit!=null&&r.hasOwnProperty("memoryLimit")&&(typeof r.memoryLimit=="number"?o.memoryLimit=A.longs===String?String(r.memoryLimit):r.memoryLimit:o.memoryLimit=A.longs===String?d.Long.prototype.toString.call(r.memoryLimit):A.longs===Number?new d.LongBits(r.memoryLimit.low>>>0,r.memoryLimit.high>>>0).toNumber(!0):r.memoryLimit),r.fileOffset!=null&&r.hasOwnProperty("fileOffset")&&(typeof r.fileOffset=="number"?o.fileOffset=A.longs===String?String(r.fileOffset):r.fileOffset:o.fileOffset=A.longs===String?d.Long.prototype.toString.call(r.fileOffset):A.longs===Number?new d.LongBits(r.fileOffset.low>>>0,r.fileOffset.high>>>0).toNumber(!0):r.fileOffset),r.filename!=null&&r.hasOwnProperty("filename")&&(typeof r.filename=="number"?o.filename=A.longs===String?String(r.filename):r.filename:o.filename=A.longs===String?d.Long.prototype.toString.call(r.filename):A.longs===Number?new d.LongBits(r.filename.low>>>0,r.filename.high>>>0).toNumber():r.filename),r.buildId!=null&&r.hasOwnProperty("buildId")&&(typeof r.buildId=="number"?o.buildId=A.longs===String?String(r.buildId):r.buildId:o.buildId=A.longs===String?d.Long.prototype.toString.call(r.buildId):A.longs===Number?new d.LongBits(r.buildId.low>>>0,r.buildId.high>>>0).toNumber():r.buildId),r.hasFunctions!=null&&r.hasOwnProperty("hasFunctions")&&(o.hasFunctions=r.hasFunctions),r.hasFilenames!=null&&r.hasOwnProperty("hasFilenames")&&(o.hasFilenames=r.hasFilenames),r.hasLineNumbers!=null&&r.hasOwnProperty("hasLineNumbers")&&(o.hasLineNumbers=r.hasLineNumbers),r.hasInlineFrames!=null&&r.hasOwnProperty("hasInlineFrames")&&(o.hasInlineFrames=r.hasInlineFrames),o},n.prototype.toJSON=function(){return this.constructor.toObject(this,rt.util.toJSONOptions)},n})(),e.Location=(function(){function n(a){if(this.line=[],a)for(var r=Object.keys(a),A=0;A>>3){case 1:i.id=r.uint64();break;case 2:i.mappingId=r.uint64();break;case 3:i.address=r.uint64();break;case 4:i.line&&i.line.length||(i.line=[]),i.line.push(N.perftools.profiles.Line.decode(r,r.uint32()));break;case 5:i.isFolded=r.bool();break;default:r.skipType(l&7);break}}return i},n.decodeDelimited=function(r){return r instanceof X||(r=new X(r)),this.decode(r,r.uint32())},n.verify=function(r){if(typeof r!="object"||r===null)return"object expected";if(r.id!=null&&r.hasOwnProperty("id")&&!d.isInteger(r.id)&&!(r.id&&d.isInteger(r.id.low)&&d.isInteger(r.id.high)))return"id: integer|Long expected";if(r.mappingId!=null&&r.hasOwnProperty("mappingId")&&!d.isInteger(r.mappingId)&&!(r.mappingId&&d.isInteger(r.mappingId.low)&&d.isInteger(r.mappingId.high)))return"mappingId: integer|Long expected";if(r.address!=null&&r.hasOwnProperty("address")&&!d.isInteger(r.address)&&!(r.address&&d.isInteger(r.address.low)&&d.isInteger(r.address.high)))return"address: integer|Long expected";if(r.line!=null&&r.hasOwnProperty("line")){if(!Array.isArray(r.line))return"line: array expected";for(var A=0;A>>0,r.id.high>>>0).toNumber(!0))),r.mappingId!=null&&(d.Long?(A.mappingId=d.Long.fromValue(r.mappingId)).unsigned=!0:typeof r.mappingId=="string"?A.mappingId=parseInt(r.mappingId,10):typeof r.mappingId=="number"?A.mappingId=r.mappingId:typeof r.mappingId=="object"&&(A.mappingId=new d.LongBits(r.mappingId.low>>>0,r.mappingId.high>>>0).toNumber(!0))),r.address!=null&&(d.Long?(A.address=d.Long.fromValue(r.address)).unsigned=!0:typeof r.address=="string"?A.address=parseInt(r.address,10):typeof r.address=="number"?A.address=r.address:typeof r.address=="object"&&(A.address=new d.LongBits(r.address.low>>>0,r.address.high>>>0).toNumber(!0))),r.line){if(!Array.isArray(r.line))throw TypeError(".perftools.profiles.Location.line: array expected");A.line=[];for(var o=0;o>>0,r.id.high>>>0).toNumber(!0):r.id),r.mappingId!=null&&r.hasOwnProperty("mappingId")&&(typeof r.mappingId=="number"?o.mappingId=A.longs===String?String(r.mappingId):r.mappingId:o.mappingId=A.longs===String?d.Long.prototype.toString.call(r.mappingId):A.longs===Number?new d.LongBits(r.mappingId.low>>>0,r.mappingId.high>>>0).toNumber(!0):r.mappingId),r.address!=null&&r.hasOwnProperty("address")&&(typeof r.address=="number"?o.address=A.longs===String?String(r.address):r.address:o.address=A.longs===String?d.Long.prototype.toString.call(r.address):A.longs===Number?new d.LongBits(r.address.low>>>0,r.address.high>>>0).toNumber(!0):r.address),r.line&&r.line.length){o.line=[];for(var l=0;l>>3){case 1:i.functionId=r.uint64();break;case 2:i.line=r.int64();break;default:r.skipType(l&7);break}}return i},n.decodeDelimited=function(r){return r instanceof X||(r=new X(r)),this.decode(r,r.uint32())},n.verify=function(r){return typeof r!="object"||r===null?"object expected":r.functionId!=null&&r.hasOwnProperty("functionId")&&!d.isInteger(r.functionId)&&!(r.functionId&&d.isInteger(r.functionId.low)&&d.isInteger(r.functionId.high))?"functionId: integer|Long expected":r.line!=null&&r.hasOwnProperty("line")&&!d.isInteger(r.line)&&!(r.line&&d.isInteger(r.line.low)&&d.isInteger(r.line.high))?"line: integer|Long expected":null},n.fromObject=function(r){if(r instanceof N.perftools.profiles.Line)return r;var A=new N.perftools.profiles.Line;return r.functionId!=null&&(d.Long?(A.functionId=d.Long.fromValue(r.functionId)).unsigned=!0:typeof r.functionId=="string"?A.functionId=parseInt(r.functionId,10):typeof r.functionId=="number"?A.functionId=r.functionId:typeof r.functionId=="object"&&(A.functionId=new d.LongBits(r.functionId.low>>>0,r.functionId.high>>>0).toNumber(!0))),r.line!=null&&(d.Long?(A.line=d.Long.fromValue(r.line)).unsigned=!1:typeof r.line=="string"?A.line=parseInt(r.line,10):typeof r.line=="number"?A.line=r.line:typeof r.line=="object"&&(A.line=new d.LongBits(r.line.low>>>0,r.line.high>>>0).toNumber())),A},n.toObject=function(r,A){A||(A={});var o={};if(A.defaults){if(d.Long){var i=new d.Long(0,0,!0);o.functionId=A.longs===String?i.toString():A.longs===Number?i.toNumber():i}else o.functionId=A.longs===String?"0":0;if(d.Long){var i=new d.Long(0,0,!1);o.line=A.longs===String?i.toString():A.longs===Number?i.toNumber():i}else o.line=A.longs===String?"0":0}return r.functionId!=null&&r.hasOwnProperty("functionId")&&(typeof r.functionId=="number"?o.functionId=A.longs===String?String(r.functionId):r.functionId:o.functionId=A.longs===String?d.Long.prototype.toString.call(r.functionId):A.longs===Number?new d.LongBits(r.functionId.low>>>0,r.functionId.high>>>0).toNumber(!0):r.functionId),r.line!=null&&r.hasOwnProperty("line")&&(typeof r.line=="number"?o.line=A.longs===String?String(r.line):r.line:o.line=A.longs===String?d.Long.prototype.toString.call(r.line):A.longs===Number?new d.LongBits(r.line.low>>>0,r.line.high>>>0).toNumber():r.line),o},n.prototype.toJSON=function(){return this.constructor.toObject(this,rt.util.toJSONOptions)},n})(),e.Function=(function(){function n(a){if(a)for(var r=Object.keys(a),A=0;A>>3){case 1:i.id=r.uint64();break;case 2:i.name=r.int64();break;case 3:i.systemName=r.int64();break;case 4:i.filename=r.int64();break;case 5:i.startLine=r.int64();break;default:r.skipType(l&7);break}}return i},n.decodeDelimited=function(r){return r instanceof X||(r=new X(r)),this.decode(r,r.uint32())},n.verify=function(r){return typeof r!="object"||r===null?"object expected":r.id!=null&&r.hasOwnProperty("id")&&!d.isInteger(r.id)&&!(r.id&&d.isInteger(r.id.low)&&d.isInteger(r.id.high))?"id: integer|Long expected":r.name!=null&&r.hasOwnProperty("name")&&!d.isInteger(r.name)&&!(r.name&&d.isInteger(r.name.low)&&d.isInteger(r.name.high))?"name: integer|Long expected":r.systemName!=null&&r.hasOwnProperty("systemName")&&!d.isInteger(r.systemName)&&!(r.systemName&&d.isInteger(r.systemName.low)&&d.isInteger(r.systemName.high))?"systemName: integer|Long expected":r.filename!=null&&r.hasOwnProperty("filename")&&!d.isInteger(r.filename)&&!(r.filename&&d.isInteger(r.filename.low)&&d.isInteger(r.filename.high))?"filename: integer|Long expected":r.startLine!=null&&r.hasOwnProperty("startLine")&&!d.isInteger(r.startLine)&&!(r.startLine&&d.isInteger(r.startLine.low)&&d.isInteger(r.startLine.high))?"startLine: integer|Long expected":null},n.fromObject=function(r){if(r instanceof N.perftools.profiles.Function)return r;var A=new N.perftools.profiles.Function;return r.id!=null&&(d.Long?(A.id=d.Long.fromValue(r.id)).unsigned=!0:typeof r.id=="string"?A.id=parseInt(r.id,10):typeof r.id=="number"?A.id=r.id:typeof r.id=="object"&&(A.id=new d.LongBits(r.id.low>>>0,r.id.high>>>0).toNumber(!0))),r.name!=null&&(d.Long?(A.name=d.Long.fromValue(r.name)).unsigned=!1:typeof r.name=="string"?A.name=parseInt(r.name,10):typeof r.name=="number"?A.name=r.name:typeof r.name=="object"&&(A.name=new d.LongBits(r.name.low>>>0,r.name.high>>>0).toNumber())),r.systemName!=null&&(d.Long?(A.systemName=d.Long.fromValue(r.systemName)).unsigned=!1:typeof r.systemName=="string"?A.systemName=parseInt(r.systemName,10):typeof r.systemName=="number"?A.systemName=r.systemName:typeof r.systemName=="object"&&(A.systemName=new d.LongBits(r.systemName.low>>>0,r.systemName.high>>>0).toNumber())),r.filename!=null&&(d.Long?(A.filename=d.Long.fromValue(r.filename)).unsigned=!1:typeof r.filename=="string"?A.filename=parseInt(r.filename,10):typeof r.filename=="number"?A.filename=r.filename:typeof r.filename=="object"&&(A.filename=new d.LongBits(r.filename.low>>>0,r.filename.high>>>0).toNumber())),r.startLine!=null&&(d.Long?(A.startLine=d.Long.fromValue(r.startLine)).unsigned=!1:typeof r.startLine=="string"?A.startLine=parseInt(r.startLine,10):typeof r.startLine=="number"?A.startLine=r.startLine:typeof r.startLine=="object"&&(A.startLine=new d.LongBits(r.startLine.low>>>0,r.startLine.high>>>0).toNumber())),A},n.toObject=function(r,A){A||(A={});var o={};if(A.defaults){if(d.Long){var i=new d.Long(0,0,!0);o.id=A.longs===String?i.toString():A.longs===Number?i.toNumber():i}else o.id=A.longs===String?"0":0;if(d.Long){var i=new d.Long(0,0,!1);o.name=A.longs===String?i.toString():A.longs===Number?i.toNumber():i}else o.name=A.longs===String?"0":0;if(d.Long){var i=new d.Long(0,0,!1);o.systemName=A.longs===String?i.toString():A.longs===Number?i.toNumber():i}else o.systemName=A.longs===String?"0":0;if(d.Long){var i=new d.Long(0,0,!1);o.filename=A.longs===String?i.toString():A.longs===Number?i.toNumber():i}else o.filename=A.longs===String?"0":0;if(d.Long){var i=new d.Long(0,0,!1);o.startLine=A.longs===String?i.toString():A.longs===Number?i.toNumber():i}else o.startLine=A.longs===String?"0":0}return r.id!=null&&r.hasOwnProperty("id")&&(typeof r.id=="number"?o.id=A.longs===String?String(r.id):r.id:o.id=A.longs===String?d.Long.prototype.toString.call(r.id):A.longs===Number?new d.LongBits(r.id.low>>>0,r.id.high>>>0).toNumber(!0):r.id),r.name!=null&&r.hasOwnProperty("name")&&(typeof r.name=="number"?o.name=A.longs===String?String(r.name):r.name:o.name=A.longs===String?d.Long.prototype.toString.call(r.name):A.longs===Number?new d.LongBits(r.name.low>>>0,r.name.high>>>0).toNumber():r.name),r.systemName!=null&&r.hasOwnProperty("systemName")&&(typeof r.systemName=="number"?o.systemName=A.longs===String?String(r.systemName):r.systemName:o.systemName=A.longs===String?d.Long.prototype.toString.call(r.systemName):A.longs===Number?new d.LongBits(r.systemName.low>>>0,r.systemName.high>>>0).toNumber():r.systemName),r.filename!=null&&r.hasOwnProperty("filename")&&(typeof r.filename=="number"?o.filename=A.longs===String?String(r.filename):r.filename:o.filename=A.longs===String?d.Long.prototype.toString.call(r.filename):A.longs===Number?new d.LongBits(r.filename.low>>>0,r.filename.high>>>0).toNumber():r.filename),r.startLine!=null&&r.hasOwnProperty("startLine")&&(typeof r.startLine=="number"?o.startLine=A.longs===String?String(r.startLine):r.startLine:o.startLine=A.longs===String?d.Long.prototype.toString.call(r.startLine):A.longs===Number?new d.LongBits(r.startLine.low>>>0,r.startLine.high>>>0).toNumber():r.startLine),o},n.prototype.toJSON=function(){return this.constructor.toObject(this,rt.util.toJSONOptions)},n})(),e})(),t})();Mu.exports=N});var Wu=k((SS,Yu)=>{Yu.exports=ie;var ut=null;try{ut=new WebAssembly.Instance(new WebAssembly.Module(new Uint8Array([0,97,115,109,1,0,0,0,1,13,2,96,0,1,127,96,4,127,127,127,127,1,127,3,7,6,0,1,1,1,1,1,6,6,1,127,1,65,0,11,7,50,6,3,109,117,108,0,1,5,100,105,118,95,115,0,2,5,100,105,118,95,117,0,3,5,114,101,109,95,115,0,4,5,114,101,109,95,117,0,5,8,103,101,116,95,104,105,103,104,0,0,10,191,1,6,4,0,35,0,11,36,1,1,126,32,0,173,32,1,173,66,32,134,132,32,2,173,32,3,173,66,32,134,132,126,34,4,66,32,135,167,36,0,32,4,167,11,36,1,1,126,32,0,173,32,1,173,66,32,134,132,32,2,173,32,3,173,66,32,134,132,127,34,4,66,32,135,167,36,0,32,4,167,11,36,1,1,126,32,0,173,32,1,173,66,32,134,132,32,2,173,32,3,173,66,32,134,132,128,34,4,66,32,135,167,36,0,32,4,167,11,36,1,1,126,32,0,173,32,1,173,66,32,134,132,32,2,173,32,3,173,66,32,134,132,129,34,4,66,32,135,167,36,0,32,4,167,11,36,1,1,126,32,0,173,32,1,173,66,32,134,132,32,2,173,32,3,173,66,32,134,132,130,34,4,66,32,135,167,36,0,32,4,167,11])),{}).exports}catch{}function ie(t,e,n){this.low=t|0,this.high=e|0,this.unsigned=!!n}ie.prototype.__isLong__;Object.defineProperty(ie.prototype,"__isLong__",{value:!0});function Ze(t){return(t&&t.__isLong__)===!0}ie.isLong=Ze;var Ju={},Pu={};function Jn(t,e){var n,a,r;return e?(t>>>=0,(r=0<=t&&t<256)&&(a=Pu[t],a)?a:(n=le(t,(t|0)<0?-1:0,!0),r&&(Pu[t]=n),n)):(t|=0,(r=-128<=t&&t<128)&&(a=Ju[t],a)?a:(n=le(t,t<0?-1:0,!1),r&&(Ju[t]=n),n))}ie.fromInt=Jn;function ft(t,e){if(isNaN(t))return e?Kn:pt;if(e){if(t<0)return Kn;if(t>=qu)return Vu}else{if(t<=-Uu)return We;if(t+1>=Uu)return $u}return t<0?ft(-t,e).neg():le(t%mr|0,t/mr|0,e)}ie.fromNumber=ft;function le(t,e,n){return new ie(t,e,n)}ie.fromBits=le;var oo=Math.pow;function Nl(t,e,n){if(t.length===0)throw Error("empty string");if(t==="NaN"||t==="Infinity"||t==="+Infinity"||t==="-Infinity")return pt;if(typeof e=="number"?(n=e,e=!1):e=!!e,n=n||10,n<2||360)throw Error("interior hyphen");if(a===0)return Nl(t.substring(1),e,n).neg();for(var r=ft(oo(n,8)),A=pt,o=0;o>>0:this.low};x.toNumber=function(){return this.unsigned?(this.high>>>0)*mr+(this.low>>>0):this.high*mr+(this.low>>>0)};x.toString=function(e){if(e=e||10,e<2||36>>0,c=s.toString(e);if(o=l,o.isZero())return c+i;for(;c.length<6;)c="0"+c;i=""+c+i}};x.getHighBits=function(){return this.high};x.getHighBitsUnsigned=function(){return this.high>>>0};x.getLowBits=function(){return this.low};x.getLowBitsUnsigned=function(){return this.low>>>0};x.getNumBitsAbs=function(){if(this.isNegative())return this.eq(We)?64:this.neg().getNumBitsAbs();for(var e=this.high!=0?this.high:this.low,n=31;n>0&&(e&1<=0};x.isOdd=function(){return(this.low&1)===1};x.isEven=function(){return(this.low&1)===0};x.equals=function(e){return Ze(e)||(e=Bt(e)),this.unsigned!==e.unsigned&&this.high>>>31===1&&e.high>>>31===1?!1:this.high===e.high&&this.low===e.low};x.eq=x.equals;x.notEquals=function(e){return!this.eq(e)};x.neq=x.notEquals;x.ne=x.notEquals;x.lessThan=function(e){return this.comp(e)<0};x.lt=x.lessThan;x.lessThanOrEqual=function(e){return this.comp(e)<=0};x.lte=x.lessThanOrEqual;x.le=x.lessThanOrEqual;x.greaterThan=function(e){return this.comp(e)>0};x.gt=x.greaterThan;x.greaterThanOrEqual=function(e){return this.comp(e)>=0};x.gte=x.greaterThanOrEqual;x.ge=x.greaterThanOrEqual;x.compare=function(e){if(Ze(e)||(e=Bt(e)),this.eq(e))return 0;var n=this.isNegative(),a=e.isNegative();return n&&!a?-1:!n&&a?1:this.unsigned?e.high>>>0>this.high>>>0||e.high===this.high&&e.low>>>0>this.low>>>0?-1:1:this.sub(e).isNegative()?-1:1};x.comp=x.compare;x.negate=function(){return!this.unsigned&&this.eq(We)?We:this.not().add(Cr)};x.neg=x.negate;x.add=function(e){Ze(e)||(e=Bt(e));var n=this.high>>>16,a=this.high&65535,r=this.low>>>16,A=this.low&65535,o=e.high>>>16,i=e.high&65535,l=e.low>>>16,s=e.low&65535,c=0,h=0,_=0,f=0;return f+=A+s,_+=f>>>16,f&=65535,_+=r+l,h+=_>>>16,_&=65535,h+=a+i,c+=h>>>16,h&=65535,c+=n+o,c&=65535,le(_<<16|f,c<<16|h,this.unsigned)};x.subtract=function(e){return Ze(e)||(e=Bt(e)),this.add(e.neg())};x.sub=x.subtract;x.multiply=function(e){if(this.isZero())return pt;if(Ze(e)||(e=Bt(e)),ut){var n=ut.mul(this.low,this.high,e.low,e.high);return le(n,ut.get_high(),this.unsigned)}if(e.isZero())return pt;if(this.eq(We))return e.isOdd()?We:pt;if(e.eq(We))return this.isOdd()?We:pt;if(this.isNegative())return e.isNegative()?this.neg().mul(e.neg()):this.neg().mul(e).neg();if(e.isNegative())return this.mul(e.neg()).neg();if(this.lt(Ou)&&e.lt(Ou))return ft(this.toNumber()*e.toNumber(),this.unsigned);var a=this.high>>>16,r=this.high&65535,A=this.low>>>16,o=this.low&65535,i=e.high>>>16,l=e.high&65535,s=e.low>>>16,c=e.low&65535,h=0,_=0,f=0,m=0;return m+=o*c,f+=m>>>16,m&=65535,f+=A*c,_+=f>>>16,f&=65535,f+=o*s,_+=f>>>16,f&=65535,_+=r*c,h+=_>>>16,_&=65535,_+=A*s,h+=_>>>16,_&=65535,_+=o*l,h+=_>>>16,_&=65535,h+=a*c+r*s+A*l+o*i,h&=65535,le(f<<16|m,h<<16|_,this.unsigned)};x.mul=x.multiply;x.divide=function(e){if(Ze(e)||(e=Bt(e)),e.isZero())throw Error("division by zero");if(ut){if(!this.unsigned&&this.high===-2147483648&&e.low===-1&&e.high===-1)return this;var n=(this.unsigned?ut.div_u:ut.div_s)(this.low,this.high,e.low,e.high);return le(n,ut.get_high(),this.unsigned)}if(this.isZero())return this.unsigned?Kn:pt;var a,r,A;if(this.unsigned){if(e.unsigned||(e=e.toUnsigned()),e.gt(this))return Kn;if(e.gt(this.shru(1)))return zu;A=Kn}else{if(this.eq(We)){if(e.eq(Cr)||e.eq(Sl))return We;if(e.eq(We))return Cr;var o=this.shr(1);return a=o.div(e).shl(1),a.eq(pt)?e.isNegative()?Cr:Sl:(r=this.sub(e.mul(a)),A=a.add(r.div(e)),A)}else if(e.eq(We))return this.unsigned?Kn:pt;if(this.isNegative())return e.isNegative()?this.neg().div(e.neg()):this.neg().div(e).neg();if(e.isNegative())return this.div(e.neg()).neg();A=pt}for(r=this;r.gte(e);){a=Math.max(1,Math.floor(r.toNumber()/e.toNumber()));for(var i=Math.ceil(Math.log(a)/Math.LN2),l=i<=48?1:oo(2,i-48),s=ft(a),c=s.mul(e);c.isNegative()||c.gt(r);)a-=l,s=ft(a,this.unsigned),c=s.mul(e);s.isZero()&&(s=Cr),A=A.add(s),r=r.sub(c)}return A};x.div=x.divide;x.modulo=function(e){if(Ze(e)||(e=Bt(e)),ut){var n=(this.unsigned?ut.rem_u:ut.rem_s)(this.low,this.high,e.low,e.high);return le(n,ut.get_high(),this.unsigned)}return this.sub(this.div(e).mul(e))};x.mod=x.modulo;x.rem=x.modulo;x.not=function(){return le(~this.low,~this.high,this.unsigned)};x.and=function(e){return Ze(e)||(e=Bt(e)),le(this.low&e.low,this.high&e.high,this.unsigned)};x.or=function(e){return Ze(e)||(e=Bt(e)),le(this.low|e.low,this.high|e.high,this.unsigned)};x.xor=function(e){return Ze(e)||(e=Bt(e)),le(this.low^e.low,this.high^e.high,this.unsigned)};x.shiftLeft=function(e){return Ze(e)&&(e=e.toInt()),(e&=63)===0?this:e<32?le(this.low<>>32-e,this.unsigned):le(0,this.low<>>e|this.high<<32-e,this.high>>e,this.unsigned):le(this.high>>e-32,this.high>=0?0:-1,this.unsigned)};x.shr=x.shiftRight;x.shiftRightUnsigned=function(e){if(Ze(e)&&(e=e.toInt()),e&=63,e===0)return this;var n=this.high;if(e<32){var a=this.low;return le(a>>>e|n<<32-e,n>>>e,this.unsigned)}else return e===32?le(n,0,this.unsigned):le(n>>>e-32,0,this.unsigned)};x.shru=x.shiftRightUnsigned;x.shr_u=x.shiftRightUnsigned;x.toSigned=function(){return this.unsigned?le(this.low,this.high,!1):this};x.toUnsigned=function(){return this.unsigned?this:le(this.low,this.high,!0)};x.toBytes=function(e){return e?this.toBytesLE():this.toBytesBE()};x.toBytesLE=function(){var e=this.high,n=this.low;return[n&255,n>>>8&255,n>>>16&255,n>>>24,e&255,e>>>8&255,e>>>16&255,e>>>24]};x.toBytesBE=function(){var e=this.high,n=this.low;return[e>>>24,e>>>16&255,e>>>8&255,e&255,n>>>24,n>>>16&255,n>>>8&255,n&255]};ie.fromBytes=function(e,n,a){return a?ie.fromBytesLE(e,n):ie.fromBytesBE(e,n)};ie.fromBytesLE=function(e,n){return new ie(e[0]|e[1]<<8|e[2]<<16|e[3]<<24,e[4]|e[5]<<8|e[6]<<16|e[7]<<24,n)};ie.fromBytesBE=function(e,n){return new ie(e[4]<<24|e[5]<<16|e[6]<<8|e[7],e[0]<<24|e[1]<<16|e[2]<<8|e[3],n)}});function uy(t){let e=t.defaultSampleType,n=t.sampleType,a=n.length-1;if(!e||!+e)return a;let r=n.findIndex(A=>A.type===e);return r===-1?a:r}function ef(t){if(t.byteLength===0)return null;let e;try{e=Zu.perftools.profiles.Profile.decode(new Uint8Array(t))}catch{return null}function n(_){return typeof _=="number"?_:_.low}function a(_){return e.stringTable[n(_)]||null}let r=new Map;function A(_){let{name:f,filename:m,startLine:u}=_,g=f!=null&&a(f)||"(unknown)",p=m!=null?a(m):null,I=u!=null?+u:null,E={key:`${g}:${p}:${I}`,name:g};return p!=null&&(E.file=p),I!=null&&(E.line=I),E}for(let _ of e.function)if(_.id){let f=A(_);f!=null&&r.set(n(_.id),f)}function o(_){let{line:f}=_;if(f==null)return null;let m=Ae(f);if(m==null)return null;if(m.functionId){let u=r.get(n(m.functionId)),g=m.line instanceof Xu.default?m.line.toNumber():m.line;return g&&g>0&&u!=null&&(u.line=g),u||null}else return null}let i=new Map;for(let _ of e.location)if(_.id!=null){let f=o(_);f&&i.set(n(_.id),f)}let l=e.sampleType.map(_=>({type:_.type&&a(_.type)||"samples",unit:_.unit&&a(_.unit)||"count"})),s=uy(e);if(s<0||s>=l.length)return null;let c=l[s],h=new ae;switch(c.unit){case"nanoseconds":case"microseconds":case"milliseconds":case"seconds":h.setValueFormatter(new ee(c.unit));break;case"bytes":h.setValueFormatter(new $e);break}for(let _ of e.sample){let f=_.locationId?_.locationId.map(u=>i.get(n(u))):[];if(f.reverse(),_.value==null||_.value.length<=s)return null;let m=_.value[s];h.appendSampleWithWeight(f.filter(u=>u!=null),+m)}return h.build()}var Zu,Xu,tf=re(()=>{"use strict";Zu=he(Ku());ke();V();Ke();Xu=he(Wu())});function py(t){return De(fy,t,e=>{let n=e.url,a=e.lineNumber,r=e.columnNumber,A=e.functionName||(n?`(anonymous ${n.split("/").pop()}:${a})`:"(anonymous)");return{key:`${A}:${n}:${a}:${r}`,name:A,file:n,line:a,col:r}})}function Fl(t){let e=new Map,n=0,a=(l,s)=>{l.id=n++,e.set(l.id,l),s&&(l.parent=s.id),l.children.forEach(c=>a(c,l))};a(t.head);let r=l=>{if(l.children.length===0)return l.selfSize||0;let s=l.children.reduce((c,h)=>(c+=r(h),c),l.selfSize);return l.totalSize=s,s},A=r(t.head),o=[];for(let l of e.values()){let s=[];for(s.push(l);l.parent!==void 0;){let c=e.get(l.parent);if(c===void 0)break;s.unshift(c),l=c}o.push(s)}let i=new ae(A);for(let l of o){let s=l[l.length-1];i.appendSampleWithWeight(l.map(c=>py(c.callFrame)),s.selfSize)}return i.setValueFormatter(new $e),i.build()}var fy,nf=re(()=>{"use strict";ke();V();Ke();fy=new Map});function io(t,e){return`${ln(""+t,10)}:${ln(""+e,10)}`}function rf(t){let e=new Map;for(let n of t)De(e,io(Number(n.pid),Number(n.tid)),()=>[]).push(n);return e}function my(t,e){if(t.length===0&&e.length===0)throw new Error("This method should not be given both queues empty");if(e.length===0)return"B";if(t.length===0)return"E";let n=t[0],a=e[0],r=n.ts,A=a.ts;return r0){let o=Number.MAX_SAFE_INTEGER;for(let i of t)o=Math.min(o,i.ts);for(let i of t)i.ts-=o}let a=[];for(let o of t)switch(o.ph){case"B":{e.push(o);break}case"E":{n.push(o);break}case"X":{a.push(o);break}default:return o}function r(o){return o.dur??o.tdur??0}a.sort((o,i)=>{if(o.tsi.ts)return 1;let l=r(o),s=r(i);return l>s?-1:li.ts?1:0}return e.sort(A),n.sort(A),[e,n]}function jy(t){let e=[];for(let n of t)switch(n.ph){case"B":case"E":case"X":e.push(n)}return e}function af(t){let e=new Map;for(let n of t)n.ph==="M"&&n.name==="process_name"&&n.args&&n.args.name&&e.set(n.pid,n.args.name);return e}function Af(t){let e=new Map;for(let n of t)n.ph==="M"&&n.name==="thread_name"&&n.args&&n.args.name&&e.set(io(n.pid,n.tid),n.args.name);return e}function of(t){return`${t.name||"(unnamed)"}`}function Rl(t){let e=of(t);return t.args&&(e+=` ${JSON.stringify(t.args)}`),e}function Pn(t,e="UNKNOWN"){if(e==="HERMES"){let a=`${t.name}:${t.args.url}:${t.args.line}:${t.args.column}`;return{name:of(t),key:a,file:t.args.url,line:t.args.line,col:t.args.column}}let n=Rl(t);return{name:n,key:n}}function lf(t,e,n,a){return t!=null&&e!=null?`${t} (pid ${n}), ${e} (tid ${a})`:t!=null?`${t} (pid ${n}, tid ${a})`:e!=null?`${e} (pid ${n}, tid ${a})`:`pid ${n}, tid ${a}`}function vy(t,e){let n=af(t),a=Af(t),r=new Map;return e.forEach(A=>{if(A.length===0)return;let o=Number(A[0].pid),i=Number(A[0].tid),l=io(o,i),s=n.get(o),c=a.get(l),h=lf(s,c,o,i);r.set(l,h)}),r}function yy(t,e){let n=af(t),a=Af(t),r=new Map;return e.forEach(A=>{if(A.length===0)return;let{pid:o,tid:i}=A[0],l=io(o,i),s=n.get(o),c=a.get(l),h=lf(s,c,o,i);r.set(l,h)}),r}function Ey(t,e,n="UNKNOWN"){let[a,r]=Iy(t),A=new Ie;A.setValueFormatter(new ee("microseconds")),A.setName(e);let o=[],i=s=>{o.push(s),A.enterFrame(Pn(s,n),s.ts)},l=s=>{let c=Ae(o);if(c==null){console.warn(`Tried to end frame "${Pn(s,n).key}", but the stack was empty. Doing nothing instead.`);return}let h=Pn(s,n),_=Pn(c,n);if(s.name!==c.name){console.warn(`ts=${s.ts}: Tried to end "${h.key}" when "${_.key}" was on the top of the stack. Doing nothing instead.`);return}h.key!==_.key&&console.warn(`ts=${s.ts}: Tried to end "${h.key}" when "${_.key}" was on the top of the stack. Ending ${_.key} instead.`),o.pop(),A.leaveFrame(_,s.ts)};for(;a.length>0||r.length>0;){let s=my(a,r);switch(s){case"B":{i(a.shift());break}case"E":{let h=Ae(o);if(h!=null){let f=Pn(h,n),m=!1;for(let u=1;ur[0].ts)break;let p=Pn(g,n);if(f.key===p.key){let I=r[0];r[0]=r[u],r[u]=I,m=!0;break}}if(!m)for(let u=1;ur[0].ts)break;if(g.name===h.name){let p=r[0];r[0]=r[u],r[u]=p,m=!0;break}}}let _=r.shift();l(_);break}default:return s}}for(let s=o.length-1;s>=0;s--){let c=Pn(o[s],n);console.warn(`Frame "${c.key}" was still open at end of profile. Closing automatically.`),A.leaveFrame(c,A.getTotalWeight())}return A.build()}function By(t){let e=[],n=Number(t[0].ts);return t.forEach((a,r)=>{if(r===0)return;let A=Number(a.ts)-n;n=Number(a.ts),e.push(A)}),e.push(0),e}function Qy({name:t,category:e}){return{key:`${t}:${e}`,name:t}}function wy(t,e){let n=[],a=e;for(;a;){let r=t[a];if(!r)throw new Error(`Could not find frame for id ${a}`);n.push(Qy(r)),a=r.parent}return n.reverse()}function by(t,e,n){let a=new ae;a.setValueFormatter(new ee("microseconds")),a.setName(n);let r=By(e);return e.forEach((A,o)=>{let i=r[o],l=wy(t.stackFrames,A.sf);a.appendSampleWithWeight(l,i)}),a.build()}function Dl(t,e="UNKNOWN"){let n=jy(t),a=rf(n),r=yy(t,a),A=[];return r.forEach((o,i)=>{let l=a.get(i);if(!l)throw new Error(`Could not find events for key: ${l}`);A.push([i,Ey(l,o,e)])}),Fe(A,o=>o[0]),{name:"",indexToView:0,profiles:A.map(o=>o[1])}}function xy(t){let e=rf(t.samples),n=vy(t.traceEvents,e),a=[];return n.forEach((r,A)=>{let o=e.get(A);if(!o)throw new Error(`Could not find samples for key: ${o}`);o.length!==0&&a.push([A,by(t,o,r)])}),Fe(a,r=>r[0]),{name:"",indexToView:0,profiles:a.map(r=>r[1])}}function _a(t){if(!Array.isArray(t)||t.length===0)return!1;for(let e of t){if(!("ph"in e))return!1;switch(e.ph){case"B":case"E":case"X":if(!("ts"in e))return!1;case"M":break}}return!0}function ky(t){return t?Cy.every(e=>e in t):!1}function Sy(t){return _a(t)?ky(t[0].args):!1}function sf(t){return"traceEvents"in t?_a(t.traceEvents):!1}function Ny(t){return"traceEvents"in t&&"stackFrames"in t&&"samples"in t&&_a(t.traceEvents)}function cf(t){return sf(t)||_a(t)}function _f(t){return Ny(t)?xy(t):sf(t)?Dl(t.traceEvents):Sy(t)?Dl(t,"HERMES"):_a(t)?Dl(t):t}var Cy,hf=re(()=>{"use strict";V();ke();Ke();Cy=["line","column","name","category","url","params","allocatedCategory","allocatedName"]});function Hl(t,e){return new Tl(t,e).parse()}var Ll,Tl,gf=re(()=>{"use strict";ke();V();Ke();Ll=class{constructor(e,n){this.fileName=e;this.fieldName=n;this.frameSet=new bt;this.totalWeights=new Map;this.childrenTotalWeights=new Map}getOrInsertFrame(e){return me.getOrInsert(this.frameSet,e)}addToTotalWeight(e,n){this.totalWeights.has(e)?this.totalWeights.set(e,this.totalWeights.get(e)+n):this.totalWeights.set(e,n)}addSelfWeight(e,n){this.addToTotalWeight(this.getOrInsertFrame(e),n)}addChildWithTotalWeight(e,n,a){let r=this.getOrInsertFrame(e),A=this.getOrInsertFrame(n),o=De(this.childrenTotalWeights,r,i=>new Map);o.has(A)?o.set(A,o.get(A)+a):o.set(A,a),this.addToTotalWeight(r,a)}toProfile(){let e=new Ie,n=1;this.fieldName==="Time_(10ns)"?(e.setName(`${this.fileName} -- Time`),n=10,e.setValueFormatter(new ee("nanoseconds"))):this.fieldName=="Memory_(bytes)"?(e.setName(`${this.fileName} -- Memory`),e.setValueFormatter(new $e)):e.setName(`${this.fileName} -- ${this.fieldName}`);let a=0,r=new Set,A=0;for(let[l,s]of this.totalWeights)A=Math.max(A,s);let o=(l,s)=>{if(r.has(l)||s<1e-4*A)return;let c=ja(this.totalWeights,l,()=>0);if(c===0)return;let h=s;e.enterFrame(l,Math.round(a*n)),r.add(l);for(let[_,f]of this.childrenTotalWeights.get(l)||[]){let m=s*(f/c),u=a;o(_,m);let g=a-u;h-=g}r.delete(l),a+=h,e.leaveFrame(l,Math.round(a*n))},i=new Set(this.frameSet);for(let[l,s]of this.childrenTotalWeights)for(let[c,h]of s)i.delete(c);for(let l of i)o(l,this.totalWeights.get(l));return e.build()}},Tl=class{constructor(e,n){this.importedFileName=n;this.callGraphs=null;this.eventsLine=null;this.filename=null;this.functionName=null;this.calleeFilename=null;this.calleeFunctionName=null;this.savedFileNames={};this.savedFunctionNames={};this.prevCostLineNumbers=[];this.lines=[...e.splitLines()],this.lineNum=0}parse(){for(;this.lineNume.toProfile())}:null}frameInfo(){let e=this.filename||"(unknown)",n=this.functionName||"(unknown)";return{key:`${e}:${n}`,name:n,file:e}}calleeFrameInfo(){let e=this.calleeFilename||this.filename||"(unknown)",n=this.calleeFunctionName||"(unknown)";return{key:`${e}:${n}`,name:n,file:e}}parseHeaderLine(e){let n=/^\s*(\w+):\s*(.*)+$/.exec(e);if(!n)return!1;if(n[1]!=="events")return!0;let a=n[2].split(" ");if(this.callGraphs!=null)throw new Error(`Duplicate "events: " lines specified. First was "${this.eventsLine}", now received "${e}" on ${this.lineNum}.`);return this.callGraphs=a.map(r=>new Ll(this.importedFileName,r)),!0}parseAssignmentLine(e){let n=/^(\w+)=\s*(.*)$/.exec(e);if(!n)return!1;let a=n[1],r=n[2];switch(a){case"fe":case"fi":{this.parseNameWithCompression(r,this.savedFileNames);break}case"fl":{this.filename=this.parseNameWithCompression(r,this.savedFileNames);break}case"fn":{this.functionName=this.parseNameWithCompression(r,this.savedFunctionNames);break}case"cfi":case"cfl":{this.calleeFilename=this.parseNameWithCompression(r,this.savedFileNames);break}case"cfn":{this.calleeFunctionName=this.parseNameWithCompression(r,this.savedFunctionNames);break}case"calls":{this.parseCostLine(this.lines[this.lineNum++],"child"),this.calleeFilename=null,this.calleeFunctionName=null;break}case"cob":case"ob":break;default:console.log(`Ignoring assignment to unrecognized key "${e}" on line ${this.lineNum}`)}return!0}parseNameWithCompression(e,n){{let a=/^\((\d+)\)\s*(.+)$/.exec(e);if(a){let r=a[1],A=a[2];if(r in n)throw new Error(`Redefinition of name with id: ${r}. Original value was "${n[r]}". Tried to redefine as "${A}" on line ${this.lineNum}.`);return n[r]=A,A}}{let a=/^\((\d+)\)$/.exec(e);if(a){let r=a[1];if(!(r in n))throw new Error(`Tried to use name with id ${r} on line ${this.lineNum} before it was defined.`);return n[r]}}return e}parseCostLine(e,n){let a=e.split(/\s+/),r=[];for(let o=0;o!/^$|^Log closed$|log opened/.exec(p)),a=-1,r=g(n[0]);if(r===null)throw Error;a=r.at;let A=Ae(n);if(A===null)throw Error;let o=g(A);if(o===null)throw Error;let i=o.at,l=new bt,s=[],c=0,h,_=-1;function f(p,I,j){function E(b,F){s.push(F),e.enterFrame(me.getOrInsert(l,{name:F,key:F}),b),c=b}_>-1&&(_=-1,h===j&&_>=c&&f(p,_,`QUEUE ${j}`));let y=`STACK ${p}`;[...s].reverse().find(b=>b.startsWith("STACK "))!==y&&(s.length===1&&m(c),E(I,y)),E(I,j)}function m(p){let I=s.pop();if(I===void 0)throw Error("Tried to leave frame when nothing was on stack.");e.leaveFrame(me.getOrInsert(l,{name:I,key:I}),p);let j=Ae(s);j!==null&&j.startsWith("QUEUE ")&&(m(p),j=Ae(s)),s.length>1&&j!==null&&j.startsWith("STACK ")&&m(p),c=p}function u(p,I,j){Ae(s)===j?m(I):c===0?(console.log(`Tried to leave frame "${j}" which was never entered. Assuming it has been running since the start.`),f(p,0,j),m(I)):console.log(`Tried to leave frame "${j}" which was never entered. Other events have happened since the start, ignoring line.`)}function g(p){if(p===void 0)throw Error("Probably tried to import empty file.");let I=p.split(":");return I.length<3?null:a!==-1?{at:parseInt(I[0])-a,event:I[1],stackInt:parseInt(I[2]),name:I[5]}:{at:parseInt(I[0]),event:I[1],stackInt:parseInt(I[2]),name:I[5]}}for(n.forEach((p,I,j)=>{let E=g(p);if(E!==null){if(E.event==="PUSH"){f(E.stackInt,E.at,E.name),I+=1;let y=g(j[I]);for(;y!==null&&y.at===E.at;)y.name===E.name&&y.stackInt===E.stackInt&&y.event==="POP"?(u(y.stackInt,y.at,y.name),j.splice(I,1),y=null):(I+=1,I0;)m(i);return e.build()}var uf=re(()=>{"use strict";ke();V();Ke()});function Ml(t){let e=new ae,n=[],a,r="0",A=-1;for(let l of t.splitLines()){let s=/^( *)[\d.]+% \[(\d+)\]\s*(\S+)(?: @ (.*))?$/gm.exec(l);if(!s)continue;let c=s[1].length;if(c<=A){let _=n.slice(0,A+1).reverse(),f=parseInt(r,10);e.appendSampleWithWeight(_,f)}let h=s[3];a=s[4]||a,n[c]={key:h,name:h,file:a},r=s[2],A=c}if(A==-1)return null;let o=n.slice(0,A+1).reverse(),i=parseInt(r,10);return e.appendSampleWithWeight(o,i),e.build()}var ff=re(()=>{"use strict";ke()});function lo(t){da===An.length&&An.push(An.length+1);let e=da;return da=An[e],An[e]=t,e}function Fy(t){t<132||(An[t]=da,da=t)}function mf(t,e){t=t>>>0;let n=Jt(),a=[];for(let r=t;r>>0,Hy(t,e)}function ga(){return(ha===null||ha.byteLength===0)&&(ha=new Uint8Array(ne.memory.buffer)),ha}function If(t){return An[t]}function Dy(t,e){let n=e(t.length*1,1)>>>0;return ga().set(t,n/1),Un=t.length,n}function Ry(t,e){let n=e(t.length*4,4)>>>0,a=Jt();for(let r=0;r>>0;return ga().subarray(l,l+i.length).set(i),Un=i.length,l}let a=t.length,r=e(a,1)>>>0,A=ga(),o=0;for(;o127)break;A[r+o]=i}if(o!==a){o!==0&&(t=t.slice(o)),r=n(r,a,a=o+t.length*3,1)>>>0;let i=ga().subarray(r+o,r+a),l=ua.encodeInto(t,i);o+=l.written,r=n(r,a,o,1)>>>0}return Un=o,r}function Pl(t){let e=If(t);return Fy(t),e}function Hy(t,e){return Kl+=e,Kl>=Ty&&(so=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0}),so.decode(),Kl=e),so.decode(ga().subarray(t,t+e))}function jf(t,e){try{let i=ne.__wbindgen_add_to_stack_pointer(-16),l=Dy(t,ne.__wbindgen_export2),s=Un;ne.interpret_jfr(i,l,s,e);var n=Jt().getInt32(i+0,!0),a=Jt().getInt32(i+4,!0),r=Jt().getInt32(i+8,!0),A=Jt().getInt32(i+12,!0);if(A)throw Pl(r);var o=mf(n,a).slice();return ne.__wbindgen_export(n,a*4,4),o}finally{ne.__wbindgen_add_to_stack_pointer(16)}}async function Ky(t,e){if(typeof Response=="function"&&t instanceof Response){if(typeof WebAssembly.instantiateStreaming=="function")try{return await WebAssembly.instantiateStreaming(t,e)}catch(a){if(t.ok&&My.has(t.type)&&t.headers.get("Content-Type")!=="application/wasm")console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",a);else throw a}let n=await t.arrayBuffer();return await WebAssembly.instantiate(n,e)}else{let n=await WebAssembly.instantiate(t,e);return n instanceof WebAssembly.Instance?{instance:n,module:t}:n}}function Jy(){let t={};return t.wbg={},t.wbg.__wbg___wbindgen_throw_dd24417ed36fc46e=function(e,n){throw new Error(Jl(e,n))},t.wbg.__wbg_frame_new=function(e){let n=Ir.__wrap(e);return lo(n)},t.wbg.__wbg_frame_unwrap=function(e){return Ir.__unwrap(If(e))},t.wbg.__wbg_methodsample_new=function(e){let n=fa.__wrap(e);return lo(n)},t.wbg.__wbindgen_cast_2241b6af4c4b2941=function(e,n){let a=Jl(e,n);return lo(a)},t.wbg.__wbindgen_object_drop_ref=function(e){Pl(e)},t}function Py(t,e){return ne=t.exports,vf.__wbindgen_wasm_module=e,Gn=null,ha=null,ne}async function vf(t){if(ne!==void 0)return ne;typeof t<"u"&&(Object.getPrototypeOf(t)===Object.prototype?{module_or_path:t}=t:console.warn("using deprecated parameters for the initialization function; pass a single object instead")),typeof t>"u"&&(t=new URL("jfrview_bg.wasm",Gy.url));let e=Jy();(typeof t=="string"||typeof Request=="function"&&t instanceof Request||typeof URL=="function"&&t instanceof URL)&&(t=fetch(t));let{instance:n,module:a}=await Ky(await t,e);return Py(n,a)}var Gy,ne,Gn,ha,An,da,so,Ty,Kl,ua,Un,pf,Cf,Ir,fa,My,yf,Ef=re(()=>{Gy={};Gn=null;ha=null;An=new Array(128).fill(void 0);An.push(void 0,null,!0,!1);da=An.length;so=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0});so.decode();Ty=2146435072,Kl=0;ua=new TextEncoder;"encodeInto"in ua||(ua.encodeInto=function(t,e){let n=ua.encode(t);return e.set(n),{read:t.length,written:n.length}});Un=0,pf=typeof FinalizationRegistry>"u"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(t=>ne.__wbg_frame_free(t>>>0,1)),Cf=typeof FinalizationRegistry>"u"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(t=>ne.__wbg_methodsample_free(t>>>0,1)),Ir=class t{static __wrap(e){e=e>>>0;let n=Object.create(t.prototype);return n.__wbg_ptr=e,pf.register(n,n.__wbg_ptr,n),n}static __unwrap(e){return e instanceof t?e.__destroy_into_raw():0}__destroy_into_raw(){let e=this.__wbg_ptr;return this.__wbg_ptr=0,pf.unregister(this),e}free(){let e=this.__destroy_into_raw();ne.__wbg_frame_free(e,0)}get name(){let e,n;try{let A=ne.__wbindgen_add_to_stack_pointer(-16);ne.__wbg_get_frame_name(A,this.__wbg_ptr);var a=Jt().getInt32(A+0,!0),r=Jt().getInt32(A+4,!0);return e=a,n=r,Jl(a,r)}finally{ne.__wbindgen_add_to_stack_pointer(16),ne.__wbindgen_export(e,n,1)}}set name(e){let n=Ly(e,ne.__wbindgen_export2,ne.__wbindgen_export3),a=Un;ne.__wbg_set_frame_name(this.__wbg_ptr,n,a)}};Symbol.dispose&&(Ir.prototype[Symbol.dispose]=Ir.prototype.free);fa=class t{static __wrap(e){e=e>>>0;let n=Object.create(t.prototype);return n.__wbg_ptr=e,Cf.register(n,n.__wbg_ptr,n),n}__destroy_into_raw(){let e=this.__wbg_ptr;return this.__wbg_ptr=0,Cf.unregister(this),e}free(){let e=this.__destroy_into_raw();ne.__wbg_methodsample_free(e,0)}get frames(){try{let r=ne.__wbindgen_add_to_stack_pointer(-16);ne.__wbg_get_methodsample_frames(r,this.__wbg_ptr);var e=Jt().getInt32(r+0,!0),n=Jt().getInt32(r+4,!0),a=mf(e,n).slice();return ne.__wbindgen_export(e,n*4,4),a}finally{ne.__wbindgen_add_to_stack_pointer(16)}}set frames(e){let n=Ry(e,ne.__wbindgen_export2),a=Un;ne.__wbg_set_methodsample_frames(this.__wbg_ptr,n,a)}};Symbol.dispose&&(fa.prototype[Symbol.dispose]=fa.prototype.free);My=new Set(["basic","cors","default"]);yf=vf});var Qf,Bf=re(()=>{Qf="./jfrview_bg-BLJXNNQB.wasm"});async function Gl(t,e){await yf({module_or_path:Qf});let n=wf(e,!1),a=wf(e,!0);return{indexToView:0,name:t,profiles:[n,a]}}function bf(t){let e=t.slice(0,3),n=new Uint8Array(e);return n[0]===70&&n[1]===76&&n[2]===82}function wf(t,e){let n=jf(new Uint8Array(t),e),a=new ae(n.length);e?a.setName("With native calls"):a.setName("Without native calls");function r(A){return{name:A.name,key:A.name}}for(let A of n)a.appendSampleWithWeight(A.frames.map(r),1);return a.build()}var xf=re(()=>{"use strict";ke();Ef();Bf()});var kf={};$l(kf,{importFromFileSystemDirectoryEntry:()=>Yy,importProfileGroupFromBase64:()=>qy,importProfileGroupFromText:()=>Oy,importProfilesFromArrayBuffer:()=>$y,importProfilesFromFile:()=>zy});async function Oy(t,e){return await co(new XA(t,e))}async function qy(t,e){return await co(an.fromArrayBuffer(t,cs(e).buffer))}async function zy(t){return co(an.fromFile(t))}async function $y(t,e){return co(an.fromArrayBuffer(t,e))}async function co(t){let e=await t.name(),n=await Vy(t);if(n){n.name||(n.name=e);for(let a of n.profiles)a&&!a.getName()&&a.setName(e);return n}return null}function Qe(t){return t?{name:t.getName(),indexToView:0,profiles:[t]}:null}async function Vy(t){let e=await t.name(),n=await t.readAsArrayBuffer();{let A=ef(n);if(A)return console.log("Importing as protobuf encoded pprof file"),Qe(A)}let a=await t.readAsText();if(e.endsWith(".speedscope.json"))return console.log("Importing as speedscope json file"),Wo(a.parseAsJSON());if(/Trace-\d{8}T\d{6}/.exec(e))return console.log("Importing as Chrome Timeline Object"),Kr(a.parseAsJSON().traceEvents,e);if(e.endsWith(".chrome.json")||/Profile-\d{8}T\d{6}/.exec(e))return console.log("Importing as Chrome Timeline"),Kr(a.parseAsJSON(),e);if(e.endsWith(".stackprof.json"))return console.log("Importing as stackprof profile"),Qe(xi(a.parseAsJSON()));if(e.endsWith(".instruments.txt"))return console.log("Importing as Instruments.app deep copy"),Qe(dl(a));if(e.endsWith(".linux-perf.txt"))return console.log("Importing as output of linux perf script"),pl(a);if(e.endsWith(".collapsedstack.txt"))return console.log("Importing as collapsed stack format"),Qe(ul(a));if(e.endsWith(".v8log.json"))return console.log("Importing as --prof-process v8 log"),Qe(fl(a.parseAsJSON()));if(e.endsWith(".heapprofile"))return console.log("Importing as Chrome Heap Profile"),Qe(Fl(a.parseAsJSON()));if(e.endsWith("-recording.json"))return console.log("Importing as Safari profile"),Qe(ml(a.parseAsJSON()));if(e.startsWith("callgrind."))return console.log("Importing as Callgrind profile"),Hl(a,e);if(e.endsWith(".pmcstat.graph"))return console.log("Importing as pmcstat callgraph format"),Qe(Ml(a));if(e.endsWith(".jfr"))return console.log("Importing as Java Flight Recorder profile"),await Gl(e,n);let r;try{r=a.parseAsJSON()}catch{}if(r){if(r.$schema==="https://www.speedscope.app/file-format-schema.json")return console.log("Importing as speedscope json file"),Wo(r);if(r.systemHost&&r.systemHost.name=="Firefox")return console.log("Importing as Firefox profile"),Qe(Dd(r));if(bi(r))return console.log("Importing as Chrome Timeline"),Kr(r,e);if(Fh(r))return console.log("Importing as Chrome Timeline Object"),Kr(r.traceEvents,e);if("nodes"in r&&"samples"in r&&"timeDeltas"in r)return console.log("Importing as Chrome CPU Profile"),Qe(DA(r));if(cf(r))return console.log("Importing as Trace Event Format profile"),_f(r);if("head"in r&&"samples"in r&&"timestamps"in r)return console.log("Importing as Chrome CPU Profile (old format)"),Qe(Dh(r));if("mode"in r&&"frames"in r&&"raw_timestamp_deltas"in r)return console.log("Importing as stackprof profile"),Qe(xi(r));if("code"in r&&"functions"in r&&"ticks"in r)return console.log("Importing as --prof-process v8 log"),Qe(fl(r));if("head"in r&&"selfSize"in r.head)return console.log("Importing as Chrome Heap Profile"),Qe(Fl(r));if("rts_arguments"in r&&"initial_capabilities"in r)return console.log("Importing as Haskell GHC JSON Profile"),Md(r);if("recording"in r&&"sampleStackTraces"in r.recording)return console.log("Importing as Safari profile"),Qe(ml(r))}else{if(/^# callgrind format/.exec(a.firstChunk())||/^events:/m.exec(a.firstChunk())&&/^fn=/m.exec(a.firstChunk()))return console.log("Importing as Callgrind profile"),Hl(a,e);if(/^[\w \t\(\)]*\tSymbol Name/.exec(a.firstChunk()))return console.log("Importing as Instruments.app deep copy"),Qe(dl(a));if(/^(Stack_|Script_|Obj_)\S+ log opened \(PC\)\n/.exec(a.firstChunk()))return console.log("Importing as Papyrus profile"),Qe(df(a));if(bf(n))return console.log("Importing as Java Flight Recorder profile"),Gl(e,n);let A=pl(a);if(A)return console.log("Importing from linux perf script output"),A;let o=ul(a);if(o)return console.log("Importing as collapsed stack format"),Qe(o);let i=Ml(a);if(i)return console.log("Importing as pmcstat callgraph format"),Qe(i)}return null}async function Yy(t){return Sd(t)}var Sf=re(()=>{"use strict";Rh();Lh();Nd();Fd();Rd();Zo();Ld();Hd();Kd();Jd();_l();tf();V();nf();hf();gf();uf();ff();xf()});var Nf=k((GN,Wy)=>{Wy.exports="./perf-vertx-stacks-01-collapsed-all-ZNUIGAJL.txt"});var J,Gf,yr,uo,Zl,Vl,Xl,es,Gt={},fo=[],Uf=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord/i;function jn(t,e){for(var n in e)t[n]=e[n];return t}function ts(t){var e=t.parentNode;e&&e.removeChild(t)}function C(t,e,n){var a,r=arguments,A={};for(a in e)a!=="key"&&a!=="ref"&&(A[a]=e[a]);if(arguments.length>3)for(n=[n],a=3;a=this.capacity;)this.map.delete(this.list.pop().data);let r=this.list.prepend(new Io(e));this.map.set(e,{value:n,listNode:r})}getOrInsert(e,n){let a=this.get(e);return a==null&&(a=n(e),this.insert(e,a)),a}removeLRU(){let e=this.list.pop();if(!e)return null;let n=e.data,a=this.map.get(n).value;return this.map.delete(n),[n,a]}clear(){this.list=new ya,this.map=new Map}};function Re(t,e,n){return tn?n:t}var v=class t{constructor(e,n){this.x=e;this.y=n}withX(e){return new t(e,this.y)}withY(e){return new t(this.x,e)}plus(e){return new t(this.x+e.x,this.y+e.y)}minus(e){return new t(this.x-e.x,this.y-e.y)}times(e){return new t(this.x*e,this.y*e)}timesPointwise(e){return new t(this.x*e.x,this.y*e.y)}dividedByPointwise(e){return new t(this.x/e.x,this.y/e.y)}dot(e){return this.x*e.x+this.y*e.y}equals(e){return this.x===e.x&&this.y===e.y}approxEquals(e,n=1e-9){return Math.abs(this.x-e.x)=0&&e<=31),t.TEXTURE0+e}var q;(f=>{class t{constructor(u=0,g=0,p=0,I=0){this.x=u;this.y=g;this.width=p;this.height=I}set(u,g,p,I){this.x=u,this.y=g,this.width=p,this.height=I}equals(u){return this.x===u.x&&this.y===u.y&&this.width===u.width&&this.height===u.height}}f.Rect=t;class e{constructor(u,g,p,I){this.redF=u;this.greenF=g;this.blueF=p;this.alphaF=I}equals(u){return this.redF===u.redF&&this.greenF===u.greenF&&this.blueF===u.blueF&&this.alphaF===u.alphaF}static{this.TRANSPARENT=new e(0,0,0,0)}}f.Color=e;let n;(w=>(w[w.ZERO=0]="ZERO",w[w.ONE=1]="ONE",w[w.SOURCE_COLOR=2]="SOURCE_COLOR",w[w.TARGET_COLOR=3]="TARGET_COLOR",w[w.INVERSE_SOURCE_COLOR=4]="INVERSE_SOURCE_COLOR",w[w.INVERSE_TARGET_COLOR=5]="INVERSE_TARGET_COLOR",w[w.SOURCE_ALPHA=6]="SOURCE_ALPHA",w[w.TARGET_ALPHA=7]="TARGET_ALPHA",w[w.INVERSE_SOURCE_ALPHA=8]="INVERSE_SOURCE_ALPHA",w[w.INVERSE_TARGET_ALPHA=9]="INVERSE_TARGET_ALPHA",w[w.CONSTANT=10]="CONSTANT",w[w.INVERSE_CONSTANT=11]="INVERSE_CONSTANT"))(n=f.BlendOperation||={});let a;(g=>(g[g.TRIANGLES=0]="TRIANGLES",g[g.TRIANGLE_STRIP=1]="TRIANGLE_STRIP"))(a=f.Primitive||={});class r{constructor(){this.resizeEventHandlers=new Set}setCopyBlendState(){this.setBlendState(1,0)}setAddBlendState(){this.setBlendState(1,1)}setPremultipliedBlendState(){this.setBlendState(1,8)}setUnpremultipliedBlendState(){this.setBlendState(6,8)}addAfterResizeEventHandler(u){this.resizeEventHandlers.add(u)}removeAfterResizeEventHandler(u){this.resizeEventHandlers.delete(u)}}f.Context=r;let A;(g=>(g[g.FLOAT=0]="FLOAT",g[g.BYTE=1]="BYTE"))(A=f.AttributeType||={});function o(m){return m==0?4:1}f.attributeByteLength=o;class i{constructor(u,g,p,I){this.name=u;this.type=g;this.count=p;this.byteOffset=I}}f.Attribute=i;class l{constructor(){this._attributes=[];this._stride=0}get attributes(){return this._attributes}get stride(){return this._stride}add(u,g,p){return this.attributes.push(new i(u,g,p,this.stride)),this._stride+=p*o(g),this}}f.VertexFormat=l;class s{uploadFloat32Array(u){this.upload(new Uint8Array(u.buffer),0)}uploadFloats(u){this.uploadFloat32Array(new Float32Array(u))}}f.VertexBuffer=s;let c;(g=>(g[g.NEAREST=0]="NEAREST",g[g.LINEAR=1]="LINEAR"))(c=f.PixelFilter||={});let h;(g=>(g[g.REPEAT=0]="REPEAT",g[g.CLAMP=1]="CLAMP"))(h=f.PixelWrap||={});class _{constructor(u,g,p){this.minFilter=u;this.magFilter=g;this.wrap=p}static{this.LINEAR_CLAMP=new _(1,1,1)}static{this.LINEAR_MIN_NEAREST_MAG_CLAMP=new _(1,0,1)}static{this.NEAREST_CLAMP=new _(0,0,1)}}f.TextureFormat=_})(q||={});var vo;(f=>{class t extends q.Context{constructor(g=document.createElement("canvas")){super();this._attributeCount=0;this._blendOperations=0;this._contextResetHandlers=[];this._currentClearColor=q.Color.TRANSPARENT;this._currentRenderTarget=null;this._defaultViewport=new q.Rect;this._forceStateUpdate=!0;this._generation=1;this._height=0;this._oldBlendOperations=0;this._oldRenderTarget=null;this._oldViewport=new q.Rect;this._width=0;this.handleWebglContextRestored=()=>{this._attributeCount=0,this._currentClearColor=q.Color.TRANSPARENT,this._forceStateUpdate=!0,this._generation++;for(let g of this._contextResetHandlers)g()};this.ANGLE_instanced_arrays=null;this.ANGLE_instanced_arrays_generation=-1;let p=g.getContext("webgl",{alpha:!1,antialias:!1,depth:!1,preserveDrawingBuffer:!1,stencil:!1});if(p==null)throw new Error("Setup failure");this._gl=p;let I=g.style;g.width=0,g.height=0,I.width=I.height="0",g.addEventListener("webglcontextlost",j=>{j.preventDefault()}),g.addEventListener("webglcontextrestored",this.handleWebglContextRestored),this._blendOperationMap={0:this._gl.ZERO,1:this._gl.ONE,2:this._gl.SRC_COLOR,3:this._gl.DST_COLOR,4:this._gl.ONE_MINUS_SRC_COLOR,5:this._gl.ONE_MINUS_DST_COLOR,6:this._gl.SRC_ALPHA,7:this._gl.DST_ALPHA,8:this._gl.ONE_MINUS_SRC_ALPHA,9:this._gl.ONE_MINUS_DST_ALPHA,10:this._gl.CONSTANT_COLOR,11:this._gl.ONE_MINUS_CONSTANT_COLOR}}get widthInPixels(){return this._width}get heightInPixels(){return this._height}testContextLoss(){this.handleWebglContextRestored()}get gl(){return this._gl}get generation(){return this._generation}addContextResetHandler(g){Zf(this._contextResetHandlers,g)}removeContextResetHandler(g){Xf(this._contextResetHandlers,g)}get currentRenderTarget(){return this._currentRenderTarget}beginFrame(){this.setRenderTarget(null)}endFrame(){}setBlendState(g,p){this._blendOperations=t._packBlendModes(g,p)}setViewport(g,p,I,j){(this._currentRenderTarget!=null?this._currentRenderTarget.viewport:this._defaultViewport).set(g,p,I,j)}get viewport(){return this._currentRenderTarget!=null?this._currentRenderTarget.viewport:this._defaultViewport}get renderTargetWidthInPixels(){return this._currentRenderTarget!=null?this._currentRenderTarget.viewport.width:this._width}get renderTargetHeightInPixels(){return this._currentRenderTarget!=null?this._currentRenderTarget.viewport.height:this._height}draw(g,p,I){this._updateRenderTargetAndViewport(),s.from(p).prepare(),c.from(I).prepare(),this._updateFormat(p.format),this._updateBlendState(),this._gl.drawArrays(g==0?this._gl.TRIANGLES:this._gl.TRIANGLE_STRIP,0,Math.floor(I.byteCount/p.format.stride)),this._forceStateUpdate=!1}resize(g,p,I,j){let E=this._gl.canvas,y=E.getBoundingClientRect();if(this._width===g&&this._height===p&&Math.abs(y.width-I)<.02&&Math.abs(y.height-j)<.02)return;let b=E.style;E.width=g,E.height=p,b.width=`${I}px`,b.height=`${j}px`,this.setViewport(0,0,g,p),this._width=g,this._height=p,this.resizeEventHandlers.forEach(F=>F())}clear(g){this._updateRenderTargetAndViewport(),this._updateBlendState(),g.equals(this._currentClearColor)||(this._gl.clearColor(g.redF,g.greenF,g.blueF,g.alphaF),this._currentClearColor=g),this._gl.clear(this._gl.COLOR_BUFFER_BIT)}setRenderTarget(g){this._currentRenderTarget=_.from(g)}createMaterial(g,p,I){let j=new s(this,g,p,I);return j.program,j}createVertexBuffer(g){return O(g>0&&g%4==0),new c(this,g)}createTexture(g,p,I,j){return new h(this,g,p,I,j)}createRenderTarget(g){return new _(this,h.from(g))}getANGLE_instanced_arrays(){if(this.ANGLE_instanced_arrays_generation!==this._generation&&(this.ANGLE_instanced_arrays=null),!this.ANGLE_instanced_arrays&&(this.ANGLE_instanced_arrays=this.gl.getExtension("ANGLE_instanced_arrays"),!this.ANGLE_instanced_arrays))throw new Error("Failed to get extension ANGLE_instanced_arrays");return this.ANGLE_instanced_arrays}_updateRenderTargetAndViewport(){let g=this._currentRenderTarget,p=g!=null?g.viewport:this._defaultViewport,I=this._gl;(this._forceStateUpdate||this._oldRenderTarget!=g)&&(I.bindFramebuffer(I.FRAMEBUFFER,g?g.framebuffer:null),this._oldRenderTarget=g),(this._forceStateUpdate||!this._oldViewport.equals(p))&&(I.viewport(p.x,this.renderTargetHeightInPixels-p.y-p.height,p.width,p.height),this._oldViewport.set(p.x,p.y,p.width,p.height))}_updateBlendState(){if(this._forceStateUpdate||this._oldBlendOperations!=this._blendOperations){let g=this._gl,p=this._blendOperations,I=this._oldBlendOperations,j=p&15,E=p>>4;O(j in this._blendOperationMap),O(E in this._blendOperationMap),p==t.COPY_BLEND_OPERATIONS?g.disable(g.BLEND):((this._forceStateUpdate||I==t.COPY_BLEND_OPERATIONS)&&g.enable(g.BLEND),g.blendFunc(this._blendOperationMap[j],this._blendOperationMap[E])),this._oldBlendOperations=p}}_updateFormat(g){let p=this._gl,I=g.attributes,j=I.length;for(let E=0;Ej;)this._attributeCount--,p.disableVertexAttribArray(this._attributeCount);this._attributeCount=j}getWebGLInfo(){let g=this.gl.getExtension("WEBGL_debug_renderer_info"),p=g?this.gl.getParameter(g.UNMASKED_RENDERER_WEBGL):null,I=g?this.gl.getParameter(g.UNMASKED_VENDOR_WEBGL):null,j=this.gl.getParameter(this.gl.VERSION);return{renderer:p,vendor:I,version:j}}static from(g){return O(g==null||g instanceof t),g}static _packBlendModes(g,p){return g|p<<4}static{this.COPY_BLEND_OPERATIONS=t._packBlendModes(1,0)}}f.Context=t;class e{constructor(u,g,p=0,I=null,j=!0){this._material=u;this._name=g;this._generation=p;this._location=I;this._isDirty=j}get location(){let u=t.from(this._material.context);if(this._generation!=u.generation&&(this._location=u.gl.getUniformLocation(this._material.program,this._name),this._generation=u.generation,!jo)){let g=this._material.program,p=u.gl;for(let I=0,j=p.getProgramParameter(g,p.ACTIVE_UNIFORMS);I0&&this._texture.height>0?this._texture.texture:null)}}class s{constructor(u,g,p,I,j={},E=[],y=0,b=null){this._context=u;this._format=g;this._vertexSource=p;this._fragmentSource=I;this._uniformsMap=j;this._uniformsList=E;this._generation=y;this._program=b}get context(){return this._context}get format(){return this._format}get vertexSource(){return this._vertexSource}get fragmentSource(){return this._fragmentSource}setUniformFloat(u,g){let p=this._uniformsMap[u]||null;p==null&&(p=new n(this,u),this._uniformsMap[u]=p,this._uniformsList.push(p)),O(p instanceof n),p.set(g)}setUniformInt(u,g){let p=this._uniformsMap[u]||null;p==null&&(p=new a(this,u),this._uniformsMap[u]=p,this._uniformsList.push(p)),O(p instanceof a),p.set(g)}setUniformVec2(u,g,p){let I=this._uniformsMap[u]||null;I==null&&(I=new r(this,u),this._uniformsMap[u]=I,this._uniformsList.push(I)),O(I instanceof r),I.set(g,p)}setUniformVec3(u,g,p,I){let j=this._uniformsMap[u]||null;j==null&&(j=new A(this,u),this._uniformsMap[u]=j,this._uniformsList.push(j)),O(j instanceof A),j.set(g,p,I)}setUniformVec4(u,g,p,I,j){let E=this._uniformsMap[u]||null;E==null&&(E=new o(this,u),this._uniformsMap[u]=E,this._uniformsList.push(E)),O(E instanceof o),E.set(g,p,I,j)}setUniformMat3(u,g,p,I,j,E,y,b,F,T){let Q=this._uniformsMap[u]||null;Q==null&&(Q=new i(this,u),this._uniformsMap[u]=Q,this._uniformsList.push(Q)),O(Q instanceof i),Q.set(g,p,I,j,E,y,b,F,T)}setUniformSampler(u,g,p){let I=this._uniformsMap[u]||null;I==null&&(I=new l(this,u),this._uniformsMap[u]=I,this._uniformsList.push(I)),O(I instanceof l),I.set(g,p)}get program(){let u=this._context.gl;if(this._generation!=this._context.generation){this._program=u.createProgram(),this._compileShader(u,u.VERTEX_SHADER,this.vertexSource),this._compileShader(u,u.FRAGMENT_SHADER,this.fragmentSource);let g=this.format.attributes;for(let p=0;p=0),O(0<=g&&g+I<=this._byteCount),O(0<=p&&p+I<=this._byteCount),this._bytes&&g!=p&&I!=0&&(this._bytes.set(this._bytes.subarray(g,this._byteCount),p),this._growDirtyRegion(Math.min(g,p),Math.max(g,p)+I))}upload(g,p=0){O(0<=p&&p+g.length<=this._byteCount),O(this._bytes!=null),this._bytes.set(g,p),this._growDirtyRegion(p,p+g.length)}free(){this._buffer&&this._context.gl.deleteBuffer(this._buffer),this._generation=0}prepare(){let g=this._context.gl;this._generation!==this._context.generation&&(this._buffer=g.createBuffer(),this._generation=this._context.generation,this._isDirty=!0),g.bindBuffer(g.ARRAY_BUFFER,this._buffer),this._isDirty&&(g.bufferData(g.ARRAY_BUFFER,this._byteCount,g.DYNAMIC_DRAW),this._dirtyMin=this._totalMin,this._dirtyMax=this._totalMax,this._isDirty=!1),this._dirtyMin{let n=U.betweenRects(e.configSpaceSrcRect,e.physicalSpaceDstRect),a=new v(this.gl.viewport.width,this.gl.viewport.height);return U.withTranslation(new v(-1,1)).times(U.withScale(new v(2,-2).dividedByPointwise(a))).times(n)})()),this.gl.setUnpremultipliedBlendState(),this.gl.draw(q.Primitive.TRIANGLES,this.material,e.batch.getBuffer())}};var fe=class t{constructor(e=0,n=0,a=0,r=1){this.r=e;this.g=n;this.b=a;this.a=r}static fromLumaChromaHue(e,n,a){let r=a/60,A=n*(1-Math.abs(r%2-1)),[o,i,l]=r<1?[n,A,0]:r<2?[A,n,0]:r<3?[0,n,A]:r<4?[0,A,n]:r<5?[A,0,n]:[n,0,A],s=e-(.3*o+.59*i+.11*l);return new t(Re(o+s,0,1),Re(i+s,0,1),Re(l+s,0,1),1)}static fromCSSHex(e){if(e.length!==7||e[0]!=="#")throw new Error(`Invalid color input ${e}`);let n=parseInt(e.substr(1,2),16)/255,a=parseInt(e.substr(3,2),16)/255,r=parseInt(e.substr(5,2),16)/255;if(n<0||n>1||a<0||a>1||r<0||r>1)throw new Error(`Invalid color input ${e}`);return new t(n,a,r)}withAlpha(e){return new t(this.r,this.g,this.b,e)}toCSS(){return`rgba(${(255*this.r).toFixed()}, ${(255*this.g).toFixed()}, ${(255*this.b).toFixed()}, ${this.a.toFixed(2)})`}};var ba=class{constructor(e,n,a){this.gl=e;this.rectangleBatchRenderer=n;this.textureRenderer=a;this.texture=e.createTexture(q.TextureFormat.NEAREST_CLAMP,4096,4096),this.renderTarget=e.createRenderTarget(this.texture),this.rowCache=new Ea(this.texture.height),this.clearLineBatch=new vn(e),this.clearLineBatch.addRect(R.unit,new fe(0,0,0,0)),e.addContextResetHandler(()=>{this.rowCache.clear()})}has(e){return this.rowCache.has(e)}getResolution(){return this.texture.width}getCapacity(){return this.texture.height}allocateLine(e){if(this.rowCache.getSize(){for(let a of e){let r=this.rowCache.get(a);if(r!=null)continue;r=this.allocateLine(a);let A=new R(new v(0,r),new v(this.texture.width,1));this.rectangleBatchRenderer.render({batch:this.clearLineBatch,configSpaceSrcRect:R.unit,physicalSpaceDstRect:A}),n(A,a)}})}renderViaAtlas(e,n){let a=this.rowCache.get(e);if(a==null)return!1;let r=new R(new v(0,a),new v(this.texture.width,1));return this.textureRenderer.render({texture:this.texture,srcRect:r,dstRect:n}),!0}};var rp=` + uniform mat3 uvTransform; + uniform mat3 positionTransform; + + attribute vec2 position; + attribute vec2 uv; + varying vec2 vUv; + + void main() { + vUv = (uvTransform * vec3(uv, 1)).xy; + gl_Position = vec4((positionTransform * vec3(position, 1)).xy, 0, 1); + } +`,ap=` + precision mediump float; + + varying vec2 vUv; + uniform sampler2D texture; + + void main() { + gl_FragColor = texture2D(texture, vUv); + } +`,xa=class{constructor(e){this.gl=e;let n=new q.VertexFormat;n.add("position",q.AttributeType.FLOAT,2),n.add("uv",q.AttributeType.FLOAT,2);let a=[{pos:[-1,1],uv:[0,1]},{pos:[1,1],uv:[1,1]},{pos:[-1,-1],uv:[0,0]},{pos:[1,-1],uv:[1,0]}],r=[];for(let A of a)r.push(A.pos[0]),r.push(A.pos[1]),r.push(A.uv[0]),r.push(A.uv[1]);this.buffer=e.createVertexBuffer(n.stride*a.length),this.buffer.upload(new Uint8Array(new Float32Array(r).buffer)),this.material=e.createMaterial(n,rp,ap)}render(e){this.material.setUniformSampler("texture",e.texture,0),kt(this.material,"uvTransform",(()=>{let{srcRect:n,texture:a}=e,A=U.withTranslation(new v(0,1)).times(U.withScale(new v(1,-1))).times(U.betweenRects(new R(v.zero,new v(a.width,a.height)),R.unit)).transformRect(n);return U.betweenRects(R.unit,A)})()),kt(this.material,"positionTransform",(()=>{let{dstRect:n}=e,{viewport:a}=this.gl,r=new v(a.width,a.height),o=U.withScale(new v(1,-1)).times(U.betweenRects(new R(v.zero,r),R.NDC)).transformRect(n);return U.betweenRects(R.NDC,o)})()),this.gl.setUnpremultipliedBlendState(),this.gl.draw(q.Primitive.TRIANGLE_STRIP,this.material,this.buffer)}};var Eo=new q.VertexFormat;Eo.add("position",q.AttributeType.FLOAT,2);var Ap=` + attribute vec2 position; + + void main() { + gl_Position = vec4(position, 0, 1); + } +`,op=t=>{let{r:e,g:n,b:a}=fe.fromCSSHex(t.fgSecondaryColor),r=`${e.toFixed(1)}, ${n.toFixed(1)}, ${a.toFixed(1)}`;return` + precision mediump float; + + uniform mat3 configSpaceToPhysicalViewSpace; + uniform vec2 physicalSize; + uniform vec2 physicalOrigin; + uniform vec2 configSpaceViewportOrigin; + uniform vec2 configSpaceViewportSize; + uniform float framebufferHeight; + + void main() { + vec2 origin = (configSpaceToPhysicalViewSpace * vec3(configSpaceViewportOrigin, 1.0)).xy; + vec2 size = (configSpaceToPhysicalViewSpace * vec3(configSpaceViewportSize, 0.0)).xy; + + vec2 halfSize = physicalSize / 2.0; + + float borderWidth = 2.0; + + origin = floor(origin * halfSize) / halfSize + borderWidth * vec2(1.0, 1.0); + size = floor(size * halfSize) / halfSize - 2.0 * borderWidth * vec2(1.0, 1.0); + + vec2 coord = gl_FragCoord.xy; + coord.x = coord.x - physicalOrigin.x; + coord.y = framebufferHeight - coord.y - physicalOrigin.y; + vec2 clamped = clamp(coord, origin, origin + size); + vec2 gap = clamped - coord; + float maxdist = max(abs(gap.x), abs(gap.y)); + + // TOOD(jlfwong): Could probably optimize this to use mix somehow. + if (maxdist == 0.0) { + // Inside viewport rectangle + gl_FragColor = vec4(0, 0, 0, 0); + } else if (maxdist < borderWidth) { + // Inside viewport rectangle at border + gl_FragColor = vec4(${r}, 0.8); + } else { + // Outside viewport rectangle + gl_FragColor = vec4(${r}, 0.5); + } + } + `},ka=class{constructor(e,n){this.gl=e;let a=[[-1,1],[1,1],[-1,-1],[1,-1]],r=[];for(let A of a)r.push(A[0]),r.push(A[1]);this.buffer=e.createVertexBuffer(Eo.stride*a.length),this.buffer.upload(new Uint8Array(new Float32Array(r).buffer)),this.material=e.createMaterial(Eo,Ap,op(n))}render(e){kt(this.material,"configSpaceToPhysicalViewSpace",e.configSpaceToPhysicalViewSpace),yo(this.material,"configSpaceViewportOrigin",e.configSpaceViewportRect.origin),yo(this.material,"configSpaceViewportSize",e.configSpaceViewportRect.size);let n=this.gl.viewport;this.material.setUniformVec2("physicalOrigin",n.x,n.y),this.material.setUniformVec2("physicalSize",n.width,n.height),this.material.setUniformFloat("framebufferHeight",this.gl.renderTargetHeightInPixels),this.gl.setBlendState(q.BlendOperation.SOURCE_ALPHA,q.BlendOperation.INVERSE_SOURCE_ALPHA),this.gl.draw(q.Primitive.TRIANGLE_STRIP,this.material,this.buffer)}};var Sa=new q.VertexFormat;Sa.add("position",q.AttributeType.FLOAT,2);Sa.add("uv",q.AttributeType.FLOAT,2);var ip=` + uniform mat3 uvTransform; + uniform mat3 positionTransform; + + attribute vec2 position; + attribute vec2 uv; + varying vec2 vUv; + + void main() { + vUv = (uvTransform * vec3(uv, 1)).xy; + gl_Position = vec4((positionTransform * vec3(position, 1)).xy, 0, 1); + } +`,lp=t=>` + precision mediump float; + + uniform vec2 uvSpacePixelSize; + uniform float renderOutlines; + + varying vec2 vUv; + uniform sampler2D colorTexture; + + // https://en.wikipedia.org/wiki/HSL_and_HSV#From_luma/chroma/hue + vec3 hcl2rgb(float H, float C, float L) { + float hPrime = H / 60.0; + float X = C * (1.0 - abs(mod(hPrime, 2.0) - 1.0)); + vec3 RGB = + hPrime < 1.0 ? vec3(C, X, 0) : + hPrime < 2.0 ? vec3(X, C, 0) : + hPrime < 3.0 ? vec3(0, C, X) : + hPrime < 4.0 ? vec3(0, X, C) : + hPrime < 5.0 ? vec3(X, 0, C) : + vec3(C, 0, X); + + float m = L - dot(RGB, vec3(0.30, 0.59, 0.11)); + return RGB + vec3(m, m, m); + } + + float triangle(float x) { + return 2.0 * abs(fract(x) - 0.5) - 1.0; + } + + ${t} + + void main() { + vec4 here = texture2D(colorTexture, vUv); + + if (here.z == 0.0) { + // Background color + gl_FragColor = vec4(0, 0, 0, 0); + return; + } + + // Sample the 4 surrounding pixels in the depth texture to determine + // if we should draw a boundary here or not. + vec4 N = texture2D(colorTexture, vUv + vec2(0, uvSpacePixelSize.y)); + vec4 E = texture2D(colorTexture, vUv + vec2(uvSpacePixelSize.x, 0)); + vec4 S = texture2D(colorTexture, vUv + vec2(0, -uvSpacePixelSize.y)); + vec4 W = texture2D(colorTexture, vUv + vec2(-uvSpacePixelSize.x, 0)); + + // NOTE: For outline checks, we intentionally check both the right + // and the left to determine if we're an edge. If a rectangle is a single + // pixel wide, we don't want to render it as an outline, so this method + // of checking ensures that we don't outline single physical-space + // pixel width rectangles. + if ( + renderOutlines > 0.0 && + ( + here.y == N.y && here.y != S.y || // Top edge + here.y == S.y && here.y != N.y || // Bottom edge + here.x == E.x && here.x != W.x || // Left edge + here.x == W.x && here.x != E.x + ) + ) { + // We're on an edge! Draw transparent. + gl_FragColor = vec4(0, 0, 0, 0); + } else { + // Not on an edge. Draw the appropriate color. + gl_FragColor = vec4(colorForBucket(here.z), here.a); + } + } +`,Na=class{constructor(e,n){this.gl=e;let a=[{pos:[-1,1],uv:[0,1]},{pos:[1,1],uv:[1,1]},{pos:[-1,-1],uv:[0,0]},{pos:[1,-1],uv:[1,0]}],r=[];for(let A of a)r.push(A.pos[0]),r.push(A.pos[1]),r.push(A.uv[0]),r.push(A.uv[1]);this.buffer=e.createVertexBuffer(Sa.stride*a.length),this.buffer.uploadFloats(r),this.material=e.createMaterial(Sa,ip,lp(n.colorForBucketGLSL))}render(e){let{srcRect:n,rectInfoTexture:a}=e,A=U.withTranslation(new v(0,1)).times(U.withScale(new v(1,-1))).times(U.betweenRects(new R(v.zero,new v(a.width,a.height)),R.unit)).transformRect(n),o=U.betweenRects(R.unit,A),{dstRect:i}=e,l=new v(this.gl.viewport.width,this.gl.viewport.height),c=U.withScale(new v(1,-1)).times(U.betweenRects(new R(v.zero,l),R.NDC)).transformRect(i),h=U.betweenRects(R.NDC,c),_=v.unit.dividedByPointwise(new v(e.rectInfoTexture.width,e.rectInfoTexture.height));this.material.setUniformSampler("colorTexture",e.rectInfoTexture,0),kt(this.material,"uvTransform",o),this.material.setUniformFloat("renderOutlines",e.renderOutlines?1:0),this.material.setUniformVec2("uvSpacePixelSize",_.x,_.y),kt(this.material,"positionTransform",h),this.gl.setUnpremultipliedBlendState(),this.gl.draw(q.Primitive.TRIANGLE_STRIP,this.material,this.buffer)}};var Fa=class{constructor(e,n){this.animationFrameRequest=null;this.beforeFrameHandlers=new Set;this.onBeforeFrame=()=>{this.animationFrameRequest=null,this.gl.setViewport(0,0,this.gl.renderTargetWidthInPixels,this.gl.renderTargetHeightInPixels);let e=fe.fromCSSHex(this.theme.bgPrimaryColor);this.gl.clear(new q.Color(e.r,e.g,e.b,e.a));for(let n of this.beforeFrameHandlers)n()};this.gl=new vo.Context(e),this.rectangleBatchRenderer=new Qa(this.gl),this.textureRenderer=new xa(this.gl),this.viewportRectangleRenderer=new ka(this.gl,n),this.flamechartColorPassRenderer=new Na(this.gl,n),this.theme=n,this.gl.addAfterResizeEventHandler(this.onBeforeFrame);let a=this.gl.getWebGLInfo();a&&console.log(`WebGL initialized. renderer: ${a.renderer}, vendor: ${a.vendor}, version: ${a.version}`),window.testContextLoss=()=>{this.gl.testContextLoss()}}addBeforeFrameHandler(e){this.beforeFrameHandlers.add(e)}removeBeforeFrameHandler(e){this.beforeFrameHandlers.delete(e)}requestFrame(){this.animationFrameRequest||(this.animationFrameRequest=requestAnimationFrame(this.onBeforeFrame))}setViewport(e,n){let{origin:a,size:r}=e,A=this.gl.viewport;this.gl.setViewport(a.x,a.y,r.x,r.y),n();let{x:o,y:i,width:l,height:s}=A;this.gl.setViewport(o,i,l,s)}renderBehind(e,n){let a=e.getBoundingClientRect(),r=new R(new v(a.left*window.devicePixelRatio,a.top*window.devicePixelRatio),new v(a.width*window.devicePixelRatio,a.height*window.devicePixelRatio));this.setViewport(r,n)}};var sn=qn(t=>e=>t.get(e.key)||0),Ut=ze(({theme:t,frameToColorBucket:e})=>{let n=sn(e);return a=>{let r=n(a)/255;return t.colorForBucket(r).toCSS()}}),Ot=ze(({theme:t,canvas:e})=>new Fa(e,t)),_s=qn(t=>new ba(t.gl,t.rectangleBatchRenderer,t.textureRenderer)),hs=ze(({profile:t,flattenRecursion:e})=>e?t.getProfileWithRecursionFlattened():t),qt=qn(t=>{let e=[];t.forEachFrame(A=>e.push(A));function n(A){return(A.file||"")+A.name}function a(A,o){return n(A)>n(o)?1:-1}e.sort(a);let r=new Map;for(let A=0;A=n.__.length&&n.__.push({}),n.__[t]}function Ge(t){return wr=1,Cs(ms,t)}function Cs(t,e,n){var a=br(zn++,2);return a.__c||(a.__c=it,a.__=[n?n(e):ms(void 0,e),function(r){var A=t(a.__[0],r);a.__[0]!==A&&(a.__[0]=A,a.__c.setState({}))}]),a.__}function Ue(t,e){var n=br(zn++,3);!J.__s&&xo(n.__H,e)&&(n.__=t,n.__H=e,it.__H.__h.push(n))}function bo(t,e){var n=br(zn++,4);!J.__s&&xo(n.__H,e)&&(n.__=t,n.__H=e,it.__h.push(n))}function cn(t){return wr=5,oe(function(){return{current:t}},[])}function oe(t,e){var n=br(zn++,7);return xo(n.__H,e)?(n.__H=e,n.__h=t,n.__=t()):n.__}function P(t,e){return wr=8,oe(function(){return t},e)}function lt(t){var e=it.context[t.__c],n=br(zn++,9);return n.__c=t,e?(n.__==null&&(n.__=!0,e.sub(it)),e.props.value):t.__}function sp(){Bo.some(function(t){if(t.__P)try{t.__H.__h.forEach(Qo),t.__H.__h.forEach(wo),t.__H.__h=[]}catch(e){return t.__H.__h=[],J.__e(e,t.__v),!0}}),Bo=[]}function Qo(t){t.t&&t.t()}function wo(t){var e=t.__();typeof e=="function"&&(t.t=e)}function xo(t,e){return!t||e.some(function(n,a){return n!==t[a]})}function ms(t,e){return typeof e=="function"?e(t):e}J.__r=function(t){ds&&ds(t),zn=0,(it=t.__c).__H&&(it.__H.__h.forEach(Qo),it.__H.__h.forEach(wo),it.__H.__h=[])},J.diffed=function(t){us&&us(t);var e=t.__c;if(e){var n=e.__H;n&&n.__h.length&&(Bo.push(e)!==1&&gs===J.requestAnimationFrame||((gs=J.requestAnimationFrame)||function(a){var r,A=function(){clearTimeout(o),cancelAnimationFrame(r),setTimeout(a)},o=setTimeout(A,100);typeof window<"u"&&(r=requestAnimationFrame(A))})(sp))}},J.__c=function(t,e){e.some(function(n){try{n.__h.forEach(Qo),n.__h=n.__h.filter(function(a){return!a.__||wo(a)})}catch(a){e.some(function(r){r.__h&&(r.__h=[])}),e=[],J.__e(a,n.__v)}}),fs&&fs(t,e)},J.unmount=function(t){ps&&ps(t);var e=t.__c;if(e){var n=e.__H;if(n)try{n.__.forEach(function(a){return a.t&&a.t()})}catch(a){J.__e(a,e.__v)}}};function ws(t,e){for(var n in e)t[n]=e[n];return t}function So(t,e){for(var n in t)if(n!=="__source"&&!(n in e))return!0;for(var a in e)if(a!=="__source"&&t[a]!==e[a])return!0;return!1}var aB=(function(t){var e,n;function a(r){var A;return(A=t.call(this,r)||this).isPureReactComponent=!0,A}return n=t,(e=a).prototype=Object.create(n.prototype),e.prototype.constructor=e,e.__proto__=n,a.prototype.shouldComponentUpdate=function(r,A){return So(this.props,r)||So(this.state,A)},a})(Ne);function pe(t,e){function n(r){var A=this.props.ref,o=A==r.ref;return!o&&A&&(A.call?A(null):A.current=null),e?!e(this.props,r)||!o:So(this.props,r)}function a(r){return this.shouldComponentUpdate=n,C(t,ws({},r))}return a.prototype.isReactComponent=!0,a.displayName="Memo("+(t.displayName||t.name)+")",a.t=!0,a}var Is=J.__b;J.__b=function(t){t.type&&t.type.t&&t.ref&&(t.props.ref=t.ref,t.ref=null),Is&&Is(t)};var _p=J.__e;function bs(t){return t&&((t=ws({},t)).__c=null,t.__k=t.__k&&t.__k.map(bs)),t}function js(){this.__u=0,this.o=null,this.__b=null}function xs(t){var e=t.__.__c;return e&&e.u&&e.u(t)}function Da(){this.i=null,this.l=null}J.__e=function(t,e,n){if(t.then){for(var a,r=e;r=r.__;)if((a=r.__c)&&a.__c)return a.__c(t,e.__c)}_p(t,e,n)},(js.prototype=new Ne).__c=function(t,e){var n=this;n.o==null&&(n.o=[]),n.o.push(e);var a=xs(n.__v),r=!1,A=function(){r||(r=!0,a?a(o):o())};e.__c=e.componentWillUnmount,e.componentWillUnmount=function(){A(),e.__c&&e.__c()};var o=function(){var i;if(!--n.__u)for(n.__v.__k[0]=n.state.u,n.setState({u:n.__b=null});i=n.o.pop();)i.forceUpdate()};n.__u++||n.setState({u:n.__b=n.__v.__k[0]}),t.then(A,A)},js.prototype.render=function(t,e){return this.__b&&(this.__v.__k[0]=bs(this.__b),this.__b=null),[C(Ne,null,e.u?null:t.children),e.u&&t.fallback]};var vs=function(t,e,n){if(++n[1]===n[0]&&t.l.delete(e),t.props.revealOrder&&(t.props.revealOrder[0]!=="t"||!t.l.size))for(n=t.i;n;){for(;n.length>3;)n.pop()();if(n[1]n()))}get(){return this.state}subscribe(e){this.observers.push(e)}unsubscribe(e){let n=this.observers.indexOf(e);n!==-1&&this.observers.splice(n,1)}};function Y(t){let[e,n]=Ge(t.get());return bo(()=>{n(t.get());function a(){n(t.get())}return t.subscribe(a),()=>{t.unsubscribe(a)}},[t]),e}function gp(t){switch(t){case"time-ordered":return 0;case"left-heavy":return 1;case"sandwich":return 2;default:return null}}function ks(t=window.location.hash){try{if(!t.startsWith("#"))return{};let e=t.substr(1).split("&"),n={};for(let a of e){let[r,A]=a.split("=");if(A=decodeURIComponent(A),r==="profileURL")n.profileURL=A;else if(r==="title")n.title=A;else if(r==="localProfilePath")n.localProfilePath=A;else if(r==="view"){let o=gp(A);o!==null?n.viewMode=o:console.error(`Ignoring invalid view specifier: ${A}`)}}return n}catch(e){return console.error("Error when loading hash fragment."),console.error(e),{}}}V();var Ra={hover:null,selectedNode:null,configSpaceViewportRect:R.empty,logicalSpaceViewportSize:v.zero},La=class extends be{constructor(){super(...arguments);this.setProfileGroup=n=>{this.set({name:n.name,indexToView:n.indexToView,profiles:n.profiles.map(a=>({profile:a,chronoViewState:Ra,leftHeavyViewState:Ra,sandwichViewState:{callerCallee:null}}))})};this.setProfileIndexToView=n=>{this.state!=null&&(n=Re(n,0,this.state.profiles.length-1),this.set({...this.state,indexToView:n}))};this.setSelectedFrame=n=>{this.state==null||this.getActiveProfile()==null||this.updateActiveSandwichViewState(r=>n==null?{callerCallee:null}:{callerCallee:{invertedCallerFlamegraph:Ra,calleeFlamegraph:Ra,selectedFrame:n}})}}set(n){let a=this.state;a!=null&&n!=null&&Qr(a,n)||super.set(n)}getActiveProfile(){return this.state==null?null:this.state.profiles[this.state?.indexToView]||null}updateActiveProfileState(n){if(this.state==null)return;let{indexToView:a,profiles:r}=this.state;this.set({...this.state,profiles:r.map((A,o)=>o!=a?A:n(A))})}updateActiveSandwichViewState(n){this.updateActiveProfileState(a=>({...a,sandwichViewState:n(a.sandwichViewState)}))}updateFlamechartState(n,a){switch(n){case"CHRONO":{this.updateActiveProfileState(r=>({...r,chronoViewState:a(r.chronoViewState)}));break}case"LEFT_HEAVY":{this.updateActiveProfileState(r=>({...r,leftHeavyViewState:a(r.leftHeavyViewState)}));break}case"SANDWICH_CALLEES":{this.updateActiveSandwichViewState(r=>({...r,callerCallee:r.callerCallee==null?null:{...r.callerCallee,calleeFlamegraph:a(r.callerCallee.calleeFlamegraph)}}));break}case"SANDWICH_INVERTED_CALLERS":{this.updateActiveSandwichViewState(r=>({...r,callerCallee:r.callerCallee==null?null:{...r.callerCallee,invertedCallerFlamegraph:a(r.callerCallee.invertedCallerFlamegraph)}}));break}}}setFlamechartHoveredNode(n,a){this.updateFlamechartState(n,r=>({...r,hover:a}))}setSelectedNode(n,a){this.updateFlamechartState(n,r=>({...r,selectedNode:a}))}setConfigSpaceViewportRect(n,a){this.updateFlamechartState(n,r=>({...r,configSpaceViewportRect:a}))}setLogicalSpaceViewportSize(n,a){this.updateFlamechartState(n,r=>({...r,logicalSpaceViewportSize:a}))}clearHoverNode(){this.setFlamechartHoveredNode("CHRONO",null),this.setFlamechartHoveredNode("LEFT_HEAVY",null),this.setFlamechartHoveredNode("SANDWICH_CALLEES",null),this.setFlamechartHoveredNode("SANDWICH_INVERTED_CALLERS",null)}};var zt=new be(!1,"flattenRecursion"),$n=new be(!1,"searchIsActive"),Vn=new be("","searchQueryAtom"),_n=new be(0,"viewMode"),He=new La(null,"profileGroup");_n.subscribe(()=>{He.clearHoverNode()});var Ns=ks(),Fs=new be(Ns,"hashParams"),yn=new be(null,"glCanvas"),Fo=new be(!1,"dragActive"),Ss=window.location.protocol,Ta=Ss==="http:"||Ss==="https:",dp=Ta&&Ns.profileURL!=null,Do=new be(dp,"loading"),Ha=new be(!1,"error"),xr=new be(null,"minimapMousePosition");var kr=new be({field:1,direction:1},"tableSortMethod");function Ma(){let t=Y(zt),e=Y(He);if(!e||e.indexToView>=e.profiles.length)return null;let n=e.indexToView,a=e.profiles[n];return{...e.profiles[e.indexToView],profile:hs({profile:a.profile,flattenRecursion:t}),index:e.indexToView}}var Ka="speedscope-color-scheme";function up(){let t=window.localStorage&&window.localStorage[Ka];return t==="DARK"?1:t==="LIGHT"?2:0}function fp(){return matchMedia("(prefers-color-scheme: dark)")}function pp(t){if(fp().matches)switch(t){case 0:return 2;case 2:return 1;case 1:return 0}else switch(t){case 0:return 1;case 1:return 2;case 2:return 0}}var Ro=class extends be{constructor(){super(...arguments);this.cycleToNextColorScheme=()=>{this.set(pp(this.get()))}}},En=new Ro(up(),"colorScheme");En.subscribe(()=>{let t=En.get();switch(t){case 1:{window.localStorage[Ka]="DARK";break}case 2:{window.localStorage[Ka]="LIGHT";break}case 0:{delete window.localStorage[Ka];break}default:return t}return t});V();V();var Ds=.2,Rs=.1,Ls=.2,Ts=.1,Cp=t=>{let e=va(30*t),n=360*(.9*t),a=Ds+Rs*e,r=Ls-Ts*e;return fe.fromLumaChromaHue(r,a,n)},mp=` + vec3 colorForBucket(float t) { + float x = triangle(30.0 * t); + float H = 360.0 * (0.9 * t); + float C = ${Ds.toFixed(1)} + ${Rs.toFixed(1)} * x; + float L = ${Ls.toFixed(1)} - ${Ts.toFixed(1)} * x; + return hcl2rgb(H, C, L); + } +`,Lo={fgPrimaryColor:"#D0D0D0",fgSecondaryColor:"#666666",bgPrimaryColor:"#060606",bgSecondaryColor:"#0C0C0C",altFgPrimaryColor:"#D0D0D0",altFgSecondaryColor:"#666666",altBgPrimaryColor:"#000000",altBgSecondaryColor:"#0C0C0C",selectionPrimaryColor:"#00769B",selectionSecondaryColor:"#004E75",weightColor:"#0F8A42",searchMatchTextColor:"#0C0C0C",searchMatchPrimaryColor:"#A66F1C",searchMatchSecondaryColor:"#D6AE24",colorForBucket:Cp,colorForBucketGLSL:mp};V();var Hs=.25,Ms=.2,Ks=.8,Js=.15,Ip=t=>{let e=va(30*t),n=360*(.9*t),a=Hs+Ms*e,r=Ks-Js*e;return fe.fromLumaChromaHue(r,a,n)},jp=` + vec3 colorForBucket(float t) { + float x = triangle(30.0 * t); + float H = 360.0 * (0.9 * t); + float C = ${Hs.toFixed(1)} + ${Ms.toFixed(1)} * x; + float L = ${Ks.toFixed(1)} - ${Js.toFixed(1)} * x; + return hcl2rgb(H, C, L); + } +`,Ja={fgPrimaryColor:"#000000",fgSecondaryColor:"#BDBDBD",bgPrimaryColor:"#FFFFFF",bgSecondaryColor:"#F6F6F6",altFgPrimaryColor:"#FFFFFF",altFgSecondaryColor:"#BDBDBD",altBgPrimaryColor:"#000000",altBgSecondaryColor:"#222222",selectionPrimaryColor:"#2F80ED",selectionSecondaryColor:"#8EB7ED",weightColor:"#6FCF97",searchMatchTextColor:"#000000",searchMatchPrimaryColor:"#FFAC02",searchMatchSecondaryColor:"#FEDC62",colorForBucket:Ip,colorForBucketGLSL:jp};var Gs=wt(Ja);function z(){return lt(Gs)}function Ce(t){return qn(t)}function Ps(){return matchMedia("(prefers-color-scheme: dark)")}function Us(t){switch(t){case 0:return"System";case 1:return"Dark";case 2:return"Light"}}function vp(t,e){switch(t){case 0:return e?Lo:Ja;case 1:return Lo;case 2:return Ja}}function Os(t){let[e,n]=Ge(()=>Ps().matches),a=P(o=>{n(o.matches)},[n]);Ue(()=>{let o=Ps();return o.addEventListener("change",a),()=>{o.removeEventListener("change",a)}},[a]);let r=Y(En),A=vp(r,e);return C(Gs.Provider,{value:A,children:t.children})}var e_=he($s()),t_=he(Ys()),n_=he(Zs()),r_=he(ec()),a_=he(rc()),A_=he(oc()),o_=he(sc()),i_=he(_c()),l_=he(gc()),s_=he(uc()),c_=he(pc()),__=he(Bc()),h_=he(wc()),g_=he(Mc()),d_=he(Yc()),L=["Webkit"],Go=["Moz"],ge=["ms"],Me=["Webkit","Moz"],xe=["Webkit","ms"],Sr=["Webkit","Moz","ms"],zC={plugins:[e_.default,t_.default,n_.default,r_.default,a_.default,A_.default,o_.default,i_.default,l_.default,s_.default,c_.default,__.default],prefixMap:{transform:xe,transformOrigin:xe,transformOriginX:xe,transformOriginY:xe,backfaceVisibility:L,perspective:L,perspectiveOrigin:L,transformStyle:L,transformOriginZ:L,animation:L,animationDelay:L,animationDirection:L,animationFillMode:L,animationDuration:L,animationIterationCount:L,animationName:L,animationPlayState:L,animationTimingFunction:L,appearance:Me,userSelect:Sr,fontKerning:L,textEmphasisPosition:L,textEmphasis:L,textEmphasisStyle:L,textEmphasisColor:L,boxDecorationBreak:L,clipPath:L,maskImage:L,maskMode:L,maskRepeat:L,maskPosition:L,maskClip:L,maskOrigin:L,maskSize:L,maskComposite:L,mask:L,maskBorderSource:L,maskBorderMode:L,maskBorderSlice:L,maskBorderWidth:L,maskBorderOutset:L,maskBorderRepeat:L,maskBorder:L,maskType:L,textDecorationStyle:Me,textDecorationSkip:Me,textDecorationLine:Me,textDecorationColor:Me,filter:L,fontFeatureSettings:Me,breakAfter:Sr,breakBefore:Sr,breakInside:Sr,columnCount:Me,columnFill:Me,columnGap:Me,columnRule:Me,columnRuleColor:Me,columnRuleStyle:Me,columnRuleWidth:Me,columns:Me,columnSpan:Me,columnWidth:Me,writingMode:xe,flex:xe,flexBasis:L,flexDirection:xe,flexGrow:L,flexFlow:xe,flexShrink:L,flexWrap:xe,alignContent:L,alignItems:L,alignSelf:L,justifyContent:L,order:L,transitionDelay:L,transitionDuration:L,transitionProperty:L,transitionTimingFunction:L,backdropFilter:L,scrollSnapType:xe,scrollSnapPointsX:xe,scrollSnapPointsY:xe,scrollSnapDestination:xe,scrollSnapCoordinate:xe,shapeImageThreshold:L,shapeImageMargin:L,shapeImageOutside:L,hyphens:Sr,flowInto:xe,flowFrom:xe,regionFragment:xe,boxSizing:Go,textAlignLast:Go,tabSize:Go,wrapFlow:ge,wrapThrough:ge,wrapMargin:ge,touchAction:ge,gridTemplateColumns:ge,gridTemplateRows:ge,gridTemplateAreas:ge,gridTemplate:ge,gridAutoColumns:ge,gridAutoRows:ge,gridAutoFlow:ge,grid:ge,gridRowStart:ge,gridColumnStart:ge,gridRowEnd:ge,gridRow:ge,gridColumn:ge,gridColumnEnd:ge,gridColumnGap:ge,gridRowGap:ge,gridArea:ge,gridGap:ge,textSizeAdjust:xe,borderImage:L,borderImageOutset:L,borderImageRepeat:L,borderImageSlice:L,borderImageSource:L,borderImageWidth:L}},$C=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},VC=(function(){function t(e,n){for(var a=0;a"u"?"undefined":$C(a))==="object"){for(var i=this.elements.hasOwnProperty(n)?this.elements[n]:new t,l=Object.keys(a),s=0;s"u"?"undefined":Xc(e))==="object"?(p_(e.src,"@font-face",[e],!1),'"'+String(e.fontFamily)+'"'):e}return t})(),animationName:(function(){function t(e,n){if(Array.isArray(e))return e.map(function(A){return t(A,n)}).join(",");if((typeof e>"u"?"undefined":Xc(e))==="object"){var a="keyframe_"+String(rm(e)),r="@keyframes "+a+"{";return e instanceof Uo?e.forEach(function(A,o){r+=Oo(o,[A],n,qo,!1).join("")}):Object.keys(e).forEach(function(A){r+=Oo(A,[e[A]],n,qo,!1).join("")}),r+="}",f_(a,[r]),a}else return e}return t})()},Yn={},hA=[],Wn=!1,f_=function(e,n){var a;if(!Yn[e]){if(!Wn){if(typeof document>"u")throw new Error("Cannot automatically buffer without a document");Wn=!0,(0,d_.default)(fm)}(a=hA).push.apply(a,_m(n)),Yn[e]=!0}},p_=function(e,n,a,r){var A=arguments.length>4&&arguments[4]!==void 0?arguments[4]:[];if(!Yn[e]){var o=Oo(n,a,A,qo,r);f_(e,o)}},gm=function(){hA=[],Yn={},Wn=!1,st=null};var dm=function(){if(Wn)throw new Error("Cannot buffer while already buffering");Wn=!0},C_=function(){Wn=!1;var e=hA;return hA=[],e},um=function(){return C_().join("")},fm=function(){var e=C_();e.length>0&&hm(e)},pm=function(){return Object.keys(Yn)},Cm=function(e){e.forEach(function(n){Yn[n]=!0})},mm=function t(e,n,a,r){for(var A=0;A0&&arguments[0]!==void 0?arguments[0]:[];Cm(e)}return t})()},Em=typeof window<"u"?null:{renderStatic:(function(){function t(e){gm(),dm();var n=e(),a=um();return{html:n,css:{content:a,renderedClassNames:pm()}}}return t})()},Bm=null;function I_(t,e){return{StyleSheet:Object.assign({},ym,{extend:(function(){function n(a){var r=a.map(function(A){return A.selectorHandler}).filter(function(A){return A});return I_(t,e.concat(r))}return n})()}),StyleSheetServer:Em,StyleSheetTestUtils:Bm,minify:(function(){function n(a){m_=a?Nr:jm}return n})(),css:(function(){function n(){for(var a=arguments.length,r=Array(a),A=0;A{let a=wm(n.name,this.searchQuery);this.matches.set(n,a.length===0?null:a)})),this.matches.get(e)||null}},dA=class{constructor(e,n){this.flamechart=e;this.profileResults=n;this.matches=null}getResults(){if(this.matches==null){let e=[],n=new Map,a=(A,o)=>{let{node:i}=A;if(this.profileResults.getMatchForFrame(i.frame)){let l=new R(new v(A.start,o),new v(A.end-A.start,1));n.set(i,e.length),e.push({configSpaceBounds:l,node:i})}A.children.forEach(l=>{a(l,o+1)})},r=this.flamechart.getLayers();r.length>0&&r[0].forEach(A=>a(A,0)),this.matches={matches:e,indexForNode:n}}return this.matches}count(){return this.getResults().matches.length}indexOf(e){let n=this.getResults().indexForNode.get(e);return n===void 0?null:n}at(e){let n=this.getResults().matches;if(e<0||e>=n.length)throw new Error(`Index ${e} out of bounds in list of ${n.length} matches.`);return n[e]}};function j_(t){t.stopPropagation()}var Qn=wt(null),v_=({children:t})=>{let e=Ma(),n=e?e.profile:null,a=Y($n),r=Y(Vn),A=oe(()=>!n||!a||r.length===0?null:new gA(n,r),[a,r,n]);return C(Qn.Provider,{value:A},t)},uA=pe(({numResults:t,resultIndex:e,selectNext:n,selectPrev:a})=>{let r=z(),A=bm(r),o=Y($n),i=Y(Vn),l=Vn.set,s=$n.set,c=P(u=>{let g=u.target.value;l(g)},[l]),h=cn(null),_=P(()=>s(!1),[s]),f=P(u=>{u.shiftKey?a():n()},[a,n]),m=P(u=>{u.stopPropagation(),u.key==="Escape"&&s(!1),u.key==="Enter"&&f(u),u.key=="f"&&(u.metaKey||u.ctrlKey)&&(h.current&&h.current.select(),u.preventDefault())},[s,f]);return Ue(()=>{let u=g=>{g.key=="f"&&(g.metaKey||g.ctrlKey)&&(g.preventDefault(),h.current?h.current.select():(s(!0),requestAnimationFrame(()=>{h.current&&h.current.select()})))};return window.addEventListener("keydown",u),()=>{window.removeEventListener("keydown",u)}},[s]),o?C("div",{className:B(A.searchView)},C("span",{className:B(A.icon)},"\u{1F50D}"),C("span",{className:B(A.inputContainer)},C("input",{className:B(A.input),value:i,onInput:c,onKeyDown:m,onKeyUp:j_,onKeyPress:j_,ref:h})),t!=null&&C(ot,null,C("span",{className:B(A.resultCount)},e==null?"?":e+1,"/",t),C("button",{className:B(A.icon,A.button),onClick:a},"\u2B05\uFE0F"),C("button",{className:B(A.icon,A.button),onClick:n},"\u27A1\uFE0F")),C("svg",{className:B(A.icon),onClick:_,width:"16",height:"16",viewBox:"0 0 16 16",fill:"none",xmlns:"http://www.w3.org/2000/svg"},C("path",{d:"M4.99999 4.16217L11.6427 10.8048M11.6427 4.16217L4.99999 10.8048",stroke:r.altFgSecondaryColor}))):null}),bm=Ce(t=>de.create({searchView:{position:"absolute",top:0,right:10,height:20,width:208,borderWidth:2,borderColor:t.altFgPrimaryColor,borderStyle:"solid",fontSize:10,boxSizing:"border-box",background:t.altBgSecondaryColor,color:t.altFgPrimaryColor,display:"flex",alignItems:"center"},inputContainer:{flexShrink:1,flexGrow:1,display:"flex"},input:{width:"100%",border:"none",background:"none",fontSize:10,lineHeight:"20px",color:t.altFgPrimaryColor,":focus":{border:"none",outline:"none"},"::selection":{color:t.altFgPrimaryColor,background:t.selectionPrimaryColor}},resultCount:{verticalAlign:"middle"},icon:{flexShrink:0,verticalAlign:"middle",height:"100%",margin:"0px 2px 0px 2px",fontSize:10},button:{display:"inline",background:"none",border:"none",padding:0,":focus":{outline:"none"}}}));function y_(t){return t.replace(/\\([a-fA-F0-9]{2})/g,(e,n)=>{let a=parseInt(n,16);return String.fromCharCode(a)})}function E_(t){let e=t.split(` +`);if(!e.length||(e[e.length-1]===""&&e.pop(),!e.length))return null;let n=new Map,a=/^(\d+):(.+)$/,r=/^([\$\w]+):([\$\w-]+)$/;for(let A of e){let o=a.exec(A);if(o){n.set(`wasm-function[${o[1]}]`,y_(o[2]));continue}let i=r.exec(A);if(i){n.set(i[1],y_(i[2]));continue}return null}return A=>n.has(A.name)?{name:n.get(A.name)}:null}Zo();V();var Wt=class{constructor(e){this.source=e;this.layers=[];this.totalWeight=0;this.minFrameWidth=1;let n=[],a=(A,o)=>{let i=Ae(n),l={node:A,parent:i,children:[],start:o,end:o};i&&i.children.push(l),n.push(l)};this.minFrameWidth=1/0;let r=(A,o)=>{console.assert(n.length>0);let i=n.pop();if(i.end=o,i.end-i.start===0)return;let l=n.length;for(;this.layers.length<=l;)this.layers.push([]);this.layers[l].push(i),this.minFrameWidth=Math.min(this.minFrameWidth,i.end-i.start)};this.totalWeight=e.getTotalWeight(),e.forEachCall(a,r),isFinite(this.minFrameWidth)||(this.minFrameWidth=1)}getTotalWeight(){return this.totalWeight}getLayers(){return this.layers}getColorBucketForFrame(e){return this.source.getColorBucketForFrame(e)}getMinFrameWidth(){return this.minFrameWidth}formatValue(e){return this.source.formatValue(e)}getClampedViewportWidth(e){let n=this.getTotalWeight(),a=Math.pow(2,40),r=Re(3*this.getMinFrameWidth(),n/a,n);return Re(e,r,n)}getClampedConfigSpaceViewportRect({configSpaceViewportRect:e,renderInverted:n}){let a=new v(this.getTotalWeight(),this.getLayers().length),r=this.getClampedViewportWidth(e.size.x),A=e.size.withX(r),o=v.clamp(e.origin,new v(0,n?0:-1),v.max(v.zero,a.minus(A).plus(new v(0,1))));return new R(o,e.size.withX(r))}};V();var Rm=1e4,CA=class{constructor(e,n,a){this.batch=e;this.bounds=n;this.numPrecedingRectanglesInRow=a;this.children=[]}getBatch(){return this.batch}getBounds(){return this.bounds}getRectCount(){return this.batch.getRectCount()}getChildren(){return this.children}getParity(){return this.numPrecedingRectanglesInRow%2}forEachLeafNodeWithinBounds(e,n){this.bounds.hasIntersectionWith(e)&&n(this)}},Xo=class{constructor(e){this.children=e;this.rectCount=0;if(e.length===0)throw new Error("Empty interior node");let n=1/0,a=-1/0,r=1/0,A=-1/0;for(let o of e){this.rectCount+=o.getRectCount();let i=o.getBounds();n=Math.min(n,i.left()),a=Math.max(a,i.right()),r=Math.min(r,i.top()),A=Math.max(A,i.bottom())}this.bounds=new R(new v(n,r),new v(a-n,A-r))}getBounds(){return this.bounds}getRectCount(){return this.rectCount}getChildren(){return this.children}forEachLeafNodeWithinBounds(e,n){if(this.bounds.hasIntersectionWith(e))for(let a of this.children)a.forEachLeafNodeWithinBounds(e,n)}},mA=class t{get key(){return`${this.stackDepth}_${this.index}_${this.zoomLevel}`}constructor(e){this.stackDepth=e.stackDepth,this.zoomLevel=e.zoomLevel,this.index=e.index}static getOrInsert(e,n){return e.getOrInsert(new t(n))}},IA=class{constructor(e,n,a,r,A,o={inverted:!1}){this.gl=e;this.rowAtlas=n;this.flamechart=a;this.rectangleBatchRenderer=r;this.colorPassRenderer=A;this.options=o;this.layers=[];this.rectInfoTexture=null;this.rectInfoRenderTarget=null;this.atlasKeys=new bt;let i=a.getLayers().length;for(let l=0;l=Rm&&(s.push(new CA(f,new R(new v(h,c),new v(_-h,1)),m)),h=1/0,_=-1/0,f=new vn(this.gl));let I=new R(new v(p.start,c),new v(p.end-p.start,1));h=Math.min(h,I.left()),_=Math.max(_,I.right());let j=new fe((1+g%255)/256,(1+l%255)/256,(1+this.flamechart.getColorBucketForFrame(p.node.frame))/256);f.addRect(I,j),m++}f.getRectCount()>0&&s.push(new CA(f,new R(new v(h,c),new v(_-h,1)),m)),this.layers.push(new Xo(s))}}getRectInfoTexture(e,n){if(this.rectInfoTexture){let a=this.rectInfoTexture;(a.width!=e||a.height!=n)&&a.resize(e,n)}else this.rectInfoTexture=this.gl.createTexture(q.TextureFormat.NEAREST_CLAMP,e,n);return this.rectInfoTexture}getRectInfoRenderTarget(e,n){let a=this.getRectInfoTexture(e,n);return this.rectInfoRenderTarget&&this.rectInfoRenderTarget.texture!=a&&(this.rectInfoRenderTarget.texture.free(),this.rectInfoRenderTarget.setColor(a)),this.rectInfoRenderTarget||(this.rectInfoRenderTarget=this.gl.createRenderTarget(a)),this.rectInfoRenderTarget}free(){this.rectInfoRenderTarget&&this.rectInfoRenderTarget.free(),this.rectInfoTexture&&this.rectInfoTexture.free()}configSpaceBoundsForKey(e){let{stackDepth:n,zoomLevel:a,index:r}=e,o=this.flamechart.getTotalWeight()/Math.pow(2,a),i=this.flamechart.getLayers().length,l=this.options.inverted?i-1-n:n;return new R(new v(o*r,l),new v(o,1))}render(e){let{configSpaceSrcRect:n,physicalSpaceDstRect:a}=e,r=[],A=U.betweenRects(n,a);if(n.isEmpty())return;let o=0;for(;;){let j=mA.getOrInsert(this.atlasKeys,{stackDepth:0,zoomLevel:o,index:0}),E=this.configSpaceBoundsForKey(j);if(A.transformRect(E).width(){let y=this.configSpaceBoundsForKey(E);this.layers[E.stackDepth].forEachLeafNodeWithinBounds(y,b=>{this.rectangleBatchRenderer.render({batch:b.getBatch(),configSpaceSrcRect:y,physicalSpaceDstRect:j})})});let p=this.getRectInfoRenderTarget(a.width(),a.height());Ba(this.gl,p,()=>{this.gl.clear(new q.Color(0,0,0,0));let j=new R(v.zero,new v(this.gl.viewport.width,this.gl.viewport.height)),E=U.betweenRects(n,j);for(let y of u){let b=this.configSpaceBoundsForKey(y);this.rowAtlas.renderViaAtlas(y,E.transformRect(b))}for(let y of g){let b=this.configSpaceBoundsForKey(y),F=E.transformRect(b);this.layers[y.stackDepth].forEachLeafNodeWithinBounds(b,T=>{this.rectangleBatchRenderer.render({batch:T.getBatch(),configSpaceSrcRect:b,physicalSpaceDstRect:F})})}});let I=this.getRectInfoTexture(a.width(),a.height());this.colorPassRenderer.render({rectInfoTexture:I,srcRect:new R(v.zero,new v(I.width,I.height)),dstRect:a,renderOutlines:e.renderOutlines})}};V();V();var Dt=Ce(t=>de.create({hoverCount:{color:t.weightColor},fill:{width:"100%",height:"100%",position:"absolute",left:0,top:0},minimap:{height:100,borderBottom:`2px solid ${t.fgSecondaryColor}`},panZoomView:{flex:1},detailView:{display:"grid",height:150,overflow:"hidden",gridTemplateColumns:"120px 120px 1fr",gridTemplateRows:"repeat(4, 1fr)",borderTop:`2px solid ${t.fgSecondaryColor}`,fontSize:10,position:"absolute",background:t.bgPrimaryColor,width:"100vw",bottom:0},stackTraceViewPadding:{padding:5},stackTraceView:{height:150,lineHeight:`${12}px`,overflow:"auto","::-webkit-scrollbar":{background:t.bgPrimaryColor},"::-webkit-scrollbar-thumb":{background:t.fgSecondaryColor,borderRadius:20,border:`3px solid ${t.bgPrimaryColor}`,":hover":{background:t.fgPrimaryColor}}},stackLine:{whiteSpace:"nowrap"},stackFileLine:{color:t.fgSecondaryColor},statsTable:{display:"grid",gridTemplateColumns:"1fr 1fr",gridTemplateRows:`repeat(3, ${20}px)`,gridGap:"1px 1px",textAlign:"center",paddingRight:1},statsTableHeader:{gridColumn:"1 / 3"},statsTableCell:{position:"relative",display:"flex",justifyContent:"center",alignItems:"center"},thisInstanceCell:{background:t.selectionPrimaryColor,color:t.altFgPrimaryColor},allInstancesCell:{background:t.selectionSecondaryColor,color:t.altFgPrimaryColor},barDisplay:{position:"absolute",top:0,left:0,background:"rgba(0, 0, 0, 0.2)",width:"100%"}}));V();var ti="\u2026",jA=new Map,F_=-1;function Zt(t,e){return window.devicePixelRatio!==F_&&(jA.clear(),F_=window.devicePixelRatio),jA.has(e)||jA.set(e,t.measureText(e).width),jA.get(e)}function ei(t,e){if(t.length<=e)return{trimmedString:t,trimmedLength:t.length,prefixLength:t.length,suffixLength:0,originalString:t,originalLength:t.length};let n=Math.floor(e/2),a=e-n-1,r=t.substring(0,n),A=t.substring(t.length-a,t.length),o=r+ti+A;return{trimmedString:o,trimmedLength:o.length,prefixLength:r.length,suffixLength:A.length,originalString:t,originalLength:t.length}}function R_(t,e,n){if(Zt(t,e)<=n)return ei(e,e.length);let[a]=ls(0,e.length,r=>Zt(t,ei(e,Math.floor(r)).trimmedString),n);return ei(e,Math.floor(a))}function D_(t,e){return e{this.container=n||null};this.overlayCanvas=null;this.overlayCtx=null;this.onWindowResize=()=>{this.onBeforeFrame()};this.onBeforeFrame=()=>{this.maybeClearInteractionLock(),this.resizeOverlayCanvasIfNeeded(),this.renderRects(),this.renderOverlays()};this.renderCanvas=()=>{this.props.canvasContext.requestFrame()};this.frameHadWheelEvent=!1;this.framesWithoutWheelEvents=0;this.interactionLock=null;this.maybeClearInteractionLock=()=>{this.interactionLock&&(this.frameHadWheelEvent||(this.framesWithoutWheelEvents++,this.framesWithoutWheelEvents>=2&&(this.interactionLock=null,this.framesWithoutWheelEvents=0)),this.props.canvasContext.requestFrame()),this.frameHadWheelEvent=!1};this.onWheel=n=>{if(n.preventDefault(),this.frameHadWheelEvent=!0,(n.metaKey||n.ctrlKey)&&this.interactionLock!=="pan"){let r=1+n.deltaY/100;n.ctrlKey&&(r=1+n.deltaY/40),r=Re(r,.1,10),this.zoom(r)}else this.interactionLock!=="zoom"&&this.pan(new v(n.deltaX,n.deltaY));this.renderCanvas()};this.dragStartConfigSpaceMouse=null;this.dragConfigSpaceViewportOffset=null;this.draggingMode=null;this.onMouseDown=n=>{let a=this.configSpaceMouse(n);a&&(this.props.configSpaceViewportRect.contains(a)?(this.draggingMode=1,this.dragConfigSpaceViewportOffset=a.minus(this.props.configSpaceViewportRect.origin)):this.draggingMode=0,this.dragStartConfigSpaceMouse=a,window.addEventListener("mousemove",this.onWindowMouseMove),window.addEventListener("mouseup",this.onWindowMouseUp),this.updateCursor(a))};this.onWindowMouseMove=n=>{if(!this.dragStartConfigSpaceMouse)return;let a=this.configSpaceMouse(n);if(a){if(this.updateCursor(a),a=new R(new v(0,0),this.configSpaceSize()).closestPointTo(a),this.draggingMode===0){let r=this.dragStartConfigSpaceMouse,A=a;if(!r||!A)return;let o=Math.min(r.x,A.x),l=Math.max(r.x,A.x)-o,s=this.props.configSpaceViewportRect.height();this.props.setConfigSpaceViewportRect(new R(new v(o,A.y-s/2),new v(l,s)))}else if(this.draggingMode===1){if(!this.dragConfigSpaceViewportOffset)return;let r=a.minus(this.dragConfigSpaceViewportOffset);this.props.setConfigSpaceViewportRect(this.props.configSpaceViewportRect.withOrigin(r))}}};this.updateCursor=n=>{this.draggingMode===1?(document.body.style.cursor="grabbing",document.body.style.cursor="-webkit-grabbing"):this.draggingMode===0?document.body.style.cursor="col-resize":this.props.configSpaceViewportRect.contains(n)?(document.body.style.cursor="grab",document.body.style.cursor="-webkit-grab"):document.body.style.cursor="col-resize"};this.onMouseLeave=()=>{this.draggingMode==null&&(document.body.style.cursor="default"),xr.set(null)};this.onMouseMove=n=>{let a=this.configSpaceMouse(n);a&&(this.updateCursor(a),xr.set(a))};this.onWindowMouseUp=n=>{this.draggingMode=null,window.removeEventListener("mousemove",this.onWindowMouseMove),window.removeEventListener("mouseup",this.onWindowMouseUp);let a=this.configSpaceMouse(n);a&&this.updateCursor(a)};this.overlayCanvasRef=n=>{n?(this.overlayCanvas=n,this.overlayCtx=this.overlayCanvas.getContext("2d"),this.renderCanvas()):(this.overlayCanvas=null,this.overlayCtx=null)}}physicalViewSize(){return new v(this.overlayCanvas?this.overlayCanvas.width:0,this.overlayCanvas?this.overlayCanvas.height:0)}getStyle(){return Dt(this.props.theme)}minimapOrigin(){return new v(0,20*window.devicePixelRatio)}configSpaceSize(){return new v(this.props.flamechart.getTotalWeight(),this.props.flamechart.getLayers().length)}configSpaceToPhysicalViewSpace(){let n=this.minimapOrigin();return U.betweenRects(new R(new v(0,0),this.configSpaceSize()),new R(n,this.physicalViewSize().minus(n)))}logicalToPhysicalViewSpace(){return U.withScale(new v(window.devicePixelRatio,window.devicePixelRatio))}windowToLogicalViewSpace(){if(!this.container)return new U;let n=this.container.getBoundingClientRect();return U.withTranslation(new v(-n.left,-n.top))}renderRects(){this.container&&(this.physicalViewSize().x<2||this.props.canvasContext.renderBehind(this.container,()=>{this.props.flamechartRenderer.render({configSpaceSrcRect:new R(new v(0,0),this.configSpaceSize()),physicalSpaceDstRect:new R(this.minimapOrigin(),this.physicalViewSize().minus(this.minimapOrigin())),renderOutlines:!1}),this.props.canvasContext.viewportRectangleRenderer.render({configSpaceViewportRect:this.props.configSpaceViewportRect,configSpaceToPhysicalViewSpace:this.configSpaceToPhysicalViewSpace()})}))}renderOverlays(){let n=this.overlayCtx;if(!n)return;let a=this.physicalViewSize();n.clearRect(0,0,a.x,a.y);let r=this.configSpaceToPhysicalViewSpace(),A=0,o=this.configSpaceSize().x,l=(this.configSpaceToPhysicalViewSpace().inverted()||new U).times(this.logicalToPhysicalViewSpace()).transformVector(new v(200,1)).x,s=20*window.devicePixelRatio,c=10*window.devicePixelRatio,h=(s-c)/2;n.font=`${c}px/${s}px "Source Code Pro", Courier, monospace`,n.textBaseline="top";let f=Math.pow(10,Math.floor(Math.log10(l)));l/f>5?f*=5:l/f>2&&(f*=2);let m=this.props.theme;{n.fillStyle=fe.fromCSSHex(m.bgPrimaryColor).withAlpha(.8).toCSS(),n.fillRect(0,0,a.x,s),n.textBaseline="top";for(let u=Math.ceil(A/f)*f;ude.create({stackChit:{position:"relative",top:-1,display:"inline-block",verticalAlign:"middle",marginRight:"0.5em",border:`1px solid ${t.fgSecondaryColor}`,width:8,height:8}}));function T_(t){let e=Dt(z()),n=t.formatter(t.selectedTotal),a=t.formatter(t.selectedSelf),r=100*t.selectedTotal/t.grandTotal,A=100*t.selectedSelf/t.grandTotal;return C("div",{className:B(e.statsTable)},C("div",{className:B(t.cellStyle,e.statsTableCell,e.statsTableHeader)},t.title),C("div",{className:B(t.cellStyle,e.statsTableCell)},"Total"),C("div",{className:B(t.cellStyle,e.statsTableCell)},"Self"),C("div",{className:B(t.cellStyle,e.statsTableCell)},n),C("div",{className:B(t.cellStyle,e.statsTableCell)},a),C("div",{className:B(t.cellStyle,e.statsTableCell)},xt(r),C("div",{className:B(e.barDisplay),style:{height:`${r}%`}})),C("div",{className:B(t.cellStyle,e.statsTableCell)},xt(A),C("div",{className:B(e.barDisplay),style:{height:`${A}%`}})))}function Tm(t){let e=Dt(z()),n=[],a=t.node;for(;a&&!a.isRoot();a=a.parent){let r=[],{frame:A}=a;if(r.push(C(EA,{color:t.getFrameColor(A)})),n.length&&r.push(C("span",{className:B(e.stackFileLine)},"> ")),r.push(A.name),A.file){let o=A.file;A.line!=null&&(o+=`:${A.line}`,A.col!=null&&(o+=`:${A.col}`)),r.push(C("span",{className:B(e.stackFileLine)}," (",o,")"))}n.push(C("div",{className:B(e.stackLine)},r))}return C("div",{className:B(e.stackTraceView)},C("div",{className:B(e.stackTraceViewPadding)},n))}function H_(t){let e=Dt(z()),{flamechart:n,selectedNode:a}=t,{frame:r}=a;return C("div",{className:B(e.detailView)},C(T_,{title:"This Instance",cellStyle:e.thisInstanceCell,grandTotal:n.getTotalWeight(),selectedTotal:a.getTotalWeight(),selectedSelf:a.getSelfWeight(),formatter:n.formatValue.bind(n)}),C(T_,{title:"All Instances",cellStyle:e.allInstancesCell,grandTotal:n.getTotalWeight(),selectedTotal:r.getTotalWeight(),selectedSelf:r.getSelfWeight(),formatter:n.formatValue.bind(n)}),C(Tm,{node:a,getFrameColor:t.getCSSColorForFrame}))}var Dr=class{constructor(){this.argsBatch=[]}text(e){this.argsBatch.push(e)}fill(e,n){if(this.argsBatch.length!==0){e.fillStyle=n;for(let a of this.argsBatch)e.fillText(a.text,a.x,a.y);this.argsBatch=[]}}},wn=class{constructor(){this.argsBatch=[]}rect(e){this.argsBatch.push(e)}drawPath(e){e.beginPath();for(let n of this.argsBatch)e.rect(n.x,n.y,n.w,n.h);e.closePath(),this.argsBatch=[]}fill(e,n){this.argsBatch.length!==0&&(e.fillStyle=n,this.drawPath(e),e.fill())}stroke(e,n,a){this.argsBatch.length!==0&&(e.strokeStyle=n,e.lineWidth=a,this.drawPath(e),e.stroke())}};var Xn=class extends Ne{constructor(){super(...arguments);this.container=null;this.containerRef=n=>{this.container=n||null};this.overlayCanvas=null;this.overlayCtx=null;this.hoveredLabel=null;this.overlayCanvasRef=n=>{n?(this.overlayCanvas=n,this.overlayCtx=this.overlayCanvas.getContext("2d"),this.renderCanvas()):(this.overlayCanvas=null,this.overlayCtx=null)};this.LOGICAL_VIEW_SPACE_FRAME_HEIGHT=20;this.onWindowResize=()=>{this.updateConfigSpaceViewport(),this.onBeforeFrame()};this.frameHadWheelEvent=!1;this.framesWithoutWheelEvents=0;this.interactionLock=null;this.maybeClearInteractionLock=()=>{this.interactionLock&&(this.frameHadWheelEvent||(this.framesWithoutWheelEvents++,this.framesWithoutWheelEvents>=2&&(this.interactionLock=null,this.framesWithoutWheelEvents=0)),this.props.canvasContext.requestFrame()),this.frameHadWheelEvent=!1};this.onBeforeFrame=()=>{this.resizeOverlayCanvasIfNeeded(),this.renderRects(),this.renderOverlays(),this.maybeClearInteractionLock()};this.renderCanvas=()=>{this.props.canvasContext.requestFrame()};this.lastDragPos=null;this.mouseDownPos=null;this.currentMousePos=null;this.onMouseDown=n=>{this.mouseDownPos=this.lastDragPos=new v(n.offsetX,n.offsetY),this.updateCursor(),window.addEventListener("mouseup",this.onWindowMouseUp)};this.onMouseDrag=n=>{if(!this.lastDragPos)return;let a=new v(n.offsetX,n.offsetY);this.pan(this.lastDragPos.minus(a)),this.lastDragPos=a,this.hoveredLabel&&this.props.onNodeHover(null)};this.onDblClick=n=>{if(this.hoveredLabel){let a=this.hoveredLabel.configSpaceBounds,r=new R(a.origin.minus(new v(0,1)),a.size.withY(this.props.configSpaceViewportRect.height()));this.props.setConfigSpaceViewportRect(r)}};this.onClick=n=>{let a=new v(n.offsetX,n.offsetY),r=this.mouseDownPos;this.mouseDownPos=null,!(r&&a.minus(r).length()>5)&&(this.hoveredLabel?(this.props.onNodeSelect(this.hoveredLabel.node),this.renderCanvas()):this.props.onNodeSelect(null))};this.onWindowMouseUp=n=>{this.lastDragPos=null,this.updateCursor(),window.removeEventListener("mouseup",this.onWindowMouseUp)};this.onMouseMove=n=>{if(this.currentMousePos=new v(n.offsetX,n.offsetY),this.updateCursor(),this.lastDragPos){n.preventDefault(),this.onMouseDrag(n);return}let a=new v(n.offsetX,n.offsetY),r=this.logicalToPhysicalViewSpace().transformPosition(a),A=this.configSpaceToPhysicalViewSpace().inverseTransformPosition(r);if(!A)return;let o=(l,s=0)=>{let c=l.end-l.start,h=this.props.renderInverted?this.configSpaceSize().y-1-s:s,_=new R(new v(l.start,h),new v(c,1));if(A.x<_.left()||A.x>_.right())return null;_.contains(A)&&(this.hoveredLabel={configSpaceBounds:_,node:l.node});for(let f of l.children)o(f,s+1)};(()=>{this.hoveredLabel=null})();for(let l of this.props.flamechart.getLayers()[0]||[])o(l);this.hoveredLabel?this.props.onNodeHover({node:this.hoveredLabel.node,event:n}):this.props.onNodeHover(null),this.renderCanvas()};this.onMouseLeave=n=>{this.currentMousePos=null,this.hoveredLabel=null,this.props.onNodeHover(null),this.renderCanvas()};this.onWheel=n=>{n.preventDefault(),this.frameHadWheelEvent=!0;let a=n.metaKey||n.ctrlKey,r=n.deltaY,A=n.deltaX;if(n.deltaMode===n.DOM_DELTA_LINE&&(r*=this.LOGICAL_VIEW_SPACE_FRAME_HEIGHT,A*=this.LOGICAL_VIEW_SPACE_FRAME_HEIGHT),a&&this.interactionLock!=="pan"){let o=1+r/100;n.ctrlKey&&(o=1+r/40),o=Re(o,.1,10),this.zoom(new v(n.offsetX,n.offsetY),o)}else this.interactionLock!=="zoom"&&this.pan(new v(A,r));this.renderCanvas()};this.onWindowKeyPress=n=>{if(!this.container)return;let{width:a,height:r}=this.container.getBoundingClientRect(),A=xr.get(),o,i=!0;if(A){let s=this.props.configSpaceViewportRect,c=A.x>=s.left()&&A.x<=s.right()&&A.y>=s.top()&&A.y<=s.bottom(),h=new v(A.x-s.width()/2,A.y-s.height()/2);this.props.setConfigSpaceViewportRect(s.withOrigin(h)),c||(i=!1),o=new v(a/2,r/2)}else o=this.currentMousePos||new v(a/2,r/2);let l=1;switch(n.key){case"=":case"+":l=.5;break;case"-":case"_":l=2;break}i&&requestAnimationFrame(()=>{this.zoom(o,l),n.preventDefault()}),!(n.ctrlKey||n.shiftKey||n.metaKey)&&(n.key==="0"?this.zoom(o,1e9):n.key==="ArrowRight"||n.code==="KeyD"?this.pan(new v(100,0)):n.key==="ArrowLeft"||n.code==="KeyA"?this.pan(new v(-100,0)):n.key==="ArrowUp"||n.code==="KeyW"?this.pan(new v(0,-100)):n.key==="ArrowDown"||n.code==="KeyS"?this.pan(new v(0,100)):n.key==="Escape"&&(this.props.onNodeSelect(null),this.renderCanvas()))}}getStyle(){return Dt(this.props.theme)}setConfigSpaceViewportRect(n){this.props.setConfigSpaceViewportRect(n)}configSpaceSize(){return new v(this.props.flamechart.getTotalWeight(),this.props.flamechart.getLayers().length)}physicalViewSize(){return new v(this.overlayCanvas?this.overlayCanvas.width:0,this.overlayCanvas?this.overlayCanvas.height:0)}physicalBounds(){if(this.props.renderInverted){let n=this.physicalViewSize().y,a=(this.configSpaceSize().y+1)*this.LOGICAL_VIEW_SPACE_FRAME_HEIGHT*window.devicePixelRatio;if(a{let F=y.end-y.start,T=this.props.renderInverted?this.configSpaceSize().y-1-b:b,Q=new R(new v(y.start,T),new v(F,1));if(!(Fthis.props.configSpaceViewportRect.right())&&!(Q.right()this.props.configSpaceViewportRect.bottom())return;if(Q.hasIntersectionWith(this.props.configSpaceViewportRect)){let w=a.transformRect(Q);if(w.left()<0&&(w=w.withOrigin(w.origin.withX(0)).withSize(w.size.withX(w.size.x+w.left()))),w.right()>o.x&&(w=w.withSize(w.size.withX(o.x-w.left()))),w.width()>i){let S=this.props.searchResults?.getMatchForFrame(y.node.frame),te=R_(n,y.node.frame.name,w.width()-2*s);if(S){let At=L_(te,S),Ct=0,on=w.left()+s,Qt=(A-r)/2-2;for(let[Pt,pa]of At){on+=Zt(n,te.trimmedString.substring(Ct,Pt));let vr=Zt(n,te.trimmedString.substring(Pt,pa));_.rect({x:on,y:w.top()+Qt,w:vr,h:A-2*Qt}),on+=vr,Ct=pa}}(this.props.searchResults!=null&&!S?h:c).text({text:te.trimmedString,x:w.left()+s,y:Math.round(w.bottom()-(A-r)/2)})}}for(let w of y.children)g(w,b+1)}},p=2*window.devicePixelRatio;n.strokeStyle=this.props.theme.selectionSecondaryColor;let I=(a.inverseTransformVector(new v(1,0))||new v(0,0)).x,j=(y,b=0)=>{if(!this.props.selectedNode&&this.props.searchResults==null)return;let F=y.end-y.start,T=this.props.renderInverted?this.configSpaceSize().y-1-b:b,Q=new R(new v(y.start,T),new v(F,1));if(!(Fthis.props.configSpaceViewportRect.right())&&!(Q.right()this.props.configSpaceViewportRect.bottom())){if(Q.hasIntersectionWith(this.props.configSpaceViewportRect)){if(this.props.searchResults?.getMatchForFrame(y.node.frame)){let w=a.transformRect(Q);u.rect({x:Math.round(w.left()+p/2),y:Math.round(w.top()+p/2),w:Math.round(Math.max(0,w.width()-p)),h:Math.round(Math.max(0,w.height()-p))})}if(this.props.selectedNode!=null&&y.node.frame===this.props.selectedNode.frame){let w=y.node===this.props.selectedNode?f:m,S=a.transformRect(Q);w.rect({x:Math.round(S.left()+1+p/2),y:Math.round(S.top()+1+p/2),w:Math.round(Math.max(0,S.width()-2-p)),h:Math.round(Math.max(0,S.height()-2-p))})}}for(let w of y.children)j(w,b+1)}};for(let y of this.props.flamechart.getLayers()[0]||[])j(y);for(let y of this.props.flamechart.getLayers()[0]||[])g(y);let E=this.props.theme;if(u.fill(n,E.searchMatchPrimaryColor),_.fill(n,E.searchMatchSecondaryColor),h.fill(n,E.fgSecondaryColor),c.fill(n,this.props.searchResults!=null?E.searchMatchTextColor:E.fgPrimaryColor),m.stroke(n,E.selectionSecondaryColor,p),f.stroke(n,E.selectionPrimaryColor,p),this.hoveredLabel){let y=E.fgPrimaryColor;this.props.selectedNode===this.hoveredLabel.node&&(y=E.selectionPrimaryColor),n.lineWidth=2*devicePixelRatio,n.strokeStyle=y;let b=a.transformRect(this.hoveredLabel.configSpaceBounds);n.strokeRect(Math.round(b.left()),Math.round(b.top()),Math.round(Math.max(0,b.width())),Math.round(Math.max(0,b.height())))}this.renderTimeIndicators()}renderTimeIndicators(){let n=this.overlayCtx;if(!n)return;let a=this.LOGICAL_VIEW_SPACE_FRAME_HEIGHT*window.devicePixelRatio,r=this.physicalViewSize(),A=this.configSpaceToPhysicalViewSpace(),o=10*window.devicePixelRatio,i=(a-o)/2,l=this.props.configSpaceViewportRect.left(),s=this.props.configSpaceViewportRect.right(),h=(this.configSpaceToPhysicalViewSpace().inverted()||new U).times(this.logicalToPhysicalViewSpace()).transformVector(new v(200,1)).x,f=Math.pow(10,Math.floor(Math.log10(h)));h/f>5?f*=5:h/f>2&&(f*=2);let m=this.props.theme;{let u=this.props.renderInverted?r.y-a:0;n.fillStyle=fe.fromCSSHex(m.bgPrimaryColor).withAlpha(.8).toCSS(),n.fillRect(0,u,r.x,a),n.textBaseline="top";for(let g=Math.ceil(l/f)*f;g{this.props.flamechartRenderer.render({physicalSpaceDstRect:this.physicalBounds(),configSpaceSrcRect:this.props.configSpaceViewportRect,renderOutlines:!0})}))}pan(n){this.interactionLock="pan";let a=this.logicalToPhysicalViewSpace().transformVector(n),r=this.configSpaceToPhysicalViewSpace().inverseTransformVector(a);this.hoveredLabel&&this.props.onNodeHover(null),r&&this.props.transformViewport(U.withTranslation(r))}zoom(n,a){this.interactionLock="zoom";let r=this.logicalToPhysicalViewSpace().transformPosition(n),A=this.configSpaceToPhysicalViewSpace().inverseTransformPosition(r);if(!A)return;let o=U.withTranslation(A.times(-1)).scaledBy(new v(a,1)).translatedBy(A);this.props.transformViewport(o)}updateCursor(){this.lastDragPos?(document.body.style.cursor="grabbing",document.body.style.cursor="-webkit-grabbing"):document.body.style.cursor="default"}shouldComponentUpdate(){return!1}componentWillReceiveProps(n){this.props.flamechart!==n.flamechart?(this.hoveredLabel=null,this.renderCanvas()):this.props.searchResults!==n.searchResults?this.renderCanvas():this.props.selectedNode!==n.selectedNode?this.renderCanvas():this.props.configSpaceViewportRect!==n.configSpaceViewportRect?this.renderCanvas():this.props.canvasContext!==n.canvasContext&&(this.props.canvasContext&&this.props.canvasContext.removeBeforeFrameHandler(this.onBeforeFrame),n.canvasContext&&(n.canvasContext.addBeforeFrameHandler(this.onBeforeFrame),n.canvasContext.requestFrame()))}componentDidMount(){this.props.canvasContext.addBeforeFrameHandler(this.onBeforeFrame),window.addEventListener("resize",this.onWindowResize),window.addEventListener("keydown",this.onWindowKeyPress)}componentWillUnmount(){this.props.canvasContext.removeBeforeFrameHandler(this.onBeforeFrame),window.removeEventListener("resize",this.onWindowResize),window.removeEventListener("keydown",this.onWindowKeyPress)}render(){let n=this.getStyle();return C("div",{className:B(n.panZoomView,Ee.vbox),onMouseDown:this.onMouseDown,onMouseMove:this.onMouseMove,onMouseLeave:this.onMouseLeave,onClick:this.onClick,onDblClick:this.onDblClick,onWheel:this.onWheel,ref:this.containerRef},C("canvas",{width:1,height:1,ref:this.overlayCanvasRef,className:B(n.fill)}))}};function QA(t){let e=Mm(z()),{containerSize:n,offset:a}=t,r=n.x,A=n.y,o=7,i=P(l=>{if(!l)return;let s=l.getBoundingClientRect(),c=a.x+o;c+s.width>r-1&&(c=r-s.width-1,c<1&&(c=1)),l.style.left=`${c}px`;let h=a.y+o;h+s.height>A-1&&(h=a.y-s.height-1,h<1&&(h=1)),l.style.top=`${h}px`},[r,A,a.x,a.y]);return C("div",{className:B(e.hoverTip),ref:i},C("div",{className:B(e.hoverTipRow)},t.children))}var BA=2,Mm=Ce(t=>de.create({hoverTip:{position:"absolute",background:t.bgPrimaryColor,border:"1px solid black",maxWidth:900,paddingTop:BA,paddingBottom:BA,pointerEvents:"none",userSelect:"none",fontSize:10,fontFamily:'"Source Code Pro", Courier, monospace',zIndex:2},hoverTipRow:{textOverflow:"ellipsis",whiteSpace:"nowrap",overflowX:"hidden",paddingLeft:BA,paddingRight:BA,maxWidth:900}}));var M_=wt(null),ni=({flamechart:t,selectedNode:e,setSelectedNode:n,configSpaceViewportRect:a,setConfigSpaceViewportRect:r,children:A})=>{let o=lt(Qn),i=oe(()=>o==null?null:new dA(t,o),[t,o]);return C(M_.Provider,{value:{results:i,flamechart:t,selectedNode:e,setSelectedNode:n,configSpaceViewportRect:a,setConfigSpaceViewportRect:r}},A)},K_=pe(()=>{let t=lt(M_),e=t==null?null:t.results,n=t==null?null:t.selectedNode,a=t==null?null:t.setSelectedNode,r=t==null?null:t.configSpaceViewportRect,A=t==null?null:t.setConfigSpaceViewportRect,o=t==null?null:t.flamechart,i=e==null?null:e.count(),l=oe(()=>e==null||n==null?null:e.indexOf(n),[e,n]),s=P(_=>{if(!a||!o||!r||!A)return;let f=_.configSpaceBounds,m=new R(f.origin.minus(new v(0,1)),f.size.withY(r.height()));a(_.node),A(o.getClampedConfigSpaceViewportRect({configSpaceViewportRect:m}))},[r,A,a,o]),{selectPrev:c,selectNext:h}=oe(()=>i==null||i===0||e==null?{selectPrev:()=>{},selectNext:()=>{}}:{selectPrev:()=>{if(!e?.at||i==null||i===0)return;let _=l==null?i-1:l-1;_<0&&(_=i-1);let f=e.at(_);s(f)},selectNext:()=>{if(!e?.at||i==null||i===0)return;let _=l==null?0:l+1;_>=i&&(_=0);let f=e.at(_);s(f)}},[i,l,e,s]);return C(uA,{resultIndex:l,numResults:i,selectPrev:c,selectNext:h})});var mt=class extends Ne{};var Rr=class extends mt{constructor(){super(...arguments);this.setConfigSpaceViewportRect=n=>{let a=150/20,r=this.configSpaceSize(),A=this.props.flamechart.getClampedViewportWidth(n.size.x),o=n.size.withX(A),i=v.clamp(n.origin,new v(0,-1),v.max(v.zero,r.minus(o).plus(new v(0,a+1))));this.props.setConfigSpaceViewportRect(new R(i,n.size.withX(A)))};this.setLogicalSpaceViewportSize=n=>{this.props.setLogicalSpaceViewportSize(n)};this.transformViewport=n=>{let a=n.transformRect(this.props.configSpaceViewportRect);this.setConfigSpaceViewportRect(a)};this.onNodeHover=n=>{this.props.setNodeHover(n)};this.onNodeClick=n=>{this.props.setSelectedNode(n)};this.container=null;this.containerRef=n=>{this.container=n||null}}getStyle(){return Dt(this.props.theme)}configSpaceSize(){return new v(this.props.flamechart.getTotalWeight(),this.props.flamechart.getLayers().length)}formatValue(n){let a=this.props.flamechart.getTotalWeight(),r=100*n/a,A=xt(r);return`${this.props.flamechart.formatValue(n)} (${A})`}renderTooltip(){if(!this.container)return null;let{hover:n}=this.props;if(!n)return null;let{width:a,height:r,left:A,top:o}=this.container.getBoundingClientRect(),i=new v(n.event.clientX-A,n.event.clientY-o),l=n.node.frame,s=this.getStyle();return C(QA,{containerSize:new v(a,r),offset:i},C("span",{className:B(s.hoverCount)},this.formatValue(n.node.getTotalWeight()))," ",l.name,l.file?C("div",null,l.file,":",l.line):void 0)}render(){let n=this.getStyle();return C("div",{className:B(n.fill,Ee.vbox),ref:this.containerRef},C(vA,{theme:this.props.theme,configSpaceViewportRect:this.props.configSpaceViewportRect,transformViewport:this.transformViewport,flamechart:this.props.flamechart,flamechartRenderer:this.props.flamechartRenderer,canvasContext:this.props.canvasContext,setConfigSpaceViewportRect:this.setConfigSpaceViewportRect}),C(Qn.Consumer,null,a=>C(ot,null,C(Xn,{theme:this.props.theme,canvasContext:this.props.canvasContext,flamechart:this.props.flamechart,flamechartRenderer:this.props.flamechartRenderer,renderInverted:!1,onNodeHover:this.onNodeHover,onNodeSelect:this.onNodeClick,selectedNode:this.props.selectedNode,transformViewport:this.transformViewport,configSpaceViewportRect:this.props.configSpaceViewportRect,setConfigSpaceViewportRect:this.setConfigSpaceViewportRect,logicalSpaceViewportSize:this.props.logicalSpaceViewportSize,setLogicalSpaceViewportSize:this.setLogicalSpaceViewportSize,searchResults:a}),C(K_,null))),this.renderTooltip(),this.props.selectedNode&&C(H_,{flamechart:this.props.flamechart,getCSSColorForFrame:this.props.getCSSColorForFrame,selectedNode:this.props.selectedNode}))}};function er(t){return{setNodeHover:P(e=>{He.setFlamechartHoveredNode(t,e)},[t]),setLogicalSpaceViewportSize:P(e=>{He.setLogicalSpaceViewportSize(t,e)},[t]),setConfigSpaceViewportRect:P(e=>{He.setConfigSpaceViewportRect(t,e)},[t]),setSelectedNode:P(e=>{He.setSelectedNode(t,e)},[t])}}var Km=ze(({profile:t,getColorBucketForFrame:e})=>new Wt({getTotalWeight:t.getTotalWeight.bind(t),forEachCall:t.forEachCall.bind(t),formatValue:t.formatValue.bind(t),getColorBucketForFrame:e})),tr=t=>ze(({canvasContext:e,flamechart:n})=>new IA(e.gl,_s(e),n,e.rectangleBatchRenderer,e.flamechartColorPassRenderer,t)),Jm=tr(),P_=pe(t=>{let{activeProfileState:e,glCanvas:n}=t,{profile:a,chronoViewState:r}=e,A=z(),o=Ot({theme:A,canvas:n}),i=qt(a),l=sn(i),s=Ut({theme:A,frameToColorBucket:i}),c=Km({profile:a,getColorBucketForFrame:l}),h=Jm({canvasContext:o,flamechart:c}),_=er("CHRONO");return C(ni,{flamechart:c,selectedNode:r.selectedNode,setSelectedNode:_.setSelectedNode,configSpaceViewportRect:r.configSpaceViewportRect,setConfigSpaceViewportRect:_.setConfigSpaceViewportRect},C(Rr,{theme:A,renderInverted:!1,flamechart:c,flamechartRenderer:h,canvasContext:o,getCSSColorForFrame:s,...r,..._}))}),Pm=ze(({profile:t,getColorBucketForFrame:e})=>new Wt({getTotalWeight:t.getTotalNonIdleWeight.bind(t),forEachCall:t.forEachCallGrouped.bind(t),formatValue:t.formatValue.bind(t),getColorBucketForFrame:e})),Gm=tr(),G_=pe(t=>{let{activeProfileState:e,glCanvas:n}=t,{profile:a,leftHeavyViewState:r}=e,A=z(),o=Ot({theme:A,canvas:n}),i=qt(a),l=sn(i),s=Ut({theme:A,frameToColorBucket:i}),c=Pm({profile:a,getColorBucketForFrame:l}),h=Gm({canvasContext:o,flamechart:c}),_=er("LEFT_HEAVY");return C(ni,{flamechart:c,selectedNode:r.selectedNode,setSelectedNode:_.setSelectedNode,configSpaceViewportRect:r.configSpaceViewportRect,setConfigSpaceViewportRect:_.setConfigSpaceViewportRect},C(Rr,{theme:A,renderInverted:!1,flamechart:c,flamechartRenderer:h,canvasContext:o,getCSSColorForFrame:s,...r,..._}))});function O_(t,e){return zm(t,e)}var ri=97,q_=122,z_=65,Um=90,Om=48,qm=57;function U_(t){let e=t.charCodeAt(0);return ri<=e&&e<=q_?1:z_<=e&&e<=Um?2:Om<=e&&e<=qm?3:0}function ai(t,e){if(t===e)return!0;let n=e.charCodeAt(0);return ri<=n&&n<=q_?t.charCodeAt(0)===n-ri+z_:!1}function zm(t,e){if(e.length==0)return{matchedRanges:[],score:0};let n=0,a=-1,r=-1,A=t.length,o=e.length;for(let i=0;i=a;i--){let l=t[i],s=e[n];if(ai(l,s)&&(n--,n<0))return a=i,Xm(t,e,a,r)}throw new Error("Implementation error. This must be a bug in fzfFuzzyMatchV1")}var Ai=16,$_=-3,oi=-1,ii=Ai/2,$m=Ai/2,Vm=ii+oi,Ym=-($_+oi),Wm=2;function Zm(t,e){return t===0&&e!==0?ii:t===1&&e==2||t!==3&&e==3?Vm:e===0?$m:0}function Xm(t,e,n,a){let r=0,A=0,o=!1,i=0,l=0,s=new Array(e.length),c=0;n>0&&(c=U_(t[n-1]));for(let _=n;_{o(),t(i)},[o,t,i]),_=P(p=>{e(i)},[e,i]),f=n.getName(),m=n.getTotalNonIdleWeight(),u=B(c.highlighted),g=oe(()=>eI(f,l,u),[f,l,u]);return C("tr",{ref:A,onMouseUp:h,onMouseEnter:_,title:f,className:B(c.profileRow,s%2===0&&c.profileRowEven,a&&c.profileRowSelected,r&&c.profileRowHovered)},C("td",{className:B(c.indexCell)},i+1),C("td",{className:B(c.nameCell)},g),C("td",{className:B(c.weightCell)},n.formatValue(m)))}function V_(t){t.stopPropagation()}function nI(t,e,n){let a=[];for(let r=0;r{let o=r.profile.getName().toLowerCase(),i=A.profile.getName().toLowerCase();return n.direction==="ascending"?o.localeCompare(i):i.localeCompare(o)}):n.field==="weight"?Fe(a,r=>n.direction==="ascending"?r.profile.getTotalNonIdleWeight():-r.profile.getTotalNonIdleWeight()):n.field==="index"?Fe(a,r=>n.direction==="ascending"?r.indexInProfileGroup:-r.indexInProfileGroup):Fe(a,r=>-r.score),a}function W_({profiles:t,closeProfileSelect:e,indexToView:n,visible:a,setProfileIndexToView:r}){let A=si(z()),[o,i]=Ge(""),[l,s]=Ge({field:"index",direction:"ascending"}),c=P(Q=>{let w=Q.target.value;i(w)},[i]),h=P(Q=>{Q&&(a?Q.select():Q.blur())},[a]),_=P((Q,w)=>{w.preventDefault(),w.stopPropagation(),l.field===Q?s({field:Q,direction:l.direction==="ascending"?"descending":"ascending"}):s({field:Q,direction:Q==="name"||Q==="index"?"ascending":"descending"})},[l,s]),f=oe(()=>nI(t,o,l),[t,o,l]),[m,u]=Ge(0),g=cn(null);Ue(()=>{a&&(u(null),g.current!==null&&g.current.scrollIntoView({behavior:"auto",block:"nearest",inline:"nearest"}))},[a]);let p=P(Q=>{Q.stopPropagation();let w=null;switch(Q.key){case"Enter":{m!=null&&(e(),r(m));break}case"Escape":{e();break}case"ArrowDown":{if(Q.preventDefault(),w=0,m!=null){let S=f.findIndex(te=>te.indexInProfileGroup===m);S!==-1&&(w=S+1)}break}case"ArrowUp":{if(Q.preventDefault(),w=f.length-1,m!=null){let S=f.findIndex(te=>te.indexInProfileGroup===m);S!==-1&&(w=S-1)}break}}if(w!=null&&w>=0&&w{f.length>0&&(u(f[0].indexInProfileGroup),j(!0))},[u,f]);let E=P(Q=>{I&&Q&&(Q.scrollIntoView({behavior:"auto",block:"nearest",inline:"nearest"}),j(!1))},[I,j]),y=P(Q=>{g.current=Q,E(Q)},[g,E]),b=P(Q=>_("name",Q),[_]),F=P(Q=>_("weight",Q),[_]),T=P(Q=>_("index",Q),[_]);return C("div",{className:B(A.profileSelectOuter)},C("div",{className:B(A.caret)}),C("div",{className:B(A.profileSelectBox)},C("div",{className:B(A.filterInputContainer)},C("input",{type:"text",className:B(A.filterInput),ref:h,placeholder:"Filter...",value:o,onInput:c,onKeyDown:p,onKeyUp:V_,onKeyPress:V_})),C("div",{className:B(A.profileSelectScrolling)},C("table",{className:B(A.tableView)},C("thead",{className:B(A.tableHeader)},C("tr",null,C("th",{className:B(A.indexHeaderCell),onClick:T},C(li,{activeDirection:l.field==="index"?l.direction:null}),"#"),C("th",{className:B(A.nameHeaderCell),onClick:b},C(li,{activeDirection:l.field==="name"?l.direction:null}),"Name"),C("th",{className:B(A.weightHeaderCell),onClick:F},C(li,{activeDirection:l.field==="weight"?l.direction:null}),"Total Weight"))),C("tbody",null,f.map(({profile:Q,matchedRanges:w,indexInProfileGroup:S},te)=>{let at,At=S===n,Ct=S===m;return At&&Ct?at=y:At?at=g:Ct&&(at=E),C(tI,{key:te,setHoveredProfileIndex:u,indexInProfileGroup:S,indexInFilteredListView:te,hovered:S==m,selected:S===n,profile:Q,nodeRef:at,matchedRanges:w,setProfileIndexToView:r,closeProfileSelect:e})}),f.length===0?C("tr",null,C("td",{colSpan:3,className:B(A.noResultsRow)},'No results match filter "',o,'"')):null)))))}var Y_=10,si=Ce(t=>de.create({filterInputContainer:{display:"flex",flexDirection:"column",padding:5,alignItems:"stretch"},filterInput:{color:t.altFgPrimaryColor,background:t.altBgSecondaryColor,borderRadius:5,padding:5,border:"none",":focus":{outline:"none"},"::selection":{color:t.altFgPrimaryColor,background:t.selectionPrimaryColor}},caret:{width:0,height:0,borderLeft:"5px solid transparent",borderRight:"5px solid transparent",borderBottom:"5px solid black"},highlighted:{background:t.selectionSecondaryColor},padding:{height:Y_,background:t.altBgPrimaryColor},tableView:{width:"100%",fontSize:10,background:t.altBgPrimaryColor,borderCollapse:"collapse"},tableHeader:{borderBottom:`2px solid ${t.altBgSecondaryColor}`,textAlign:"left",color:t.altFgPrimaryColor,userSelect:"none"},indexHeaderCell:{cursor:"pointer",padding:"8px 10px",textAlign:"right",fontWeight:"bold",width:"20px"},nameHeaderCell:{cursor:"pointer",padding:"8px 10px",textAlign:"left",fontWeight:"bold"},weightHeaderCell:{cursor:"pointer",padding:"8px 10px",textAlign:"right",fontWeight:"bold",width:"150px"},sortIcon:{position:"relative",top:1,marginRight:4},profileRow:{height:18,background:t.altBgPrimaryColor,cursor:"pointer",":hover":{background:t.altBgSecondaryColor}},profileRowHovered:{background:t.altBgSecondaryColor},profileRowSelected:{background:t.selectionPrimaryColor,color:t.altFgPrimaryColor},profileRowEven:{background:t.altBgSecondaryColor},indexCell:{padding:"4px 10px",textAlign:"right",whiteSpace:"nowrap",width:"20px",fontFamily:"monospace",color:t.altFgSecondaryColor},nameCell:{padding:"4px 10px",textAlign:"left",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",maxWidth:"300px"},weightCell:{padding:"4px 10px",textAlign:"right",whiteSpace:"nowrap",width:"150px",fontFamily:"monospace"},noResultsRow:{padding:"20px",textAlign:"center",color:t.altFgSecondaryColor,fontStyle:"italic"},profileSelectScrolling:{maxHeight:`min(calc(100vh - ${20-2*Y_}px), ${400}px)`,overflow:"auto","::-webkit-scrollbar":{background:t.altBgPrimaryColor},"::-webkit-scrollbar-thumb":{background:t.altFgSecondaryColor,borderRadius:20,border:`3px solid ${t.altBgPrimaryColor}`,":hover":{background:t.altBgPrimaryColor}}},profileSelectBox:{width:"100%",paddingBottom:10,background:t.altBgPrimaryColor,color:t.altFgPrimaryColor},profileSelectOuter:{width:"100%",maxWidth:600,margin:"0 auto",position:"relative",zIndex:1,alignItems:"center",display:"flex",flexDirection:"column"},profileIndex:{textAlign:"right",display:"inline-block",color:t.altFgSecondaryColor},profileIndexSelected:{color:t.altFgPrimaryColor}}));V();function ci(t,e){return P(()=>t(e),[t,e])}function aI(t){let e=wA(z()),n=ci(_n.set,0),a=ci(_n.set,1),r=ci(_n.set,2);return t.activeProfileState?C("div",{className:B(e.toolbarLeft)},C("div",{className:B(e.toolbarTab,t.viewMode===0&&e.toolbarTabActive),onClick:n},C("span",{className:B(e.emoji)},"\u{1F570}"),"Time Order"),C("div",{className:B(e.toolbarTab,t.viewMode===1&&e.toolbarTabActive),onClick:a},C("span",{className:B(e.emoji)},"\u2B05\uFE0F"),"Left Heavy"),C("div",{className:B(e.toolbarTab,t.viewMode===2&&e.toolbarTabActive),onClick:r},C("span",{className:B(e.emoji)},"\u{1F96A}"),"Sandwich")):null}var AI=(()=>{let t=null;return e=>{let n=e?.profiles.map(a=>a.profile)||null;return(t===null||n!=null&&!Qr(t,n))&&(t=n),t}})();function oI(t){let e=wA(z()),{activeProfileState:n,profileGroup:a}=t,r=AI(a),[A,o]=Ge(!1),i=P(()=>{o(!0)},[o]),l=P(()=>{o(!1)},[o]);return Ue(()=>{let s=c=>{c.key==="t"&&(c.preventDefault(),o(!0))};return window.addEventListener("keypress",s),()=>{window.removeEventListener("keypress",s)}},[o]),Ue(()=>{let s=c=>{c.key==="t"&&(c.preventDefault(),o(!0))};return window.addEventListener("keypress",s),()=>{window.removeEventListener("keypress",s)}},[o]),n&&a&&r?a.profiles.length===1?C(ot,null,n.profile.getName()):C("div",{className:B(e.toolbarCenter),onMouseLeave:l},C("span",{onMouseOver:i},n.profile.getName()," ",C("span",{className:B(e.toolbarProfileIndex)},"(",n.index+1,"/",a.profiles.length,")")),C("div",{style:{display:A?"block":"none"}},C(W_,{setProfileIndexToView:t.setProfileIndexToView,indexToView:a.indexToView,profiles:r,closeProfileSelect:l,visible:A}))):C(ot,null,"\u{1F52C}speedscope")}function iI(t){let e=wA(z()),n=Y(En),a=C("div",{className:B(e.toolbarTab),onClick:t.saveFile},C("span",{className:B(e.emoji)},"\u2934\uFE0F"),"Export"),r=C("div",{className:B(e.toolbarTab),onClick:t.browseForFile},C("span",{className:B(e.emoji)},"\u2935\uFE0F"),"Import"),A=C("div",{className:B(e.toolbarTab),onClick:En.cycleToNextColorScheme},C("span",{className:B(e.emoji)},"\u{1F3A8}"),C("span",{className:B(e.toolbarTabColorSchemeToggle)},Us(n))),o=C("div",{className:B(e.toolbarTab)},C("a",{href:"https://github.com/jlfwong/speedscope#usage",className:B(e.noLinkStyle),target:"_blank"},C("span",{className:B(e.emoji)},"\u2753"),"Help"));return C("div",{className:B(e.toolbarRight)},t.activeProfileState&&a,r,A,o)}function Z_(t){let e=wA(z());return C("div",{className:B(e.toolbar)},C(aI,{...t}),C(oI,{...t}),C(iI,{...t}))}var wA=Ce(t=>de.create({toolbar:{height:20,flexShrink:0,background:t.altBgPrimaryColor,color:t.altFgPrimaryColor,textAlign:"center",fontFamily:'"Source Code Pro", Courier, monospace',fontSize:12,lineHeight:"18px",userSelect:"none"},toolbarLeft:{position:"absolute",height:20,overflow:"hidden",top:0,left:0,marginRight:2,textAlign:"left"},toolbarCenter:{paddingTop:1,height:20},toolbarRight:{height:20,overflow:"hidden",position:"absolute",top:0,right:0,marginRight:2,textAlign:"right"},toolbarProfileIndex:{color:t.altFgSecondaryColor},toolbarTab:{background:t.altBgSecondaryColor,marginTop:2,height:18,lineHeight:"18px",paddingLeft:2,paddingRight:8,display:"inline-block",marginLeft:2,transition:"all 0.07s ease-in",":hover":{background:t.selectionSecondaryColor}},toolbarTabActive:{background:t.selectionPrimaryColor,":hover":{background:t.selectionPrimaryColor}},toolbarTabColorSchemeToggle:{display:"inline-block",textAlign:"center",minWidth:"50px"},emoji:{display:"inline-block",verticalAlign:"middle",paddingTop:"0px",marginRight:"0.3em"},noLinkStyle:{textDecoration:"none",color:"inherit"}}));V();var wI=Promise.resolve().then(()=>he(Ei())),bI=!1;async function vh(t,e){let n=await wI,a=null,r=null;try{r=JSON.parse(t),a=new n.default.SourceMapConsumer(r)}catch{return null}let A=[];a.eachMapping(function(i){A.push({...i,generatedColumn:i.generatedColumn+1,originalColumn:i.originalColumn+1})},{},n.default.SourceMapConsumer.GENERATED_ORDER);let o=e.replace(/\.[^/]*$/,"");return i=>{let l=!1;if((r?.file&&r?.file===i.file||("/"+i.file?.replace(/\.[^/]*$/,"")).endsWith("/"+o))&&(l=!0),!l||i.line==null||i.col==null)return null;let s=ss(A,_=>_.generatedLine>i.line?!0:_.generatedLine=i.col);if(s===-1)s=A.length-1;else{if(s===0)return null;s--}let c=A[s],h={};if(c.name!=null)h.name=c.name;else if(c.source!=null){let _=a?.sourceContentFor(c.source,!0);if(_){let m=_.split(` +`)[c.originalLine-1];if(m){let u=/\w+/.exec(m.substr(c.originalColumn-1));u&&(h.name=u[0])}}}switch(h.name){case"constructor":{h.name=i.name+" constructor";break}case"function":{h.name=i.name;break}case"const":case"export":{h.name=i.name;break}}return h.name&&i.name.includes(h.name)&&(h.name=i.name),c.source!=null&&(h.file=c.source,h.line=c.originalLine,h.col=c.originalColumn),bI&&(console.groupCollapsed(`Remapping "${i.name}" -> "${h.name}"`),console.log("before",{...i}),console.log("item @ index",c),console.log("item @ index + 1",A[s+1]),console.log("after",h),console.groupEnd()),h}}V();var yh=({items:t,axis:e,renderItems:n,className:a,initialIndexInView:r})=>{let[A,o]=Ge(null),[i,l]=Ge(0),s=cn(null),c=e==="x"?"width":"height",h=e==="x"?"left":"top",_=e==="x"?"scrollLeft":"scrollTop",f=r?t.reduce((y,b,F)=>F{y?requestAnimationFrame(()=>{o(y.getBoundingClientRect()[c]),m.current!=null&&(y.scrollTo({[h]:m.current}),m.current=null)}):o(null),s.current=y},[o,c,h]),g=oe(()=>{if(s.current==null||A==null||i==null)return null;let y=i-A/4,b=i+A+A/4,F=0,T=0,Q=0;for(;Q=y)break}let w=Q;for(;Q=b)break}let S=Math.min(Q,t.length-1);return{firstVisibleIndex:w,lastVisibleIndex:S,invisiblePrefixSize:T}},[A,i,t]),p=oe(()=>t.reduce((y,b)=>y+b.size,0),[t]),I=P(()=>{s.current!=null&&l(s.current[_])},[_]);Ue(()=>{let y=()=>{s.current!=null&&o(s.current.getBoundingClientRect()[c])};return window.addEventListener("resize",y),()=>{window.removeEventListener("resize",y)}},[c]);let j=oe(()=>g?n(g.firstVisibleIndex,g.lastVisibleIndex):null,[n,g]),E=oe(()=>C("div",{style:{height:p}},C("div",{style:{transform:`translateY(${g?.invisiblePrefixSize||0}px)`}},j)),[g,j,p]);return C("div",{className:a,ref:u,onScroll:I},E)};function Eh(t){let e=FA(z());return C("div",{className:B(e.hBarDisplay)},C("div",{className:B(e.hBarDisplayFilled),style:{width:`${t.perc}%`}}))}function Bi(t){let e=z(),n=FA(e),{activeDirection:a}=t,r=a===0?e.fgPrimaryColor:e.fgSecondaryColor,A=a===1?e.fgPrimaryColor:e.fgSecondaryColor;return C("svg",{width:"8",height:"10",viewBox:"0 0 8 10",fill:"none",xmlns:"http://www.w3.org/2000/svg",className:B(n.sortIcon)},C("path",{d:"M0 4L4 0L8 4H0Z",fill:r}),C("path",{d:"M0 4L4 0L8 4H0Z",transform:"translate(0 10) scale(1 -1)",fill:A}))}function SI(t,e,n){let a=[],r=0;for(let A of e)a.push(t.slice(r,A[0])),a.push(C("span",{className:n},t.slice(A[0],A[1]))),r=A[1];return a.push(t.slice(r)),C("span",null,a)}var NI=({frame:t,matchedRanges:e,profile:n,index:a,selectedFrame:r,setSelectedFrame:A,getCSSColorForFrame:o})=>{let i=FA(z()),l=t.getTotalWeight(),s=t.getSelfWeight(),c=100*l/n.getTotalNonIdleWeight(),h=100*s/n.getTotalNonIdleWeight(),_=t===r;return C("tr",{key:`${a}`,onClick:A.bind(null,t),className:B(i.tableRow,a%2==0&&i.tableRowEven,_&&i.tableRowSelected)},C("td",{className:B(i.numericCell)},n.formatValue(l)," (",xt(c),")",C(Eh,{perc:c})),C("td",{className:B(i.numericCell)},n.formatValue(s)," (",xt(h),")",C(Eh,{perc:h})),C("td",{title:t.file,className:B(i.textCell)},C(EA,{color:o(t)}),e?SI(t.name,e,B(i.matched,_&&i.matchedSelected)):t.name))},FI=pe(({profile:t,sortMethod:e,setSortMethod:n,selectedFrame:a,setSelectedFrame:r,getCSSColorForFrame:A,searchQuery:o,searchIsActive:i})=>{let l=FA(z()),s=P((g,p)=>{if(p.preventDefault(),e.field==g)n({field:g,direction:e.direction===0?1:0});else switch(g){case 0:{n({field:g,direction:0});break}case 1:{n({field:g,direction:1});break}case 2:{n({field:g,direction:1});break}}},[e,n]),c=lt(Mr),h=P((g,p)=>{if(!c)return null;let I=[];for(let j=g;j<=p;j++){let E=c.rowList[j],y=c.getSearchMatchForFrame(E);I.push(NI({frame:E,matchedRanges:y??null,index:j,profile:t,selectedFrame:a,setSelectedFrame:r,getCSSColorForFrame:A}))}return I.length===0&&(i?I.push(C("tr",null,C("td",{className:B(l.emptyState)},'No symbol names match query "',o,'".'))):I.push(C("tr",null,C("td",{className:B(l.emptyState)},"No symbols found.")))),C("table",{className:B(l.tableView)},I)},[c,t,a,r,A,i,o,l.emptyState,l.tableView]),_=oe(()=>c==null?[]:c.rowList.map(g=>({size:20})),[c]),f=P(g=>s(2,g),[s]),m=P(g=>s(1,g),[s]),u=P(g=>s(0,g),[s]);return C("div",{className:B(Ee.vbox,l.profileTableView)},C("table",{className:B(l.tableView)},C("thead",{className:B(l.tableHeader)},C("tr",null,C("th",{className:B(l.numericCell),onClick:f},C(Bi,{activeDirection:e.field===2?e.direction:null}),"Total"),C("th",{className:B(l.numericCell),onClick:m},C(Bi,{activeDirection:e.field===1?e.direction:null}),"Self"),C("th",{className:B(l.textCell),onClick:u},C(Bi,{activeDirection:e.field===0?e.direction:null}),"Symbol Name")))),C(yh,{axis:"y",items:_,className:B(l.scrollView),renderItems:h,initialIndexInView:a==null?null:c?.getIndexForFrame(a)}))}),FA=Ce(t=>de.create({profileTableView:{background:t.bgPrimaryColor,height:"100%"},scrollView:{overflowY:"auto",overflowX:"hidden",flexGrow:1,"::-webkit-scrollbar":{background:t.bgPrimaryColor},"::-webkit-scrollbar-thumb":{background:t.fgSecondaryColor,borderRadius:20,border:`3px solid ${t.bgPrimaryColor}`,":hover":{background:t.fgPrimaryColor}}},tableView:{width:"100%",fontSize:10,background:t.bgPrimaryColor},tableHeader:{borderBottom:`2px solid ${t.bgSecondaryColor}`,textAlign:"left",color:t.fgPrimaryColor,userSelect:"none"},sortIcon:{position:"relative",top:1,marginRight:20/4},tableRow:{background:t.bgPrimaryColor,height:20},tableRowEven:{background:t.bgSecondaryColor},tableRowSelected:{background:t.selectionPrimaryColor,color:t.altFgPrimaryColor},numericCell:{textOverflow:"ellipsis",overflow:"hidden",whiteSpace:"nowrap",position:"relative",textAlign:"right",paddingRight:20,width:120,minWidth:120},textCell:{textOverflow:"ellipsis",overflow:"hidden",whiteSpace:"nowrap",width:"100%",maxWidth:0},hBarDisplay:{position:"absolute",background:fe.fromCSSHex(t.weightColor).withAlpha(.2).toCSS(),bottom:2,height:2,width:`calc(100% - ${40}px)`,right:20},hBarDisplayFilled:{height:"100%",position:"absolute",background:t.weightColor,right:0},matched:{borderBottom:`2px solid ${t.fgPrimaryColor}`},matchedSelected:{borderColor:t.altFgPrimaryColor},emptyState:{textAlign:"center",fontWeight:"bold"}})),Bh=pe(t=>{let{activeProfileState:e}=t,{profile:n,sandwichViewState:a}=e;if(!n)throw new Error("profile missing");let r=Y(kr),A=z(),{callerCallee:o}=a,i=o?o.selectedFrame:null,l=qt(n),s=Ut({theme:A,frameToColorBucket:l}),c=P(f=>{He.setSelectedFrame(f)},[]),h=Y($n),_=Y(Vn);return C(FI,{profile:n,selectedFrame:i,getCSSColorForFrame:s,sortMethod:r,setSelectedFrame:c,setSortMethod:kr.set,searchIsActive:h,searchQuery:_})});V();V();var ir=class extends mt{constructor(){super(...arguments);this.setConfigSpaceViewportRect=n=>{this.props.setConfigSpaceViewportRect(this.clampViewportToFlamegraph(n))};this.setLogicalSpaceViewportSize=n=>{this.props.setLogicalSpaceViewportSize(n)};this.transformViewport=n=>{this.setConfigSpaceViewportRect(n.transformRect(this.props.configSpaceViewportRect))};this.container=null;this.containerRef=n=>{this.container=n||null};this.setNodeHover=n=>{this.props.setNodeHover(n)}}clampViewportToFlamegraph(n){let{flamechart:a,renderInverted:r}=this.props;return a.getClampedConfigSpaceViewportRect({configSpaceViewportRect:n,renderInverted:r})}formatValue(n){let a=this.props.flamechart.getTotalWeight(),r=100*n/a,A=xt(r);return`${this.props.flamechart.formatValue(n)} (${A})`}renderTooltip(){if(!this.container)return null;let{hover:n}=this.props;if(!n)return null;let{width:a,height:r,left:A,top:o}=this.container.getBoundingClientRect(),i=new v(n.event.clientX-A,n.event.clientY-o),l=DI(this.props.theme),s=n.node.frame;return C(QA,{containerSize:new v(a,r),offset:i},C("span",{className:B(l.hoverCount)},this.formatValue(n.node.getTotalWeight()))," ",s.name,s.file?C("div",null,s.file,":",s.line):void 0)}render(){return C("div",{className:B(Ee.fillY,Ee.fillX,Ee.vbox),ref:this.containerRef},C(Xn,{theme:this.props.theme,selectedNode:null,onNodeHover:this.setNodeHover,onNodeSelect:On,configSpaceViewportRect:this.props.configSpaceViewportRect,setConfigSpaceViewportRect:this.setConfigSpaceViewportRect,transformViewport:this.transformViewport,flamechart:this.props.flamechart,flamechartRenderer:this.props.flamechartRenderer,canvasContext:this.props.canvasContext,renderInverted:this.props.renderInverted,logicalSpaceViewportSize:this.props.logicalSpaceViewportSize,setLogicalSpaceViewportSize:this.setLogicalSpaceViewportSize,searchResults:null}),this.renderTooltip())}},DI=Ce(t=>de.create({hoverCount:{color:t.weightColor}}));var RI=ze(({profile:t,frame:e,flattenRecursion:n})=>{let a=t.getInvertedProfileForCallersOf(e);return n?a.getProfileWithRecursionFlattened():a}),LI=ze(({invertedCallerProfile:t,getColorBucketForFrame:e})=>new Wt({getTotalWeight:t.getTotalNonIdleWeight.bind(t),forEachCall:t.forEachCallGrouped.bind(t),formatValue:t.formatValue.bind(t),getColorBucketForFrame:e})),TI=tr({inverted:!0}),Qh=pe(t=>{let{activeProfileState:e}=t,{profile:n,sandwichViewState:a}=e,r=Y(zt),A=Y(yn),o=z();if(!n)throw new Error("profile missing");if(!A)throw new Error("glCanvas missing");let{callerCallee:i}=a;if(!i)throw new Error("callerCallee missing");let{selectedFrame:l}=i,s=qt(n),c=sn(s),h=Ut({theme:o,frameToColorBucket:s}),_=Ot({theme:o,canvas:A}),f=LI({invertedCallerProfile:RI({profile:n,frame:l,flattenRecursion:r}),getColorBucketForFrame:c}),m=TI({canvasContext:_,flamechart:f});return C(ir,{theme:o,renderInverted:!0,flamechart:f,flamechartRenderer:m,canvasContext:_,getCSSColorForFrame:h,...er("SANDWICH_INVERTED_CALLERS"),...i.invertedCallerFlamegraph,setSelectedNode:On})});V();var HI=ze(({profile:t,frame:e,flattenRecursion:n})=>{let a=t.getProfileForCalleesOf(e);return n?a.getProfileWithRecursionFlattened():a}),MI=ze(({calleeProfile:t,getColorBucketForFrame:e})=>new Wt({getTotalWeight:t.getTotalNonIdleWeight.bind(t),forEachCall:t.forEachCallGrouped.bind(t),formatValue:t.formatValue.bind(t),getColorBucketForFrame:e})),KI=tr(),wh=pe(t=>{let{activeProfileState:e}=t,{profile:n,sandwichViewState:a}=e,r=Y(zt),A=Y(yn),o=z();if(!n)throw new Error("profile missing");if(!A)throw new Error("glCanvas missing");let{callerCallee:i}=a;if(!i)throw new Error("callerCallee missing");let{selectedFrame:l}=i,s=qt(n),c=sn(s),h=Ut({theme:o,frameToColorBucket:s}),_=Ot({theme:o,canvas:A}),f=MI({calleeProfile:HI({profile:n,frame:l,flattenRecursion:r}),getColorBucketForFrame:c}),m=KI({canvasContext:_,flamechart:f});return C(ir,{theme:o,renderInverted:!1,flamechart:f,flamechartRenderer:m,canvasContext:_,getCSSColorForFrame:h,...er("SANDWICH_CALLEES"),...i.calleeFlamegraph,setSelectedNode:On})});var bh=pe(()=>{let t=lt(Mr),e=t!=null?t.rowList:null,n=t?.selectedFrame!=null?t.getIndexForFrame(t.selectedFrame):null,a=e!=null?e.length:null,{selectPrev:r,selectNext:A}=oe(()=>e==null||a==null||a===0||t==null?{selectPrev:()=>{},selectNext:()=>{}}:{selectPrev:()=>{let o=n==null?a-1:n-1;o<0&&(o=a-1),t.setSelectedFrame(e[o])},selectNext:()=>{let o=n==null?0:n+1;o>=a&&(o=0),t.setSelectedFrame(e[o])}},[n,e,a,t]);return C(uA,{resultIndex:n,numResults:a,selectPrev:r,selectNext:A})});V();var Qi=class extends mt{constructor(){super(...arguments);this.setSelectedFrame=n=>{this.props.setSelectedFrame(n)};this.onWindowKeyPress=n=>{n.key==="Escape"&&this.setSelectedFrame(null)}}componentDidMount(){window.addEventListener("keydown",this.onWindowKeyPress)}componentWillUnmount(){window.removeEventListener("keydown",this.onWindowKeyPress)}render(){let n=JI(this.props.theme),{selectedFrame:a}=this.props,r=null;return a&&(r=C("div",{className:B(Ee.fillY,n.callersAndCallees,Ee.vbox)},C("div",{className:B(Ee.hbox,n.panZoomViewWraper)},C("div",{className:B(n.flamechartLabelParent)},C("div",{className:B(n.flamechartLabel)},"Callers")),C(Qh,{glCanvas:this.props.glCanvas,activeProfileState:this.props.activeProfileState})),C("div",{className:B(n.divider)}),C("div",{className:B(Ee.hbox,n.panZoomViewWraper)},C("div",{className:B(n.flamechartLabelParent,n.flamechartLabelParentBottom)},C("div",{className:B(n.flamechartLabel,n.flamechartLabelBottom)},"Callees")),C(wh,{glCanvas:this.props.glCanvas,activeProfileState:this.props.activeProfileState})))),C("div",{className:B(Ee.hbox,Ee.fillY)},C("div",{className:B(n.tableView)},C(Bh,{activeProfileState:this.props.activeProfileState}),C(bh,null)),r)}},JI=Ce(t=>de.create({tableView:{position:"relative",flex:1},panZoomViewWraper:{flex:1},flamechartLabelParent:{display:"flex",flexDirection:"column",justifyContent:"flex-end",alignItems:"flex-start",fontSize:12,width:12*1.2,borderRight:`1px solid ${t.fgSecondaryColor}`},flamechartLabelParentBottom:{justifyContent:"flex-start"},flamechartLabel:{transform:"rotate(-90deg)",transformOrigin:"50% 50% 0",width:12*1.2,flexShrink:1},flamechartLabelBottom:{transform:"rotate(-90deg)",display:"flex",justifyContent:"flex-end"},callersAndCallees:{flex:1,borderLeft:`2px solid ${t.fgSecondaryColor}`},divider:{height:2,background:t.fgSecondaryColor}})),Mr=wt(null),xh=pe(t=>{let{activeProfileState:e,glCanvas:n}=t,{sandwichViewState:a,index:r}=e,{callerCallee:A}=a,o=z(),i=P(g=>{He.setSelectedFrame(g)},[]),l=e.profile,s=Y(kr),c=lt(Qn),h=A?A.selectedFrame:null,_=oe(()=>{let g=[];switch(l.forEachFrame(p=>{c&&!c.getMatchForFrame(p)||g.push(p)}),s.field){case 0:{Fe(g,p=>p.name.toLowerCase());break}case 1:{Fe(g,p=>p.getSelfWeight());break}case 2:{Fe(g,p=>p.getTotalWeight());break}}return s.direction===1&&g.reverse(),g},[l,c,s]),f=oe(()=>{let g=new Map;for(let p=0;p<_.length;p++)g.set(_[p],p);return p=>{let I=g.get(p);return I??null}},[_]),m=oe(()=>g=>c==null?null:c.getMatchForFrame(g),[c]),u={rowList:_,selectedFrame:h,setSelectedFrame:i,getIndexForFrame:f,getSearchMatchForFrame:m};return C(Mr.Provider,{value:u},C(Qi,{theme:o,activeProfileState:e,glCanvas:n,setSelectedFrame:i,selectedFrame:h,profileIndex:r}))});var jr=Promise.resolve().then(()=>(Sf(),kf));jr.then(()=>{});Promise.resolve().then(()=>(Vo(),$o)).then(()=>{});Promise.resolve().then(()=>he(Ei())).then(()=>{});async function Ff(t,e){return(await jr).importProfileGroupFromText(t,e)}async function Zy(t,e){return(await jr).importProfileGroupFromBase64(t,e)}async function Xy(t,e){return(await jr).importProfilesFromArrayBuffer(t,e)}async function eE(t){return(await jr).importProfilesFromFile(t)}async function tE(t){return(await jr).importFromFileSystemDirectoryEntry(t)}var nE=Nf();function rE(t){return t!=null&&t.isDirectory}var Ul=class extends mt{constructor(){super(...arguments);this.canvas=null;this.ref=n=>{n instanceof HTMLCanvasElement?this.canvas=n:this.canvas=null,this.props.setGLCanvas(this.canvas)};this.container=null;this.containerRef=n=>{n instanceof HTMLElement?this.container=n:this.container=null};this.maybeResize=()=>{if(!this.container||!this.props.canvasContext)return;let{width:n,height:a}=this.container.getBoundingClientRect(),r=n,A=a,o=n*window.devicePixelRatio,i=a*window.devicePixelRatio;this.props.canvasContext.gl.resize(o,i,r,A)};this.onWindowResize=()=>{this.props.canvasContext&&this.props.canvasContext.requestFrame()}}componentWillReceiveProps(n){this.props.canvasContext!==n.canvasContext&&(this.props.canvasContext&&this.props.canvasContext.removeBeforeFrameHandler(this.maybeResize),n.canvasContext&&(n.canvasContext.addBeforeFrameHandler(this.maybeResize),n.canvasContext.requestFrame()))}componentDidMount(){window.addEventListener("resize",this.onWindowResize)}componentWillUnmount(){this.props.canvasContext&&this.props.canvasContext.removeBeforeFrameHandler(this.maybeResize),window.removeEventListener("resize",this.onWindowResize)}render(){let n=Df(this.props.theme);return C("div",{ref:this.containerRef,className:B(n.glCanvasView)},C("canvas",{ref:this.ref,width:1,height:1}))}},_o=class extends mt{constructor(){super(...arguments);this.loadExample=()=>{this.loadProfile(async()=>{let n="perf-vertx-stacks-01-collapsed-all.txt",a=await fetch(nE).then(r=>r.text());return await Ff(n,a)})};this.onDrop=n=>{if(this.props.setDragActive(!1),n.preventDefault(),!n.dataTransfer)return;let a=n.dataTransfer.items[0];if("webkitGetAsEntry"in a){let A=a.webkitGetAsEntry();if(A&&rE(A)&&A.name.endsWith(".trace")){console.log("Importing as Instruments.app .trace file");let o=A;this.loadProfile(async()=>await tE(o));return}}let r=n.dataTransfer.files.item(0);r&&this.loadFromFile(r)};this.onDragOver=n=>{this.props.setDragActive(!0),n.preventDefault()};this.onDragLeave=n=>{this.props.setDragActive(!1),n.preventDefault()};this.onWindowKeyPress=async n=>{if(n.key==="1")this.props.setViewMode(0);else if(n.key==="2")this.props.setViewMode(1);else if(n.key==="3")this.props.setViewMode(2);else if(n.key==="r"){let{flattenRecursion:a}=this.props;this.props.setFlattenRecursion(!a)}else if(n.key==="n"){let{activeProfileState:a}=this.props;a&&this.props.setProfileIndexToView(a.index+1)}else if(n.key==="p"){let{activeProfileState:a}=this.props;a&&this.props.setProfileIndexToView(a.index-1)}};this.saveFile=()=>{if(this.props.profileGroup){let{name:n,indexToView:a,profiles:r}=this.props.profileGroup,A={name:n,indexToView:a,profiles:r.map(o=>o.profile)};N_(A)}};this.browseForFile=()=>{let n=document.createElement("input");n.type="file",n.addEventListener("change",this.onFileSelect),n.click()};this.onWindowKeyDown=async n=>{n.key==="s"&&(n.ctrlKey||n.metaKey)?(n.preventDefault(),this.saveFile()):n.key==="o"&&(n.ctrlKey||n.metaKey)&&(n.preventDefault(),this.browseForFile())};this.onDocumentPaste=n=>{if(document.activeElement!=null&&document.activeElement.nodeName==="INPUT")return;n.preventDefault(),n.stopPropagation();let a=n.clipboardData;if(!a)return;let r=a.getData("text");this.loadProfile(async()=>await Ff("From Clipboard",r))};this.onFileSelect=n=>{let a=n.target.files.item(0);a&&this.loadFromFile(a)}}async loadProfile(n){if(this.props.setError(!1),this.props.setLoading(!0),await new Promise(r=>setTimeout(r,0)),!this.props.glCanvas)return;console.time("import");let a=null;try{a=await n()}catch(r){console.log("Failed to load format",r),this.props.setError(!0);return}if(a==null){alert("Unrecognized format! See documentation about supported formats."),this.props.setLoading(!1);return}else if(a.profiles.length===0){alert("Successfully imported profile, but it's empty!"),this.props.setLoading(!1);return}this.props.hashParams.title&&(a={...a,name:this.props.hashParams.title}),document.title=`${a.name} - speedscope`,this.props.hashParams.viewMode&&this.props.setViewMode(this.props.hashParams.viewMode);for(let r of a.profiles)await r.demangle();for(let r of a.profiles){let A=this.props.hashParams.title||r.getName();r.setName(A)}console.timeEnd("import"),this.props.setProfileGroup(a),this.props.setLoading(!1)}getStyle(){return Df(this.props.theme)}loadFromFile(n){this.loadProfile(async()=>{let a=await eE(n);if(a){for(let r of a.profiles)r.getName()||r.setName(n.name);return a}if(this.props.profileGroup&&this.props.activeProfileState){let r=new FileReader,A=new Promise(c=>{r.addEventListener("loadend",()=>{if(typeof r.result!="string")throw new Error("Expected reader.result to be a string");c(r.result)})});r.readAsText(n);let o=await A,i=null,l=E_(o);l&&(console.log("Importing as emscripten symbol map"),i=l);let s=await vh(o,n.name);if(!i&&s&&(console.log("Importing as JavaScript source map"),i=s),i!=null)return{name:this.props.profileGroup.name||"profile",indexToView:this.props.profileGroup.indexToView,profiles:this.props.profileGroup.profiles.map(c=>{let h=c.profile.shallowClone();return h.remapSymbols(i),h})}}return null})}componentDidMount(){window.addEventListener("keydown",this.onWindowKeyDown),window.addEventListener("keypress",this.onWindowKeyPress),document.addEventListener("paste",this.onDocumentPaste),this.maybeLoadHashParamProfile()}componentWillUnmount(){window.removeEventListener("keydown",this.onWindowKeyDown),window.removeEventListener("keypress",this.onWindowKeyPress),document.removeEventListener("paste",this.onDocumentPaste)}async maybeLoadHashParamProfile(){let{profileURL:n}=this.props.hashParams;if(n){if(!Ta){alert(`Cannot load a profile URL when loading from "${window.location.protocol}" URL protocol`);return}this.loadProfile(async()=>{let a=await fetch(n),r=new URL(n,window.location.href).pathname;return r.includes("/")&&(r=r.slice(r.lastIndexOf("/")+1)),await Xy(r,await a.arrayBuffer())})}else if(this.props.hashParams.localProfilePath){window.speedscope={loadFileFromBase64:(r,A)=>{this.loadProfile(()=>Zy(r,A))}};let a=document.createElement("script");a.src=`file:///${this.props.hashParams.localProfilePath}`,document.head.appendChild(a)}}renderLanding(){let n=this.getStyle();return C("div",{className:B(n.landingContainer)},C("div",{className:B(n.landingMessage)},C("p",{className:B(n.landingP)},"\u{1F44B} Hi there! Welcome to \u{1F52C}speedscope, an interactive"," ",C("a",{className:B(n.link),href:"http://www.brendangregg.com/FlameGraphs/cpuflamegraphs.html"},"flamegraph")," ","visualizer. Use it to help you make your software faster."),Ta?C("p",{className:B(n.landingP)},"Drag and drop a profile file onto this window to get started, click the big blue button below to browse for a profile to explore, or"," ",C("a",{tabIndex:0,className:B(n.link),onClick:this.loadExample},"click here")," ","to load an example profile."):C("p",{className:B(n.landingP)},"Drag and drop a profile file onto this window to get started, or click the big blue button below to browse for a profile to explore."),C("div",{className:B(n.browseButtonContainer)},C("input",{type:"file",name:"file",id:"file",onChange:this.onFileSelect,className:B(n.hide)}),C("label",{for:"file",className:B(n.browseButton),tabIndex:0},"Browse")),C("p",{className:B(n.landingP)},"See the"," ",C("a",{className:B(n.link),href:"https://github.com/jlfwong/speedscope#usage",target:"_blank"},"documentation")," ","for information about supported file formats, keyboard shortcuts, and how to navigate around the profile."),C("p",{className:B(n.landingP)},"speedscope is open source. Please"," ",C("a",{className:B(n.link),target:"_blank",href:"https://github.com/jlfwong/speedscope/issues"},"report any issues on GitHub"),".")))}renderError(){let n=this.getStyle();return C("div",{className:B(n.error)},C("div",null,"\u{1F63F} Something went wrong."),C("div",null,"Check the JS console for more details."))}renderLoadingBar(){let n=this.getStyle();return C("div",{className:B(n.loading)})}renderContent(){let{viewMode:n,activeProfileState:a,error:r,loading:A,glCanvas:o}=this.props;if(r)return this.renderError();if(A)return this.renderLoadingBar();if(!a||!o)return this.renderLanding();switch(n){case 0:return C(P_,{activeProfileState:a,glCanvas:o});case 1:return C(G_,{activeProfileState:a,glCanvas:o});case 2:return C(xh,{activeProfileState:a,glCanvas:o})}}render(){let n=this.getStyle();return C("div",{onDrop:this.onDrop,onDragOver:this.onDragOver,onDragLeave:this.onDragLeave,className:B(n.root,this.props.dragActive&&n.dragTargetRoot)},C(Ul,{setGLCanvas:this.props.setGLCanvas,canvasContext:this.props.canvasContext,theme:this.props.theme}),C(Z_,{saveFile:this.saveFile,browseForFile:this.browseForFile,...this.props}),C("div",{className:B(n.contentContainer)},this.renderContent()),this.props.dragActive&&C("div",{className:B(n.dragTarget)}))}},Df=Ce(t=>de.create({glCanvasView:{position:"absolute",width:"100vw",height:"100vh",zIndex:-1,pointerEvents:"none"},error:{display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",height:"100%"},loading:{height:3,marginBottom:-3,background:t.selectionPrimaryColor,transformOrigin:"0% 50%",animationName:[{from:{transform:"scaleX(0)"},to:{transform:"scaleX(1)"}}],animationTimingFunction:"cubic-bezier(0, 1, 0, 1)",animationDuration:"30s"},root:{width:"100vw",height:"100vh",overflow:"hidden",display:"flex",flexDirection:"column",position:"relative",fontFamily:'"Source Code Pro", Courier, monospace',lineHeight:"20px",color:t.fgPrimaryColor},dragTargetRoot:{cursor:"copy"},dragTarget:{boxSizing:"border-box",position:"absolute",top:0,left:0,width:"100%",height:"100%",border:`5px dashed ${t.selectionPrimaryColor}`,pointerEvents:"none"},contentContainer:{position:"relative",display:"flex",overflow:"hidden",flexDirection:"column",flex:1},landingContainer:{display:"flex",alignItems:"center",justifyContent:"center",flex:1},landingMessage:{maxWidth:600},landingP:{marginBottom:16},hide:{display:"none"},browseButtonContainer:{display:"flex",alignItems:"center",justifyContent:"center"},browseButton:{marginBottom:16,height:72,flex:1,maxWidth:256,textAlign:"center",fontSize:36,lineHeight:"72px",background:t.selectionPrimaryColor,color:t.altFgPrimaryColor,transition:"all 0.07s ease-in",":hover":{background:t.selectionSecondaryColor}},link:{color:t.selectionPrimaryColor,cursor:"pointer",textDecoration:"none",transition:"all 0.07s ease-in",":hover":{color:t.selectionSecondaryColor}}}));var Rf=pe(()=>{let t=Y(yn),e=z(),{canvasContext:n,error:a}=oe(()=>{if(!t)return{canvasContext:null,error:null};try{return{canvasContext:Ot({theme:e,canvas:t}),error:null}}catch(r){return console.error("Failed to create WebGL context:",r),{canvasContext:null,error:r}}},[e,t]);return Ue(()=>{a&&Ha.set(!0)},[a]),C(v_,null,C(_o,{activeProfileState:Ma(),canvasContext:n,setGLCanvas:yn.set,setLoading:Do.set,setError:Ha.set,setProfileGroup:He.setProfileGroup,setDragActive:Fo.set,setViewMode:_n.set,setFlattenRecursion:zt.set,setProfileIndexToView:He.setProfileIndexToView,profileGroup:Y(He),theme:e,flattenRecursion:Y(zt),viewMode:Y(_n),hashParams:Y(Fs),glCanvas:t,dragActive:Y(Fo),loading:Y(Do),error:Y(Ha)}))});console.log(`speedscope v${Yo().version}`);mo(C(Os,null,C(Rf,null)),document.body,document.body.lastElementChild||void 0);})(); +//# sourceMappingURL=speedscope-Y2522XSH.js.map diff --git a/tests/MauiSherpa.Core.Tests/Handlers/Profiling/AnalyzeProfilingArtifactHandlerTests.cs b/tests/MauiSherpa.Core.Tests/Handlers/Profiling/AnalyzeProfilingArtifactHandlerTests.cs new file mode 100644 index 00000000..8d0d2cf0 --- /dev/null +++ b/tests/MauiSherpa.Core.Tests/Handlers/Profiling/AnalyzeProfilingArtifactHandlerTests.cs @@ -0,0 +1,48 @@ +using FluentAssertions; +using MauiSherpa.Core.Handlers.Profiling; +using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Models.Profiling; +using MauiSherpa.Core.Requests.Profiling; +using Moq; +using Shiny.Mediator; + +namespace MauiSherpa.Core.Tests.Handlers.Profiling; + +public class AnalyzeProfilingArtifactHandlerTests +{ + [Fact] + public async Task Handle_ReturnsAnalysisFromService() + { + var service = new Mock(); + var context = new Mock(); + var expected = new ProfilingArtifactAnalysisResult( + new ProfilingArtifactAnalysis( + new ProfilingArtifactMetadata( + "artifact-1", + "session-1", + ProfilingArtifactKind.Trace, + "Trace capture", + "trace.json", + null, + "application/json", + DateTimeOffset.UtcNow), + "/tmp/trace.json", + true, + ProfilingAnalysisKind.Speedscope, + "summary", + [], + [], + [], + [])); + + service.Setup(x => x.AnalyzeArtifactAsync("artifact-1", It.IsAny())) + .ReturnsAsync(expected); + + var handler = new AnalyzeProfilingArtifactHandler(service.Object); + + var result = await handler.Handle(new AnalyzeProfilingArtifactRequest("artifact-1"), context.Object, CancellationToken.None); + + result.Should().Be(expected); + service.Verify(x => x.AnalyzeArtifactAsync("artifact-1", It.IsAny()), Times.Once); + } +} diff --git a/tests/MauiSherpa.Core.Tests/Handlers/Profiling/GetProfilingCapabilitiesHandlerTests.cs b/tests/MauiSherpa.Core.Tests/Handlers/Profiling/GetProfilingCapabilitiesHandlerTests.cs new file mode 100644 index 00000000..3b949306 --- /dev/null +++ b/tests/MauiSherpa.Core.Tests/Handlers/Profiling/GetProfilingCapabilitiesHandlerTests.cs @@ -0,0 +1,58 @@ +using FluentAssertions; +using MauiSherpa.Core.Handlers.Profiling; +using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Models.Profiling; +using MauiSherpa.Core.Requests.Profiling; +using Moq; +using Shiny.Mediator; + +namespace MauiSherpa.Core.Tests.Handlers.Profiling; + +public class GetProfilingCapabilitiesHandlerTests +{ + private readonly Mock _profilingCatalogService; + private readonly Mock _context; + private readonly GetProfilingCapabilitiesHandler _handler; + + public GetProfilingCapabilitiesHandlerTests() + { + _profilingCatalogService = new Mock(); + _context = new Mock(); + _handler = new GetProfilingCapabilitiesHandler(_profilingCatalogService.Object); + } + + [Fact] + public async Task Handle_ReturnsCapabilitiesForRequestedPlatform() + { + var expectedCapabilities = new ProfilingPlatformCapabilities( + ProfilingTargetPlatform.MacCatalyst, + "Mac Catalyst", + [ProfilingTargetKind.Desktop], + [ProfilingCaptureKind.Cpu, ProfilingCaptureKind.Memory], + [ProfilingArtifactKind.Trace], + [ProfilingScenarioKind.Launch, ProfilingScenarioKind.Interaction], + SupportsLaunchProfiling: true, + SupportsAttachToProcess: true, + SupportsLiveMetrics: true, + SupportsSymbolication: true); + + _profilingCatalogService.Setup(x => x.GetCapabilitiesAsync(ProfilingTargetPlatform.MacCatalyst, It.IsAny())) + .ReturnsAsync(expectedCapabilities); + + var result = await _handler.Handle( + new GetProfilingCapabilitiesRequest(ProfilingTargetPlatform.MacCatalyst), + _context.Object, + CancellationToken.None); + + result.Should().Be(expectedCapabilities); + _profilingCatalogService.Verify(x => x.GetCapabilitiesAsync(ProfilingTargetPlatform.MacCatalyst, It.IsAny()), Times.Once); + } + + [Fact] + public void GetKey_ReturnsPlatformSpecificKey() + { + var request = new GetProfilingCapabilitiesRequest(ProfilingTargetPlatform.Windows); + + request.GetKey().Should().Be("profiling:capabilities:Windows"); + } +} diff --git a/tests/MauiSherpa.Core.Tests/Handlers/Profiling/GetProfilingCatalogHandlerTests.cs b/tests/MauiSherpa.Core.Tests/Handlers/Profiling/GetProfilingCatalogHandlerTests.cs new file mode 100644 index 00000000..449378d6 --- /dev/null +++ b/tests/MauiSherpa.Core.Tests/Handlers/Profiling/GetProfilingCatalogHandlerTests.cs @@ -0,0 +1,62 @@ +using FluentAssertions; +using MauiSherpa.Core.Handlers.Profiling; +using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Models.Profiling; +using MauiSherpa.Core.Requests.Profiling; +using Moq; +using Shiny.Mediator; + +namespace MauiSherpa.Core.Tests.Handlers.Profiling; + +public class GetProfilingCatalogHandlerTests +{ + private readonly Mock _profilingCatalogService; + private readonly Mock _context; + private readonly GetProfilingCatalogHandler _handler; + + public GetProfilingCatalogHandlerTests() + { + _profilingCatalogService = new Mock(); + _context = new Mock(); + _handler = new GetProfilingCatalogHandler(_profilingCatalogService.Object); + } + + [Fact] + public async Task Handle_ReturnsCatalogFromService() + { + var expectedCatalog = new ProfilingCatalog( + [new ProfilingPlatformCapabilities( + ProfilingTargetPlatform.Android, + "Android", + [ProfilingTargetKind.PhysicalDevice, ProfilingTargetKind.Emulator], + [ProfilingCaptureKind.Cpu], + [ProfilingArtifactKind.Trace], + [ProfilingScenarioKind.Launch], + SupportsLaunchProfiling: true, + SupportsAttachToProcess: true, + SupportsLiveMetrics: true, + SupportsSymbolication: false)], + [new ProfilingScenarioDefinition( + ProfilingScenarioKind.Launch, + "Launch & startup", + "desc", + [ProfilingCaptureKind.Cpu], + TimeSpan.FromMinutes(1))]); + + _profilingCatalogService.Setup(x => x.GetCatalogAsync(It.IsAny())) + .ReturnsAsync(expectedCatalog); + + var result = await _handler.Handle(new GetProfilingCatalogRequest(), _context.Object, CancellationToken.None); + + result.Should().Be(expectedCatalog); + _profilingCatalogService.Verify(x => x.GetCatalogAsync(It.IsAny()), Times.Once); + } + + [Fact] + public void GetKey_ReturnsStableCatalogKey() + { + var request = new GetProfilingCatalogRequest(); + + request.GetKey().Should().Be("profiling:catalog"); + } +} diff --git a/tests/MauiSherpa.Core.Tests/Handlers/Profiling/GetProfilingPrerequisitesHandlerTests.cs b/tests/MauiSherpa.Core.Tests/Handlers/Profiling/GetProfilingPrerequisitesHandlerTests.cs new file mode 100644 index 00000000..a855e6e1 --- /dev/null +++ b/tests/MauiSherpa.Core.Tests/Handlers/Profiling/GetProfilingPrerequisitesHandlerTests.cs @@ -0,0 +1,71 @@ +using FluentAssertions; +using MauiSherpa.Core.Handlers.Profiling; +using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Models.Profiling; +using MauiSherpa.Core.Requests.Profiling; +using Moq; +using Shiny.Mediator; + +namespace MauiSherpa.Core.Tests.Handlers.Profiling; + +public class GetProfilingPrerequisitesHandlerTests +{ + private readonly Mock _profilingPrerequisitesService = new(); + private readonly Mock _context = new(); + private readonly GetProfilingPrerequisitesHandler _handler; + + public GetProfilingPrerequisitesHandlerTests() + { + _handler = new GetProfilingPrerequisitesHandler(_profilingPrerequisitesService.Object); + } + + [Fact] + public async Task Handle_ReturnsPrerequisiteReport() + { + var requestedCaptureKinds = new[] { ProfilingCaptureKind.Cpu }; + var expectedReport = new ProfilingPrerequisiteReport( + new ProfilingPrerequisiteContext( + ProfilingTargetPlatform.Android, + requestedCaptureKinds, + "/tmp", + "/usr/local/share/dotnet/dotnet", + new DoctorContext("/tmp", "/usr/local/share/dotnet", null, null, null, "10.0.100")), + [ + new ProfilingPrerequisiteStatus( + "dotnet-trace", + ProfilingPrerequisiteKind.DotNetTool, + DependencyStatusType.Ok, + IsRequired: true, + RequiredVersion: "10.x", + RecommendedVersion: "10.x", + InstalledVersion: "10.0.41001", + Message: "Ready") + ], + DateTimeOffset.UtcNow); + + _profilingPrerequisitesService + .Setup(x => x.GetPrerequisitesAsync( + ProfilingTargetPlatform.Android, + It.Is>(kinds => kinds.SequenceEqual(requestedCaptureKinds)), + null, + It.IsAny())) + .ReturnsAsync(expectedReport); + + var result = await _handler.Handle( + new GetProfilingPrerequisitesRequest(ProfilingTargetPlatform.Android, requestedCaptureKinds), + _context.Object, + CancellationToken.None); + + result.Should().Be(expectedReport); + } + + [Fact] + public void GetKey_ReturnsStablePlatformAndCaptureKey() + { + var request = new GetProfilingPrerequisitesRequest( + ProfilingTargetPlatform.Android, + [ProfilingCaptureKind.Memory, ProfilingCaptureKind.Cpu, ProfilingCaptureKind.Memory]); + + request.GetKey().Should().Be("profiling:prerequisites:Android:Cpu,Memory"); + } +} diff --git a/tests/MauiSherpa.Core.Tests/Handlers/Profiling/PlanProfilingCaptureHandlerTests.cs b/tests/MauiSherpa.Core.Tests/Handlers/Profiling/PlanProfilingCaptureHandlerTests.cs new file mode 100644 index 00000000..e5e108dc --- /dev/null +++ b/tests/MauiSherpa.Core.Tests/Handlers/Profiling/PlanProfilingCaptureHandlerTests.cs @@ -0,0 +1,69 @@ +using FluentAssertions; +using MauiSherpa.Core.Handlers.Profiling; +using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Models.Profiling; +using MauiSherpa.Core.Requests.Profiling; +using Moq; +using Shiny.Mediator; + +namespace MauiSherpa.Core.Tests.Handlers.Profiling; + +public class PlanProfilingCaptureHandlerTests +{ + private readonly Mock _profilingCaptureOrchestrationService = new(); + private readonly Mock _context = new(); + + [Fact] + public async Task Handle_ReturnsPlanFromOrchestrationService() + { + var session = new ProfilingSessionDefinition( + "session-1", + "Android launch", + new ProfilingTarget( + ProfilingTargetPlatform.Android, + ProfilingTargetKind.Emulator, + "emulator-5554", + "Pixel 8"), + ProfilingScenarioKind.Launch, + [ProfilingCaptureKind.Cpu], + CreatedAt: DateTimeOffset.UtcNow); + var request = new PlanProfilingCaptureRequest(session, new ProfilingCapturePlanOptions(ProjectPath: "/Users/test/App.csproj")); + var expectedPlan = new ProfilingCapturePlan( + session, + new ProfilingPlatformCapabilities( + ProfilingTargetPlatform.Android, + "Android", + [ProfilingTargetKind.PhysicalDevice, ProfilingTargetKind.Emulator], + [ProfilingCaptureKind.Cpu], + [ProfilingArtifactKind.Trace], + [ProfilingScenarioKind.Launch], + SupportsLaunchProfiling: true, + SupportsAttachToProcess: true, + SupportsLiveMetrics: true, + SupportsSymbolication: false), + request.Options!, + "macOS", + "net10.0-android", + "artifacts/profiling/session-1", + "/Users/test", + true, + null, + null, + new ProfilingPlanValidation([], []), + [], + [], + [], + new Dictionary()); + + _profilingCaptureOrchestrationService.Setup(service => + service.PlanCaptureAsync(session, request.Options, It.IsAny())) + .ReturnsAsync(expectedPlan); + + var handler = new PlanProfilingCaptureHandler(_profilingCaptureOrchestrationService.Object); + var result = await handler.Handle(request, _context.Object, CancellationToken.None); + + result.Should().Be(expectedPlan); + _profilingCaptureOrchestrationService.Verify(service => + service.PlanCaptureAsync(session, request.Options, It.IsAny()), Times.Once); + } +} diff --git a/tests/MauiSherpa.Core.Tests/Services/CopilotToolsServiceTests.cs b/tests/MauiSherpa.Core.Tests/Services/CopilotToolsServiceTests.cs new file mode 100644 index 00000000..ca834aa4 --- /dev/null +++ b/tests/MauiSherpa.Core.Tests/Services/CopilotToolsServiceTests.cs @@ -0,0 +1,31 @@ +using FluentAssertions; +using Moq; +using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Services; + +namespace MauiSherpa.Core.Tests.Services; + +public class CopilotToolsServiceTests +{ + [Fact] + public void Constructor_RegistersProfilingToolsAsReadOnly() + { + var sut = new CopilotToolsService( + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object); + + sut.GetTool("get_profiling_catalog").Should().NotBeNull(); + sut.GetTool("list_profiling_targets").Should().NotBeNull(); + sut.GetTool("list_profiling_artifacts").Should().NotBeNull(); + sut.GetTool("get_profiling_snapshot").Should().NotBeNull(); + sut.GetTool("analyze_profiling_artifact").Should().NotBeNull(); + sut.ReadOnlyToolNames.Should().Contain(new[] { "get_profiling_catalog", "list_profiling_targets", "list_profiling_artifacts", "get_profiling_snapshot", "analyze_profiling_artifact" }); + } +} diff --git a/tests/MauiSherpa.Core.Tests/Services/GcDumpReportServiceTests.cs b/tests/MauiSherpa.Core.Tests/Services/GcDumpReportServiceTests.cs new file mode 100644 index 00000000..7c7969aa --- /dev/null +++ b/tests/MauiSherpa.Core.Tests/Services/GcDumpReportServiceTests.cs @@ -0,0 +1,147 @@ +using MauiSherpa.Core.Services; +using FluentAssertions; + +namespace MauiSherpa.Core.Tests.Services; + +public class GcDumpReportServiceTests +{ + [Fact] + public void ParseHeapStatOutput_WithValidOutput_ReturnsReport() + { + var output = """ + MT Count TotalSize Class Name + 00007ffa12345678 100 4800 System.String + 00007ffa12345680 50 2400 System.Byte[] + 00007ffa12345690 25 1200 System.Int32[] + 00007ffa123456a0 10 480 System.Object + Total 185 objects, 8880 bytes + """; + + var report = GcDumpReportParser.ParseHeapStatOutput(output); + + report.Should().NotBeNull(); + report!.Types.Should().HaveCount(4); + report.TotalCount.Should().Be(185); + report.TotalSize.Should().Be(8880); + + // Should be sorted by size descending + report.Types[0].TypeName.Should().Be("System.String"); + report.Types[0].Count.Should().Be(100); + report.Types[0].Size.Should().Be(4800); + + report.Types[1].TypeName.Should().Be("System.Byte[]"); + report.Types[1].Count.Should().Be(50); + report.Types[1].Size.Should().Be(2400); + } + + [Fact] + public void ParseHeapStatOutput_WithEmptyOutput_ReturnsNull() + { + var report = GcDumpReportParser.ParseHeapStatOutput(""); + report.Should().BeNull(); + } + + [Fact] + public void ParseHeapStatOutput_WithOnlyHeaders_ReturnsNull() + { + var output = """ + MT Count TotalSize Class Name + Total 0 objects, 0 bytes + """; + + var report = GcDumpReportParser.ParseHeapStatOutput(output); + report.Should().BeNull(); + } + + [Fact] + public void ParseHeapStatOutput_WithLargeNumbers_ParsesCorrectly() + { + var output = """ + MT Count TotalSize Class Name + 00007ffaab8159c0 792355 110854724 System.String + 00007ffaab81aaa0 102205 60898249 System.Byte[] + """; + + var report = GcDumpReportParser.ParseHeapStatOutput(output); + + report.Should().NotBeNull(); + report!.Types.Should().HaveCount(2); + report.Types[0].TypeName.Should().Be("System.String"); + report.Types[0].Count.Should().Be(792355); + report.Types[0].Size.Should().Be(110854724); + } + + [Fact] + public void ParseHeapStatOutput_PreservesRawOutput() + { + var output = """ + MT Count TotalSize Class Name + 00007ffa12345678 5 240 System.String + """; + + var report = GcDumpReportParser.ParseHeapStatOutput(output); + + report.Should().NotBeNull(); + report!.RawOutput.Should().Be(output); + } + + [Fact] + public void ParseHeapStatOutput_WithGenericTypes_ParsesCorrectly() + { + var output = """ + MT Count TotalSize Class Name + 00007ffa12345678 10 480 System.Collections.Generic.Dictionary`2[[System.String],[System.Object]] + 00007ffa12345680 5 240 System.Collections.Generic.List`1[[System.Int32]] + """; + + var report = GcDumpReportParser.ParseHeapStatOutput(output); + + report.Should().NotBeNull(); + report!.Types.Should().HaveCount(2); + report.Types[0].TypeName.Should().Contain("Dictionary"); + report.Types[1].TypeName.Should().Contain("List"); + } + + [Fact] + public void ParseHeapStatOutput_WithModernFormat_ParsesCorrectly() + { + // Modern dotnet-gcdump output uses "Object Bytes Count Type" header + // with comma-formatted numbers and no hex MT prefix + var output = """ + 2,530,744 GC Heap bytes + 38,658 GC Heap objects + + Object Bytes Count Type + 46,376 1 System.Collections.Generic.Dictionary.Entry[] (Bytes > 10K) [Module(0xb400006fe0fb8390)] + 22,096 1 System.Collections.Generic.Dictionary.Entry[] (Bytes > 10K) [Module(0xb400006fe0fb8390)] + 8,224 1 System.Char[] (Bytes > 1K) [Module(0xb400006fe0fb8390)] + 4,016 3 System.String (Bytes > 1K) [Module(0xb400006fe0fb8390)] + 1,072 22 System.Collections.Generic.Dictionary.Entry[] (Bytes > 1K) [Module(0xb400006fe0fb8390)] + """; + + var report = GcDumpReportParser.ParseHeapStatOutput(output); + + report.Should().NotBeNull(); + report!.Types.Should().HaveCount(5); + + // Should be sorted by size descending + report.Types[0].TypeName.Should().StartWith("System.Collections.Generic.Dictionary.Entry 1K)" and "[Module(...)]" should be stripped + report.Types[3].TypeName.Should().NotContain("Bytes"); + report.Types[3].TypeName.Should().NotContain("Module"); + + report.TotalSize.Should().Be(46376 + 22096 + 8224 + 4016 + 1072); + report.TotalCount.Should().Be(1 + 1 + 1 + 3 + 22); + } +} diff --git a/tests/MauiSherpa.Core.Tests/Services/ProfilingArtifactAnalysisServiceTests.cs b/tests/MauiSherpa.Core.Tests/Services/ProfilingArtifactAnalysisServiceTests.cs new file mode 100644 index 00000000..a7ff29a0 --- /dev/null +++ b/tests/MauiSherpa.Core.Tests/Services/ProfilingArtifactAnalysisServiceTests.cs @@ -0,0 +1,221 @@ +using FluentAssertions; +using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Models.Profiling; +using MauiSherpa.Core.Services; +using Moq; + +namespace MauiSherpa.Core.Tests.Services; + +public class ProfilingArtifactAnalysisServiceTests : IDisposable +{ + private readonly string _libraryRoot; + private readonly string _externalRoot; + private readonly InMemoryEncryptedSettingsService _settingsService = new(); + private readonly Mock _logger = new(); + private readonly ProfilingArtifactLibraryService _artifactLibraryService; + private readonly ProfilingArtifactAnalysisService _analysisService; + + public ProfilingArtifactAnalysisServiceTests() + { + _libraryRoot = Path.Combine(Path.GetTempPath(), $"maui-sherpa-profiling-analysis-library-{Guid.NewGuid():N}"); + _externalRoot = Path.Combine(Path.GetTempPath(), $"maui-sherpa-profiling-analysis-external-{Guid.NewGuid():N}"); + _artifactLibraryService = new ProfilingArtifactLibraryService(_settingsService, _logger.Object, _libraryRoot); + _analysisService = new ProfilingArtifactAnalysisService(_artifactLibraryService, _logger.Object); + } + + [Fact] + public async Task AnalyzeArtifactAsync_SpeedscopeTrace_ReturnsHotspotSummary() + { + var tracePath = CreateExternalFile("session-1-trace.speedscope.json", """ + { + "$schema": "https://www.speedscope.app/file-format-schema.json", + "shared": { + "frames": [ + { "name": "RenderFrame", "file": "Render.cs", "line": 42 }, + { "name": "LayoutCycle", "file": "Layout.cs", "line": 15 } + ] + }, + "profiles": [ + { + "type": "sampled", + "name": "UI Thread", + "unit": "milliseconds", + "startValue": 0, + "endValue": 100, + "samples": [[0], [0], [1], [0]], + "weights": [40, 30, 10, 20] + } + ] + } + """); + + await SaveArtifactAsync( + id: "session-1-trace", + sessionId: "session-1", + kind: ProfilingArtifactKind.Trace, + fileName: "session-1-trace.speedscope.json", + artifactPath: tracePath, + contentType: "application/json"); + + var result = await _analysisService.AnalyzeArtifactAsync("session-1-trace"); + + result.Analysis.Should().NotBeNull(); + result.Analysis!.Kind.Should().Be(ProfilingAnalysisKind.Speedscope); + result.Analysis.Summary.Should().Contain("RenderFrame"); + result.Analysis.Hotspots.Should().NotBeEmpty(); + result.Analysis.Hotspots[0].Name.Should().Be("RenderFrame"); + result.Analysis.Hotspots[0].PercentOfTrace.Should().BeApproximately(90, 0.1); + result.Analysis.Metrics.Should().Contain(metric => metric.Key == "durationMs" && metric.NumericValue == 100); + result.Analysis.Insights.Should().Contain(insight => insight.Title == "Single hotspot dominates the trace"); + } + + [Fact] + public async Task AnalyzeArtifactAsync_LogArtifact_ReturnsCountsAndRecurringHotspots() + { + var logPath = CreateExternalFile("session-2-logs.txt", """ + 2025-01-01T12:00:00Z INFO Starting capture + 2025-01-01T12:00:01Z WARN Slow request to /api/items + 2025-01-01T12:00:02Z ERROR Timeout talking to backend + 2025-01-01T12:00:03Z ERROR Timeout talking to backend + 2025-01-01T12:00:04Z INFO Capture complete + """); + + await SaveArtifactAsync( + id: "session-2-logs", + sessionId: "session-2", + kind: ProfilingArtifactKind.Logs, + fileName: "session-2-logs.txt", + artifactPath: logPath, + contentType: "text/plain"); + + var result = await _analysisService.AnalyzeArtifactAsync("session-2-logs"); + + result.Analysis.Should().NotBeNull(); + result.Analysis!.Kind.Should().Be(ProfilingAnalysisKind.Logs); + result.Analysis.Summary.Should().Contain("2 error"); + result.Analysis.Hotspots.Should().ContainSingle(); + result.Analysis.Hotspots[0].Name.Should().Be("ERROR Timeout talking to backend"); + result.Analysis.Metrics.Should().Contain(metric => metric.Key == "warningCount" && metric.NumericValue == 1); + result.Analysis.Metrics.Should().Contain(metric => metric.Key == "errorCount" && metric.NumericValue == 2); + result.Analysis.Insights.Should().Contain(insight => insight.Title == "Errors detected in captured logs"); + } + + [Fact] + public async Task AnalyzeArtifactAsync_UnsupportedExport_FallsBackToMetadataSummary() + { + var exportPath = CreateExternalFile("session-3-memory.gcdump", "gcdump-binary-placeholder"); + + await SaveArtifactAsync( + id: "session-3-memory", + sessionId: "session-3", + kind: ProfilingArtifactKind.Export, + fileName: "session-3-memory.gcdump", + artifactPath: exportPath, + contentType: "application/octet-stream"); + + var result = await _analysisService.AnalyzeArtifactAsync("session-3-memory"); + + result.Analysis.Should().NotBeNull(); + result.Analysis!.Kind.Should().Be(ProfilingAnalysisKind.Metadata); + result.Analysis.Summary.Should().Contain("metadata-only analysis"); + result.Analysis.Notes.Should().Contain(note => note.Contains("specialized tool", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public async Task AnalyzeArtifactsAsync_UsesArtifactLibraryQuery() + { + var tracePath = CreateExternalFile("session-4-trace.json", "{ \"value\": 1 }"); + var logPath = CreateExternalFile("session-5-log.txt", "2025-01-01T00:00:00Z INFO Hello"); + + await SaveArtifactAsync("session-4-trace", "session-4", ProfilingArtifactKind.Report, "session-4-trace.json", tracePath, "application/json"); + await SaveArtifactAsync("session-5-log", "session-5", ProfilingArtifactKind.Logs, "session-5-log.txt", logPath, "text/plain"); + + var results = await _analysisService.AnalyzeArtifactsAsync(new ProfilingArtifactLibraryQuery(SessionId: "session-4")); + + results.Should().ContainSingle(); + results[0].Artifact.Id.Should().Be("session-4-trace"); + results[0].Kind.Should().Be(ProfilingAnalysisKind.Json); + } + + public void Dispose() + { + if (Directory.Exists(_libraryRoot)) + { + Directory.Delete(_libraryRoot, recursive: true); + } + + if (Directory.Exists(_externalRoot)) + { + Directory.Delete(_externalRoot, recursive: true); + } + } + + private async Task SaveArtifactAsync( + string id, + string sessionId, + ProfilingArtifactKind kind, + string fileName, + string artifactPath, + string contentType) + { + await _artifactLibraryService.SaveArtifactAsync(new ProfilingArtifactLibrarySaveRequest( + CreateMetadata(id, sessionId, kind, fileName, contentType), + ArtifactPath: artifactPath)); + } + + private string CreateExternalFile(string relativePath, string contents) + { + var path = Path.Combine(_externalRoot, relativePath); + var directory = Path.GetDirectoryName(path); + if (!string.IsNullOrWhiteSpace(directory)) + { + Directory.CreateDirectory(directory); + } + + File.WriteAllText(path, contents); + return path; + } + + private static ProfilingArtifactMetadata CreateMetadata( + string id, + string sessionId, + ProfilingArtifactKind kind, + string fileName, + string contentType) => + new( + Id: id, + SessionId: sessionId, + Kind: kind, + DisplayName: fileName, + FileName: fileName, + RelativePath: null, + ContentType: contentType, + CreatedAt: DateTimeOffset.UtcNow, + Properties: new Dictionary + { + ["targetPlatform"] = "MacCatalyst", + ["scenario"] = "Launch", + ["category"] = kind.ToString() + }); + + private sealed class InMemoryEncryptedSettingsService : IEncryptedSettingsService + { + public MauiSherpaSettings Current { get; private set; } = new(); + + public event Action? OnSettingsChanged; + + public Task GetSettingsAsync() => Task.FromResult(Current); + + public Task SaveSettingsAsync(MauiSherpaSettings settings) + { + Current = settings; + OnSettingsChanged?.Invoke(); + return Task.CompletedTask; + } + + public Task UpdateSettingsAsync(Func transform) => + SaveSettingsAsync(transform(Current)); + + public Task SettingsExistAsync() => Task.FromResult(true); + } +} diff --git a/tests/MauiSherpa.Core.Tests/Services/ProfilingArtifactLibraryServiceTests.cs b/tests/MauiSherpa.Core.Tests/Services/ProfilingArtifactLibraryServiceTests.cs new file mode 100644 index 00000000..bfa100a2 --- /dev/null +++ b/tests/MauiSherpa.Core.Tests/Services/ProfilingArtifactLibraryServiceTests.cs @@ -0,0 +1,208 @@ +using FluentAssertions; +using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Models.Profiling; +using MauiSherpa.Core.Services; +using Moq; + +namespace MauiSherpa.Core.Tests.Services; + +public class ProfilingArtifactLibraryServiceTests : IDisposable +{ + private readonly string _testRoot; + private readonly string _externalRoot; + private readonly InMemoryEncryptedSettingsService _settingsService = new(); + private readonly Mock _logger = new(); + private readonly ProfilingArtifactLibraryService _service; + + public ProfilingArtifactLibraryServiceTests() + { + _testRoot = Path.Combine(Path.GetTempPath(), $"maui-sherpa-profiling-artifacts-{Guid.NewGuid()}"); + _externalRoot = Path.Combine(Path.GetTempPath(), $"maui-sherpa-profiling-external-{Guid.NewGuid()}"); + _service = new ProfilingArtifactLibraryService(_settingsService, _logger.Object, _testRoot); + } + + [Fact] + public async Task SaveArtifactAsync_PersistsArtifactMetadataAndResolvesExternalPath() + { + var externalFile = CreateExternalFile("captures/session-1-trace.speedscope.json", "trace"); + var metadata = CreateMetadata("session-1-trace", "session-1", ProfilingArtifactKind.Trace, "session-1-trace.speedscope.json"); + + var saved = await _service.SaveArtifactAsync(new ProfilingArtifactLibrarySaveRequest( + Metadata: metadata, + ArtifactPath: externalFile)); + + saved.IsManagedPath.Should().BeFalse(); + saved.Metadata.RelativePath.Should().Be(Path.GetFullPath(externalFile)); + saved.Metadata.SizeBytes.Should().Be(new FileInfo(externalFile).Length); + saved.SourcePath.Should().BeNull(); + + var artifacts = await _service.GetArtifactsAsync(); + artifacts.Should().ContainSingle() + .Which.Metadata.Id.Should().Be(metadata.Id); + + var resolvedPath = await _service.GetArtifactPathAsync(metadata.Id); + resolvedPath.Should().Be(Path.GetFullPath(externalFile)); + _settingsService.Current.ProfilingArtifacts.Should().ContainSingle(); + } + + [Fact] + public async Task SaveArtifactAsync_CopyToLibrary_CreatesManagedCopy() + { + var externalFile = CreateExternalFile("imports/session-2-logs.txt", "hello logs"); + var metadata = CreateMetadata("session-2-logs", "session-2", ProfilingArtifactKind.Logs, "session-2-logs.txt"); + + var saved = await _service.SaveArtifactAsync(new ProfilingArtifactLibrarySaveRequest( + Metadata: metadata, + ArtifactPath: externalFile, + CopyToLibrary: true)); + + saved.IsManagedPath.Should().BeTrue(); + saved.SourcePath.Should().Be(Path.GetFullPath(externalFile)); + Path.IsPathRooted(saved.Metadata.RelativePath).Should().BeFalse(); + + var managedPath = await _service.GetArtifactPathAsync(metadata.Id); + managedPath.Should().NotBeNull(); + File.Exists(managedPath!).Should().BeTrue(); + File.ReadAllText(managedPath!).Should().Be("hello logs"); + } + + [Fact] + public async Task SaveArtifactAsync_UpdatesExistingArtifactWithoutDuplicatingEntry() + { + var externalFile = CreateExternalFile("captures/session-3-report.json", "{}"); + var original = await _service.SaveArtifactAsync(new ProfilingArtifactLibrarySaveRequest( + Metadata: CreateMetadata("session-3-report", "session-3", ProfilingArtifactKind.Report, "session-3-report.json"), + ArtifactPath: externalFile)); + + var updatedMetadata = original.Metadata with + { + DisplayName = "Updated report", + Properties = new Dictionary { ["scenario"] = "launch" } + }; + + var updated = await _service.SaveArtifactAsync(new ProfilingArtifactLibrarySaveRequest( + Metadata: updatedMetadata, + ArtifactPath: externalFile)); + + updated.AddedAt.Should().Be(original.AddedAt); + updated.UpdatedAt.Should().BeOnOrAfter(original.UpdatedAt); + updated.Metadata.DisplayName.Should().Be("Updated report"); + updated.Metadata.Properties.Should().ContainKey("scenario").WhoseValue.Should().Be("launch"); + + var artifacts = await _service.GetArtifactsAsync(); + artifacts.Should().ContainSingle(); + } + + [Fact] + public async Task DeleteArtifactAsync_RemovesMetadataAndManagedFile() + { + var externalFile = CreateExternalFile("imports/session-4-memory.gcdump", "gcdump"); + var saved = await _service.SaveArtifactAsync(new ProfilingArtifactLibrarySaveRequest( + Metadata: CreateMetadata("session-4-memory", "session-4", ProfilingArtifactKind.Metrics, "session-4-memory.gcdump"), + ArtifactPath: externalFile, + CopyToLibrary: true)); + var managedPath = await _service.GetArtifactPathAsync(saved.Metadata.Id); + + await _service.DeleteArtifactAsync(saved.Metadata.Id, deleteFile: true); + + (await _service.GetArtifactsAsync()).Should().BeEmpty(); + File.Exists(managedPath!).Should().BeFalse(); + } + + [Fact] + public async Task GetArtifactsAsync_FiltersBySessionKindAndExistingFiles() + { + var existingTrace = CreateExternalFile("captures/session-5-trace.speedscope.json", "trace"); + var missingLogs = Path.Combine(_externalRoot, "missing", "session-6-logs.txt"); + + await _service.SaveArtifactAsync(new ProfilingArtifactLibrarySaveRequest( + Metadata: CreateMetadata("session-5-trace", "session-5", ProfilingArtifactKind.Trace, "session-5-trace.speedscope.json"), + ArtifactPath: existingTrace)); + + await _service.SaveArtifactAsync(new ProfilingArtifactLibrarySaveRequest( + Metadata: CreateMetadata("session-6-logs", "session-6", ProfilingArtifactKind.Logs, "session-6-logs.txt"), + ArtifactPath: missingLogs)); + + var bySession = await _service.GetArtifactsAsync(new ProfilingArtifactLibraryQuery(SessionId: "session-5")); + bySession.Should().ContainSingle().Which.Metadata.Kind.Should().Be(ProfilingArtifactKind.Trace); + + var existingOnly = await _service.GetArtifactsAsync(new ProfilingArtifactLibraryQuery(Kind: ProfilingArtifactKind.Logs, IncludeMissing: false)); + existingOnly.Should().BeEmpty(); + } + + [Fact] + public void GetDefaultArtifactDirectory_ReturnsSessionScopedLibraryPath() + { + var path = _service.GetDefaultArtifactDirectory("session-7"); + + path.Should().Be(Path.Combine(_testRoot, "session-7")); + } + + public void Dispose() + { + if (Directory.Exists(_testRoot)) + { + Directory.Delete(_testRoot, recursive: true); + } + + if (Directory.Exists(_externalRoot)) + { + Directory.Delete(_externalRoot, recursive: true); + } + } + + private string CreateFile(string relativePath, string contents) + => CreateFile(_testRoot, relativePath, contents); + + private string CreateExternalFile(string relativePath, string contents) + => CreateFile(_externalRoot, relativePath, contents); + + private static string CreateFile(string root, string relativePath, string contents) + { + var fullPath = Path.Combine(root, relativePath); + var directory = Path.GetDirectoryName(fullPath); + if (!string.IsNullOrWhiteSpace(directory)) + { + Directory.CreateDirectory(directory); + } + + File.WriteAllText(fullPath, contents); + return fullPath; + } + + private static ProfilingArtifactMetadata CreateMetadata( + string id, + string sessionId, + ProfilingArtifactKind kind, + string fileName) => + new( + Id: id, + SessionId: sessionId, + Kind: kind, + DisplayName: fileName, + FileName: fileName, + RelativePath: null, + ContentType: "application/octet-stream", + CreatedAt: DateTimeOffset.UtcNow); + + private sealed class InMemoryEncryptedSettingsService : IEncryptedSettingsService + { + public MauiSherpaSettings Current { get; private set; } = new(); + + public event Action? OnSettingsChanged; + + public Task GetSettingsAsync() => Task.FromResult(Current); + + public Task SaveSettingsAsync(MauiSherpaSettings settings) + { + Current = settings; + OnSettingsChanged?.Invoke(); + return Task.CompletedTask; + } + + public Task UpdateSettingsAsync(Func transform) => + SaveSettingsAsync(transform(Current)); + + public Task SettingsExistAsync() => Task.FromResult(true); + } +} diff --git a/tests/MauiSherpa.Core.Tests/Services/ProfilingCaptureOrchestrationServiceTests.cs b/tests/MauiSherpa.Core.Tests/Services/ProfilingCaptureOrchestrationServiceTests.cs new file mode 100644 index 00000000..00387c71 --- /dev/null +++ b/tests/MauiSherpa.Core.Tests/Services/ProfilingCaptureOrchestrationServiceTests.cs @@ -0,0 +1,344 @@ +using FluentAssertions; +using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Models.Profiling; +using MauiSherpa.Core.Services; +using Moq; + +namespace MauiSherpa.Core.Tests.Services; + +public class ProfilingCaptureOrchestrationServiceTests +{ + private readonly ProfilingCatalogService _catalogService = new([]); + private readonly Mock _prerequisitesService = new(); + private readonly Mock _deviceMonitorService = new(); + private readonly Mock _platformService = new(); + private readonly Mock _androidSdkSettingsService = new(); + private readonly Mock _loggingService = new(); + + public ProfilingCaptureOrchestrationServiceTests() + { + _platformService.SetupGet(x => x.PlatformName).Returns("macOS"); + _platformService.SetupGet(x => x.IsMacOS).Returns(true); + _platformService.SetupGet(x => x.IsMacCatalyst).Returns(false); + _platformService.SetupGet(x => x.IsWindows).Returns(false); + _platformService.SetupGet(x => x.IsLinux).Returns(false); + _deviceMonitorService.SetupGet(x => x.Current).Returns(ConnectedDevicesSnapshot.Empty); + _androidSdkSettingsService.Setup(x => x.GetEffectiveSdkPathAsync()) + .ReturnsAsync("/Users/test/Library/Android/sdk"); + _prerequisitesService.Setup(x => x.GetPrerequisitesAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync((ProfilingTargetPlatform platform, IReadOnlyList? captureKinds, string? _, CancellationToken _) => + CreateReadyPrerequisites(platform, captureKinds ?? [])); + } + + [Fact] + public async Task PlanCaptureAsync_AndroidEmulatorLaunch_UsesDsRouterServerServerAndDiagnosticBuildProperties() + { + var snapshot = ConnectedDevicesSnapshot.Empty with + { + AndroidEmulators = [new DeviceInfo("emulator-5554", "device", "Pixel 8", true)] + }; + _deviceMonitorService.SetupGet(x => x.Current).Returns(snapshot); + + var service = CreateService(); + var session = _catalogService.CreateSessionDefinition( + new ProfilingTarget( + ProfilingTargetPlatform.Android, + ProfilingTargetKind.Emulator, + "emulator-5554", + "Pixel 8"), + ProfilingScenarioKind.Launch, + appId: "com.example.app"); + + var plan = await service.PlanCaptureAsync(session, new ProfilingCapturePlanOptions( + ProjectPath: "/Users/test/src/HelloMaui/HelloMaui.csproj")); + + plan.Validation.IsValid.Should().BeTrue(); + plan.CanExecute.Should().BeTrue(); + plan.Diagnostics.Should().NotBeNull(); + plan.Diagnostics!.DsRouterMode.Should().Be(ProfilingDsRouterMode.ServerServer); + plan.Diagnostics.Address.Should().Be("10.0.2.2"); + plan.IsTargetCurrentlyAvailable.Should().BeTrue(); + + // SuspendAtStartup defaults to false, so trace goes post-launch (after build). + // Android also gets adb setup-diagnostic-port step before build-and-run. + // Trace is on-demand (not in pipeline commands), but artifact is still expected. + plan.Commands.Select(command => command.Id).Should().ContainInOrder( + "start-dsrouter", + "setup-diagnostic-port", + "build-and-run"); + plan.Commands.Should().Contain(command => command.Id == "start-dsrouter"); + plan.Commands.Should().NotContain(command => command.Id == "capture-trace"); + + // Both trace and GC dump are on-demand, not in the pipeline commands, but still expected artifacts + plan.ExpectedArtifacts.Should().Contain(a => a.DisplayName == "GC dump"); + plan.ExpectedArtifacts.Should().Contain(a => a.Kind == ProfilingArtifactKind.Trace); + + // The adb setprop step configures the Mono diagnostic port + var setupStep = plan.Commands.Single(command => command.Id == "setup-diagnostic-port"); + setupStep.CommandLine.Should().Contain("debug.mono.profile"); + setupStep.CommandLine.Should().Contain("10.0.2.2:9000"); + + var buildStep = plan.Commands.Single(command => command.Id == "build-and-run"); + buildStep.CommandLine.Should().Contain("-p:AndroidEnableProfiler=true"); + buildStep.CommandLine.Should().NotContain("-p:DiagnosticAddress"); + buildStep.CommandLine.Should().Contain("-f net10.0-android"); + buildStep.CanRunParallel.Should().BeTrue(); + buildStep.StopTrigger.Should().Be(ProfilingStopTrigger.OnPipelineStop); + buildStep.Environment.Should().ContainKey("ANDROID_SERIAL"); + } + + [Fact] + public async Task PlanCaptureAsync_IosPhysicalDeviceLaunch_UsesDsRouterServerClientAndListenMode() + { + var snapshot = ConnectedDevicesSnapshot.Empty with + { + ApplePhysicalDevices = + [ + new AppleDeviceInfo("00008110-000E64C20A91801E", "Test iPhone", "iPhone 15", "iOS", "arm64", "17.5", false, true, "usb", null) + ] + }; + _deviceMonitorService.SetupGet(x => x.Current).Returns(snapshot); + + var service = CreateService(); + var session = _catalogService.CreateSessionDefinition( + new ProfilingTarget( + ProfilingTargetPlatform.iOS, + ProfilingTargetKind.PhysicalDevice, + "00008110-000E64C20A91801E", + "Test iPhone"), + ProfilingScenarioKind.Launch, + appId: "com.example.iosapp"); + + var plan = await service.PlanCaptureAsync(session, new ProfilingCapturePlanOptions( + ProjectPath: "/Users/test/src/HelloMaui/HelloMaui.csproj")); + + plan.Validation.IsValid.Should().BeTrue(); + plan.CanExecute.Should().BeTrue(); + plan.Diagnostics.Should().NotBeNull(); + plan.Diagnostics!.DsRouterMode.Should().Be(ProfilingDsRouterMode.ServerClient); + plan.Diagnostics.ListenMode.Should().Be(ProfilingDiagnosticListenMode.Listen); + + // Trace is on-demand (not in pipeline commands), but artifact is still expected. + plan.Commands.Select(command => command.Id).Should().ContainInOrder( + "start-dsrouter", + "build-and-run"); + plan.Commands.Should().Contain(command => command.Id == "start-dsrouter"); + plan.Commands.Should().NotContain(command => command.Id == "capture-trace"); + + // Both trace and GC dump are on-demand, not in the pipeline commands, but still expected artifacts + plan.ExpectedArtifacts.Should().Contain(a => a.DisplayName == "GC dump"); + plan.ExpectedArtifacts.Should().Contain(a => a.Kind == ProfilingArtifactKind.Trace); + + var buildStep = plan.Commands.Single(command => command.Id == "build-and-run"); + buildStep.CommandLine.Should().Contain("-f net10.0-ios"); + buildStep.CommandLine.Should().NotContain("-p:DiagnosticListenMode"); + buildStep.CommandLine.Should().NotContain("-p:DiagnosticAddress"); + } + + [Fact] + public async Task PlanCaptureAsync_AndroidEmulatorTraceOnly_UsesDsRouter() + { + var snapshot = ConnectedDevicesSnapshot.Empty with + { + AndroidEmulators = [new DeviceInfo("emulator-5554", "device", "Pixel 8", true)] + }; + _deviceMonitorService.SetupGet(x => x.Current).Returns(snapshot); + + var service = CreateService(); + // Only CPU trace, no memory — still uses standalone dsrouter for on-demand trace + var session = _catalogService.CreateSessionDefinition( + new ProfilingTarget( + ProfilingTargetPlatform.Android, + ProfilingTargetKind.Emulator, + "emulator-5554", + "Pixel 8"), + ProfilingScenarioKind.Launch, + captureKinds: [ProfilingCaptureKind.Cpu], + appId: "com.example.app"); + + var plan = await service.PlanCaptureAsync(session, new ProfilingCapturePlanOptions( + ProjectPath: "/Users/test/src/HelloMaui/HelloMaui.csproj")); + + plan.Validation.IsValid.Should().BeTrue(); + // Even trace-only uses standalone dsrouter since trace is now on-demand + plan.Commands.Should().Contain(command => command.Id == "start-dsrouter"); + // Trace is on-demand, not in pipeline commands + plan.Commands.Should().NotContain(command => command.Id == "capture-trace"); + // But trace artifact is expected + plan.ExpectedArtifacts.Should().Contain(a => a.Kind == ProfilingArtifactKind.Trace); + } + + [Fact] + public async Task PlanCaptureAsync_MacCatalystLaunch_AddsRuntimeBindingForProcessAttach() + { + var service = CreateService(); + var session = new ProfilingSessionDefinition( + "session-1", + "Mac Catalyst Trace", + new ProfilingTarget( + ProfilingTargetPlatform.MacCatalyst, + ProfilingTargetKind.Desktop, + "local-desktop", + "MAUI Sherpa"), + ProfilingScenarioKind.Interaction, + [ProfilingCaptureKind.Cpu], + AppId: "codes.redth.mauisherpa", + Duration: TimeSpan.FromMinutes(5), + CreatedAt: DateTimeOffset.UtcNow); + + var plan = await service.PlanCaptureAsync(session, new ProfilingCapturePlanOptions( + ProjectPath: "/Users/test/src/MauiSherpa/MauiSherpa.csproj")); + + plan.Validation.IsValid.Should().BeTrue(); + plan.Diagnostics.Should().BeNull(); + plan.RequiresRuntimeInputs.Should().BeTrue(); + plan.CanExecute.Should().BeFalse(); + plan.RuntimeBindings.Should().ContainSingle(binding => binding.Token == "{{PROCESS_ID}}"); + plan.Commands.Select(command => command.Id).Should().ContainInOrder( + "build-and-run", + "discover-process-id"); + plan.Commands.Should().NotContain(command => command.Id == "capture-trace"); + plan.ExpectedArtifacts.Should().Contain(a => a.Kind == ProfilingArtifactKind.Trace); + } + + [Fact] + public async Task PlanCaptureAsync_DefaultOutputDirectory_UsesProjectNameAndDate() + { + var snapshot = ConnectedDevicesSnapshot.Empty with + { + AndroidEmulators = [new DeviceInfo("emulator-5554", "device", "Pixel 8", true)] + }; + _deviceMonitorService.SetupGet(x => x.Current).Returns(snapshot); + + var service = CreateService(); + var createdAt = new DateTimeOffset(2026, 3, 9, 14, 0, 0, TimeSpan.Zero); + var session = _catalogService.CreateSessionDefinition( + new ProfilingTarget( + ProfilingTargetPlatform.Android, + ProfilingTargetKind.Emulator, + "emulator-5554", + "Pixel 8"), + ProfilingScenarioKind.Launch, + appId: "com.example.app"); + session = session with { CreatedAt = createdAt }; + + var plan = await service.PlanCaptureAsync(session, new ProfilingCapturePlanOptions( + ProjectPath: "/Users/test/src/HelloMaui/HelloMaui.csproj")); + + plan.Validation.IsValid.Should().BeTrue(); + var dateStr = createdAt.LocalDateTime.ToString("yyyy-MM-dd"); + var expectedDir = Path.GetFullPath( + Path.Combine("/Users/test/src/HelloMaui", "artifacts", "profiling", "HelloMaui", $"{dateStr}-1")); + plan.Options.OutputDirectory.Should().Be(expectedDir); + } + + [Fact] + public async Task PlanCaptureAsync_ArtifactFileNames_UseSimpleNames() + { + var snapshot = ConnectedDevicesSnapshot.Empty with + { + AndroidEmulators = [new DeviceInfo("emulator-5554", "device", "Pixel 8", true)] + }; + _deviceMonitorService.SetupGet(x => x.Current).Returns(snapshot); + + var service = CreateService(); + var session = _catalogService.CreateSessionDefinition( + new ProfilingTarget( + ProfilingTargetPlatform.Android, + ProfilingTargetKind.Emulator, + "emulator-5554", + "Pixel 8"), + ProfilingScenarioKind.Launch, + appId: "com.example.app"); + + var plan = await service.PlanCaptureAsync(session, new ProfilingCapturePlanOptions( + ProjectPath: "/Users/test/src/HelloMaui/HelloMaui.csproj")); + + plan.Validation.IsValid.Should().BeTrue(); + plan.ExpectedArtifacts.Should().Contain(a => a.FileName == "trace.nettrace"); + plan.ExpectedArtifacts.Should().Contain(a => a.FileName == "memory.gcdump"); + } + + [Fact] + public async Task PlanCaptureAsync_NoProjectPath_FallsBackToSessionInOutputDir() + { + var service = CreateService(); + var session = _catalogService.CreateSessionDefinition( + new ProfilingTarget( + ProfilingTargetPlatform.MacCatalyst, + ProfilingTargetKind.Desktop, + "local-desktop", + "MAUI Sherpa"), + ProfilingScenarioKind.Interaction, + appId: "codes.redth.mauisherpa"); + + var plan = await service.PlanCaptureAsync(session, new ProfilingCapturePlanOptions()); + + // Missing project path in launch mode causes validation error, but for attach scenarios + // the output dir still uses "session" fallback when no project path is given + plan.Options.OutputDirectory.Should().Contain(Path.Combine("artifacts", "profiling", "session")); + } + + [Fact] + public async Task PlanCaptureAsync_LaunchWithoutProjectPath_ReturnsValidationError() + { + var service = CreateService(); + var session = _catalogService.CreateSessionDefinition( + new ProfilingTarget( + ProfilingTargetPlatform.Android, + ProfilingTargetKind.PhysicalDevice, + "device-01", + "Pixel 9"), + ProfilingScenarioKind.Launch); + + var plan = await service.PlanCaptureAsync(session, new ProfilingCapturePlanOptions()); + + plan.Validation.IsValid.Should().BeFalse(); + plan.Validation.Errors.Should().Contain(error => + error.Contains("project path", StringComparison.OrdinalIgnoreCase)); + } + + private ProfilingCaptureOrchestrationService CreateService() => + new( + _catalogService, + _prerequisitesService.Object, + _deviceMonitorService.Object, + _platformService.Object, + _androidSdkSettingsService.Object, + _loggingService.Object); + + private static ProfilingPrerequisiteReport CreateReadyPrerequisites( + ProfilingTargetPlatform platform, + IReadOnlyList captureKinds) + { + return new ProfilingPrerequisiteReport( + new ProfilingPrerequisiteContext( + platform, + captureKinds, + "/Users/test/code/MAUI.Sherpa", + "/usr/local/share/dotnet/dotnet", + new DoctorContext( + "/Users/test/code/MAUI.Sherpa", + "/usr/local/share/dotnet", + "/Users/test/code/MAUI.Sherpa/global.json", + "10.0.100", + null, + "10.0.100", + ActiveSdkVersion: "10.0.103", + ResolvedSdkVersion: "10.0.103")), + [new ProfilingPrerequisiteStatus( + "Host Platform", + ProfilingPrerequisiteKind.HostPlatform, + DependencyStatusType.Ok, + IsRequired: true, + RequiredVersion: null, + RecommendedVersion: null, + InstalledVersion: "macOS", + Message: "Host ready")], + DateTimeOffset.UtcNow); + } +} diff --git a/tests/MauiSherpa.Core.Tests/Services/ProfilingCatalogServiceTests.cs b/tests/MauiSherpa.Core.Tests/Services/ProfilingCatalogServiceTests.cs new file mode 100644 index 00000000..37a15cc6 --- /dev/null +++ b/tests/MauiSherpa.Core.Tests/Services/ProfilingCatalogServiceTests.cs @@ -0,0 +1,99 @@ +using FluentAssertions; +using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Models.Profiling; +using MauiSherpa.Core.Services; +using Moq; + +namespace MauiSherpa.Core.Tests.Services; + +public class ProfilingCatalogServiceTests +{ + [Fact] + public async Task GetCatalogAsync_ReturnsBuiltInPlatformsAndScenarios() + { + var service = new ProfilingCatalogService([]); + + var result = await service.GetCatalogAsync(); + + result.Platforms.Should().HaveCount(5); + result.Scenarios.Should().Contain(x => x.Kind == ProfilingScenarioKind.Launch); + result.Platforms.Should().Contain(x => + x.Platform == ProfilingTargetPlatform.Android && + x.SupportedTargetKinds.Contains(ProfilingTargetKind.Emulator)); + } + + [Fact] + public async Task GetCapabilitiesAsync_UsesRegisteredProviderOverride() + { + var customCapabilities = new ProfilingPlatformCapabilities( + ProfilingTargetPlatform.Android, + "Android (custom)", + [ProfilingTargetKind.PhysicalDevice], + [ProfilingCaptureKind.Cpu], + [ProfilingArtifactKind.Trace], + [ProfilingScenarioKind.Launch], + SupportsLaunchProfiling: true, + SupportsAttachToProcess: false, + SupportsLiveMetrics: false, + SupportsSymbolication: false, + Notes: "Custom override"); + + var provider = new Mock(); + provider.SetupGet(x => x.Platform).Returns(ProfilingTargetPlatform.Android); + provider.Setup(x => x.GetCapabilitiesAsync(It.IsAny())) + .ReturnsAsync(customCapabilities); + + var service = new ProfilingCatalogService([provider.Object]); + + var result = await service.GetCapabilitiesAsync(ProfilingTargetPlatform.Android); + + result.Should().Be(customCapabilities); + } + + [Fact] + public void CreateSessionDefinition_UsesScenarioDefaultsWhenCaptureKindsNotProvided() + { + var service = new ProfilingCatalogService([]); + var target = new ProfilingTarget( + ProfilingTargetPlatform.Android, + ProfilingTargetKind.Emulator, + "emulator-5554", + "Pixel 8"); + + var result = service.CreateSessionDefinition(target, ProfilingScenarioKind.Launch); + + result.Name.Should().Be("Pixel 8 - Launch & startup"); + result.CaptureKinds.Should().BeEquivalentTo( + [ProfilingCaptureKind.Startup, ProfilingCaptureKind.Cpu, ProfilingCaptureKind.Memory]); + result.Duration.Should().Be(TimeSpan.FromMinutes(2)); + result.Tags.Should().NotBeNull().And.BeEmpty(); + } + + [Fact] + public async Task ValidateSessionDefinition_ReturnsErrorsForUnsupportedValues() + { + var service = new ProfilingCatalogService([]); + var capabilities = await service.GetCapabilitiesAsync(ProfilingTargetPlatform.Windows); + var definition = new ProfilingSessionDefinition( + "session-1", + "", + new ProfilingTarget( + ProfilingTargetPlatform.Android, + ProfilingTargetKind.Emulator, + "", + "Android emulator"), + ProfilingScenarioKind.Launch, + [(ProfilingCaptureKind)999], + Duration: TimeSpan.Zero, + CreatedAt: DateTimeOffset.UtcNow); + + var result = service.ValidateSessionDefinition(definition, capabilities); + + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(x => x.Contains("session name", StringComparison.OrdinalIgnoreCase)); + result.Errors.Should().Contain(x => x.Contains("identifier", StringComparison.OrdinalIgnoreCase)); + result.Errors.Should().Contain(x => x.Contains("does not match", StringComparison.OrdinalIgnoreCase)); + result.Errors.Should().Contain(x => x.Contains("not supported", StringComparison.OrdinalIgnoreCase)); + result.UnsupportedCaptureKinds.Should().Contain((ProfilingCaptureKind)999); + } +} diff --git a/tests/MauiSherpa.Core.Tests/Services/ProfilingContextServiceTests.cs b/tests/MauiSherpa.Core.Tests/Services/ProfilingContextServiceTests.cs new file mode 100644 index 00000000..242f549d --- /dev/null +++ b/tests/MauiSherpa.Core.Tests/Services/ProfilingContextServiceTests.cs @@ -0,0 +1,125 @@ +using FluentAssertions; +using MauiSherpa.Core.Models.DevFlow; +using MauiSherpa.Core.Services; + +namespace MauiSherpa.Core.Tests.Services; + +public class ProfilingContextServiceTests +{ + [Fact] + public void BuildNetworkSummary_UsesMostRecentRequestsAndCalculatesMetrics() + { + var now = DateTimeOffset.UtcNow; + var requests = new[] + { + new DevFlowNetworkRequest + { + Timestamp = now.AddMinutes(-10), + Method = "GET", + Url = "https://example.com/old", + DurationMs = 15, + StatusCode = 200, + RequestSize = 1, + ResponseSize = 10 + }, + new DevFlowNetworkRequest + { + Timestamp = now.AddMinutes(-1), + Method = "GET", + Url = "https://example.com/fast", + DurationMs = 120, + StatusCode = 200, + RequestSize = 20, + ResponseSize = 100 + }, + new DevFlowNetworkRequest + { + Timestamp = now.AddMinutes(-2), + Method = "POST", + Url = "https://example.com/slow", + DurationMs = 450, + StatusCode = 500, + RequestSize = 30, + ResponseSize = 0 + }, + new DevFlowNetworkRequest + { + Timestamp = now.AddMinutes(-3), + Method = "GET", + Url = "https://example.com/error", + DurationMs = 240, + Error = "timeout", + RequestSize = 40, + ResponseSize = 5 + } + }; + + var summary = ProfilingContextService.BuildNetworkSummary(requests, 3); + + summary.SampleSize.Should().Be(3); + summary.SuccessCount.Should().Be(1); + summary.FailureCount.Should().Be(2); + summary.AverageDurationMs.Should().BeApproximately(270, 0.001); + summary.P95DurationMs.Should().BeGreaterThan(400); + summary.MaxDurationMs.Should().Be(450); + summary.TotalRequestBytes.Should().Be(90); + summary.TotalResponseBytes.Should().Be(105); + summary.SlowestRequests.Select(r => r.Url).Should().ContainInOrder( + "https://example.com/slow", + "https://example.com/error", + "https://example.com/fast"); + } + + [Fact] + public void BuildVisualTreeSummary_FlattensTreeAndCalculatesCounts() + { + var roots = new[] + { + new DevFlowElementInfo + { + Id = "root", + Type = "Grid", + IsVisible = true, + Children = new List + { + new() + { + Id = "button", + Type = "Button", + IsVisible = true, + Gestures = new List { "Tap" } + }, + new() + { + Id = "stack", + Type = "VerticalStackLayout", + IsVisible = true, + Children = new List + { + new() + { + Id = "entry", + Type = "Entry", + IsVisible = false, + IsFocused = true, + Gestures = new List { "Focus" } + } + } + } + } + } + }; + + var summary = ProfilingContextService.BuildVisualTreeSummary(roots); + + summary.RootCount.Should().Be(1); + summary.TotalElementCount.Should().Be(4); + summary.VisibleElementCount.Should().Be(3); + summary.FocusedElementCount.Should().Be(1); + summary.InteractiveElementCount.Should().Be(2); + summary.MaxDepth.Should().Be(3); + summary.TopElementTypes.Should().ContainEquivalentOf(new { Type = "Grid", Count = 1 }); + summary.TopElementTypes.Should().ContainEquivalentOf(new { Type = "Button", Count = 1 }); + summary.TopElementTypes.Should().ContainEquivalentOf(new { Type = "Entry", Count = 1 }); + } +} diff --git a/tests/MauiSherpa.Core.Tests/Services/ProfilingPrerequisitesServiceTests.cs b/tests/MauiSherpa.Core.Tests/Services/ProfilingPrerequisitesServiceTests.cs new file mode 100644 index 00000000..c5f475c6 --- /dev/null +++ b/tests/MauiSherpa.Core.Tests/Services/ProfilingPrerequisitesServiceTests.cs @@ -0,0 +1,188 @@ +using FluentAssertions; +using MauiSherpa.Core.Interfaces; +using MauiSherpa.Core.Models.Profiling; +using MauiSherpa.Core.Services; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; + +namespace MauiSherpa.Core.Tests.Services; + +public class ProfilingPrerequisitesServiceTests +{ + private readonly Mock _doctorService = new(); + private readonly Mock _platformService = new(); + private readonly Mock _loggingService = new(); + + public ProfilingPrerequisitesServiceTests() + { + _platformService.SetupGet(x => x.PlatformName).Returns("macOS"); + _platformService.SetupGet(x => x.IsMacOS).Returns(true); + _platformService.SetupGet(x => x.IsMacCatalyst).Returns(false); + _platformService.SetupGet(x => x.IsWindows).Returns(false); + _platformService.SetupGet(x => x.IsLinux).Returns(false); + } + + [Fact] + public async Task GetPrerequisitesAsync_ForAndroidMemoryCapture_ReturnsReadyWhenRequiredToolsExist() + { + var context = CreateDoctorContext(activeSdkVersion: "10.0.103"); + SetupDoctor(context, CreateDoctorReport( + context, + new DependencyStatus(".NET SDK", DependencyCategory.DotNetSdk, null, "10.0.103", "10.0.103", DependencyStatusType.Ok, "SDK ready", false), + new DependencyStatus("Android SDK", DependencyCategory.AndroidSdk, null, null, "/Users/test/Library/Android/sdk", DependencyStatusType.Ok, "Found SDK", false), + new DependencyStatus("Platform Tools", DependencyCategory.AndroidSdk, null, null, "Installed", DependencyStatusType.Ok, "adb available", false))); + + var service = CreateService((request, _) => Task.FromResult(CreateToolListResult(request, """ + Package Id Version Commands + ---------------------------------------------------------- + dotnet-trace 10.0.41001 dotnet-trace + dotnet-gcdump 10.0.41001 dotnet-gcdump + dotnet-dsrouter 10.0.41001 dotnet-dsrouter + """))); + + var report = await service.GetPrerequisitesAsync( + ProfilingTargetPlatform.Android, + [ProfilingCaptureKind.Cpu, ProfilingCaptureKind.Memory]); + + report.IsReady.Should().BeTrue(); + report.Checks.Should().ContainSingle(x => x.Name == "dotnet-trace" && x.Status == DependencyStatusType.Ok); + report.Checks.Should().ContainSingle(x => x.Name == "dotnet-gcdump" && x.IsRequired && x.Status == DependencyStatusType.Ok); + report.Checks.Should().ContainSingle(x => x.Name == "dotnet-dsrouter" && x.IsRequired && x.Status == DependencyStatusType.Ok); + report.Checks.Should().ContainSingle(x => x.Name == "Platform Tools" && x.Status == DependencyStatusType.Ok); + } + + [Fact] + public async Task GetPrerequisitesAsync_UpgradesRequiredAndroidPlatformToolsWarningToError() + { + var context = CreateDoctorContext(activeSdkVersion: "10.0.103"); + SetupDoctor(context, CreateDoctorReport( + context, + new DependencyStatus(".NET SDK", DependencyCategory.DotNetSdk, null, "10.0.103", "10.0.103", DependencyStatusType.Ok, "SDK ready", false), + new DependencyStatus("Android SDK", DependencyCategory.AndroidSdk, null, null, "/Users/test/Library/Android/sdk", DependencyStatusType.Ok, "Found SDK", false), + new DependencyStatus("Platform Tools", DependencyCategory.AndroidSdk, null, null, null, DependencyStatusType.Warning, "Platform tools not installed", true, "install-android-package:platform-tools"))); + + var service = CreateService((request, _) => Task.FromResult(CreateToolListResult(request, """ + Package Id Version Commands + ---------------------------------------------------------- + dotnet-trace 10.0.41001 dotnet-trace + dotnet-dsrouter 10.0.41001 dotnet-dsrouter + """))); + + var report = await service.GetPrerequisitesAsync( + ProfilingTargetPlatform.Android, + [ProfilingCaptureKind.Cpu]); + + report.IsReady.Should().BeFalse(); + report.Checks.Should().ContainSingle(x => + x.Name == "Platform Tools" && + x.Status == DependencyStatusType.Error && + x.Message!.Contains("required for profiling readiness", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public async Task GetPrerequisitesAsync_RequiresGcDumpForMemoryCapture() + { + var context = CreateDoctorContext(activeSdkVersion: "10.0.103"); + SetupDoctor(context, CreateDoctorReport( + context, + new DependencyStatus(".NET SDK", DependencyCategory.DotNetSdk, null, "10.0.103", "10.0.103", DependencyStatusType.Ok, "SDK ready", false))); + + var service = CreateService((request, _) => Task.FromResult(CreateToolListResult(request, """ + Package Id Version Commands + -------------------------------------------------------- + dotnet-trace 10.0.41001 dotnet-trace + """))); + + var report = await service.GetPrerequisitesAsync( + ProfilingTargetPlatform.MacOS, + [ProfilingCaptureKind.Memory]); + + report.IsReady.Should().BeFalse(); + report.Checks.Should().ContainSingle(x => + x.Name == "dotnet-gcdump" && + x.Status == DependencyStatusType.Error && + x.IsRequired); + } + + [Fact] + public async Task GetPrerequisitesAsync_DoesNotWarnWhenToolMajorDoesNotMatchActiveSdk() + { + var context = CreateDoctorContext(activeSdkVersion: "10.0.103"); + SetupDoctor(context, CreateDoctorReport( + context, + new DependencyStatus(".NET SDK", DependencyCategory.DotNetSdk, null, "10.0.103", "10.0.103", DependencyStatusType.Ok, "SDK ready", false))); + + var service = CreateService((request, _) => Task.FromResult(CreateToolListResult(request, """ + Package Id Version Commands + -------------------------------------------------------- + dotnet-trace 9.0.553801 dotnet-trace + """))); + + var report = await service.GetPrerequisitesAsync( + ProfilingTargetPlatform.MacOS, + [ProfilingCaptureKind.Cpu]); + + report.IsReady.Should().BeTrue(); + report.Checks.Should().ContainSingle(x => + x.Name == "dotnet-trace" && + x.Status == DependencyStatusType.Ok && + x.RecommendedVersion == null && + x.RequiredVersion == null && + x.SuggestedCommand == null); + } + + private ProfilingPrerequisitesService CreateService( + Func> processExecutor) + { + return new ProfilingPrerequisitesService( + _doctorService.Object, + _platformService.Object, + _loggingService.Object, + processExecutor, + NullLoggerFactory.Instance); + } + + private void SetupDoctor(DoctorContext context, DoctorReport report) + { + _doctorService.Setup(x => x.GetContextAsync(It.IsAny())) + .ReturnsAsync(context); + _doctorService.Setup(x => x.RunDoctorAsync(It.IsAny(), It.IsAny?>())) + .ReturnsAsync(report); + _doctorService.Setup(x => x.GetDotNetExecutablePath()) + .Returns("/usr/local/share/dotnet/dotnet"); + } + + private static ProcessResult CreateToolListResult(ProcessRequest request, string output) + { + if (request.Arguments.Length >= 2 && + request.Arguments[0] == "tool" && + request.Arguments[1] == "list") + { + return new ProcessResult(0, output, string.Empty, TimeSpan.Zero, ProcessState.Completed); + } + + return new ProcessResult(1, string.Empty, $"Unexpected command: {request.CommandLine}", TimeSpan.Zero, ProcessState.Failed); + } + + private static DoctorContext CreateDoctorContext(string activeSdkVersion) => new( + WorkingDirectory: "/Users/test/code/MAUI.Sherpa", + DotNetSdkPath: "/usr/local/share/dotnet", + GlobalJsonPath: "/Users/test/code/MAUI.Sherpa/global.json", + PinnedSdkVersion: "10.0.100", + PinnedWorkloadSetVersion: null, + EffectiveFeatureBand: "10.0.100", + IsPreviewSdk: false, + ActiveSdkVersion: activeSdkVersion, + RollForwardPolicy: "latestPatch", + ResolvedSdkVersion: activeSdkVersion); + + private static DoctorReport CreateDoctorReport(DoctorContext context, params DependencyStatus[] dependencies) => new( + context, + [new SdkVersionInfo(context.ActiveSdkVersion ?? "10.0.103", "10.0.100", 10, 0, false)], + AvailableSdkVersions: null, + InstalledWorkloadSetVersion: null, + AvailableWorkloadSetVersions: null, + Manifests: [], + Dependencies: dependencies, + DateTime.UtcNow); +}