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 @@ +