GC EventPipe 모니터링

C# 2022. 3. 8. 09:34
namespace Test
{

    public struct GcHeapInfo
    {
        public int Generation { get; set; } // Note that this field is derived from the TotalPromotedSize* fields. If nothing was promoted, it is possible that this could give a number that is smaller than what GC/Start or GC/Stop would indicate.
        public long TotalHeapSize { get; set; }
        public long TotalPromoted { get; set; }

        public long GenerationSize0 { get; set; } // The size, in bytes, of generation 0 memory.
        public long TotalPromotedSize0 { get; set; } // The number of bytes that are promoted from generation 0 to generation 1.

        public long GenerationSize1 { get; set; } // The size, in bytes, of generation 1 memory.
        public long TotalPromotedSize1 { get; set; } // The number of bytes that are promoted from generation 1 to generation 2.

        public long GenerationSize2 { get; set; } // The size, in bytes, of generation 2 memory.
        public long TotalPromotedSize2 { get; set; } // The number of bytes that survived in generation 2 after the last collection

        public long GenerationSize3 { get; set; } // The size, in bytes, of the large object heap.
        public long TotalPromotedSize3 { get; set; } // The number of bytes that survived in the large object heap after the last collection.

        public long GenerationSize4 { get; set; } // The size, in bytes, of the pinned object heap.
        public long TotalPromotedSize4 { get; set; } // The number of bytes that survived in the pinned object heap after the last collection.

        public int GCHandleCount { get; set; } // The number of garbage collection handles in use.

        public int SinkBlockCount { get; set; } // The number of synchronization blocks in use.

        public int PinnedObjectCount { get; set; } // The number of pinned (unmovable) objects.

        public long FinalizationPromotedCount { get; set; } // The number of objects that are ready for finalization.
        public long FinalizationPromotedSize { get; set; } // The total size, in bytes, of the objects that are ready for finalization.

        public override string ToString()
        {
            return $"Generation: {Generation}, TotalHeapSize: {TotalHeapSize}, TotalPromoted: {TotalPromoted}, " +
                $"GenerationSize0: {GenerationSize0}, TotalPromotedSize0: {TotalPromotedSize0}, " +
                $"GenerationSize1: {GenerationSize1}, TotalPromotedSize1: {TotalPromotedSize1}, " +
                $"GenerationSize2: {GenerationSize2}, TotalPromotedSize2: {TotalPromotedSize2}, " +
                $"GenerationSize3: {GenerationSize3}, TotalPromotedSize3: {TotalPromotedSize3}, " +
                $"GenerationSize4: {GenerationSize4}, TotalPromotedSize4: {TotalPromotedSize4}, " +
                $"GCHandleCount: {GCHandleCount}, SinkBlockCount: {SinkBlockCount}, PinnedObjectCount: {PinnedObjectCount}, " +
                $"FinalizationPromotedCount: {FinalizationPromotedCount}, FinalizationPromotedSize: {FinalizationPromotedSize}";
        }

    }
}

 

 

 

using Microsoft.Diagnostics.NETCore.Client;
using Microsoft.Diagnostics.Tracing;
using Microsoft.Diagnostics.Tracing.Parsers;
using Microsoft.Diagnostics.Tracing.Parsers.Clr;
using System;
using System.Diagnostics;
using System.Diagnostics.Tracing;
using System.Threading;

namespace Test
{

    /// <summary>
    /// GC 이벤트 수신용 클래스
    /// 참고. EventPipe 를 사용하면 디버깅(breakpoint 사용) 시 IDE 에서 메시지 팝업을 주어 불편하다.
    ///
    /// EventPipe 사용을 위한 nuget 설치
    /// - Microsoft.Diagnostics.NETCore.Client 0.2.257301
    /// - Microsoft.Diagnostics.Tracing.TraceEvent 2.0.74
    ///
    /// ref.
    /// - EventPipe: https://docs.microsoft.com/ko-kr/dotnet/core/diagnostics/eventpipe
    /// - Event: https://docs.microsoft.com/ko-kr/dotnet/fundamentals/diagnostics/runtime-garbage-collection-events#gcsuspendeebegin_v1-event
    /// </summary>
    public class GcEventListener
    {
        private EventPipeSession _session;
        private EventPipeEventSource _source;

        // EventPipe 는 thread 를 점유하므로 별도 스레드로 실행한다.
        private Thread _thread;

        // 에러 수신용 콜백
        private readonly Action<string> _onErrorCallback;

        // GC Event 수신용 콜백
        private readonly Action<GcInfo> _onGcInfoCallback;

        // GC Heap 정보 수신용 콜백
        private readonly Action<GcHeapInfo> _onGcHeapInfoCallback;

