WebKit Bugzilla
New
Browse
Log In
×
Sign in with GitHub
or
Remember my login
Create Account
·
Forgot Password
Forgotten password account recovery
NEW
94733
Setting innerHTML can be O(n^2) when selection is inside the replaced content
https://bugs.webkit.org/show_bug.cgi?id=94733
Summary
Setting innerHTML can be O(n^2) when selection is inside the replaced content
Ryosuke Niwa
Reported
2012-08-22 11:23:58 PDT
From
http://code.google.com/p/chromium/issues/detail?id=139552
Open attached example (
http://code.google.com/p/chromium/issues/attachmentText?id=139552&aid=1395520000000&name=test.html&token=_zfslVPKMGj56AZt5m4QuCjHpJ0%3A1345659739164
) and press "Highlight" button 2x It takes around 791ms on my machine (i7 2.8) where in Chrome 15 it only takes 30ms. Now press the "Collapse selection before syntax highlight" which calls: window.getSelection().collapse() before it assigns new innerHTML. When selection is collapsed I get around 30ms. Chrome 15 is around 30-40ms in both cases (slightly better when not checked)
Attachments
Test case (original from Chromium, by jaroslav.benc)
(70.49 KB, text/html)
2013-03-06 19:24 PST
,
Nils Barth
no flags
Details
Test case (revised: simplified, configurable)
(1.11 KB, text/html)
2013-03-11 00:47 PDT
,
Nils Barth
no flags
Details
Test case (revised: various selection types)
(1.75 KB, text/html)
2013-03-11 22:08 PDT
,
Nils Barth
no flags
Details
Initial performance test
(1.77 KB, patch)
2013-03-11 22:20 PDT
,
Nils Barth
no flags
Details
Formatted Diff
Diff
Convert test to order-of-magnitude regression test
(2.46 KB, patch)
2013-03-12 01:45 PDT
,
Nils Barth
no flags
Details
Formatted Diff
Diff
Simplify initialization in test
(2.36 KB, patch)
2013-03-12 18:16 PDT
,
Nils Barth
no flags
Details
Formatted Diff
Diff
Test case (tweak: simpler initialization)
(1.66 KB, text/html)
2013-03-12 18:28 PDT
,
Nils Barth
no flags
Details
Call graph from gperftools (as GraphViz DOT file)
(13.51 KB, text/vnd.graphviz)
2013-04-10 20:34 PDT
,
Nils Barth
no flags
Details
Proposed solution (first draft)
(7.86 KB, patch)
2013-04-10 22:12 PDT
,
Nils Barth
no flags
Details
Formatted Diff
Diff
Show Obsolete
(6)
View All
Add attachment
proposed patch, testcase, etc.
Eric Seidel (no email)
Comment 1
2013-02-20 13:17:56 PST
I wonder if this came from the original HTML5 Parser work. It's not clear to me what the timeframe of this regression is.
Nils Barth
Comment 2
2013-03-06 19:24:03 PST
Created
attachment 191895
[details]
Test case (original from Chromium, by jaroslav.benc) (Uploading original test case.)
Kentaro Hara
Comment 3
2013-03-06 20:31:34 PST
You can also use Performance/Bindings/innerHTML-setter.html for micro benchmarks.
Nils Barth
Comment 4
2013-03-06 21:35:22 PST
Regression is in Chromium r-103242
http://crrev.com/103242
which is a WebKit roll
r96201
:96267 (
r96201
:
r96267
). Revision
r96257
by Ryosuke, fixing:
Bug 62092
- Setting innerText to an empty string on editable div loses focus ...looks suspicious, and is the likely culprit. This change caused a different regression, fixed in
r115460
namely:
Bug 83983
- REGRESSION(
r96257
): Deleting a large amount of text is very slow The changeset message for
r115460
is very instructive: "The change in
r96257
did not cause the performance regression per se, but exposed a problem ... If the container has a very large number of children, we walk the entire list of child nodes in the container simply to find out how many they are." So the concrete issue looks like: Updating selection on (start/end?) child node removal is slow (b/c triggers some underlying slow computation, say offset). Prior to
r96257
the selection was *cleared* if start/end node was deleted, so this wasn't triggered. When this bug was fixed (preserving the selection), it hit the slow computation. Revision
r115460
fixed some of these regressions, but, given this bug, not all. Probably has similar cause and fix to that bug. Other notes: Ryosuke comments at
http://crbug.com/139552#c3
that: "This is probably because we're updating selection on every child node removal." To Kentaro: Thanks for the benchmark ref! I'll next work on building a simple test case, as we'll need it anyway, and then look at which slow code we're hitting.
Nils Barth
Comment 5
2013-03-06 21:44:00 PST
Some links. WebKit roll revision log:
http://trac.webkit.org/log/trunk/?rev=96267&stop_rev=96201
Full logs:
http://trac.webkit.org/log/trunk/?rev=96267&stop_rev=96201&verbose=on
Revision
r96257
for
Bug 62092
- Setting innerText to an empty string on editable div loses focus
http://trac.webkit.org/changeset/96257
Revision
r115460
for
Bug 83983
- REGRESSION(
r96257
): Deleting a large amount of text is very slow
http://trac.webkit.org/changeset/115460
Kentaro Hara
Comment 6
2013-03-06 21:46:09 PST
nbarth: Great discovery! Let's dive into the fix!
Ryosuke Niwa
Comment 7
2013-03-06 21:51:18 PST
(In reply to
comment #4
)
> Regression is in Chromium r-103242
http://crrev.com/103242
> which is a WebKit roll
r96201
:96267 (
r96201
:
r96267
). > > Revision
r96257
by Ryosuke, fixing:
That's not my patch although I reviewed it. The key is to mass-remove nodes as much as possible since the cost of updating selection manifests only at a node removal.
Nils Barth
Comment 8
2013-03-06 22:04:00 PST
(In reply to
comment #7
)
> (In reply to
comment #4
) > > Regression is in Chromium r-103242
http://crrev.com/103242
> > which is a WebKit roll
r96201
:96267 (
r96201
:
r96267
). > > > > Revision
r96257
by Ryosuke, fixing: > > That's not my patch although I reviewed it.
Oops, sorry! (For the record, patch by Una Sabovic)
> The key is to mass-remove nodes as much as possible since the cost of updating selection manifests only at a node removal.
Abstractly, the two natural solutions are: 1. Make updating selection fast 2. Avoid updating selection (e.g., batch node removals) Looking at the solution in
r115460
(by Enrica Casucci ;-) for
Bug 83983
- REGRESSION(
r96257
): Deleting a large amount of text is very slow
http://trac.webkit.org/changeset/115460
...looks like it just speeds up the computation, without needing to make major code changes (i.e., #1). Perhaps we can do something similar here. In terms of numbers, I'm guessing we're hitting an O(n^2) algorithm here, presumably doing something O(n) to update the selection for each of the n child nodes. If we can make updating the selection O(1) without much trouble, this would solve it.
Ryosuke Niwa
Comment 9
2013-03-06 22:17:16 PST
(In reply to
comment #8
)
> (In reply to
comment #7
) > > (In reply to
comment #4
) > > > Regression is in Chromium r-103242
http://crrev.com/103242
> > > which is a WebKit roll
r96201
:96267 (
r96201
:
r96267
). > > > > > > Revision
r96257
by Ryosuke, fixing: > > > > That's not my patch although I reviewed it. > > Oops, sorry! (For the record, patch by Una Sabovic) > > > The key is to mass-remove nodes as much as possible since the cost of updating selection manifests only at a node removal. > > Abstractly, the two natural solutions are: > 1. Make updating selection fast > 2. Avoid updating selection (e.g., batch node removals)
We need to do 2. There's nothing of significance we can do for 1 at this point.
Nils Barth
Comment 10
2013-03-06 22:35:34 PST
(In reply to
comment #9
)
> (In reply to
comment #8
) > > Abstractly, the two natural solutions are: > > 1. Make updating selection fast > > 2. Avoid updating selection (e.g., batch node removals) > > We need to do 2. There's nothing of significance we can do for 1 at this point.
I was afraid you would say that... (>.<)
Nils Barth
Comment 11
2013-03-11 00:47:35 PDT
Created
attachment 192416
[details]
Test case (revised: simplified, configurable) Narrowed down test case -- specific bug is: setting innerHTML with selection is O(n^2) in number of children of *existing* value Notably, clearing innerHTML (setting to "") takes O(n^2) time. This looks very similar to the other regression caused by the original bug:
Bug 83983
- REGRESSION(
r96257
): Deleting a large amount of text is very slow This test case just sets innerHTML to a large number of <br>, then clears it, allowing you to vary the number of children and whether the selection is set or not, seeing performance in each case. With selection set, performance is O(n^2); with selection unset, performance is O(n). For reference: * setting innerHTML is O(n) in number of children of *new* value * div.focus() is O(n) in number of children * window.getSelection().collapse() is O(n) in number of children
Nils Barth
Comment 12
2013-03-11 01:07:59 PDT
(In reply to
comment #1
)
> I wonder if this came from the original HTML5 Parser work. It's not clear to me what the timeframe of this regression is.
Just to reply to this: This isn't related to the HTML5 Parser work; it's just due to a bugfix meaning we preserve a selection (instead of clearing it), hitting a slow code path we hadn't hit before.
Nils Barth
Comment 13
2013-03-11 22:08:28 PDT
Created
attachment 192635
[details]
Test case (revised: various selection types) Revised test case to allow various selection types: None, Caret, and Range. (Code cleanup too, e.g., explicitly setting selection, rather than implicitly setting via focus.) Existing example used Caret selection (from focus); having range (selecting all innerHTML) also quadratic and even slower (factor of about 3).
Nils Barth
Comment 14
2013-03-11 22:20:28 PDT
Created
attachment 192637
[details]
Initial performance test * platform/gtk/TestExpectations: Removing a few expectations for tests that were rolled out in
r145296
. * platform/gtk/editing/pasteboard/paste-text-016-expected.txt: Rebaselining after
r145296
. * platform/gtk/fast/dynamic/002-expected.txt: Ditto. git-svn-id:
http://svn.webkit.org/repository/webkit/trunk@145343
268f45cc-cd09-0410-ab3c-d52691b4dbfc
Ryosuke Niwa
Comment 15
2013-03-11 22:22:14 PDT
Comment on
attachment 192637
[details]
Initial performance test View in context:
https://bugs.webkit.org/attachment.cgi?id=192637&action=review
> PerformanceTests/ChangeLog:10 > + * Parser/innerHTML-setter-selection-long.html: Added.
Since this is asymptotic behavior, you should be able to test it inside LayoutTests/perf/
Nils Barth
Comment 16
2013-03-11 22:27:19 PDT
(In reply to
comment #14
)
> Created an attachment (id=192637) [details]
Sorry, wrong comment (fixed my script). Just uploaded a performance test.
Ryosuke Niwa
Comment 17
2013-03-11 22:30:05 PDT
To clarify my position, please don't add a test for this bug as a performance test. This bug is about an asymptotic behavior, and should be tested by a layout test using magnitude-perf.js.
Kentaro Hara
Comment 18
2013-03-11 22:31:56 PDT
(In reply to
comment #17
)
> To clarify my position, please don't add a test for this bug as a performance test. This bug is about an asymptotic behavior, and should be tested by a layout test using magnitude-perf.js.
nbarth: c.f. See this patch for how to write a layout perf test.
https://bugs.webkit.org/attachment.cgi?id=191094&action=review
Nils Barth
Comment 19
2013-03-11 22:32:31 PDT
(In reply to
comment #15
) (In reply to
comment #17
)
> To clarify my position, please don't add a test for this bug as a performance test. This bug is about an asymptotic behavior, and should be tested by a layout test using magnitude-perf.js.
Thanks, will do! What's the rule for when to put tests in PerformanceTests/ vs. LayoutTests/perf/ ? Is it simply: * asymptotic behavior in LayoutTests/perf/ using magnitude-perf.js * other (performance etc.) in PerformanceTests/
Ryosuke Niwa
Comment 20
2013-03-11 22:44:21 PDT
(In reply to
comment #19
) >
> What's the rule for when to put tests in PerformanceTests/ vs. LayoutTests/perf/ ? > Is it simply: > * asymptotic behavior in LayoutTests/perf/ using magnitude-perf.js > * other (performance etc.) in PerformanceTests/
Yes. Whenever we're testing asymptotic behavior, we should be writing a layout test using magnitude-perf.js.
Nils Barth
Comment 21
2013-03-12 01:45:53 PDT
Created
attachment 192669
[details]
Convert test to order-of-magnitude regression test
Nils Barth
Comment 22
2013-03-12 01:47:44 PDT
(In reply to
comment #20
)
> Whenever we're testing asymptotic behavior, we should be writing a layout test using magnitude-perf.js.
Got it, thanks! Order-of-growth test using magnitude-perf.js uploaded.
Ojan Vafai
Comment 23
2013-03-12 10:39:13 PDT
Comment on
attachment 192669
[details]
Convert test to order-of-magnitude regression test View in context:
https://bugs.webkit.org/attachment.cgi?id=192669&action=review
> LayoutTests/perf/innerHTML-setter-selection-long.html:12 > + var childArray = new Array(magnitude); > + for (var i = 0; i < magnitude; ++i) { > + childArray[i] = "<br>"; > + } > + childList = childArray.join("\n");
Nit: This whole thing can just be: childList = new Array(magnitude).join("<br>\n");
Nils Barth
Comment 24
2013-03-12 18:16:24 PDT
Created
attachment 192848
[details]
Simplify initialization in test
Nils Barth
Comment 25
2013-03-12 18:28:18 PDT
Created
attachment 192850
[details]
Test case (tweak: simpler initialization)
Nils Barth
Comment 26
2013-03-12 18:30:07 PDT
(In reply to
comment #23
)
>>[...long string initialization...] > Nit: This whole thing can just be: > childList = new Array(magnitude).join("<br>\n");
Thanks for the tip -- tests revised! BTW, there's a fenceposting error (join fills in gaps), so you need to add 1 to get it right: childList = new Array(magnitude + 1).join("<br>\n");
Nils Barth
Comment 27
2013-04-10 20:29:11 PDT
I’ve tracked down the cause and have a proposed solution. First, to explain why the current code is O(n^2). Setting innerHTML first removes all the children in the DOM subtree. If there is a selection, it checks each of the n nodes to see if it is in the selection, which involves 3 linear searches, which is O(n) work each (3 * average n/2 ~= 3n/2), for O(n^2) work. See attached DOT file for call graph from gperf-tools; you can see the slow section in the 2 big boxes at the bottom, under compareNode. In detail, at the top we have HTMLElement::setInnerHTML in the middle, in function FrameSelection::respondToNodeModification the line: Range::CompareResults compareResult = range->compareNode(node, ec); gets called for almost every node (except the first, and presumably last). Range::compareNode is the slow function (O(n)): it first walks backwards through the tree (calling previousSibling), then walks forwards twice (calling nextSibling), hence at the bottom previousSibling and nextSibling are the hot spots, in a 1:2 ratio. Stack trace, starting from setInnerHTML (indented section is secondary branch): WebCore::Node::nextSibling() << primary hot spot WebCore::ContainerNode::childNode(unsigned int) const << tertiary hot spot WebCore::Node::childNode(unsigned int) const WebCore::Range::checkNodeWOffset(WebCore::Node*, int, int&) const WebCore::Range::comparePoint(WebCore::Node*, int, int&) const WebCore::Node::previousSibling() << secondary hot spot WebCore::Node::nodeIndex << quaternary hot spot WebCore::Range::compareNode(WebCore::Node*, int&) const WebCore::FrameSelection::respondToNodeModification(WebCore::Node*, bool, bool, bool, bool) WebCore::FrameSelection::nodeWillBeRemoved(WebCore::Node*) WebCore::Document::nodeChildrenWillBeRemoved(WebCore::ContainerNode*) WebCore::willRemoveChildren(WebCore::ContainerNode*) WebCore::ContainerNode::removeChildren() WebCore::replaceChildrenWithFragment(WebCore::ContainerNode*, WTF::PassRefPtr<WebCore::DocumentFragment>, int&) WebCore::HTMLElement::setInnerHTML(WTF::String const&, int&)
Nils Barth
Comment 28
2013-04-10 20:34:21 PDT
Created
attachment 197500
[details]
Call graph from gperftools (as GraphViz DOT file) You can see the slow section in the 2 big boxes at the bottom, under compareNode. A simple DOT viewer is available here:
https://code.google.com/p/jrfonseca/wiki/XDot
Nils Barth
Comment 29
2013-04-10 20:35:06 PDT
Turning to solutions, two options present themselves: 1. Low-level: still check each node against the selection, but update the selection so it doesn’t hit the search 2. High-level: update the selection once for the whole container as a batch, rather than for each child Proposed patch (next post) takes approach #2; resulting code is much clearer than a low-level solution, and avoids many possible problems with trying to optimize updating the selection one node at a time (e.g., what if selection is a caret in the middle of innerHTML, and then it’s set to zero, etc.?); this follows Ryosuke’s rec in
comment #9
:
https://bugs.webkit.org/show_bug.cgi?id=94733#c9
This also yields calculation overhead that is O(1) (in number of children), since we only need to make a single check for if the boundary points of the selection are in the deleted subtree, and resulting code is very fast.
Nils Barth
Comment 30
2013-04-10 20:36:23 PDT
Discussion moved to:
https://code.google.com/p/chromium/issues/detail?id=139552
Nils Barth
Comment 31
2013-04-10 21:58:54 PDT
(CC'ing people who've touched FrameSelection::respondToNodeModification)
Ryosuke Niwa
Comment 32
2013-04-10 22:00:44 PDT
As I mentioned earlier, the solution here is to detect this case and update the selection pre-emptively in setInnerHTML or removeAllChildren.
Nils Barth
Comment 33
2013-04-10 22:12:52 PDT
Created
attachment 197504
[details]
Proposed solution (first draft)
Nils Barth
Comment 34
2013-04-10 22:15:55 PDT
(In reply to
comment #32
)
> As I mentioned earlier, the solution here is to detect this case and update the selection pre-emptively in setInnerHTML or removeAllChildren.
Done in patch (uploaded); hopefully works as is. Otherwise further development against Blink (hopefully easy to port), over at Chromium Issue Tracker:
https://code.google.com/p/chromium/issues/detail?id=139552
Ryosuke Niwa
Comment 35
2013-04-10 22:18:42 PDT
Comment on
attachment 197504
[details]
Proposed solution (first draft) View in context:
https://bugs.webkit.org/attachment.cgi?id=197504&action=review
> Source/WebCore/editing/FrameSelection.cpp:389 > + Position start = m_selection.start(); > + Position end = m_selection.end();
We need to also check base & extent. I'm very skeptical that this will do the right thing in all cases. I'd much prefer adding some flag to respondToNodeModification and handle it there.
> Source/WebCore/editing/FrameSelection.cpp:394 > + if (startRemoved) > + start = Position(container, 0, Position::PositionIsOffsetInAnchor);
Please use firstPositionInNode(container).
> Source/WebCore/editing/FrameSelection.cpp:396 > + end = Position(container, 0, Position::PositionIsOffsetInAnchor);
Ditto.
> Source/WebCore/editing/FrameSelection.cpp:406 > + setSelection(VisibleSelection(), DoNotSetFocus); > + > + clearRenderViewSelection(m_selection.start());
The ordering here matters. This is why respondToNodeModification has a flag for it. Another reason to modify respondToNodeModification instead of duplicating code.
Nils Barth
Comment 36
2013-04-11 00:33:39 PDT
FYI, this has been fixed in Chromium due to a separate change:
https://code.google.com/p/chromium/issues/detail?id=139552
Ryosuke Niwa
Comment 37
2013-04-17 09:59:13 PDT
Please don't assign bugs to other contributors without first asking if he or she is intending to be working on it. As it turns out, I don't intend to work on this bug.
Nils Barth
Comment 38
2013-04-17 15:51:51 PDT
(In reply to
comment #37
)
> Please don't assign bugs to other contributors without first asking if he or she is intending to be working on it. As it turns out, I don't intend to work on this bug.
Sorry about that.
Note
You need to
log in
before you can comment on or make changes to this bug.
Top of Page
Format For Printing
XML
Clone This Bug