PerfView专题 (第七篇):如何洞察触发 GC 的 C# 代码?

   2023-02-09 学习力0
核心提示:一:背景上一篇我们聊到了如何用 PerfView 洞察 GC 的变化,但总感觉还缺了点什么? 对,就是要跟踪到底是什么代码触发了 GC,这对我们分析由于 GC 导致的 CPU 爆高有非常大的参考价值,在以前我都是用 WinDBG 来实现,但这玩意需要做一些侵入性操作,实战起

一:背景

上一篇我们聊到了如何用 PerfView 洞察 GC 的变化,但总感觉还缺了点什么? 对,就是要跟踪到底是什么代码触发了 GC,这对我们分析由于 GC 导致的 CPU 爆高有非常大的参考价值,在以前我都是用 WinDBG 来实现,但这玩意需要做一些侵入性操作,实战起来不是那么丝滑,虽然有可以录制功能的 TTD,所以需寻找完美的解决方案,在此可以借助一下 PerfView 。

二:如何洞察

1. 一个小案例

为了方便讲解,先上一段简单的测试代码,不断的往 List 中塞入数据,可以实现不断的 GC 触发。


namespace ConsoleApp6
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Task.Run(Alloc1);

            Console.ReadLine();
        }

        static void Alloc1()
        {
            List<string> list = new List<string>();

            var rand = new Random();

            for (int i = 0; i < int.MaxValue; i++)
            {
                list.Add(string.Join(",", Enumerable.Range(1, 1000)));
                Console.WriteLine(i);
            }
        }
    }
}

2. WinDbg 拦截

用 windbg 拦截的话非常简单,只需要找到 CoreCLR 中触发 GC 的入口方法,然后下一个断点,再用 k 查看其线程栈即可。


0:012> bp coreclr!WKS::GCHeap::GarbageCollectGeneration
0:012> g
Breakpoint 0 hit
coreclr!WKS::GCHeap::GarbageCollectGeneration:
00007ffa`7823f8cc 48895c2408      mov     qword ptr [rsp+8],rbx ss:00000000`2321f200=00000000006c49f8
0:008> k
 # Child-SP          RetAddr               Call Site
00 00000000`2321f1f8 00007ffa`782f59db     coreclr!WKS::GCHeap::GarbageCollectGeneration [D:\a\_work\1\s\src\coreclr\gc\gc.cpp @ 44615] 
01 00000000`2321f200 00007ffa`78337b5d     coreclr!WKS::gc_heap::trigger_gc_for_alloc+0x2b [D:\a\_work\1\s\src\coreclr\gc\gc.cpp @ 16846] 
02 00000000`2321f230 00007ffa`782db675     coreclr!WKS::gc_heap::try_allocate_more_space+0x5c4c1 [D:\a\_work\1\s\src\coreclr\gc\gc.cpp @ 16977] 
03 00000000`2321f290 00007ffa`78247784     coreclr!WKS::gc_heap::allocate_more_space+0x31 [D:\a\_work\1\s\src\coreclr\gc\gc.cpp @ 17447] 
04 (Inline Function) --------`--------     coreclr!WKS::gc_heap::allocate+0x58 [D:\a\_work\1\s\src\coreclr\gc\gc.cpp @ 17478] 
05 00000000`2321f2c0 00007ffa`781d9768     coreclr!WKS::GCHeap::Alloc+0x84 [D:\a\_work\1\s\src\coreclr\gc\gc.cpp @ 43676] 
06 (Inline Function) --------`--------     coreclr!Alloc+0x125 [D:\a\_work\1\s\src\coreclr\vm\gchelpers.cpp @ 228] 
07 00000000`2321f2f0 00007ffa`782cc0d9     coreclr!AllocateString+0x19c [D:\a\_work\1\s\src\coreclr\vm\gchelpers.cpp @ 861] 
08 00000000`2321f390 00007ffa`18782d10     coreclr!FramedAllocateString+0x79 [D:\a\_work\1\s\src\coreclr\vm\jithelpers.cpp @ 2429] 
09 00000000`2321f4e0 00007ffa`1878687d     System_Private_CoreLib!System.Number.Int32ToDecStr+0xb0
0a 00000000`2321f530 00007ffa`1878668a     System_Private_CoreLib!System.String.JoinCore<int>+0x1bd
0b 00000000`2321f7e0 00007ffa`1877bdcc     System_Private_CoreLib!System.String.Join<int>+0x2a
0c 00000000`2321f820 00007ffa`776d2d0e     ConsoleApp10!ConsoleApp10.Program.Alloc1+0xbc [D:\net6\ConsoleApp1\ConsoleApp10\Program.cs @ 24] 
0d 00000000`2321f8a0 00007ffa`776d7716     System_Private_CoreLib!System.Threading.Tasks.Task.InnerInvoke+0x1e
0e 00000000`2321f8d0 00007ffa`776bd7f5     System_Private_CoreLib!System.Threading.Tasks.Task.<>c.<.cctor>b__272_0+0x16
0f 00000000`2321f900 00007ffa`776d2a38     System_Private_CoreLib!System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop+0x35
10 00000000`2321f950 00007ffa`776d2943     System_Private_CoreLib!System.Threading.Tasks.Task.ExecuteWithThreadLocal+0x98
11 00000000`2321f9f0 00007ffa`776c6213     System_Private_CoreLib!System.Threading.Tasks.Task.ExecuteEntryUnsafe+0x53
12 00000000`2321fa30 00007ffa`776cdd8a     System_Private_CoreLib!System.Threading.ThreadPoolWorkQueue.Dispatch+0x263
13 00000000`2321fad0 00007ffa`776b278f     System_Private_CoreLib!System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart+0x14a
14 00000000`2321fbe0 00007ffa`7830a853     System_Private_CoreLib!System.Threading.Thread.StartCallback+0x3f
15 00000000`2321fc20 00007ffa`781fd43c     coreclr!CallDescrWorkerInternal+0x83
16 00000000`2321fc60 00007ffa`782ebf93     coreclr!DispatchCallSimple+0x80 [D:\a\_work\1\s\src\coreclr\vm\callhelpers.cpp @ 220] 
17 00000000`2321fcf0 00007ffa`78258695     coreclr!ThreadNative::KickOffThread_Worker+0x63 [D:\a\_work\1\s\src\coreclr\vm\comsynchronizable.cpp @ 158] 
18 (Inline Function) --------`--------     coreclr!ManagedThreadBase_DispatchInner+0xd [D:\a\_work\1\s\src\coreclr\vm\threads.cpp @ 7321] 
19 00000000`2321fd50 00007ffa`7825859a     coreclr!ManagedThreadBase_DispatchMiddle+0x85 [D:\a\_work\1\s\src\coreclr\vm\threads.cpp @ 7365] 
1a 00000000`2321fe30 00007ffa`782583b9     coreclr!ManagedThreadBase_DispatchOuter+0xae [D:\a\_work\1\s\src\coreclr\vm\threads.cpp @ 7524] 
1b (Inline Function) --------`--------     coreclr!ManagedThreadBase_FullTransition+0x2d [D:\a\_work\1\s\src\coreclr\vm\threads.cpp @ 7569] 
1c (Inline Function) --------`--------     coreclr!ManagedThreadBase::KickOff+0x2d [D:\a\_work\1\s\src\coreclr\vm\threads.cpp @ 7604] 
1d 00000000`2321fed0 00007ffa`e9e47034     coreclr!ThreadNative::KickOffThread+0x79 [D:\a\_work\1\s\src\coreclr\vm\comsynchronizable.cpp @ 230] 
1e 00000000`2321ff30 00007ffa`e9f9d0d1     KERNEL32!BaseThreadInitThunk+0x14
1f 00000000`2321ff60 00000000`00000000     ntdll!RtlUserThreadStart+0x21

从输出中可以看到,当前触发的GC的操作是由于 ConsoleApp10!ConsoleApp10.Program.Alloc1+0xbc 方法所致,原因是由于0代阈值用完,但这种调试 CoreCLR 源码的方式终究还是侵入性较大,那有没有自动帮我收集分配 线程调用栈 的工具呢? 这就需要借助 PerfView 啦。

3. PerfView 拦截

Windows 系统内置强大的事件跟踪机制(ETW)再次显示出了威力,我们可以用 PerfView 去拦截 GC 的触发事件,并同时记录此时事件触发的调用栈,而且对应用程序的开销非常小。

接下来我们在 Provider Browser 对话框中选择 GCKeyword 选项,

然后再配上一个记录调用栈参数 @StacksEnabled=true, 最后的结果就是:


Microsoft-Windows-DotNETRuntime:GCKeyword:Always:@StacksEnabled=true

配置好之后就可以 Start Collection 了,收集好之后,我们点击面板上的 Events 项,然后输出 GC 关键词,寻找 GC 相关的事件,这里我们看下 GC/Start 事件,如下图所示:

从截图看,非常舒服,清晰的记录着每一个 GC 的详细情况。


HasStack="True" ThreadID="12,912" ProcessorNumber="3" Count="67" Reason="AllocSmall" Depth="0" Type="NonConcurrentGC" ClrInstanceID="8" ClientSequenceNumber="0" 

接下来在 Time MSec 列上点击右键选择 Open Any Stacks 打开触发这个 GC 的调用栈代码。

从图中可以非常清晰的看到,Alloc1() 方法触发了 GC 并清晰记录 ETW 事件的全过程,真是太强大,太令人兴奋了。

 
反对 0举报 0 评论 0
 

免责声明:本文仅代表作者个人观点,与乐学笔记(本网)无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
    本网站有部分内容均转载自其它媒体,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责,若因作品内容、知识产权、版权和其他问题,请及时提供相关证明等材料并与我们留言联系,本网站将在规定时间内给予删除等相关处理.

  • 使用C#编写一个.NET分析器(一) 使用csv模块的什么方法可以一次性将一行数据写入文件
    使用C#编写一个.NET分析器(一) 使用csv模块的
    译者注这是在Datadog公司任职的Kevin Gosse大佬使用C#编写.NET分析器的系列文章之一,在国内只有很少很少的人了解和研究.NET分析器,它常被用于APM(应用性能诊断)、IDE、诊断工具中,比如Datadog的APM,Visual Studio的分析器以及Rider和Reshaper等等。之前
    03-08
  • 跨语言调用C#代码的新方式-DllExport 跨语言调用本质
    跨语言调用C#代码的新方式-DllExport 跨语言调
    简介上一篇文章使用C#编写一个.NET分析器文章发布以后,很多小伙伴都对最新的NativeAOT函数导出比较感兴趣,今天故写一篇短文来介绍一下如何使用它。在以前,如果有其他语言需要调用C#编写的库,那基本上只有通过各种RPC的方式(HTTP、GRPC)或者引入一层C++
    03-08
  • 我比较了 Go 和 C# 的速度
    我比较了 Go 和 C# 的速度
    我在 Go 和 C# 之间进行了速度比较。我通常使用 C#,但我有机会使用 Go,并且由于传闻 Go 速度很快,所以我实际测量了它。测量内容我在 Go 和 C# 中执行了一个简单的循环和判断过程,以查看整数 2 到 N 是否为质数。来源是Github参考。测量模式 逻辑内核 8 Wi
    03-08
  • [C#]使用 AltCover 获得代码覆盖率 - E2E Test 和 Unit Test
    [C#]使用 AltCover 获得代码覆盖率 - E2E Test
    背景在 CI/CD 流程当中,测试是 CI 中很重要的部分。跟开发人员关系最大的就是单元测试,单元测试编写完成之后,我们可以使用 IDE 或者 dot cover 等工具获得单元测试对于业务代码的覆盖率。不过我们需要一个独立的 CLI 工具,这样我们才能够在 Jenkins 的 CI
  • C#中LINQ的Select与SelectMany函数如何使用 c反应蛋白高说明什么
    C#中LINQ的Select与SelectMany函数如何使用 c反
    本篇内容主要讲解“C#中LINQ的Select与SelectMany函数如何使用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“C#中LINQ的Select与SelectMany函数如何使用”吧!LINQ的Select与SelectMany函数使用Select扩展函
    02-09 linqselect
  • PerfView专题 (第三篇):如何寻找 C# 中的 VirtualAlloc 内存泄漏
    PerfView专题 (第三篇):如何寻找 C# 中的 Virt
    一:背景上一篇我们聊到了如何用 PerfView 去侦察 NTHeap 的内存泄漏,这种内存泄漏往往是用 C 的 malloc 或者 C++ 的 new 分配而不释放所造成的,这一篇我们来聊一下由 VirtualAlloc 方法造成的泄漏如何去甄别?了解 VirtualAlloc 的朋友肯定说, C# 这种高
    02-09
  • Blazor和Vue对比学习(知识点杂锦3.04):Blazor中C#和JS互操作(超长文)
    Blazor和Vue对比学习(知识点杂锦3.04):Blazo
    C#和JS互操作的基本语法是比较简单的,但小知识点特别多,同时,受应用加载顺序、组件生命周期以及参数类型的影响,会有比较多坑,需要耐心的学习。在C#中调用JS的场景会比较多,特别是在WASM模式下,由于WebAssembly的限制,很多时候,还是需要借助JS去控制D
    02-09
  • 的键">C#怎么使用struct类型作为泛型Dictionary
    本文小编为大家详细介绍“C#怎么使用struct类型作为泛型DictionaryTKey,TValue的键”,内容详细,步骤清晰,细节处理妥当,希望这篇“C#怎么使用struct类型作为泛型DictionaryTKey,TValue的键”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学
  • C#如何实现折半查找算法 彩票查询
    C#如何实现折半查找算法 彩票查询
    本篇内容主要讲解“C#如何实现折半查找算法”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“C#如何实现折半查找算法”吧!折半查找,也叫二分查找,当在一个数组或集合中查找某个元素时,先定位出中间位置元素
    02-09
  • C#如何实现选择排序 c罗
    C#如何实现选择排序 c罗
    本篇内容主要讲解“C#如何实现选择排序”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“C#如何实现选择排序”吧!选择排序是一种低效的排序算法,大致过程是:遍历数组的每一个元素,先假设0号位置上的元素是最
    02-09
点击排行