ShadowRoot is marked to be in document, although it isn't

RESOLVED FIXED in mozilla32

Status

()

defect
RESOLVED FIXED
5 years ago
a month ago

People

(Reporter: smaug, Assigned: wchen)

Tracking

Trunk
mozilla32
Points:
---
Dependency tree / graph
Bug Flags:
in-testsuite +

Firefox Tracking Flags

(firefox28 wontfix, firefox29 affected, firefox30 affected, firefox31 affected)

Details

Attachments

(5 attachments, 3 obsolete attachments)

(Reporter)

Description

5 years ago
We really can't have 
http://mxr.mozilla.org/mozilla-central/source/content/base/src/ShadowRoot.cpp?rev=20a329b254ae&mark=76-77#67

We have code all over relying on IsInDoc() meaning that you can get from such
node to document (and that you can get from document to such node, although perhaps
via some xbl stuff).
You can still get from a ShadowRoot to the document via nsNodeInfo, it is just the reverse direction that does not hold.
(Reporter)

Comment 2

5 years ago
That is the ownerdoc edge, which should be there, but there isn't
parent node chain from ShadowRoot to document, AFAIK.
(Reporter)

Comment 3

5 years ago
Or perhaps we can have that IsInDoc, but need to go through all the GetCurrentDoc, GetDocument and
IsInDoc checks to ensure that they expect this rather unusual setup.
OS: Linux → All
Hardware: x86_64 → All
Whiteboard: [MemShrink]
They absolutely do not.  We assume that IsInDoc() means exactly that for various spec bits....  Why is that line there?
Flags: needinfo?(wchen)
(Assignee)

Comment 5

5 years ago
The spec says:

"All other HTML elements in the shadow trees must behave as if they were part of the document tree."

"other" refers to link and base elements.

http://w3c.github.io/webcomponents/spec/shadow/#html-elements-in-shadow-trees

Anonymous content also lie about being in the document, so the code should already be expecting this unusual case to some extent. Although shadow DOM is even a bit more unusual than anonymous content because you can't reach the document by traversing the parent chain, in addition to being unable to reach the content traversing down child lists from the document.
Flags: needinfo?(wchen)
> Anonymous content also lie about being in the document

Anonymous content has the property the going up the GetParent() chain will reach the document.

There are various places where we use IsInDoc() to check whether such a walk makes sense and assume that it does if true is returned.

It sounds like we really need to go through and audit our IsInDoc()/GetDocument() callers and make sure they can deal with a change in the behavior here or something.
khuey thinks this isn't MemShrink relevant.
Whiteboard: [MemShrink]
Well, it can make us leak documents.
(Assignee)

Comment 9

5 years ago
How does it make us leak documents and how does XBL deal with this?
(Reporter)

Comment 10

5 years ago
We have various cycle collection optimizations for Nodes which rely on IsInDoc() to not lie.
XBL doesn't have this issue, since it deals with document and elements and such, not document fragments (which so far have never been in document).
(Assignee)

Comment 11

5 years ago
Ah, OK. So the problem is ShadowRoot being in doc, and not necessarily its children (which are also just elements, text nodes, etc)? I can remove that flag on ShadowRoot as the spec doesn't mention anything about the ShadowRoot itself behaving as if in document.
(Reporter)

Comment 12

5 years ago
Well the children should be in the doc also only in case they actually are in the doc.
I mean, there should be child->parent/insertion-parent chain from in-doc node to document.
This is blocking (through 777674) our ability to show the shadow dom in the devtools inspector.
No longer blocks: 957109
Having a DocumentFragment that claims to be IsInDoc() is just broken beyond belief.  It violates all sorts of invariants.

For the actual shadow kids, they should be IsInDoc() if and only if the shadow host IsInDoc(), no?
(Assignee)

Comment 15

5 years ago
sicking has a proposal to change the spec so that we don't treat shadow tree elements as if in the document and explicitly specify "in document" features that we want to work like <style> and <script>.

I'm working on a patch right now that makes ShadowRoot and its descendants never marked in document.
Assignee: nobody → wchen
(Assignee)

Comment 16

