Skip to content
Open
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
1 change: 1 addition & 0 deletions src/GCRealTimeMon/GCRealTimeMon.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<PackageReference Include="Microsoft.Diagnostics.NETCore.Client" Version="0.2.251802" />
<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="2.0.74" />
<PackageReference Include="Sharprompt" Version="2.3.7" />
<PackageReference Include="System.Reactive" Version="5.0.0" />
<PackageReference Include="YamlDotNet" Version="11.2.1" />
</ItemGroup>

Expand Down
54 changes: 22 additions & 32 deletions src/GCRealTimeMon/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using realmon.Configuration;
using realmon.Utilities;
using CommandLine.Text;
using realmon.Service;

namespace realmon
{
Expand Down Expand Up @@ -53,22 +54,18 @@ public class Options
static TraceGC lastGC;
static object writerLock = new object();

public static void RealTimeProcessing(int pid, Options options, Configuration.Configuration configuration)
public static void RealTimeProcessing(int pid, Configuration.Configuration configuration)
{
Console.WriteLine();
Process process = Process.GetProcessById(pid);
double? minDurationForGCPausesInMSec = null;
if (configuration.DisplayConditions != null &&
configuration.DisplayConditions.TryGetValue("min gc duration (msec)", out var minDuration))
{
minDurationForGCPausesInMSec = double.Parse(minDuration);
}

Console.WriteLine($"Monitoring process with name: {process.ProcessName} and pid: {pid}");
Console.WriteLine(PrintUtilities.GetHeader(configuration));
Console.WriteLine(PrintUtilities.GetLineSeparator(configuration));
IGCRealTimeMonResult result = GCRealTimeMonService.Instance.Value.Initialize(pid: pid, configuration: configuration);
IDisposable session = result.Source;

var source = PlatformUtilities.GetTraceEventDispatcherBasedOnPlatform(pid, out var session);
Console.CancelKeyPress += (_, e) =>
{
// Dispose the session.
Expand All @@ -78,36 +75,22 @@ public static void RealTimeProcessing(int pid, Options options, Configuration.Co
};

// this thread is responsible for listening to user input on the console and dispose the session accordingly
Thread monitorThread = new Thread(() => HandleConsoleInput(session)) ;
Thread monitorThread = new Thread(() => HandleConsoleInput(result)) ;
monitorThread.Start();

source.NeedLoadedDotNetRuntimes();
source.AddCallbackOnProcessStart(delegate (TraceProcess proc)
// Life time of the process is the same as that of this disposable => never dispose.
IDisposable subscriptionHandle = result.GCEndObservable.Subscribe(gc =>
{
proc.AddCallbackOnDotNetRuntimeLoad(delegate (TraceLoadedDotNetRuntime runtime)
lastGCTime = DateTime.UtcNow;
lastGC = gc;
lock (writerLock)
{
runtime.GCEnd += delegate (TraceProcess p, TraceGC gc)
{
if (p.ProcessID == pid)
{
// If no min duration is specified or if the min duration specified is less than the pause duration, log the event.
if (!minDurationForGCPausesInMSec.HasValue ||
(minDurationForGCPausesInMSec.HasValue && minDurationForGCPausesInMSec.Value < gc.PauseDurationMSec))
{
lastGCTime = DateTime.UtcNow;
lastGC = gc;

lock (writerLock) {
Console.WriteLine(PrintUtilities.GetRowDetails(gc, configuration));
}
}
}
};
});
Console.WriteLine(PrintUtilities.GetRowDetails(gc, configuration));
}
});

// blocking call on the main thread until the session gets disposed upon user action
source.Process();
result.Source.Process();
}

private static void SetupHeapStatsTimerIfEnabled(Configuration.Configuration configuration)
Expand Down Expand Up @@ -176,7 +159,7 @@ private static void PrintLastStats()
}
}

static void HandleConsoleInput(IDisposable session)
static void HandleConsoleInput(IGCRealTimeMonResult session)
{
var k = Console.ReadKey(true);

Expand Down Expand Up @@ -295,10 +278,17 @@ await result.MapResult(async options =>
options.ProcessId = processes[0].Id;
}

Console.WriteLine();
int pid = options.ProcessId;
Process process = Process.GetProcessById(pid);
Console.WriteLine("------- press s for current stats or any other key to exit -------");

Console.WriteLine($"Monitoring process with name: {process.ProcessName} and pid: {pid}");
Console.WriteLine(PrintUtilities.GetHeader(configuration));
Console.WriteLine(PrintUtilities.GetLineSeparator(configuration));

SetupHeapStatsTimerIfEnabled(configuration);
RealTimeProcessing(options.ProcessId, options, configuration);
RealTimeProcessing(pid, configuration);
},
errors => Task.FromResult(errors)
);
Expand Down
101 changes: 101 additions & 0 deletions src/GCRealTimeMon/Service/GCRealTimeMonService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using Microsoft.Diagnostics.Tracing;
using Microsoft.Diagnostics.Tracing.Analysis;
using Microsoft.Diagnostics.Tracing.Analysis.GC;
using realmon.Utilities;
using System;
using System.Reactive.Subjects;

