Closed samreid closed 3 weeks ago
We could use a pattern like this, where the list of dependencies is inferred:
this.isKeyboardSelectArrowVisibleProperty = derive(
() => this.focusedSoccerBallProperty.value !== null &&
!this.isSoccerBallKeyboardGrabbedProperty.value &&
this.isKeyboardFocusedProperty.value &&
!this.hasKeyboardSelectedDifferentBallProperty.value
);
The derived function would track all of the Property instances that are visited during the closure evaluation.
So something like this:
public static derive<T>( closure: () => T ) {
// doubly nested, etc.
const tracker: Property<unknown>[] = [];
closure();
// done tracking
// TODO: No need to evaluate closure again.
return DerivedProperty.deriveAny( tracker, closure );
}
Some caveats discussed with @matthew-blackman
// MB: With DerivedProperty, you explicitly list your dependencies. With this, it is more implicit.
// See React, where the method: useEffect() or any react hooks (maybe useState). That also has the auto-update
// thing so it is more implicit. That can lead to headaches.
// SR: What if one dependencies triggers a change in another? Then you would get 2x callbacks?
// Maybe a way to visualize the dependencies would be helpful and make sure they are what you expect.
In discussion with @zepumph:
Michael also describes that the 2 problems of: slight duplication and maybe forgetting a dependency are not too bad. This proposal suffers from a magical/unexpected symptom where the behavior is tricky rather than straightforward.
Also, we could not get rid of DerivedProperty, so then there would be 2 ways of doing the same thing.
I was interested in how this could clean up call sites, however this implementation may be unworkable due to short circuiting in the derivations.
For instance, I converted:
this.hasDraggedCardProperty = new DerivedProperty( [ this.totalDragDistanceProperty, this.hasKeyboardMovedCardProperty ], ( totalDragDistance, hasKeyboardMovedCard ) => {
return totalDragDistance > 15 || hasKeyboardMovedCard;
} );
to
this.hasDraggedCardProperty = new DerivedPropertyC( () => this.totalDragDistanceProperty.value > 15 || this.hasKeyboardMovedCardProperty.value );
But since the totalDragDistanceProperty
evaluated to true
it didn't even get a chance to visit the hasKeyboardMovedCardProperty
, so it isn't aware it is a dependency. Same problem in this predicate:
this.isKeyboardSelectArrowVisibleProperty = new DerivedProperty( [ this.focusedCardProperty, this.isCardGrabbedProperty,
this.hasKeyboardSelectedDifferentCardProperty, this.isKeyboardFocusedProperty ],
( focusedCard, isCardGrabbed, hasSelectedDifferentCard, hasKeyboardFocus ) => {
return focusedCard !== null && !isCardGrabbed && !hasSelectedDifferentCard && hasKeyboardFocus;
} );
Based on the short circuiting, this seems unworkable. I wonder if we want to explore lint rules an alternative:
I wonder if a lint rule constraining the parameter names to match the Property names would be helpful? For instance, in the above example: the property is hasKeyboardSelectedDifferentCardProperty
but the parameter hasSelectedDifferentCard
so it would be renamed to hasKeyboardSelectedDifferentCard
. But writing this rule to support the arbitrary case could be very difficult.
Or a lint rule that tries to avoid Property access on a non-dependency? If you are listening to propertyA and propertyB, but the derivation checks propertyC.value, then that may be buggy. But writing this rule to support the arbitrary case would be very difficult.
@zepumph any other thoughts, or should we just close?
Has chatgtp been any help on those two rules? They seem potentially helpful and possible given the control we have over knowing when a DerivedProperty is being created within many cases.
I like those rules and feel like it may be helpful to spend a bit of time on them. What do you think?
The bug identified above in https://github.com/phetsims/center-and-variability/issues/519 was caused by querying a Property.value during a callback, but it was via another method call so could not be caught by a lint rule. Likewise a lint rule for parameter names could be of limited value in cases like new DerivedProperty( [this.someFunctionThatGivesAProperty()] )
.
We could maybe add a runtime assertion that when executing a DerivedProperty or Multilink you are not allowed to query a Property.value that isn't in the listener list, but I don't know if that could be problematic in recursion (one DerivedProperty triggers another). So I'm not sure what's best here. I was hoping someone could think of a way out of the short circuiting problem above, because other than that it is pretty nice. So let's double check on that, and if we cannot see any way forward, let's close this issue and consider lint rules elsewhere (if at all).
I like a lint rule that catches some but not all cases when creating a multilink or derivedProperty and we have the list of Properties in the first array, assert that no variable in the derivation ends with Property
that isn't in the list of dependencies. My guess is that we will catch an large number of cases that are currently potentially buggy.
was hoping someone could think of a way out of the short circuiting problem above, because other than that it is pretty nice.
I'm not sure of way around this personally.
This patch checks at runtime for Property
access outside of the dependencies
.
About half of our sims fail this assertion, here is local aqua fuzzing:
Here is the list of failing sims.
By the way, the patch fixes 2 related common code dependency issues and one sim specific one (bending light).
Instead of overhauling/changing DerivedProperty (and Multilink?) would a lint rule address the problem?
@pixelzoom and I discussed the patch above. We also discussed that a lint rule is not sufficient to cover many cases. @pixelzoom would like to take a closer look at his sims, to see if these are potential bugs and if this is a good use of time.
link
callback queries other Property values, maybe it should be a multilink.We will start with investigating DerivedProperty, thanks!
- Also, if a
link
callback queries other Property values, maybe it should be a multilink.
I think we need to be careful about making generalizations like this if we are codifying behavior. There could be many spots where the event should just be a single item, but the value requires 20 entities.
Sims that I'm going to take a look at:
[x] beers-law-lab
2 missing dependencies for 1 DerivedProperty, see https://github.com/phetsims/beers-law-lab/issues/333
[x] fourier-making-waves
8 missing dependencies for 4 DerivedProperties, see https://github.com/phetsims/fourier-making-waves/issues/239
[x] gas-properties, gases-intro, diffusion
1 missing dependency for 1 DerivedProperty, see https://github.com/phetsims/gas-properties/issues/211
Missing dependencies for NumberDisplay numberFormatter
, see https://github.com/phetsims/scenery-phet/issues/824
[x] geometric-optics
10 missing dependencies for 7 DerivedProperties, see https://github.com/phetsims/geometric-optics/issues/486
[x] graphing-quadratics
No problems were reported.
[x] hookes-law
1 missing dependency for 1 DerivedProperty, see https://github.com/phetsims/hookes-law/issues/84
[x] keplers-laws
Missing dependencies for NumberDisplay numberFormatter
, see https://github.com/phetsims/scenery-phet/issues/824
[x] my-solar-system
No problems were reported.
[x] natural-selection
No problems were reported.
[x] ph-scale
No problems were reported.
[x] units-rates
No problems were reported.
My process was:
get
:- if ( !currentDependencies.includes( this ) ) {
+ if ( !currentDependencies.includes( this ) && !phet.skip ) {
assert && assert( false, 'accessed value outside of dependency tracking' );
phet.skip
, like this: this.rightProperty = new DerivedProperty( [ this.equilibriumXProperty, this.displacementProperty ],
( equilibriumX, displacement ) => {
+ phet.skip = true;
const left = this.leftProperty.value;
+ phet.skip = false;
I completed investigation of the sims listed in https://github.com/phetsims/axon/issues/441#issuecomment-1802852296. I was surprised that 5 of the sims exhibited no problems -- @samreid thoughts?
For the sims that did exhibit problems, the problems seemed worrisome and worth addressing, and I created sim-specific issues. There was 1 common-code problem, see https://github.com/phetsims/scenery-phet/issues/824.
@samreid let's discuss where to go from here.
From discussion with @pixelzoom:
multilink
, @pixelzoom will take a look at his sims (like he did above for DerivedProperty)link
I also saw that a DerivedProperty in ph-scale accesses itself in its own derivation:
this.colorProperty = new DerivedProperty(
[ this.soluteProperty, this.soluteVolumeProperty, this.waterVolumeProperty ],
( solute, soluteVolume, waterVolume ) => {
if ( this.ignoreVolumeUpdate ) {
return this.colorProperty.value;
}
I also saw that a DerivedProperty in ph-scale accesses itself in its own derivation:
I don't think that's a problem. There's no way that it can return a stale or incorrect value. Unless I'm missing something...
Two questions about the above commits, where @samreid added accessNonDependencies: true
to many sims
(1) Should those usages have a TODO that points to a GitHub issue? (...either this issue or a sim-specific issue.) I realize that we can search for accessNonDependencies: true
. But how will we tell the difference between uses that have reviewed and kept, versus those that no one has looked at?
(2) I had previously addressed beers-law-lab problems in https://github.com/phetsims/beers-law-lab/issues/333. So I was surprised to see new problems in https://github.com/phetsims/beers-law-lab/commit/308dae021080663fa6e437cdb526e2c7f6b0fe9d. @samreid Can you clarify? Did you change the patch used to identify missing dependencies?
Should those usages have a TODO that points to a GitHub issue?
That would be good to discuss. We haven't decided if we want to chip away and eliminate most/all of these. There are 68 occurrences of accessNonDependencies: true
across 40 directories.
But how will we tell the difference between uses that have reviewed and kept, versus those that no one has looked at?
As far as I can tell, none have been looked at yet or reviewed and kept. I agree we will need a clear way of annotating which is which if we ever decide "this is permanent" for one.
I was surprised to see new problems
I did not change the test harness, but I did fuzz quite a bit longer.
My opinion is that putting workarounds, or (worse) code that is intended to temporarily silence errors, into production code without a TODO is a bad practice. Suite yourself for the sims that you're responsibile for. But in the above commits, I added TODOs for accessNonDependencies: true
in my sims and common code.
There are now 35 occurrences of accessNonDependencies: true
with no comment or TODO that links back to this issue.
Based on my initial investigation on Multilink, it seems we should address it in the same way.
But Multilink does not have a way to provide options. The constructor signature is:
public constructor( dependencies: RP1<T1>, callback: ( ...params: [ T1 ] ) => void, lazy?: boolean ) ;
Perhaps we should change the API to something like:
type SelfOptions = {
lazy?: boolean; //TODO document
accessNonDependencies?: boolean; //TODO document
};
type MulitlinkOptions = SelfOptions;
...
public constructor( dependencies: RP1<T1>, callback: ( ...params: [ T1 ] ) => void, providedOptions?: MulitlinkOptions ) ;
I have added the options in my working copy. Should we use the same option name accessNonDependencies
or a different one to distinguish it from DerivedProperty?
Same option name, accessNonDependencies
.
For https://github.com/phetsims/unit-rates/issues/223:
this.quantityProperty = new DerivedProperty(
[ this.numberOfBagsProperty, this.numberOfItemsProperty ],
( numberOfBags, numberOfItems ) => {
if ( this.quantityUpdateEnabled ) {
return ( numberOfBags * options.quantityPerBag ) + numberOfItems;
}
else {
return this.quantityProperty.value;
}
},
@samreid It looks like this is similar to the case that you reported in https://github.com/phetsims/axon/issues/441#issuecomment-1810181500 for ph-scale, where a DerivedProperty has itself as a dependency. As I mentioned in https://github.com/phetsims/axon/issues/441#issuecomment-1810338145, I don't think that's a problem, so I don't think that (in cases like this) a DerivedProperty needs to include itself as a dependency.
Thoughts?
Here is my patch so far investigating Multilink:
I also observed that cases like this are triggering. Maybe the check is hitting set
as well as get
? Can we restrict it to just visit the get
occurrences?
// Set the dispensing rate to zero when the shaker becomes empty or invisible.
Multilink.multilink( [ this.isEmptyProperty, this.visibleProperty ], ( isEmpty, visible ) => {
if ( isEmpty || !visible ) {
this.dispensingRateProperty.value = 0;
}
}, {
accessNonDependencies: true
} );
... Can we restrict it to just visit the
get
occurrences?
Yes. There is no dependency on a Property's value unless get
is called.
I wonder if https://github.com/phetsims/axon/issues/441#issuecomment-1810598219 is just triggering get
s elsewhere. The guard is only in ReadOnlyProperty.get
Anyways, it feels there are 2x-5x more issues with Multilink than we saw with DerivedProperty. I'm at a good point to check in with @pixelzoom synchronously.
I have some concerns that these checks might lead to brittle code. Ran into the above thing broken, however I'm able to break things with completely-unrelated changes now.
Say I need to refactor Color, and it needs to read a Property during the execution of withAlpha
. Bam, we've broken the DerivedProperty in RectangularButton that uses withAlpha. (immediate fails on any sims)
Even more tricky, say, I add a Property access to Color.colorUtilsBrighter
... BAM now I've broken a DerivedProperty in gravity-force-lab. Or accessing a Property in BunnyCounts' constructor will error out natural-selection.
Essentially, now you have to provide a flag to a DerivedProperty if ANY part if its implementation could possibly in the future access a Property?
But also... does this mean for instance if we ever have something in StringUtils.format/fillIn that accesses a Property, we need to tag thousands of DerivedProperties with this flag? Or are we saying "hey, for all sorts of common behaviors, we're no longer ALLOWED to use Properties in their implementation"?
I'm worried people are going to be breaking this left-and-right.
Based on https://github.com/phetsims/greenhouse-effect/issues/370 and https://github.com/phetsims/axon/issues/441#issuecomment-1811150610 I've commented out the assertion for now. Let's check in at or before next developer meeting to discuss.
Demo patch, may be stale in a few weeks:
Following up on https://github.com/phetsims/axon/issues/441#issuecomment-1810338145 and https://github.com/phetsims/axon/issues/441#issuecomment-1810592906... @samreid Can we please remove the requirement for self-referential dependencies? Besides the fact that it's impossible to add a DerviedProperty as a dependency of itself ("used before being assigned" error), it's not even an actual problem. And I have GitHub issues on hold that I'd like to close.
After addressing missing depedencies in several of my sims, CT is reporting numerous problems -- see the issues linked immediately above. I'm regretting making these changes, because this is going to be significant work to investigate.
In this patch, I experimented with allowing the DerivedProperty to access its own value in its derivation. It is working but I don't want to commit without discussion due to some complex parts.
First, I could not find a suitable type for the derivedProperty in getDerivedValue
because I could not get UnknownDerivedProperty to work.
Second, we must compute the initial value of the DerivedProperty before calling super()
so it knows the initial value. However, we cannot access this
before the super
call. So I added a null alternative for that call. But it also means that DerivedProperty instances should not self-access during their first derivation.
Dev Meeting 12/7/23
Consensus: Turn it into a Query Parameter. SR & CM will finalize other details.
@samreid let's pick a time to meet and figure out next steps.
Also a reminder that before we can put this behind a query parameter (and/or package.json config), we need to figure out how to omit self-referrential dependencies, see https://github.com/phetsims/axon/issues/441#issuecomment-1817537544.
Notes from meeting with @samreid:
Add a query parameter, to be used for opting in, as a development tool: &strictAxonDependencies=true|false
(default false
). We chose this name for it's potential to be applicable to Multilink, etc. in the future. And we don't want to create confusion with other types of "dependencies" like dev/repo dependencies.
Add a package.json entry for opting in for a repo: phet.simFeatures.strictAxonDependencies: true|false
(default false
). Query parameter takes precedence over package.json.
Add strictAxonDependencies
to phetmarks, with UI: o true o false o Simulation Default
Rename DerivedPropertyOptions.accessNonDependencies: false
to strictAxonDependencies: true
(and invert logic).
This can be done in the next iteration, followed by a developer PSA.
@samreid will take the lead on the above bullets, @pixelzoom will review.
@samreid and I moved forward on the issue of self-referrential dependency. Below is a refined version on the patch that @samreid created above in https://github.com/phetsims/axon/issues/441#issuecomment-1817969157.
We decided that this complicates DerivedProperty unnecessarily. And self-reference should be something that is rarely done, possibly even to be avoided. In cases where it is necessary, the developer should opt out of with strictAxonDependencies: false
.
OK, I addressed the bullet points in https://github.com/phetsims/axon/issues/441#issuecomment-1861261509 and ran the following tests:
const aProperty = new Property( 1 );
const bProperty = new Property( 2 );
const dProperty = new Property( 'x' );
const cProperty = new DerivedProperty( [ aProperty, bProperty ], ( a, b ) => {
console.log( dProperty.value );
return a + b;
} );
console.log( cProperty.value );
Test these scenarios:
query parameter | package.json | expected | actual |
---|---|---|---|
true | true | assertion error | assertion error |
true | false | assertion error | assertion error |
true | not specified | assertion error | assertion error |
false | true | no error | no error |
false | false | no error | no error |
false | not specified | no error | no error |
not specified | true | assertion error | assertion error |
not specified | false | no error | no error |
not specified | not specified | no error | no error |
Additionally test with one of the assertion error
rows above, and this option, to make sure it doesn't fali.
const aProperty = new Property( 1 );
const bProperty = new Property( 2 );
const dProperty = new Property( 'x' );
const cProperty = new DerivedProperty( [ aProperty, bProperty ], ( a, b ) => {
console.log( dProperty.value );
return a + b;
}, {
strictAxonDependencies: false
} );
console.log( cProperty.value );
Everything seems to be working correctly here, and this additionally caught 2x regressions in Projectile Data Lab that would not have i18n correctly. So I think we are ready for a commit and review.
I additionally tried turning off the strictAxonDependencies: false
in ConcentrationSolution, and specifying the ?strictAxonDependencies=true
but was unable to get that error to trigger by saturating a solution in Beer's Law Lab. @pixelzoom should double check this context and make sure there is no trouble.
I wanted to double-check the opt-out cases in ph-scale, but it was catching an earlier problem in ComboBoxButton.toString which was calling a Property.toString. Likely this was introduced in https://github.com/phetsims/sun/commit/071966ef2adeecc77d75340a86c3b17f51f52d33. So we will likely need to re-review cases as we enable this test for specific sims.
That being said, the tests above seem like it is a reasonable point for check in with @pixelzoom before we go further. @pixelzoom can you please review and advise next steps?
@pixelzoom and I discussed that we aren't certain whether this should be opt-in or opt-out. It is currently set for opt-in, with only 1 sim (projectile-data-lab) opting in.
In https://github.com/phetsims/axon/issues/441#issuecomment-1873399696, @samreid said:
... Everything seems to be working correctly here, and this additionally caught 2x regressions in Projectile Data Lab that would not have i18n correctly. So I think we are ready for a commit and review.
Your testing looks thorough.
I additionally tried turning off the strictAxonDependencies: false in ConcentrationSolution, and specifying the ?strictAxonDependencies=true but was unable to get that error to trigger by saturating a solution in Beer's Law Lab. @pixelzoom should double check this context and make sure there is no trouble.
I'll investigate.
I wanted to double-check the opt-out cases in ph-scale, but it was catching an earlier problem in ComboBoxButton.toString which was calling a Property.toString. Likely this was introduced in https://github.com/phetsims/sun/commit/071966ef2adeecc77d75340a86c3b17f51f52d33. So we will likely need to re-review cases as we enable this test for specific sims.
I investigated. This was because ph-scale.Solute.toString was using a StringProperty, and being called in an assertion message in ComboBoxButton. I changed the implementation fo Solute.toString, and the problem is resolved.
That being said, the tests above seem like it is a reasonable point for check in with @pixelzoom before we go further. @pixelzoom can you please review and advise next steps?
Which commits should I review?
In https://github.com/phetsims/axon/issues/441#issuecomment-1881584454, @samreid said:
@pixelzoom and I discussed that we aren't certain whether this should be opt-in or opt-out. It is currently set for opt-in, with only 1 sim (projectile-data-lab) opting in.
The default should definitely be strictAxonDependencies: true
.
I tested locally to see how many sims fail with strictAxonDependencies=true
. Here's my phetmarks URL:
http://localhost:8080/aqua/fuzz-lightyear/?loadTimeout=30000&testTask=true&ea&audio=disabled&testDuration=10000&brand=phet&fuzz&strictAxonDependencies=true
The failing repos are:
Note that 5 sims are still failing due to problems with StopwatchNode and NumberDisplay, https://github.com/phetsims/scenery-phet/issues/781.
Since there are only a handful of sims failing, I recommend:
strictAxonDependencies: false
(with a TODO referencing GitHub issue) for the failing cases. If that's not possible, opt out in package.json. Note in the sim issues. @samreid How do you want to proceed?
@samreid said:
I additionally tried turning off the strictAxonDependencies: false in ConcentrationSolution, and specifying the
?strictAxonDependencies=true
but was unable to get that error to trigger by saturating a solution in Beer's Law Lab. @pixelzoom should double check this context and make sure there is no trouble.
I had the same result, with fuzzing. For this case, I was opting out because of a missing self-referential dependency. Did you commit something that omits self-referential dependencies?
UPDATE from discussion with @samreid and @pixelzoom
SR: We saw precipitateMolesProperty
has a self reference, but has to opt out of strict check due to that self reference.
Would be good to review:
https://github.com/phetsims/axon/commit/e411b7b41630d7c9f1eca473f1e0cf22b930cd38
@samreid and I worked on this for ~1 hour today, related commits above.
Remaining work, which I'll do:
Reviewing https://github.com/phetsims/axon/commit/e411b7b41630d7c9f1eca473f1e0cf22b930cd38 resulted in 1 documentation fix in https://github.com/phetsims/axon/commit/089c4b10fbc059ead6657692779b57a6d8883a7f.
All work completed, closing.
Reopening because there is a TODO marked for this issue.
There are TODOs in common code, see below. I'll handle these, either by addressing them, or by creating specific GitHub issues.
pixelzoom edit: Fixed in https://github.com/phetsims/scenery-phet/commit/1fb6dc83a7f9dcda7e941be041a9ffa46ddbd0e0 and https://github.com/phetsims/scenery-phet/commit/0e57c5738fd486c45b3c49d329a4863ee80ec893
While reviewing https://github.com/phetsims/center-and-variability/issues/433, I saw that each property in a DerivedProperty is listed 3x times:
There is also the opportunity to forget to register certain properties in the listeners. I have seen code like this (someOtherProperty) not registered as a dependency: