/* -*- 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/. */
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;
}
template <typename MessageType> staticbool 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)) { returnfalse;
}
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;
// 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())
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())) returnfalse;
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))) returnfalse;
// Should only deserialize each node once. if (NS_WARN_IF(nodes.has(id))) returnfalse;
if (NS_WARN_IF(!JS::ubi::Uint32IsValidCoarseType(node.coarsetype()))) returnfalse; auto coarseType = JS::ubi::Uint32ToCoarseType(node.coarsetype());
if (NS_WARN_IF(!node.has_size())) returnfalse;
uint64_t size = node.size();
auto edgesLength = node.edges_size();
DeserializedNode::EdgeVector edges; if (NS_WARN_IF(!edges.reserve(edgesLength))) returnfalse; for (decltype(edgesLength) i = 0; i < edgesLength; i++) { auto& protoEdge = node.edges(i);
if (NS_WARN_IF(!protoEdge.has_referent())) returnfalse;
NodeId referent = protoEdge.referent();
if (NS_WARN_IF(!edgeReferents.put(referent))) returnfalse;
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())) returnfalse;
outFrameId = frame.ref(); returntrue;
}
// Incomplete message. if (!frame.has_data()) returnfalse;
auto data = frame.data();
if (!data.has_id()) returnfalse;
StackFrameId id = data.id();
// This should be the first and only time we see this frame. if (frames.has(id)) returnfalse;
if (!data.has_line()) returnfalse;
uint32_t line = data.line();
if (!data.has_column()) returnfalse;
JS::TaggedColumnNumberOneOrigin column(
JS::LimitedColumnNumberOneOrigin(data.column()));
if (!data.has_issystem()) returnfalse; bool isSystem = data.issystem();
if (!data.has_isselfhosted()) returnfalse; bool isSelfHosted = data.isselfhosted();
Maybe<StringOrRef> sourceOrRef = GET_STRING_OR_REF(data, source); auto source =
getOrInternString<char16_t>(internedTwoByteStrings, sourceOrRef); if (!source) returnfalse;
// 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. staticbool readSizeOfNextMessage(ZeroCopyInputStream& stream,
uint32_t* sizep) {
MOZ_ASSERT(sizep);
CodedInputStream codedStream(&stream); return codedStream.ReadVarint32(sizep) && *sizep > 0;
}
protobuf::Metadata metadata; if (NS_WARN_IF(!readSizeOfNextMessage(gzipStream, &sizeOfMessage))) returnfalse; if (!parseMessage(gzipStream, sizeOfMessage, metadata)) returnfalse; if (metadata.has_timestamp()) timestamp.emplace(metadata.timestamp());
// Next is the root node.
protobuf::Node root; if (NS_WARN_IF(!readSizeOfNextMessage(gzipStream, &sizeOfMessage))) returnfalse; if (!parseMessage(gzipStream, sizeOfMessage, root)) returnfalse;
// 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())) returnfalse;
rootId = root.id();
// The set of all node ids we've found edges pointing to.
NodeIdSet edgeReferents(cx);
if (NS_WARN_IF(!saveNode(root, edgeReferents))) returnfalse;
// 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)) returnfalse; if (NS_WARN_IF(!saveNode(node, edgeReferents))) returnfalse;
}
// 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 iter = edgeReferents.iter(); !iter.done(); iter.next()) { if (NS_WARN_IF(!nodes.has(iter.get()))) returnfalse;
}
// 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. staticbool PopulateCompartmentsWithGlobals(
CompartmentSet& compartments, JS::HandleVector<JSObject*> globals) { unsigned length = globals.length(); for (unsigned i = 0; i < length; i++) { if (!compartments.put(JS::GetCompartment(globals[i]))) returnfalse;
}
returntrue;
}
// Add the given set of globals as explicit roots in the given roots // list. Returns false on OOM failure. staticbool AddGlobalsAsRoots(JS::HandleVector<JSObject*> 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")) { returnfalse;
}
} returntrue;
}
// 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`. // // Return value is a pair of the status and an AutoCheckCannotGC token, // forwarded from ubi::RootList::init(), to ensure that the caller does // not GC while the RootList is live and initialized. static std::pair<bool, AutoCheckCannotGC> EstablishBoundaries(
JSContext* cx, ErrorResult& rv, const HeapSnapshotBoundaries& boundaries,
ubi::RootList& roots, CompartmentSet& compartments) {
MOZ_ASSERT(!roots.initialized());
MOZ_ASSERT(compartments.empty());
bool foundBoundaryProperty = false;
if (boundaries.mRuntime.WasPassed()) {
foundBoundaryProperty = true;
if (!boundaries.mRuntime.Value()) {
rv.Throw(NS_ERROR_INVALID_ARG); return {false, AutoCheckCannotGC(cx)};
}
auto [ok, nogc] = roots.init(); if (!ok) {
rv.Throw(NS_ERROR_OUT_OF_MEMORY); return {false, nogc};
}
}
if (boundaries.mDebugger.WasPassed()) { if (foundBoundaryProperty) {
rv.Throw(NS_ERROR_INVALID_ARG); return {false, AutoCheckCannotGC(cx)};
}
foundBoundaryProperty = 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>;
// Rewrap the inner value of a JS::ubi::AtomOrTwoByteChars as a TwoByteString. static TwoByteString from(JS::ubi::AtomOrTwoByteChars&& s) { return s.match([](auto* a) { return TwoByteString(a); });
}
// Returns true if the given TwoByteString is non-null, false otherwise. bool isNonNull() const { return match([](auto& t) { return t != nullptr; });
}
// Return the length of the string, 0 if it is null.
size_t length() const { return match(
[](JSAtom* atom) -> size_t {
MOZ_ASSERT(atom);
JS::ubi::AtomOrTwoByteChars s(atom); return s.length();
},
[](const char16_t* chars) -> size_t {
MOZ_ASSERT(chars); return NS_strlen(chars);
},
[](const JS::ubi::EdgeName& ptr) -> size_t {
MOZ_ASSERT(ptr); return NS_strlen(ptr.get());
});
}
// 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;
// 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. staticbool 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! returntrue;
}
// 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.
if (!compartment || compartments->has(compartment)) { returntrue;
}
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<constchar*, 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;
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.ByteSizeLong());
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()); returntrue;
}
auto length = string.length(); auto stringData = MakeUnique<std::string>(length * sizeof(char16_t), '\0'); if (!stringData) returnfalse;
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, std::move(string), ref)) returnfalse;
setString(stringData.release()); returntrue;
}
// 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(constchar* string, SetStringFunction setString,
SetRefFunction setRef) { auto ptr = oneByteStringsAlreadySerialized.lookupForAdd(string); if (ptr) {
setRef(ptr->value()); returntrue;
}
auto length = strlen(string); auto stringData = MakeUnique<std::string>(string, length); if (!stringData) returnfalse;
uint64_t ref = oneByteStringsAlreadySerialized.count(); if (!oneByteStringsAlreadySerialized.add(ptr, string, ref)) returnfalse;
setString(stringData.release()); returntrue;
}
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;
bool writeNode(const JS::ubi::Node& ubiNode, EdgePolicy includeEdges) 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.
if (ubiNode.hasAllocationStack()) { auto ubiStackFrame = ubiNode.allocationStack(); auto protoStackFrame = getProtobufStackFrame(ubiStackFrame); if (NS_WARN_IF(!protoStackFrame)) returnfalse;
protobufNode.set_allocated_allocationstack(protoStackFrame);
}
if (ubiNode.descriptiveTypeName()) { auto descriptiveTypeName = TwoByteString(ubiNode.descriptiveTypeName()); if (NS_WARN_IF(!attachTwoByteString(
descriptiveTypeName,
[&](std::string* name) {
protobufNode.set_allocated_descriptivetypename(name);
},
[&](uint64_t ref) {
protobufNode.set_descriptivetypenameref(ref);
}))) { returnfalse;
}
}
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: // For telemetry.
uint32_t nodeCount;
uint32_t edgeCount;
// 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) returntrue;
CoreDumpWriter::EdgePolicy policy; if (!ShouldIncludeEdge(compartments, origin, edge, &policy)) { // Because ShouldIncludeEdge considers the |origin| node as well, we don't // want to consider this node 'visited' until we write it to the core // dump.
traversal.doNotMarkReferentAsVisited(); returntrue;
}
nodeCount++;
if (policy == CoreDumpWriter::EXCLUDE_EDGES) traversal.abandonReferent();
nsAutoString tempPath;
rv = file->GetPath(tempPath); 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;
// The snapshot ID must be computed in the process that created the // temp file, because TmpD may not be the same in all processes.
outSnapshotId.Assign(Substring(
outFilePath, tempPath.Length() + 1,
outFilePath.Length() - tempPath.Length() - sizeof(".fxsnapshot")));
return file.forget();
}
// Deletion policy for cleaning up PHeapSnapshotTempFileHelperChild pointers. class DeleteHeapSnapshotTempFileHelperChild { public:
constexpr DeleteHeapSnapshotTempFileHelperChild() {}
// 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,
nsAString& outSnapshotId) { if (XRE_IsParentProcess()) { // Create the file and open the output stream directly.
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.