namespace realmon.Service
{
internal sealed class GCRealTimeMonService : IGCRealTimeMonService
{
public static Lazy<IGCRealTimeMonService> Instance = new Lazy<IGCRealTimeMonService>(new GCRealTimeMonService());

public IGCRealTimeMonResult Initialize(int pid, Configuration.Configuration configuration)
{
double? minDurationForGCPausesInMSec = null;
if (configuration.DisplayConditions != null &&
configuration.DisplayConditions.TryGetValue("min gc duration (msec)", out var minDuration))
{
minDurationForGCPausesInMSec = double.Parse(minDuration);
}

var source = PlatformUtilities.GetTraceEventDispatcherBasedOnPlatform(pid, out var session);
Subject<TraceGC> gcEndSubject = new Subject<TraceGC>();
source.NeedLoadedDotNetRuntimes();
source.AddCallbackOnProcessStart((TraceProcess proc) =>
{
if (proc.ProcessID != pid)
{
return;
}

Action<TraceProcess, TraceGC> gcEndAction =
(p, gc) =>
{
if (p.ProcessID == pid)
{
// If no min duration is specified or if the min duration specified is less than the pause duration, log the event.
if (!minDurationForGCPausesInMSec.HasValue ||
(minDurationForGCPausesInMSec.HasValue && minDurationForGCPausesInMSec.Value < gc.PauseDurationMSec))
{
gcEndSubject.OnNext(gc);
}
}
};

proc.AddCallbackOnDotNetRuntimeLoad((TraceLoadedDotNetRuntime runtime) =>
{
// TODO: When there are multiple clients, fix this leak by unsubscribing.
runtime.GCEnd += gcEndAction;
});
});

return new GCRealTimeMonResult(gcEndSubject: gcEndSubject,
source: source,
session: session);
}

private sealed class GCRealTimeMonResult : IGCRealTimeMonResult
{
private Subject<TraceGC> m_gcEndSubject;
private IDisposable m_session;

public GCRealTimeMonResult(Subject<TraceGC> gcEndSubject, TraceEventDispatcher source, IDisposable session)
{
m_gcEndSubject = gcEndSubject;
Source = source;
m_session = session;
}

private bool disposedValue;

public IObservable<TraceGC> GCEndObservable => m_gcEndSubject;
public TraceEventDispatcher Source { get; }

private void Dispose(bool disposing)
{
m_gcEndSubject?.Dispose();

if (!disposedValue)
{
Source?.Dispose();
m_session?.Dispose();
m_gcEndSubject = null;
disposedValue = true;
}
}

~GCRealTimeMonResult()
{
Dispose(disposing: false);
}

public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}
}
17 changes: 17 additions & 0 deletions src/GCRealTimeMon/Service/Interfaces.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Microsoft.Diagnostics.Tracing;
using Microsoft.Diagnostics.Tracing.Analysis.GC;
using System;

namespace realmon.Service
{
internal interface IGCRealTimeMonService
{
IGCRealTimeMonResult Initialize(int pid, Configuration.Configuration configuration);
}

internal interface IGCRealTimeMonResult : IDisposable
{
TraceEventDispatcher Source { get; }
IObservable<TraceGC> GCEndObservable { get; }
}
}
3 changes: 3 additions & 0 deletions src/dotnet-gcmon/dotnet-gcmon.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
<Compile Include="..\GCRealTimeMon\Utilities\PlatformUtilities.cs" Link="Utilities\PlatformUtilities.cs" />
<Compile Include="..\GCRealTimeMon\Utilities\PrintUtilities.cs" Link="Utilities\PrintUtilities.cs" />
<Compile Include="..\GCRealTimeMon\Utilities\CommandLineUtilities.cs" Link="Utilities\CommandLineUtilities.cs" />
<Compile Include="..\GCRealTimeMon\Service\GCRealTimeMonService.cs" Link="Service\GCRealTimeMonService.cs" />
<Compile Include="..\GCRealTimeMon\Service\Interfaces.cs" Link="Service\Interfaces.cs" />
</ItemGroup>

<!--add LICENCE.txt file to the package (PackageLicenceUlr no more supported)-->
Expand All @@ -50,6 +52,7 @@
<PackageReference Include="Microsoft.Diagnostics.NETCore.Client" Version="0.2.251802" />
<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="2.0.74" />
<PackageReference Include="YamlDotNet" Version="11.2.1" />
<PackageReference Include="System.Reactive" Version="5.0.0" />
<PackageReference Include="Sharprompt" Version="2.3.7" />
</ItemGroup>

Expand Down