Coder Perfect

What causes the debugger in Visual Studio to stop evaluating ToString overrides?

Problem

Environment: Visual Studio 2015 RTM. (I haven’t tried older versions.)

I’ve been debugging some Noda Time code recently, and I’ve observed that when I have a local variable of type NodaTime. The “Locals” and “Watch” windows do not appear to call the ToString() override of Instant (one of the key struct types in Noda Time). I see the appropriate representation if I explicitly call ToString() in the watch window, but otherwise I only see:

variableName       {NodaTime.Instant}

This is inconvenient.

If I update the override to return a constant string, the string appears in the debugger, indicating that it can recognize it – it just won’t utilize it in its “regular” form.

I decided to make a little sample program to replicate this locally, and this is what I came up with. (Note that DemoStruct was a class in an earlier version of this post, and DemoClass didn’t exist at all – my error, but it answers some odd remarks now…)

using System;
using System.Diagnostics;
using System.Threading;

public struct DemoStruct
{
    public string Name { get; }

    public DemoStruct(string name)
    {
        Name = name;
    }

    public override string ToString()
    {
        Thread.Sleep(1000); // Vary this to see different results
        return $"Struct: {Name}";
    }
}

public class DemoClass
{
    public string Name { get; }

    public DemoClass(string name)
    {
        Name = name;
    }

    public override string ToString()
    {
        Thread.Sleep(1000); // Vary this to see different results
        return $"Class: {Name}";
    }
}

public class Program
{
    static void Main()
    {
        var demoClass = new DemoClass("Foo");
        var demoStruct = new DemoStruct("Bar");
        Debugger.Break();
    }
}

I now see the following in the debugger:

demoClass    {DemoClass}
demoStruct   {Struct: Bar}

There’s still a little gap if I reduce the Thread.Sleep call from 1 second to 900ms, but then I see Class: Foo as the value. It doesn’t appear to make a difference how long the Thread is. DemoStruct has a sleep call. ToString() always displays the value correctly, and the debugger displays the value before the sleep has finished. (Thread.Sleep appears to be disabled.)

Now is the time to act. ToString() in Noda Time does a lot of work, but it doesn’t take a whole second, therefore there must be other circumstances that cause the debugger to stop processing ToString() calls. And, of course, it’s a struct in the first place.

I attempted recursion to check if it was a stack limit, but it doesn’t appear to be the case.

So, how can I figure out why VS isn’t fully evaluating Instant.ToString()? DebuggerDisplayAttribute appears to assist, as indicated below, but without knowing why, I’ll never be certain when I need it and when I don’t.

Update

Things change when I use DebuggerDisplayAttribute:

// For the sample code in the question...
[DebuggerDisplay("{ToString()}")]
public class DemoClass

gives me:

demoClass      Evaluation timed out

When I use it in Noda Time, however:

[DebuggerDisplay("{ToString()}")]
public struct Instant

A simple test app reveals the correct outcome:

instant    "1970-01-01T00:00:00Z"

So, even if DebuggerDisplayAttribute does not force through timeouts, the problem in Noda Time is most likely a condition that DebuggerDisplayAttribute does force through. (This corresponds to my expectation that Instant.ToString is quick enough to avoid a timeout.)

This may be a sufficient answer, but I’d like to know what’s going on and whether I can simply change the code to avoid having to add the attribute to all of Noda Time’s multiple value types.

Curiouser and curiouser

Whatever is perplexing the debugger only perplexes it on occasion. Let’s make a class that stores an Instant and uses it in its ToString() method:

using NodaTime;
using System.Diagnostics;

public class InstantWrapper
{
    private readonly Instant instant;

    public InstantWrapper(Instant instant)
    {
        this.instant = instant;
    }

    public override string ToString() => instant.ToString();
}

public class Program
{
    static void Main()
    {
        var instant = NodaConstants.UnixEpoch;
        var wrapper = new InstantWrapper(instant);

        Debugger.Break();
    }
}

Now I’m looking at:

instant    {NodaTime.Instant}
wrapper    {1970-01-01T00:00:00Z}

If I change InstantWrapper to a struct, as suggested by Eren in the comments, I get:

instant    {NodaTime.Instant}
wrapper    {InstantWrapper}

So it can evaluate Instant.ToString() as long as it’s called by another ToString function inside a class. The class/struct part appears to be relevant based on the type of variable being displayed, rather than the code that must be executed to obtain the result.

As an example, consider the following:

object boxed = NodaConstants.UnixEpoch;

…and it works perfectly, displaying the correct value. To put it another way, I’m perplexed.

Asked by Jon Skeet

Solution #1

Visual Studio 2015 Update 2 contains a patch for this issue. If you’re still having trouble evaluating ToString on struct values with Update 2 or later, please let me know.

When using Visual Studio 2015 and executing ToString on struct types, you are encountering a known bug/design constraint. When dealing with System, this can be seen as well. DateTimeSpan. System.DateTimeSpan. With Visual Studio 2013, ToString() works in the evaluation windows, but not always with 2015.

Here’s what’s going on if you’re interested in the nitty gritty:

The debugger uses a technique known as “function evaluation” to evaluate ToString. To put it another way, the debugger suspends all threads in the process except the current one, changes the current thread’s context to the ToString method, sets a hidden guard breakpoint, and then lets the process run. When the guard breakpoint is hit, the debugger returns the process to its prior state and populates the window with the function’s return value.

We had to entirely redesign the CLR Expression Evaluator in Visual Studio 2015 to handle lambda expressions. The implementation is, at a high level:

The debugger is always dealing with a confusing mix of “actual” and “fake” values due to the execution of IL. In the process being debugged, real values exist. Only in the debugging process are fake values possible. When pushing a struct value on the IL stack, the debugger must always make a duplicate of the value to implement proper struct semantics. The copied value is no longer a “actual” value; it now lives solely in the debugger process. That means we won’t be able to evaluate ToString’s method later because the value doesn’t exist in the process. To try and acquire the value, we’ll need to simulate the ToString method’s execution. While we can copy some things, there are a lot of things we can’t.

With that in mind, here’s what’s producing the different behaviors you’ve noticed:

In terms of the design issue/bug, we intend to address it in a future release of Visual Studio.

Hopefully, this has clarified matters. Please let me know if you have any other queries. 🙂

Answered by Patrick Nelson – MSFT

Post is based on https://stackoverflow.com/questions/31562791/what-makes-the-visual-studio-debugger-stop-evaluating-a-tostring-override