-
Notifications
You must be signed in to change notification settings - Fork 379
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
Custom elements disconnected from a document should not be upgraded #419
Comments
This also results in rather unexpected behavior of unresolved custom elements inside template content and document without a browsing context getting upgraded. THAT should at least be avoided. |
That is a different issue and is about what documents the registry works for. As for this issue, it seems reasonable that the upgrade candidates map holds weak references to the elements. That is, if they are only referenced from there, they should just be collected and not instantiated. |
I initially considered that approach but that could expose GC behavior. For example, WebKit currently implements stack-scanning conservative generational GC. It would mean that if the stack happens to contain a value that matches the node's memory address or if the major GC (non-eden collection) doesn't happen in time before the end of nano-task occurs, we may end up instantiating custom elements that would otherwise be dead in other browsers. |
Can't we figure out by the time we invoke the callback whether anyone but "us" holds a reference to the object? That seems like the kind of deterministic check we'd need here. |
We can't because JSC/WebKit uses stack-scanning conservative GC. It's literally impossible for GC to deterministically tell us whether a given node is definitely alive or not. Note that this is not a problem unique to WebKit. Blink's new memory collector, Oilpan, uses conservative stack-scanning GC as well so they won't be able to implement such a deterministic check unless they modify their GC either. |
So let me try to understand the proposal:
It seems a little easier to just upgrade all of them and assume developers won't allocate a million of custom elements they don't intend to use. |
Yeah, that's the proposal. The problem is that the current spec says that we need to treat any element with |
I would really prefer not to introduce this kind of inconsistency by requiring document-insertion for upgrade. Developers do complicated and interesting things with elements before inserting them into documents, as part of libraries and such. They shouldn't need to do some kind of insert-then-remove dance to get their elements to have the correct prototype and behavior. I would prefer to keep the list of unresolved elements as the spec currently has them. It could be made a weak set in the spec, and implementers can either implement that immediately or will wait until they see problems in practice caused by lots of unused un-upgraded elements. I know Blink has implemented such things in the past with just weak references without a problem (we do that for the unhandled promises weak set). |
Generally agreeing with @domenic here. I understand the concern though. It's just tree-connected concept seems like the wrong tool for the job. |
As I mentioned in #419 (comment), that kind of optimization will be observably non-interoperable between browsers. |
@rniwa you must be misunderstanding me. I am referring to holding a weak reference in an unobservable way, which is certainly possible in Blink with Oilpan. It might not be possible with your GC, which is fine; it just means you cannot implement this optimization. |
@domenic : Oilpan is a stack-scanning conservative GC so it can retain objects that are not actually retained. If you implemented this optimization using Oilpan, it would be observable. Also, it can't possibly an optimization because it's observable whether a given custom element is upgraded or not. Just because JS no longer has a reference to it, it doesn't mean whether it got later upgraded or not is not. You can create an element, never store the reference, and you can observe that your element got upgraded later. You can't just not upgrade the element just because JS doesn't keep a reference to it. The proposal @annevk made was about making this weak map mandatory so that any element that loses JS reference and detached from the tree would not be upgraded at the end of current nano-task. I'm pointing out that it's impossible to implement such semantics in WebKit (and Blink if your design doc is still up-to-date). |
@rniwa I think I see now. The case in question is when there are no JS consumers holding a reference to the element at all, only the browser holding a single weak reference to it. Let
And then the claim is that our GCs have no way to detect how many non-weak references there are to a given element. (Which seems reasonable, as otherwise they would probably be refcounting systems instead of GCs.) This does seem problematic. |
Another problem is eden collection versus major collections in (multi) generational GC. Depending on what heuristics are used, you may decide that some elements should not be collected in minor GC even if there was no JS reference to it. In theory, we should be able to always trigger precise full GC which can deterministically tell us how many weak and strong references we have, but I don't think that's an acceptable implementation requirement for many browser vendors. |
You just need to spec a deterministic time for when the upgrade maps are cleared. Today in blink the references are weak so you could indeed observe the GC by waiting and then doing registerElement at various times. In practice no one has ever complained or noticed. ex. How useful is upgrade after the load event as fired? Maybe we should turn that system off at that point? |
I'm not sure this helps without a way to tell who is referencing the elements at this time, which seems equivalent to triggering a precise full GC at these times.
That seems like it would break lazy-loading of sections of pages along with their element definitions. |
You don't tie to who is holding references. You spec something like "after the load event, clear the upgrade candidate map for all elements". Then you spec some way for authors to keep things alive in the map for longer, for example with waitUntil().
It means you might need to expose something like a waitUntil() promise on the registry. For example document.customElementRegistry.get("x-foo").waitUntil(promise) to deal with your lazy load. You're explicitly making it live in the upgrade map longer. |
Hmm. That doesn't seem much better than just not doing upgrades for disconnected nodes, plus maybe adding some kind of |
If being connected to a document was too restrictive, we could add a new flag on element to indicate whether an element is upgradable, and make all elements created by the parser to have that flag turned on by default. e.g. Allowing all disconnected nodes to be upgraded indefinitely in the future without any author intent is not acceptable for us. |
You mean |
It doesn't matter which one. We can define |
https://bugs.webkit.org/show_bug.cgi?id=155107 Reviewed by Darin Adler. Source/WebCore: Added the support for upgrading existing unresolved custom elements when defineElement is called. The current implementation upgrades elements in the order they were created and has the issue that it keeps accumulating all elements with a hyphen in its name until defineElement is called as documented in WICG/webcomponents#419 This patch re-purposes IsEditingTextFlag to indicate that the node is an unresolved custom element. Since isEditingText() is only called in textRendererIsNeeded only on Text nodes, it's mutually exclusive with isUnresolvedCustomElement(). The list of unresolved custom elements is kept in m_upgradeCandidatesMap, a hash map of element names to the list of unresolved elements with that name. In addition, added the logic to use HTMLElement as the interface for unresolved custom element instead of HTMLUnknownElement. Test: fast/custom-elements/upgrading/upgrading-parser-created-element.html * bindings/js/JSCustomElementInterface.cpp: (WebCore::JSCustomElementInterface::upgradeElement): Clear the flag. * bindings/js/JSDocumentCustom.cpp: (WebCore::JSDocument::defineElement): Set the unique private name to keep the interface alive before calling addElementDefinition as the call can now invoke author scripts. * dom/CustomElementDefinitions.cpp: (WebCore::CustomElementDefinitions::addElementDefinition): Upgrade existing unresolved elements kept in m_upgradeCandidatesMap. (WebCore::CustomElementDefinitions::addUpgradeCandidate): Added. * dom/CustomElementDefinitions.h: * dom/Document.cpp: (WebCore::createHTMLElementWithNameValidation): Added the code to add the unresolved custom elements to the upgrade candidates map. Also instantiate it as HTMLElement instead of HTMLUnknownElement. (WebCore::createFallbackHTMLElement): Ditto. * dom/Node.h: (WebCore::Node::setIsCustomElement): (WebCore::Node::isUnresolvedCustomElement): Added. (WebCore::Node::setIsUnresolvedCustomElement): Added. (WebCore::Node::setCustomElementIsResolved): Added. Clears IsEditingTextOrUnresolvedCustomElementFlag and sets IsCustomElement. (WebCore::Node::isEditingText): Check both IsEditingTextOrUnresolvedCustomElementFlag and IsTextFlag for safety even though it's currently only used in textRendererIsNeeded which takes Text&. * dom/make_names.pl: (defaultParametersHash): Added customElementInterfaceName as a parameter. (printWrapperFactoryCppFile): Generate the code to use customElementInterfaceName when the element for which the wrapper is created has isUnresolvedCustomElement flag set. * html/HTMLTagNames.in: Use HTMLElement for unresolved custom elements. * html/parser/HTMLConstructionSite.cpp: (WebCore::HTMLConstructionSite::createHTMLElementOrFindCustomElementInterface): Added the code to add the unresolved custom elements to the upgrade candidates map. Also instantiate it as HTMLElement instead of HTMLUnknownElement. LayoutTests: Added W3C style testharness.js tests for asynchronously defining custom elements. * fast/custom-elements/upgrading/Node-cloneNode.html: * fast/custom-elements/upgrading/upgrading-parser-created-element-expected.txt: Added. * fast/custom-elements/upgrading/upgrading-parser-created-element.html: Added. git-svn-id: http://svn.webkit.org/repository/webkit/trunk@197917 268f45cc-cd09-0410-ab3c-d52691b4dbfc
After thinking about this some more I am a fan of the following parts of @rniwa's plan:
Unresolved questions:
I think "yes" is probably OK for both of these questions? The undefined potentially-custom elements inside the parsed fragment or cloned tree will have other things referencing them, almost certainly. We then have the issue of how to provide more ergonomic sugar for after-the-fact upgrading. @rniwa proposed a template-based helper but I think we should explore this space in a bit more detail before settling on that. I think the lowest-level possible primitive is A higher-level API might look something like Finally if we think that it'll be common to use disconnected This is a pretty big issue, so thanks @rniwa for catching it. It's frustrating we can't just do things automatically, and this will definitely hurt the ability for upgrades to "just work" without the developer knowing about them, but it's important to stay deterministic and not expose GC semantics. |
@domenic re:
I take this to mean that if you've loaded your definition and then call |
@treshugart in that case the new element would simply be the custom element. |
@domenic is there any reason for the |
This is true. In the future, I'll be able to recommend using |
Two reasons, neither super-compelling:
|
If we have { deep }, do we also have { superDeep }, including shadow roots? I wonder what |
In Chrome it does not clone the shadow roots. |
That seems kind of bad, hmm. |
Sorry, I thought about it, and it's good. Shadow roots should be created when the element is created, as they are part of the behavior of the element. Since cloning is defined in terms of creation, it should all work out for properly allocated shadow roots. |
Just to clarify. Given these two cases: const el = document.createElement("div");
el.attachShadow();
const clone = el.cloneNode(true); and document.customElements.define("x-tag", class extends HTMLElement {
constructor() {
super();
this.attachShadow();
}
});
const el = document.createElement("x-tag");
const clone = el.cloneNode(true); the first will have no shadow root in I guess I can't think of any better alternative. It seems a bit weird that cloning a tree does not clone the entire tree. And it seems to break the desire to orthogonalize custom elements and shadow DOM. But oh well. |
Yeah, otherwise you get weird code paths. Cloning clones the shadow tree, unless the newly created element already has one? |
That could work, but is probably a bit too magic. Maybe @dglazkov or @hayatoito or someone can give us background on why Blink/the current spec decided not to clone shadow trees, and make us feel better about that decision? |
Since the most common use case of shadow DOM is in conjunction with custom elements, it should work best in that use case. Now, I don't think we can check whether the newly created element has shadow DOM or not because we're going to upgrade those elements right before returning to author scripts. If anything, it needs to be keyed off of element names (i.e. whether the cloned node's name has |
This will take a while to page in, but I distinctly remember https://bugs.webkit.org/show_bug.cgi?id=61997 as being one of the first places we've discovered that cloning shadow trees along with the tree is fraught with peril. |
Part of #419. Also introduces the "custom" flag, instead of using the vague terminology "is a custom element".
I've taken care of the majority of this in recent commits. However I haven't added a |
The new example illustrates the conclusion we came to in #419.
Since nobody besides me seems enthusiastic about |
This was part of the previous conclusion in WICG/webcomponents#419, but never got added back to the spec: - WICG/webcomponents@c4a829a removed all auto-upgrading - WICG/webcomponents@70b9d8d only added it back for when you insert an element into a document, not for when you define an element.
This was part of the previous conclusion in WICG/webcomponents#419, but never got added back to the spec: - WICG/webcomponents@c4a829a removed all auto-upgrading - WICG/webcomponents@70b9d8d only added it back for when you insert an element into a document, not for when you define an element.
This was part of the previous conclusion in WICG/webcomponents#419, but never got added back to the spec: - WICG/webcomponents@c4a829a removed all auto-upgrading - WICG/webcomponents@70b9d8d only added it back for when you insert an element into a document, not for when you define an element.
This was part of the previous conclusion in WICG/webcomponents#419, but never got added back to the spec: - WICG/webcomponents@c4a829a removed all auto-upgrading - WICG/webcomponents@70b9d8d only added it back for when you insert an element into a document, not for when you define an element.
This was part of the previous conclusion in WICG/webcomponents#419, but never got added back to the spec: - WICG/webcomponents@c4a829a removed all auto-upgrading - WICG/webcomponents@70b9d8d only added it back for when you insert an element into a document, not for when you define an element.
https://bugs.webkit.org/show_bug.cgi?id=155107 Reviewed by Darin Adler. Source/WebCore: Added the support for upgrading existing unresolved custom elements when defineElement is called. The current implementation upgrades elements in the order they were created and has the issue that it keeps accumulating all elements with a hyphen in its name until defineElement is called as documented in WICG/webcomponents#419 This patch re-purposes IsEditingTextFlag to indicate that the node is an unresolved custom element. Since isEditingText() is only called in textRendererIsNeeded only on Text nodes, it's mutually exclusive with isUnresolvedCustomElement(). The list of unresolved custom elements is kept in m_upgradeCandidatesMap, a hash map of element names to the list of unresolved elements with that name. In addition, added the logic to use HTMLElement as the interface for unresolved custom element instead of HTMLUnknownElement. Test: fast/custom-elements/upgrading/upgrading-parser-created-element.html * bindings/js/JSCustomElementInterface.cpp: (WebCore::JSCustomElementInterface::upgradeElement): Clear the flag. * bindings/js/JSDocumentCustom.cpp: (WebCore::JSDocument::defineElement): Set the unique private name to keep the interface alive before calling addElementDefinition as the call can now invoke author scripts. * dom/CustomElementDefinitions.cpp: (WebCore::CustomElementDefinitions::addElementDefinition): Upgrade existing unresolved elements kept in m_upgradeCandidatesMap. (WebCore::CustomElementDefinitions::addUpgradeCandidate): Added. * dom/CustomElementDefinitions.h: * dom/Document.cpp: (WebCore::createHTMLElementWithNameValidation): Added the code to add the unresolved custom elements to the upgrade candidates map. Also instantiate it as HTMLElement instead of HTMLUnknownElement. (WebCore::createFallbackHTMLElement): Ditto. * dom/Node.h: (WebCore::Node::setIsCustomElement): (WebCore::Node::isUnresolvedCustomElement): Added. (WebCore::Node::setIsUnresolvedCustomElement): Added. (WebCore::Node::setCustomElementIsResolved): Added. Clears IsEditingTextOrUnresolvedCustomElementFlag and sets IsCustomElement. (WebCore::Node::isEditingText): Check both IsEditingTextOrUnresolvedCustomElementFlag and IsTextFlag for safety even though it's currently only used in textRendererIsNeeded which takes Text&. * dom/make_names.pl: (defaultParametersHash): Added customElementInterfaceName as a parameter. (printWrapperFactoryCppFile): Generate the code to use customElementInterfaceName when the element for which the wrapper is created has isUnresolvedCustomElement flag set. * html/HTMLTagNames.in: Use HTMLElement for unresolved custom elements. * html/parser/HTMLConstructionSite.cpp: (WebCore::HTMLConstructionSite::createHTMLElementOrFindCustomElementInterface): Added the code to add the unresolved custom elements to the upgrade candidates map. Also instantiate it as HTMLElement instead of HTMLUnknownElement. LayoutTests: Added W3C style testharness.js tests for asynchronously defining custom elements. * fast/custom-elements/upgrading/Node-cloneNode.html: * fast/custom-elements/upgrading/upgrading-parser-created-element-expected.txt: Added. * fast/custom-elements/upgrading/upgrading-parser-created-element.html: Added. Canonical link: https://commits.webkit.org/173391@main git-svn-id: https://svn.webkit.org/repository/webkit/trunk@197917 268f45cc-cd09-0410-ab3c-d52691b4dbfc
The current specification says, in section 6, that:
This results in the following code to keep accumulating the list of unresolved elements in the heap:
I don't think this is an acceptable behavior. I don't think any custom element created prior to its definition getting loaded and not inserted into a document should be upgraded. In particular, any element that's not even referenced by JS and not in any node tree shouldn't be kept alive just for the sole purpose of being upgraded.
The text was updated successfully, but these errors were encountered: