-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Faster row swapping for keyed updates #588
Comments
The problem is the do loop that flags everything for deletion until the right node is met. In #373 the proposal was to mark for deletion only if the item to be inserted matches the actual pointer's next sibling, indicating single intruder.
I don't know the build system enough to be able to test the change. Is there a quick way to make changes and test them? (BTW i'm not using the |
For info, I decided to take a stab at it. BTW is there an easy way to run a single test without hacking into the |
@hville You can add |
I just can't understand the intro-outro logic in EachBlock.js#L254
This line gets fired for every child |
@hville thanks for taking a poke at this!
Yes — an iteration may exist but be in the process of outroing: [ a b c ] # a.intro(), b.intro() and c.intro() fade elements in (or whatever)
[ a c ] # a.intro() and c.intro() are no-ops. b still exists, but is outroing
[ a b c ] # a.intro() and c.intro() and no-ops. b.intro() aborts the outro We could avoid the no-op calls with some extra book-keeping but it's probably not worth the effort. |
Thanks. That's how I thought it was working but I am still puzzled by a bug I introduced that is linked to this very case All tests and permutations without transitions pass but I am stuck at In any case, I am not sure it is an idea worth considering further since the transitions require that deleted nodes stay where they are as much as possible.
Above is the behaviour of the current "delete or move forward" algorithm. What I was proposing improves 4. at the expense of 6., the more common case |
About performance, this is the ultimate challenge for Svelte. In the last benchmark posted on November 20, Svelte is the second fastest in non-keyed results, and I wish I could have enough knowledge to help with this inner detail and allow Svelte to be also in second for keyed results. :) |
Interestingly, the implementation of row swap in Bobril is even faster than vanilla. Maybe @Bobris (Boris Letocha) can tell us his secret. |
localvoid written very detailed blog about it here: Bobril also virtualize events se there are no dom listeners actually attached directly to rows. |
I'm not an expert in JavaScript, so I don't know what algorithm goes/went wrong that the swapping rows benchmark is slow. Is it solved now? (By the way, the use case I see for swapping the rows is like in Facebook chat, that some slow client sent a chat message to a fast client. S/he typed the message and pressed enter before the faster client, but the message reached later, because of the slow connection, but the server somehow knows that the message from slower client was earlier, so the app will swap those two rows, but only two rows, not thousands of rows like in that benchmark, still I'd love to see a faster result for this benchmark for svelte, this slower benchmark actually degrades the fame of svelte). |
I took a quick stab at speeding up the benchmark by optimizing the update function for the #each loop the generated code & found a near 10x speedup: From
To
https://gist.github.com/btakita/6a40207ca263490a599caa3d39ea3ed4 Here's the component for the benchmark. https://github.com/krausest/js-framework-benchmark/blob/master/svelte-v1.41.2-keyed/src/Main.html In the swap benchmark, there are 1000 keyed rows. Position 2 & 999 are swapped. The benchmark is almost a worst case scenario for the current output, as the DOM nodes between position 2 & 1000 are thrown away & recreated. This optimization reuses all of the DOM nodes. I'm not sure that this does not break anything else. This is simply a proof of concept. From the gist above: p: function update(changed, state) {
var data = state.store.data;
var each_expected = each_head;
var each_last = null;
// <<new
var rendered = {};
var all = {}
// new>>
// var discard_pile = [];
// <<new
var each_all = each_head
while(each_all) {
all[each_all.key] = each_all
each_all = each_all.next
}
// new>>
for (i = 0; i < data.length; i += 1) {
var key = data[i].id;
var each_iteration = each_lookup[key];
var each_context = assign({}, state, {
data: data,
row: data[i],
num: i
});
if (each_iteration) { each_iteration.p(changed, each_context); }
if (each_expected) {
if (key === each_expected.key) {
each_expected = each_expected.next;
} else {
if (each_iteration) {
// // probably a deletion
// while (each_expected && each_expected.key !== key) {
// each_expected.discard = true;
// discard_pile.push(each_expected);
// each_expected = each_expected.next;
// }
// <<new
var next_data = data[i+1];
var next = next_data && each_lookup[next_data.id];
var first = each_iteration.first;
var first_next = next && next.first;
insertNode(first, tbody, first_next);
each_expected = next;
each_iteration.next = each_expected;
var prev_data = data[i-1]
var prev = prev_data && each_lookup[prev_data.id];
prev.next = each_iteration;
// new>>
// each_expected = each_expected && each_expected.next;
// each_iteration.discard = false;
// each_iteration.last = each_last;
// if (!each_expected) { each_iteration.m(tbody, null); }
} else {
// key is being inserted
each_iteration = each_lookup[key] = create_each_block(component, key, each_context);
each_iteration.c();
each_iteration.m(tbody, each_expected.first);
each_expected.last = each_iteration;
each_iteration.next = each_expected;
}
}
} else {
// we're appending from this point forward
if (each_iteration) {
// each_iteration.discard = false;
each_iteration.next = null;
each_iteration.m(tbody, null);
} else {
each_iteration = each_lookup[key] = create_each_block(component, key, each_context);
each_iteration.c();
each_iteration.m(tbody, null);
}
}
// <<new
if (each_iteration) {
rendered[each_iteration.key] = each_iteration
}
// new>>
if (each_last) { each_last.next = each_iteration; }
each_iteration.last = each_last;
each_last = each_iteration;
}
if (each_last) { each_last.next = null; }
// <<new
for (var key_all in all) {
if (!rendered[key_all]) each_destroy(all[key_all]);
}
// new>>
// while (each_expected) {
// each_destroy(each_expected);
// each_expected = each_expected.next;
// }
//
// for (i = 0; i < discard_pile.length; i += 1) {
// var each_iteration = discard_pile[i];
// if (each_iteration.discard) {
// each_destroy(each_iteration);
// }
// }
each_head = each_lookup[data[0] && data[0].id];
} |
For what it's worth, I came across Svelte because I saw these benchmarks awhile ago. Svelte was one of the fastest and had the lowest memory consumption. I know benchmarks are not 1-1 with real world performance, but they are a great marketing tool. Anything we can do to put Svelte in the green for round 8 of these benchmarks would help Svelte stand out from the noise of virtual doms. Anything that doesn't negatively affect real-world usage of Svelte of course! 😄 |
Did a straight-up quick conversion of @btakita 's code to Let me know if something's wrong with my conversion, but it's also likely the original proposed change doesn't handle all cases, as the author already mentioned. AFAICT there have been multiple serious attempts at this issue in the past; it's a hard problem. https://github.com/talklittle/svelte/tree/test-btakita-588 |
Not surprised there's failures. It looks like there's some issues with the deletion logic. I didn't spend much time on the deletion logic as I was focusing on the swap benchmark. My understanding is that this issue was not a priority because it's rare to see it in production code. |
I don't think it is rare. A chat app with a list of friends sorted by online status would see a friend move from the top to the bottom when they go offline. Of course, the benchmark uses 10,000 rows which may be rare. But it is still a big marketing piece. I'm sold on Svelte, but I'd like to see it last, which means more people using and contributing to it. |
* Introduced jsdom.VirtualConsole
I fixed a test. Still working on the rest...
|
* Introduced jsdom.VirtualConsole
@talklittle I'm only running one of the samples in that test suite. Unfortunately, it's still failing. Sorry about the confusion. talklittle@5f0bfad#diff-02c1a328e99315bf82acb8fc1747f0aaR214 |
I fixed the tests:
This test is still broken:
The majority of the changes are in: I have some questions re: outros. It seems like outros block a DOM element being detached until it's completion. From the tests, I'm not able to tell if move & insert operations wait for the outro completions. Would all of the delete outros this also apply to move & insert operations? In the current implementation of the https://github.com/btakita/svelte/tree/issues/588 branch, the destroy outros block the move & add operations. I separating the data graph reconciliation from the DOM manipulation. Deletes occur first to make inserts & moves simpler. Just one more test to go :-) ${fn_destroy}(${all}, ${keep}, function() {
// Work backwards due to DOM api having insertBefore
for (#i = ${each_block_value}.${length} - 1; #i >= 0; #i -= 1) {
var data = ${each_block_value}[#i];
var ${key} = data.${this.key};
${iteration} = ${lookup}[${key}]
if (${inserts}[${key}] || ${moves}[${key}]) {
var first_next = ${next_iteration} && ${next_iteration}.first;
if (${inserts}[${key}]) {
${inserts}[${key}].${mountOrIntro}(${updateMountNode}, first_next);
} else if (${moves}[${key}]) {
${moves}[${key}].m(${updateMountNode}, first_next);
}
}
${next_iteration} = ${iteration};
}
}) |
* Introduced jsdom.VirtualConsole
Ok, I fixed the build. Some cleanup is probably in order before the PR though. I'll get to it either tonight or this weekend. |
* Introduced jsdom.VirtualConsole
* Introduced jsdom.VirtualConsole
* Introduced jsdom.VirtualConsole
* Introduced jsdom.VirtualConsole
* Introduced jsdom.VirtualConsole
* Introduced jsdom.VirtualConsole
* Introduced jsdom.VirtualConsole
* Performance Improvement with Keyed EachBlock * All DOM nodes for existing data are reused between changes to state * Speed up Keyed Swap Rows Benchmark * https://github.com/krausest/js-framework-benchmark * Fixed Build * Introduced jsdom.VirtualConsole
* Performance Improvement with Keyed EachBlock * All DOM nodes for existing data are reused between changes to state * Speed up Keyed Swap Rows Benchmark * https://github.com/krausest/js-framework-benchmark * Fixed Build * Introduced jsdom.VirtualConsole
* Performance Improvement with Keyed EachBlock * All DOM nodes for existing data are reused between changes to state * Speed up Keyed Swap Rows Benchmark * https://github.com/krausest/js-framework-benchmark * Fixed Build * Introduced jsdom.VirtualConsole
* Performance Improvement with Keyed EachBlock * All DOM nodes for existing data are reused between changes to state * Speed up Keyed Swap Rows Benchmark * https://github.com/krausest/js-framework-benchmark * Fixed Build * Introduced jsdom.VirtualConsole
Released 1.58 with @btakita's changes! 🎉 I'll prepare a pull request for js-framework-benchmark. |
Congratulations on getting it working, @btakita! That's huge! |
@Rich-Harris I've updated a local copy of the js-framework-benchmark svelte keyed benchmark to use svelte 1.58.0, but I'm currently not able to reporduce the speedup in the swap rows benchmark. |
Huh, that's very odd — I ran the benchmarks right before publishing and it was close to vanilla speed. I may have messed something up. Reopening pending an investigation, thanks |
The relative perfomance of svelte was 1.48 in round 7 of the js-framework-benchmark. |
This is fixed in 1.58.2 — closing again |
The results are in for the latest version of Svelte on js-framework-benchmark. Svelte is among the fastest frameworks on most tests (there's really not much between them at the faster end of the table), is the fastest to start up, and uses the least memory. All in all, not bad.
But there's one place we don't perform that well, and it's dragging our overall score down badly — swapping rows. I'm not sure how common it is to swap rows in a list, but if there's a way we can tweak the keyed updates so that it doesn't perform so badly, without adding too much code, then we should.
The text was updated successfully, but these errors were encountered: