394 lines
11 KiB
C#
394 lines
11 KiB
C#
using Microsoft.Diagnostics.NETCore.Client;
|
|
using Microsoft.Diagnostics.Tracing;
|
|
using Microsoft.Diagnostics.Tracing.Etlx;
|
|
using Microsoft.Diagnostics.Tracing.Parsers;
|
|
using Microsoft.Diagnostics.Tracing.Parsers.Clr;
|
|
using Microsoft.Diagnostics.Tracing.Parsers.Kernel;
|
|
using Microsoft.Diagnostics.Tracing.Session;
|
|
using ProfilerHelpers;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Diagnostics.Tracing;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using TraceKeywords = Microsoft.Diagnostics.Tracing.Parsers.ClrTraceEventParser.Keywords;
|
|
|
|
namespace Tracing
|
|
{
|
|
public class MemoryPipe
|
|
{
|
|
private readonly DiagnosticsClient _client;
|
|
private readonly PerProcessProfilingState _processes;
|
|
|
|
// because we are not interested in self monitoring
|
|
private readonly int _currentPid;
|
|
|
|
private int _started = 0;
|
|
|
|
Thread _thread;
|
|
|
|
public MemoryPipe()
|
|
{
|
|
|
|
_currentPid = Process.GetCurrentProcess().Id;
|
|
|
|
_client = new DiagnosticsClient( _currentPid );
|
|
|
|
_processes = new();
|
|
|
|
|
|
var threadStart = new ThreadStart( () => Start( true ) );
|
|
_thread = new Thread( threadStart );
|
|
_thread.Start();
|
|
|
|
|
|
}
|
|
|
|
//public async Task StartAsync( bool allAllocations )
|
|
public void Start( bool allAllocations )
|
|
{
|
|
if( Interlocked.CompareExchange( ref _started, 1, 0 ) == 1 )
|
|
{
|
|
throw new InvalidOperationException( "Impossible to start profiling more than once." );
|
|
}
|
|
|
|
log.info( $"Starting MemoryPipe on thread {Thread.CurrentThread.ManagedThreadId}" );
|
|
|
|
var providers = new List<EventPipeProvider>()
|
|
{
|
|
new EventPipeProvider("Microsoft-Windows-DotNETRuntime",
|
|
EventLevel.Verbose, (long)(
|
|
TraceKeywords.GC |
|
|
TraceKeywords.Contention |
|
|
TraceKeywords.Debugger |
|
|
TraceKeywords.Exception |
|
|
TraceKeywords.GCAllObjectAllocation |
|
|
TraceKeywords.GCSampledObjectAllocationHigh |
|
|
TraceKeywords.GCSampledObjectAllocationLow |
|
|
TraceKeywords.Security |
|
|
TraceKeywords.Threading |
|
|
TraceKeywords.Type |
|
|
TraceKeywords.TypeDiagnostic |
|
|
TraceKeywords.WaitHandle |
|
|
TraceKeywords.All
|
|
) )
|
|
};
|
|
|
|
|
|
//await Task.Factory.StartNew( () => {
|
|
using EventPipeSession session = _client.StartEventPipeSession( providers, false );
|
|
|
|
//log.info( $"SetupProviders" );
|
|
//SetupProviders( session, allAllocations );
|
|
var source = new EventPipeEventSource( session.EventStream );
|
|
|
|
|
|
log.info( $"SetupListeners" );
|
|
SetupListeners( source );
|
|
|
|
log.info( $"Run Process" );
|
|
source.Process();
|
|
|
|
log.info( $"Done" );
|
|
//} );
|
|
}
|
|
|
|
private void SetupProviders( TraceEventSession session, bool noSampling )
|
|
{
|
|
// Note: the kernel provider MUST be the first provider to be enabled
|
|
// If the kernel provider is not enabled, the callstacks for CLR events are still received
|
|
// but the symbols are not found (except for the application itself)
|
|
// Maybe a TraceEvent implementation details triggered when a module (image) is loaded
|
|
var success = true;
|
|
|
|
//*
|
|
log.info( $"EnableKernelProvider" );
|
|
success = log.var( session.EnableKernelProvider( KernelTraceEventParser.Keywords.None |
|
|
// KernelTraceEventParser.Keywords.ImageLoad |
|
|
// KernelTraceEventParser.Keywords.Process
|
|
0 ,
|
|
KernelTraceEventParser.Keywords.None
|
|
) );
|
|
log.info( $"EnableKernelProvider {success}" );
|
|
//*/
|
|
|
|
// The CLR source code indicates that the provider must be set before the monitored application starts
|
|
// Note: no real difference between High and Low
|
|
ClrTraceEventParser.Keywords eventsKeyword = noSampling
|
|
? ClrTraceEventParser.Keywords.GCSampledObjectAllocationLow | ClrTraceEventParser.Keywords.GCSampledObjectAllocationHigh
|
|
: ClrTraceEventParser.Keywords.GCSampledObjectAllocationLow
|
|
;
|
|
|
|
log.info( $"EnableProvider" );
|
|
success = log.var( session.EnableProvider(
|
|
ClrTraceEventParser.ProviderGuid,
|
|
TraceEventLevel.Verbose, // this is needed in order to receive GCSampledObjectAllocation event
|
|
(ulong)(
|
|
|
|
eventsKeyword |
|
|
|
|
// required to receive the BulkType events that allows
|
|
// mapping between the type ID received in the allocation events
|
|
ClrTraceEventParser.Keywords.GCHeapAndTypeNames |
|
|
ClrTraceEventParser.Keywords.Type |
|
|
|
|
// events related to JITed methods
|
|
ClrTraceEventParser.Keywords.Jit | // Turning on JIT events is necessary to resolve JIT compiled code
|
|
ClrTraceEventParser.Keywords.JittedMethodILToNativeMap | // This is needed if you want line number information in the stacks
|
|
ClrTraceEventParser.Keywords.Loader | // You must include loader events as well to resolve JIT compiled code.
|
|
|
|
// this is mandatory to get the callstacks in each CLR event payload.
|
|
//ClrTraceEventParser.Keywords.Stack |
|
|
|
|
0
|
|
)
|
|
) );
|
|
log.info( $"EnableProvider {success}" );
|
|
|
|
|
|
// Note: ClrRundown is not needed because only new processes will be monitored
|
|
}
|
|
|
|
private void SetupListeners( EventPipeEventSource source )
|
|
{
|
|
// register for high and low keyword
|
|
// if both are set, each allocation will trigger an event (beware performance issues...)
|
|
//source.Clr.GCSampledObjectAllocation += OnSampleObjectAllocation;
|
|
source.Clr.GCAllocationTick += OnAllocTick;
|
|
|
|
// required to receive the mapping between type ID (received in GCSampledObjectAllocation)
|
|
// and their name (received in TypeBulkType)
|
|
source.Clr.TypeBulkType += OnTypeBulkType;
|
|
|
|
// messages to get callstacks
|
|
// the correlation seems to be as "simple" as taking the last event on the same thread
|
|
source.Clr.ClrStackWalk += OnClrStackWalk;
|
|
|
|
// needed to get JITed method details
|
|
source.Clr.MethodLoadVerbose += OnMethodDetails;
|
|
source.Clr.MethodDCStartVerboseV2 += OnMethodDetails;
|
|
|
|
source.Clr.ContentionLockCreated += OnLockCreated;
|
|
source.Clr.ContentionStart += OnLockStart;
|
|
source.Clr.ContentionStop += OnLockStop;
|
|
|
|
// get notified when a module is load to map the corresponding symbols
|
|
source.Kernel.ImageLoad += OnImageLoad;
|
|
}
|
|
|
|
private void OnAllocTick( GCAllocationTickTraceData data )
|
|
{
|
|
if( FilterOutEvent( data ) )
|
|
return;
|
|
|
|
//log.info( $"*** RAW: {data}" );
|
|
|
|
//var callStack = data.CallStack();
|
|
//var caller = callStack.Caller;
|
|
|
|
//log.info( $"Call stack {callStack}" );
|
|
|
|
var thisThreadId = Thread.CurrentThread.ManagedThreadId;
|
|
|
|
var typeName = GetProcessTypeName( data.ProcessID, data.TypeID );
|
|
//if( data.TotalSizeForTypeSample >= 85000 )
|
|
{
|
|
var message = $"{data.ThreadID}/{thisThreadId} Alloc {data.ObjectSize, 8} at 0x{data.Address:0000000000000000} in {data.TypeName} (or {GetProcessTypeName( data.ProcessID, data.TypeID )}) ";
|
|
log.info( message );
|
|
}
|
|
GetProcessAllocations( data.ProcessID )
|
|
.AddAllocation(
|
|
data.ThreadID,
|
|
(ulong)data.ObjectSize,
|
|
(ulong)1,
|
|
typeName
|
|
);
|
|
}
|
|
|
|
private void OnLockCreated( ContentionLockCreatedTraceData data )
|
|
{
|
|
log.info( $"{data}" );
|
|
}
|
|
|
|
private void OnLockStart( ContentionStartTraceData data )
|
|
{
|
|
log.info( $"{data}" );
|
|
}
|
|
|
|
private void OnLockStop( ContentionStopTraceData data )
|
|
{
|
|
log.info( $"{data}" );
|
|
}
|
|
|
|
private void OnImageLoad( ImageLoadTraceData data )
|
|
{
|
|
//if( FilterOutEvent( data ) )
|
|
// return;
|
|
|
|
log.info( $"{data}" );
|
|
|
|
//GetProcessMethods( data.ProcessID ).AddModule( data.FileName, data.ImageBase, data.ImageSize );
|
|
|
|
log.info($"{data.ProcessID}.{data.ThreadID} --> {data.FileName}");
|
|
}
|
|
|
|
private void OnMethodDetails( MethodLoadUnloadVerboseTraceData data )
|
|
{
|
|
//if( FilterOutEvent( data ) )
|
|
// return;
|
|
|
|
//log.info( $"{data}" );
|
|
|
|
// care only about jitted methods
|
|
if( !data.IsJitted )
|
|
return;
|
|
|
|
var method = GetProcessMethods( data.ProcessID )
|
|
.Add( data.MethodStartAddress, data.MethodSize, data.MethodNamespace, data.MethodName, data.MethodSignature );
|
|
|
|
//log.info( $"0x{data.MethodStartAddress.ToString( "x12" )} - {data.MethodSize,6} | {data.MethodName}" );
|
|
}
|
|
|
|
private MethodStore GetProcessMethods( int pid )
|
|
{
|
|
if( !_processes.Methods.TryGetValue( pid, out var methods ) )
|
|
{
|
|
methods = new MethodStore( pid );
|
|
_processes.Methods[pid] = methods;
|
|
}
|
|
return methods;
|
|
}
|
|
|
|
|
|
private void OnSampleObjectAllocation( GCSampledObjectAllocationTraceData data )
|
|
{
|
|
if( FilterOutEvent( data ) )
|
|
return;
|
|
|
|
//log.info( $"{data}" );
|
|
|
|
var typeName = GetProcessTypeName( data.ProcessID, data.TypeID );
|
|
//if( data.TotalSizeForTypeSample >= 85000 )
|
|
{
|
|
var message = $"{data.ProcessID}.{data.ThreadID} - {data.TimeStampRelativeMSec,12} | Alloc {GetProcessTypeName( data.ProcessID, data.TypeID )} ({data.TotalSizeForTypeSample})";
|
|
log.info( message );
|
|
}
|
|
GetProcessAllocations( data.ProcessID )
|
|
.AddAllocation(
|
|
data.ThreadID,
|
|
(ulong)data.TotalSizeForTypeSample,
|
|
(ulong)data.ObjectCountForTypeSample,
|
|
typeName
|
|
);
|
|
}
|
|
|
|
private ProcessAllocations GetProcessAllocations( int pid )
|
|
{
|
|
if( !_processes.Allocations.TryGetValue( pid, out var allocations ) )
|
|
{
|
|
allocations = new ProcessAllocations( pid );
|
|
_processes.Allocations[pid] = allocations;
|
|
}
|
|
return allocations;
|
|
}
|
|
|
|
private void OnClrStackWalk( ClrStackWalkTraceData data )
|
|
{
|
|
var message = $"{data.ProcessID}.{data.ThreadID} - {data.TimeStampRelativeMSec,12} | {data.FrameCount} frames";
|
|
log.info(message);
|
|
|
|
var callstack = BuildCallStack( data );
|
|
GetProcessAllocations( data.ProcessID ).AddStack( data.ThreadID, callstack );
|
|
//DumpStack(data);
|
|
}
|
|
|
|
private AddressStack BuildCallStack( ClrStackWalkTraceData data )
|
|
{
|
|
//log.info( $"{data}" );
|
|
|
|
var length = data.FrameCount;
|
|
AddressStack stack = new AddressStack( length );
|
|
|
|
// frame 0 is the last frame of the stack (i.e. last called method)
|
|
for( int i = 0; i < length; i++ )
|
|
{
|
|
stack.AddFrame( data.InstructionPointer( i ) );
|
|
}
|
|
|
|
return stack;
|
|
}
|
|
|
|
private void DumpStack( ClrStackWalkTraceData data )
|
|
{
|
|
if( FilterOutEvent( data ) )
|
|
return;
|
|
|
|
log.info( $"{data}" );
|
|
|
|
var methods = GetProcessMethods( data.ProcessID );
|
|
for( int i = 0; i < data.FrameCount; i++ )
|
|
{
|
|
var address = data.InstructionPointer( i );
|
|
log.info( methods.GetFullName( address ) );
|
|
}
|
|
log.info( $"" );
|
|
}
|
|
|
|
private void OnTypeBulkType( GCBulkTypeTraceData data )
|
|
{
|
|
|
|
ProcessTypeMapping mapping = GetProcessTypesMapping( data.ProcessID );
|
|
for( int currentType = 0; currentType < data.Count; currentType++ )
|
|
{
|
|
GCBulkTypeValues value = data.Values( currentType );
|
|
mapping[value.TypeID] = value.TypeName;
|
|
//log.info( $"{value}" );
|
|
}
|
|
}
|
|
|
|
private ProcessTypeMapping GetProcessTypesMapping( int pid )
|
|
{
|
|
ProcessTypeMapping mapping;
|
|
if( !_processes.Types.TryGetValue( pid, out mapping ) )
|
|
{
|
|
AssociateProcess( pid );
|
|
|
|
mapping = new ProcessTypeMapping( pid );
|
|
_processes.Types[pid] = mapping;
|
|
}
|
|
return mapping;
|
|
}
|
|
|
|
private void AssociateProcess( int pid )
|
|
{
|
|
try
|
|
{
|
|
_processes.Names[pid] = Process.GetProcessById( pid ).ProcessName;
|
|
}
|
|
catch( Exception )
|
|
{
|
|
log.info( $"? {pid}" );
|
|
// we might not have access to the process
|
|
}
|
|
}
|
|
|
|
private string GetProcessTypeName( int pid, ulong typeID )
|
|
{
|
|
if( !_processes.Types.TryGetValue( pid, out var mapping ) )
|
|
{
|
|
return typeID.ToString();
|
|
}
|
|
|
|
var name = mapping[typeID];
|
|
return string.IsNullOrEmpty( name ) ? typeID.ToString() : name;
|
|
}
|
|
|
|
private bool FilterOutEvent( TraceEvent data )
|
|
{
|
|
return data.ThreadID == Thread.CurrentThread.ManagedThreadId;
|
|
//return false; // ( data.ProcessID == _currentPid );
|
|
}
|
|
}
|
|
}
|