-
-
Notifications
You must be signed in to change notification settings - Fork 927
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
Recycling heuristics have false positives and false negatives #1653
Comments
@isiahmeadows I'm a bit confused by the tag, AFAICT, the only thing the proposal would break is buggy behavior. Unless you refer to the addition of a field to |
@pygy I swapped the labels. While I was on the bus, I had a similar idea myself, as a basis for other optimizations. (I'll detail them later in a gist.) It was mostly the part regarding changing pooling, though. I have a better idea, though, although it's a little more breaking in some places:
That way, we are using a constant that we can track for our own purposes, and we can keep our appropriate default. Note that performance-wise, it makes no difference. |
Note that we shouldn't pool custom elements by default, since they aren't purely host objects (they do usually execute JavaScript). |
I'd add [bug] as well... False negatives in the heuristics for adding an object to the pool lead to slower diff with correct semantics, but false positives are bugs...
function pooled(status, vnode) {
vnode.pooled = status
return vnode
} Or a If we get it right , And good catch re. custom elements. |
I meant `vnode.state.pooled` instead of `vnode.reuse`. Also, I'm thinking
just relying on that, and leaving the rest of `vnode` out of it, since the
property only needs checked after `onremove`.
…On Tue, Feb 28, 2017, 14:39 Pierre-Yves Gérardy ***@***.***> wrote:
I'd add [bug] as well... False negatives in the heuristics for adding an
object to the pool lead to slower diff with correct semantics, but false
positives are bugs...
vnode.pooled, when non-null, would take precedence on vnode.reuse?
We can skip the vnode.state.pooled transfer, and use a function decorator:
function pooled(status, vnode) {
vnode.pooled = status
return vnode
}
And good catch re. custom elements.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#1653 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AERrBBgwGWOt62jq1V6Xd_ntJw-nzGC2ks5rhHgKgaJpZM4MHJZf>
.
|
To clarify, I mean removing `vnode.reuse`/`vnode.pooled` altogether.
…On Tue, Feb 28, 2017, 15:54 Isiah Meadows ***@***.***> wrote:
I meant `vnode.state.pooled` instead of `vnode.reuse`. Also, I'm thinking
just relying on that, and leaving the rest of `vnode` out of it, since the
property only needs checked after `onremove`.
On Tue, Feb 28, 2017, 14:39 Pierre-Yves Gérardy ***@***.***>
wrote:
I'd add [bug] as well... False negatives in the heuristics for adding an
object to the pool lead to slower diff with correct semantics, but false
positives are bugs...
vnode.pooled, when non-null, would take precedence on vnode.reuse?
We can skip the vnode.state.pooled transfer, and use a function decorator:
function pooled(status, vnode) {
vnode.pooled = status
return vnode
}
And good catch re. custom elements.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#1653 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AERrBBgwGWOt62jq1V6Xd_ntJw-nzGC2ks5rhHgKgaJpZM4MHJZf>
.
|
👎 for adding another reserved name on I don't understand why you'd want |
My other idea is this: Make |
So it would default to Nodes that have either What we could have is a pair of boolean fields, one for the engine, and one for users. The engine flag ( i.e. the logic would be in removeNode: |
@pygy I'd like to clarify:
So what would happen is a single function shouldReuse(vnode) {
if (vnode.reuse != null) return !!vnode.reuse
if (vnode.dom.tagName.indexOf("-") >= 0 && !isSpecialNode(vnode.dom)) return false
if (typeof vnode.state.oncreate === "function") return false
if (typeof vnode.state.onupdate === "function") return false
// etc...
return true
}
// Helper for brevity
function isSpecialNode(node) {
if (node.tagName === "color-profile") {
return node.namespaceURI === "http://www.w3.org/2000/svg"
} else if (node.tagName === "annotation-xml") {
return node.namespaceURI === "http://www.w3.org/1998/Math/MathML"
} else {
return false
}
} |
You can't rely on the presence of hooks at removal time. The |
Oh, good point. We can set the default after |
I think we can get away with a single, non-nullable If the engine sets function initLifecycle(source, vnode, hooks) {
if (typeof source.oninit === "function") source.oninit.call(vnode.state, vnode)
if (typeof source.oncreate === "function") hooks.push(source.oncreate.bind(vnode.state, vnode))
} Would become function initLifecycle(source, vnode, hooks) {
if (typeof source.oninit === "function") source.oninit.call(vnode.state, vnode)
if (typeof source.oncreate === "function") {
vnode.reuse = false
hooks.push(source.oncreate.bind(vnode.state, vnode))
}
} etc... This would allow the hooks that only read from |
@pygy Eh... I still would rather defer resolving the default as late as possible, even if it's via object member. Just in case it's set outside of |
@lhorie I'm trying to make sense of function isRecyclable(old, vnodes) {
if (old.pool != null && Math.abs(old.pool.length - vnodes.length) <= Math.abs(old.length - vnodes.length)) {
var oldChildrenLength = old[0] && old[0].children && old[0].children.length || 0
var poolChildrenLength = old.pool[0] && old.pool[0].children && old.pool[0].children.length || 0
var vnodesChildrenLength = vnodes[0] && vnodes[0].children && vnodes[0].children.length || 0
if (Math.abs(poolChildrenLength - vnodesChildrenLength) <= Math.abs(oldChildrenLength - vnodesChildrenLength)) {
return true
}
}
return false
} In plain English, it returns true when the new Simplified: function test(old, vnodes, pool) {
return Math.abs(pool - vnodes) <= Math.abs(old - vnodes)
}
test(1, 1, 1) // true
test(0, 1, 1) // true, used in most tests
test(0, 1, 4) // false
test(0, 3, 4) // true I don't understand the reason for this. It means that a very large pool will be ignored, for example. I also don't understand why it looks at the children of each first element. o("element nodes are not recycled when the pool is much larger than the new/old vnode lists (non-keyed)", function(){
var initial = [{tag: "div"}, {tag: "div"}, {tag: "div"}, {tag: "div"}]
var empty = []
var next = {tag: "div"}
render(root, initial)
render(root, empty)
o(empty.pool.length).equals(4)
empty.pool.forEach(function(vnode){vnode.dom.wasRecycled = true})
render(root, [next])
o(next.dom.wasRecycled).equals(undefined) // passes, I wish it didn't
})
o("here recycling works:", function(){
var initial = [{tag: "div"}, {tag: "div"}, {tag: "div"}, {tag: "div"}]
var empty = []
var next = {tag: "div"}
render(root, initial)
render(root, empty)
o(empty.pool.length).equals(4)
empty.pool.forEach(function(vnode){vnode.dom.wasRecycled = true})
render(root, [next, {tag: "div"}, {tag: "div"}])
o(next.dom.wasRecycled).equals(true)
}) Edit: couldn't we use something along these lines?
|
75b45a6#diff-000bdfae56251b0715111d2b18c9de3cL349 implements the last suggestion and seems harmless. |
I'm finding it very easy to get into a state where mithril will try to recreate entire portions of the DOM even when literally nothing in the vnode tree has changed. I'm just starting to delve into why but it's clear it has something to do with recycling false positives, possibly due to pools sticking around on vnodes when they shouldn't. It seems like once a vnode is ever recycled, its DOM gets recreated every I applied pygy's diff to the current npm version (1.1.6) and it did not fix the issue. I'll post here again if I learn more about what's causing it. |
@CarlLeth If you have code to repro your issue, that'd be perfect. 👍 |
Ok I tried to trim down my scenario to the bare bones and I was able to recreate the issue. You'll need to put a breakpoint in the removeNode function to see it happening, since measuring it in-place with an onremove event stops the issue from happening. That could actually be a temporary workaround when problem nodes are identified. It happens when columns in a table shift around. To see the issue, add a column, then remove the first column. Calls to redraw will hit removeNode several times despite no difference in the vnode tree. Note that "Keyed Table" is the thing I'm actually trying to draw; everything else is there to help diagnose the issue. https://jsfiddle.net/9myxqwha/2/ Of course it's not actually clear that mithril is recreating any DOM here, but in my situation I think it's responsible for at least a forced reflow. |
That smells like the old tree is being retained when it shouldn't, or something - it's seeing old keys, not new ones. |
I'm not persisting vnodes between render cycles. Just data. The keys are just integers. What makes a key "old"? Shouldn't the vnode corresponding to the same data have the same key? |
I'll look into this tomorrow, thanks for the repro @CarlLeth. |
@CarlLeth What I'm seeing is that it's trying to remove the previously removed nodes twice - to take a row of that table, it's seeing @pygy Forewarning: this is going to be a beast to test. You'll need to add the raw DOM node to the parent DOM node manually after it gets removed from |
This is actually fixed in the here are a reduced example: v1.1.6, next. Keyed nodes can leak memory when recycled, the pool is disabled for them by default in Edit: updated the samples. |
Currently, vnodes are added to a recycling pool if:
typeof vnode.tag === "string"
)vnode.domSize == null
)vnode.dom
invnode.attrs
, at the time of removalThis excludes nodes that could be recycled (fragments and components), and includes nodes that may have had problematic
vnode.attrs
in the past.These heuristics are only applied to vnode that are detached (those where
onbeforeremove
can fire). However, once a vnode has joined the pool, its children are recycled as well even if they don't meet those criteria, thus possibly recycling DOM nodes that have been modified in the hooks.Proposed solution:
vnode.reuse = true
field.false
for trusted nodes and when a problematic hook fires (by modifyinginitLifecycle()
and friends)vnode.reuse
to the pool;updateNodes()
, whenrecycling === true
, verify thatvnode.reuse === true
as well.AFAICT these heuristics would allow to recycle components, fragments and don't have the holes described above.
The text was updated successfully, but these errors were encountered: