Skip to content

Commit 9239376

Browse files
authored
[Profiler] Cleanup generic method frame encoding and namespaceless types (#8961)
## Summary of changes Ensure that no '.' is added when there is no namespace and provide full type name in generics ## Reason for change Weird `<.<Foo>>` or `.Bar` type names in frames ## Implementation details - Fix namespaceless case - Reuse helper to get full type names in generics ## Test coverage Fix regex and add dedicated tests for better coverage ## Other details <!-- Fixes #{issue} --> <!-- ⚠️ Note: Where possible, please obtain 2 approvals prior to merging. Unless CODEOWNERS specifies otherwise, for external teams it is typically best to have one review from a team member, and one review from apm-dotnet. Trivial changes do not require 2 reviews. MergeQueue is NOT enabled in this repository. If you have write access to the repo, the PR has 1-2 approvals (see above), and all of the required checks have passed, you can use the Squash and Merge button to merge the PR. If you don't have write access, or you need help, reach out in the #apm-dotnet channel in Slack. -->
1 parent aa67bbf commit 9239376

6 files changed

Lines changed: 142 additions & 26 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// <copyright file="GlobalStruct.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2022 Datadog, Inc.
4+
// </copyright>
5+
6+
#pragma warning disable SA1401 // Fields should be private
7+
8+
9+
// This value type is deliberately declared outside of any namespace: when it is given as the type
10+
// argument of a generic method, the |fg: part of the frame must not be prefixed by a '.' standing
11+
// for the empty namespace.
12+
internal struct GlobalStruct
13+
{
14+
public int Member;
15+
}
16+
17+
#pragma warning restore SA1401 // Fields should be private

profiler/src/Demos/Samples.Computer01/MethodsSignature.cs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Collections.Generic;
88
using System.Runtime.CompilerServices;
99
using System.Threading;
10+
using System.Threading.Tasks;
1011

1112
#pragma warning disable SA1402 // File may only contain a single type
1213
#pragma warning disable SA1401 // Fields should be private
@@ -22,7 +23,58 @@ public override void OnProcess()
2223
// because at least one of each type will be sampled.
2324
// Allocations beyond 100 KB would be fine except that we could not test
2425
// the .NET Framework runtime
25-
TriggerExceptions();
26+
WithResult().GetAwaiter().GetResult();
27+
}
28+
29+
// Same shape as BuggyBits ProductsController.WithResult() / GetSyncOverAsync(): the exception
30+
// is thrown from the synchronous segment of an async method, so the compiler-generated state
31+
// machines and AsyncMethodBuilderCore.Start<TStateMachine> are on the stack when it is
32+
// captured. Each state machine is a type nested in MethodsSignature: its name must keep both
33+
// the namespace and the enclosing type.
34+
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
35+
private async Task<int> WithResult()
36+
{
37+
return await GetSyncOverAsync();
38+
}
39+
40+
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
41+
private async Task<int> GetSyncOverAsync()
42+
{
43+
// nothing is awaited before this call: the whole chain below runs in the synchronous
44+
// segment of the state machine, i.e. under AsyncMethodBuilderCore.Start<...>
45+
RunStateMachine();
46+
47+
await Task.Yield();
48+
return 42;
49+
}
50+
51+
// The state machines generated by the compiler for the two methods above are structs in release
52+
// builds but classes in debug builds. In the latter case, the shared canonical instantiation of
53+
// Start<TStateMachine> is used, so the exact state machine type is not available to the profiler.
54+
// Driving our own state machine, always a struct, gives the same frames in both configurations.
55+
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
56+
private void RunStateMachine()
57+
{
58+
var builder = AsyncTaskMethodBuilder.Create();
59+
var stateMachine = new SyncOverAsyncStateMachine { Owner = this };
60+
61+
// no need to complete the builder: nothing awaits its task and the exceptions thrown by
62+
// the chain below are caught before they could escape MoveNext()
63+
builder.Start(ref stateMachine);
64+
}
65+
66+
private struct SyncOverAsyncStateMachine : IAsyncStateMachine
67+
{
68+
public MethodsSignature Owner;
69+
70+
public void MoveNext()
71+
{
72+
Owner.TriggerExceptions();
73+
}
74+
75+
public void SetStateMachine(IAsyncStateMachine stateMachine)
76+
{
77+
}
2678
}
2779

2880
// call methods with different signatures
@@ -132,6 +184,13 @@ private void ThrowGenericMethod1<T>(T element)
132184
}
133185
else
134186
if (element is MyStruct)
187+
{
188+
GlobalStruct gs;
189+
gs.Member = 42;
190+
ThrowGenericMethod1(gs);
191+
}
192+
else
193+
if (element is GlobalStruct)
135194
{
136195
ThrowGenericMethod1("this");
137196
}

profiler/src/ProfilerEngine/Datadog.Profiler.Native/FrameStore.cpp

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -609,12 +609,13 @@ std::tuple<ULONG, std::string, std::string, mdTypeDef> FrameStore::GetMethodName
609609
}
610610
else // normal namespace.type case
611611
{
612+
// a type declared outside of any namespace has no namespace: don't prefix it with a '.'
612613
if (!ns.empty())
613614
{
614-
builder << ns;
615+
builder << ns << ".";
615616
}
616617

617-
builder << "." << typeName;
618+
builder << typeName;
618619
}
619620

620621
if (i < genericParametersCount - 1)
@@ -1117,30 +1118,17 @@ std::pair<std::string, std::string> FrameStore::GetManagedTypeName(ICorProfilerI
11171118
return std::make_pair("", "T");
11181119
}
11191120

1120-
IMetaDataImport2* pMetadata;
1121-
hr = pInfo->GetModuleMetaData(moduleId, ofRead, IID_IMetaDataImport2, reinterpret_cast<IUnknown**>(&pMetadata));
1121+
ComPtr<IMetaDataImport2> pMetadata;
1122+
hr = pInfo->GetModuleMetaData(moduleId, ofRead, IID_IMetaDataImport2, reinterpret_cast<IUnknown**>(pMetadata.GetAddressOf()));
11221123
if (FAILED(hr))
11231124
{
11241125
return std::make_pair("", "T");
11251126
}
11261127

1127-
std::string typeName = GetTypeNameFromMetadata(pMetadata, mdTypeToken);
1128-
pMetadata->Release();
1129-
if (typeName.empty())
1130-
{
1131-
return std::make_pair("", "T");
1132-
}
1133-
1134-
// look for the namespace
1135-
auto const pos = typeName.find_last_of('.');
1136-
if (pos == std::string::npos)
1137-
{
1138-
// no namespace
1139-
return std::make_pair("", std::move(typeName));
1140-
}
1141-
1142-
// need to split to get the namespace and type name
1143-
return std::make_pair(typeName.substr(0, pos), typeName.substr(pos + 1));
1128+
// the namespace and the enclosing types are not part of the metadata name of a nested type
1129+
// (such as the state machine generated for an async method): GetTypeWithNamespace() rebuilds
1130+
// them and, for a type that is not nested, splits the namespace from the type name
1131+
return GetTypeWithNamespace(pMetadata.Get(), mdTypeToken);
11441132
}
11451133

11461134
// use Peter Sollich way in ClrProfiler to parse the binary signature

profiler/test/Datadog.Profiler.IntegrationTests/Helpers/EnvironmentHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ private string GetNativeLoaderPath()
371371
if (!IsRunningInCi())
372372
{
373373
// native loader output folder
374-
var binFolder = Path.Combine(GetSolutionDirectory(), "native-bin", "Datadog.Trace.ClrProfiler.Native", "bin");
374+
var binFolder = Path.Combine(GetSolutionDirectory(), "artifacts", "native-bin", "Datadog.Trace.ClrProfiler.Native");
375375

376376
return GetOS() switch
377377
{

profiler/test/Datadog.Profiler.IntegrationTests/Helpers/StackFrame.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public StackFrame(string rawStackFrame)
2828
public StackFrame(string rawStackFrame, string filename, long line)
2929
{
3030
// |lm:Datadog.Demos.ExceptionGenerator |ns:ExceptionGenerator |ct:ExceptionsProfilerTestScenario |fn:Throw1_2
31-
var match = Regex.Match(rawStackFrame, @"^\|lm:(?<module>.*) \|ns:(?<namespace>.*) \|ct:(?<type>.*) \|cg:(?<typeAdorn>.*) \|fn:(?<function>.*) \|fg:(?<functionArdorn>.*) \|sg:(?<signature>.*)$");
31+
var match = Regex.Match(rawStackFrame, @"^\|lm:(?<module>.*) \|ns:(?<namespace>.*) \|ct:(?<type>.*) \|cg:(?<typeAdorn>.*) \|fn:(?<function>.*) \|fg:(?<functionAdorn>.*) \|sg:(?<signature>.*)$");
3232

3333
if (!match.Success)
3434
{

profiler/test/Datadog.Profiler.IntegrationTests/Signature/SignatureTest.cs

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,17 @@ public SignatureTest(ITestOutputHelper output)
2323

2424
[TestAppFact("Samples.Computer01", new[] { "net48", "netcoreapp3.1", "net6.0", "net8.0", })] // FIXME: .NET 9 skipping .NET 9 for now
2525
public void ValidateSignatures(string appName, string framework, string appAssembly)
26+
{
27+
CheckExceptionsInProfiles(framework, GetExceptionSamples(appName, framework, appAssembly));
28+
}
29+
30+
[TestAppFact("Samples.Computer01", new[] { "net48", "netcoreapp3.1", "net6.0", "net8.0", })] // FIXME: .NET 9 skipping .NET 9 for now
31+
public void ValidateAsyncStateMachineSignatures(string appName, string framework, string appAssembly)
32+
{
33+
CheckAsyncStateMachineFrames(GetExceptionSamples(appName, framework, appAssembly));
34+
}
35+
36+
private (string Type, string Message, long Count, StackTrace Stacktrace)[] GetExceptionSamples(string appName, string framework, string appAssembly)
2637
{
2738
var runner = new TestApplicationRunner(appName, framework, appAssembly, _output, commandLine: "--scenario 20");
2839
EnvironmentHelper.DisableDefaultProfilers(runner);
@@ -33,8 +44,47 @@ public void ValidateSignatures(string appName, string framework, string appAssem
3344
runner.Run(agent);
3445
Assert.True(agent.NbCallsOnProfilingEndpoint > 0);
3546

36-
var exceptionSamples = SamplesHelper.ExtractExceptionSamples(runner.Environment.PprofDir).ToArray();
37-
CheckExceptionsInProfiles(framework, exceptionSamples);
47+
return SamplesHelper.ExtractExceptionSamples(runner.Environment.PprofDir).ToArray();
48+
}
49+
50+
private static void CheckAsyncStateMachineFrames((string Type, string Message, long Count, StackTrace Stacktrace)[] exceptionSamples)
51+
{
52+
// A state machine is a type nested in the type declaring the async method: the generic
53+
// parameter of Start<TStateMachine> must carry its namespace and its enclosing type, exactly
54+
// like the signature already does. The frame is AsyncMethodBuilderCore.Start on .NET Core
55+
// and AsyncTaskMethodBuilder.Start on .NET Framework.
56+
var frames = exceptionSamples
57+
.SelectMany(sample => Enumerable.Range(0, sample.Stacktrace.FramesCount).Select(i => sample.Stacktrace[i]))
58+
.Distinct()
59+
.ToArray();
60+
61+
// split code to debug more easily outside of LINQ
62+
var startFrames = frames
63+
.Where(frame => frame.Function == "Start")
64+
.Distinct()
65+
.ToArray();
66+
67+
var startAdornments = startFrames
68+
.Select(frame => frame.FunctionAdornment)
69+
.Distinct()
70+
.ToArray();
71+
72+
#if DEBUG
73+
// MethodsSignature drives its own state machine: a struct is never shared between
74+
// instantiations so its exact type is always known, whatever the configuration used to
75+
// build the sample
76+
startAdornments.Should().Contain("<Samples.Computer01.MethodsSignature.SyncOverAsyncStateMachine>");
77+
78+
// the state machines generated by the compiler are structs in release builds but classes in
79+
// debug builds: in the latter case, only the shared canonical instantiation is known (<T0>)
80+
startAdornments.Should().Contain("<T0>");
81+
#else
82+
startAdornments.Should().Contain("<Samples.Computer01.MethodsSignature.<GetSyncOverAsync>d__2>");
83+
startAdornments.Should().Contain("<Samples.Computer01.MethodsSignature.<WithResult>d__1>");
84+
#endif
85+
86+
// regression test to check the "type with empty namespace" bug is fixed
87+
frames.Should().OnlyContain(frame => !frame.FunctionAdornment.StartsWith("<."));
3888
}
3989

4090
private static void CheckExceptionsInProfiles(string framework, (string Type, string Message, long Count, StackTrace Stacktrace)[] exceptionSamples)
@@ -56,6 +106,7 @@ private static void CheckExceptionsInProfiles(string framework, (string Type, st
56106
new StackFrame("|lm:Samples.Computer01 |ns:Samples.Computer01 |ct:GenericClassForValueTypeTest |cg:<System.Int32, System.Boolean> |fn:ThrowOneGenericFromType |fg: |sg:(TKey value)"),
57107
new StackFrame("|lm:Samples.Computer01 |ns:Samples.Computer01 |ct:MethodsSignature |cg: |fn:ThrowGenericMethod2 |fg:<T0, System.Int32, T2, T3> |sg:(T0 key1, System.Int32 value1, System.Int32 value2, T2 key2, T3 key3, System.Collections.Generic.List<System.Int32> listOfTValue)"),
58108
new StackFrame("|lm:Samples.Computer01 |ns:Samples.Computer01 |ct:MethodsSignature |cg: |fn:ThrowGenericMethod1 |fg:<T0> |sg:(T0 element)"),
109+
new StackFrame("|lm:Samples.Computer01 |ns:Samples.Computer01 |ct:MethodsSignature |cg: |fn:ThrowGenericMethod1 |fg:<GlobalStruct> |sg:(GlobalStruct element)"),
59110
new StackFrame("|lm:Samples.Computer01 |ns:Samples.Computer01 |ct:MethodsSignature |cg: |fn:ThrowGenericMethod1 |fg:<Samples.Computer01.MyStruct> |sg:(Samples.Computer01.MyStruct element)"),
60111
new StackFrame("|lm:Samples.Computer01 |ns:Samples.Computer01 |ct:MethodsSignature |cg: |fn:ThrowGenericMethod1 |fg:<T0> |sg:(T0 element)"),
61112
new StackFrame("|lm:Samples.Computer01 |ns:Samples.Computer01 |ct:MethodsSignature |cg: |fn:ThrowGenericMethod1 |fg:<System.Boolean> |sg:(System.Boolean element)"),
@@ -80,6 +131,7 @@ private static void CheckExceptionsInProfiles(string framework, (string Type, st
80131
new StackFrame("|lm:Samples.Computer01 |ns:Samples.Computer01 |ct:GenericClassForValueTypeTest |cg:<System.Int32, System.Boolean> |fn:ThrowOneGenericFromType |fg: |sg:(TVal value)"),
81132
new StackFrame("|lm:Samples.Computer01 |ns:Samples.Computer01 |ct:MethodsSignature |cg: |fn:ThrowGenericMethod2 |fg:<T0, System.Int32, T2, T3> |sg:(T0 key1, System.Int32 value1, System.Int32 value2, T2 key2, T3 key3, System.Collections.Generic.List<System.Int32> listOfTValue)"),
82133
new StackFrame("|lm:Samples.Computer01 |ns:Samples.Computer01 |ct:MethodsSignature |cg: |fn:ThrowGenericMethod1 |fg:<T0> |sg:(T0 element)"),
134+
new StackFrame("|lm:Samples.Computer01 |ns:Samples.Computer01 |ct:MethodsSignature |cg: |fn:ThrowGenericMethod1 |fg:<GlobalStruct> |sg:(GlobalStruct element)"),
83135
new StackFrame("|lm:Samples.Computer01 |ns:Samples.Computer01 |ct:MethodsSignature |cg: |fn:ThrowGenericMethod1 |fg:<Samples.Computer01.MyStruct> |sg:(Samples.Computer01.MyStruct element)"),
84136
new StackFrame("|lm:Samples.Computer01 |ns:Samples.Computer01 |ct:MethodsSignature |cg: |fn:ThrowGenericMethod1 |fg:<T0> |sg:(T0 element)"),
85137
new StackFrame("|lm:Samples.Computer01 |ns:Samples.Computer01 |ct:MethodsSignature |cg: |fn:ThrowGenericMethod1 |fg:<System.Boolean> |sg:(System.Boolean element)"),

0 commit comments

Comments
 (0)