您可以记录每次分配。但是您在流程中执行此操作的逻辑是有缺陷的。 .NET Core 支持进程内 ETW 数据收集,这使得记录所有分配事件成为可能。
看
- https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-core-2-2 https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-core-2-2
- https://devblogs.microsoft.com/dotnet/a-portable-way-to-get-gc-events-in-process-and-no-admin-privilege-with-10-lines-of-code-and-动态启用禁用事件的能力/ https://devblogs.microsoft.com/dotnet/a-portable-way-to-get-gc-events-in-process-and-no-admin-privilege-with-10-lines-of-code-and-ability-to-dynamically-enable-disable-events/
从 .NET Core 2.2 开始,现在可以使用以下方式使用 CoreCLR 事件
System.Diagnostics.Tracing.EventListener 类。这些事件
描述诸如 GC、JIT、ThreadPool 等运行时服务的行为
和互操作。这些事件与作为
CoreCLR ETW 提供商。这允许应用程序使用这些
事件或使用传输机制将它们发送到遥测
聚合服务。您可以在中查看如何订阅事件
以下代码示例:
internal sealed class SimpleEventListener : EventListener
{
// Called whenever an EventSource is created.
protected override void OnEventSourceCreated(EventSource eventSource)
{
// Watch for the .NET runtime EventSource and enable all of its events.
if (eventSource.Name.Equals("Microsoft-Windows-DotNETRuntime"))
{
EnableEvents(eventSource, EventLevel.Verbose, (EventKeywords)(-1));
}
}
// Called whenever an event is written.
protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
// Write the contents of the event to the console.
Console.WriteLine($"ThreadID = {eventData.OSThreadId} ID = {eventData.EventId} Name = {eventData.EventName}");
for (int i = 0; i < eventData.Payload.Count; i++)
{
string payloadString = eventData.Payload[i]?.ToString() ?? string.Empty;
Console.WriteLine($"\tName = \"{eventData.PayloadNames[i]}\" Value = \"{payloadString}\"");
}
Console.WriteLine("\n");
}
}
当您启用 GC 事件 (0x1) 而不是 -1 时,应该给出您在进程中诊断所需的所有 GC 暂停时间和 GC 事件。
.NET Core 和 .NET Framework 自古以来就内置了分配采样机制,可以对最多 5 个分配事件/秒 GC_Alloc_Low 或 100 个分配事件/秒 GC_Alloc_High 分配对象进行对象分配指标采样。似乎没有办法获取所有分配事件,但如果您阅读 .NET Core 代码
BOOL ETW::TypeSystemLog::IsHeapAllocEventEnabled()
{
LIMITED_METHOD_CONTRACT;
return
// Only fire the event if it was enabled at startup (and thus the slow-JIT new
// helper is used in all cases)
s_fHeapAllocEventEnabledOnStartup &&
// AND a keyword is still enabled. (Thus people can turn off the event
// whenever they want; but they cannot turn it on unless it was also on at startup.)
(s_fHeapAllocHighEventEnabledNow || s_fHeapAllocLowEventEnabledNow);
}
你发现你可以通过 ETW 获取所有分配事件
- ETW 分配分析必须在进程启动时启用(稍后启用将不起作用)
- GC_Alloc_High 和 GC_Allow_Low 关键字已启用
如果存在记录分配分析数据的 ETW 会话,您可以记录 .NET Core 2.1+ 进程内的所有分配。
Sample:
C>perfview collect c:\temp\perfViewOnly.etl -Merge:true -Wpr -OnlyProviders:"Microsoft-Windows-DotNETRuntime":0x03280095::@StacksEnabled=true
C>AllocTracker.exe
Microsoft-Windows-DotNETRuntime
System.Threading.Tasks.TplEventSource
System.Runtime
Hello World!
Did allocate 24 bytes
Did allocate 24 bytes
Did allocate 24 bytes
Did allocate 76 bytes
Did allocate 76 bytes
Did allocate 32 bytes
Did allocate 64 bytes
Did allocate 24 bytes
... endless loop!
using System;
using System.Diagnostics.Tracing;
namespace AllocTracker
{
enum ClrRuntimeEventKeywords
{
GC = 0x1,
GCHandle = 0x2,
Fusion = 0x4,
Loader = 0x8,
Jit = 0x10,
Contention = 0x4000,
Exceptions = 0x8000,
Clr_Type = 0x80000,
GC_AllocHigh = 0x200000,
GC_HeapAndTypeNames = 0x1000000,
GC_AllocLow = 0x2000000,
}
class SimpleEventListener : EventListener
{
public ulong countTotalEvents = 0;
public static int keyword;
EventSource eventSourceDotNet;
public SimpleEventListener() { }
// Called whenever an EventSource is created.
protected override void OnEventSourceCreated(EventSource eventSource)
{
Console.WriteLine(eventSource.Name);
if (eventSource.Name.Equals("Microsoft-Windows-DotNETRuntime"))
{
EnableEvents(eventSource, EventLevel.Informational, (EventKeywords) (ClrRuntimeEventKeywords.GC_AllocHigh | ClrRuntimeEventKeywords.GC_AllocLow) );
eventSourceDotNet = eventSource;
}
}
// Called whenever an event is written.
protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
if( eventData.EventName == "GCSampledObjectAllocationHigh")
{
Console.WriteLine($"Did allocate {eventData.Payload[3]} bytes");
}
//eventData.EventName
//"BulkType"
//eventData.PayloadNames
//Count = 2
// [0]: "Count"
// [1]: "ClrInstanceID"
//eventData.Payload
//Count = 2
// [0]: 1
// [1]: 11
//eventData.PayloadNames
//Count = 5
// [0]: "Address"
// [1]: "TypeID"
// [2]: "ObjectCountForTypeSample"
// [3]: "TotalSizeForTypeSample"
// [4]: "ClrInstanceID"
//eventData.EventName
//"GCSampledObjectAllocationHigh"
}
}
class Program
{
static void Main(string[] args)
{
SimpleEventListener.keyword = (int)ClrRuntimeEventKeywords.GC;
var listener = new SimpleEventListener();
Console.WriteLine("Hello World!");
Allocate10();
Allocate5K();
GC.Collect();
Console.ReadLine();
}
static void Allocate10()
{
for (int i = 0; i < 10; i++)
{
int[] x = new int[100];
}
}
static void Allocate5K()
{
for (int i = 0; i < 5000; i++)
{
int[] x = new int[100];
}
}
}
}
现在您可以在记录的 ETL 文件中找到所有分配事件。一种方法分配 10 个数组,另一种方法分配 5000 个数组。
我之所以告诉您逻辑有缺陷,是因为即使是像将分配事件打印到控制台这样的简单操作也会分配对象。你知道这最终会怎样吗?
如果您想实现完整的代码路径必须是免费分配的,我猜这是不可能的,因为至少 ETW 事件侦听器需要分配您的事件数据。
您已经达到了目标,但使您的应用程序崩溃了。因此,我会依赖 ETW 并从外部或使用分析器记录数据,出于同样的原因,分析器需要不受管理。
使用 ETW,您可以获得所有分配堆栈和类型信息,这些信息不仅是您报告所需的,而且也是查找有问题的代码片段所需的。关于方法内联还有更多内容,但我想这对于 SO 帖子来说已经足够了。