Closed samreid closed 3 years ago
The Proxy-based solution is working well, I think it has compatibility with Array API. I also added adapters to give compatibility with ObservableArray API to aid in migration. I took it for a test drive in Natural Selection, and it was straightforward to convert ObservableArray => createArrayProxy. Even though this doesn't use subtyping, there is a way to simulate BunnyArray (not as nice as subclassing, but it seems workable).
To try to understand the performance implications of the Proxy based approach, I used it in a central point in scenery, like so:
I changed Node.js from:
this._children = []; // {Array.<Node>} - Ordered array of child Nodes.
to
this._children = createArrayProxy(); // {Array.<Node>} - Ordered array of child Nodes.
Note: this is not meant to be a realistic simulation of what it would be like if createArrayProxy
was used in a few places in simulation specific code, but rather as a "worst case scenario". I ran natural selection with ?secondsPerGeneration=1 and pressed "Add a Mate". On the left is []
and on the right is createArrayProxy
, which does all of the element emitter work in addition to the overhead of Proxy.
For instance, with []
, getChildren
takes 3.1 seconds of self-time and with createArrayProxy
it takes 7.6% of self-time.
Subjectively, performance in natural selection looked smooth in both cases. I also measured that there are 1644 of those arrayProxy allocations in this test, and that number didn't increase over bunny generations.
EDIT: Also this performance test used the Natural Selection patch from the preceding comment (Natural Selection uses createArrayProxy instead of ObservableArray).
I also batch converted all new ObservableArray
to createArrayProxy
and that helped catch a few bugs, fixed above. Many sims are passing aqua fuzzing. Also, I was interested to see that when making that swap, all of the ObservableArray unit tests passed (using createArrayProxy
instead of ObservableArray
). I noticed that the reduce
arguments are swapped--that will need to be rectified.
I wrote to @jonathanolson on slack:
Can we schedule a time to discuss createArrayProxy.js? It is working well, but I’d like to get your recommendations on how to improve its performance and how to run pperformance testing.
Also a reminder not to make common code changes until EFAC is published, since master and a branch are being maintained in parallel.
Some notes about potential performance improvements:
Array
and ObservableArray
methods. targetArray
@jonathanolson and I ran a deoptimization in Sim.js like so:
const a = createArrayProxy();
a.push( 'hello' );
a.push( 'hello' );
a.push( 'hello' );
_.map( a, t => t );
_.indexOf( a, 'bye' );
_.indexOf( a, 'hello' );
_.uniq( a );
_.some( a, t => t === 'hello' );
_.includes( a, 'hello' );
const b = new Node();
const childArray = createArrayProxy();
const childArray2 = createArrayProxy();
childArray.push( new Text( 'hello' ) );
childArray2.push( new Text( 'bye' ) );
b.children = childArray;
b.accessibleOrder = childArray2;
Then we compared using the snapshot-comparison tool with 100 frames (instead of 10).
Firefox:
Chrome:
Safari:
On Chrome, replacing every ObservableArray with createArrayProxy:
For all these tests, createSnapshot has:
for ( let i = 0; i < 100; i++ ) {
sendFuzz( 100 );
sendStep( random.nextDouble() * 0.5 + 0.016 );
}
Tests from the previous comment had this in BASEModel:
for (let i=0;i<10000;i++){
_.indexOf(myArray,'hello');
}
But this test didn't have it.
Summarizing results from discussion with @jonathanolson:
Other features to make sure we handle:
myArray.length=0; myArray[5]='hello'
We discussed making sure we have robust support for cases like: myArray.push = 'hello';
but we couldn't think of a performance-optimized way to do this, so we would likely say we do not cover this case.
For my notes: the test harness is http://localhost/main/aqua/html/snapshot-comparison.html?sims=acid-base-solutions,gravity-and-orbits,natural-selection,balloons-and-static-electricity
I ran another before/after test on Firefox, and the results were much closer together (this test also does not have
for (let i=0;i<10000;i++){
_.indexOf(myArray,'hello');
}
in BaseModel.js
To check reproducibility of the results before I start performance improvements for createArrayProxy
, I tested with and without the deoptimizations in Sim.js in Firefox 3x each:
Firefox no deoptimizations
82336
82435
82026
Firefox with deoptimizations 83526 82385 84953
I tried moving pop
to a new methods
object like so:
Index: js/createArrayProxy.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- js/createArrayProxy.js (revision 7e1725fe33b989e88dd6ea36dd32e8a47668fa1e)
+++ js/createArrayProxy.js (date 1600795064717)
@@ -62,6 +62,7 @@
tandem: options.tandem.createTandem( 'elementRemovedEmitter' ),
parameters: [ emitterParameterOptions ]
} );
+ elementRemovedEmitter.addListener( e=>console.log('removed '+e));
// @public (read-only) observe this, but don't set it. Updated when Array modifiers are called (except array.length=...)
const lengthProperty = new NumberProperty( 0, {
@@ -72,8 +73,24 @@
const targetArray = [];
+ const methods = {
+ pop() {
+ const initialLength = targetArray.length;
+ this.lengthProperty.value = this.length;
+ // debugger;
+ const returnValue = Array.prototype.pop.call( this );
+ initialLength > 0 && this.elementRemovedEmitter.emit( returnValue );
+ return returnValue;
+ }
+ };
+
const arrayProxy = new Proxy( targetArray, {
get: function( target, key, receiver ) {
+
+ if ( methods.hasOwnProperty( key ) ) {
+ return methods[ key ];
+ }
+
const value = target[ key ];
if ( typeof value !== 'function' ) {
return value;
But it has the problem that calling Array.prototype.pop
triggers other notifications so it signifies that the same element is removed twice.
I'm interested to know why that's happening or dig in sometime!
On the left is master, on the right is all ObservableArray ported to createArrayProxy, with the methods factored out, in firefox.
I added tests for the cases described in https://github.com/phetsims/axon/issues/330#issuecomment-696837306, and it is all working OK. I also swapped the order of the parameters in ObservableArray.reduce to make a migration easier. I also addressed a bad phetioType that appeared in charges-and-fields, to make the migration easier.
@jonathanolson can you please review createArrayProxy
and its unit tests? I'm interested in determining whether we can replace all occurrences of AxonArray
and ObservableArray
with this, and eliminate them.
Let's touch base at developer meeting:
Summary of Status:
createArrayProxy
is compatible with the Array API as well as the ObservableArray API--in fact, you can query/replace ObservableArray
with createArrayProxy
and almost everything works.extends ObservableArray
, the desired object and tandem structure can be emulated by "tacking on" children, as described in https://github.com/phetsims/axon/issues/330#issuecomment-695463444Questions and Next Steps
createArrayProxy
? Do we need a typedef?From Dev Meeting on 09/24/20:
@pixelzoom: The timeline for working on this issue should not conflict with Natural Selection 1.2.0. @zepumph mentioned that momentum on this issue is valuable and delaying may slow done further work on this issue.
What other review or investigation should be done before production usage?
@samreid mentioned that @pixelzoom should test drive these changes (~30 minutes). It should also be noted that the phet-io api for ObservableArrayIO has changed and should be reviewed.
@pixelzoom is cleared to work test drive with some help from @samreid.
How to document in JSDoc that a parameter is an object created by createArrayProxy? Do we need a typedef?
@pixelzoom will make a recommendation during the test drive.
From Dev Meeting on 09/24/20:
Can we replace all usages of ObservableArray with this? Can ObservableArray be deleted? Can we replace all usages of AxonArray with this? Can AxonArray be deleted?
Devs are signing off on deleting ObservableArray and AxonArray.
For jsDoc, I think the best way forward would be to create something like ArrayProxyDef.js
, and mark that in jsdoc everywhere that the return value of createArrayProxy
is used.
Self-assigning to write up review instructions, I'll do this in the next day or two.
Some steps you could take as part of the review:
grunt generate-phet-io-api
in natural selection first, to get an initial snapshot of the PhET-iO APIThe following steps 2-5 are in this patch:
Here's a copy of createBunnyArray
by itself in case the patch doesn't apply:
new ObservableArray
with createArrayProxy
, also change the imports.phetioType: ObservableArray.ObservableArrayIO
with phetioType: createArrayProxy.ArrayProxyIO
BunnyArray.js
to createBunnyArray.js
(this part is kind of sketchy, and not very amenable to further subtyping)instanceof ObservableArray
grunt generate-phet-io-api
in natural selection, for comparison. Note the added elementAddedEmitters, etc. and that there are no more ObservableArray. Note that the tandem structure is the same (aside from the new items).createArrayProxy
and eliminate ObservableArray and AxonArray?count
, find
, shuffle
, getArrayCopy
, etc. Note the 2 TODOs are in this category.createArrayProxy
and ArrayProxyIO
?I committed ArrayProxyDef as prescribed by @zepumph and now cases like
* @param {ObservableArray.<Vector2>} observableArray
can be replaced by:
* @param {ArrayProxyDef} arrayProxy
and it has nice support for autocomplete, thanks @zepumph!
After a quick code review...
Many code-review related fixes in the above commits, feel free to review.
A bunch of items to address in the checklist below. I will pause further review/testing until the first item is answered.
[x] AxonArray appears numerous times in createArrayProxy. Why is AxonArray involved at all? This one seems especially wrong:
// @public (read-only) (ArrayProxyIO)
AxonArray.ArrayProxyPhetioObject = ArrayProxyPhetioObject;
395 // @public (read-only) (ArrayProxyIO)
{arrayProxy}
? Why does a type start with a lowercase letter?23 * @param {arrayProxy[]} arrayProxy
assertMutuallyExclusiveOptions
if ( options && options.hasOwnProperty( 'length' ) ) {
assert && assert( !options.hasOwnProperty( 'elements' ), 'options.elements and options.length are mutually exclusive' );
}
288 // console.log( `Changing ${key} (type===${typeof key}), from ${oldValue} to ${newValue}` );
315 // console.log( `deleteProperty ${key}, ${typeof key}` );
removedElements
is set is relatively far away. Add an assertion that you expect removedElements
to be set. removedElements.forEach( element => elementRemovedEmitter.emit( element ) );
{ArrayProxyDef}
?378 * @param {Object} arrayProxy
Thanks, I committed fixes for the recommendations above.
@samreid spent some time with me on Zoom, answering questions, and bringing me up to speed on how Proxy/Reflect are used. Then we reviewed my "test drive" in Natural Selection.
Next step will be to merge my Natural Selection test drive into master, and replace some of the methods that are specific to ObservableArray. Then @samreid will address REVIEW comments and globally replace ObservableArray.
@samreid and I spent a little more time on Zoom, working on issues related to WebStorm type recognition.
In an example like this:
1 /**
2 * @param {Array} array
3 */
4 function doSomething( array ) {...}
5
6 const arrayProxy = createArrayProxy( ... );
7
8 doSomething( arrayProxy );
... WebStorm flags the arrayProxy
argument on line 8 with "Argument type ArrayProxyDef is not assignable to parameter type Array". So it doesn't know that ArrayProxyDef is an Array.
NS has examples like this in PopulationModel.js (calls to recordCounts
) and DataProbe.js (calls to this.getCount
). In those cases, the Array element type is specified (e.g. {Array.<Vector2>}
) which may be a further complication.
We don't think this is a deal breaker, but it needs more investigation.
createArrayProxy is causing a performance problem in Natural Selection. See https://github.com/phetsims/natural-selection/issues/252#issuecomment-700326821.
In slack, I said:
This seems to work OK, until we get the typedef working:
class ArrayProxyDef extends Array { // eslint-disable-line
constructor() {
assert && assert( false, 'This class is for documentation and Webstorm support only and should not be instantiated' );
super();
this.lengthProperty = new NumberProperty( 0 );
this.elementAddedEmitter = new Emitter();
this.elementRemovedEmitter = new Emitter();
}
}
@pixelzoom replied:
That looks dangerous (even with the assertion). I don't think we want an entire Array subclass masquerading/paralleling createArrayProxy. I'd rather live with the WebStorm complaints.
Next steps discussed with @samreid on Slack:
Sam Reid 10:53 AM Do you think you will be able to address https://github.com/phetsims/axon/issues/330#issuecomment-700277191 before you leave? Also, that comment makes it sound like I should wait for NS before I take further steps. Is that accurate?
Chris Malley 11:07 AM Assuming that performance in NS has been resolved (see https://github.com/phetsims/natural-selection/issues/252#issuecomment-702270823)... I don't think that https://github.com/phetsims/axon/issues/331 is a deal breaker. Looking at REVIEW comments for createArrayProxy, this one seems like it should be addressed before proceeding with replacement throughout sims:
//REVIEW https://github.com/phetsims/axon/issues/330 wondering if createArrayProxy is the best name for this.
// It exposes the details of how it's implemented (Proxy) and there could be other Proxy implementations that
// add other features. It would be better if the name described what features it adds to Array, not how those
// features are added. I unfortunately can't think of better name than createObservableArray.
Sam Reid 11:10 AM
I’m OK with createObservableArray
and ObservableArrayDef
, I can’t think of something better
Sound good to you?
Chris Malley 11:10 AM Yes, I think that's the best option. "observable array" definitely is the best description of what this is.
From https://github.com/phetsims/axon/issues/330#issuecomment-700277191:
Next step will be to merge my Natural Selection test drive into master, and replace some of the methods that are specific to ObservableArray. Then @samreid will address REVIEW comments and globally replace ObservableArray.
Natural Selection (master) has been converted to createObservableArray
, see https://github.com/phetsims/natural-selection/issues/252#issuecomment-702350034.
When you rename to createObservableArray
and ObservableArrayDef
, please hit the occurrences of ArrayProxyDef
in NS.
//REVIEW https://github.com/phetsims/axon/issues/330 do we need methods.slice?
I don't think we need to add methods.slice
, since slice
is already defined on the proxy.
I renamed it to createObservableArray and addressed REVIEW issues. Some I responded with a comment above, some moved to side issues. Ready to review my changes, and I'll create a new issue to migrate sims to use this.
Changes looks good. I don't see anything else that needs to be done here, so closing.
From https://github.com/phetsims/axon/issues/311, we would like to create a Proxy-based Array implementation that has full API compatibility, which will make it easier to migrate from plain arrays to arrays that can be observed. We discussed that this solution will likely have these disadvantages:
BunnyArray extends AxonArrayProxy
We agreed it will be OK to not be able to subclass this solution (difficulty in extending Proxy discussed in https://github.com/phetsims/axon/issues/311#issuecomment-682882281), but that we will need to characterize performance to see if it is acceptable. Our main concern is that if one Array Proxy is passed to performance-critical code, it could deoptimize all of its calls.