5 years ago
- Make ShadowRoot and its descendants no longer marked in document.
- Remove union of mSubtreeRoot and mPrimaryFrame in nsINode because nodes in a non-document subtree may have frames.
- Added GetCrossShadowCurrentDoc() to replace GetCurrentDoc() for cases where we want the current document crossing shadow root boundaries.
(Assignee)

Comment 21

5 years ago
For reference, this comment describes alternate behavior instead of treating shadow root elements as if in document:

https://www.w3.org/Bugs/Public/show_bug.cgi?id=25562#c1
(Reporter)

Comment 22

5 years ago
Comment on attachment 8422136 [details] [diff] [review]
Part 1: Remove "in document" flag on ShadowRoot and its descendants.

So this approach increases size of nsINode 4 or 8 bytes :/
(Reporter)

Comment 23

5 years ago
(In reply to Boris Zbarsky [:bz] from comment #14)
> Having a DocumentFragment that claims to be IsInDoc() is just broken beyond
> belief.  It violates all sorts of invariants.
> 
> For the actual shadow kids, they should be IsInDoc() if and only if the
> shadow host IsInDoc(), no?

I think we should go with this approach in Gecko. (spec terminology can be something else)
Then we don't need to increase the size of nsINode, and only InDoc() nodes can have frames.
(That is also a thing which code tend to rely on)
(Assignee)

Comment 24

5 years ago
If we put ShadowRoot elements in document we have to deal with:

(In reply to Olli Pettay [:smaug] from comment #12)
> Well the children should be in the doc also only in case they actually are
> in the doc.
> I mean, there should be child->parent/insertion-parent chain from in-doc
> node to document.

(In reply to Boris Zbarsky [:bz] from comment #6)
> > Anonymous content also lie about being in the document
> 
> Anonymous content has the property the going up the GetParent() chain will
> reach the document.
> 
> There are various places where we use IsInDoc() to check whether such a walk
> makes sense and assume that it does if true is returned.
> 
> It sounds like we really need to go through and audit our
> IsInDoc()/GetDocument() callers and make sure they can deal with a change in
> the behavior here or something.

Alternatively, if we don't put the children in document we have to deal with:

(In reply to Olli Pettay [:smaug] from comment #22)
> Comment on attachment 8422136 [details] [diff] [review]
> Part 1: Remove "in document" flag on ShadowRoot and its descendants.
> 
> So this approach increases size of nsINode 4 or 8 bytes :/

(In reply to Olli Pettay [:smaug] from comment #23)
> Then we don't need to increase the size of nsINode, and only InDoc() nodes
> can have frames.
> (That is also a thing which code tend to rely on)

I talked with sicking about this and he recommended that we don't put the ShadowRoot elements in doc, despite the increase in node size.

I'm now torn about what approach to take.
(Reporter)

Comment 25

5 years ago
(In reply to William Chen [:wchen] from comment #24)
> If we put ShadowRoot elements in document we have to deal with:

ShadowRoot is not an element.

In my mind ShadowRoot, which is a DocumentFragment wouldn't ever be in document, but
its children would be in document if shadow host is in document.
I think that would be the least surprising behavior and not too far from the XBL setup.
I don't understand why it needs to take an additional 4-8 bytes?

I could see that if we don't set the "in document" flag, that we might need a "in shadowroot" flag. But that's one bit, not 4-8 bytes (unless we happen to be out of bits. But if we are, we'll have to add more bits eventually anyway).
(Reporter)

Comment 27

5 years ago
Currently InDoc() means document is the root of subtree, and union can be used for the primary frame.
In case !InDoc() union points to the root of the subtree (since there can't be primary frame)

Ín shadow dom case root of subtree would need to be defined anyway. I'd expect root to be
the root of shadow host, not ShadowRoot (but don't have strong opinion about that).
What is the "root of subtree" for shadow DOM? Should it be the node you reach if you follow the parent chain (i.e. the ShadowRoot doc-fragment) or should it be the document? I'm also curious *why* we should use one or the other.

Anyhow, if it should be the document, then we could still just use the current union and do

nsINode* SubtreeRoot() {
  return (IsInDoc() || IsInShadowRootInDoc()) ? OwnerDocAsNode() : mSubtreeRoot;
}

where IsInShadowRootInDoc() is a flag we'd set if and only if a node is inserted into a shadowroot and the shadow host has the IsInDoc() flag set. This is a useful flag to have anyway in order to know when a in-shadowdom-stylesheet should be activated, and when a in-shadowdom-script should run etc.


If the "root of subtree" should be the node you reach if you follow the parent chain, then it appears that we couldn't keep the union either way?
The reason I don't want to set the IsInDoc() flag for shadow content is that it has a very gecko-specific effect. It will cause some set of elements to behave differently, but which elements and how they behave differently is basically due to some very gecko-specific implementation details.

For example it depends on if we decide that elements should check their IsInDoc() flag and then notify their OwnerDoc(), or if we do it by having the owner document use nsIMutationObserver and walking subtrees to find elements. Currently we implement some features using one strategy and some features using the other strategy. Which one we choose is a random combination of performance, ease of coding, how often we think features are used etc.

I don't want those implementation decisions to leak out to the web.
(Assignee)

Comment 30

5 years ago
I've updated the patch to keep the union. It looks like SubtreeRoot() expects the node you get if you keep walking GetParentNode(). For elements in the shadow tree, this is the shadow root and we already keep track of that. I've made it so that we don't use the mSubtreeRoot member of the union for content in the shadow tree.
Attachment #8422136 - Attachment is obsolete: true
(Assignee)

Updated

5 years ago
Attachment #8423449 - Flags: review?(bugs)
(Reporter)

Comment 31

5 years ago
Have you pushed the patch to try?
(Reporter)

Comment 33

5 years ago
Comment on attachment 8423449 [details] [diff] [review]
Part 1: Remove "in document" flag on ShadowRoot and its descendants. v2

>   nsIFrame* GetPrimaryFrame() const
>   {
>-    return IsInDoc() ? mPrimaryFrame : nullptr;
>+    return (IsInDoc() || HasFlag(NODE_IS_IN_SHADOW_TREE)) ? mPrimaryFrame : nullptr;
>   }
So this would return non-null nsIFrame* pointing to an nINode for ShadowRoot.


>   void SetPrimaryFrame(nsIFrame* aFrame) {
>-    NS_ASSERTION(IsInDoc(), "This will end badly!");
>+    NS_ASSERTION(IsInDoc() || HasFlag(NODE_IS_IN_SHADOW_TREE), "This will end badly!");
Could you make this MOZ_ASSERT


>   /**
>+   * This method gets the current doc of the node hosting this content
>+   * or the current doc of this content if it is not being hosted. This
>+   * method walks through ShadowRoot boundaries until it reach the host
>+   * that is located in the root of the "tree of trees" (see Shadow DOM
>+   * spec) and returns the current doc for that host.
>+   */
>+  nsIDocument *GetCrossShadowCurrentDoc();
* goes with the type.

So this method is slower than GetCurrentDoc().
Could we perhaps take the slow path only in case of HasFlag(NODE_IS_IN_SHADOW_TREE) , and otherwise inline the call.
Something like
return HasFlag(NODE_IS_IN_SHADOW_TREE) ? GetCrossShadowCurrentDocInternal() : GetCurrentDoc();
And GetCrossShadowCurrentDocInternal() would do the shadowroot-host iteration.

>@@ -1415,16 +1417,17 @@ Element::UnbindFromTree(bool aDeep, bool
>       mParent = nullptr;
>       NS_RELEASE(p);
>     } else {
>       mParent = nullptr;
>     }
>     SetParentIsContent(false);
>   }
>   ClearInDocument();
>+  UnsetFlags(NODE_IS_IN_SHADOW_TREE);
Hmm, how does this shadow dom work... do elements in shadow dom not get UnbindFromTree call when host is unbound?
I guess so.

>+nsINode::SubtreeRoot() const
>+{
>+  // There are four cases of interest here.  nsINodes that are really:
>+  // 1. nsIDocument nodes - Are always in the document.
>+  // 2.a nsIContent nodes not in a shadow tree - Are either in the document,
>+  //     or mSubtreeRoot is updated in BindToTree/UnbindFromTree.
>+  // 2.b nsIContent nodes in a shadow tree - Are never in the document,
>+  //     ignore mSubtreeRoot and return the containing shadow root.
>+  // 4. nsIAttribute nodes - Are never in the document, and mSubtreeRoot
>+  //    is always 'this' (as set in nsINode's ctor).
>+  nsINode* node;
>+  if (IsInDoc()) {
>+    node = OwnerDocAsNode();
>+  } else if (IsNodeOfType(eCONTENT)) {
IsContent() please. That is not virtual.

>+nsINode::GetCrossShadowCurrentDoc()
>+{
>+  if (HasFlag(NODE_IS_IN_SHADOW_TREE) && IsNodeOfType(eCONTENT)) {
IsContent()

The following issues could be fixed in followup patches.

It is not clear why things like Element::NotifyStateChange/Element::UpdateState/Element::NotifyStyleStateChange or Element::ScrollIntoView would work.

Also nsIContent::GetDesiredIMEState() hints that IME handling is broken for shadow dom.

Element::CreateShadowRoot seems to rely GetCurrentDoc() to return non-null if there are frames.

Also Element::SetSMILOverrideStyleRule wants probably GetCrossShadowCurrentDoc as does
Element::PostHandleEventForLinks (though, I'm not sure about link handling in shadow dom. The sane approach is to handle them normally).

Link::LinkState()/Link::ResetLinkState() don't deal with stuff not in document.

FragmentOrElement::CanSkip* could use GetCrossShadowCurrentDoc()


nsContentUtils::IsSubDocumentTabbable should probably use GetCrossShadowCurrentDoc() so that we can tab into an iframe inside shadow dom.

nsPointerLockPermissionRequest::Run should use GetCrossShadowCurrentDoc, and the pointer lock related stuff in nsDocument::Observe

IsInvisibleBreak should use GetCrossShadowCurrentDoc, and nsFrameLoader::Create and nsFrameLoader::ShowRemoteFrame and nsINode::Traverse

CheckPluginStopEvent::Run() probably should use GetCrossShadowCurrentDoc, and probably also nsSimplePluginEvent() and nsObjectLoadingContent::UnbindFromTree
and nsObjectLoadingContent::InstantiatePluginInstance


...and that was for content/base/*

rest of the GetCurrentDoc() usage needs to be reviewed. and content/base/* re-reviewed.

But r- for the patch anyhow.
Attachment #8423449 - Flags: review?(bugs) → review-
(Assignee)

Comment 34

5 years ago
(In reply to Olli Pettay [:smaug] (vacation-ish May 26-30) from comment #33)
> Comment on attachment 8423449 [details] [diff] [review]
> Part 1: Remove "in document" flag on ShadowRoot and its descendants. v2
> 
> >   nsIFrame* GetPrimaryFrame() const
> >   {
> >-    return IsInDoc() ? mPrimaryFrame : nullptr;
> >+    return (IsInDoc() || HasFlag(NODE_IS_IN_SHADOW_TREE)) ? mPrimaryFrame : nullptr;
> >   }
> So this would return non-null nsIFrame* pointing to an nINode for ShadowRoot.
> 
I've updated ShadowRoot to clear the mSubtreeRoot pointer on construction.
> 
> >@@ -1415,16 +1417,17 @@ Element::UnbindFromTree(bool aDeep, bool
> >       mParent = nullptr;
> >       NS_RELEASE(p);
> >     } else {
> >       mParent = nullptr;
> >     }
> >     SetParentIsContent(false);
> >   }
> >   ClearInDocument();
> >+  UnsetFlags(NODE_IS_IN_SHADOW_TREE);
> Hmm, how does this shadow dom work... do elements in shadow dom not get
> UnbindFromTree call when host is unbound?
> I guess so.
> 
That's right, elements in the shadow DOM don't get the UnbindFromTree call when the host is unbound. They are still bound to the shadow tree. Unlike XBL, the shadow DOM doesn't get torn down when the host is unbound. The ShadowRoot and nodes in the shadow tree have API that still needs to work regardless of whether the host is bound or not.
Attachment #8423449 - Attachment is obsolete: true
Attachment #8431029 - Flags: review?(bugs)
William: Is it correct to say that BindToTree and UnbindFromTree for Shadow-Dom behaves exactly like BindToTree and UnbindFromTree for content in a document fragment?

I.e. when you are inserted into the fragment you get BindToTree, when you are removed from the fragment you get UnbindFromTree. When the shadow host is removed from or inserted into the document, or anywhere else, there are no BindToTree/UnbindFromTree calls to the content in the shadow DOM.
(Assignee)

Comment 36

5 years ago
sicking: Yes, that is how it works right now.
(Reporter)

Comment 37

5 years ago
Comment on attachment 8431029 [details] [diff] [review]
Part 1: Remove "in document" flag on ShadowRoot and its descendants. v3

> ShadowRoot::~ShadowRoot()
> {
>   if (mPoolHost) {
>     // mPoolHost may have been unlinked or a new ShadowRoot may have been
>     // creating, making this one obsolete.
>     mPoolHost->RemoveMutationObserver(this);
>   }
> 
>-  ClearInDocument();
>+  UnsetFlags(NODE_IS_IN_SHADOW_TREE);
>+
>+  SetSubtreeRootPointer(this);
This could use a comment that nsINode dtor expects mSubtreeRoot == this

>+nsINode::GetCrossShadowCurrentDocInternal() const
>+{
>+  if (HasFlag(NODE_IS_IN_SHADOW_TREE) && IsContent()) {
Just MOZ_ASSERT here that 
HasFlag(NODE_IS_IN_SHADOW_TREE) && IsContent()



>+    // Walk through ShadowRoot boundary to the host if this node is
>+    // contained in a shadow tree.
>+    const nsIContent* content = static_cast<const nsIContent*>(this);
Use AsContent() here (and elsewhere in the patch), no need for static_cast

I assume following patches will increase usage of GetCrossShadowCurrentDoc
Attachment #8431029 - Flags: review?(bugs) → review+
(Assignee)

Updated

5 years ago
Attachment #8422141 - Flags: review?(bugs)
(Assignee)

Updated

5 years ago
Attachment #8422140 - Flags: review?(bugs)
(Assignee)

Updated

5 years ago
Attachment #8422138 - Flags: review?(bugs)
(Assignee)

Updated

5 years ago
Attachment #8422137 - Flags: review?(bugs)
(Reporter)

Comment 38

5 years ago
Comment on attachment 8422137 [details] [diff] [review]
Make styles work when not in document, but contained by a ShadowRoot with a host in document.

>+  if (mOwningNode && !mDisabled &&
>+      mOwningNode->HasFlag(NODE_IS_IN_SHADOW_TREE) &&
>+      mOwningNode->IsNodeOfType(nsINode::eCONTENT)) {
mOwningNode->IsContent();
Attachment #8422137 - Flags: review?(bugs) → review+
(Reporter)

Comment 39

5 years ago
Comment on attachment 8422138 [details] [diff] [review]
Part 3: Make script run when not in document, but contained by a ShadowRoot with a host in document.

Looks like this is a wrong patch. Not about scripts.
Attachment #8422138 - Flags: review?(bugs)
(Reporter)

Updated

5 years ago
Attachment #8422140 - Flags: review?(bugs) → review+
(Reporter)

Updated

5 years ago
Attachment #8436028 - Flags: review?(bugs) → review+
(Reporter)

Comment 41

5 years ago
Comment on attachment 8422141 [details] [diff] [review]
Part 5: Bind native anonymous content to null document when node is contained by a ShadowRoot.

This is scary, but makes sense.
Attachment #8422141 - Flags: review?(bugs) → review+
Depends on: 1022741
(Reporter)

Updated

5 years ago
Depends on: 1024428
(Reporter)

Updated

5 years ago
Depends on: shadowcurrentdoc
The patches introduced here break accessibility. I'll file a bug in the a11y component, but i bet the original author of these patches (wchen?) has the best idea of how to fix this.
Depends on: 1026125
Component: DOM → DOM: Core & HTML
Product: Core → Core
You need to log in before you can comment on or make changes to this bug.