New issue
Advanced search Search tips
Note: Color blocks (like or ) mean that a user may not be available. Tooltip shows the reason.

Issue 684555 link

Starred by 1 user

Issue metadata

Status: Available
Owner: ----
Cc:
Components:
EstimatedDays: ----
NextAction: ----
OS: ----
Pri: 3
Type: Bug



Sign in to add a comment

Possible code duplication in blink::HTMLElement::offset*ForBinding and blink::Element::offset* ?

Project Member Reported by smcgruer@chromium.org, Jan 24 2017

Issue description

Currently, blink::HTMLElement and blink::Element both implement methods for calculating element offsets. These are named offset{Top/Left/Height/Width}ForBinding in HTMLElement, and offset{Top/Left/Height/Width} in Element. Note that HTMLElement inherits from Element, so it also has access to the non-suffixed methods.

To me, it appears that the implementation of these methods are *nearly* functionally equivalent, with two exceptions:

1. kochi@'s fix for layout object lifecycle (https://crrev.com/5c480f3117fe314b8cace4f33b84020e9e3424e2) was landed only for HTMLElement and not Element.

2. They have slightly different behavior in LayoutObject::offsetParent, due to passing 'this' versus a null pointer. See below for details on this.

So who calls each version of these methods? As the name may suggest, the offset*ForBinding methods are bindings for the V8 HTMLElement interface, so are the ones called when Javascript calls the offset* methods (e.g. https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetTop). The Element offset* methods are called in very few places: blink::ImageDocument::imageClicked and blink::SpatialNavigation::rectToAbsoluteCoordinates (static method) are the only non-test locations.

This entire thing smells of code duplication to me, and I'm hoping that we can combine them such that HTMLElement::offset*ForBinding just call their related Element::offset* methods. We would need to clear up #1 and #2 above first though.



To give a concrete walkthrough, let us compare offsetTopForBinding and offsetTop:

int HTMLElement::offsetTopForBinding() {
  Element* offsetParent = unclosedOffsetParent();
  if (LayoutBoxModelObject* layoutObject = layoutBoxModelObject())
    return adjustLayoutUnitForAbsoluteZoom(
               LayoutUnit(layoutObject->pixelSnappedOffsetTop(offsetParent)),
               layoutObject->styleRef())
        .round();
  return 0;
}

int Element::offsetTop() {
  document().updateStyleAndLayoutIgnorePendingStylesheetsForNode(this);
  if (LayoutBoxModelObject* layoutObject = layoutBoxModelObject())
    return adjustLayoutUnitForAbsoluteZoom(
               LayoutUnit(layoutObject->pixelSnappedOffsetTop(offsetParent())),
               layoutObject->styleRef())
        .round();
  return 0;
}

Apart from when the offset parent is calculated (as noted, changed by kochi@ in HTMLElement to fix lifecycle issues), the only differences are that Element::offsetTop() calls 'document().updateStyleAndLayoutIgnorePendingStylesheetsForNode(this);' at the start of its method, and they have differently named methods for getting the offset parent.

Let us look at the offset parent methods:

Element* HTMLElement::unclosedOffsetParent() {
  document().updateStyleAndLayoutIgnorePendingStylesheetsForNode(this);

  LayoutObject* layoutObject = this->layoutObject();
  if (!layoutObject)
    return nullptr;

  return layoutObject->offsetParent(this);
}

Element* Element::offsetParent() {
  document().updateStyleAndLayoutIgnorePendingStylesheetsForNode(this);

  LayoutObject* layoutObject = this->layoutObject();
  return layoutObject ? layoutObject->offsetParent() : nullptr;
}

Note that both of these call 'document().updateStyleAndLayoutIgnorePendingStylesheetsForNode(this)', so that difference between the parent offset* methods is gone. However, the call to LayoutObject::offsetParent is slightly different - HTMLElement passes 'this', whilst Element passes nullptr (default parameter value). So what impact does that have? In LayoutObject::offsetParent:

    // TODO(kochi): If |base| or |node| is nested deep in shadow roots, this
    // loop may get expensive, as isUnclosedNodeOf() can take up to O(N+M) time
    // (N and M are depths).
    if (base &&
        (node->isClosedShadowHiddenFrom(*base) ||
         (node->isInShadowTree() &&
          node->containingShadowRoot()->type() == ShadowRootType::UserAgent))) {
      // If 'position: fixed' node is found while traversing up, terminate the
      // loop and return null.
      if (ancestor->isFixedPositioned())
        return nullptr;
      continue;
    }

This is the only difference - base will be the element for HTMLElement's call, and nullptr for Element's call. Sadly, I'm not familiar enough with blink to understand the impact of this shadow tree checking.
 
Cc: kochi@chromium.org
Kochi; I'm cc-ing you because you recently(ish) worked on HTMLElement::offset*ForBinding, hopefully you might have some idea of how sane this code cleanup idea is before I try pushing this to a wider audience :).

webkit_unit_tests pass on my machine with the suggested change made locally. All but 4 LayoutTests pass as well, and I'm unsure if the failures are even related to the change:

virtual/scalefactor200/fast/hidpi/static/data-suggestion-picker-appearance.html
virtual/scalefactor200/fast/hidpi/static/popup-menu-with-scrollbar-appearance.html
virtual/scalefactor200withzoom/fast/hidpi/static/data-suggestion-picker-appearance.html
virtual/scalefactor200withzoom/fast/hidpi/static/popup-menu-with-scrollbar-appearance.html

(results zip attached).
layout-test-results.zip
13.8 MB Download

Comment 2 by kochi@chromium.org, Jan 25 2017

Thanks for taking a close look at the code.

The need for the offset* change was due to discussion on
https://github.com/w3c/csswg-drafts/issues/159 ,
TL;DR if a offsetParent hits inside Shadow DOM, such offsetParent should be
adjusted to appropriate element.  And the issue was re-opened for further
change, but we haven't got to a conclusion.

For more detailed background for the change, please check
https://codereview.chromium.org/2051703002
rather than LayoutObject refcount bugfix you quoted.

The offset*ForBinding APIs are split from the original offset* because of
the ShadowDOM handling.  There are still some users of the original one,
and in order not to break those, the non-binding interfaces are left there.

Probably you can take a close look at their usage and can be converted to
use *ForBinding variant, and definitely there are clean up opportunities
around offset* code in both dom and layout.

I'm happy to review CLs for this.
Cc: flackr@chromium.org
Cc: hayato@chromium.org
Thanks Kochi for the quick response and excellent overview. I see from the original review that hayato@ was the one who had concerns about the existing callers of Element::offset*, and requested that the new offset*ForBinding methods be created instead.

hayato: Before I dig into the call sites myself (blink::ImageDocument::imageClicked and rectToAbsoluteCoordinates in blink::SpatialNavigation) - did you have specific knowledge on how these would break if Element::offset* was changed to respect unclosed shadow DOM parents, or were you just concerned that we didn't know the effect?


Comment 5 by kochi@chromium.org, Jan 26 2017

Yeah, at that time we didn't have much time to investigate each call site whether
it works with *ForBinding, so we left all the non-binding callers as is.

Please note that CSSOM spec may have further change depending on the conclusion
of https://github.com/w3c/csswg-drafts/issues/159 , so it may be premature to convert
all the offset* sites to *ForBinding, so I'd say keeping both interfaces for now
is a safer option, although I believe even while we keep
both interfaces we can clean code up better.

Status: Available (was: Untriaged)
Components: Blink>HTML
Owner: ----

Sign in to add a comment