Stopping Garbage Collection in .NET Core 3.0 (part II) - Luca Bolognese

Stopping Garbage Collection in .NET Core 3.0 (part II)

Luca -

☕ 4 min. read

Let’s see how it’s im­ple­mented. For why it is im­ple­mented, see part I.

Thanks to Mike for re­view­ing this.

using System;
using System.Diagnostics.Tracing;
using System.Runtime;

The FxCop code an­a­lyz­ers get up­set if I don’t de­clare this, which also im­pede me from us­ing un­signed nu­meral types in in­ter­faces.

[assembly: CLSCompliant(true)]

namespace LNativeMemory
{

The first piece of the puz­zle is to im­ple­ment an event lis­tener. It is a not-ob­vi­ous (for me) class. I don’t fully un­der­stand the life­time se­man­tics, but the code be­low seems to do the right thing.

The in­ter­est­ing piece is _started and the method Start(). The con­struc­tor for EventListener al­lo­cates plenty of stuff. I don’t want to do those al­lo­ca­tions af­ter call­ing TryStartNoGCRegion be­cause they would use part of the GC Heap that I want for my pro­gram. In­stead, I cre­ate it be­fore such call, but then I make it switch on’ just af­ter the Start() method is called.

    internal sealed class GcEventListener : EventListener
{
Action _action;
EventSource _eventSource;
bool _active = false;

internal void Start() { _active = true; }
internal void Stop() { _active = false; }

As de­scribed in part one, you pass a del­e­gate at cre­ation time, which is called when garbage col­lec­tion is restarted.

        internal GcEventListener(Action action) => _action = action ?? throw new ArgumentNullException(nameof(action));

We reg­is­ter to all the events com­ing from .NET. We want to call the del­e­gate at the ex­act point when garbage col­lec­tion is turned on again. We don’t have a clean way to do that (aka there is no run­time event we can hook up to, see here, so lis­ten­ing to every sin­gle GC event gives us the most chances of do­ing it right. Also it ties us the least to any pat­tern of events, which might change in the fu­ture.

        // from https://docs.microsoft.com/en-us/dotnet/framework/performance/garbage-collection-etw-events
private const int GC_KEYWORD = 0x0000001;
private const int TYPE_KEYWORD = 0x0080000;
private const int GCHEAPANDTYPENAMES_KEYWORD = 0x1000000;

protected override void OnEventSourceCreated(EventSource eventSource)
{
if (eventSource.Name.Equals("Microsoft-Windows-DotNETRuntime", StringComparison.Ordinal))
{
_eventSource = eventSource;
EnableEvents(eventSource, EventLevel.Verbose, (EventKeywords)(GC_KEYWORD | GCHEAPANDTYPENAMES_KEYWORD | TYPE_KEYWORD));
}
}

For each event, I check if the garbage col­lec­tor has ex­ited the NoGC re­gion. If it has, then let’s in­voke the del­e­gate.

        protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
var eventName = eventData.EventName;
if(_active && GCSettings.LatencyMode != GCLatencyMode.NoGCRegion)
{
_action?.Invoke();
}
}
}

Now that we have our event lis­tener, we need to hook it up. The code be­low im­ple­ments what I de­scribed ear­lier.

  1. Do your al­lo­ca­tions for the event lis­tener
  2. Start the NoGc re­gion
  3. Start mon­i­tor­ing the run­time for the start of the NoGC re­gion
    public static class GC2
{
static private GcEventListener _evListener;

public static bool TryStartNoGCRegion(long totalSize, Action actionWhenAllocatedMore)
{

_evListener = new GcEventListener(actionWhenAllocatedMore);
var succeeded = GC.TryStartNoGCRegion(totalSize, disallowFullBlockingGC: false);
_evListener.Start();

return succeeded;
}

As puz­zling as this might be, I pro­vi­sion­ally be­lieve it to be cor­rect. Apparently, even if the GC is not in a NoGC re­gion, you still need to call EndNoGCRegion if you have called TryStartNoGCRegion ear­lier, oth­er­wise your next call to TryStartNoGCRegion will fail. EndNoGCRegion will throw an ex­cep­tion, but that’s OK. Your next call to TryStartNoGCRegion will now suc­ceed.

Now read the above re­peat­edly un­til you got. Or just trust that it works some­how.

        public static void EndNoGCRegion()
{
_evListener.Stop();

try
{
GC.EndNoGCRegion();
} catch (Exception)
{

}
}
}

This is used as the de­fault be­hav­ior for the del­e­gate in the wrap­per class be­low. I was made aware by the code an­a­lyzer that I should­n’t be throw­ing an OOF ex­cep­tion here. At first, I dis­missed it, but then it hit me. It is right.

We are not run­ning out of mem­ory here. We sim­ply have al­lo­cated more mem­ory than what we de­clared we would. There is likely plenty of mem­ory left on the ma­chine. Thinking more about it, I grew ashamed of my ini­tial re­ac­tion. Think about a sup­port en­gi­neer get­ting an OOM ex­cep­tion at that point and try­ing to fig­ure out why. So, al­ways lis­ten to Lint …

    public class OutOfGCHeapMemoryException : OutOfMemoryException {
public OutOfGCHeapMemoryException(string message) : base(message) { }
public OutOfGCHeapMemoryException(string message, Exception innerException) : base(message, innerException) { }
public OutOfGCHeapMemoryException() : base() { }

}

This is an util­ity class that im­ple­ments the IDisposable pat­tern for this sce­nario. The size of the de­fault ephemeral seg­ment comes from here.

    public sealed class NoGCRegion: IDisposable
{
static readonly Action defaultErrorF = () => throw new OutOfGCHeapMemoryException();
const int safeEphemeralSegment = 16 * 1024 * 1024;

public NoGCRegion(int totalSize, Action actionWhenAllocatedMore)
{
var succeeded = GC2.TryStartNoGCRegion(totalSize, actionWhenAllocatedMore);
if (!succeeded)
throw new InvalidOperationException("Cannot enter NoGCRegion");
}

public NoGCRegion(int totalSize) : this(totalSize, defaultErrorF) { }
public NoGCRegion() : this(safeEphemeralSegment, defaultErrorF) { }

public void Dispose() => GC2.EndNoGCRegion();
}
}
0 Webmentions

These are webmentions via the IndieWeb and webmention.io.