diff options
Diffstat (limited to 'dom/heapsnapshot/HeapSnapshot.cpp')
-rw-r--r-- | dom/heapsnapshot/HeapSnapshot.cpp | 1619 |
1 files changed, 1619 insertions, 0 deletions
diff --git a/dom/heapsnapshot/HeapSnapshot.cpp b/dom/heapsnapshot/HeapSnapshot.cpp new file mode 100644 index 000000000..299a96a9c --- /dev/null +++ b/dom/heapsnapshot/HeapSnapshot.cpp @@ -0,0 +1,1619 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "HeapSnapshot.h" + +#include <google/protobuf/io/coded_stream.h> +#include <google/protobuf/io/gzip_stream.h> +#include <google/protobuf/io/zero_copy_stream_impl_lite.h> + +#include "js/Debug.h" +#include "js/TypeDecls.h" +#include "js/UbiNodeBreadthFirst.h" +#include "js/UbiNodeCensus.h" +#include "js/UbiNodeDominatorTree.h" +#include "js/UbiNodeShortestPaths.h" +#include "mozilla/Attributes.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/devtools/AutoMemMap.h" +#include "mozilla/devtools/CoreDump.pb.h" +#include "mozilla/devtools/DeserializedNode.h" +#include "mozilla/devtools/DominatorTree.h" +#include "mozilla/devtools/FileDescriptorOutputStream.h" +#include "mozilla/devtools/HeapSnapshotTempFileHelperChild.h" +#include "mozilla/devtools/ZeroCopyNSIOutputStream.h" +#include "mozilla/dom/ChromeUtils.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/HeapSnapshotBinding.h" +#include "mozilla/RangedPtr.h" +#include "mozilla/Unused.h" + +#include "jsapi.h" +#include "jsfriendapi.h" +#include "nsCycleCollectionParticipant.h" +#include "nsCRTGlue.h" +#include "nsDirectoryServiceDefs.h" +#include "nsIFile.h" +#include "nsIOutputStream.h" +#include "nsISupportsImpl.h" +#include "nsNetUtil.h" +#include "nsPrintfCString.h" +#include "prerror.h" +#include "prio.h" +#include "prtypes.h" + +namespace mozilla { +namespace devtools { + +using namespace JS; +using namespace dom; + +using ::google::protobuf::io::ArrayInputStream; +using ::google::protobuf::io::CodedInputStream; +using ::google::protobuf::io::GzipInputStream; +using ::google::protobuf::io::ZeroCopyInputStream; + +using JS::ubi::AtomOrTwoByteChars; +using JS::ubi::ShortestPaths; + +MallocSizeOf +GetCurrentThreadDebuggerMallocSizeOf() +{ + auto ccjscx = CycleCollectedJSContext::Get(); + MOZ_ASSERT(ccjscx); + auto cx = ccjscx->Context(); + MOZ_ASSERT(cx); + auto mallocSizeOf = JS::dbg::GetDebuggerMallocSizeOf(cx); + MOZ_ASSERT(mallocSizeOf); + return mallocSizeOf; +} + +/*** Cycle Collection Boilerplate *****************************************************************/ + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(HeapSnapshot, mParent) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(HeapSnapshot) +NS_IMPL_CYCLE_COLLECTING_RELEASE(HeapSnapshot) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(HeapSnapshot) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +/* virtual */ JSObject* +HeapSnapshot::WrapObject(JSContext* aCx, HandleObject aGivenProto) +{ + return HeapSnapshotBinding::Wrap(aCx, this, aGivenProto); +} + +/*** Reading Heap Snapshots ***********************************************************************/ + +/* static */ already_AddRefed<HeapSnapshot> +HeapSnapshot::Create(JSContext* cx, + GlobalObject& global, + const uint8_t* buffer, + uint32_t size, + ErrorResult& rv) +{ + RefPtr<HeapSnapshot> snapshot = new HeapSnapshot(cx, global.GetAsSupports()); + if (!snapshot->init(cx, buffer, size)) { + rv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + return snapshot.forget(); +} + +template<typename MessageType> +static bool +parseMessage(ZeroCopyInputStream& stream, uint32_t sizeOfMessage, MessageType& message) +{ + // We need to create a new `CodedInputStream` for each message so that the + // 64MB limit is applied per-message rather than to the whole stream. + CodedInputStream codedStream(&stream); + + // The protobuf message nesting that core dumps exhibit is dominated by + // allocation stacks' frames. In the most deeply nested case, each frame has + // two messages: a StackFrame message and a StackFrame::Data message. These + // frames are on top of a small constant of other messages. There are a + // MAX_STACK_DEPTH number of frames, so we multiply this by 3 to make room for + // the two messages per frame plus some head room for the constant number of + // non-dominating messages. + codedStream.SetRecursionLimit(HeapSnapshot::MAX_STACK_DEPTH * 3); + + auto limit = codedStream.PushLimit(sizeOfMessage); + if (NS_WARN_IF(!message.ParseFromCodedStream(&codedStream)) || + NS_WARN_IF(!codedStream.ConsumedEntireMessage()) || + NS_WARN_IF(codedStream.BytesUntilLimit() != 0)) + { + return false; + } + + codedStream.PopLimit(limit); + return true; +} + +template<typename CharT, typename InternedStringSet> +struct GetOrInternStringMatcher +{ + InternedStringSet& internedStrings; + + explicit GetOrInternStringMatcher(InternedStringSet& strings) : internedStrings(strings) { } + + const CharT* match(const std::string* str) { + MOZ_ASSERT(str); + size_t length = str->length() / sizeof(CharT); + auto tempString = reinterpret_cast<const CharT*>(str->data()); + + UniquePtr<CharT[], NSFreePolicy> owned(NS_strndup(tempString, length)); + if (!owned || !internedStrings.append(Move(owned))) + return nullptr; + + return internedStrings.back().get(); + } + + const CharT* match(uint64_t ref) { + if (MOZ_LIKELY(ref < internedStrings.length())) { + auto& string = internedStrings[ref]; + MOZ_ASSERT(string); + return string.get(); + } + + return nullptr; + } +}; + +template< + // Either char or char16_t. + typename CharT, + // A reference to either `internedOneByteStrings` or `internedTwoByteStrings` + // if CharT is char or char16_t respectively. + typename InternedStringSet> +const CharT* +HeapSnapshot::getOrInternString(InternedStringSet& internedStrings, + Maybe<StringOrRef>& maybeStrOrRef) +{ + // Incomplete message: has neither a string nor a reference to an already + // interned string. + if (MOZ_UNLIKELY(maybeStrOrRef.isNothing())) + return nullptr; + + GetOrInternStringMatcher<CharT, InternedStringSet> m(internedStrings); + return maybeStrOrRef->match(m); +} + +// Get a de-duplicated string as a Maybe<StringOrRef> from the given `msg`. +#define GET_STRING_OR_REF_WITH_PROP_NAMES(msg, strPropertyName, refPropertyName) \ + (msg.has_##refPropertyName() \ + ? Some(StringOrRef(msg.refPropertyName())) \ + : msg.has_##strPropertyName() \ + ? Some(StringOrRef(&msg.strPropertyName())) \ + : Nothing()) + +#define GET_STRING_OR_REF(msg, property) \ + (msg.has_##property##ref() \ + ? Some(StringOrRef(msg.property##ref())) \ + : msg.has_##property() \ + ? Some(StringOrRef(&msg.property())) \ + : Nothing()) + +bool +HeapSnapshot::saveNode(const protobuf::Node& node, NodeIdSet& edgeReferents) +{ + // NB: de-duplicated string properties must be read back and interned in the + // same order here as they are written and serialized in + // `CoreDumpWriter::writeNode` or else indices in references to already + // serialized strings will be off. + + if (NS_WARN_IF(!node.has_id())) + return false; + NodeId id = node.id(); + + // NodeIds are derived from pointers (at most 48 bits) and we rely on them + // fitting into JS numbers (IEEE 754 doubles, can precisely store 53 bit + // integers) despite storing them on disk as 64 bit integers. + if (NS_WARN_IF(!JS::Value::isNumberRepresentable(id))) + return false; + + // Should only deserialize each node once. + if (NS_WARN_IF(nodes.has(id))) + return false; + + if (NS_WARN_IF(!JS::ubi::Uint32IsValidCoarseType(node.coarsetype()))) + return false; + auto coarseType = JS::ubi::Uint32ToCoarseType(node.coarsetype()); + + Maybe<StringOrRef> typeNameOrRef = GET_STRING_OR_REF_WITH_PROP_NAMES(node, typename_, typenameref); + auto typeName = getOrInternString<char16_t>(internedTwoByteStrings, typeNameOrRef); + if (NS_WARN_IF(!typeName)) + return false; + + if (NS_WARN_IF(!node.has_size())) + return false; + uint64_t size = node.size(); + + auto edgesLength = node.edges_size(); + DeserializedNode::EdgeVector edges; + if (NS_WARN_IF(!edges.reserve(edgesLength))) + return false; + for (decltype(edgesLength) i = 0; i < edgesLength; i++) { + auto& protoEdge = node.edges(i); + + if (NS_WARN_IF(!protoEdge.has_referent())) + return false; + NodeId referent = protoEdge.referent(); + + if (NS_WARN_IF(!edgeReferents.put(referent))) + return false; + + const char16_t* edgeName = nullptr; + if (protoEdge.EdgeNameOrRef_case() != protobuf::Edge::EDGENAMEORREF_NOT_SET) { + Maybe<StringOrRef> edgeNameOrRef = GET_STRING_OR_REF(protoEdge, name); + edgeName = getOrInternString<char16_t>(internedTwoByteStrings, edgeNameOrRef); + if (NS_WARN_IF(!edgeName)) + return false; + } + + edges.infallibleAppend(DeserializedEdge(referent, edgeName)); + } + + Maybe<StackFrameId> allocationStack; + if (node.has_allocationstack()) { + StackFrameId id = 0; + if (NS_WARN_IF(!saveStackFrame(node.allocationstack(), id))) + return false; + allocationStack.emplace(id); + } + MOZ_ASSERT(allocationStack.isSome() == node.has_allocationstack()); + + const char* jsObjectClassName = nullptr; + if (node.JSObjectClassNameOrRef_case() != protobuf::Node::JSOBJECTCLASSNAMEORREF_NOT_SET) { + Maybe<StringOrRef> clsNameOrRef = GET_STRING_OR_REF(node, jsobjectclassname); + jsObjectClassName = getOrInternString<char>(internedOneByteStrings, clsNameOrRef); + if (NS_WARN_IF(!jsObjectClassName)) + return false; + } + + const char* scriptFilename = nullptr; + if (node.ScriptFilenameOrRef_case() != protobuf::Node::SCRIPTFILENAMEORREF_NOT_SET) { + Maybe<StringOrRef> scriptFilenameOrRef = GET_STRING_OR_REF(node, scriptfilename); + scriptFilename = getOrInternString<char>(internedOneByteStrings, scriptFilenameOrRef); + if (NS_WARN_IF(!scriptFilename)) + return false; + } + + if (NS_WARN_IF(!nodes.putNew(id, DeserializedNode(id, coarseType, typeName, + size, Move(edges), + allocationStack, + jsObjectClassName, + scriptFilename, *this)))) + { + return false; + }; + + return true; +} + +bool +HeapSnapshot::saveStackFrame(const protobuf::StackFrame& frame, + StackFrameId& outFrameId) +{ + // NB: de-duplicated string properties must be read in the same order here as + // they are written in `CoreDumpWriter::getProtobufStackFrame` or else indices + // in references to already serialized strings will be off. + + if (frame.has_ref()) { + // We should only get a reference to the previous frame if we have already + // seen the previous frame. + if (!frames.has(frame.ref())) + return false; + + outFrameId = frame.ref(); + return true; + } + + // Incomplete message. + if (!frame.has_data()) + return false; + + auto data = frame.data(); + + if (!data.has_id()) + return false; + StackFrameId id = data.id(); + + // This should be the first and only time we see this frame. + if (frames.has(id)) + return false; + + if (!data.has_line()) + return false; + uint32_t line = data.line(); + + if (!data.has_column()) + return false; + uint32_t column = data.column(); + + if (!data.has_issystem()) + return false; + bool isSystem = data.issystem(); + + if (!data.has_isselfhosted()) + return false; + bool isSelfHosted = data.isselfhosted(); + + Maybe<StringOrRef> sourceOrRef = GET_STRING_OR_REF(data, source); + auto source = getOrInternString<char16_t>(internedTwoByteStrings, sourceOrRef); + if (!source) + return false; + + const char16_t* functionDisplayName = nullptr; + if (data.FunctionDisplayNameOrRef_case() != + protobuf::StackFrame_Data::FUNCTIONDISPLAYNAMEORREF_NOT_SET) + { + Maybe<StringOrRef> nameOrRef = GET_STRING_OR_REF(data, functiondisplayname); + functionDisplayName = getOrInternString<char16_t>(internedTwoByteStrings, nameOrRef); + if (!functionDisplayName) + return false; + } + + Maybe<StackFrameId> parent; + if (data.has_parent()) { + StackFrameId parentId = 0; + if (!saveStackFrame(data.parent(), parentId)) + return false; + parent = Some(parentId); + } + + if (!frames.putNew(id, DeserializedStackFrame(id, parent, line, column, + source, functionDisplayName, + isSystem, isSelfHosted, *this))) + { + return false; + } + + outFrameId = id; + return true; +} + +#undef GET_STRING_OR_REF_WITH_PROP_NAMES +#undef GET_STRING_OR_REF + +// Because protobuf messages aren't self-delimiting, we serialize each message +// preceded by its size in bytes. When deserializing, we read this size and then +// limit reading from the stream to the given byte size. If we didn't, then the +// first message would consume the entire stream. +static bool +readSizeOfNextMessage(ZeroCopyInputStream& stream, uint32_t* sizep) +{ + MOZ_ASSERT(sizep); + CodedInputStream codedStream(&stream); + return codedStream.ReadVarint32(sizep) && *sizep > 0; +} + +bool +HeapSnapshot::init(JSContext* cx, const uint8_t* buffer, uint32_t size) +{ + if (!nodes.init() || !frames.init()) + return false; + + ArrayInputStream stream(buffer, size); + GzipInputStream gzipStream(&stream); + uint32_t sizeOfMessage = 0; + + // First is the metadata. + + protobuf::Metadata metadata; + if (NS_WARN_IF(!readSizeOfNextMessage(gzipStream, &sizeOfMessage))) + return false; + if (!parseMessage(gzipStream, sizeOfMessage, metadata)) + return false; + if (metadata.has_timestamp()) + timestamp.emplace(metadata.timestamp()); + + // Next is the root node. + + protobuf::Node root; + if (NS_WARN_IF(!readSizeOfNextMessage(gzipStream, &sizeOfMessage))) + return false; + if (!parseMessage(gzipStream, sizeOfMessage, root)) + return false; + + // Although the id is optional in the protobuf format for future proofing, we + // can't currently do anything without it. + if (NS_WARN_IF(!root.has_id())) + return false; + rootId = root.id(); + + // The set of all node ids we've found edges pointing to. + NodeIdSet edgeReferents(cx); + if (NS_WARN_IF(!edgeReferents.init())) + return false; + + if (NS_WARN_IF(!saveNode(root, edgeReferents))) + return false; + + // Finally, the rest of the nodes in the core dump. + + // Test for the end of the stream. The protobuf library gives no way to tell + // the difference between an underlying read error and the stream being + // done. All we can do is attempt to read the size of the next message and + // extrapolate guestimations from the result of that operation. + while (readSizeOfNextMessage(gzipStream, &sizeOfMessage)) { + protobuf::Node node; + if (!parseMessage(gzipStream, sizeOfMessage, node)) + return false; + if (NS_WARN_IF(!saveNode(node, edgeReferents))) + return false; + } + + // Check the set of node ids referred to by edges we found and ensure that we + // have the node corresponding to each id. If we don't have all of them, it is + // unsafe to perform analyses of this heap snapshot. + for (auto range = edgeReferents.all(); !range.empty(); range.popFront()) { + if (NS_WARN_IF(!nodes.has(range.front()))) + return false; + } + + return true; +} + + +/*** Heap Snapshot Analyses ***********************************************************************/ + +void +HeapSnapshot::TakeCensus(JSContext* cx, JS::HandleObject options, + JS::MutableHandleValue rval, ErrorResult& rv) +{ + JS::ubi::Census census(cx); + if (NS_WARN_IF(!census.init())) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + JS::ubi::CountTypePtr rootType; + if (NS_WARN_IF(!JS::ubi::ParseCensusOptions(cx, census, options, rootType))) { + rv.Throw(NS_ERROR_UNEXPECTED); + return; + } + + JS::ubi::RootedCount rootCount(cx, rootType->makeCount()); + if (NS_WARN_IF(!rootCount)) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + JS::ubi::CensusHandler handler(census, rootCount, GetCurrentThreadDebuggerMallocSizeOf()); + + { + JS::AutoCheckCannotGC nogc; + + JS::ubi::CensusTraversal traversal(cx, handler, nogc); + if (NS_WARN_IF(!traversal.init())) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + if (NS_WARN_IF(!traversal.addStart(getRoot()))) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + if (NS_WARN_IF(!traversal.traverse())) { + rv.Throw(NS_ERROR_UNEXPECTED); + return; + } + } + + if (NS_WARN_IF(!handler.report(cx, rval))) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } +} + +void +HeapSnapshot::DescribeNode(JSContext* cx, JS::HandleObject breakdown, uint64_t nodeId, + JS::MutableHandleValue rval, ErrorResult& rv) { + MOZ_ASSERT(breakdown); + JS::RootedValue breakdownVal(cx, JS::ObjectValue(*breakdown)); + JS::ubi::CountTypePtr rootType = JS::ubi::ParseBreakdown(cx, breakdownVal); + if (NS_WARN_IF(!rootType)) { + rv.Throw(NS_ERROR_UNEXPECTED); + return; + } + + JS::ubi::RootedCount rootCount(cx, rootType->makeCount()); + if (NS_WARN_IF(!rootCount)) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + JS::ubi::Node::Id id(nodeId); + Maybe<JS::ubi::Node> node = getNodeById(id); + if (NS_WARN_IF(node.isNothing())) { + rv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + MallocSizeOf mallocSizeOf = GetCurrentThreadDebuggerMallocSizeOf(); + if (NS_WARN_IF(!rootCount->count(mallocSizeOf, *node))) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + if (NS_WARN_IF(!rootCount->report(cx, rval))) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } +} + + +already_AddRefed<DominatorTree> +HeapSnapshot::ComputeDominatorTree(ErrorResult& rv) +{ + Maybe<JS::ubi::DominatorTree> maybeTree; + { + auto ccjscx = CycleCollectedJSContext::Get(); + MOZ_ASSERT(ccjscx); + auto cx = ccjscx->Context(); + MOZ_ASSERT(cx); + JS::AutoCheckCannotGC nogc(cx); + maybeTree = JS::ubi::DominatorTree::Create(cx, nogc, getRoot()); + } + + if (NS_WARN_IF(maybeTree.isNothing())) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return nullptr; + } + + return MakeAndAddRef<DominatorTree>(Move(*maybeTree), this, mParent); +} + +void +HeapSnapshot::ComputeShortestPaths(JSContext*cx, uint64_t start, + const Sequence<uint64_t>& targets, + uint64_t maxNumPaths, + JS::MutableHandleObject results, + ErrorResult& rv) +{ + // First ensure that our inputs are valid. + + if (NS_WARN_IF(maxNumPaths == 0)) { + rv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + Maybe<JS::ubi::Node> startNode = getNodeById(start); + if (NS_WARN_IF(startNode.isNothing())) { + rv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + if (NS_WARN_IF(targets.Length() == 0)) { + rv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + // Aggregate the targets into a set and make sure that they exist in the heap + // snapshot. + + JS::ubi::NodeSet targetsSet; + if (NS_WARN_IF(!targetsSet.init())) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + for (const auto& target : targets) { + Maybe<JS::ubi::Node> targetNode = getNodeById(target); + if (NS_WARN_IF(targetNode.isNothing())) { + rv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + if (NS_WARN_IF(!targetsSet.put(*targetNode))) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + } + + // Walk the heap graph and find the shortest paths. + + Maybe<ShortestPaths> maybeShortestPaths; + { + JS::AutoCheckCannotGC nogc(cx); + maybeShortestPaths = ShortestPaths::Create(cx, nogc, maxNumPaths, *startNode, + Move(targetsSet)); + } + + if (NS_WARN_IF(maybeShortestPaths.isNothing())) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + auto& shortestPaths = *maybeShortestPaths; + + // Convert the results into a Map object mapping target node IDs to arrays of + // paths found. + + RootedObject resultsMap(cx, JS::NewMapObject(cx)); + if (NS_WARN_IF(!resultsMap)) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + for (auto range = shortestPaths.eachTarget(); !range.empty(); range.popFront()) { + JS::RootedValue key(cx, JS::NumberValue(range.front().identifier())); + JS::AutoValueVector paths(cx); + + bool ok = shortestPaths.forEachPath(range.front(), [&](JS::ubi::Path& path) { + JS::AutoValueVector pathValues(cx); + + for (JS::ubi::BackEdge* edge : path) { + JS::RootedObject pathPart(cx, JS_NewPlainObject(cx)); + if (!pathPart) { + return false; + } + + JS::RootedValue predecessor(cx, NumberValue(edge->predecessor().identifier())); + if (!JS_DefineProperty(cx, pathPart, "predecessor", predecessor, JSPROP_ENUMERATE)) { + return false; + } + + RootedValue edgeNameVal(cx, NullValue()); + if (edge->name()) { + RootedString edgeName(cx, JS_AtomizeUCString(cx, edge->name().get())); + if (!edgeName) { + return false; + } + edgeNameVal = StringValue(edgeName); + } + + if (!JS_DefineProperty(cx, pathPart, "edge", edgeNameVal, JSPROP_ENUMERATE)) { + return false; + } + + if (!pathValues.append(ObjectValue(*pathPart))) { + return false; + } + } + + RootedObject pathObj(cx, JS_NewArrayObject(cx, pathValues)); + return pathObj && paths.append(ObjectValue(*pathObj)); + }); + + if (NS_WARN_IF(!ok)) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + JS::RootedObject pathsArray(cx, JS_NewArrayObject(cx, paths)); + if (NS_WARN_IF(!pathsArray)) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + JS::RootedValue pathsVal(cx, ObjectValue(*pathsArray)); + if (NS_WARN_IF(!JS::MapSet(cx, resultsMap, key, pathsVal))) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + } + + results.set(resultsMap); +} + +/*** Saving Heap Snapshots ************************************************************************/ + +// If we are only taking a snapshot of the heap affected by the given set of +// globals, find the set of compartments the globals are allocated +// within. Returns false on OOM failure. +static bool +PopulateCompartmentsWithGlobals(CompartmentSet& compartments, AutoObjectVector& globals) +{ + if (!compartments.init()) + return false; + + unsigned length = globals.length(); + for (unsigned i = 0; i < length; i++) { + if (!compartments.put(GetObjectCompartment(globals[i]))) + return false; + } + + return true; +} + +// Add the given set of globals as explicit roots in the given roots +// list. Returns false on OOM failure. +static bool +AddGlobalsAsRoots(AutoObjectVector& globals, ubi::RootList& roots) +{ + unsigned length = globals.length(); + for (unsigned i = 0; i < length; i++) { + if (!roots.addRoot(ubi::Node(globals[i].get()), + u"heap snapshot global")) + { + return false; + } + } + return true; +} + +// Choose roots and limits for a traversal, given `boundaries`. Set `roots` to +// the set of nodes within the boundaries that are referred to by nodes +// outside. If `boundaries` does not include all JS compartments, initialize +// `compartments` to the set of included compartments; otherwise, leave +// `compartments` uninitialized. (You can use compartments.initialized() to +// check.) +// +// If `boundaries` is incoherent, or we encounter an error while trying to +// handle it, or we run out of memory, set `rv` appropriately and return +// `false`. +static bool +EstablishBoundaries(JSContext* cx, + ErrorResult& rv, + const HeapSnapshotBoundaries& boundaries, + ubi::RootList& roots, + CompartmentSet& compartments) +{ + MOZ_ASSERT(!roots.initialized()); + MOZ_ASSERT(!compartments.initialized()); + + bool foundBoundaryProperty = false; + + if (boundaries.mRuntime.WasPassed()) { + foundBoundaryProperty = true; + + if (!boundaries.mRuntime.Value()) { + rv.Throw(NS_ERROR_INVALID_ARG); + return false; + } + + if (!roots.init()) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return false; + } + } + + if (boundaries.mDebugger.WasPassed()) { + if (foundBoundaryProperty) { + rv.Throw(NS_ERROR_INVALID_ARG); + return false; + } + foundBoundaryProperty = true; + + JSObject* dbgObj = boundaries.mDebugger.Value(); + if (!dbgObj || !dbg::IsDebugger(*dbgObj)) { + rv.Throw(NS_ERROR_INVALID_ARG); + return false; + } + + AutoObjectVector globals(cx); + if (!dbg::GetDebuggeeGlobals(cx, *dbgObj, globals) || + !PopulateCompartmentsWithGlobals(compartments, globals) || + !roots.init(compartments) || + !AddGlobalsAsRoots(globals, roots)) + { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return false; + } + } + + if (boundaries.mGlobals.WasPassed()) { + if (foundBoundaryProperty) { + rv.Throw(NS_ERROR_INVALID_ARG); + return false; + } + foundBoundaryProperty = true; + + uint32_t length = boundaries.mGlobals.Value().Length(); + if (length == 0) { + rv.Throw(NS_ERROR_INVALID_ARG); + return false; + } + + AutoObjectVector globals(cx); + for (uint32_t i = 0; i < length; i++) { + JSObject* global = boundaries.mGlobals.Value().ElementAt(i); + if (!JS_IsGlobalObject(global)) { + rv.Throw(NS_ERROR_INVALID_ARG); + return false; + } + if (!globals.append(global)) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return false; + } + } + + if (!PopulateCompartmentsWithGlobals(compartments, globals) || + !roots.init(compartments) || + !AddGlobalsAsRoots(globals, roots)) + { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return false; + } + } + + if (!foundBoundaryProperty) { + rv.Throw(NS_ERROR_INVALID_ARG); + return false; + } + + MOZ_ASSERT(roots.initialized()); + MOZ_ASSERT_IF(boundaries.mDebugger.WasPassed(), compartments.initialized()); + MOZ_ASSERT_IF(boundaries.mGlobals.WasPassed(), compartments.initialized()); + return true; +} + + +// A variant covering all the various two-byte strings that we can get from the +// ubi::Node API. +class TwoByteString : public Variant<JSAtom*, const char16_t*, JS::ubi::EdgeName> +{ + using Base = Variant<JSAtom*, const char16_t*, JS::ubi::EdgeName>; + + struct AsTwoByteStringMatcher + { + TwoByteString match(JSAtom* atom) { + return TwoByteString(atom); + } + + TwoByteString match(const char16_t* chars) { + return TwoByteString(chars); + } + }; + + struct IsNonNullMatcher + { + template<typename T> + bool match(const T& t) { return t != nullptr; } + }; + + struct LengthMatcher + { + size_t match(JSAtom* atom) { + MOZ_ASSERT(atom); + JS::ubi::AtomOrTwoByteChars s(atom); + return s.length(); + } + + size_t match(const char16_t* chars) { + MOZ_ASSERT(chars); + return NS_strlen(chars); + } + + size_t match(const JS::ubi::EdgeName& ptr) { + MOZ_ASSERT(ptr); + return NS_strlen(ptr.get()); + } + }; + + struct CopyToBufferMatcher + { + RangedPtr<char16_t> destination; + size_t maxLength; + + CopyToBufferMatcher(RangedPtr<char16_t> destination, size_t maxLength) + : destination(destination) + , maxLength(maxLength) + { } + + size_t match(JS::ubi::EdgeName& ptr) { + return ptr ? match(ptr.get()) : 0; + } + + size_t match(JSAtom* atom) { + MOZ_ASSERT(atom); + JS::ubi::AtomOrTwoByteChars s(atom); + return s.copyToBuffer(destination, maxLength); + } + + size_t match(const char16_t* chars) { + MOZ_ASSERT(chars); + JS::ubi::AtomOrTwoByteChars s(chars); + return s.copyToBuffer(destination, maxLength); + } + }; + +public: + template<typename T> + MOZ_IMPLICIT TwoByteString(T&& rhs) : Base(Forward<T>(rhs)) { } + + template<typename T> + TwoByteString& operator=(T&& rhs) { + MOZ_ASSERT(this != &rhs, "self-move disallowed"); + this->~TwoByteString(); + new (this) TwoByteString(Forward<T>(rhs)); + return *this; + } + + TwoByteString(const TwoByteString&) = delete; + TwoByteString& operator=(const TwoByteString&) = delete; + + // Rewrap the inner value of a JS::ubi::AtomOrTwoByteChars as a TwoByteString. + static TwoByteString from(JS::ubi::AtomOrTwoByteChars&& s) { + AsTwoByteStringMatcher m; + return s.match(m); + } + + // Returns true if the given TwoByteString is non-null, false otherwise. + bool isNonNull() const { + IsNonNullMatcher m; + return match(m); + } + + // Return the length of the string, 0 if it is null. + size_t length() const { + LengthMatcher m; + return match(m); + } + + // Copy the contents of a TwoByteString into the provided buffer. The buffer + // is NOT null terminated. The number of characters written is returned. + size_t copyToBuffer(RangedPtr<char16_t> destination, size_t maxLength) { + CopyToBufferMatcher m(destination, maxLength); + return match(m); + } + + struct HashPolicy; +}; + +// A hashing policy for TwoByteString. +// +// Atoms are pointer hashed and use pointer equality, which means that we +// tolerate some duplication across atoms and the other two types of two-byte +// strings. In practice, we expect the amount of this duplication to be very low +// because each type is generally a different semantic thing in addition to +// having a slightly different representation. For example, the set of edge +// names and the set stack frames' source names naturally tend not to overlap +// very much if at all. +struct TwoByteString::HashPolicy { + using Lookup = TwoByteString; + + struct HashingMatcher { + js::HashNumber match(const JSAtom* atom) { + return js::DefaultHasher<const JSAtom*>::hash(atom); + } + + js::HashNumber match(const char16_t* chars) { + MOZ_ASSERT(chars); + auto length = NS_strlen(chars); + return HashString(chars, length); + } + + js::HashNumber match(const JS::ubi::EdgeName& ptr) { + MOZ_ASSERT(ptr); + return match(ptr.get()); + } + }; + + static js::HashNumber hash(const Lookup& l) { + HashingMatcher hasher; + return l.match(hasher); + } + + struct EqualityMatcher { + const TwoByteString& rhs; + explicit EqualityMatcher(const TwoByteString& rhs) : rhs(rhs) { } + + bool match(const JSAtom* atom) { + return rhs.is<JSAtom*>() && rhs.as<JSAtom*>() == atom; + } + + bool match(const char16_t* chars) { + MOZ_ASSERT(chars); + + const char16_t* rhsChars = nullptr; + if (rhs.is<const char16_t*>()) + rhsChars = rhs.as<const char16_t*>(); + else if (rhs.is<JS::ubi::EdgeName>()) + rhsChars = rhs.as<JS::ubi::EdgeName>().get(); + else + return false; + MOZ_ASSERT(rhsChars); + + auto length = NS_strlen(chars); + if (NS_strlen(rhsChars) != length) + return false; + + return memcmp(chars, rhsChars, length * sizeof(char16_t)) == 0; + } + + bool match(const JS::ubi::EdgeName& ptr) { + MOZ_ASSERT(ptr); + return match(ptr.get()); + } + }; + + static bool match(const TwoByteString& k, const Lookup& l) { + EqualityMatcher eq(l); + return k.match(eq); + } + + static void rekey(TwoByteString& k, TwoByteString&& newKey) { + k = Move(newKey); + } +}; + +// Returns whether `edge` should be included in a heap snapshot of +// `compartments`. The optional `policy` out-param is set to INCLUDE_EDGES +// if we want to include the referent's edges, or EXCLUDE_EDGES if we don't +// want to include them. +static bool +ShouldIncludeEdge(JS::CompartmentSet* compartments, + const ubi::Node& origin, const ubi::Edge& edge, + CoreDumpWriter::EdgePolicy* policy = nullptr) +{ + if (policy) { + *policy = CoreDumpWriter::INCLUDE_EDGES; + } + + if (!compartments) { + // We aren't targeting a particular set of compartments, so serialize all the + // things! + return true; + } + + // We are targeting a particular set of compartments. If this node is in our target + // set, serialize it and all of its edges. If this node is _not_ in our + // target set, we also serialize under the assumption that it is a shared + // resource being used by something in our target compartments since we reached it + // by traversing the heap graph. However, we do not serialize its outgoing + // edges and we abandon further traversal from this node. + // + // If the node does not belong to any compartment, we also serialize its outgoing + // edges. This case is relevant for Shapes: they don't belong to a specific + // compartment and contain edges to parent/kids Shapes we want to include. Note + // that these Shapes may contain pointers into our target compartment (the + // Shape's getter/setter JSObjects). However, we do not serialize nodes in other + // compartments that are reachable from these non-compartment nodes. + + JSCompartment* compartment = edge.referent.compartment(); + + if (!compartment || compartments->has(compartment)) { + return true; + } + + if (policy) { + *policy = CoreDumpWriter::EXCLUDE_EDGES; + } + + return !!origin.compartment(); +} + +// A `CoreDumpWriter` that serializes nodes to protobufs and writes them to the +// given `ZeroCopyOutputStream`. +class MOZ_STACK_CLASS StreamWriter : public CoreDumpWriter +{ + using FrameSet = js::HashSet<uint64_t>; + using TwoByteStringMap = js::HashMap<TwoByteString, uint64_t, TwoByteString::HashPolicy>; + using OneByteStringMap = js::HashMap<const char*, uint64_t>; + + JSContext* cx; + bool wantNames; + // The set of |JS::ubi::StackFrame::identifier()|s that have already been + // serialized and written to the core dump. + FrameSet framesAlreadySerialized; + // The set of two-byte strings that have already been serialized and written + // to the core dump. + TwoByteStringMap twoByteStringsAlreadySerialized; + // The set of one-byte strings that have already been serialized and written + // to the core dump. + OneByteStringMap oneByteStringsAlreadySerialized; + + ::google::protobuf::io::ZeroCopyOutputStream& stream; + + JS::CompartmentSet* compartments; + + bool writeMessage(const ::google::protobuf::MessageLite& message) { + // We have to create a new CodedOutputStream when writing each message so + // that the 64MB size limit used by Coded{Output,Input}Stream to prevent + // integer overflow is enforced per message rather than on the whole stream. + ::google::protobuf::io::CodedOutputStream codedStream(&stream); + codedStream.WriteVarint32(message.ByteSize()); + message.SerializeWithCachedSizes(&codedStream); + return !codedStream.HadError(); + } + + // Attach the full two-byte string or a reference to a two-byte string that + // has already been serialized to a protobuf message. + template <typename SetStringFunction, + typename SetRefFunction> + bool attachTwoByteString(TwoByteString& string, SetStringFunction setString, + SetRefFunction setRef) { + auto ptr = twoByteStringsAlreadySerialized.lookupForAdd(string); + if (ptr) { + setRef(ptr->value()); + return true; + } + + auto length = string.length(); + auto stringData = MakeUnique<std::string>(length * sizeof(char16_t), '\0'); + if (!stringData) + return false; + + auto buf = const_cast<char16_t*>(reinterpret_cast<const char16_t*>(stringData->data())); + string.copyToBuffer(RangedPtr<char16_t>(buf, length), length); + + uint64_t ref = twoByteStringsAlreadySerialized.count(); + if (!twoByteStringsAlreadySerialized.add(ptr, Move(string), ref)) + return false; + + setString(stringData.release()); + return true; + } + + // Attach the full one-byte string or a reference to a one-byte string that + // has already been serialized to a protobuf message. + template <typename SetStringFunction, + typename SetRefFunction> + bool attachOneByteString(const char* string, SetStringFunction setString, + SetRefFunction setRef) { + auto ptr = oneByteStringsAlreadySerialized.lookupForAdd(string); + if (ptr) { + setRef(ptr->value()); + return true; + } + + auto length = strlen(string); + auto stringData = MakeUnique<std::string>(string, length); + if (!stringData) + return false; + + uint64_t ref = oneByteStringsAlreadySerialized.count(); + if (!oneByteStringsAlreadySerialized.add(ptr, string, ref)) + return false; + + setString(stringData.release()); + return true; + } + + protobuf::StackFrame* getProtobufStackFrame(JS::ubi::StackFrame& frame, + size_t depth = 1) { + // NB: de-duplicated string properties must be written in the same order + // here as they are read in `HeapSnapshot::saveStackFrame` or else indices + // in references to already serialized strings will be off. + + MOZ_ASSERT(frame, + "null frames should be represented as the lack of a serialized " + "stack frame"); + + auto id = frame.identifier(); + auto protobufStackFrame = MakeUnique<protobuf::StackFrame>(); + if (!protobufStackFrame) + return nullptr; + + if (framesAlreadySerialized.has(id)) { + protobufStackFrame->set_ref(id); + return protobufStackFrame.release(); + } + + auto data = MakeUnique<protobuf::StackFrame_Data>(); + if (!data) + return nullptr; + + data->set_id(id); + data->set_line(frame.line()); + data->set_column(frame.column()); + data->set_issystem(frame.isSystem()); + data->set_isselfhosted(frame.isSelfHosted(cx)); + + auto dupeSource = TwoByteString::from(frame.source()); + if (!attachTwoByteString(dupeSource, + [&] (std::string* source) { data->set_allocated_source(source); }, + [&] (uint64_t ref) { data->set_sourceref(ref); })) + { + return nullptr; + } + + auto dupeName = TwoByteString::from(frame.functionDisplayName()); + if (dupeName.isNonNull()) { + if (!attachTwoByteString(dupeName, + [&] (std::string* name) { data->set_allocated_functiondisplayname(name); }, + [&] (uint64_t ref) { data->set_functiondisplaynameref(ref); })) + { + return nullptr; + } + } + + auto parent = frame.parent(); + if (parent && depth < HeapSnapshot::MAX_STACK_DEPTH) { + auto protobufParent = getProtobufStackFrame(parent, depth + 1); + if (!protobufParent) + return nullptr; + data->set_allocated_parent(protobufParent); + } + + protobufStackFrame->set_allocated_data(data.release()); + + if (!framesAlreadySerialized.put(id)) + return nullptr; + + return protobufStackFrame.release(); + } + +public: + StreamWriter(JSContext* cx, + ::google::protobuf::io::ZeroCopyOutputStream& stream, + bool wantNames, + JS::CompartmentSet* compartments) + : cx(cx) + , wantNames(wantNames) + , framesAlreadySerialized(cx) + , twoByteStringsAlreadySerialized(cx) + , oneByteStringsAlreadySerialized(cx) + , stream(stream) + , compartments(compartments) + { } + + bool init() { + return framesAlreadySerialized.init() && + twoByteStringsAlreadySerialized.init() && + oneByteStringsAlreadySerialized.init(); + } + + ~StreamWriter() override { } + + virtual bool writeMetadata(uint64_t timestamp) final { + protobuf::Metadata metadata; + metadata.set_timestamp(timestamp); + return writeMessage(metadata); + } + + virtual bool writeNode(const JS::ubi::Node& ubiNode, + EdgePolicy includeEdges) override final { + // NB: de-duplicated string properties must be written in the same order + // here as they are read in `HeapSnapshot::saveNode` or else indices in + // references to already serialized strings will be off. + + protobuf::Node protobufNode; + protobufNode.set_id(ubiNode.identifier()); + + protobufNode.set_coarsetype(JS::ubi::CoarseTypeToUint32(ubiNode.coarseType())); + + auto typeName = TwoByteString(ubiNode.typeName()); + if (NS_WARN_IF(!attachTwoByteString(typeName, + [&] (std::string* name) { protobufNode.set_allocated_typename_(name); }, + [&] (uint64_t ref) { protobufNode.set_typenameref(ref); }))) + { + return false; + } + + mozilla::MallocSizeOf mallocSizeOf = dbg::GetDebuggerMallocSizeOf(cx); + MOZ_ASSERT(mallocSizeOf); + protobufNode.set_size(ubiNode.size(mallocSizeOf)); + + if (includeEdges) { + auto edges = ubiNode.edges(cx, wantNames); + if (NS_WARN_IF(!edges)) + return false; + + for ( ; !edges->empty(); edges->popFront()) { + ubi::Edge& ubiEdge = edges->front(); + if (!ShouldIncludeEdge(compartments, ubiNode, ubiEdge)) { + continue; + } + + protobuf::Edge* protobufEdge = protobufNode.add_edges(); + if (NS_WARN_IF(!protobufEdge)) { + return false; + } + + protobufEdge->set_referent(ubiEdge.referent.identifier()); + + if (wantNames && ubiEdge.name) { + TwoByteString edgeName(Move(ubiEdge.name)); + if (NS_WARN_IF(!attachTwoByteString(edgeName, + [&] (std::string* name) { protobufEdge->set_allocated_name(name); }, + [&] (uint64_t ref) { protobufEdge->set_nameref(ref); }))) + { + return false; + } + } + } + } + + if (ubiNode.hasAllocationStack()) { + auto ubiStackFrame = ubiNode.allocationStack(); + auto protoStackFrame = getProtobufStackFrame(ubiStackFrame); + if (NS_WARN_IF(!protoStackFrame)) + return false; + protobufNode.set_allocated_allocationstack(protoStackFrame); + } + + if (auto className = ubiNode.jsObjectClassName()) { + if (NS_WARN_IF(!attachOneByteString(className, + [&] (std::string* name) { protobufNode.set_allocated_jsobjectclassname(name); }, + [&] (uint64_t ref) { protobufNode.set_jsobjectclassnameref(ref); }))) + { + return false; + } + } + + if (auto scriptFilename = ubiNode.scriptFilename()) { + if (NS_WARN_IF(!attachOneByteString(scriptFilename, + [&] (std::string* name) { protobufNode.set_allocated_scriptfilename(name); }, + [&] (uint64_t ref) { protobufNode.set_scriptfilenameref(ref); }))) + { + return false; + } + } + + return writeMessage(protobufNode); + } +}; + +// A JS::ubi::BreadthFirst handler that serializes a snapshot of the heap into a +// core dump. +class MOZ_STACK_CLASS HeapSnapshotHandler +{ + CoreDumpWriter& writer; + JS::CompartmentSet* compartments; + +public: + HeapSnapshotHandler(CoreDumpWriter& writer, + JS::CompartmentSet* compartments) + : writer(writer), + compartments(compartments) + { } + + // JS::ubi::BreadthFirst handler interface. + + class NodeData { }; + typedef JS::ubi::BreadthFirst<HeapSnapshotHandler> Traversal; + bool operator() (Traversal& traversal, + JS::ubi::Node origin, + const JS::ubi::Edge& edge, + NodeData*, + bool first) + { + // We're only interested in the first time we reach edge.referent, not in + // every edge arriving at that node. "But, don't we want to serialize every + // edge in the heap graph?" you ask. Don't worry! This edge is still + // serialized into the core dump. Serializing a node also serializes each of + // its edges, and if we are traversing a given edge, we must have already + // visited and serialized the origin node and its edges. + if (!first) + return true; + + CoreDumpWriter::EdgePolicy policy; + if (!ShouldIncludeEdge(compartments, origin, edge, &policy)) + return true; + + if (policy == CoreDumpWriter::EXCLUDE_EDGES) + traversal.abandonReferent(); + + return writer.writeNode(edge.referent, policy); + } +}; + + +bool +WriteHeapGraph(JSContext* cx, + const JS::ubi::Node& node, + CoreDumpWriter& writer, + bool wantNames, + JS::CompartmentSet* compartments, + JS::AutoCheckCannotGC& noGC) +{ + // Serialize the starting node to the core dump. + + if (NS_WARN_IF(!writer.writeNode(node, CoreDumpWriter::INCLUDE_EDGES))) { + return false; + } + + // Walk the heap graph starting from the given node and serialize it into the + // core dump. + + HeapSnapshotHandler handler(writer, compartments); + HeapSnapshotHandler::Traversal traversal(cx, handler, noGC); + if (!traversal.init()) + return false; + traversal.wantNames = wantNames; + + bool ok = traversal.addStartVisited(node) && + traversal.traverse(); + + return ok; +} + +static unsigned long +msSinceProcessCreation(const TimeStamp& now) +{ + bool ignored; + auto duration = now - TimeStamp::ProcessCreation(ignored); + return (unsigned long) duration.ToMilliseconds(); +} + +/* static */ already_AddRefed<nsIFile> +HeapSnapshot::CreateUniqueCoreDumpFile(ErrorResult& rv, + const TimeStamp& now, + nsAString& outFilePath) +{ + nsCOMPtr<nsIFile> file; + rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(file)); + if (NS_WARN_IF(rv.Failed())) + return nullptr; + + auto ms = msSinceProcessCreation(now); + rv = file->AppendNative(nsPrintfCString("%lu.fxsnapshot", ms)); + if (NS_WARN_IF(rv.Failed())) + return nullptr; + + rv = file->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0666); + if (NS_WARN_IF(rv.Failed())) + return nullptr; + + rv = file->GetPath(outFilePath); + if (NS_WARN_IF(rv.Failed())) + return nullptr; + + return file.forget(); +} + +// Deletion policy for cleaning up PHeapSnapshotTempFileHelperChild pointers. +class DeleteHeapSnapshotTempFileHelperChild +{ +public: + constexpr DeleteHeapSnapshotTempFileHelperChild() { } + + void operator()(PHeapSnapshotTempFileHelperChild* ptr) const { + Unused << NS_WARN_IF(!HeapSnapshotTempFileHelperChild::Send__delete__(ptr)); + } +}; + +// A UniquePtr alias to automatically manage PHeapSnapshotTempFileHelperChild +// pointers. +using UniqueHeapSnapshotTempFileHelperChild = UniquePtr<PHeapSnapshotTempFileHelperChild, + DeleteHeapSnapshotTempFileHelperChild>; + +// Get an nsIOutputStream that we can write the heap snapshot to. In non-e10s +// and in the e10s parent process, open a file directly and create an output +// stream for it. In e10s child processes, we are sandboxed without access to +// the filesystem. Use IPDL to request a file descriptor from the parent +// process. +static already_AddRefed<nsIOutputStream> +getCoreDumpOutputStream(ErrorResult& rv, TimeStamp& start, nsAString& outFilePath) +{ + if (XRE_IsParentProcess()) { + // Create the file and open the output stream directly. + + nsCOMPtr<nsIFile> file = HeapSnapshot::CreateUniqueCoreDumpFile(rv, + start, + outFilePath); + if (NS_WARN_IF(rv.Failed())) + return nullptr; + + nsCOMPtr<nsIOutputStream> outputStream; + rv = NS_NewLocalFileOutputStream(getter_AddRefs(outputStream), file, + PR_WRONLY, -1, 0); + if (NS_WARN_IF(rv.Failed())) + return nullptr; + + return outputStream.forget(); + } else { + // Request a file descriptor from the parent process over IPDL. + + auto cc = ContentChild::GetSingleton(); + if (!cc) { + rv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + UniqueHeapSnapshotTempFileHelperChild helper( + cc->SendPHeapSnapshotTempFileHelperConstructor()); + if (NS_WARN_IF(!helper)) { + rv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + OpenHeapSnapshotTempFileResponse response; + if (!helper->SendOpenHeapSnapshotTempFile(&response)) { + rv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + if (response.type() == OpenHeapSnapshotTempFileResponse::Tnsresult) { + rv.Throw(response.get_nsresult()); + return nullptr; + } + + auto opened = response.get_OpenedFile(); + outFilePath = opened.path(); + nsCOMPtr<nsIOutputStream> outputStream = + FileDescriptorOutputStream::Create(opened.descriptor()); + if (NS_WARN_IF(!outputStream)) { + rv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + return outputStream.forget(); + } +} + +} // namespace devtools + +namespace dom { + +using namespace JS; +using namespace devtools; + +/* static */ void +ThreadSafeChromeUtils::SaveHeapSnapshot(GlobalObject& global, + const HeapSnapshotBoundaries& boundaries, + nsAString& outFilePath, + ErrorResult& rv) +{ + auto start = TimeStamp::Now(); + + bool wantNames = true; + CompartmentSet compartments; + + nsCOMPtr<nsIOutputStream> outputStream = getCoreDumpOutputStream(rv, start, outFilePath); + if (NS_WARN_IF(rv.Failed())) + return; + + ZeroCopyNSIOutputStream zeroCopyStream(outputStream); + ::google::protobuf::io::GzipOutputStream gzipStream(&zeroCopyStream); + + JSContext* cx = global.Context(); + + { + Maybe<AutoCheckCannotGC> maybeNoGC; + ubi::RootList rootList(cx, maybeNoGC, wantNames); + if (!EstablishBoundaries(cx, rv, boundaries, rootList, compartments)) + return; + + StreamWriter writer(cx, gzipStream, wantNames, + compartments.initialized() ? &compartments : nullptr); + if (NS_WARN_IF(!writer.init())) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + MOZ_ASSERT(maybeNoGC.isSome()); + ubi::Node roots(&rootList); + + // Serialize the initial heap snapshot metadata to the core dump. + if (!writer.writeMetadata(PR_Now()) || + // Serialize the heap graph to the core dump, starting from our list of + // roots. + !WriteHeapGraph(cx, + roots, + writer, + wantNames, + compartments.initialized() ? &compartments : nullptr, + maybeNoGC.ref())) + { + rv.Throw(zeroCopyStream.failed() + ? zeroCopyStream.result() + : NS_ERROR_UNEXPECTED); + return; + } + } +} + +/* static */ already_AddRefed<HeapSnapshot> +ThreadSafeChromeUtils::ReadHeapSnapshot(GlobalObject& global, + const nsAString& filePath, + ErrorResult& rv) +{ + UniquePtr<char[]> path(ToNewCString(filePath)); + if (!path) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return nullptr; + } + + AutoMemMap mm; + rv = mm.init(path.get()); + if (rv.Failed()) + return nullptr; + + RefPtr<HeapSnapshot> snapshot = HeapSnapshot::Create( + global.Context(), global, reinterpret_cast<const uint8_t*>(mm.address()), + mm.size(), rv); + + return snapshot.forget(); +} + +} // namespace dom +} // namespace mozilla |