V8GCController.cpp   [plain text]


/*
 * Copyright (C) 2009 Google Inc. All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 * 
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "V8GCController.h"

#include "DOMObjectsInclude.h"
#include "V8DOMMap.h"
#include "V8Index.h"
#include "V8Proxy.h"

#include <algorithm>
#include <utility>
#include <v8.h>
#include <v8-debug.h>
#include <wtf/HashMap.h>
#include <wtf/StdLibExtras.h>
#include <wtf/UnusedParam.h>

namespace WebCore {

#ifndef NDEBUG
// Keeps track of global handles created (not JS wrappers
// of DOM objects). Often these global handles are source
// of leaks.
//
// If you want to let a C++ object hold a persistent handle
// to a JS object, you should register the handle here to
// keep track of leaks.
//
// When creating a persistent handle, call:
//
// #ifndef NDEBUG
//    V8GCController::registerGlobalHandle(type, host, handle);
// #endif
//
// When releasing the handle, call:
//
// #ifndef NDEBUG
//    V8GCController::unregisterGlobalHandle(type, host, handle);
// #endif
//
typedef HashMap<v8::Value*, GlobalHandleInfo*> GlobalHandleMap;

static GlobalHandleMap& globalHandleMap()
{
    DEFINE_STATIC_LOCAL(GlobalHandleMap, staticGlobalHandleMap, ());
    return staticGlobalHandleMap;
}

// The function is the place to set the break point to inspect
// live global handles. Leaks are often come from leaked global handles.
static void enumerateGlobalHandles()
{
    for (GlobalHandleMap::iterator it = globalHandleMap().begin(), end = globalHandleMap().end(); it != end; ++it) {
        GlobalHandleInfo* info = it->second;
        UNUSED_PARAM(info);
        v8::Value* handle = it->first;
        UNUSED_PARAM(handle);
    }
}

void V8GCController::registerGlobalHandle(GlobalHandleType type, void* host, v8::Persistent<v8::Value> handle)
{
    ASSERT(!globalHandleMap().contains(*handle));
    globalHandleMap().set(*handle, new GlobalHandleInfo(host, type));
}

void V8GCController::unregisterGlobalHandle(void* host, v8::Persistent<v8::Value> handle)
{
    ASSERT(globalHandleMap().contains(*handle));
    GlobalHandleInfo* info = globalHandleMap().take(*handle);
    ASSERT(info->m_host == host);
    delete info;
}
#endif // ifndef NDEBUG

typedef HashMap<Node*, v8::Object*> DOMNodeMap;
typedef HashMap<void*, v8::Object*> DOMObjectMap;

#ifndef NDEBUG

static void enumerateDOMObjectMap(DOMObjectMap& wrapperMap)
{
    for (DOMObjectMap::iterator it = wrapperMap.begin(), end = wrapperMap.end(); it != end; ++it) {
        v8::Persistent<v8::Object> wrapper(it->second);
        V8ClassIndex::V8WrapperType type = V8DOMWrapper::domWrapperType(wrapper);
        void* object = it->first;
        UNUSED_PARAM(type);
        UNUSED_PARAM(object);
    }
}

class DOMObjectVisitor : public DOMWrapperMap<void>::Visitor {
public:
    void visitDOMWrapper(void* object, v8::Persistent<v8::Object> wrapper)
    {
        V8ClassIndex::V8WrapperType type = V8DOMWrapper::domWrapperType(wrapper);
        UNUSED_PARAM(type);
        UNUSED_PARAM(object);
    }
};

class EnsureWeakDOMNodeVisitor : public DOMWrapperMap<Node>::Visitor {
public:
    void visitDOMWrapper(Node* object, v8::Persistent<v8::Object> wrapper)
    {
        UNUSED_PARAM(object);
        ASSERT(wrapper.IsWeak());
    }
};

#endif // NDEBUG

// A map from a DOM node to its JS wrapper, the wrapper
// is kept as a strong reference to survive GCs.
static DOMObjectMap& gcProtectedMap()
{
    DEFINE_STATIC_LOCAL(DOMObjectMap, staticGcProtectedMap, ());
    return staticGcProtectedMap;
}

void V8GCController::gcProtect(void* domObject)
{
    if (!domObject)
        return;
    if (gcProtectedMap().contains(domObject))
        return;
    if (!getDOMObjectMap().contains(domObject))
        return;

    // Create a new (strong) persistent handle for the object.
    v8::Persistent<v8::Object> wrapper = getDOMObjectMap().get(domObject);
    if (wrapper.IsEmpty())
        return;

    gcProtectedMap().set(domObject, *v8::Persistent<v8::Object>::New(wrapper));
}

void V8GCController::gcUnprotect(void* domObject)
{
    if (!domObject)
        return;
    if (!gcProtectedMap().contains(domObject))
        return;

    // Dispose the strong reference.
    v8::Persistent<v8::Object> wrapper(gcProtectedMap().take(domObject));
    wrapper.Dispose();
}

class GCPrologueVisitor : public DOMWrapperMap<void>::Visitor {
public:
    void visitDOMWrapper(void* object, v8::Persistent<v8::Object> wrapper)
    {
        ASSERT(wrapper.IsWeak());
        V8ClassIndex::V8WrapperType type = V8DOMWrapper::domWrapperType(wrapper);
        switch (type) {
#define MAKE_CASE(TYPE, NAME)                             \
        case V8ClassIndex::TYPE: {                    \
            NAME* impl = static_cast<NAME*>(object);  \
            if (impl->hasPendingActivity())           \
                wrapper.ClearWeak();                  \
            break;                                    \
        }
    ACTIVE_DOM_OBJECT_TYPES(MAKE_CASE)
    default:
        ASSERT_NOT_REACHED();
#undef MAKE_CASE
        }

    // Additional handling of message port ensuring that entangled ports also
    // have their wrappers entangled. This should ideally be handled when the
    // ports are actually entangled in MessagePort::entangle, but to avoid
    // forking MessagePort.* this is postponed to GC time. Having this postponed
    // has the drawback that the wrappers are "entangled/unentangled" for each
    // GC even though their entaglement most likely is still the same.
    if (type == V8ClassIndex::MESSAGEPORT) {
        // Get the port and its entangled port.
        MessagePort* port1 = static_cast<MessagePort*>(object);
        MessagePort* port2 = port1->locallyEntangledPort();

        // If we are remotely entangled, then mark this object as reachable
        // (we can't determine reachability directly as the remote object is
        // out-of-proc).
        if (port1->isEntangled() && !port2)
            wrapper.ClearWeak();

        if (port2) {
            // As ports are always entangled in pairs only perform the entanglement
            // once for each pair (see ASSERT in MessagePort::unentangle()).
            if (port1 < port2) {
                v8::Handle<v8::Value> port1Wrapper = V8DOMWrapper::convertToV8Object(V8ClassIndex::MESSAGEPORT, port1);
                v8::Handle<v8::Value> port2Wrapper = V8DOMWrapper::convertToV8Object(V8ClassIndex::MESSAGEPORT, port2);
                ASSERT(port1Wrapper->IsObject());
                v8::Handle<v8::Object>::Cast(port1Wrapper)->SetInternalField(V8Custom::kMessagePortEntangledPortIndex, port2Wrapper);
                ASSERT(port2Wrapper->IsObject());
                v8::Handle<v8::Object>::Cast(port2Wrapper)->SetInternalField(V8Custom::kMessagePortEntangledPortIndex, port1Wrapper);
            }
        } else {
            // Remove the wrapper entanglement when a port is not entangled.
            if (V8DOMWrapper::domObjectHasJSWrapper(port1)) {
                v8::Handle<v8::Value> wrapper = V8DOMWrapper::convertToV8Object(V8ClassIndex::MESSAGEPORT, port1);
                ASSERT(wrapper->IsObject());
                v8::Handle<v8::Object>::Cast(wrapper)->SetInternalField(V8Custom::kMessagePortEntangledPortIndex, v8::Undefined());
            }
        }
    }
}
};

class GrouperItem {
public:
    GrouperItem(uintptr_t groupId, Node* node, v8::Persistent<v8::Object> wrapper) 
        : m_groupId(groupId)
        , m_node(node)
        , m_wrapper(wrapper) 
        {
        }

    uintptr_t groupId() const { return m_groupId; }
    Node* node() const { return m_node; }
    v8::Persistent<v8::Object> wrapper() const { return m_wrapper; }

private:
    uintptr_t m_groupId;
    Node* m_node;
    v8::Persistent<v8::Object> m_wrapper;
};

bool operator<(const GrouperItem& a, const GrouperItem& b)
{
    return a.groupId() < b.groupId();
}

typedef Vector<GrouperItem> GrouperList;

class ObjectGrouperVisitor : public DOMWrapperMap<Node>::Visitor {
public:
    ObjectGrouperVisitor()
    {
        // FIXME: grouper_.reserveCapacity(node_map.size());  ?
    }

    void visitDOMWrapper(Node* node, v8::Persistent<v8::Object> wrapper)
    {

        // If the node is in document, put it in the ownerDocument's object group.
        //
        // If an image element was created by JavaScript "new Image",
        // it is not in a document. However, if the load event has not
        // been fired (still onloading), it is treated as in the document.
        //
        // Otherwise, the node is put in an object group identified by the root
        // element of the tree to which it belongs.
        uintptr_t groupId;
        if (node->inDocument() || (node->hasTagName(HTMLNames::imgTag) && !static_cast<HTMLImageElement*>(node)->haveFiredLoadEvent()))
            groupId = reinterpret_cast<uintptr_t>(node->document());
        else {
            Node* root = node;
            while (root->parent())
                root = root->parent();

            // If the node is alone in its DOM tree (doesn't have a parent or any
            // children) then the group will be filtered out later anyway.
            if (root == node && !node->hasChildNodes())
                return;

            groupId = reinterpret_cast<uintptr_t>(root);
        }
        m_grouper.append(GrouperItem(groupId, node, wrapper));
    }

    void applyGrouping()
    {
        // Group by sorting by the group id.
        std::sort(m_grouper.begin(), m_grouper.end());

        // FIXME Should probably work in iterators here, but indexes were easier for my simple mind.
        for (size_t i = 0; i < m_grouper.size(); ) {
            // Seek to the next key (or the end of the list).
            size_t nextKeyIndex = m_grouper.size();
            for (size_t j = i; j < m_grouper.size(); ++j) {
                if (m_grouper[i].groupId() != m_grouper[j].groupId()) {
                    nextKeyIndex = j;
                    break;
                }
            }

            ASSERT(nextKeyIndex > i);

            // We only care about a group if it has more than one object. If it only
            // has one object, it has nothing else that needs to be kept alive.
            if (nextKeyIndex - i <= 1) {
                i = nextKeyIndex;
                continue;
            }

            Vector<v8::Persistent<v8::Value> > group;
            group.reserveCapacity(nextKeyIndex - i);
            for (; i < nextKeyIndex; ++i) {
                Node* node = m_grouper[i].node();
                v8::Persistent<v8::Value> wrapper = m_grouper[i].wrapper();
                if (!wrapper.IsEmpty())
                    group.append(wrapper);
                /* FIXME: Re-enabled this code to avoid GCing these wrappers!
                             Currently this depends on looking up the wrapper
                             during a GC, but we don't know which isolated world
                             we're in, so it's unclear which map to look in...

                // If the node is styled and there is a wrapper for the inline
                // style declaration, we need to keep that style declaration
                // wrapper alive as well, so we add it to the object group.
                if (node->isStyledElement()) {
                  StyledElement* element = reinterpret_cast<StyledElement*>(node);
                  CSSStyleDeclaration* style = element->inlineStyleDecl();
                  if (style != NULL) {
                    wrapper = getDOMObjectMap().get(style);
                    if (!wrapper.IsEmpty())
                      group.append(wrapper);
                  }
                }
                */
            }

            if (group.size() > 1)
                v8::V8::AddObjectGroup(&group[0], group.size());

            ASSERT(i == nextKeyIndex);
        }
    }

