Skip to content

Conversation

@jazzdelightsme
Copy link
Contributor

@jazzdelightsme jazzdelightsme commented May 18, 2018

PR Summary

Dynamic (DLR) objects work in some places today, but not others. This
change expands that support to ForEach-Object, Where-Object and the
family of cmdlets that use MshExpression (Select-Object, etc.). (see the
linked issue for the simple repro and explanation: fixes #6567)

This change addresses both wildcard and non-wildcard cases. In wildcard
cases, it uses the existing support of generating PSDynamicMember
objects for names returned by GetDynamicMemberNames.

In non-wildcard cases, a dynamic property access is attempted whether or
not the name shows up in GetDynamicMemberNames, but a truly "blind"
access is only attempted if we see that the base object is an IDMOP. If
the dynamic access fails, you'll get the same or a similar error
experience as before ("The property 'Blarg' cannot be found", or no
error at all, depending on the cmdlet and the strict mode setting).

The included test coverage includes a stub for the .ForEach
operator--once people are happy with this change, I can continue by
adding support there.

This change should allegedly also have positive perf impact, though I
have not tested that assertion.

PR Checklist

@iSazonov iSazonov requested a review from lzybkr May 19, 2018 19:54
@jazzdelightsme
Copy link
Contributor Author

About the CodeFactor checks: it looks like most of these are not caused by my commit (although I exacerbated an existing issue--added another small helper class to a file that already contains multiple classes). What is the strategy for dealing with these? Ignore them? Take an IOU to fix them in a separate commit?

@iSazonov
Copy link
Collaborator

CodeFactor issues is optional. Please fix only code you created/changed.

Copy link
Contributor

@lzybkr lzybkr left a comment

Choose a reason for hiding this comment

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

Changes look great, just some minor feedback.

It'd be great if you can measure the improvement using dynamic sites.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe also test *Prop (should be error with -ExpandProperty, maybe also test without -ExpandProperty.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good ideas; will do.

Copy link
Contributor

Choose a reason for hiding this comment

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

Why was the null test added? The C# operator is can be safely used with null values.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

D'oh! Thanks.

Copy link
Contributor

Choose a reason for hiding this comment

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

I know you're not a fan of silent errors, but I think consistency here is important - you wouldn't want an error just because the object implemented IDMOP, e.g. if some object didn't in the past, but a new version does, scripts would break when they silently "worked" before.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The problem here is that I can't tell the difference between "it failed because the property does not exist" (let's call this case 1) and "it failed because accessing it actually threw some exception" (let's call that case 2).

Existing PowerShell behavior is different for these two cases: case 1 gets an error (which is possible because the ETS tells us up front if the property exists or not), and case 2 does not.

For non-IDMOP objects, there's no problem; but for IDMOPs, we have the chance to attempt a "blind" access, but the cost is that we must have the same response to both cases (because we cannot distinguish between the two). So we have to make a choice: we can either swallow ALL errors (including "The property 'Blarg' does not exist"), or expose them all.

I felt like it was more important to preserve the behavior of showing "The property 'Blarg' does not exist" (case 1) errors than to suppress "FooException thrown when accessing Bloop property" (case 2) errors. This decision certainly must be influenced by my predisposition against silent errors, but actually I was going for usability: a typo or otherwise trying to access a property that does not exist is a common mistake, and generally easily fixed when presented with the error, and if I had to pick one of "error suppression" or "tell the user s/he goofed", I'd choose the latter.

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

(I can at least add a comment about this)

Copy link
Contributor

Choose a reason for hiding this comment

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

You can make some assumptions though:

  • non-dynamic members must exist
  • if GetDynamicMemberNames says the member exists, trust it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes--in fact, those are exactly the assumptions I am making: if it had shown up in GetDynamicMemberNames, it would show up as a dynamic member, and isBlindDynamicAccess would be false. (And it would be false for normal properties as well, of course.) I'll add more comments about this.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't have any intuition for how often wildcards are used to specify multiple properties, but I know I use them that way for formatting commands.

I'm not sure perf matters much here, but it would be good to understand the impact on something like ps | select *mem*.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For select, the wildcard gets resolved into individual MshExpressionResult objects, so no problems there. So this case here really is for the heterogenous-objects-with-different-matching-names case, which I think is probably a rare enough case not to care too much about (though it will work, of course).

@jazzdelightsme
Copy link
Contributor Author

Regarding the perf:

Ultimately the property access itself is such a small part of what goes on that I found it difficult to find any difference at all.

Here's what I did:

I added a switch to the code, [danthom]::IsDynamicStuffEnabled, to allow me to switch back and forth between the old and new behavior at runtime. Then I had a script to create an array of objects (normal ones, not dynamic) to run through a pipeline, and then had a for loop to repeatedly run the pipeline.

There was enough variability that I tried to eliminate the effect of GCs (by using [gc]::TryStartNoGCRegion), but that only worked for fairly small amounts of work (a handful of seconds), and besides, how much garbage each method produced would actually be an interesting and material effect on perf, so I just printed out the number of GCs along with the times.

I tested where, select, and foreach-object, with and without wildcards (and for select, with wildcards to match multiple properties), and couldn't see much difference between them. The dynamic stuff seemed like it might be slightly faster (and generated a little less garbage--perhaps that's what made the difference).

