singrdk/base/Kernel/Singularity/GCTracing.cs

635 lines
23 KiB
C#

////////////////////////////////////////////////////////////////////////////////
//
// Microsoft Research Singularity
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// File: GCTracing.cs
//
// Note: Provides logging facilities for the garbage collector.
// This is intended to be used in both kernel and SIPs, SIPs
// will get a private memory buffer for these entries, separated from the kernel log
//
using System;
using System.Diagnostics;
using System.Text;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
using System.Threading;
using Microsoft.Singularity;
using Microsoft.Singularity.Isal;
using System.GCs;
using System.Collections;
using Microsoft.Singularity.Memory;
using Microsoft.Singularity.V1.Services;
using Microsoft.Singularity.Eventing;
namespace Microsoft.Singularity
{
[CLSCompliant(false)]
[AccessedByRuntime("output to header - methods defined in GCTracing.cpp")]
public class GCProfilerLogger : GCProfiler
{
[CLSCompliant(false)]
public class GCTypeSource : EventSource {
public static GCTypeSource Create(string sourceName, uint typeSize, ulong options) {
uint qos = QualityOfService.PermanentEvents;
if (options != 0) {
qos |= QualityOfService.OOM_BreakOnRecycle;
}
EventingStorage storage = EventingStorage.CreateLocalStorage(qos, typeSize);
if (storage == null) {
return null;
}
GCTypeSource Logger = new GCTypeSource(sourceName,
storage,
ENABLE_ALL_MASK);
if (Logger != null) {
Logger.Register();
}
return Logger;
}
GCTypeSource(string sourceName, EventingStorage storage, uint controlFlags)
:base(sourceName, storage, controlFlags) {}
}
[CLSCompliant(false)]
public class GCEventSource : EventSource {
public static GCEventSource Create(string sourceName, uint typeSize, ulong options) {
uint qos = QualityOfService.RecyclableEvents;
if (options != 0) {
qos |= QualityOfService.OOM_BreakOnRecycle;
}
EventingStorage storage = EventingStorage.CreateLocalStorage(qos, typeSize);
if (storage == null) {
return null;
}
GCEventSource Logger = new GCEventSource(sourceName,
storage,
ENABLE_ALL_MASK);
if (Logger != null) {
Logger.Register();
}
return Logger;
}
GCEventSource(string sourceName, EventingStorage storage, uint controlFlags)
:base(sourceName, storage, controlFlags) {}
}
[CLSCompliant(false)]
[AccessedByRuntime("output to header - methods defined in GCTracing.cpp")]
public class ProfilerBuffer
{
private const int cycleGranularity = 5000000;
public Thread OwningThread;
private ulong lastCycle;
private ulong firstCycle;
#if LEGACY_GCTRACING
[AccessedByRuntime("output to header : defined in GCTracing.cpp")]
[MethodImpl(MethodImplOptions.InternalCall)]
[StackBound(256)]
[NoHeapAllocation]
public static extern unsafe void SetupBuffer(byte * memoryBuffer, ulong bufferSize);
#endif // LEGACY_GCTRACING
//
// ABI entries
//
[AccessedByRuntime("output to header : defined in GCTracing.cpp")]
[MethodImpl(MethodImplOptions.InternalCall)]
[StackBound(256)]
[NoHeapAllocation]
public static extern unsafe void LogGenerations(int maxGeneration, int * generations);
[AccessedByRuntime("output to header : defined in GCTracing.cpp")]
[MethodImpl(MethodImplOptions.InternalCall)]
[StackBound(256)]
[NoHeapAllocation]
public static extern unsafe void LogInterval(ulong Delta);
[AccessedByRuntime("output to header : defined in GCTracing.cpp")]
[MethodImpl(MethodImplOptions.InternalCall)]
[StackBound(256)]
[NoHeapAllocation]
public static extern unsafe void LogAllocation(int threadId, UIntPtr objectAddress, uint stkNo);
[AccessedByRuntime("output to header : defined in GCTracing.cpp")]
[MethodImpl(MethodImplOptions.InternalCall)]
[StackBound(256)]
[NoHeapAllocation]
public static extern unsafe void LogObject(uint ArraySize, UIntPtr * objectParameters);
[AccessedByRuntime("output to header : defined in GCTracing.cpp")]
[MethodImpl(MethodImplOptions.InternalCall)]
[StackBound(256)]
[NoHeapAllocation]
public static extern unsafe void LogRoots(uint ArraySize, UIntPtr * objectRoots);
[AccessedByRuntime("output to header : defined in GCTracing.cpp")]
[MethodImpl(MethodImplOptions.InternalCall)]
[StackBound(256)]
[NoHeapAllocation]
public static extern unsafe void LogType(uint typeId, string typeName);
[AccessedByRuntime("output to header : defined in GCTracing.cpp")]
[MethodImpl(MethodImplOptions.InternalCall)]
[StackBound(256)]
[NoHeapAllocation]
public static extern unsafe void LogStack(uint stackNo, uint typeNo, UIntPtr size, uint stackSize, uint * funcIDs); // 'n'
[AccessedByRuntime("output to header : defined in GCTracing.cpp")]
[MethodImpl(MethodImplOptions.InternalCall)]
[StackBound(256)]
[NoHeapAllocation]
public static extern unsafe void LogFunction(uint funcNo, UIntPtr eip);
public ProfilerBuffer()
{
firstCycle = Processor.CycleCount;
}
//
// Local definitions
//
public void LogTick() {
// Log an entry describing the delta in cycles
ulong nowCycle = Processor.CycleCount;
// Emit a tick-count record only if it's been a while since the last one.
if (nowCycle > lastCycle + cycleGranularity) {
lastCycle = nowCycle;
ProfilerBuffer.LogInterval((ulong) ((lastCycle - firstCycle) / 1000000));
}
}
}
//
// Local variables
//
private const int maxGeneration = 3; // CLR Profiler only handles this many
private int[] generations;
ProfilerBuffer Buffer;
private Hashtable typeTable; // hash of types->ids
private uint nextTypeNo; // hand out IDs monotonically
private Hashtable stackTable; // hash of stacks->ids
private uint nextStackNo;
private const int stackSize = 16; // increase for bigger / slower stack traces
private UIntPtr[] stackEips;
private uint[] stackNos;
private Hashtable funcTable; // hash of Eips->ids
private uint nextFuncNo;
private uint[] functionsIDs;
private Thread recurseThread; // prevent recursion when the profiler allocates
private SpinLock allocationLock;
private bool enabled; // profiling is enabled?
private ulong options;
private const uint tempBufferSize = 1024;
private UIntPtr[] tempGCBuffer;
private uint tempGCBufferEntries;
#if LEGACY_GCTRACING
private ulong bufferSize;
private static unsafe byte * bufferMemory;
#else // LEGACY_GCTRACING
private GCTypeSource typeSource;
private GCEventSource EventSource;
[AccessedByRuntime("referenced from Monitoring.cpp")]
private static unsafe UIntPtr TypeStorageHandle = 0;
[AccessedByRuntime("referenced from Monitoring.cpp")]
private static unsafe UIntPtr StorageHandle = 0;
#endif // LEGACY_GCTRACING
internal GCProfilerLogger()
{
#if SINGULARITY_KERNEL
// Initialize spinlock
allocationLock = new SpinLock(SpinLock.Types.GCTracing);
#endif
}
internal static GCProfilerLogger CurrentProfiler = null;
// The public API for a typical client app.
static public void StartProfiling()
{
// Query the diagnosis service whether the GC profiling is enabled
// This allows setting from the kernel debugger the buffer sizes
// for both kernel and SIP profiling.
// Note, these are controlled independently, the implementation
// of GCProfileSettings is different between kernel and SIP
if (CurrentProfiler != null) {
//
// The profiler has been set already. No second attempt is allowed
//
return;
}
unsafe{
int result;
ulong defaultMemorySize = 0;
ulong Options = 0;
result = DiagnosisService.GCProfileSettingsImpl(
out defaultMemorySize,
out Options
);
if ((result == 0) && (defaultMemorySize > 0)) {
CurrentProfiler = new GCProfilerLogger();
CurrentProfiler.Initialize(defaultMemorySize, Options);
GC.SetProfiler(CurrentProfiler);
DebugStub.WriteLine("GC Profiling started");
}
}
}
// Initialization, prior to attempting to set this profiler into the GC. It's
// inappropriate to do this stuff inside a constructor.
internal void Initialize(ulong Size, ulong Flags)
{
options = Flags;
typeTable = new Hashtable();
stackTable = new Hashtable();
funcTable = new Hashtable();
stackEips = new UIntPtr[stackSize];
stackNos = new uint[stackSize];
generations = new int[maxGeneration];
functionsIDs = new uint[stackSize];
Buffer = new ProfilerBuffer();
tempGCBuffer = new UIntPtr[tempBufferSize];
tempGCBufferEntries = 0;
#if LEGACY_GCTRACING
bufferSize = Size;
unsafe{
// Allocate the memory indicated as parameter, as nonGC
#if SINGULARITY_KERNEL
UIntPtr pages = Microsoft.Singularity.Memory.MemoryManager.PagesFromBytes(bufferSize);
bufferMemory = (byte *)Microsoft.Singularity.Memory.MemoryManager.KernelAllocate(
pages, null, 0, System.GCs.PageType.NonGC).ToPointer();
#else // !SINGULARITY_KERNEL
bufferMemory = (byte *)PageTableService.Allocate(bufferSize);
#endif //SINGULARITY_KERNEL
if (bufferMemory != null) {
// When we set this, we are no longer single-threaded:
ProfilerBuffer.SetupBuffer(bufferMemory, bufferSize);
this.enabled = true;
}
}
#else // LEGACY_GCTRACING
typeSource = GCTypeSource.Create("GC.TypeDefinitions", (uint)Size, options);
EventSource = GCEventSource.Create("GC.Events", (uint)Size, options);
if ((typeSource == null) || (EventSource == null)) {
typeSource = null;
EventSource = null;
this.enabled = false;
} else {
TypeStorageHandle = typeSource.Storage.GetHandle();
StorageHandle = EventSource.Storage.GetHandle();
this.enabled = true;
}
#endif // LEGACY_GCTRACING
}
// the API by which our base class calls us
protected override void Shutdown()
{
// This is presumed to be called when the process is single-threaded, since
// the entire GC heap is shutting down.
if (enabled) {
enabled = false;
}
}
protected override void PreGC(int generation)
{
try {
// Take ownership of the buffer to prevent mutating threads from
// interleaving with us.
DebugStub.Assert(Buffer.OwningThread == null);
Buffer.OwningThread = Thread.CurrentThread;
Buffer.LogTick();
if (generation >= maxGeneration) {
generation = maxGeneration-1;
}
generations[generation]++;
unsafe{
fixed (int * ptr = &generations[0]) {
ProfilerBuffer.LogGenerations(maxGeneration, ptr);
}
}
}
catch (Exception) {
enabled = false;
throw;
}
}
// A GC has finished. The world is in a sane place, except that we might not
// have started up all the mutator threads if this is a StopTheWorld collection.
protected override void PostGC()
{
try {
// emit the fact a GC has happened, including the state of the heap.
// TODO: have another function to log the tick count here to estimate the
// time spent in GC too.
Buffer.LogTick();
// We should have an empty buffer, meaning we completed logging from the
// previous operation while entering this code.
DebugStub.Assert(tempGCBufferEntries == 0);
ScanRoots();
unsafe{
fixed (UIntPtr * ptr = &tempGCBuffer[0]) {
ProfilerBuffer.LogRoots(tempGCBufferEntries, ptr);
tempGCBufferEntries = 0;
}
}
// Write all the reachability graph of the heap
ScanObjects();
// Once we have finished writing everything, we can allow mutator threads to
// share access to the fileBuffer with their own consistent entries.
DebugStub.Assert(Buffer.OwningThread == Thread.CurrentThread);
Buffer.OwningThread = null;
}
catch (Exception) {
enabled = false;
throw;
}
}
// In the list of roots, we have found another object
protected override void ScanOneRoot(UIntPtr objectAddress)
{
if (objectAddress != 0) {
//
// If we filled in the temporary buffer, log it and resume
// from the beginning.
if (tempGCBufferEntries >= tempBufferSize) {
unsafe {
fixed (UIntPtr * ptr = &tempGCBuffer[0]) {
ProfilerBuffer.LogRoots(tempGCBufferEntries, ptr);
tempGCBufferEntries = 0;
}
}
}
tempGCBuffer[ tempGCBufferEntries++ ] = (UIntPtr)objectAddress;
}
}
// In the heap reachability graph, we are starting to dump a new object
protected override void StartScanOneObject(UIntPtr objectAddress, Type type, UIntPtr size)
{
DebugStub.Assert(tempGCBufferEntries == 0);
tempGCBuffer[ tempGCBufferEntries++ ] = (UIntPtr)objectAddress;
tempGCBuffer[ tempGCBufferEntries++ ] = (UIntPtr)GetTypeId(type);
tempGCBuffer[ tempGCBufferEntries++ ] = (UIntPtr)size;
}
// This is one of the references that the current object holds.
protected override void ScanOneObjectField(UIntPtr objectAddress)
{
if ((objectAddress != 0) && (tempGCBufferEntries < tempBufferSize)) {
tempGCBuffer[ tempGCBufferEntries++ ] = (UIntPtr)objectAddress;
}
}
// We are finished scanning one object
protected override void EndScanOneObject()
{
unsafe{
fixed (UIntPtr * ptr = &tempGCBuffer[0]) {
ProfilerBuffer.LogObject(tempGCBufferEntries, ptr);
tempGCBufferEntries = 0;
}
}
}
// Create a log entry for the allocation that just occurred on this thread.
protected override void Allocation(UIntPtr objAddr, Type type, UIntPtr size)
{
bool iflag;
// We cannot recurse inside an Allocation notification, or we will simply
// blow the stack on the first entry. Also, we don't want to log allocations
// that occur as a consequence of logging the state of the GC heap -- though
// we could support that if we chose to.
if (enabled &&
recurseThread != Thread.CurrentThread && // recurse?
Buffer.OwningThread != Thread.CurrentThread) { // GC logging?
iflag = Processor.DisableLocalPreemption();
allocationLock.Acquire();
try {
DebugStub.Assert(recurseThread == null);
recurseThread = Thread.CurrentThread;
Buffer.LogTick();
uint stackSize = Isa.GetStackReturnAddresses(stackEips);
uint stkNo = 0;
if (stackSize > 0) {
stkNo = GetStackId(type, size, stackEips, stackSize);
}
ProfilerBuffer.LogAllocation(Thread.CurrentThread.GetThreadId(), objAddr, stkNo);
}
finally {
recurseThread = null;
allocationLock.Release();
Processor.RestoreLocalPreemption(iflag);
}
}
}
// The rest of our implementation details:
private uint GetStackId(Type type, UIntPtr size, UIntPtr[] stackEips, uint stackSize)
{
// First make sure that we have a type record for the object being
// instantiated at this stack.
uint typeNo = GetTypeId(type);
DebugStub.Assert(stackSize <= stackEips.Length);
// Then, make sure we have a function record for each Eip in the stack. Of course
// we don't know when a bunch of Eips map to different offsets in the same function.
// So make a function for each unique Eip & fix it up in the post-processing.
// Hopefully there aren't too many unique callsites in each method.
ulong hash = typeNo; // perhaps "typeNo ^ size" ?
for (int i = 0; i < stackSize; i++) {
// Map the individual Eips into their corresponding functions
stackNos[i] = GetFunctionId(stackEips[i]);
hash = (hash << 11) + (hash ^ stackNos[i]);
}
// TODO: Note that we will statistically map some distinct stacks into the same
// stack if they have the same hash.
object o = stackTable[hash];
if (o != null) {
return (uint) o;
}
// It's a novel stack. Note that we embed the size into the stack, but we
// don't include the size in the hash. There's a technique for sharing
// prefixes of stacks that could be explored here to get more accurate profiles
// without huge stack expansion.
// TODO: consider the above.
uint stackNo = nextStackNo++;
// Stacks are emitted backwards, presumably to support common prefixes better.
for (int i = (int)stackSize - 1; i >= 0; i--) {
functionsIDs[stackSize - 1 - i] = stackNos[i];
}
unsafe{
fixed (uint * ptr = &functionsIDs[0]) {
ProfilerBuffer.LogStack(stackNo, typeNo, size, stackSize, ptr);
}
}
stackTable[hash] = stackNo;
return stackNo;
}
private uint GetFunctionId(UIntPtr eip)
{
// Given the EIP of a Function, make sure the function has been defined. Since we
// don't have enough runtime information to map Eips to functions, we must rely on
// post-processing.
object o = funcTable[eip];
if (o != null) {
return (uint) o;
}
uint funcNo = nextFuncNo++;
ProfilerBuffer.LogFunction(funcNo, eip);
funcTable[eip] = funcNo;
return funcNo;
}
private uint GetTypeId(Type type)
{
// Given a Type, make sure that it has been defined.
object o = typeTable[type];
if (o != null) {
return (uint) o;
}
uint typeNo = nextTypeNo++;
// Log this entry before putting it into the hashtable, where other threads might
// see it. This means we may have duplicate conflicting entries for the same logical
// type, but this is tolerated by the profiler.
ProfilerBuffer.LogType(typeNo, type.FullName);
typeTable[type] = typeNo;
return typeNo;
}
}
}