Bug 80155 - Frame flattening ASSERT(!needsLayout()) in FrameView::paintContents()
Summary: Frame flattening ASSERT(!needsLayout()) in FrameView::paintContents()
Status: RESOLVED FIXED
Alias: None
Product: WebKit
Classification: Unclassified
Component: Layout and Rendering (show other bugs)
Version: 528+ (Nightly build)
Hardware: Unspecified Unspecified
: P2 Normal
Assignee: zalan
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2012-03-02 05:49 PST by zalan
Modified: 2012-04-05 18:27 PDT (History)
6 users (show)

See Also:


Attachments
test case (204 bytes, text/html)
2012-03-02 05:49 PST, zalan
no flags Details
Patch (8.86 KB, patch)
2012-03-11 12:39 PDT, zalan
no flags Details | Formatted Diff | Diff
Patch (11.98 KB, patch)
2012-03-13 07:02 PDT, zalan
no flags Details | Formatted Diff | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description zalan 2012-03-02 05:49:19 PST
FrameView::paintContents() asserts when frame flattening is on with the following simplified test case (originally is seen on nytimes.com)

<html><head></head><body><iframe src="data:text/html;charset=utf-8,<html><body>test</body></html>" scrolling="no" height="50" width="50"></iframe><script>document.body.offsetHeight</script></body></html>
Comment 1 zalan 2012-03-02 05:49:58 PST
Created attachment 129891 [details]
test case
Comment 2 zalan 2012-03-02 05:52:43 PST
also repro on wired.com
Comment 3 zalan 2012-03-02 07:15:30 PST
the test case has
1, iframe which does not get flattened (fixed width/height + no scrolling)
2, offsetHeight which triggers sync layout

Assert happens, because the iframe remains dirty even after a few layout cycles on both the main and the child frame.

What happens here is
1, first layout cycle triggered by document.body.offsetHeight

offsetHeight -> FrameView::layout(mainframe) -> RenderIFrame::layout(no frame flattening, so child FrameView::layout() is not called) -> FrameView::performPostLayoutTasks() -> RenderWidget::updateWidgetPosition() -> FrameView::layout(childframe) -> (nothing to layout yet)
(after return back from layout(childframe) a recursive layout call on the mainframe leaves pending post layout tasks.)

2, second layout cycle triggered by finished parsing
Document::implicitClose() -> FrameView::layout(mainframe) -> FrameView::performPostLayoutTasks(pending post layout from 1st cycle) -> RenderWidget::updateWidgetPosition() -> FrameView::layout(childframe) -> nested layout call? no -> start layout from parent frame -> FrameView::layout(mainframe) -> RenderIFrame::layout(no frame flattening, so child FrameView::layout() is not called) -> FrameView::layout(childframe) early returns and it leaves the child frame dirty.

3, third layout cycle triggered by paint

paint -> layoutIfNeeded() -> FrameView::layout(mainframe) -> FrameView::performPostLayoutTasks(pending post layout from 2nd cycle) -> RenderWidget::updateWidgetPosition() -> FrameView::layout(childframe) -> nested layout call? no -> start layout from parent frame -> FrameView::layout(mainframe) -> RenderIFrame::layout(no frame flattening, so child FrameView::layout() is not called) -> FrameView::layout(childframe) early returns and it leaves the child frame dirty.
->assert in paintContents() of the child FrameView()

This bug occurs, when m_postLayoutTasksTimer has no time to hit, before FrameView::layout() is called. 
Calling postlayout task from FrameView::layout() ends up calling FrameView::layout(childframe) with a state (m_nestedLayoutCount == 0) where child frameview always initiates the layout from the top and since the RenderIFrame is not calling back to the child FrameView::layout(), child frame remains dirty.
(The forced sync layout with offsetHeight at the beginning is needed in order to start the 2nd cycle with pending postlayout tasks.)
Comment 4 zalan 2012-03-03 12:37:41 PST
There seems to be 2 issues with frame flattening code in this context:
1, FrameView::layout(childframe) wrongly assumes that if layout is re-initiated from parent (parentView->layout(allowSubtree);), it will eventually come back to the child frame to finished off layouting. If the associated iframe is not getting flattened, it is absolutely unnecessary to call on parentView->layout() for layouting purposes. (and it still leaves childframe dirty as FrameView::layout(childframe) is not getting called from RenderIFrame::layout()). The flattening code should be smarter to not to initiate parent layouts, if possible.