I tried adding artificial magnification--doing either the dynamic access or normal access in a for loop--but even that didn't make much of a difference. But in general, the dynamic access seemed ever-so-slightly faster, probably because it usually generated a little less garbage. (And as a result of my GC-consciousness, I also switched the DynamicPropertyGetter class to be a struct.)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Please put the comment on separate line.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd move the line to line 50 - it is more clear.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Please use full names instead of aliases like -EA.

Copy link
Collaborator

Choose a reason for hiding this comment

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

We don't use "m" prefix - right pattern is _serialNumber.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we make it static?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Please move to line 135.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Please address the comment.

if ((x == null) && (target.BaseObject is System.Dynamic.IDynamicMetaObjectProvider))

Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe throw here with user friendly message then in line 413 with standard null exception?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not going to change this; it's totally unrelated to my change.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I leave this open - we should fix this in the PR or later.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Please use C# 7 syntax

if ( ex is MethodException mex ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This code is also unrelated to my change (harder to tell for this one, but it's just moved a bit).

Copy link
Collaborator

Choose a reason for hiding this comment

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

Please remove the 3 comments - modern editors work well with large files.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Please remove the unneeded comments.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Please remove. We can put the code in the file. Or in HelpCommon module.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The same.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The same.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The module is auto-loaded. Please remove the line.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The same.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The same.

Dynamic (DLR) objects work in some places today, but not others.  This
change expands that support to ForEach-Object, Where-Object and the
family of cmdlets that use MshExpression (Select-Object, etc.). (see the
linked issue for the simple repro and explanation: fixes PowerShell#6567)

This change addresses both wildcard and non-wildcard cases. In wildcard
cases, it uses the existing support of generating PSDynamicMember
objects for names returned by GetDynamicMemberNames.

In non-wildcard cases, a dynamic property access is attempted whether or
not the name shows up in GetDynamicMemberNames, but a truly "blind"
access is only attempted if we see that the base object is an IDMOP. If
the dynamic access fails, you'll get the same or a similar error
experience as before ("The property 'Blarg' cannot be found", or no
error at all, depending on the cmdlet and the strict mode setting).

The included test coverage includes a stub for the .ForEach
operator--once people are happy with this change, I can continue by
adding support there.

This change should allegedly also have positive perf impact, though in
actual perf testing, although it does seem ever-so-slightly faster, I
found it difficult to measure much difference at all.
@jazzdelightsme jazzdelightsme force-pushed the user/danthom/PwshHeartsIDMOP branch from 14349eb to 58ebb74 Compare June 4, 2018 14:40
@TravisEz13
Copy link
Member

@lzybkr Can you update your review?

@jazzdelightsme
Copy link
Contributor Author

Thanks for the reviews, people! @TravisEz13 what is the process for getting this merged? Is there any transparency to how you track proposed merges, or is it a "it happens when it happens" sort of thing?

I don't mean to be impatient; I'm just wondering how it works, and I want to make sure there aren't any extra steps I need to do or anything like that.

@iSazonov
Copy link
Collaborator

iSazonov commented Jun 7, 2018

@jazzdelightsme MSFT team has announced that they is actively working on porting Windows PowerShell modules with minimal activity on GitHub. So now some PRs is waiting reviews from MSFT experts.

@daxian-dbw
Copy link
Member

Hey @jazzdelightsme, thanks for your contribution and patience!
Can you please push another commit with the [feature] tag at the beginning of the commit message? That way it will trigger the daily build run which runs more tests. I want the chagnes to be validated by the daily test run. Thanks!

@jazzdelightsme jazzdelightsme force-pushed the user/danthom/PwshHeartsIDMOP branch from 58ebb74 to 460beb8 Compare June 9, 2018 16:38
@jazzdelightsme jazzdelightsme requested a review from anmenaga as a code owner June 9, 2018 16:38
@jazzdelightsme
Copy link
Contributor Author

Can you please push another commit with the [feature] tag at the beginning of the commit message?

Did it work (to trigger the extra testing)?

@TravisEz13
Copy link
Member

@jazzdelightsme The feature tag did work.

Copy link
Member

@daxian-dbw daxian-dbw left a comment

Choose a reason for hiding this comment

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

LGTM.
@jazzdelightsme I only have one comment about a comment in the code.
I can update the comment for you if you are OK with what I suggest.


if ((x == null) && (target.BaseObject is System.Dynamic.IDynamicMetaObjectProvider))
{
// We could check if GetDynamicMemberNames includes the name... but
Copy link
Member

Choose a reason for hiding this comment

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

About this comment, we have already checked GetDynamicMemberNames when reaching here (target.Members includes the dynamic members returned from GetDynamicMemberNames). So maybe we can change comment to: GetDynamicMemberNames does not return this name, but GetDynamicMemberNames is only a hint ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Your comment is not quite correct. (Maybe it should be correct and there is another bug; I'm not sure what the intended behavior is.)

If you iterate through .Members you will indeed see the dynamic members. However, if you do .Members[propName], where propName is the name of one of those dynamic members... you will get null back.

Sample output:

PS C:\src\PowerShell> $myObj.PSObject.Members | where Name -eq FooProp


MemberType      : Dynamic
Value           :
TypeNameOfValue : dynamic
Name            : FooProp
IsInstance      : True



PS C:\src\PowerShell> $myObj.PSObject.Members['FooProp']
PS C:\src\PowerShell> $myObj.PSObject.Members['FooProp'] -eq $null
True
PS C:\src\PowerShell>

I'm not sure if that's a bug or By Design. Because you can't do much with PSDynamicMembers (for instance, the getter/setter just throws), maybe it is intentionally excluded when indexing directly in. But it does seem odd.

Copy link
Member

@daxian-dbw daxian-dbw Jun 13, 2018

Choose a reason for hiding this comment

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

I see. That does seem odd. I will look into it tomorrow. Thanks for bringing it up!

Copy link
Member

Choose a reason for hiding this comment

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

This is because DotNetAdapter.GetMember<T>(object obj, string memberName) doesn't return dynamic members. I think it makes sense to not return dynamic member from this method, and submit #7087 to clarify it in the code comments.

@daxian-dbw daxian-dbw merged commit ce3cb68 into PowerShell:master Jun 13, 2018
@iSazonov
Copy link
Collaborator

@jazzdelightsme Thanks for the great contribution!

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.

The PSObject implementation does not handle classes derived from DynamicObject properly

5 participants