Resolve iterators and async methods in stacktrace#14655
Conversation
b09fd14 to
89a01b5
Compare
| return !(mb.IsDefined(typeof(StackTraceHiddenAttribute)) || (mb.DeclaringType?.IsDefined(typeof(StackTraceHiddenAttribute)) ?? false)); | ||
| } | ||
|
|
||
| private static Type ResolveStateMachineMethod(ref MethodBase mb, Type stateMachineType) |
There was a problem hiding this comment.
Why is mb passed by ref but stateMachineType passed as an arg and then returned?
| mb = method as MethodBase ?? mb; | ||
| stateMachineType = mb.DeclaringType; | ||
| break; | ||
| } |
There was a problem hiding this comment.
if (asma != null && asma.StateMachineType == stateMachineType && method is MethodBase matched)
{
mb = matched;
stateMachineType = matched.DeclaringType;
break;
}?
| sb.AppendFormat(CultureInfo.InvariantCulture, " {0} ", word_At); | ||
|
|
||
| Type t = mb.DeclaringType; | ||
| if (typeof(IAsyncStateMachine).IsAssignableFrom(t) || typeof(IEnumerator).IsAssignableFrom(t)) |
There was a problem hiding this comment.
Check for [CompilerGenerated]?
|
Seems a bit strange that we're hiding the MoveNext method for compiler-generated enumerators, but we don't (and presumably wouldn't want to) for others (e.g. the one for list in your example stack trace). Why are the compiler-generated ones special? |
User code mapping; like the line numbers refer to the original source its mapping the method name back to the original source. The statemachine's MoveNext is a post-compile transform to the original source and can be impenetrable if there are overloads and no line numbers - which doesn't help with diagnosing the problem. The List.MoveNext is the original source; whereas this isn't The line number; while correct, certainly isn't in a method called |
Neither does There is a fundamental difference between an exception escaping from: and an exception escaping from: as they're two distinct methods. An exception escaping from the former means it's coming out of the synchronous call to SkipWhileIterator, whereas an exception coming out of the latter is coming from a call at some point later to MoveNext, which of course does exist on the I get that it can help in some situations. But this is why it seems strange to me. If others are ok with the change, fine, but we should fully understand the ramifications. |
Not sure what you mean? Using the full exceptions (with line numbers): Refers to source line 1209 in Or do you mean the odd formatting of generics? Is referring to source line 107 in
Only is so much as System.Collections.Generic.List`1.Enumerator.MoveNextRare() is also lying because its not instead using the Jit's compiler output of VS also lies to you in the same way as its generally more productive. I think I'm missing what you mean? If you are not trying to work out a compiler bug (and you can still do this with the The later you just look at the source; the former you need to crack open ildasm or similar to even find the method. (Though if you have been programming .NET for a while you might be used to guessing the right method even without line numbers) |
I simply meant it's not in the user's Program.cs file. In one case it's compiler-transformed, in the other it's using a 3rd party library from the user's code... in either case, it's not in the user's source.
Given an iterator: public IEnumerable<int> FooAsync()
{
yield return 1;
throw new Exception();
}when I call: IEnumerable<int> enumerable = FooAsync();no exception will be thrown, because the Exception isn't coming from the synchronous call to FooAsync. It's only when I then do: IEnumerator<int> enumerator = enumerable.GetEnumerator();
enumerator.MoveNext();
enumerator.MoveNext();that the exception will be thrown. So I'm calling MoveNext and the exception is coming from MoveNext. I'm not again calling |
Maybe poor language choice I'm referring to the cs files as the "user's code" whether its their source, 3rd party or framework - e.g. its something you can directly look at; source step into the non-compiled files etc
Hmm... you may have me there... Is async ok? No one (other than you 😉); is manually calling |
|
I agree with @stephentoub it is a bit confusing. FWIW, the VS debugger does display it like so (removes The difference between async and an iterator is that an async method starts executing immediately when called, so it makes sense to hide the generated code there. |
It is the method where exception happens in the source file; so if you were trying to follow it through its the method you should look at - but yes I can see the disconnect with IEnumerable, if you call Question though; does putting In your example where you immediately throw an exception public IEnumerable<int> FooAsync()
{
throw new Exception();
}It cannot throw an exception (short of C# compiler bug) so |
| Type dt = declaringType.DeclaringType; | ||
| if (dt != null) | ||
| { | ||
| var methods = dt.GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance); |
There was a problem hiding this comment.
I think dt.DeclaredMethods could be slightly cheaper here (don't need non-method members nor methods declared on base types).
There was a problem hiding this comment.
Is an extension on TypeInfo rather than Type also IEnumerable rather than array. Will add BindingFlags.DeclaredOnly
|
e.g. for the Linq example there are two overloads Whereas with this PR they would show as |
|
This may not sit well with others, but what about showing both methods, e.g. rendering it as something like: |
Seems a fair comprimise |
|
Output with that change |
|
@stephentoub so since .... redacted ... |
|
@benaadams, I'd suggest that you limit PRs to a single purpose. There's a tendency here to push for additional changes that then slow the progress of the original PR. |
e9b81d8 to
b84a0cd
Compare
|
K, not included that change |
Thanks. Let's work on getting this change through, and if folks agree on it, then your subsequent one-line change can be debated on its own merits without tying it to debate on this one. |
b84a0cd to
5c64d20
Compare
|
Updated summary and squashed |
| return !(mb.IsDefined(typeof(StackTraceHiddenAttribute)) || (mb.DeclaringType?.IsDefined(typeof(StackTraceHiddenAttribute)) ?? false)); | ||
| } | ||
|
|
||
| private static bool TryResolveStateMachineMethod(ref MethodBase mb, ref Type declaringType) |
There was a problem hiding this comment.
I don't find anything particularly wrong with it, but may I suggest inverting the if statement to reduce the nesting? I think it will help readability, but I would like to hear your option.
private static bool TryResolveStateMachineMethod(ref MethodBase mb, ref Type declaringType)
{
Debug.Assert(mb != null);
Debug.Assert(declaringType != null);
Type dt = declaringType.DeclaringType;
if (dt == null)
return false;
var methods = dt.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly);
if(methods == null)
return false;
foreach (var method in methods)
{
var asma = method.GetCustomAttribute<StateMachineAttribute>();
if (asma != null && asma.StateMachineType == declaringType && method is MethodBase matched)
{
mb = matched;
declaringType = matched.DeclaringType;
return true;
}
}
return false;
}3f45055 to
e9a7ee9
Compare
|
Rebased to fix merge clash with "Remove Edi boundary for async" #15781 For async Pre Post |
|
For iterators Pre Post Code Details```csharp class Program { static void Main(string[] args) { try { foreach (var i in Range(0, 5)) { Console.WriteLine(i); } } catch (Exception e) { Console.WriteLine(e); } }} |
| } | ||
| catch | ||
| { | ||
| // Linq.Expression compiled functions can throw when checking attributes |
There was a problem hiding this comment.
What does it throw? Is that a bug?
There was a problem hiding this comment.
Don't need this; dynamic expressions have no declaring type so it wouldn't get here; and the other issues (benaadams/Ben.Demystifier#31, benaadams/Ben.Demystifier#20) are full framework bugs.
20618a1 to
1d83cdd
Compare
|
@dotnet-bot test Windows_NT x64 Checked corefx_baseline |
|
Failed "Ubuntu arm Cross Debug Innerloop Build" not a CI test anymore? #15959 |
|
no more feedback, I'm happy 👍 |
|
👍 @karelz I don't see anything on the PR that would allow me to mark Approve. |
|
Thanks @tmat -- on the top - "Files Changed" - right side top "Review changes" - "Approve" option. |
|
@dotnet-bot test CentOS7.1 x64 Release Innerloop Build and Test |
|
|
|
CentOS7.1 and Tizen armel legs are failing all over other PRs as well, so we are ready for merge I guess :) |
|
@karelz Oh. OK. see it now. That's not very discoverable :( |
|
@benaadams - thanks for all your work on this, I know it was a bit of slog but its a very nice improvement for .NET! |
Allows a developer to determine *correct* overload called for *async* and *iterators*, from a stacktrace. Matches the compiler generated containing `Type` to the source method using: `method.DeclaringType == StateMachineAttribute.StateMachineType` attribute to allow the stack trace to resolve the mangled method name to the correct overload. Also appends the statemachine method name with `+` for iterators. Resolves: dotnet/corefx#24627
|
netfx-port-consider? |
Allows a developer to determine correct overload called for async and iterators, from a stacktrace.
Matches the complier generated containing
Typeto the source method using:method.DeclaringType == StateMachineAttribute.StateMachineTypeattribute to allow the stack trace to resolve the mangled method name to the correct overload. Also appends the statemachine method name with+for iterators (addressing #14655 (comment)).Changes:
async
Instead of the current:
iterators
Instead of the current:
Builds on the other PRs for improving async stacktrace readability:
Which removed the async noise:
Resolves: https://github.com/dotnet/corefx/issues/24627
PTAL @stephentoub @tmat @noahfalk @aelij