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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions src/ProfileTool/Input/HeapProfileInputLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.IO;
using Spectre.Console;

namespace Asynkron.Profiler;

internal sealed class HeapProfileInputLoader
{
private readonly Func<Theme> _getTheme;
private readonly Func<string, string, bool> _ensureToolAvailable;
private readonly Func<string, IEnumerable<string>, string?, int, (bool Success, string StdOut, string StdErr)> _runProcess;
private readonly Func<string, HeapProfileResult> _parseGcdumpReport;
private readonly Action<string> _writeLine;
private readonly string _dotnetGcdumpInstallHint;

public HeapProfileInputLoader(
Func<Theme> getTheme,
Func<string, string, bool> ensureToolAvailable,
Func<string, IEnumerable<string>, string?, int, (bool Success, string StdOut, string StdErr)> runProcess,
Func<string, HeapProfileResult> parseGcdumpReport,
Action<string> writeLine,
string dotnetGcdumpInstallHint)
{
_getTheme = getTheme;
_ensureToolAvailable = ensureToolAvailable;
_runProcess = runProcess;
_parseGcdumpReport = parseGcdumpReport;
_writeLine = writeLine;
_dotnetGcdumpInstallHint = dotnetGcdumpInstallHint;
}

public HeapProfileResult? Load(string inputPath)
{
if (!TryEnsureInputExists(inputPath))
{
return null;
}

return ProfileInputKindResolver.Resolve(inputPath) switch
{
ProfileInputKind.Gcdump => LoadGcdump(inputPath),
ProfileInputKind.HeapReport => _parseGcdumpReport(File.ReadAllText(inputPath)),
_ => WriteUnsupportedInput(inputPath)
};
}

private HeapProfileResult? LoadGcdump(string inputPath)
{
if (!_ensureToolAvailable("dotnet-gcdump", _dotnetGcdumpInstallHint))
{
return null;
}

return GcdumpReportLoader.Load(
inputPath,
_getTheme(),
_runProcess,
_parseGcdumpReport,
_writeLine);
}

private bool TryEnsureInputExists(string inputPath)
{
if (File.Exists(inputPath))
{
return true;
}

_writeLine($"[{_getTheme().ErrorColor}]Input file not found:[/] {Markup.Escape(inputPath)}");
return false;
}

private HeapProfileResult? WriteUnsupportedInput(string inputPath)
{
_writeLine($"[{_getTheme().ErrorColor}]Unsupported heap input:[/] {Markup.Escape(inputPath)}");
return null;
}
}
37 changes: 37 additions & 0 deletions src/ProfileTool/Input/MemoryProfileResultFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Linq;
using static Asynkron.Profiler.CallTreeHelpers;

namespace Asynkron.Profiler;

internal static class MemoryProfileResultFactory
{
public static MemoryProfileResult Create(AllocationCallTreeResult callTree)
{
var allocationEntries = callTree.TypeRoots
.OrderByDescending(root => root.TotalBytes)
.Take(50)
.Select(root => new AllocationEntry(root.Name, root.Count, FormatBytes(root.TotalBytes)))
.ToList();

var totalAllocated = FormatBytes(callTree.TotalBytes);

return new MemoryProfileResult(
null,
null,
null,
totalAllocated,
null,
null,
null,
null,
null,
null,
null,
null,
totalAllocated,
allocationEntries,
callTree,
null,
null);
}
}
37 changes: 37 additions & 0 deletions src/ProfileTool/Input/ProfileInputDefaultsPolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace Asynkron.Profiler;

internal static class ProfileInputDefaultsPolicy
{
public static void Apply(
string inputPath,
ref bool runCpu,
ref bool runMemory,
ref bool runHeap,
ref bool runException,
ref bool runContention)
{
switch (ProfileInputKindResolver.Resolve(inputPath))
{
case ProfileInputKind.Speedscope:
runCpu = true;
break;
case ProfileInputKind.NetTrace:
runCpu = true;
runException = true;
runContention = true;
break;
case ProfileInputKind.Etlx:
runMemory = true;
runException = true;
runContention = true;
break;
case ProfileInputKind.Gcdump:
case ProfileInputKind.HeapReport:
runHeap = true;
break;
default:
runCpu = true;
break;
}
}
}
11 changes: 11 additions & 0 deletions src/ProfileTool/Input/ProfileInputKind.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Asynkron.Profiler;

internal enum ProfileInputKind
{
Unknown,
Speedscope,
NetTrace,
Etlx,
Gcdump,
HeapReport
}
24 changes: 24 additions & 0 deletions src/ProfileTool/Input/ProfileInputKindResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.IO;

namespace Asynkron.Profiler;

internal static class ProfileInputKindResolver
{
public static ProfileInputKind Resolve(string inputPath)
{
return Path.GetExtension(inputPath).ToLowerInvariant() switch
{
".json" => ProfileInputKind.Speedscope,
".nettrace" => ProfileInputKind.NetTrace,
".etlx" => ProfileInputKind.Etlx,
".gcdump" => ProfileInputKind.Gcdump,
".txt" or ".log" => ProfileInputKind.HeapReport,
_ => ProfileInputKind.Unknown
};
}

public static bool IsTrace(ProfileInputKind kind)
{
return kind is ProfileInputKind.NetTrace or ProfileInputKind.Etlx;
}
}
11 changes: 11 additions & 0 deletions src/ProfileTool/Input/ProfileInputLabelFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.IO;

