wiki:Abandoned documents

What is an abandoned document?

Abandoned documents are effectively leaked: they are Document objects which have never been destroyed, and persist after loading about:blank, running a garbage collection and clearing caches. This is probably because there's some object that is holding a reference to the Document object, possibly in the GC heap, or via a retain cycle (often involving Nodes in the document).

All documents are referenced by Document::allDocumentsMap(), so they are not leaked in the sense that the 'leaks' tool doesn't show them, but we refer to them as a "world leak" (i.e. a leak of a high-level object that tends to entrain lots of other objects).

Why are layout test results showing me tests with leaks?

Leaked documents usually entrain a lot of other objects, which can use lots of memory (e.g. via entries in the memory cache). Leaking documents is bad because it will cause ever-increasing memory use as the user browses. See https://bugs.webkit.org/show_bug.cgi?id=186214

What should I do if see a new document leak?

If you made a code change that is causing a test to newly show that a document is leaked, it probably means you have a coding bug that is triggered a leak or (more likely) a reference cycle. You need to resolve this before committing.

How do I debug document leaks?

Let's take an example https://bugs.webkit.org/show_bug.cgi?id=188722. We run tests, checking for world leaks:

run-webkit-tests fast/forms/ --world-leaks

The results say that fast/forms/textarea-paste-newline.html was leaked. Now you have to figure out why this Document object is not going away.

A bit of testing in MiniBrowser can be useful, and if you're lucky, the leak will reproduce there. To test this, do these steps:

  1. Run MiniBrowser
  2. Load a simple HTML file (not the test!)
  3. Load the test file
  4. Go back to the simple HTML file
  5. Simulate a memory warning (on macOS, you can do this by running notifyutil -p "org.WebKit.lowMemory" in the Terminal).
  6. Now dump the list of live documents: notifyutil -p "com.apple.WebKit.showAllDocuments". This will show something like:
    2 live documents:
    Document 0x630002400 (refCount 5, referencingNodeCount 1) file:///Volumes/Data/webkit/LayoutTests/fast/forms/textarea-paste-newline.html
    Document 0x630024400 (refCount 2, referencingNodeCount 4) file:///Volumes/Data/simple.html
    
  7. That confirms that after a memory warning (which clears caches and does a GC) that the document is still alive. So something is holding a reference to it.
  8. Now you could start doing manual debugging of ref() and deref() to try to figure out where the extra ref() is happening. But there's a better way.
    1. Apply the most recent patch from https://bugs.webkit.org/show_bug.cgi?id=186269 (if this has not been checked in yet) and build.
    2. Repeat the above steps. Now the output will be something like:
      2 live documents:
      Document 0x630002400 (refCount 5, referencingNodeCount 1) file:///Volumes/Data/Development/apple/webkit/OpenSource/LayoutTests/fast/forms/textarea-paste-newline.html
      Document 0x630002400 reference stacks:
      Backtrace for token 6074
      1   0x6179baa97 WebCore::Node::ref()
      2   0x615af576e unsigned int WTF::refIfNotNull<WebCore::Node>(WebCore::Node*)
      3   0x615af5728 WTF::RefPtr<WebCore::Node, WTF::DumbPtrTraits<WebCore::Node> >::RefPtr(WebCore::Node*)
      4   0x615af4add WTF::RefPtr<WebCore::Node, WTF::DumbPtrTraits<WebCore::Node> >::RefPtr(WebCore::Node*)
      5   0x615af4b73 WTF::RefPtr<WebCore::Node, WTF::DumbPtrTraits<WebCore::Node> >::operator=(WebCore::Node*)
      6   0x61788ccb2 WebCore::Document::removeFocusNavigationNodeOfSubtree(WebCore::Node&, bool)
      7   0x61788c82a WebCore::Document::nodeChildrenWillBeRemoved(WebCore::ContainerNode&)
      8   0x6178252de WebCore::ContainerNode::removeAllChildrenWithScriptAssertion(WebCore::ContainerNode::ChildChangeSource, WebCore::ContainerNode::DeferChildrenChanged)
      9   0x617828d49 WebCore::ContainerNode::removeChildren()
      10  0x617882aef WebCore::Document::implicitOpen()
      11  0x617878043 WebCore::Document::open(WebCore::Document*)
      12  0x617884062 WebCore::Document::write(WebCore::Document*, WebCore::SegmentedString&&)
      13  0x61788432b WebCore::Document::write(WebCore::Document*, WTF::Vector<WTF::String, 0ul, WTF::CrashOnOverflow, 16ul>&&)
      14  0x61609d242 WebCore::jsDocumentPrototypeFunctionWriteBody(JSC::ExecState*, WebCore::JSDocument*, JSC::ThrowScope&)
      15  0x61608046e long long WebCore::IDLOperation<WebCore::JSDocument>::call<&(WebCore::jsDocumentPrototypeFunctionWriteBody(JSC::ExecState*, WebCore::JSDocument*, JSC::ThrowScope&)), (WebCore::CastedThisErrorBehavior)0>(JSC::ExecState&, char const*)
      16  0x61608016c WebCore::jsDocumentPrototypeFunctionWrite(JSC::ExecState*)
      17  0xeef1324177
      18  0x62650d44b llint_entry
      19  0x62650d44b llint_entry
      20  0x626504e37 vmEntryToJavaScript
      21  0x6270a7c7a JSC::JITCode::execute(JSC::VM*, JSC::ProtoCallFrame*)
      22  0x6270a8303 JSC::Interpreter::executeCall(JSC::ExecState*, JSC::JSObject*, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&)
      23  0x627356dce JSC::call(JSC::ExecState*, JSC::JSValue, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&)
      24  0x627356eac JSC::call(JSC::ExecState*, JSC::JSValue, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&, WTF::NakedPtr<JSC::Exception>&)
      25  0x62735717d JSC::profiledCall(JSC::ExecState*, JSC::ProfilingReason, JSC::JSValue, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&, WTF::NakedPtr<JSC::Exception>&)
      26  0x617351ffb WebCore::JSExecState::profiledCall(JSC::ExecState*, JSC::ProfilingReason, JSC::JSValue, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&, WTF::NakedPtr<JSC::Exception>&)
      27  0x617398407 WebCore::JSEventListener::handleEvent(WebCore::ScriptExecutionContext&, WebCore::Event&)
      28  0x617960d3e WebCore::EventTarget::fireEventListeners(WebCore::Event&, WTF::Vector<WTF::RefPtr<WebCore::RegisteredEventListener, WTF::DumbPtrTraits<WebCore::RegisteredEventListener> >, 1ul, WTF::CrashOnOverflow, 16ul>)
      29  0x61795c7a0 WebCore::EventTarget::fireEventListeners(WebCore::Event&)
      30  0x61824752d WebCore::DOMWindow::dispatchEvent(WebCore::Event&, WebCore::EventTarget*)
      31  0x6182523de WebCore::DOMWindow::dispatchLoadEvent()
      32  0x6178839fa WebCore::Document::dispatchWindowLoadEvent()
      33  0x61787bff3 WebCore::Document::implicitClose()
      34  0x6180d6f2b WebCore::FrameLoader::checkCallImplicitClose()
      35  0x6180d69c8 WebCore::FrameLoader::checkCompleted()
      36  0x6180d58f5 WebCore::FrameLoader::finishedParsing()
      37  0x617895ca9 WebCore::Document::finishedParsing()
      38  0x617e3df58 WebCore::HTMLConstructionSite::finishedParsing()
      39  0x617e8c969 WebCore::HTMLTreeBuilder::finished()
      40  0x617e4708c WebCore::HTMLDocumentParser::end()
      41  0x617e440e9 WebCore::HTMLDocumentParser::attemptToRunDeferredScriptsAndEnd()
      42  0x617e43de3 WebCore::HTMLDocumentParser::prepareToStopParsing()
      43  0x617e470ff WebCore::HTMLDocumentParser::attemptToEnd()
      44  0x617e471d8 WebCore::HTMLDocumentParser::finish()
      45  0x6180c55fe WebCore::DocumentWriter::end()
      46  0x618084a1f WebCore::DocumentLoader::finishedLoading()
      [snip]
      
      Backtrace for token 5651
      1   0x6179baa97 WebCore::Node::ref()
      2   0x615d77d88 WTF::Ref<WebCore::Document, WTF::DumbPtrTraits<WebCore::Document> >::Ref(WebCore::Document&)
      3   0x615d77d4d WTF::Ref<WebCore::Document, WTF::DumbPtrTraits<WebCore::Document> >::Ref(WebCore::Document&)
      4   0x617396d29 WebCore::toJS(JSC::ExecState*, WebCore::JSDOMGlobalObject*, WebCore::Document&)
      5   0x6173ad378 WebCore::createWrapperInline(JSC::ExecState*, WebCore::JSDOMGlobalObject*, WTF::Ref<WebCore::Node, WTF::DumbPtrTraits<WebCore::Node> >&&)
      6   0x6173ad0a0 WebCore::createWrapper(JSC::ExecState*, WebCore::JSDOMGlobalObject*, WTF::Ref<WebCore::Node, WTF::DumbPtrTraits<WebCore::Node> >&&)
      7   0x615c363de WebCore::toJS(JSC::ExecState*, WebCore::JSDOMGlobalObject*, WebCore::Node&)
      8   0x615c8fbb0 WebCore::toJS(JSC::ExecState*, WebCore::JSDOMGlobalObject*, WebCore::Node*)
      9   0x617384fd3 WebCore::JSDOMWindowBase::updateDocument()
      10  0x6173d677e WebCore::ScriptController::initScriptForWindowProxy(WebCore::JSWindowProxy&)
      11  0x617457455 WebCore::WindowProxy::createJSWindowProxyWithInitializedScript(WebCore::DOMWrapperWorld&)
      12  0x6173c70d6 WebCore::WindowProxy::jsWindowProxy(WebCore::DOMWrapperWorld&)
      13  0x6173d556b WebCore::ScriptController::jsWindowProxy(WebCore::DOMWrapperWorld&)
      14  0x6173d5355 WebCore::ScriptController::evaluateInWorld(WebCore::ScriptSourceCode const&, WebCore::DOMWrapperWorld&, WebCore::ExceptionDetails*)
      15  0x6173d57bd WebCore::ScriptController::evaluate(WebCore::ScriptSourceCode const&, WebCore::ExceptionDetails*)
      16  0x617a04d37 WebCore::ScriptElement::executeClassicScript(WebCore::ScriptSourceCode const&)
      17  0x617a03084 WebCore::ScriptElement::prepareScript(WTF::TextPosition const&, WebCore::ScriptElement::LegacyTypeSupport)
      18  0x617e64798 WebCore::HTMLScriptRunner::runScript(WebCore::ScriptElement&, WTF::TextPosition const&)
      19  0x617e645bf WebCore::HTMLScriptRunner::execute(WTF::Ref<WebCore::ScriptElement, WTF::DumbPtrTraits<WebCore::ScriptElement> >&&, WTF::TextPosition const&)
      20  0x617e455a3 WebCore::HTMLDocumentParser::runScriptsForPausedTreeBuilder()
      21  0x617e45b63 WebCore::HTMLDocumentParser::pumpTokenizerLoop(WebCore::HTMLDocumentParser::SynchronousMode, bool, WebCore::PumpSession&)
      22  0x617e44684 WebCore::HTMLDocumentParser::pumpTokenizer(WebCore::HTMLDocumentParser::SynchronousMode)
      23  0x617e43f8f WebCore::HTMLDocumentParser::pumpTokenizerIfPossible(WebCore::HTMLDocumentParser::SynchronousMode)
      24  0x617e46e9a WebCore::HTMLDocumentParser::append(WTF::RefPtr<WTF::StringImpl, WTF::DumbPtrTraits<WTF::StringImpl> >&&)
      25  0x61785fe62 WebCore::DecodedDataDocumentParser::flush(WebCore::DocumentWriter&)
      26  0x6180c55b4 WebCore::DocumentWriter::end()
      27  0x618084a1f WebCore::DocumentLoader::finishedLoading()
      [snip]
      

