-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Instrumented stateful Properties should only accept values from the state during state set #409
Comments
This patch works great. I was able to remove all of the Index: main/axon/js/ReadOnlyProperty.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/main/axon/js/ReadOnlyProperty.ts b/main/axon/js/ReadOnlyProperty.ts
--- a/main/axon/js/ReadOnlyProperty.ts (revision a5961582e9fed7de7a0df002257961872cec4d7c)
+++ b/main/axon/js/ReadOnlyProperty.ts (date 1661307101650)
@@ -214,9 +214,23 @@
/**
* Sets the value and notifies listeners, unless deferred or disposed. You can also use the es5 getter
* (property.value) but this means is provided for inner loops or internal code that must be fast. If the value
- * hasn't changed, this is a no-op.
+ * hasn't changed, this is a no-op. For PhET-iO instrumented Properties that are phetioState: true, the value is only
+ * set by the state and cannot be modified by other code while isSettingPhetioStateProperty === true
*/
protected set( value: T ): void {
+ if ( window.phet && phet?.joist?.sim?.isSettingPhetioStateProperty.value
+ && this.isPhetioInstrumented() && this.phetioState ) {
+ // state is managed by the state engine
+ }
+ else {
+ this.unguardedSet( value );
+ }
+ }
+
+ /**
+ * For usage by the IO Type during PhET-iO state setting.
+ */
+ public unguardedSet( value: T ): void {
if ( !this.isDisposed ) {
if ( this.isDeferred ) {
this.deferredValue = value;
@@ -540,7 +554,7 @@
const units = NullableIO( StringIO ).fromStateObject( stateObject.units );
assert && assert( property.units === units, 'Property units do not match' );
assert && assert( property.isSettable(), 'Property should be settable' );
- ( property as Property<T> ).set( parameterType.fromStateObject( stateObject.value ) );
+ ( property ).unguardedSet( parameterType.fromStateObject( stateObject.value ) );
if ( stateObject.validValues ) {
property.validValues = stateObject.validValues.map( ( validValue: StateType ) => parameterType.fromStateObject( validValue ) );
Index: main/mean-share-and-balance/js/intro/model/IntroModel.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/main/mean-share-and-balance/js/intro/model/IntroModel.ts b/main/mean-share-and-balance/js/intro/model/IntroModel.ts
--- a/main/mean-share-and-balance/js/intro/model/IntroModel.ts (revision c28937ffa489f3069e9857c3fa35a57d7564596f)
+++ b/main/mean-share-and-balance/js/intro/model/IntroModel.ts (date 1661306333349)
@@ -232,11 +232,9 @@
* Reset 2D waterLevelProperty to 3D waterLevelProperty.
*/
private matchCupWaterLevels(): void {
- if ( !phet.joist.sim.isSettingPhetioStateProperty.value ) {
- this.iterateCups( ( cup2D, cup3D ) => {
- cup2D.waterLevelProperty.set( cup3D.waterLevelProperty.value );
- } );
- }
+ this.iterateCups( ( cup2D, cup3D ) => {
+ cup2D.waterLevelProperty.set( cup3D.waterLevelProperty.value );
+ } );
}
/**
@@ -257,19 +255,15 @@
this.assertConsistentState();
this.stepWaterLevels( dt );
this.pipeArray.forEach( pipe => pipe.step( dt ) );
-
- assert && assert( !phet.joist.sim.isSettingPhetioStateProperty.value, 'Cannot step while setting state' );
}
private assertConsistentState(): void {
- if ( !phet.joist.sim.isSettingPhetioStateProperty.value ) {
- const numberOfCups = this.numberOfCupsProperty.value;
- assert && assert( numberOfCups === this.getNumberOfActiveCups(), `Expected ${numberOfCups} cups, but found: ${this.getNumberOfActiveCups()}.` );
- assert && assert( numberOfCups > 0, 'There should always be at least 1 cup' );
- assert && assert( this.getNumberOfActiveCups() - 1 === this.getActivePipes().length, `The length of pipes is: ${this.getActivePipes().length}, but should be one less the length of water cups or: ${this.getNumberOfActiveCups() - 1}.` );
- assert && assert( this.waterCup3DArray.length === MeanShareAndBalanceConstants.MAXIMUM_NUMBER_OF_CUPS, `There should be ${MeanShareAndBalanceConstants.MAXIMUM_NUMBER_OF_CUPS}, but there were actually ${this.waterCup3DArray.length} cups` );
- assert && assert( this.waterCup2DArray.length === MeanShareAndBalanceConstants.MAXIMUM_NUMBER_OF_CUPS, `There should be ${MeanShareAndBalanceConstants.MAXIMUM_NUMBER_OF_CUPS}, but there were actually ${this.waterCup2DArray.length} cups` );
- }
+ const numberOfCups = this.numberOfCupsProperty.value;
+ assert && assert( numberOfCups === this.getNumberOfActiveCups(), `Expected ${numberOfCups} cups, but found: ${this.getNumberOfActiveCups()}.` );
+ assert && assert( numberOfCups > 0, 'There should always be at least 1 cup' );
+ assert && assert( this.getNumberOfActiveCups() - 1 === this.getActivePipes().length, `The length of pipes is: ${this.getActivePipes().length}, but should be one less the length of water cups or: ${this.getNumberOfActiveCups() - 1}.` );
+ assert && assert( this.waterCup3DArray.length === MeanShareAndBalanceConstants.MAXIMUM_NUMBER_OF_CUPS, `There should be ${MeanShareAndBalanceConstants.MAXIMUM_NUMBER_OF_CUPS}, but there were actually ${this.waterCup3DArray.length} cups` );
+ assert && assert( this.waterCup2DArray.length === MeanShareAndBalanceConstants.MAXIMUM_NUMBER_OF_CUPS, `There should be ${MeanShareAndBalanceConstants.MAXIMUM_NUMBER_OF_CUPS}, but there were actually ${this.waterCup2DArray.length} cups` );
}
public reset(): void {
@@ -292,14 +286,12 @@
* @param oldWaterLevel - the old water level from the 3D cup's listener
*/
public changeWaterLevel( cup3DModel: WaterCup, waterLevel: number, oldWaterLevel: number ): void {
- if ( !phet.joist.sim.isSettingPhetioStateProperty.value ) {
- const delta = waterLevel - oldWaterLevel;
- const cup2D = this.waterCup2DArray[ cup3DModel.linePlacement ];
- const cup2DWaterLevel = Utils.clamp( cup2D.waterLevelProperty.value + delta, 0, 1 );
- cup2D.waterLevelProperty.set( cup2DWaterLevel );
+ const delta = waterLevel - oldWaterLevel;
+ const cup2D = this.waterCup2DArray[ cup3DModel.linePlacement ];
+ const cup2DWaterLevel = Utils.clamp( cup2D.waterLevelProperty.value + delta, 0, 1 );
+ cup2D.waterLevelProperty.set( cup2DWaterLevel );
- this.arePipesOpenProperty.value && this.distributeWaterRipple( this.getActive2DCups(), cup2D, delta );
- }
+ this.arePipesOpenProperty.value && this.distributeWaterRipple( this.getActive2DCups(), cup2D, delta );
}
}
Next steps:
|
I believe that you are on a great track! Thanks for implementing. I believe that many cases will indeed be fixed by this, but in general there are more stateful items than just Properties. We don't want to be adding/removing/mutating dynamic elements during state setting, or also updating observable arrays etc. So we will likely not be able to cover all usages. For example these usages in ProjectileMotion don't seem to be fixed by the patch. Index: js/common/model/ProjectileMotionModel.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/common/model/ProjectileMotionModel.js b/js/common/model/ProjectileMotionModel.js
--- a/js/common/model/ProjectileMotionModel.js (revision 5fab9a1da11a852d6c6eb12e79e1ce3190bde26d)
+++ b/js/common/model/ProjectileMotionModel.js (date 1661366912287)
@@ -12,8 +12,8 @@
import Emitter from '../../../../axon/js/Emitter.js';
import EnumerationProperty from '../../../../axon/js/EnumerationProperty.js';
import NumberProperty from '../../../../axon/js/NumberProperty.js';
-import VarianceNumberProperty from '../../../../axon/js/VarianceNumberProperty.js';
import Property from '../../../../axon/js/Property.js';
+import VarianceNumberProperty from '../../../../axon/js/VarianceNumberProperty.js';
import Range from '../../../../dot/js/Range.js';
import Vector2 from '../../../../dot/js/Vector2.js';
import EventTimer from '../../../../phet-core/js/EventTimer.js';
@@ -302,20 +302,14 @@
// if any of the global Properties change, update the status of moving projectiles
this.airDensityProperty.link( () => {
- if ( !phet.joist.sim.isSettingPhetioStateProperty.value ) {
- this.updateTrajectoriesWithMovingProjectiles();
- }
+ this.updateTrajectoriesWithMovingProjectiles();
} );
this.gravityProperty.link( () => {
- if ( !phet.joist.sim.isSettingPhetioStateProperty.value ) {
- this.updateTrajectoriesWithMovingProjectiles();
- }
+ this.updateTrajectoriesWithMovingProjectiles();
} );
this.selectedProjectileObjectTypeProperty.link(
selectedProjectileObjectType => {
- if ( !phet.joist.sim.isSettingPhetioStateProperty.value ) {
- this.setProjectileParameters( selectedProjectileObjectType );
- }
+ this.setProjectileParameters( selectedProjectileObjectType );
}
);
} |
I made a couple of adjustments, ensuring the unguarded set was private. Looks great though! Committed below. Thoughts? |
After the commit, instrumented DerivedProperty instances cannot get updated during PhET-iO state set. So I'll add a check for that. |
Love it! |
Here's a chip-away for the remaining cases. The main rule: If there is a guard like
then the guard can probably be removed. The sim state should be tested before (to make sure it was already working OK) and after removal of the guard (to make sure it still works correctly after the change). I'll do a few examples then bring it to dev meeting for kicking off a chip-away.
Also the names above are from my memory and partly from the responsible devs list when I didn't recall. That doesn't mean the responsible dev needs to be the implementer---we can adjust accordingly at the dev meeting. UPDATE: The cases in axon and center-and-variability could not be removed. But both in gravity-and-orbits could be removed, please see the commits below. Ready for team discussion and chip-away. |
Self assigning to chip away. |
Discussed with @samreid. I'm relucant to remove |
In ReadOnlyProperty.ts: private unguardedSet( value: T ): void {
if ( !this.isDisposed ) {
if ( this.isDeferred ) {
this.deferredValue = value;
this.hasDeferredValue = true;
}
else if ( !this.equalsValue( value ) ) {
const oldValue = this.get();
this.setPropertyValue( value );
this._notifyListeners( oldValue );
}
}
} Why are we not checking |
@zepumph I'm unclear about whether the deferred value can be the same. What do you think? |
@samreid will document this. |
We determined today that this was not worth the time to go back to change/remove this workaround. It also has a low rate of success (most aren't removed). As a result, we will document this nuance in PhET-iO doc, and try to be more sparring with usages of |
Doc updated. Closing |
Just noting here that we the guard on ReadOnlyProperty added here was WAY too powerful, and we had to tamper it down to allow Properties to be changed during stateSetEmitter over in https://github.com/phetsims/phet-io/issues/1958. |
Signed-off-by: Michael Kauzmann <michael.kauzmann@colorado.edu>
@pixelzoom and I were got by this no-op today in |
@marlitas and I discovered this during working on Mean: Share and Balance and @zepumph says it sounds promising. Instrumented stateful Properties should only accept values from the state during state set.
Hopefully we can get rid of all of the manual guards for
joist.isSettingPhetioStateProperty
The text was updated successfully, but these errors were encountered: