wiki:Abandoned documents

Version 8 (modified by Simon Fraser, 6 years ago) (diff)

--

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() 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.