namespace Asynkron.Profiler;

internal static class ProfileInputLabelFactory
{
public static string Build(string inputPath)
{
return FileLabelSanitizer.Sanitize(Path.GetFileNameWithoutExtension(inputPath), "input");
}
}
152 changes: 152 additions & 0 deletions src/ProfileTool/Input/TraceProfileInputLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
using System;
using System.IO;
using Spectre.Console;

namespace Asynkron.Profiler;

internal sealed class TraceProfileInputLoader
{
private readonly ProfilerTraceAnalyzer _traceAnalyzer;
private readonly Func<Theme> _getTheme;
private readonly Action<string> _writeLine;

public TraceProfileInputLoader(
ProfilerTraceAnalyzer traceAnalyzer,
Func<Theme> getTheme,
Action<string> writeLine)
{
_traceAnalyzer = traceAnalyzer;
_getTheme = getTheme;
_writeLine = writeLine;
}

public CpuProfileResult? LoadCpu(string inputPath)
{
if (!TryEnsureInputExists(inputPath))
{
return null;
}

return ProfileInputKindResolver.Resolve(inputPath) switch
{
ProfileInputKind.Speedscope => AnalyzeSpeedscope(inputPath),
var kind when ProfileInputKindResolver.IsTrace(kind) => AnalyzeCpuTrace(inputPath),
_ => WriteUnsupportedInput<CpuProfileResult>("Unsupported CPU input", inputPath)
};
}

public MemoryProfileResult? LoadMemory(string inputPath)
{
var callTree = LoadTraceInput(inputPath, "Unsupported memory input", AnalyzeAllocationTrace);
return callTree == null ? null : MemoryProfileResultFactory.Create(callTree);
}

public ExceptionProfileResult? LoadException(string inputPath)
{
return LoadTraceInput(inputPath, "Unsupported exception input", AnalyzeExceptionTrace);
}

public ContentionProfileResult? LoadContention(string inputPath)
{
return LoadTraceInput(inputPath, "Unsupported contention input", AnalyzeContentionTrace);
}

public CpuProfileResult? AnalyzeCpuTrace(string traceFile)
{
var result = RunAnalysis(() => _traceAnalyzer.AnalyzeCpuTrace(traceFile), "CPU trace parse failed");
if (result == null)
{
return null;
}

if (result.AllFunctions.Count == 0)
{
_writeLine($"[{_getTheme().AccentColor}]No CPU samples found in trace.[/]");
return null;
}

return result;
}

public AllocationCallTreeResult? AnalyzeAllocationTrace(string traceFile)
{
return RunAnalysis(() => _traceAnalyzer.AnalyzeAllocationTrace(traceFile), "Allocation trace parse failed");
}

public ExceptionProfileResult? AnalyzeExceptionTrace(string traceFile)
{
return RunAnalysis(() => _traceAnalyzer.AnalyzeExceptionTrace(traceFile), "Exception trace parse failed");
}

public ContentionProfileResult? AnalyzeContentionTrace(string traceFile)
{
return RunAnalysis(() => _traceAnalyzer.AnalyzeContentionTrace(traceFile), "Contention trace parse failed");
}

private CpuProfileResult? AnalyzeSpeedscope(string speedscopePath)
{
return RunAnalysis(() => ProfilerTraceAnalyzer.AnalyzeSpeedscope(speedscopePath), "Speedscope parse failed");
}

private TResult? LoadTraceInput<TResult>(
string inputPath,
string unsupportedMessage,
Func<string, TResult?> analyzeTrace)
where TResult : class
{
if (!TryValidateTraceInput(inputPath, unsupportedMessage))
{
return null;
}

return analyzeTrace(inputPath);
}

private TResult? RunAnalysis<TResult>(Func<TResult> analyze, string failureLabel)
where TResult : class
{
try
{
return analyze();
}
catch (Exception ex)
{
_writeLine($"[{_getTheme().AccentColor}]{failureLabel}:[/] {Markup.Escape(ex.Message)}");
return null;
}
}

private bool TryValidateTraceInput(string inputPath, string unsupportedMessage)
{
if (!TryEnsureInputExists(inputPath))
{
return false;
}

if (ProfileInputKindResolver.IsTrace(ProfileInputKindResolver.Resolve(inputPath)))
{
return true;
}

WriteUnsupportedInput<object>(unsupportedMessage, inputPath);
return false;
}

private bool TryEnsureInputExists(string inputPath)
{
if (File.Exists(inputPath))
{
return true;
}

_writeLine($"[{_getTheme().ErrorColor}]Input file not found:[/] {Markup.Escape(inputPath)}");
return false;
}

private TResult? WriteUnsupportedInput<TResult>(string message, string inputPath)
where TResult : class
{
_writeLine($"[{_getTheme().ErrorColor}]{message}:[/] {Markup.Escape(inputPath)}");
return null;
}
}
2 changes: 1 addition & 1 deletion src/ProfileTool/ProfileCollectionRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public ProfileCollectionRunner(
},
_profileInputLoader.AnalyzeAllocationTrace);

return callTree == null ? null : ProfileInputLoader.BuildMemoryProfileResult(callTree);
return callTree == null ? null : MemoryProfileResultFactory.Create(callTree);
}

public ExceptionProfileResult? RunExceptionProfile(string[] command, string label)
Expand Down
Loading
Loading