phetsims / density-buoyancy-common

Common code for the Density and Buoyancy simulations
GNU General Public License v3.0
0 stars 2 forks source link

Interpolated values should not be read in the model #132

Closed zepumph closed 1 month ago

zepumph commented 5 months ago

From https://github.com/phetsims/buoyancy/issues/64, @samreid and I viewed that InterpolatedProperty instances were being read from inside the engine model step's postStepListener. This is generally a bug, and should instead use the currentValue. We think an assertion will help prevent future errors like this.

Perhaps by wrapping the DBC model step with an InterpolatedProperty.lock/unlock() functionality.

zepumph commented 5 months ago

I was going to commit this, but ran into trouble when a multilink in the view updates because of the liquidVolumeProperty changes during model steps and updates in the view eagerly. This triggers a get() on the InterpolationProperty. This seems buggy to me, since we are mid, engine stepping, the interpolated value will be outdated. This intermediate state may be acceptable, but I couldn't be sure immediately.

https://github.com/phetsims/density-buoyancy-common/blob/b1e21e5edd175ad063c795ef394ccca92948e1ed/js/buoyancy/view/BoatView.ts#L97-L101

```diff Subject: [PATCH] update doc, https://github.com/phetsims/buoyancy/issues/64 --- Index: js/common/model/DensityBuoyancyModel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/js/common/model/DensityBuoyancyModel.ts b/js/common/model/DensityBuoyancyModel.ts --- a/js/common/model/DensityBuoyancyModel.ts (revision 1705c5a5f4b76ec587233cfd62b77e1c1ce5dd53) +++ b/js/common/model/DensityBuoyancyModel.ts (date 1716322373251) @@ -35,6 +35,7 @@ import { PhetioObjectOptions } from '../../../../tandem/js/PhetioObject.js'; import PickRequired from '../../../../phet-core/js/types/PickRequired.js'; import Matrix3 from '../../../../dot/js/Matrix3.js'; +import InterpolatedProperty from './InterpolatedProperty.js'; // constants const BLOCK_SPACING = 0.01; @@ -563,6 +564,7 @@ * Steps forward in time. This is a "simulation step", not a "physics engine step" */ public step( dt: number ): void { + InterpolatedProperty.lock(); this.engine.step( dt ); this.masses.forEach( mass => { @@ -570,6 +572,7 @@ } ); this.pool.liquidYInterpolatedProperty.setRatio( this.engine.interpolationRatio ); + InterpolatedProperty.unlock(); } /** Index: js/common/model/InterpolatedProperty.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/js/common/model/InterpolatedProperty.ts b/js/common/model/InterpolatedProperty.ts --- a/js/common/model/InterpolatedProperty.ts (revision 1705c5a5f4b76ec587233cfd62b77e1c1ce5dd53) +++ b/js/common/model/InterpolatedProperty.ts (date 1716333246385) @@ -28,6 +28,10 @@ }; export type InterpolatedPropertyOptions = SelfOptions & PropertyOptions; +// Lock the ability to read the value of InterpolatedProperties. This is because the interpolated value can be buggy +// if read by certain functionality. Instead, the interpolated value is just for the rendering portion, not the model +let readLockCount = 0; + export default class InterpolatedProperty extends Property { // The most recently set value, but changing this will not fire listeners. @@ -69,10 +73,21 @@ public setRatio( ratio: number ): void { this.ratio = ratio; + const lockCount = readLockCount; + InterpolatedProperty.unlock(); + // Interpolating between two values ago and the most recent value is a common heuristic to do, but it is important // that no model code relies on this, and instead would just use currentValue directly. This is also duplicated from // what p2 World does after step. this.value = this.interpolate( this.previousValue, this.currentValue, this.ratio ); + + InterpolatedProperty.lock(); + assert && assert( lockCount === readLockCount, 'InterpolatedProperty does not support reading other InterpolatedProperties from value set' ); + } + + public override get(): T { + assert && assert( readLockCount === 0, 'Cannot access InterpolatedProperty value when locked. Are you in the model?' ); + return super.get(); } /** @@ -107,6 +122,14 @@ return a.blend( b, ratio ); } + public static lock(): void { + readLockCount++; + } + + public static unlock(): void { + readLockCount--; + } + public static readonly InterpolatedPropertyIO = ( parameterType: IOType ): IOType => { assert && assert( parameterType, 'InterpolatedPropertyIO needs parameterType' );
zepumph commented 4 months ago

@jonathanolson helped us understand the interpolation algorithm that interpolates between previousValue and currentValue (both of which are in the past relative to DT). Using these two values creates a notion of consistent speed, even if it is a bit "delayed". This depends on the notion that the whole view is looking at the interpolated values. So basically the view is one model step behind the view, but with consistency. This prevents jumpiness in exchange for a single step of delay.

Here is some paper trail about why we care about interpolation: https://github.com/phetsims/wave-on-a-string/issues/64

While testing on SR's computer. We found that model steps were running between 1-3 times per step. If there were 20 model steps between DTs, then interpolation is not that big of a deal. If the model fixed time step is greater than DT, then it is much more important to have interpolation to smooth out the timing.

zepumph commented 4 months ago

Oscillation described in https://github.com/phetsims/buoyancy/issues/140#issuecomment-2118481755 is expected by p2 internals, says @jonathanolson.

zepumph commented 4 months ago

Our conversation has made me want to try to continue to think about adding an assertion like this. I can be very buggy to access interpolated values from the model. Let's keep thinking about this.

AgustinVallejo commented 3 months ago

Discussed this today with the Buoyancy Devs and we decided to leave this on hold until some other major changes to the model are in place. Afterwards, it's a matter of double checking if this is fine or not.

samreid commented 1 month ago

I refreshed the patch and committed to main behind a query parameter ?debugInterpolatedProperty. Fuzzing with assertions enabled, it shows stacks that trigger an InterpolatedProperty read during the model step.

In my fuzzing, I saw updateScaleReadoutNode, which is safe. The other one is:

// in Mass.ts
this.stepEmitter.addListener( () => this.contactForceBlendedProperty.step( this.contactForceInterpolatedProperty.value ) );

Which is blending incremental interpolates, which also seems reasonable to me.

@zepumph Want to do anything else here? You could fuzz with &debugInterpolatedProperty&fuzz or we could revert the commit above, or review it.

zepumph commented 1 month ago

I was able to get to a point where this could become an assertion. I don't know that we need this complexity, and noting here that it involved changing a multilink to a stepListener call so that we didn't call get from Multilink. I also turned it on by default so CT can report trouble. What do you think? Have I gone too far?

zepumph commented 1 month ago

I discussed with @samreid, in addition to the above commit:

zepumph commented 1 month ago

Let's see if CT complains before closing

samreid commented 1 month ago

CT showed 8 columns of solid green, nice work, closing.