AOT漫谈专题(第二篇): 如何对C# AOT轻量级APM监控
一:背景
1. 讲故事
上一篇我们聊到了如何调试.NET Native AOT 程序,这是研究一个未知领域知识的入口,这篇我们再来看下如何对 Native AOT 程序进行轻量级的APM监控,当然这里的轻量级更多的是对 AOT 中的coreclr内容的挖掘。
二:如何轻量级APM监控
1. 一个简单的例子
用一个不断的往内存中囤积数据的例子来演示吧,然后观察内存的趋势变化,参考代码如下:
internal class Program
{
public static List<string> list = new List<string>();
static void Main(string[] args)
{
Debugger.Break();
Task.Run(() => { Run(); }).Wait();
}
static void Run()
{
for (int i = 0; i < 10000; i++)
{
list.Add(string.Join(", ", Enumerable.Range(0, 20000)));
Thread.Sleep(1);
Console.WriteLine($"i={i}");
}
}
}
这里要注意的是,AOT在 ilc 编译的过程中会采用摇树优化
,言外之意就是 eventpipe跟踪组件
默认是不加入的,这个组件的源码在 src\native\eventpipe
目录下,主要是采用 ipc 的方式与接收者进行通讯。
如果有点懵的话可以简单的理解成这东西是托管版的ETW
,msdn 上也给了一张对比图,参考如下:
说了这么多,言外之意就是要把 eventpipe
保留,方式就是在 csproj 中配置 EventSourceSupport =true 即可,参考如下:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
<EventSourceSupport>true</EventSourceSupport>
</PropertyGroup>
</Project>
接下来用 dotnet cli
进行 public 发布,参考如下:
dotnet publish -r win-x64 -c Debug -o D:\testdump
程序启动好之后,接下来用dotnet-counter
来洞察一下程序的托管堆,CPU 等各种指标,安装方式可以参考链接:https://learn.microsoft.com/zh-cn/dotnet/core/diagnostics/dotnet-counters
PS C:\Users\Administrator\Desktop> dotnet-counters monitor -n Example_21_1
从上图中我们确实也真真切切的看到了进程的各个指标,有些朋友可能会好奇,如果我没开 EventPipe还能挖到这些信息吗?这当然是可以的,不过这就需要你对 coreclr 底层有一定的了解度了。
2. 如何用windbg去挖
首先要知道的是sos 在 aot 中用不了的,但用不了不代表我们没办法,因为 sos 也是从 coreclr 中挖取的数据,那为什么我就不能自己去挖呢?
- 如何挖托管堆数据
sos 是在coreclr 的 gc_heap::generation_table
全局数组中挖取的数据,言外之意我也可以手工的去挖,然后拼凑成托管堆 ,参考如下:
0:009> dx -r1 (*((Example_21_1!WKS::generation (*)[5])0x7ff6fa99ca90))
(*((Example_21_1!WKS::generation (*)[5])0x7ff6fa99ca90)) [Type: WKS::generation [5]]
[0] [Type: WKS::generation]
[1] [Type: WKS::generation]
[2] [Type: WKS::generation]
[3] [Type: WKS::generation]
[4] [Type: WKS::generation]
这个数组就对应着 0代,1代,2代,LOH代,POH代 等数据。
- 如何知道当前机器的内存总量和CPU核心数
这个数据在dump分析过程中非常重要,它可以表示当前机器的robust能力,这些数据在 coreclr 的 g_SystemInfo
和 total_physical_mem
全局变量中,参考如下:
0:009> x Example_21_1!WKS::gc_heap::total_physical_mem
00007ff6`fa9ac1b0 Example_21_1!WKS::gc_heap::total_physical_mem = 0x00000007`f7bbd000
0:009> ? 0x00000007`f7bbd000 /0n1024/0n1024/0n1024
Evaluate expression: 31 = 00000000`0000001f
0:009> dt Example_21_1!g_SystemInfo
+0x000 dwNumberOfProcessors : 0xc
+0x004 dwPageSize : 0x1000
+0x008 dwAllocationGranularity : 0x10000
可以看到当前机器的内存总量是 31G(32G),同时机器的CPU=12。
- 如何挖掘GC触发信息
捕获gc的触发可以有效的获取gc的触发代,触发原因,以及当前的托管堆静态和动态阈值情况,对我们洞察程序非常有帮助,那如何捕获呢?可以对gc触发的必经之路上下一个断点即可,比如:bp Example_21_1!WKS::gc_heap::gc1
。
0:010> bp Example_21_1!WKS::gc_heap::gc1
0:010> g
Breakpoint 0 hit
Example_21_1!WKS::gc_heap::gc1:
00007ff7`e268cd20 488bc4 mov rax,rsp
0:006> k 5
# Child-SP RetAddr Call Site
00 0000001d`6c6febb8 00007ff7`e268ccac Example_21_1!WKS::gc_heap::gc1 [D:\a\_work\1\s\src\coreclr\gc\gc.cpp @ 22250]
01 0000001d`6c6febc0 00007ff7`e26742e0 Example_21_1!WKS::gc_heap::garbage_collect+0x77c [D:\a\_work\1\s\src\coreclr\gc\gc.cpp @ 24332]
02 0000001d`6c6fec70 00007ff7`e26a5a8a Example_21_1!WKS::GCHeap::GarbageCollectGeneration+0x300 [D:\a\_work\1\s\src\coreclr\gc\gc.cpp @ 50529]
03 0000001d`6c6fecd0 00007ff7`e2672d84 Example_21_1!WKS::gc_heap::trigger_gc_for_alloc+0x5a [D:\a\_work\1\s\src\coreclr\gc\gc.cpp @ 18906]
04 (Inline Function) --------`-------- Example_21_1!WKS::gc_heap::try_allocate_more_space+0x14e [D:\a\_work\1\s\src\coreclr\gc\gc.cpp @ 19048]
...
接下来洞察内部的 gc_heap::settings 结构,信息如下:
0:006> dt Example_21_1!WKS::gc_heap::settings 00007ff7`e2c46fb0
+0x000 gc_index : Volatile<unsigned __int64>
+0x008 condemned_generation : 0n0
+0x00c promotion : 0n0
+0x010 compaction : 0n1
+0x014 loh_compaction : 0n0
+0x018 heap_expansion : 0n0
+0x01c concurrent : 0
+0x020 demotion : 0n0
+0x024 card_bundles : 0n1
+0x028 gen0_reduction_count : 0n0
+0x02c should_lock_elevation : 0n0
+0x030 elevation_locked_count : 0n0
+0x034 elevation_reduced : 0n0
+0x038 minimal_gc : 0n0
+0x03c reason : 0 ( reason_alloc_soh )
+0x040 pause_mode : 1 ( pause_interactive )
+0x044 found_finalizers : 0n0
+0x048 background_p : 0n0
+0x04c b_state : 0 ( bgc_not_in_process )
+0x050 entry_memory_load : 0x30
+0x058 entry_available_physical_mem : 0x00000004`1a660000
+0x060 exit_memory_load : 0
0:006> dx -r1 (*((Example_21_1!Volatile<unsigned __int64> *)0x7ff7e2c46fb0))
(*((Example_21_1!Volatile<unsigned __int64> *)0x7ff7e2c46fb0)) [Type: Volatile<unsigned __int64>]
[+0x000] m_val : 0xd [Type: unsigned __int64]
0:006> ? 0x41a660000/0n1024/0n1024/0n1024
Evaluate expression: 16 = 00000000`00000010
从上面的输出中可以看到当前GC的信息特别多:0xd次触发,并且是压缩式的回收,触发的是 0代,原因是代满了,当前机器还可使用的内存是 16G。
三:总结
虽然 .NET AOT 越来越成熟,但目前还是不能对 gcheap 进行sos级的分析,暂时只能手工的挖掘整理,不过我相信在 .NET10 或者 11 上应该能够得到完整的支持,毕竟这势不可挡!