c# – How to track GC launches and take them into account when logging from different threads?

Question:

The method is called from different threads and you need to log its work. Because GC during garbage collection in WinForms and WPF applications can suspend the work of application threads, this must be taken into account when logging.
How can you track GC runs?

Answer:

In order to track GC, you need to call the RegisterForFullGCNotification method, as well as WaitForFullGCApproach and WaitForFullGCComplete .

using System.Diagnostics;
using System.Collections.Concurrent;
using System.Threading;
using System.Runtime.CompilerServices;

class LogLine {
    public int Number; 
    public int ThreadId; 
    public long Ticks; 
    public object Value;
    public long GCMemory; 
    public int[] GCCollections;
    public LogLine(int num, object value, long ticks) {
        this.Number = num; 
        this.Value = value; 
        this.Ticks = ticks;
        this.ThreadId = Environment.CurrentManagedThreadId;
        this.GCMemory = GC.GetTotalMemory(false);
        int[] arr = new int[GC.MaxGeneration + 1];
        for (var i = 0; i < arr.Length; i++) arr[i] = GC.CollectionCount(i);
        this.GCCollections = arr;
    }
}

class Log : IDisposable {
    Stopwatch sw = Stopwatch.StartNew();
    BlockingCollection<LogLine> lines = new BlockingCollection<LogLine>();
    void Add(LogLine line) {
        if (!lines.IsAddingCompleted) lines.Add(line);
    }
    public Log() {
        GC.RegisterForFullGCNotification(1, 1);
        new Thread(() => {
            while (!lines.IsAddingCompleted) {
                Add(new LogLine(-1, GC.WaitForFullGCApproach(), sw.ElapsedTicks));
                Add(new LogLine(-2, GC.WaitForFullGCComplete(), sw.ElapsedTicks));
            }
        }).Start();
    }
    public void WriteLine(object value = null, [CallerLineNumber] int cnumber = 0) {
        Add(new LogLine(cnumber, value, sw.ElapsedTicks));
    }
    void IDisposable.Dispose() {
        GC.CancelFullGCNotification();
        Add(new LogLine(-3, "Disposed", sw.ElapsedTicks));
        lines.CompleteAdding();
    }
    public IEnumerable<string> ToCsv() {
        var s = ",\t ";
        yield return String.Concat(
            "Number", s, "Ticks", s, "ThreadId", s, "GCMemory", s, 
            "GCCollections", s, "Value");
        foreach (var l in lines)
            yield return String.Concat(
                l.Number, s, l.Ticks, s, l.ThreadId, s, l.GCMemory, s,
                String.Join(";", l.GCCollections), s, l.Value);
    }
}

For the test in two threads, we create and fill a list of arrays of 100 thousand int

var log = new Log();
using (log) {
    Parallel.For(0, 2, i => {
        var lst = new List<int[]>();
        log.WriteLine("new List");
        try { 
            while (true) lst.Add(new int[100000]); 
        }
        catch (OutOfMemoryException) {
            log.WriteLine("OutOfMemory; lst.Count=" + lst.Count);
        }
    });
}

Displaying the collected data

foreach (var line in log.ToCsv()) 
   Console.WriteLine(line);

Result (retrieved in C # Interactive, Microsoft (R) Roslyn C # Compiler version 1.1.0.51204)

Number, Ticks,   ThreadId, GCMemory,    GCCollections, Value
55,     4030,    6,        6905852,     1;0;0,         new List
55,     4426,    9,        6905852,     1;0;0,         new List
-1,     274370,  11,       1481676908,  6;5;5,         Succeeded
-2,     274430,  11,       1482877004,  6;5;5,         Succeeded
-1,     277392,  11,       1528080620,  6;5;5,         Succeeded
58,     355462,  6,        1528087152,  8;7;7,         OutOfMemory; lst.Count=1888
58,     355462,  9,        1528087152,  8;7;7,         OutOfMemory; lst.Count=1920
-2,     317257,  11,       1528087152,  8;7;7,         Succeeded
-3,     356158,  6,        1528095344,  8;7;7,         Disposed
Scroll to Top