dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.27k stars 4.73k forks source link

Proposal: Make local variables available upon exception at runtime #12698

Open mrlacey opened 5 years ago

mrlacey commented 5 years ago

Summary When executing through VisualStudio, having access to local variables can be invaluable for debugging exceptions. It would be great if there was a way to also make this information available to runtime exceptions, even if that comes at a trade-off with performance.

Detail Exception messages and stack traces are useful but often don't provide enough information to identify the root cause of an exception. Having access to the variables assigned within a method and the variables passed to that method at the time of an exception would help identify the cause of more exceptions and in less time. It's possible to manually track the values of individual variables but doing this for all variables, in all methods, requires writing lots of code and adds noise to the code. The desire behind this proposal is to be able to get this information without having developers write lots of extra code. I'd be willing to trade some performance for access to this information.

What the change may look like Ideally, this information would be as a new property (Locals) on the Exception object and would be a Dictionary<string, object> where the key is the variable name and the value is the variable value. If restrictions make this impossible, I'd settle for Dictionary<string, string> where the value is the serialized variable contents.

I could then run code like this:

private bool DoSomething(string name, int rank)
{
    var someValue = 42;
    bool result = false;

    try
    {
        var otherValue = new System.Text.StringBuilder();

        for (int i = 0; i < rank; i++)
        {
            // Some other code that throws an error
        }
    }
    catch (Exception exc)
    {
        foreach (var local in exc.Locals)
        {
            Console.WriteLine($"{local.Key}: {local.Value}");
        }
    }

    return result;
}

DoSomething("matt", 1);

and see console output like:

name: matt
rank: 1
someValue: 42
result: false
otherValue: 
i: 0

Consequences of this change Making this possible will mean removing some performance/garbage collection optimizations that prevent access to the required information. Rather than forcing a drop in performance on all applications, this new functionality should be made available only when enabled during compilation. (via a new switch?)--I imagine such a variation in behavior would be similar to what is done when debugging through VS. This would enable enhanced debugging information for those applications willing (and able) to make the trade in performance. If the change in behavior was not enabled, the new property would always be null.

dotMorten commented 5 years ago

What if those locals are out of scope or disposed once the exception hits? If there's a perf impact I'd prefer this would be opt in only and possibly only in debug builds.

mrlacey commented 5 years ago

What if those locals are out of scope or disposed once the exception hits? If there's a perf impact I'd prefer this would be opt in only and possibly only in debug builds.

Under the VS debugger, apparently, there's extra work that's done to re-scope variables to the method level to make their values available to the local window. Ideally that's what I'd aim for but happy to be advised of any restrictions that prevent this by someone who knows the internals and optimizations better if there are reasons this can't be done. For things like for loop variables where the loop has completed before an exception is thrown, that's not as valuable.

Definitely opt-in only.

Why enforce this only in debug builds? There are plenty of places where I'd want this in release builds. It's "in the wild" where data is hard to predict/recreate that this information is especially valuable, and if performance isn't a priority (or an issue) then why not opt in?

benaadams commented 5 years ago

Were an exception is thrown that method's locals probably usually aren't greatly exciting; except on the odd occasion, what would be more interesting are the locals further up the stack from other methods that called it e.g. why did the method pass null into this method that throws are argument exception because it was passed null.

So for that having the local's of the throwing method wouldn't necessarily help? You'd need a full chained stack dump?

mrlacey commented 5 years ago

So for that having the local's of the throwing method wouldn't necessarily help? You'd need a full chained stack dump?

I hadn't thought about it from this direction.

Having the values passed to each method in the call stack would be valuable so you can see how got to the method where the error was caught. But yes, as you say, the ability to know more about where the exception was thrown, not just where it was caught would be great too.

benaadams commented 5 years ago

Sounds like you more want a way to programmaticallu create a mini-dump file internal to your app (rather than doing it externally) for the call chain?

Which should have all the info you are after (if it was built in Debug mode)

mattwarren commented 5 years ago

This seems related to the new 'variable tracking' work, see https://github.com/dotnet/coreclr/pull/23373 and the design doc for a bit more info. Maybe some of what that doc is proposing would solve some of what you're asking for

svick commented 5 years ago

I wonder if something like this could be built using ClrMD: you would attach it to the current process and then walk the stack frames of the current thread, while inspecting local variables of each frame.

Though it won't work today, because ClrMD doesn't support local variables (https://github.com/Microsoft/clrmd/issues/11). Also, when I tried it, I wasn't able to attach to the current process, but I could be doing something wrong.

sywhang commented 5 years ago

cc @cshung In case you want to comment about Brian's work.

PathogenDavid commented 5 years ago

Also, when I tried it, I wasn't able to attach to the current process, but I could be doing something wrong.

I've been able to get it to work with the following:

int processId;
using (Process self = Process.GetCurrentProcess())
{ processId = self.Id; }

using (DataTarget dataTarget = DataTarget.AttachToProcess(processId, 1000, AttachFlag.Passive))

That being said, it's considered a bad idea and not everything is going to work. (Although personally I was able to walk the stacks of other threads and the heap with seemingly no issue with both .NET Core 3.0 and .NET Framework 4.7.2.)

TahirAhmadov commented 5 years ago

This would be a great idea. All parameters, local variables, and "this" class fields should be captured up and down the stack on exception. This behavior should be optional for obvious performance reasons (ie. debug vs release build). Things like closures can be ignored for simplicity sake - obviously it's a language feature and CLR cannot decompile C# on the fly.

darkguy2008 commented 10 months ago

I'm getting this now in a .NET 8 app:

Local variables and arguments are not available in '[Exception]' call stack frames. To obtain these, configure the debugger to stop when the exception is thrown and rerun the scenario.

Needless to say, it's frustrating because if I enable "All exceptions" then the debugging experience is a mess. I don't know but this behavior wasn't in .NET 7 or older versions, it just naturally worked. What changed in the debugger now that this is not the default? The debugging experience has decreased lately since Microsoft took ownership of the Omnisharp project...

fandrei commented 1 month ago

Also, this problem doesn't happen when "Break When Thrown" is turned on. I wonder what could make this scenarios different? In both cases, it should be the same with regards to variable visibility.