private:
    GrouperList m_grouper;
};

// Create object groups for DOM tree nodes.
void V8GCController::gcPrologue()
{
    v8::HandleScope scope;

#ifndef NDEBUG
    DOMObjectVisitor domObjectVisitor;
    visitDOMObjectsInCurrentThread(&domObjectVisitor);
#endif

    // Run through all objects with possible pending activity making their
    // wrappers non weak if there is pending activity.
    GCPrologueVisitor prologueVisitor;
    visitActiveDOMObjectsInCurrentThread(&prologueVisitor);

    // Create object groups.
    ObjectGrouperVisitor objectGrouperVisitor;
    visitDOMNodesInCurrentThread(&objectGrouperVisitor);
    objectGrouperVisitor.applyGrouping();
}

class GCEpilogueVisitor : public DOMWrapperMap<void>::Visitor {
public:
    void visitDOMWrapper(void* object, v8::Persistent<v8::Object> wrapper)
    {
        V8ClassIndex::V8WrapperType type = V8DOMWrapper::domWrapperType(wrapper);
        switch (type) {
#define MAKE_CASE(TYPE, NAME)                                           \
        case V8ClassIndex::TYPE: {                                  \
          NAME* impl = static_cast<NAME*>(object);                  \
          if (impl->hasPendingActivity()) {                         \
            ASSERT(!wrapper.IsWeak());                              \
            wrapper.MakeWeak(impl, &weakActiveDOMObjectCallback);   \
          }                                                         \
          break;                                                    \
        }
ACTIVE_DOM_OBJECT_TYPES(MAKE_CASE)
        default:
            ASSERT_NOT_REACHED();
#undef MAKE_CASE
    }
}
};

void V8GCController::gcEpilogue()
{
    v8::HandleScope scope;

    // Run through all objects with pending activity making their wrappers weak
    // again.
    GCEpilogueVisitor epilogueVisitor;
    visitActiveDOMObjectsInCurrentThread(&epilogueVisitor);

#ifndef NDEBUG
    // Check all survivals are weak.
    DOMObjectVisitor domObjectVisitor;
    visitDOMObjectsInCurrentThread(&domObjectVisitor);

    EnsureWeakDOMNodeVisitor weakDOMNodeVisitor;
    visitDOMNodesInCurrentThread(&weakDOMNodeVisitor);

    enumerateDOMObjectMap(gcProtectedMap());
    enumerateGlobalHandles();
#endif
}

}  // namespace WebCore