2, Good amount of the FrameView::layout(childframe) calls are originated from parentView->performPostLayoutTasks(). Now, performPostLayoutTasks() is called from layout() at multiple places with different nesting state, depending whether this is a pending postlayouttask to be executed before root->layout() (no nesting), or normal sync postlayout after root->layout() (yes nesting). layout(childframe) relies on the parent's nesting state (m_nestedLayoutCount) to decide, if layout should be re-initiated from parent. In both cases, we are in parent's layout, and still child FrameView::layout() behaves differently.
Comment 5 zalan 2012-03-03 12:47:07 PST
btw, this is not reproducible on mac port all the time due to timing issues. (m_postLayoutTasksTimer hits before the 2nd layout is initiated by documentClose())
Comment 6 zalan 2012-03-11 12:39:25 PDT
Created attachment 131255 [details]
Patch
Comment 7 Antti Koivisto 2012-03-12 09:20:07 PDT
Comment on attachment 131255 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=131255&action=review

r=me

> Source/WebCore/ChangeLog:9
> +        This patch ensures that iframes only start layouting from the topmost parent,
> +        when the they are going to be flattened.

It would be good to explain how it ensures it.
Comment 8 Antti Koivisto 2012-03-12 09:23:33 PDT
Comment on attachment 131255 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=131255&action=review

> LayoutTests/fast/frames/flattening/iframe-flattening-fixed-width-and-height-no-scrolling-with-js-forced-layout.html:5
> +<html>
> +<head>
> +    <script type="text/javascript">
> +        if (window.layoutTestController) {
> +            layoutTestController.waitUntilDone();

This should use layoutTestController.dumpAsText(). The point is to (not) hit the assert, the output is not important. This would probably not be cross-platform.
Comment 9 Philippe Normand 2012-03-12 09:52:17 PDT
*** Bug 65530 has been marked as a duplicate of this bug. ***
Comment 10 zalan 2012-03-13 07:02:41 PDT
Created attachment 131604 [details]
Patch
Comment 11 Antti Koivisto 2012-03-14 09:58:24 PDT
Comment on attachment 131604 [details]
Patch

r=me
Comment 12 WebKit Review Bot 2012-03-14 13:23:10 PDT
Comment on attachment 131604 [details]
Patch

Clearing flags on attachment: 131604

Committed r110738: <http://trac.webkit.org/changeset/110738>
Comment 13 WebKit Review Bot 2012-03-14 13:23:15 PDT
All reviewed patches have been landed.  Closing bug.
Comment 14 Martin Robinson 2012-04-05 18:19:08 PDT
(In reply to comment #10)
> Created an attachment (id=131604) [details]
> Patch

Zalan, it makes gardening a bit easier when you unskip tests upon fixing a bug. I'll unskip the faililng tests for GTK+ now.
Comment 15 zalan 2012-04-05 18:25:12 PDT
(In reply to comment #14)
> (In reply to comment #10)
> > Created an attachment (id=131604) [details] [details]
> > Patch
> 
> Zalan, it makes gardening a bit easier when you unskip tests upon fixing a bug. I'll unskip the faililng tests for GTK+ now.

Yeah, sorry. Thanks for doing it.
Comment 16 Martin Robinson 2012-04-05 18:27:45 PDT
(In reply to comment #15)

> > Zalan, it makes gardening a bit easier when you unskip tests upon fixing a bug. I'll unskip the faililng tests for GTK+ now.
> 
> Yeah, sorry. Thanks for doing it.

Not a problem. Thanks for fixing the bug!