sharplib/prof/MemoryPipe.cs
2024-07-17 16:35:23 -07:00

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 );
}
}
}