-
-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Rework component update logic avoiding memory allocations #5474
Conversation
@@ -340,31 +340,6 @@ suite('registerPrimitive (using innerHTML)', function () { | |||
}); | |||
}); | |||
|
|||
test('handles primitive created and updated in init method', function (done) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test was specifically introduced to cover a regression, however I now believe the covered behaviour is actually a bug. The partial combining of object-based single-property data only occurs when applied before the entity is initialized, not after. This inconsistent behaviour isn't ideal, so either it should always combine or never.
If backwards compatibility to this extend is needed, I can introduce a quirk that explicitly causes the old behaviour when updating a object-based single property before initialization.
assert.equal(updateStub.getCalls()[0].args[0].x, undefined); | ||
assert.equal(updateStub.getCalls()[0].args[0].y, undefined); | ||
assert.equal(updateStub.getCalls()[0].args[0].z, undefined); | ||
assert.deepEqual(updateStub.getCalls()[1].args[0], {x: 1, y: 1, z: 1}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test has been re-arranged since the captured values are by reference. Properties on oldData are only valid for the duration of update
. Now the relevant assertions are taken during the update
.
On master this tests 'works' because the referenced object hasn't been changed yet. Making the chain longer (say three updates), it also shows the same problem. Now this is a small behavioural change, but users shouldn't be keeping references to either data or oldData even before this change.
@@ -494,7 +510,7 @@ suite('Component', function () { | |||
}); | |||
|
|||
el.hasLoaded = true; | |||
el.setAttribute('dummy', ''); | |||
el.setAttribute('dummy', 'color: red'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The value of attrValue
is only set once a value is set. Without setting any properties, attrValue
isn't allocated (saving memory). This test inspects the attrValue
which is internal and thus this change
Furthermore the test only cares about whether or not the selector property el
is being cloned or not. So setting a different property to ensure attrValue
is allocated has no bearing on the tested behaviour.
@@ -27,7 +27,7 @@ suite('Component', function () { | |||
}); | |||
}); | |||
|
|||
suite('buildData', function () { | |||
suite('recomputeData', function () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The outcome of recomputeData
is roughly the same as buildData
. One difference being that recomputeData
doesn't return the updated data value, requiring these test cases to be changed accordingly.
@@ -216,6 +217,44 @@ suite('a-mixin', function () { | |||
}); | |||
}); | |||
}); | |||
|
|||
suite('parseComponentAttrValue', function () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved and adapted from component.test.js
where it tested the parseAttrValue
method. Since this PR causes it to only be used by a-mixin
, the tests were moved as well.
needs rebase |
de7fd27
to
2574d71
Compare
needs rebase |
2574d71
to
80c7a19
Compare
Is still the plan to break this PR on smaller ones or should I take it as is? |
This is done? |
The PR is in a complete state, so you could start reviewing it. I know I said I would open a PR with additional tests, but sadly haven't gotten around to it. That shouldn't change the implementation, though, mostly cover the bugfixes listed above. |
Thanks! Great work! |
Haven't had a chance to look in detail, but this looks like a substantial improvement to an area of A-Frame that was rather patchy and inconsistent (and hence frequently hard to work with) Thank you @mrxz. Awesome vision and execution! |
Thanks, I'm quite pleased with how it turned out. It's still a complex part of A-Frame, simply by the fact that there a so many possible code paths when updating components, but the code should be easier to follow and behave more consistent. Some inconsistency (e.g. between single- and multi-prop) have explicitly been added for backwards compatibility and now "stick out" instead of being unintended side-effects of the different implementations. In any case, if you do happen to look at it in more detail or notice any changes or regressions after updating, let me know. |
Description:
The component update logic is very complicated due to the many cases it needs to take care of. There are multi-property, object-based single-property and single-property components. The schema for multi-property components may be dynamic. There are three methods used to update (or as a reaction to an update):
updateProperties
,resetProperty
andhandleMixinUpdate
. TheupdateProperties
accepts both CSS-style strings as objects, and optionally clobbers. Combined this means that there are 6 update interactions on 4 different component types, resulting in 24 different cases. And that's not even taking into account the case of updates happening before/after the entity has initialized.The goal of this PR was two-fold: making the component update logic more readable and reducing memory allocations. As a nice side-effect the overall performance of
setAttribute
has improved a fair bit.type: 'vec3'
)The three methods mentioned above now all effectively follow the following pattern:
Memory changes
parsingAttrValue
. This intermediate object held the (raw) values that would end up onattrValue
. Instead the parsing is done ontoattrValue
(through a proxy object)previousOldData
. InsteadoldData
is passed and if-and-only-if a recursive update occurs, a newoldData
object is retrieved from the objectPool and the old one recycled once done, ensuring it remains valid for the duration of theupdate
call and supports arbitrary nesting (though recursivesetAttribute
isn't ideal)buildData
was handled through the use ofcopyData
andextendProperties
both of whichclone
objects by default, even if said values might not end up in the data. Instead,getComputedProperty
handles the same inheritance chain, but always returns the reference, leaving it up torecomputeProperty
to perform the copy/clone (if needed)parse(Property)
to allow passing in an (optional) target value that can be re-used. This allows theparse
method to handle parsing, cloning and copying in one.string -> output
) and cloning (output -> clone
) it would still work. If instead of cloning it becomes the identity (output -> output
), this only works if re-using by reference isn't in issue for said type. Otherwise it'll be a breaking changedata
intooldData
and from the computed value intodata
Note: Effectively the
data
andoldData
will hold and own object based properties. Theparse
method can both allocate a new one (initially) or re-use the one already present. This is versatile approach as the exact behaviour is up to the propertyType. This also (partially) addresses #5181 as coordinates can interpret Vectors just fine and will copy from them, instead of using references. I believe this was also the original behaviour but broke when Three.js switched to classes.Performance changes
deep-equal
, instead the schema is used to pick the correspondingequals
function per property.data
outside of it's known properties, e.g. adding athis.el.components.dummy.data.someVector.myField = true
won't be picked up as a change (nor appear inoldData
)updateSchema
unless needed. Since the changes are detected on-write, we know when aschemaChange
property has changedBug fixes
resetProperty
andhandleMixinUpdate
did not take schema changes into account, now they doresetProperty
would reset the property to its schema default, ignoring any mixed in values (inconsistent inheritance)parsingAttrValue
, usingsetAttribute
with a CSS-style string could often lead toupdateSchema
being called even when noschemaChange
properties were included or changedNaN
for numeric, single-property component schemas in 0.7.* and 0.8.0 #3442Next steps
This PR ended up being quite large and I can imagine hard to review. In the coming days I'll try to create a separate PR that introduces a bunch of tests (including ones failing on master) to document both the fixes and changes in behaviour.
There are bound to be more behavioural changes when tested in the wild. Especially for users that directly mutate
.data
or even use internal methods or fields.Following this PR, there are a couple of other changes that could follow:
registerSystem
andregisterComponent
, effectively turning systems into components.