        // event 수신할 pid
        public int _processId;

        /// <summary>
        /// 생성자
        /// </summary>
        /// <param name="errorCallback">에러 수신용 콜백</param>
        /// <param name="gcInfoCallback">GC Event 수신용 콜백</param>
        /// <param name="gcHeapInfoCallback">GC Heap 정보 수신용 콜백</param>
        public GcEventListener(
            Action<string> errorCallback,
            Action<GcInfo> gcInfoCallback,
            Action<GcHeapInfo> gcHeapInfoCallback)
        {
            _processId = Process.GetCurrentProcess().Id;

            _onErrorCallback = (errorCallback is null) ? OnErrorCallback : errorCallback;
            _onGcInfoCallback = (gcInfoCallback is null) ? OnGcInfoCallback : gcInfoCallback;
            _onGcHeapInfoCallback = (gcHeapInfoCallback is null) ? OnGcHeapInfoCallback : gcHeapInfoCallback;
        }

        /// <summary>
        /// GC Event 수신 시작
        /// </summary>
        public void Start()
        {
            if (_thread is null)
            {
                _thread = new Thread(new ThreadStart(Run));
                _thread.Start();
            }
        }

        /// <summary>
        /// GC Event 수신 종료
        /// </summary>
        public void Stop()
        {
            if (_source != null)
            {
                _source.StopProcessing();
            }

            if (_session != null)
            {
                _session.Dispose();
            }

            if (_thread != null)
            {
                _thread.Join();
                _thread = null;
            }
        }

        private void Run()
        {
            try
            {
                DiagnosticsClient client = new DiagnosticsClient(_processId);
                _session = client.StartEventPipeSession(
                    new EventPipeProvider(
                        "Microsoft-Windows-DotNETRuntime",
                        EventLevel.Informational,
                        (long)ClrTraceEventParser.Keywords.GC
                    ),
                    false
                );
                _source = new EventPipeEventSource(_session.EventStream);

                GcInfo gcInfo = new GcInfo();
                GcHeapInfo gcHeapInfo = new GcHeapInfo();

                // Event 정보
                // https://apisof.net/catalog/6410c1dc-7784-d05f-c107-93a10a97ae80
                // https://docs.microsoft.com/en-us/dotnet/fundamentals/diagnostics/runtime-garbage-collection-events

                // GCStart 후 GCStop 이 쌍으로 호출된다.
                _source.Clr.GCStart += (GCStartTraceData evt) =>
                {
                    gcInfo.Generation = evt.Depth;
                    gcInfo.TotalCount = evt.Count;
                    gcInfo.Reason = evt.Reason;
                    gcInfo.StartTime = evt.TimeStamp;
                };

                _source.Clr.GCStop += (GCEndTraceData evt) =>
                {
                    gcInfo.Generation = evt.Depth;
                    gcInfo.TotalCount = evt.Count;
                    gcInfo.EndTime = evt.TimeStamp;

                    _onGcInfoCallback(gcInfo);
                };

                _source.Clr.GCHeapStats += (GCHeapStatsTraceData evt) =>
                {
                    gcHeapInfo.Generation = evt.Depth;
                    gcHeapInfo.TotalHeapSize = evt.TotalHeapSize;
                    gcHeapInfo.TotalPromoted = evt.TotalPromoted;
                    gcHeapInfo.GenerationSize0 = evt.GenerationSize0;
                    gcHeapInfo.TotalPromotedSize0 = evt.TotalPromotedSize0;
                    gcHeapInfo.GenerationSize1 = evt.GenerationSize1;
                    gcHeapInfo.TotalPromotedSize1 = evt.TotalPromotedSize1;
                    gcHeapInfo.GenerationSize2 = evt.GenerationSize2;
                    gcHeapInfo.TotalPromotedSize2 = evt.TotalPromotedSize2;
                    gcHeapInfo.GenerationSize3 = evt.GenerationSize3;
                    gcHeapInfo.TotalPromotedSize3 = evt.TotalPromotedSize3;
                    gcHeapInfo.GenerationSize4 = evt.GenerationSize4;
                    gcHeapInfo.TotalPromotedSize4 = evt.TotalPromotedSize4;
                    gcHeapInfo.GCHandleCount = evt.GCHandleCount;
                    gcHeapInfo.SinkBlockCount = evt.SinkBlockCount;
                    gcHeapInfo.PinnedObjectCount = evt.PinnedObjectCount;
                    gcHeapInfo.FinalizationPromotedCount = evt.FinalizationPromotedCount;
                    gcHeapInfo.FinalizationPromotedSize = evt.FinalizationPromotedSize;

                    _onGcHeapInfoCallback(gcHeapInfo);
                };

                _source.Process();
            }

            catch (Exception e)
            {
                _onErrorCallback($"gc event error: {e}");
            }
        }

        // 기본 콜백 메소드
        private void OnErrorCallback(string _) { }
        private void OnGcInfoCallback(GcInfo _) { }
        private void OnGcHeapInfoCallback(GcHeapInfo _) { }

    }
}

 

 

using Microsoft.Diagnostics.Tracing.Parsers.Clr;
using System;

namespace Test
{
    public struct GcInfo
    {
        public int Generation { get; set; }  // GC generation 0,1,2
        public int TotalCount { get; set; } // GC 발생 횟수
        public GCReason Reason { get; set; } // GC 발생 사유
        public GCType Type { get; set; } // GC type
        public DateTime StartTime { get; set; } // GC 시작 시간
        public DateTime EndTime { get; set; } // GC 종료 시간
        public double ElapsedTimeMsec => (EndTime - StartTime).TotalMilliseconds;

        public override string ToString()
        {
            return $"Generation: {Generation}, TotalCount: {TotalCount}, ElapsedTimeMsec: {ElapsedTimeMsec}, " +
                $"Reason: {Reason}, Type: {Type}, " +
                $"StartTime: {StartTime:yyyy-MM-dd hh:mm:ss.fff}, EndTime: {EndTime:yyyy-MM-dd hh:mm:ss.fff}";
        }
    }
}

 

 

 

 

using System;

namespace Test
{
    public static class GcMemory
    {
        public static string Get()
        {
            GCMemoryInfo gcMemoryInfo = GC.GetGCMemoryInfo();

            return
                $"FragmentedBytes: {gcMemoryInfo.FragmentedBytes}, " + // 마지막 가비지 수집이 발생한 총 조각화를 가져옵니다.
                $"HeapSizeBytes: {gcMemoryInfo.HeapSizeBytes}, " + // 마지막 가비지 수집이 발생한 총 힙 크기를 가져옵니다.
                $"HighMemoryLoadThresholdBytes: {gcMemoryInfo.HighMemoryLoadThresholdBytes}, " + // 마지막 가비지 수집이 발생했을 때의 높은 메모리 로드 임계값을 가져옵니다.
                $"MemoryLoadBytes: {gcMemoryInfo.MemoryLoadBytes}, " + // 마지막 가비지 수집이 발생했을 때의 메모리 로드를 가져옵니다.
                $"TotalAvailableMemoryBytes: {gcMemoryInfo.TotalAvailableMemoryBytes }"; // 가비지 수집기에서 마지막 가비지 수집이 발생했을 때 사용할 수 있는 총 메모리를 가져옵니다.
        }
    }
}

 

 

 

Performance Counter - cpu usage + @ 정보

using System;
using System.Diagnostics;
using System.Threading;

namespace Test
{

    /// <summary>
    /// cpu, mem, gc 정보 조회
    ///
    /// nuget 설치: System.Diagnostics.PerformanceCounter
    /// </summary>
    public class ResourceMonitor
    {
        // 대상 프로세스 이름(# 포함)
        private string _processName = "";


        // cpu performance counter
        private PerformanceCounter _cpu;

        // process name getter
        public string ProcessName
        {
            get
            {
                return _processName;
            }
        }


        // GC
        public class GcGenerationCount
        {
            public int Gen0 { get; set; }
            public int Gen1 { get; set; }
            public int Gen2 { get; set; }
        }
        public GcGenerationCount GcCount { get; private set; } = new GcGenerationCount();



        // Thread Pool
        public class ThreadPoolCount
        {
            // worker thread count
            public int UseWorkerCount { get; set; }
            public int MinWorkerCount { get; set; }
            public int MaxWorkerCount { get; set; }
            public int AvailWorkerCount { get; set; }

            // completion port thread count
            public int UseCompletionPortCount { get; set; }
            public int MinCompletionPortCount { get; set; }
            public int MaxCompletionPortCount { get; set; }
            public int AvailCompletionPortCount { get; set; }
        }
        public ThreadPoolCount ThreadCount { get; private set; } = new ThreadPoolCount();



        public ResourceMonitor()
        {
            UpdateForPerformanceCounter();
        }

        /// <summary>
        /// 프로세스 명(#)이 변경될 수 있기 때문에 주기적으로 호출해야 한다.
        /// </summary>
        public void UpdateForPerformanceCounter()
        {
            // process name 이 변경될 수 있다.
            // ex. (App, App#1, App#2) => (App, App#1)
            string processName = GetProcessNameWithSharp(AppDomain.CurrentDomain.FriendlyName, Process.GetCurrentProcess().Id);

            if (!_processName.Equals(processName))
            {
                _processName = processName;
                _cpu = new PerformanceCounter("Process", "% Processor Time", processName, true);
            }
        }

        public string GetMonitoringData()
        {
            UpdateForPerformanceCounter();

            var cpu = GetCpu();
            var mem = GetMemory();
            var gc = GetGcCount();
            var threads = GetThreadCount();

            return
                $"[Resource] cpu: {cpu} %, mem: {HumanReadableConverter.ByteSizeToString(mem)}, gc: {gc.Gen0}-{gc.Gen1}-{gc.Gen2}, " +
                $"[Worker] use: {threads.UseWorkerCount} (min: {threads.MinWorkerCount}, max: {threads.MaxWorkerCount}, avail: {threads.AvailWorkerCount}), " +
                $"[CP] use: {threads.UseCompletionPortCount} (min: {threads.MinCompletionPortCount}, max: {threads.MaxCompletionPortCount}, avail: {threads.AvailCompletionPortCount})";
        }

        /// <summary>
        /// 동일 프로세스 명으로 여러개가 떠있을 수 있으므로
        /// 퍼포먼스 카운트에서 사용하는 실제 이름을 구한다.
        /// ex. App#1
        /// </summary>
        /// <param name="processNamePrefix">ex. chrome</param>
        /// <param name="pid">ex. 1234</param>
        /// <returns>ex. chrome#18</returns>
        private string GetProcessNameWithSharp(string processNamePrefix, int pid)
        {
            string[] instances = new PerformanceCounterCategory("Process").GetInstanceNames();
            foreach (string instance in instances)
            {
                try
                {
                    using var counter = new PerformanceCounter("Process", "ID Process", instance, true);
                    if (!counter.InstanceName.StartsWith(processNamePrefix))
                    {
                        continue;
                    }

                    if (pid == (int)counter.RawValue)
                    {
                        return instance;
                    }
                }
                catch (Exception)
                {
                    // nothing to do
                }
            }

            // 현재 프로세스를 감지하므로 실제 빈 문자열을 주는 경우는 없다.
            return "";
        }


        /// <summary>
        /// 현재 프로세스 cpu 사용량 (%)
        /// </summary>
        /// <returns></returns>
        public int GetCpu()
        {
            try
            {
                double usage = _cpu.NextValue() / Environment.ProcessorCount;
                return (int)Math.Round(usage);
            }
            catch (Exception)
            {
                // 비정상적인 상황(서버 초기화시 발생 가능)
                return -1;
            }
        }


        /// <summary>
        /// 메모리 사용량 (개인 작업 집합)
        /// </summary>
        /// <returns></returns>
        public long GetMemory()
        {
            return GC.GetTotalMemory(false);
        }


        /// <summary>
        /// 세대별 gc 발생 횟수 (누적치)
        /// </summary>
        /// <returns></returns>
        public GcGenerationCount GetGcCount()
        {
            GcCount.Gen0 = GC.CollectionCount(0);
            GcCount.Gen1 = GC.CollectionCount(1);
            GcCount.Gen2 = GC.CollectionCount(2);

            return GcCount;
        }

        /// <summary>
        /// .net thread pool 사용 개수
        /// </summary>
        /// <returns></returns>
        public ThreadPoolCount GetThreadCount()
        {
            ThreadPool.GetMinThreads(out int minWorker, out int minCP);
            ThreadPool.GetMaxThreads(out int maxWorker, out int maxCP);
            ThreadPool.GetAvailableThreads(out int availWorker, out int availCP);

            ThreadCount.UseWorkerCount = maxWorker - availWorker;
            ThreadCount.MinWorkerCount = minWorker;
            ThreadCount.MaxWorkerCount = maxWorker;
            ThreadCount.AvailWorkerCount = availWorker;

            ThreadCount.UseCompletionPortCount = maxCP - availCP;
            ThreadCount.MinCompletionPortCount = minCP;
            ThreadCount.MaxCompletionPortCount = maxCP;
            ThreadCount.AvailCompletionPortCount = availCP;

            return ThreadCount;
        }
    }
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

'C#' 카테고리의 다른 글

c# HttpClient  (0) 2022.03.15
c# channel  (0) 2022.03.15
object pool, object type pool  (0) 2022.03.11
GC TEST  (0) 2022.03.08
C# .net core 빌드 및 powershell 전송  (0) 2022.03.01
c# stack size 확인  (0) 2022.02.24
숫자 범위 추출 및 확장  (0) 2021.11.03
C# LZ4  (0) 2021.10.20