blob: 69c8f6bc6dda95c85eaf56063ca84cfbe9bd0d50 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/bindings/core/v8/v8_embedder_graph_builder.h"
#include "third_party/blink/renderer/bindings/core/v8/active_script_wrappable.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_gc_controller.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_node.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/platform/bindings/script_wrappable.h"
#include "third_party/blink/renderer/platform/bindings/wrapper_type_info.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/wtf/allocator/allocator.h"
namespace blink {
namespace {
using Traceable = const void*;
using Graph = v8::EmbedderGraph;
// Information about whether a node is attached to the main DOM tree
// or not. It is computed as follows:
// 1) A Document with IsContextDestroyed() = true is detached.
// 2) A Document with IsContextDestroyed() = false is attached.
// 3) A Node that is not connected to any Document is detached.
// 4) A Node that is connected to a detached Document is detached.
// 5) A Node that is connected to an attached Document is attached.
// 6) A ScriptWrappable that is reachable from an attached Node is
// attached.
// 7) A ScriptWrappable that is reachable from a detached Node is
// detached.
// 8) A ScriptWrappable that is not reachable from any Node is
// considered (conservatively) as attached.
// The unknown state applies to ScriptWrappables during graph
// traversal when we don't have reachability information yet.
enum class DomTreeState { kAttached, kDetached, kUnknown };
DomTreeState DomTreeStateFromWrapper(v8::Isolate* isolate,
uint16_t class_id,
v8::Local<v8::Object> v8_value) {
if (class_id != WrapperTypeInfo::kNodeClassId)
return DomTreeState::kUnknown;
Node* node = V8Node::ToImpl(v8_value);
Node* root = V8GCController::OpaqueRootForGC(isolate, node);
if (root->isConnected() &&
!node->GetDocument().MasterDocument().IsContextDestroyed()) {
return DomTreeState::kAttached;
}
return DomTreeState::kDetached;
}
class EmbedderNode : public Graph::Node {
public:
EmbedderNode(const char* name,
Graph::Node* wrapper,
DomTreeState dom_tree_state)
: name_(name), wrapper_(wrapper), dom_tree_state_(dom_tree_state) {}
DomTreeState GetDomTreeState() { return dom_tree_state_; }
void UpdateDomTreeState(DomTreeState parent_dom_tree_state) {
// If the child's state is unknown, then take the parent's state.
// If the parent is attached, then the child is also attached.
if (dom_tree_state_ == DomTreeState::kUnknown ||
parent_dom_tree_state == DomTreeState::kAttached) {
dom_tree_state_ = parent_dom_tree_state;
}
}
// Graph::Node overrides.
const char* Name() override { return name_; }
const char* NamePrefix() override {
return dom_tree_state_ == DomTreeState::kDetached ? "Detached" : nullptr;
}
size_t SizeInBytes() override { return 0; }
Graph::Node* WrapperNode() override { return wrapper_; }
private:
const char* name_;
Graph::Node* wrapper_;
DomTreeState dom_tree_state_;
};
class EmbedderRootNode : public EmbedderNode {
public:
explicit EmbedderRootNode(const char* name)
: EmbedderNode(name, nullptr, DomTreeState::kUnknown) {}
// Graph::Node override.
bool IsRootNode() override { return true; }
};
class NodeBuilder final {
USING_FAST_MALLOC(NodeBuilder);
public:
explicit NodeBuilder(Graph* graph) : graph_(graph) {}
Graph::Node* GraphNode(const v8::Local<v8::Value>&);
EmbedderNode* GraphNode(Traceable,
const char* name,
Graph::Node* wrapper,
DomTreeState);
bool Contains(Traceable traceable) const {
return graph_nodes_.Contains(traceable);
}
private:
Graph* const graph_;
HashMap<Traceable, EmbedderNode*> graph_nodes_;
};
v8::EmbedderGraph::Node* NodeBuilder::GraphNode(
const v8::Local<v8::Value>& value) {
return graph_->V8Node(value);
}
EmbedderNode* NodeBuilder::GraphNode(Traceable traceable,
const char* name,
v8::EmbedderGraph::Node* wrapper,
DomTreeState dom_tree_state) {
auto iter = graph_nodes_.find(traceable);
if (iter != graph_nodes_.end()) {
iter->value->UpdateDomTreeState(dom_tree_state);
return iter->value;
}
// Ownership of the new node is transferred to the graph_.
// graph_node_.at(tracable) is valid for all BuildEmbedderGraph execution.
auto* raw_node = new EmbedderNode(name, wrapper, dom_tree_state);
EmbedderNode* node = static_cast<EmbedderNode*>(
graph_->AddNode(std::unique_ptr<Graph::Node>(raw_node)));
graph_nodes_.insert(traceable, node);
return node;
}
// V8EmbedderGraphBuilder is used to build heap snapshots of Blink's managed
// object graph. On a high level, the following operations are performed:
// - Objects are classified as attached, detached, or unknown.
// - Depending an implicit mode, objects are classified as relevant or internal.
// This classification happens based on NameTrait and the fact that all
// ScriptWrappable objects (those that can have JS properties) are using that
// trait.
// - Not relevant objects are filtered where possible, e.g., sub graphs of
// internals are filtered and not reported.
//
// The algorithm performs a single pass on the graph, starting from V8-to-Blink
// references that identify attached nodes. Each object then starts a recursion
// into its own subgraph to identify and filter subgraphs that only consist of
// internals. Roots, which are potentially Blink only, are transitively
// traversed after handling JavaScript related objects.
class GC_PLUGIN_IGNORE(
"This class is not managed by Oilpan but GC plugin recognizes it as such "
"due to Trace methods.") V8EmbedderGraphBuilder
: public Visitor,
public v8::PersistentHandleVisitor,
public v8::EmbedderHeapTracer::TracedGlobalHandleVisitor {
public:
V8EmbedderGraphBuilder(v8::Isolate*, Graph*, NodeBuilder*);
~V8EmbedderGraphBuilder() override;
void BuildEmbedderGraph();
// v8::PersistentHandleVisitor override.
void VisitPersistentHandle(v8::Persistent<v8::Value>*,
uint16_t class_id) override;
// v8::EmbedderHeapTracer::TracedGlobalHandleVisitor override.
void VisitTracedGlobalHandle(
const v8::TracedGlobal<v8::Value>& value) override;
// Visitor overrides.
void Visit(const TraceWrapperV8Reference<v8::Value>&) final;
void Visit(void*, TraceDescriptor) final;
void VisitBackingStoreStrongly(const char*,
void* object,
void** object_slot,
TraceDescriptor desc) final;
// Unused Visitor overrides.
void VisitWeak(void* object,
void** object_slot,
TraceDescriptor desc,
WeakCallback callback) final {}
void VisitBackingStoreWeakly(const char*,
void*,
void**,
TraceDescriptor,
WeakCallback,
void*) final {}
void VisitBackingStoreOnly(const char*, void*, void**) final {}
void RegisterBackingStoreCallback(void**, MovingObjectCallback, void*) final {
}
void RegisterWeakCallback(void*, WeakCallback) final {}
private:
class ParentScope {
STACK_ALLOCATED();
public:
ParentScope(V8EmbedderGraphBuilder* visitor, Traceable traceable)
: visitor_(visitor) {
visitor->current_parent_ = traceable;
}
~ParentScope() { visitor_->current_parent_ = nullptr; }
private:
V8EmbedderGraphBuilder* const visitor_;
};
class State final {
USING_FAST_MALLOC(State);
public:
State(Traceable traceable, const char* name, DomTreeState dom_tree_state)
: traceable_(traceable), name_(name), dom_tree_state_(dom_tree_state) {}
explicit State(EmbedderNode* node)
: node_(node), dom_tree_state_(node->GetDomTreeState()) {}
bool IsVisited() const { return visited_; }
void MarkVisited() { visited_ = true; }
bool IsPending() const { return pending_; }
void MarkPending() { pending_ = true; }
void UnmarkPending() { pending_ = false; }
bool HasNode() const { return node_; }
EmbedderNode* GetOrCreateNode(NodeBuilder* builder) {
if (!node_) {
DCHECK(name_);
node_ = builder->GraphNode(traceable_, name_, nullptr, dom_tree_state_);
}
return node_;
}
DomTreeState GetDomTreeState() const { return dom_tree_state_; }
void UpdateDomTreeState(DomTreeState parent_dom_tree_state) {
// If the child's state is unknown, then take the parent's state.
// If the parent is attached, then the child is also attached.
if (dom_tree_state_ == DomTreeState::kUnknown ||
parent_dom_tree_state == DomTreeState::kAttached) {
dom_tree_state_ = parent_dom_tree_state;
}
if (node_)
node_->UpdateDomTreeState(dom_tree_state_);
}
private:
EmbedderNode* node_ = nullptr;
Traceable traceable_ = nullptr;
const char* name_ = nullptr;
DomTreeState dom_tree_state_;
bool visited_ = false;
bool pending_ = false;
};
// WorklistItemBase is used for different kinds of items that require
// processing the regular worklist.
class WorklistItemBase {
USING_FAST_MALLOC(WorklistItemBase);
public:
explicit WorklistItemBase(State* parent, State* to_process)
: parent_(parent), to_process_(to_process) {}
virtual ~WorklistItemBase() = default;
virtual void Process(V8EmbedderGraphBuilder*) = 0;
State* to_process() const { return to_process_; }
State* parent() const { return parent_; }
private:
State* const parent_;
State* const to_process_;
};
// A VisitationItem processes a given object and visits all its children.
class VisitationItem final : public WorklistItemBase {
public:
VisitationItem(State* parent,
State* to_process,
Traceable traceable,
TraceCallback trace_callback)
: WorklistItemBase(parent, to_process),
traceable_(traceable),
trace_callback_(trace_callback) {}
void Process(V8EmbedderGraphBuilder* builder) final {
// Post-order traversal as parent needs the full information on child.
builder->worklist_.push_back(std::unique_ptr<WorklistItemBase>{
new VisitationDoneItem(parent(), to_process())});
to_process()->MarkPending();
DCHECK(to_process()->IsPending());
DCHECK(to_process()->IsVisited());
ParentScope parent_scope(builder, traceable_);
trace_callback_(builder, const_cast<void*>(traceable_));
}
private:
Traceable traceable_;
TraceCallback trace_callback_;
};
// A VisitationDoneItem unmarks the pending state of an object and creates an
// edge from a parent in case there is one.
class VisitationDoneItem final : public WorklistItemBase {
public:
VisitationDoneItem(State* parent, State* to_process)
: WorklistItemBase(parent, to_process) {}
void Process(V8EmbedderGraphBuilder* builder) final {
if (parent() && to_process()->HasNode()) {
NodeBuilder* node_builder = builder->node_builder_;
builder->graph_->AddEdge(parent()->GetOrCreateNode(node_builder),
to_process()->GetOrCreateNode(node_builder));
}
to_process()->UnmarkPending();
}
};
State* GetOrCreateState(Traceable traceable,
const char* name,
DomTreeState dom_tree_state) {
if (!states_.Contains(traceable)) {
states_.insert(traceable, new State(traceable, name, dom_tree_state));
}
return states_.at(traceable);
}
State* GetStateNotNull(Traceable traceable) {
CHECK(states_.Contains(traceable));
return states_.at(traceable);
}
State* EnsureState(Traceable traceable, EmbedderNode* node) {
if (!states_.Contains(traceable)) {
states_.insert(traceable, new State(node));
}
return states_.at(traceable);
}
void EnsureRootState(EmbedderNode* node) {
CHECK(!states_.Contains(node));
states_.insert(node, new State(node));
}
void VisitPersistentHandleInternal(v8::Local<v8::Object>, uint16_t);
void VisitPendingActivities();
void VisitBlinkRoots();
void VisitTransitiveClosure();
// Push a VisitatonItem to the main worklist in case the State has not been
// already visited.
void CreateAndPushVisitationItem(State* parent,
State* to_process,
Traceable traceable,
TraceCallback trace_callback) {
DCHECK(!to_process->IsVisited());
to_process->MarkVisited();
worklist_.push_back(std::unique_ptr<WorklistItemBase>{
new VisitationItem(parent, to_process, traceable, trace_callback)});
}
void PushVisitationItem(std::unique_ptr<VisitationItem> item) {
if (!item->to_process()->IsVisited()) {
item->to_process()->MarkVisited();
worklist_.push_back(std::move(item));
}
}
v8::Isolate* const isolate_;
Graph* const graph_;
NodeBuilder* const node_builder_;
Traceable current_parent_ = nullptr;
HashMap<Traceable, State*> states_;
// The default worklist that is used to visit transitive closure.
Deque<std::unique_ptr<WorklistItemBase>> worklist_;
// The worklist that collects detached Nodes during persistent handle
// iteration.
Deque<std::unique_ptr<VisitationItem>> detached_worklist_;
// The worklist that collects ScriptWrappables with unknown information
// about attached/detached state during persistent handle iteration.
Deque<std::unique_ptr<VisitationItem>> unknown_worklist_;
};
V8EmbedderGraphBuilder::V8EmbedderGraphBuilder(v8::Isolate* isolate,
Graph* graph,
NodeBuilder* node_builder)
: Visitor(ThreadState::Current()),
isolate_(isolate),
graph_(graph),
node_builder_(node_builder) {
CHECK(isolate);
CHECK(graph);
CHECK_EQ(isolate, ThreadState::Current()->GetIsolate());
}
V8EmbedderGraphBuilder::~V8EmbedderGraphBuilder() {
for (const auto& kvp : states_) {
delete kvp.value;
}
}
void V8EmbedderGraphBuilder::BuildEmbedderGraph() {
isolate_->VisitHandlesWithClassIds(this);
v8::EmbedderHeapTracer* tracer =
V8PerIsolateData::From(isolate_)->GetEmbedderHeapTracer();
if (tracer)
tracer->IterateTracedGlobalHandles(this);
// At this point we collected ScriptWrappables in three groups:
// attached, detached, and unknown.
#if DCHECK_IS_ON()
for (auto const& item : worklist_) {
DCHECK_EQ(DomTreeState::kAttached, item->to_process()->GetDomTreeState());
}
for (auto const& item : detached_worklist_) {
DCHECK_EQ(DomTreeState::kDetached, item->to_process()->GetDomTreeState());
}
for (auto const& item : unknown_worklist_) {
DCHECK_EQ(DomTreeState::kUnknown, item->to_process()->GetDomTreeState());
}
#endif
// We need to propagate attached/detached information to ScriptWrappables
// with the unknown state. The information propagates from a parent to
// a child as follows:
// - if the parent is attached, then the child is considered attached.
// - if the parent is detached and the child is unknown, then the child is
// considered detached.
// - if the parent is unknown, then the state of the child does not change.
//
// We need to organize DOM traversal in three stages to ensure correct
// propagation:
// 1) Traverse from the attached nodes. All nodes discovered in this stage
// will be marked as kAttached.
// 2) Traverse from the detached nodes. All nodes discovered in this stage
// will be marked as kDetached if they are not already marked as kAttached.
// 3) Traverse from the unknown nodes. This is needed only for edge recording.
// Stage 1: find transitive closure of the attached nodes.
VisitTransitiveClosure();
// Stage 2: find transitive closure of the detached nodes.
while (!detached_worklist_.empty()) {
auto item = std::move(detached_worklist_.back());
detached_worklist_.pop_back();
PushVisitationItem(std::move(item));
}
VisitTransitiveClosure();
// Stage 3: find transitive closure of the unknown nodes.
// Nodes reachable only via pending activities are treated as unknown.
VisitPendingActivities();
VisitBlinkRoots();
while (!unknown_worklist_.empty()) {
auto item = std::move(unknown_worklist_.back());
unknown_worklist_.pop_back();
PushVisitationItem(std::move(item));
}
VisitTransitiveClosure();
DCHECK(worklist_.empty());
DCHECK(detached_worklist_.empty());
DCHECK(unknown_worklist_.empty());
}
void V8EmbedderGraphBuilder::VisitPersistentHandleInternal(
v8::Local<v8::Object> v8_value,
uint16_t class_id) {
ScriptWrappable* traceable = ToScriptWrappable(v8_value);
if (!traceable)
return;
Graph::Node* wrapper = node_builder_->GraphNode(v8_value);
DomTreeState dom_tree_state =
DomTreeStateFromWrapper(isolate_, class_id, v8_value);
EmbedderNode* graph_node = node_builder_->GraphNode(
traceable, traceable->NameInHeapSnapshot(), wrapper, dom_tree_state);
State* const to_process_state = EnsureState(traceable, graph_node);
const TraceDescriptor& descriptor =
TraceDescriptorFor<ScriptWrappable>(traceable);
switch (graph_node->GetDomTreeState()) {
case DomTreeState::kAttached:
CreateAndPushVisitationItem(nullptr, to_process_state, traceable,
descriptor.callback);
break;
case DomTreeState::kDetached:
detached_worklist_.push_back(
std::unique_ptr<VisitationItem>{new VisitationItem(
nullptr, to_process_state, traceable, descriptor.callback)});
break;
case DomTreeState::kUnknown:
unknown_worklist_.push_back(
std::unique_ptr<VisitationItem>{new VisitationItem(
nullptr, to_process_state, traceable, descriptor.callback)});
break;
}
}
void V8EmbedderGraphBuilder::VisitTracedGlobalHandle(
const v8::TracedGlobal<v8::Value>& value) {
const uint16_t class_id = value.WrapperClassId();
if (class_id != WrapperTypeInfo::kNodeClassId &&
class_id != WrapperTypeInfo::kObjectClassId)
return;
VisitPersistentHandleInternal(value.As<v8::Object>().Get(isolate_), class_id);
}
void V8EmbedderGraphBuilder::VisitPersistentHandle(
v8::Persistent<v8::Value>* value,
uint16_t class_id) {
if (class_id != WrapperTypeInfo::kNodeClassId &&
class_id != WrapperTypeInfo::kObjectClassId)
return;
v8::Local<v8::Object> v8_value = v8::Local<v8::Object>::New(
isolate_, v8::Persistent<v8::Object>::Cast(*value));
VisitPersistentHandleInternal(v8_value, class_id);
}
void V8EmbedderGraphBuilder::Visit(
const TraceWrapperV8Reference<v8::Value>& traced_wrapper) {
// Add an edge from the current parent to the V8 object.
v8::Local<v8::Value> v8_value = traced_wrapper.NewLocal(isolate_);
if (!v8_value.IsEmpty()) {
State* parent = GetStateNotNull(current_parent_);
graph_->AddEdge(parent->GetOrCreateNode(node_builder_),
node_builder_->GraphNode(v8_value));
}
}
void V8EmbedderGraphBuilder::Visit(void* object,
TraceDescriptor wrapper_descriptor) {
const void* traceable = wrapper_descriptor.base_object_payload;
const GCInfo* info = GCInfoTable::Get().GCInfoFromIndex(
HeapObjectHeader::FromPayload(traceable)->GcInfoIndex());
HeapObjectName name = info->name(traceable);
State* const parent = GetStateNotNull(current_parent_);
State* const current =
GetOrCreateState(traceable, name.value, parent->GetDomTreeState());
if (current->IsPending()) {
if (parent->HasNode()) {
// Backedge in currently processed graph.
graph_->AddEdge(parent->GetOrCreateNode(node_builder_),
current->GetOrCreateNode(node_builder_));
}
return;
}
// Immediately materialize the node if it is not hidden.
if (!name.name_is_hidden) {
current->GetOrCreateNode(node_builder_);
}
// Propagate the parent's DomTreeState down to the current state.
current->UpdateDomTreeState(parent->GetDomTreeState());
if (!current->IsVisited()) {
CreateAndPushVisitationItem(parent, current, traceable, info->trace);
} else {
// Edge into an already processed subgraph.
if (current->HasNode()) {
// Create an edge in case the current node has already been visited.
graph_->AddEdge(parent->GetOrCreateNode(node_builder_),
current->GetOrCreateNode(node_builder_));
}
}
}
void V8EmbedderGraphBuilder::VisitBackingStoreStrongly(const char*,
void* object,
void** object_slot,
TraceDescriptor desc) {
if (!object)
return;
desc.callback(this, desc.base_object_payload);
}
void V8EmbedderGraphBuilder::VisitPendingActivities() {
// Ownership of the new node is transferred to the graph_.
EmbedderNode* root =
static_cast<EmbedderNode*>(graph_->AddNode(std::unique_ptr<Graph::Node>(
new EmbedderRootNode("Pending activities"))));
EnsureRootState(root);
ParentScope parent(this, root);
ActiveScriptWrappableBase::TraceActiveScriptWrappables(isolate_, this);
}
void V8EmbedderGraphBuilder::VisitBlinkRoots() {
{
EmbedderNode* root = static_cast<EmbedderNode*>(graph_->AddNode(
std::unique_ptr<Graph::Node>(new EmbedderRootNode("Blink roots"))));
EnsureRootState(root);
ParentScope parent(this, root);
ThreadState::Current()->GetPersistentRegion()->TracePersistentNodes(this);
}
{
EmbedderNode* root =
static_cast<EmbedderNode*>(graph_->AddNode(std::unique_ptr<Graph::Node>(
new EmbedderRootNode("Blink cross-thread roots"))));
EnsureRootState(root);
ParentScope parent(this, root);
MutexLocker persistent_lock(ProcessHeap::CrossThreadPersistentMutex());
ProcessHeap::GetCrossThreadPersistentRegion().TracePersistentNodes(this);
}
}
void V8EmbedderGraphBuilder::VisitTransitiveClosure() {
// Depth-first search.
while (!worklist_.empty()) {
std::unique_ptr<WorklistItemBase> item = std::move(worklist_.back());
worklist_.pop_back();
item->Process(this);
}
}
} // namespace
void EmbedderGraphBuilder::BuildEmbedderGraphCallback(v8::Isolate* isolate,
v8::EmbedderGraph* graph,
void*) {
NodeBuilder node_builder(graph);
V8EmbedderGraphBuilder builder(isolate, graph, &node_builder);
builder.BuildEmbedderGraph();
}
} // namespace blink