The second stack there is normal document wrapper creation. The first one is more interesting, and points to the bug, which is that Document::removeFocusNavigationNodeOfSubtree() store the Document in a RefPtr data member of the same document, creating a ref cycle.

Here's another example https://bugs.webkit.org/show_bug.cgi?id=188776:

In this case, notifyutil -p "com.apple.WebKit.showAllDocuments" dumps:

SVGDocument 0x1262e8400 5 (refCount 0, referencingNodeCount 1) file:///Volumes/Data/Development/apple/webkit/OpenSource/LayoutTests/svg/wicd/resources/test-svg-child-object-rightsizing.svg
Document 0x1262e8400 5 reference stacks:

So there were no unmatched ref()/deref(). But what's this referencingNodeCount? That means that there are Nodes alive that belong to this document. To investigate that, in Node.h change the #define DUMP_NODE_STATISTICS 0 to #define DUMP_NODE_STATISTICS 1 and add a call to Node::dumpStatistics(); in the dumping code in Page::platformInitialize(). Now you can do the MiniBrowser steps above, but do it in a WebKit1 window, and close the window when done, followed by the lowMemory notification, and the showAllDocuments notification. Dumped node statistics say:

Number of Nodes: 16

Number of Nodes with RareData: 0

NodeType distribution:
  Number of Element nodes: 6
  Number of Attribute nodes: 0
  Number of Text nodes: 7
  Number of CDATASection nodes: 0
  Number of Comment nodes: 0
  Number of ProcessingInstruction nodes: 0
  Number of Document nodes: 2
  Number of DocumentType nodes: 1
  Number of DocumentFragment nodes: 0
  Number of ShadowRoot nodes: 0
