diff --git a/src/GCRealTimeMon/GCRealTimeMon.csproj b/src/GCRealTimeMon/GCRealTimeMon.csproj
index 41c20db..edae8ef 100644
--- a/src/GCRealTimeMon/GCRealTimeMon.csproj
+++ b/src/GCRealTimeMon/GCRealTimeMon.csproj
@@ -11,6 +11,7 @@
+
diff --git a/src/GCRealTimeMon/Program.cs b/src/GCRealTimeMon/Program.cs
index 306a14d..5c9da4d 100644
--- a/src/GCRealTimeMon/Program.cs
+++ b/src/GCRealTimeMon/Program.cs
@@ -10,6 +10,7 @@
using realmon.Configuration;
using realmon.Utilities;
using CommandLine.Text;
+using realmon.Service;
namespace realmon
{
@@ -53,10 +54,8 @@ 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))
@@ -64,11 +63,9 @@ public static void RealTimeProcessing(int pid, Options options, Configuration.Co
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.
@@ -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)
@@ -176,7 +159,7 @@ private static void PrintLastStats()
}
}
- static void HandleConsoleInput(IDisposable session)
+ static void HandleConsoleInput(IGCRealTimeMonResult session)
{
var k = Console.ReadKey(true);
@@ -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)
);
diff --git a/src/GCRealTimeMon/Service/GCRealTimeMonService.cs b/src/GCRealTimeMon/Service/GCRealTimeMonService.cs
new file mode 100644
index 0000000..572c7fc
--- /dev/null
+++ b/src/GCRealTimeMon/Service/GCRealTimeMonService.cs
@@ -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 Instance = new Lazy(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 gcEndSubject = new Subject();
+ source.NeedLoadedDotNetRuntimes();
+ source.AddCallbackOnProcessStart((TraceProcess proc) =>
+ {
+ if (proc.ProcessID != pid)
+ {
+ return;
+ }
+
+ Action 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 m_gcEndSubject;
+ private IDisposable m_session;
+
+ public GCRealTimeMonResult(Subject gcEndSubject, TraceEventDispatcher source, IDisposable session)
+ {
+ m_gcEndSubject = gcEndSubject;
+ Source = source;
+ m_session = session;
+ }
+
+ private bool disposedValue;
+
+ public IObservable 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);
+ }
+ }
+ }
+}
diff --git a/src/GCRealTimeMon/Service/Interfaces.cs b/src/GCRealTimeMon/Service/Interfaces.cs
new file mode 100644
index 0000000..9a0f7be
--- /dev/null
+++ b/src/GCRealTimeMon/Service/Interfaces.cs
@@ -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 GCEndObservable { get; }
+ }
+}
diff --git a/src/dotnet-gcmon/dotnet-gcmon.csproj b/src/dotnet-gcmon/dotnet-gcmon.csproj
index 7ce9670..708c889 100644
--- a/src/dotnet-gcmon/dotnet-gcmon.csproj
+++ b/src/dotnet-gcmon/dotnet-gcmon.csproj
@@ -37,6 +37,8 @@
+
+
@@ -50,6 +52,7 @@
+