diff --git a/toolkit/devtools/server/ChromeUtils.cpp b/toolkit/devtools/server/ChromeUtils.cpp index 7d13abd015ae..db291198f61e 100644 --- a/toolkit/devtools/server/ChromeUtils.cpp +++ b/toolkit/devtools/server/ChromeUtils.cpp @@ -21,6 +21,7 @@ #include "prtypes.h" #include "js/Debug.h" +#include "js/TypeDecls.h" #include "js/UbiNodeTraverse.h" namespace mozilla { @@ -170,8 +171,8 @@ EstablishBoundaries(JSContext *cx, } -// A `CoreDumpWriter` that serializes nodes to protobufs and writes them to -// the given `CodedOutputStream`. +// A `CoreDumpWriter` that serializes nodes to protobufs and writes them to the +// given `ZeroCopyOutputStream`. class MOZ_STACK_CLASS StreamWriter : public CoreDumpWriter { JSContext *cx; @@ -382,5 +383,50 @@ ChromeUtils::SaveHeapSnapshot(GlobalObject &global, } } +/* static */ already_AddRefed +ChromeUtils::ReadHeapSnapshot(GlobalObject &global, + JSContext *cx, + const nsAString &filePath, + ErrorResult &rv) +{ + UniquePtr path(ToNewCString(filePath)); + if (!path) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return nullptr; + } + + PRFileInfo fileInfo; + if (PR_GetFileInfo(path.get(), &fileInfo) != PR_SUCCESS) { + rv.Throw(NS_ERROR_FILE_NOT_FOUND); + return nullptr; + } + + uint32_t size = fileInfo.size; + ScopedFreePtr buffer(static_cast(malloc(size))); + if (!buffer) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return nullptr; + } + + PRFileDesc *fd = PR_Open(path.get(), PR_RDONLY, 0); + if (!fd) { + rv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + uint32_t bytesRead = 0; + while (bytesRead < size) { + uint32_t bytesLeft = size - bytesRead; + int32_t bytesReadThisTime = PR_Read(fd, buffer.get() + bytesRead, bytesLeft); + if (bytesReadThisTime < 1) { + rv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + bytesRead += bytesReadThisTime; + } + + return HeapSnapshot::Create(cx, global, buffer.get(), size, rv); } -} + +} // namespace devtools +} // namespace mozilla diff --git a/toolkit/devtools/server/ChromeUtils.h b/toolkit/devtools/server/ChromeUtils.h index 211e755872f4..efbed444c3f3 100644 --- a/toolkit/devtools/server/ChromeUtils.h +++ b/toolkit/devtools/server/ChromeUtils.h @@ -12,6 +12,7 @@ #include "js/UbiNode.h" #include "js/UbiNodeTraverse.h" +#include "mozilla/AlreadyAddRefed.h" #include "mozilla/ErrorResult.h" #include "mozilla/dom/BindingDeclarations.h" #include "mozilla/dom/ChromeUtilsBinding.h" @@ -56,6 +57,9 @@ WriteHeapGraph(JSContext *cx, JS::AutoCheckCannotGC &noGC); +class HeapSnapshot; + + class ChromeUtils { public: @@ -64,6 +68,11 @@ public: const nsAString &filePath, const dom::HeapSnapshotBoundaries &boundaries, ErrorResult &rv); + + static already_AddRefed ReadHeapSnapshot(dom::GlobalObject &global, + JSContext *cx, + const nsAString &filePath, + ErrorResult &rv); }; } // namespace devtools diff --git a/toolkit/devtools/server/DeserializedNode.cpp b/toolkit/devtools/server/DeserializedNode.cpp new file mode 100644 index 000000000000..db1515f414d8 --- /dev/null +++ b/toolkit/devtools/server/DeserializedNode.cpp @@ -0,0 +1,109 @@ +/* -*- 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 "mozilla/devtools/DeserializedNode.h" + +namespace mozilla { +namespace devtools { + +DeserializedEdge::DeserializedEdge() + : referent(0) + , name(nullptr) +{ } + +DeserializedEdge::DeserializedEdge(DeserializedEdge &&rhs) +{ + referent = rhs.referent; + name = rhs.name; +} + +DeserializedEdge &DeserializedEdge::operator=(DeserializedEdge &&rhs) +{ + MOZ_ASSERT(&rhs != this); + this->~DeserializedEdge(); + new(this) DeserializedEdge(Move(rhs)); + return *this; +} + +bool +DeserializedEdge::init(const protobuf::Edge &edge, HeapSnapshot &owner) +{ + // Although the referent property is optional in the protobuf format for + // future compatibility, we can't semantically have an edge to nowhere and + // require a referent here. + if (!edge.has_referent()) + return false; + referent = edge.referent(); + + if (edge.has_name()) { + const char16_t* duplicateEdgeName = reinterpret_cast(edge.name().c_str()); + name = owner.borrowUniqueString(duplicateEdgeName, edge.name().length() / sizeof(char16_t)); + if (!name) + return false; + } + + return true; +} + +/* static */ UniquePtr +DeserializedNode::Create(const protobuf::Node &node, HeapSnapshot &owner) +{ + if (!node.has_id()) + return nullptr; + NodeId id = node.id(); + + if (!node.has_typename_()) + return nullptr; + + const char16_t* duplicatedTypeName = reinterpret_cast(node.typename_().c_str()); + const char16_t* uniqueTypeName = owner.borrowUniqueString(duplicatedTypeName, + node.typename_().length() / sizeof(char16_t)); + if (!uniqueTypeName) + return nullptr; + + auto edgesLength = node.edges_size(); + EdgeVector edges; + if (!edges.reserve(edgesLength)) + return nullptr; + for (decltype(edgesLength) i = 0; i < edgesLength; i++) { + DeserializedEdge edge; + if (!edge.init(node.edges(i), owner)) + return nullptr; + edges.infallibleAppend(Move(edge)); + } + + if (!node.has_size()) + return nullptr; + uint64_t size = node.size(); + + return MakeUnique(id, + uniqueTypeName, + size, + Move(edges), + owner); +} + +DeserializedNode::DeserializedNode(NodeId id, + const char16_t *typeName, + uint64_t size, + EdgeVector &&edges, + HeapSnapshot &owner) + : id(id) + , typeName(typeName) + , size(size) + , edges(Move(edges)) + , owner(&owner) +{ } + +DeserializedNode & +DeserializedNode::getEdgeReferent(const DeserializedEdge &edge) +{ + auto ptr = owner->nodes.lookup(edge.referent); + MOZ_ASSERT(ptr); + return *ptr->value(); +} + +} // namespace devtools +} // namespace mozilla diff --git a/toolkit/devtools/server/DeserializedNode.h b/toolkit/devtools/server/DeserializedNode.h new file mode 100644 index 000000000000..b1b3218e8604 --- /dev/null +++ b/toolkit/devtools/server/DeserializedNode.h @@ -0,0 +1,91 @@ +/* -*- 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/. */ + +#ifndef mozilla_devtools_DeserializedNode__ +#define mozilla_devtools_DeserializedNode__ + +#include "mozilla/devtools/CoreDump.pb.h" +#include "mozilla/MaybeOneOf.h" +#include "mozilla/Move.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Vector.h" + +// `Deserialized{Node,Edge}` translate protobuf messages from our core dump +// format into structures we can rely upon for implementing `JS::ubi::Node` +// specializations on top of. All of the properties of the protobuf messages are +// optional for future compatibility, and this is the layer where we validate +// that the properties that do actually exist in any given message fulfill our +// semantic requirements. +// +// Both `DeserializedNode` and `DeserializedEdge` are always owned by a +// `HeapSnapshot` instance, and their lifetimes must not extend after that of +// their owning `HeapSnapshot`. + +namespace mozilla { +namespace devtools { + +class HeapSnapshot; + +using NodeId = uint64_t; + +// A `DeserializedEdge` represents an edge in the heap graph pointing to the +// node with id equal to `DeserializedEdge::referent` that we deserialized from +// a core dump. +struct DeserializedEdge { + NodeId referent; + // A borrowed reference to a string owned by this node's owning HeapSnapshot. + const char16_t *name; + + explicit DeserializedEdge(); + DeserializedEdge(DeserializedEdge &&rhs); + DeserializedEdge &operator=(DeserializedEdge &&rhs); + + // Initialize this `DeserializedEdge` from the given `protobuf::Edge` message. + bool init(const protobuf::Edge &edge, HeapSnapshot &owner); + +private: + DeserializedEdge(const DeserializedEdge &) = delete; + DeserializedEdge& operator=(const DeserializedEdge &) = delete; +}; + +// A `DeserializedNode` is a node in the heap graph that we deserialized from a +// core dump. +struct DeserializedNode { + using EdgeVector = Vector; + using UniqueStringPtr = UniquePtr; + + NodeId id; + // A borrowed reference to a string owned by this node's owning HeapSnapshot. + const char16_t *typeName; + uint64_t size; + EdgeVector edges; + // A weak pointer to this node's owning `HeapSnapshot`. Safe without + // AddRef'ing because this node's lifetime is equal to that of its owner. + HeapSnapshot *owner; + + // Create a new `DeserializedNode` from the given `protobuf::Node` message. + static UniquePtr Create(const protobuf::Node &node, + HeapSnapshot &owner); + + DeserializedNode(NodeId id, + const char16_t *typeName, + uint64_t size, + EdgeVector &&edges, + HeapSnapshot &owner); + virtual ~DeserializedNode() { } + + // Get a borrowed reference to the given edge's referent. This method is + // virtual to provide a hook for gmock and gtest. + virtual DeserializedNode &getEdgeReferent(const DeserializedEdge &edge); + +private: + DeserializedNode(const DeserializedNode &) = delete; + DeserializedNode &operator=(const DeserializedNode &) = delete; +}; + +} // namespace devtools +} // namespace mozilla + +#endif // mozilla_devtools_DeserializedNode__ diff --git a/toolkit/devtools/server/HeapSnapshot.cpp b/toolkit/devtools/server/HeapSnapshot.cpp new file mode 100644 index 000000000000..4b61e4eb3e64 --- /dev/null +++ b/toolkit/devtools/server/HeapSnapshot.cpp @@ -0,0 +1,194 @@ +/* -*- 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 +#include +#include + +#include "mozilla/devtools/DeserializedNode.h" +#include "mozilla/dom/HeapSnapshotBinding.h" + +#include "CoreDump.pb.h" +#include "nsCycleCollectionParticipant.h" +#include "nsCRTGlue.h" +#include "nsISupportsImpl.h" + +namespace mozilla { +namespace devtools { + +using ::google::protobuf::io::ArrayInputStream; +using ::google::protobuf::io::CodedInputStream; +using ::google::protobuf::io::GzipInputStream; +using ::google::protobuf::io::ZeroCopyInputStream; + +NS_IMPL_CYCLE_COLLECTION_CLASS(HeapSnapshot) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(HeapSnapshot) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(HeapSnapshot) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(HeapSnapshot) + NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +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, JS::Handle aGivenProto) +{ + return dom::HeapSnapshotBinding::Wrap(aCx, this, aGivenProto); +} + +/* static */ already_AddRefed +HeapSnapshot::Create(JSContext *cx, + dom::GlobalObject &global, + const uint8_t *buffer, + uint32_t size, + ErrorResult &rv) +{ + nsRefPtr snapshot = new HeapSnapshot(cx, global.GetAsSupports()); + if (!snapshot->init(buffer, size)) { + rv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + return snapshot.forget(); +} + +template +static bool +parseMessage(ZeroCopyInputStream &stream, 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); + + // Because protobuf messages aren't self-delimiting, we serialize each message + // preceeded 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. + + uint32_t size = 0; + if (NS_WARN_IF(!codedStream.ReadVarint32(&size))) + return false; + + auto limit = codedStream.PushLimit(size); + if (NS_WARN_IF(!message.ParseFromCodedStream(&codedStream)) || + NS_WARN_IF(!codedStream.ConsumedEntireMessage())) + { + return false; + } + + codedStream.PopLimit(limit); + return true; +} + +bool +HeapSnapshot::saveNode(const protobuf::Node &node) +{ + UniquePtr dn(DeserializedNode::Create(node, *this)); + if (!dn) + return false; + return nodes.put(dn->id, Move(dn)); +} + +static inline bool +StreamHasData(GzipInputStream &stream) +{ + // 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 data and extrapolate guestimations + // from the result of that operation. + + const void *buf; + int size; + bool more = stream.Next(&buf, &size); + if (!more) + // Could not read any more data. We are optimistic and assume the stream is + // just exhausted and there is not an underlying IO error, since this + // function is only called at message boundaries. + return false; + + // There is more data still available in the stream. Return the data we read + // to the stream and let the parser get at it. + stream.BackUp(size); + return true; +} + +bool +HeapSnapshot::init(const uint8_t *buffer, uint32_t size) +{ + if (!nodes.init() || !strings.init()) + return false; + + ArrayInputStream stream(buffer, size); + GzipInputStream gzipStream(&stream); + + // First is the metadata. + + protobuf::Metadata metadata; + if (!parseMessage(gzipStream, metadata)) + return false; + if (metadata.has_timestamp()) + timestamp.emplace(metadata.timestamp()); + + // Next is the root node. + + protobuf::Node root; + if (!parseMessage(gzipStream, 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(); + + if (NS_WARN_IF(!saveNode(root))) + return false; + + // Finally, the rest of the nodes in the core dump. + + while (StreamHasData(gzipStream)) { + protobuf::Node node; + if (!parseMessage(gzipStream, node)) + return false; + if (NS_WARN_IF(!saveNode(node))) + return false; + } + + return true; +} + +const char16_t* +HeapSnapshot::borrowUniqueString(const char16_t* duplicateString, size_t length) +{ + MOZ_ASSERT(duplicateString); + UniqueStringHashPolicy::Lookup lookup(duplicateString, length); + auto ptr = strings.lookupForAdd(lookup); + + if (!ptr) { + UniqueString owned(NS_strndup(duplicateString, length)); + if (!owned || !strings.add(ptr, Move(owned))) + return nullptr; + } + + MOZ_ASSERT(ptr->get() != duplicateString); + return ptr->get(); +} + + +} // namespace devtools +} // namespace mozilla diff --git a/toolkit/devtools/server/HeapSnapshot.h b/toolkit/devtools/server/HeapSnapshot.h new file mode 100644 index 000000000000..9d14c829633e --- /dev/null +++ b/toolkit/devtools/server/HeapSnapshot.h @@ -0,0 +1,133 @@ +/* -*- 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/. */ + +#ifndef mozilla_devtools_HeapSnapshot__ +#define mozilla_devtools_HeapSnapshot__ + +#include "js/HashTable.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/devtools/DeserializedNode.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/HashFunctions.h" +#include "mozilla/Maybe.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" + +#include "CoreDump.pb.h" +#include "nsCOMPtr.h" +#include "nsCRTGlue.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsWrapperCache.h" +#include "nsXPCOM.h" + +namespace mozilla { +namespace devtools { + +struct NSFreePolicy { + void operator()(void* ptr) { + NS_Free(ptr); + } +}; + +using UniqueString = UniquePtr; + +struct UniqueStringHashPolicy { + struct Lookup { + const char16_t* str; + size_t length; + + Lookup(const char16_t* str, size_t length) + : str(str) + , length(length) + { } + }; + + static js::HashNumber hash(const Lookup& lookup) { + MOZ_ASSERT(lookup.str); + return HashString(lookup.str, lookup.length); + } + + static bool match(const UniqueString& existing, const Lookup& lookup) { + MOZ_ASSERT(lookup.str); + return NS_strncmp(existing.get(), lookup.str, lookup.length) == 0; + } +}; + +class HeapSnapshot final : public nsISupports + , public nsWrapperCache +{ + friend struct DeserializedNode; + + explicit HeapSnapshot(JSContext *cx, nsISupports *aParent) + : timestamp(Nothing()) + , rootId(0) + , nodes(cx) + , strings(cx) + , mParent(aParent) + { + MOZ_ASSERT(aParent); + }; + + // Initialize this HeapSnapshot from the given buffer that contains a + // serialized core dump. Do NOT take ownership of the buffer, only borrow it + // for the duration of the call. Return false on failure. + bool init(const uint8_t *buffer, uint32_t size); + + // Save the given `protobuf::Node` message in this `HeapSnapshot` as a + // `DeserializedNode`. + bool saveNode(const protobuf::Node &node); + + // If present, a timestamp in the same units that `PR_Now` gives. + Maybe timestamp; + + // The id of the root node for this deserialized heap graph. + NodeId rootId; + + // The set of nodes in this deserialized heap graph, keyed by id. + using NodeMap = js::HashMap>; + NodeMap nodes; + + // Core dump files have many duplicate strings: type names are repeated for + // each node, and although in theory edge names are highly customizable for + // specific edges, in practice they are also highly duplicated. Rather than + // make each Deserialized{Node,Edge} malloc their own copy of their edge and + // type names, we de-duplicate the strings here and Deserialized{Node,Edge} + // get borrowed pointers into this set. + using UniqueStringSet = js::HashSet; + UniqueStringSet strings; + +protected: + nsCOMPtr mParent; + + virtual ~HeapSnapshot() { } + +public: + // Create a `HeapSnapshot` from the given buffer that contains a serialized + // core dump. Do NOT take ownership of the buffer, only borrow it for the + // duration of the call. + static already_AddRefed Create(JSContext *cx, + dom::GlobalObject &global, + const uint8_t *buffer, + uint32_t size, + ErrorResult &rv); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(HeapSnapshot) + MOZ_DECLARE_REFCOUNTED_TYPENAME(HeapSnapshot) + + nsISupports *GetParentObject() const { return mParent; } + + virtual JSObject *WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + const char16_t* borrowUniqueString(const char16_t* duplicateString, + size_t length); +}; + +} // namespace devtools +} // namespace mozilla + +#endif // mozilla_devtools_HeapSnapshot__ diff --git a/toolkit/devtools/server/moz.build b/toolkit/devtools/server/moz.build index b4fcdca05b26..1cedd326048a 100644 --- a/toolkit/devtools/server/moz.build +++ b/toolkit/devtools/server/moz.build @@ -20,12 +20,16 @@ XPIDL_MODULE = 'jsinspector' EXPORTS.mozilla.devtools += [ 'ChromeUtils.h', 'CoreDump.pb.h', + 'DeserializedNode.h', + 'HeapSnapshot.h', 'ZeroCopyNSIOutputStream.h', ] SOURCES += [ 'ChromeUtils.cpp', 'CoreDump.pb.cc', + 'DeserializedNode.cpp', + 'HeapSnapshot.cpp', 'nsJSInspector.cpp', 'ZeroCopyNSIOutputStream.cpp', ] diff --git a/xpcom/glue/nsCRTGlue.cpp b/xpcom/glue/nsCRTGlue.cpp index c2fdbede0d37..17c034932cdc 100644 --- a/xpcom/glue/nsCRTGlue.cpp +++ b/xpcom/glue/nsCRTGlue.cpp @@ -76,6 +76,7 @@ NS_strtok(const char* aDelims, char** aStr) uint32_t NS_strlen(const char16_t* aString) { + MOZ_ASSERT(aString); const char16_t* end; for (end = aString; *end; ++end) { @@ -101,6 +102,23 @@ NS_strcmp(const char16_t* aStrA, const char16_t* aStrB) return *aStrA != '\0'; } +int +NS_strncmp(const char16_t* aStrA, const char16_t* aStrB, size_t aLen) +{ + while (aLen && *aStrB) { + int r = *aStrA - *aStrB; + if (r) { + return r; + } + + ++aStrA; + ++aStrB; + --aLen; + } + + return aLen ? *aStrA != '\0' : *aStrA - *aStrB; +} + char16_t* NS_strdup(const char16_t* aString) { diff --git a/xpcom/glue/nsCRTGlue.h b/xpcom/glue/nsCRTGlue.h index 1c10611a0e11..4e59c137c455 100644 --- a/xpcom/glue/nsCRTGlue.h +++ b/xpcom/glue/nsCRTGlue.h @@ -47,6 +47,11 @@ uint32_t NS_strlen(const char16_t* aString); */ int NS_strcmp(const char16_t* aStrA, const char16_t* aStrB); +/** + * "strncmp" for char16_t strings + */ +int NS_strncmp(const char16_t* aStrA, const char16_t* aStrB, size_t aLen); + /** * "strdup" for char16_t strings, uses the NS_Alloc allocator. */