Element tag name distibution:
  Number of <DIV> tags: 1
  Number of <BODY> tags: 1
  Number of <HTML> tags: 1
  Number of <HEAD> tags: 1
  Number of <font-face> tags: 1
  Number of <STYLE> tags: 1
Attributes:
  Number of Attributes (non-Node and Node): 6 [32]
  Number of Attributes with an Attr: 6
  Number of Elements with attribute storage: 1 [64]
  Number of Elements with RareData: 0
  Number of Elements with NamedNodeMap: 0 [16]

Hmm, those <font-face> tags look suspicious. Breakpoints in Node::dumpStatistics() (you can add a call to this function in the "com.apple.WebKit.showAllDocuments" callback in PageMac.mm) would let you confirm that they are still referencing the leaked document. So the bug fix would involve ensuring those font-face elements get released. We can use ref token tracking again to identify who's retaining the SVGFontFaceElement. There's a patch here that does that; it just puts all the live SVGFontFaceElements in a HashSet so they can be dumped, and adds code to the "com.apple.WebKit.showAllDocuments" callback to dump their remaining references. That showed up this stack:

3   0x107903384 WTF::RefPtr<WebCore::SVGFontFaceElement, WTF::DumbPtrTraits<WebCore::SVGFontFaceElement> >::RefPtr(WebCore::SVGFontFaceElement*)
4   0x1078db8ed WTF::RefPtr<WebCore::SVGFontFaceElement, WTF::DumbPtrTraits<WebCore::SVGFontFaceElement> >::RefPtr(WebCore::SVGFontFaceElement*)
5   0x1078db696 WebCore::CSSFontFaceSource::CSSFontFaceSource(WebCore::CSSFontFace&, WTF::String const&, WebCore::CachedFont*, WebCore::SVGFontFaceElement*, WTF::RefPtr<JSC::ArrayBufferView, WTF::DumbPtrTraits<JSC::ArrayBufferView> >&&)
6   0x1078dbb9d WebCore::CSSFontFaceSource::CSSFontFaceSource(WebCore::CSSFontFace&, WTF::String const&, WebCore::CachedFont*, WebCore::SVGFontFaceElement*, WTF::RefPtr<JSC::ArrayBufferView, WTF::DumbPtrTraits<JSC::ArrayBufferView> >&&)
7   0x1078cc4f6 WebCore::CSSFontFace::appendSources(WebCore::CSSFontFace&, WebCore::CSSValueList&, WebCore::Document*, bool)
8   0x1078de81d WebCore::CSSFontSelector::addFontFaceRule(WebCore::StyleRuleFontFace&, bool)
9   0x1078ddd9d WebCore::CSSFontSelector::buildCompleted()

and a bit of debugging (and logging via the Fonts log channel) shows that the CSSFontSelectors never go away. Document directly creates CSSFontSelectors, so now we've identified a retain cycle (Document -> CSSFontSelector -> CSSFontFaceSource -> SVGFontFaceElement -> Document) and the fix is to do some CSSFontSelector cleanup when the Document loses its last ref.

Last modified 14 months ago Last modified on Sep 8, 2018 9:16:05 PM