Skip to content

Conversation

@DefaultRyan
Copy link
Member

Users trying to debug Xaml apps were having issues that rendered the debugging experience unusable. Namely:

  • Generic types in the property list would throw an exception, closing down the visualizer pane.
  • Properties of type "System.Object" (aka IInspectable) weren't handled properly, causing undefined behavior via uninitialized variables. This was causing crashes and other bizarre behavior in the visualizer.
  • Types that had more than one page of properties would repeatedly recreate and append the properties to the list when the debugger requested more of the list, causing the list to grow endlessly.

In this change:

  • Generics changed from throwing to omitting, with a diagnostic message.
  • IInspectable properties changed from bad behavior to omitting, with a diagnostic message. (If anybody has pointers on how to enable them, I'm listening).
  • We only create the property list once for a type/object.

I tested this by excercising scenarios that were previously blowing up the visualizer. Specifically, setting a breakpoint in a Xaml Button click handler, and trying to inspect the "sender" object. This sender is a Xaml Button, and so its property list satisfies all three conditions: it has generics, IInspectables, and a long list of properties that requires the debugger to make multiple requests.

Copy link
Member

@Scottj1s Scottj1s left a comment

Choose a reason for hiding this comment

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

Thank you!!!

@DefaultRyan
Copy link
Member Author

Thank you!!!

Happy to improve it and build on your original work. If you ever get a few minutes, I'd enjoy brainstorming some ideas as to why nested property visualization doesn't seem functional, and what the proper syntax is to visualize IInspectable (aka System.Object) property types.

Copy link

@arkrumbe arkrumbe left a comment

Choose a reason for hiding this comment

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

Not my codebase, so I won't block on feedback or anything, but just had a few questions.

[&](ElementType type)
{
if ((type < ElementType::Boolean) || (type > ElementType::String))
if ((ElementType::Boolean <= type) && (type <= ElementType::String))

Choose a reason for hiding this comment

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

I figure you're about to merge this anyway, but 3 questions:

  1. I'm assuming ElementType is an enum or something that supports std::less, so you can do this comparison safely?
  2. Why are you flipping the operands and putting type as the second operand in the first comparison. That seems smelly.
  3. Can you explain the logic here: is it "if it's between these types"?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, ElementType is an enum.

I'm doing the operands that way, because it most closely mimics the visuals of "min <= x <= max". I've found this to be the least error-prone and most intuitive way to compare a value against a range.

The logic of "between this types" is that this is the range of "fundamental" types, as defined in the winmd. Unfortunately, treating it this way led to the bug that I had to fix, as it inadvertently ignored ElementType::Object, which is another possible type that is represented via a simple ElementType. Honestly, I think this would be more intuitive and less brittle as a switch-case statement, mapping the various simple ElementType values to their corresponding 'PropertyValue' values. I'm likely to make that change in the near future.

{
//propDisplayType = L"winrt::Windows::Foundation::IInspectable";
//propCategory = PropertyCategory::Class;
//propAbiType = L"winrt::impl::inspectable_abi*";

Choose a reason for hiding this comment

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

I don't know about your codebase: do you tend to put TODOs when you know future work needs to happen (i.e. The object visualization issue that you have to fix in future)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, this should be a TODO - ideally one that also includes an issue #.

{
return;
propCategory = (PropertyCategory)(static_cast<std::underlying_type<ElementType>::type>(type) -
static_cast<std::underlying_type<ElementType>::type>(ElementType::Boolean));

Choose a reason for hiding this comment

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

Pointer/enum arithmetic?

Choose a reason for hiding this comment

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

and then just a C-style cast back? Confused.

Copy link
Member Author

Choose a reason for hiding this comment

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

The two enums have corresponding ranges. Think of it as analogous to performing an upper-to-lower case conversion like this:

char upper_to_lower(char input)
{
  return (input - 'A') + 'a';
}

propertyData.push_back({ propIid, propIndex, propCategory, propAbiType, propDisplayType, propDisplayName });
if (propCategory)
{
auto propName = method.Name().substr(4);

Choose a reason for hiding this comment

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

Why substr(4)?

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 believe because this is stripping off the leading "get_" in the method name.

If I was being pedantic, the metadata spec doesn't actually mandate that the getter for a property Foo be named "get_Foo". It's just a convention used by all the tooling that we're aware of. The slightly more robust way (and slightly more expensive way) of doing this would be to utilize the PropertyMap and MethodSemantics tables to lookup each property getter for a type, and extract the name directly from the Property table.

for (auto&& impl : impls)
{
GetInterfaceData(impl.Interface(), m_propertyData, m_isStringable);
GetInterfaceData(process, impl.Interface(), m_propertyData, m_isStringable);

Choose a reason for hiding this comment

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

Why the inclusion of this process as a param?

Copy link
Member Author

Choose a reason for hiding this comment

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

So it can be supplied to the call to NatvisDiagnostic for logging properties we couldn't visualize.

@BenJKuhn BenJKuhn merged commit 3a95080 into master Aug 21, 2020
@BenJKuhn BenJKuhn deleted the defaultryan/visualizer branch August 21, 2020 19:52
@ChrisGuzak
Copy link
Member

I was getting the VS debugger to crash when using the visualizer in the past. do you think this change could have addressed that?

also, want to point out #571 as another debugging related issue

@arkrumbe
Copy link

arkrumbe commented Aug 23, 2020 via email

@kennykerr kennykerr mentioned this pull request Dec 17, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Visualizer unusable on VS 16.6+ Natvis kills visualizer if a property has a generic

6 participants