Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report -1 for GC.GetGeneration(nongc_obj) and same for GetObjectGeneration #85017

Merged
merged 14 commits into from
Apr 23, 2023
5 changes: 3 additions & 2 deletions src/coreclr/gc/gc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45893,9 +45893,10 @@ unsigned int GCHeap::WhichGeneration (Object* object)
{
uint8_t* o = (uint8_t*)object;
#ifdef FEATURE_BASICFREEZE
if (!((o < g_gc_highest_address) && (o >= g_gc_lowest_address)))
if (GCHeap::IsInFrozenSegment (object))
{
return max_generation;
// Report -1 for objects allocated on NonGC heaps (aka frozen segments)
return -1;
}
#endif //FEATURE_BASICFREEZE
gc_heap* hp = gc_heap::heap_of (o);
Expand Down
2 changes: 1 addition & 1 deletion src/coreclr/gc/gcinterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -984,7 +984,7 @@ void updateGCShadow(Object** ptr, Object* val);
#define GC_CALL_INTERIOR 0x1
#define GC_CALL_PINNED 0x2

// keep in sync with GC_ALLOC_FLAGS in GC.cs
// keep in sync with GC_ALLOC_FLAGS in GC.CoreCLR.cs
enum GC_ALLOC_FLAGS
{
GC_ALLOC_NO_FLAGS = 0,
Expand Down
5 changes: 5 additions & 0 deletions src/coreclr/inc/corerror.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1182,6 +1182,11 @@
<Comment> The runtime cannot be suspened since a suspension is already in progress. </Comment>
</HRESULT>

<HRESULT NumericValue="0x80131389">
<SymbolicName>CORPROF_E_NOT_GC_OBJECT</SymbolicName>
<Comment> This object belongs to a non-gc heap. </Comment>
</HRESULT>

<HRESULT NumericValue="0x80131416">
<SymbolicName>CORSEC_E_POLICY_EXCEPTION</SymbolicName>
<Message>"PolicyException thrown."</Message>
Expand Down
5 changes: 3 additions & 2 deletions src/coreclr/pal/prebuilt/corerror/mscorurt.rc
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ BEGIN
MSG_FOR_URT_HR(FUSION_E_INVALID_NAME) "The given assembly name was invalid."
MSG_FOR_URT_HR(FUSION_E_CACHEFILE_FAILED) "Failed to add file to AppDomain cache."
MSG_FOR_URT_HR(FUSION_E_APP_DOMAIN_LOCKED) "The requested assembly version conflicts with what is already bound in the app domain or specified in the manifest."
MSG_FOR_URT_HR(COR_E_LOADING_REFERENCE_ASSEMBLY) "Reference assemblies cannot be loaded for execution."
MSG_FOR_URT_HR(COR_E_LOADING_REFERENCE_ASSEMBLY) "Reference assemblies cannot not be loaded for execution."
MSG_FOR_URT_HR(COR_E_AMBIGUOUSIMPLEMENTATION) "Ambiguous implementation found."
MSG_FOR_URT_HR(CLDB_E_FILE_BADREAD) "Error occurred during a read."
MSG_FOR_URT_HR(CLDB_E_FILE_BADWRITE) "Error occurred during a write."
Expand Down Expand Up @@ -245,7 +245,7 @@ BEGIN
MSG_FOR_URT_HR(CORDBG_E_CANNOT_RESOLVE_ASSEMBLY) "We failed to resolve assembly given an AssemblyRef token. Assembly may be not loaded yet or not a valid token."
MSG_FOR_URT_HR(CORDBG_E_MUST_BE_IN_LOAD_MODULE) "Must be in context of LoadModule callback to perform requested operation."
MSG_FOR_URT_HR(CORDBG_E_CANNOT_BE_ON_ATTACH) "Requested operation cannot be performed during an attach operation."
MSG_FOR_URT_HR(CORDBG_E_NGEN_NOT_SUPPORTED) "NGEN is not supported."
MSG_FOR_URT_HR(CORDBG_E_NGEN_NOT_SUPPORTED) "NGEN must be supported to perform the requested operation."
EgorBo marked this conversation as resolved.
Show resolved Hide resolved
MSG_FOR_URT_HR(CORDBG_E_ILLEGAL_SHUTDOWN_ORDER) "Trying to shutdown out of order."
MSG_FOR_URT_HR(CORDBG_E_CANNOT_DEBUG_FIBER_PROCESS) "Debugging fiber mode managed process is not supported."
MSG_FOR_URT_HR(CORDBG_E_MUST_BE_IN_CREATE_PROCESS) "Must be in context of CreateProcess callback to perform requested operation."
Expand Down Expand Up @@ -288,6 +288,7 @@ BEGIN
MSG_FOR_URT_HR(CORDBG_E_MISSING_DEBUGGER_EXPORTS) "The debuggee memory space does not have the expected debugging export table."
MSG_FOR_URT_HR(CORDBG_E_DATA_TARGET_ERROR) "Failure when calling a data target method."
MSG_FOR_URT_HR(CORDBG_E_UNSUPPORTED_DELEGATE) "The delegate contains a delegate currently not supported by the API."
MSG_FOR_URT_HR(CORDBG_E_ASSEMBLY_UPDATES_APPLIED) "The operation is not supported because assembly updates have been applied."
MSG_FOR_URT_HR(PEFMT_E_64BIT) "File is PE32+."
MSG_FOR_URT_HR(PEFMT_E_32BIT) "File is PE32"
MSG_FOR_URT_HR(CLR_E_BIND_ASSEMBLY_VERSION_TOO_LOW) "The bound assembly has a version that is lower than that of the request."
Expand Down
1 change: 1 addition & 0 deletions src/coreclr/pal/prebuilt/inc/corerror.h
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@
#define CORDIAGIPC_E_UNKNOWN_MAGIC EMAKEHR(0x1386)
#define CORDIAGIPC_E_UNKNOWN_ERROR EMAKEHR(0x1387)
#define CORPROF_E_SUSPENSION_IN_PROGRESS EMAKEHR(0x1388)
#define CORPROF_E_NOT_GC_OBJECT EMAKEHR(0x1389)
#define CORSEC_E_POLICY_EXCEPTION EMAKEHR(0x1416)
#define CORSEC_E_MIN_GRANT_FAIL EMAKEHR(0x1417)
#define CORSEC_E_NO_EXEC_PERM EMAKEHR(0x1418)
Expand Down
9 changes: 9 additions & 0 deletions src/coreclr/vm/proftoeeinterfaceimpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9141,6 +9141,15 @@ HRESULT ProfToEEInterfaceImpl::GetObjectGeneration(ObjectID objectId,

IGCHeap *hp = GCHeapUtilities::GetGCHeap();

if (hp->IsInFrozenSegment((Object*)objectId))
{
range->generation = (COR_PRF_GC_GENERATION)-1;
range->rangeStart = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can return memory range for the given frozen segments here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can return memory range for the given frozen segments here.

I agree, but I thought that the idea was to not provide support for apis with explicit GC prefix/name/suffix for nongc objects (cc @noahfalk)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, the plan of record is to avoid treating Non-GC objects as if they are on the GC heap, and that also included not treating the non-GC segments as if they are some type of special GC generation. We could smuggle the range here at the cost of blurring the lines and potentially creating some confusion. If profiler vendors said it would be specifically helpful to them I don't think it bends the rules too badly though. From the limited profiler feedback I've heard so far folks wanted to enumerate the Non-GC region boundaries directly and resolve the pointers themsevles offline.

I'm expecting @davmason to broadcast the changes we are doing to profiler vendors and then depending on their feedback we can make adjustments.

range->rangeLength = 0;
range->rangeLengthReserved = 0;
return CORPROF_E_NOT_GC_OBJECT;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning error here makes this more breaking than necessary. Why can't we just return S_OK?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning -1 seemed like it was going to be a breaking change no matter what so I figured we may as well make it more obvious rather than try to minimize it and increase risk that tools break more subtly. However we don't yet have feedback from profiler authors one way or another and this behavior is just a best guess on my part. If you think we'd be better off defaulting to keeping S_OK @jkotas I don't have a strong opinion on it.

Copy link
Member

@VSadov VSadov Apr 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Traditionally (in textbook sense) GC generations imply that genX can be collected without collecting generations X+.

-1 makes impression that any level of GC will collect frozen, while in fact it is the opposite - any generation can be collected without touching frozen.

That seems to imply that frozen objects logically belong to generation > MAX_GEN.
Perhaps 3 or MAXINT could work better that -1 ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you think we'd be better off defaulting to keeping S_OK

It looks odd to me to return failure error code and still try to return meaningful values. I see that there are other profiler APIs that work like that, so it should be fine to return error code. You can ignore my feedback.

Copy link
Member Author

@EgorBo EgorBo Apr 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of agree with @VSadov that e.g. INT_MAX value would be safer, e.g. I already had to apply a hack in code because GC/VM usages of WhichGeneration participate in comparisons so -1 will trigger a different path than previously max_generation did. Although, this is a part of the plan to make this breaking change more visible so 🤷

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps 3 or MAXINT could work better that -1

I have no particular preference between MAXINT and -1. I don't anticipate its going to make a difference myself but I also have no concern changing it. 3 I would not use - in the right context 3 is used as a legitimate generation number and the goal is that nobody should treat this value as if it is a generation.

Copy link
Member

@VSadov VSadov Apr 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are not that many purposes for WhichGeneration other than to answer questions like “can this object be collected before that one?” or “will this object survive GC X?”.

Assigning immortal objects to gen -1 feels like not the most convenient/intuitive.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried INT_MAX and there was only one place that I had to change (some NativeAOT test expecting frozen objects to be in Gen2). Also, GC.Collect(GC.GetGeneration.. is not breaking now as Jan highlighted.
For -1 it becomes a little bit more complicated.

So any strong opinions for keeping -1 ?

Copy link
Member

@VSadov VSadov Apr 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think INT_MAX is the correct value. Even with unforeseen future changes, I do not see anything collecting less often than frozen.

}

uint8_t* pStart;
uint8_t* pAllocated;
uint8_t* pReserved;
Expand Down
49 changes: 49 additions & 0 deletions src/tests/profiler/gc/nongcheap.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Runtime.CompilerServices;
using System.Threading;

namespace Profiler.Tests
{
class NonGCHeapTests
{
static readonly Guid GcAllocateEventsProfilerGuid = new Guid("EF0D191C-3FC7-4311-88AF-E474CBEB2859");

[MethodImpl(MethodImplOptions.NoInlining)]
static void AllocateNonGcHeapObjects()
{
// When this method is invoked, JIT is expected to trigger allocations for these
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might not work for ReadyToRun?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, disabled the test for crossgen

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think that this is correct. Did you see the test actually failing with crossgen?

If I am reading the code correctly, this test should work with crossgen just fine. The string literals referenced by R2R code must be allocated on frozen heap. Otherwise, the tiering from R2R to Tier1 would work poorly - the tiering would be stuck with non-frozen string literals.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, you're absolutely right

// string literals and they're expected to end up in a nongc segment (also known as frozen)
Consume("string1");
Consume("string2");
Consume("string3");
Consume("string4");
Consume("string5");
Consume("string6");
}

[MethodImpl(MethodImplOptions.NoInlining)]
static void Consume(object o) {}

public static int RunTest(String[] args)
{
AllocateNonGcHeapObjects();
Console.WriteLine("Test Passed");
return 100;
}

public static int Main(string[] args)
{
if (args.Length > 0 && args[0].Equals("RunTest", StringComparison.OrdinalIgnoreCase))
{
return RunTest(args);
}

return ProfilerTestRunner.Run(profileePath: System.Reflection.Assembly.GetExecutingAssembly().Location,
testName: "NonGCHeapAllocate",
profilerClsid: GcAllocateEventsProfilerGuid);
}
}
}
21 changes: 21 additions & 0 deletions src/tests/profiler/gc/nongcheap.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworkIdentifier>.NETCoreApp</TargetFrameworkIdentifier>
<OutputType>exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Optimize>true</Optimize>
<!-- This test provides no interesting scenarios for GCStress -->
<GCStressIncompatible>true</GCStressIncompatible>
<!-- The test launches a secondary process and process launch creates
an infinite event loop in the SocketAsyncEngine on Linux. Since
runincontext loads even framework assemblies into the unloadable
context, locals in this loop prevent unloading -->
<UnloadabilityIncompatible>true</UnloadabilityIncompatible>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildProjectName).cs" />
<ProjectReference Include="$(TestSourceDir)Common/CoreCLRTestLibrary/CoreCLRTestLibrary.csproj" />
<ProjectReference Include="../common/profiler_common.csproj" />
<CMakeProjectReference Include="$(MSBuildThisFileDirectory)/../native/CMakeLists.txt" />
</ItemGroup>
</Project>
1 change: 1 addition & 0 deletions src/tests/profiler/native/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ set(SOURCES
eventpipeprofiler/eventpipewritingprofiler.cpp
eventpipeprofiler/eventpipemetadatareader.cpp
gcallocateprofiler/gcallocateprofiler.cpp
nongcheap/nongcheap.cpp
gcbasicprofiler/gcbasicprofiler.cpp
gcprofiler/gcprofiler.cpp
getappdomainstaticaddress/getappdomainstaticaddress.cpp
Expand Down
5 changes: 5 additions & 0 deletions src/tests/profiler/native/classfactory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "eventpipeprofiler/eventpipewritingprofiler.h"
#include "getappdomainstaticaddress/getappdomainstaticaddress.h"
#include "gcallocateprofiler/gcallocateprofiler.h"
#include "nongcheap/nongcheap.h"
#include "gcbasicprofiler/gcbasicprofiler.h"
#include "gcprofiler/gcprofiler.h"
#include "handlesprofiler/handlesprofiler.h"
Expand Down Expand Up @@ -69,6 +70,10 @@ HRESULT STDMETHODCALLTYPE ClassFactory::CreateInstance(IUnknown *pUnkOuter, REFI
{
profiler = new GCAllocateProfiler();
}
else if (clsid == NonGcHeapProfiler::GetClsid())
{
profiler = new NonGcHeapProfiler();
}
else if (clsid == GCBasicProfiler::GetClsid())
{
profiler = new GCBasicProfiler();
Expand Down
74 changes: 74 additions & 0 deletions src/tests/profiler/native/nongcheap/nongcheap.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#include "nongcheap.h"

GUID NonGcHeapProfiler::GetClsid()
{
// {EF0D191C-3FC7-4311-88AF-E474CBEB2859}
GUID clsid = { 0xef0d191c, 0x3fc7, 0x4311, { 0x88, 0xaf, 0xe4, 0x74, 0xcb, 0xeb, 0x28, 0x59 } };
return clsid;
}

HRESULT NonGcHeapProfiler::Initialize(IUnknown* pICorProfilerInfoUnk)
{
Profiler::Initialize(pICorProfilerInfoUnk);

HRESULT hr = S_OK;
if (FAILED(hr = pCorProfilerInfo->SetEventMask2(
COR_PRF_ENABLE_OBJECT_ALLOCATED | COR_PRF_MONITOR_OBJECT_ALLOCATED,
cshung marked this conversation as resolved.
Show resolved Hide resolved
COR_PRF_HIGH_BASIC_GC)))
{
printf("FAIL: ICorProfilerInfo::SetEventMask2() failed hr=0x%x", hr);
return hr;
}

return S_OK;
}

HRESULT STDMETHODCALLTYPE NonGcHeapProfiler::ObjectAllocated(ObjectID objectId, ClassID classId)
{
COR_PRF_GC_GENERATION_RANGE gen;
HRESULT hr = pCorProfilerInfo->GetObjectGeneration(objectId, &gen);

// non-GC objects (same for GC.GetGeneration() API) have generation = -1
if (gen.generation == (COR_PRF_GC_GENERATION)-1)
{
if (!FAILED(hr))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about calling this API to verify that it is a frozen object as well?

Copy link
Member Author

@EgorBo EgorBo Apr 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my understanding we sort of ignore these old Frozen* profiler APIs as we re-branded Frozen as NonGC. For the same reason we introduce a new API to traverse frozen objects and ignore existing EnumModuleFrozenObjects

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignore is probably fine when we were still designing, but it is really lousy from a testing perspective. It should either supposed to work or it doesn't. If it is supposed to work, and we are not testing it, it would be a test hole. If it is not supposed to work, then we should update the code to make it fails. If we wanted to depreciate old APIs in favor of new ones, we should make that clear in the documentation. Either way, something is missing.

{
// We expect GetObjectGeneration to return an error (CORPROF_E_NOT_GC_OBJECT)
// for non-GC objects.
_failures++;
}
_nonGcHeapObjects++;
if (gen.rangeLength != 0 || gen.rangeLengthReserved != 0 || gen.rangeStart != 0)
{
_failures++;
}
}
else if (FAILED(hr))
{
_failures++;
}
return S_OK;
}

HRESULT NonGcHeapProfiler::Shutdown()
{
if (_failures > 0)
{
printf("PROFILER TEST FAILS\n");
}
else if (_nonGcHeapObjects == 0)
{
printf("PROFILER TEST FAILS: non-GC heap objects were not allocated\n");
}
else
{
printf("PROFILER TEST PASSES\n");
}
printf("Non-GC objects allocated: %d\n", (int)_nonGcHeapObjects);
printf("PROFILER TEST PASSES\n");
fflush(stdout);
return S_OK;
}
24 changes: 24 additions & 0 deletions src/tests/profiler/native/nongcheap/nongcheap.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#pragma once

#include "../profiler.h"

class NonGcHeapProfiler : public Profiler
{
public:
NonGcHeapProfiler() : Profiler(),
_nonGcHeapObjects(0),
_failures(0)
{}

static GUID GetClsid();
virtual HRESULT STDMETHODCALLTYPE Initialize(IUnknown* pICorProfilerInfoUnk);
virtual HRESULT STDMETHODCALLTYPE ObjectAllocated(ObjectID objectId, ClassID classId);
virtual HRESULT STDMETHODCALLTYPE Shutdown();

private:
std::atomic<int> _nonGcHeapObjects;
std::atomic<int> _failures;
};