Last active
May 13, 2023 01:52
-
-
Save Hixon10/4698bc49bd378f504b567de48dd82995 to your computer and use it in GitHub Desktop.
Force Full GC in dotnet
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace ForceGcCLI; | |
using System; | |
using System.Collections.Generic; | |
using System.Diagnostics.Tracing; | |
using System.IO; | |
using System.Threading; | |
using System.Threading.Tasks; | |
using Microsoft.Diagnostics.Tracing; | |
using Microsoft.Diagnostics.NETCore.Client; | |
using Microsoft.Diagnostics.Tracing.Parsers; | |
using Microsoft.Diagnostics.Tracing.Parsers.Clr; | |
public class ForceGcCLI | |
{ | |
internal static volatile bool eventPipeDataPresent; | |
internal static volatile bool dumpComplete; | |
/// <summary> | |
/// Force Full GC via ETW internal event. | |
/// </summary> | |
/// <param name="ct"></param> | |
/// <param name="processID"></param> | |
/// <param name="log"></param> | |
/// <param name="timeoutInSecounds"></param> | |
/// <returns></returns> | |
public static bool ForceFullGC(CancellationToken ct, int processID, TextWriter log, int timeoutInSecounds) | |
{ | |
DateTime start = DateTime.Now; | |
Func<TimeSpan> getElapsed = () => DateTime.Now - start; | |
try | |
{ | |
TimeSpan lastEventPipeUpdate = getElapsed(); | |
// Start the providers and trigger the GCs. | |
log.WriteLine("{0,5:n1}s: Requesting a .NET Heap Dump", getElapsed().TotalSeconds); | |
using EventPipeSessionController gcDumpSession = new EventPipeSessionController(processID, new List<EventPipeProvider> | |
{ | |
new EventPipeProvider("Microsoft-Windows-DotNETRuntime", EventLevel.Verbose, | |
(long)(ClrTraceEventParser.Keywords.GCHeapSnapshot)) | |
}); | |
log.WriteLine("{0,5:n1}s: gcdump EventPipe Session started", getElapsed().TotalSeconds); | |
int gcNum = -1; | |
gcDumpSession.Source.Clr.GCStart += delegate(GCStartTraceData data) | |
{ | |
if (data.ProcessID != processID) | |
{ | |
return; | |
} | |
eventPipeDataPresent = true; | |
if (gcNum < 0 && data.Depth == 2 && data.Type != GCType.BackgroundGC) | |
{ | |
gcNum = data.Count; | |
log.WriteLine("{0,5:n1}s: .NET Dump Started...", getElapsed().TotalSeconds); | |
} | |
}; | |
gcDumpSession.Source.Clr.GCStop += delegate(GCEndTraceData data) | |
{ | |
if (data.ProcessID != processID) | |
{ | |
return; | |
} | |
if (data.Count == gcNum) | |
{ | |
log.WriteLine("{0,5:n1}s: .NET GC Complete.", getElapsed().TotalSeconds); | |
dumpComplete = true; | |
} | |
}; | |
gcDumpSession.Source.Clr.GCBulkNode += delegate(GCBulkNodeTraceData data) | |
{ | |
if (data.ProcessID != processID) | |
{ | |
return; | |
} | |
eventPipeDataPresent = true; | |
if ((getElapsed() - lastEventPipeUpdate).TotalMilliseconds > 500) | |
{ | |
log.WriteLine("{0,5:n1}s: Making GC Heap Progress...", getElapsed().TotalSeconds); | |
} | |
lastEventPipeUpdate = getElapsed(); | |
}; | |
// Set up a separate thread that will listen for EventPipe events coming back telling us we succeeded. | |
Task readerTask = Task.Run(() => | |
{ | |
// cancelled before we began | |
if (ct.IsCancellationRequested) | |
{ | |
return; | |
} | |
log.WriteLine("{0,5:n1}s: Starting to process events", getElapsed().TotalSeconds); | |
gcDumpSession.Source.Process(); | |
log.WriteLine("{0,5:n1}s: EventPipe Listener dying", getElapsed().TotalSeconds); | |
}, ct); | |
for (;;) | |
{ | |
if (ct.IsCancellationRequested) | |
{ | |
log.WriteLine("{0,5:n1}s: Cancelling...", getElapsed().TotalSeconds); | |
break; | |
} | |
if (readerTask.Wait(100)) | |
{ | |
break; | |
} | |
if (!eventPipeDataPresent && getElapsed().TotalSeconds > 5) // Assume it started within 5 seconds. | |
{ | |
log.WriteLine("{0,5:n1}s: Assume no .NET Heap", getElapsed().TotalSeconds); | |
break; | |
} | |
if (getElapsed().TotalSeconds > timeoutInSecounds) // Time out after `timeout` seconds. defaults to 30s. | |
{ | |
log.WriteLine("{0,5:n1}s: Timed out after {1} seconds", getElapsed().TotalSeconds, timeoutInSecounds); | |
break; | |
} | |
if (dumpComplete) | |
{ | |
break; | |
} | |
} | |
Task stopTask = Task.Run(() => | |
{ | |
log.WriteLine("{0,5:n1}s: Shutting down gcdump EventPipe session", getElapsed().TotalSeconds); | |
gcDumpSession.EndSession(); | |
log.WriteLine("{0,5:n1}s: gcdump EventPipe session shut down", getElapsed().TotalSeconds); | |
}, ct); | |
try | |
{ | |
while (!Task.WaitAll(new Task[] { readerTask, stopTask }, 1000)) | |
{ | |
log.WriteLine("{0,5:n1}s: still reading...", getElapsed().TotalSeconds); | |
} | |
} | |
catch (AggregateException ae) // no need to throw if we're just cancelling the tasks | |
{ | |
foreach (Exception e in ae.Flatten().InnerExceptions) | |
{ | |
if (e is not TaskCanceledException) | |
{ | |
throw; | |
} | |
} | |
} | |
log.WriteLine("{0,5:n1}s: gcdump EventPipe Session closed", getElapsed().TotalSeconds); | |
if (ct.IsCancellationRequested) | |
{ | |
return false; | |
} | |
} | |
catch (Exception e) | |
{ | |
log.WriteLine($"{getElapsed().TotalSeconds,5:n1}s: [Error] Exception during gcdump: {e}"); | |
} | |
log.WriteLine("[{0,5:n1}s: Done Dumping .NET heap success={1}]", getElapsed().TotalSeconds, dumpComplete); | |
return dumpComplete; | |
} | |
internal sealed class EventPipeSessionController : IDisposable | |
{ | |
private List<EventPipeProvider> _providers; | |
private DiagnosticsClient _client; | |
private EventPipeSession _session; | |
private EventPipeEventSource _source; | |
private int _pid; | |
public IReadOnlyList<EventPipeProvider> Providers => _providers.AsReadOnly(); | |
public EventPipeEventSource Source => _source; | |
public EventPipeSessionController(int pid, List<EventPipeProvider> providers, bool requestRundown = true) | |
{ | |
_pid = pid; | |
_providers = providers; | |
_client = new DiagnosticsClient(pid); | |
_session = _client.StartEventPipeSession(providers, requestRundown, 1024); | |
_source = new EventPipeEventSource(_session.EventStream); | |
} | |
public void EndSession() | |
{ | |
_session.Stop(); | |
} | |
#region IDisposable Support | |
private bool disposedValue; // To detect redundant calls | |
private void Dispose(bool disposing) | |
{ | |
if (!disposedValue) | |
{ | |
if (disposing) | |
{ | |
_session?.Dispose(); | |
_source?.Dispose(); | |
} | |
disposedValue = true; | |
} | |
} | |
public void Dispose() | |
{ | |
Dispose(true); | |
} | |
#endregion | |
} | |
static void Main(string[] args) | |
{ | |
int processId = 43112; | |
bool dumpFromEventPipe = ForceFullGC(CancellationToken.None, processId, Console.Out, 30); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Code is copied from https://github.com/dotnet/diagnostics/blob/main/src/Tools/dotnet-gcdump/DotNetHeapDump/EventPipeDotNetHeapDumper.cs