mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-10 02:08:38 +09:00
Merge remote-tracking branch 'origin/master' into custom
This commit is contained in:
commit
55828393ce
2798 changed files with 8839 additions and 257595 deletions
|
|
@ -1144,7 +1144,7 @@ nsScriptLoader::InstantiateModuleTree(nsModuleLoadRequest* aRequest)
|
|||
|
||||
nsModuleScript* ms = aRequest->mModuleScript;
|
||||
MOZ_ASSERT(ms);
|
||||
if (!ms->ModuleRecord()) {
|
||||
if (!ms || !ms->ModuleRecord()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
1
dom/heapsnapshot/.gitattributes
vendored
Normal file
1
dom/heapsnapshot/.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
CoreDump.pb.* binary
|
||||
64
dom/heapsnapshot/AutoMemMap.cpp
Normal file
64
dom/heapsnapshot/AutoMemMap.cpp
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* 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/AutoMemMap.h"
|
||||
|
||||
#include "mozilla/Unused.h"
|
||||
#include "nsDebug.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace devtools {
|
||||
|
||||
AutoMemMap::~AutoMemMap()
|
||||
{
|
||||
if (addr) {
|
||||
Unused << NS_WARN_IF(PR_MemUnmap(addr, size()) != PR_SUCCESS);
|
||||
addr = nullptr;
|
||||
}
|
||||
|
||||
if (fileMap) {
|
||||
Unused << NS_WARN_IF(PR_CloseFileMap(fileMap) != PR_SUCCESS);
|
||||
fileMap = nullptr;
|
||||
}
|
||||
|
||||
if (fd) {
|
||||
Unused << NS_WARN_IF(PR_Close(fd) != PR_SUCCESS);
|
||||
fd = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
nsresult
|
||||
AutoMemMap::init(const char* filePath, int flags, int mode, PRFileMapProtect prot)
|
||||
{
|
||||
MOZ_ASSERT(!fd);
|
||||
MOZ_ASSERT(!fileMap);
|
||||
MOZ_ASSERT(!addr);
|
||||
|
||||
if (PR_GetFileInfo64(filePath, &fileInfo) != PR_SUCCESS)
|
||||
return NS_ERROR_FILE_NOT_FOUND;
|
||||
|
||||
// Check if the file is too big to memmap.
|
||||
if (fileInfo.size > int64_t(UINT32_MAX))
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
auto length = uint32_t(fileInfo.size);
|
||||
|
||||
fd = PR_Open(filePath, flags, flags);
|
||||
if (!fd)
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
|
||||
fileMap = PR_CreateFileMap(fd, fileInfo.size, prot);
|
||||
if (!fileMap)
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
|
||||
addr = PR_MemMap(fileMap, 0, length);
|
||||
if (!addr)
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
} // namespace devtools
|
||||
} // namespace mozilla
|
||||
75
dom/heapsnapshot/AutoMemMap.h
Normal file
75
dom/heapsnapshot/AutoMemMap.h
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* 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_AutoMemMap_h
|
||||
#define mozilla_devtools_AutoMemMap_h
|
||||
|
||||
#include <prio.h>
|
||||
#include "mozilla/GuardObjects.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace devtools {
|
||||
|
||||
// # AutoMemMap
|
||||
//
|
||||
// AutoMemMap is an RAII class to manage mapping a file to memory. It is a
|
||||
// wrapper aorund managing opening and closing a file and calling PR_MemMap and
|
||||
// PR_MemUnmap.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// {
|
||||
// AutoMemMap mm;
|
||||
// if (NS_FAILED(mm.init("/path/to/desired/file"))) {
|
||||
// // Handle the error however you see fit.
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// doStuffWithMappedMemory(mm.address());
|
||||
// }
|
||||
// // The memory is automatically unmapped when the AutoMemMap leaves scope.
|
||||
class MOZ_RAII AutoMemMap
|
||||
{
|
||||
MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER;
|
||||
|
||||
PRFileInfo64 fileInfo;
|
||||
PRFileDesc* fd;
|
||||
PRFileMap* fileMap;
|
||||
void* addr;
|
||||
|
||||
AutoMemMap(const AutoMemMap& aOther) = delete;
|
||||
void operator=(const AutoMemMap& aOther) = delete;
|
||||
|
||||
public:
|
||||
explicit AutoMemMap(MOZ_GUARD_OBJECT_NOTIFIER_ONLY_PARAM)
|
||||
: fd(nullptr)
|
||||
, fileMap(nullptr)
|
||||
, addr(nullptr)
|
||||
{
|
||||
MOZ_GUARD_OBJECT_NOTIFIER_INIT;
|
||||
};
|
||||
~AutoMemMap();
|
||||
|
||||
// Initialize this AutoMemMap.
|
||||
nsresult init(const char* filePath, int flags = PR_RDONLY, int mode = 0,
|
||||
PRFileMapProtect prot = PR_PROT_READONLY);
|
||||
|
||||
// Get the size of the memory mapped file.
|
||||
uint32_t size() const {
|
||||
MOZ_ASSERT(fileInfo.size <= UINT32_MAX,
|
||||
"Should only call size() if init() succeeded.");
|
||||
return uint32_t(fileInfo.size);
|
||||
}
|
||||
|
||||
// Get the mapped memory.
|
||||
void* address() { MOZ_ASSERT(addr); return addr; }
|
||||
const void* address() const { MOZ_ASSERT(addr); return addr; }
|
||||
};
|
||||
|
||||
} // namespace devtools
|
||||
} // namespace mozilla
|
||||
|
||||
#endif // mozilla_devtools_AutoMemMap_h
|
||||
2542
dom/heapsnapshot/CoreDump.pb.cc
Normal file
2542
dom/heapsnapshot/CoreDump.pb.cc
Normal file
File diff suppressed because it is too large
Load diff
1893
dom/heapsnapshot/CoreDump.pb.h
Normal file
1893
dom/heapsnapshot/CoreDump.pb.h
Normal file
File diff suppressed because it is too large
Load diff
143
dom/heapsnapshot/CoreDump.proto
Normal file
143
dom/heapsnapshot/CoreDump.proto
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
/* -*- Mode: protobuf; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
|
||||
* vim: set ts=8 sts=4 et sw=4 tw=99:
|
||||
* 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/. */
|
||||
|
||||
// # Core Dumps
|
||||
//
|
||||
// A core dump is a serialized snapshot of the heap graph. We serialize the heap
|
||||
// as a series of protobuf messages with each message prefixed by its Varint32
|
||||
// byte size so we can delimit individual protobuf messages (protobuf parsers
|
||||
// cannot determine where a message ends on their own).
|
||||
//
|
||||
// The first protobuf message is an instance of the `Metadata` message. All
|
||||
// subsequent messages will be instances of the `Node` message. The first of
|
||||
// these `Node` messages is the root node of the serialized heap graph. Here is
|
||||
// a diagram of our core dump format:
|
||||
//
|
||||
// +-----------------------------------------------------------------------+
|
||||
// | Varint32: The size of following `Metadata` message. |
|
||||
// +-----------------------------------------------------------------------+
|
||||
// | message: The core dump `Metadata` message. |
|
||||
// +-----------------------------------------------------------------------+
|
||||
// | Varint32: The size of the following `Node` message. |
|
||||
// +-----------------------------------------------------------------------+
|
||||
// | message: The first `Node` message. This is the root node. |
|
||||
// +-----------------------------------------------------------------------+
|
||||
// | Varint32: The size of the following `Node` message. |
|
||||
// +-----------------------------------------------------------------------+
|
||||
// | message: A `Node` message. |
|
||||
// +-----------------------------------------------------------------------+
|
||||
// | Varint32: The size of the following `Node` message. |
|
||||
// +-----------------------------------------------------------------------+
|
||||
// | message: A `Node` message. |
|
||||
// +-----------------------------------------------------------------------+
|
||||
// | . |
|
||||
// | . |
|
||||
// | . |
|
||||
// +-----------------------------------------------------------------------+
|
||||
//
|
||||
// Core dumps should always be written with a
|
||||
// `google::protobuf::io::GzipOutputStream` and read from a
|
||||
// `google::protobuf::io::GzipInputStream`.
|
||||
//
|
||||
// Note that all strings are de-duplicated. The first time the N^th unique
|
||||
// string is encountered, the full string is serialized. Subsequent times that
|
||||
// same string is encountered, it is referenced by N. This de-duplication
|
||||
// happens across string properties, not on a per-property basis. For example,
|
||||
// if the same K^th unique string is first used as an Edge::EdgeNameOrRef and
|
||||
// then as a StackFrame::Data::FunctionDisplayNameOrRef, the first will be the
|
||||
// actual string as the functionDisplayName oneof property, and the second will
|
||||
// be a reference to the first as the edgeNameRef oneof property whose value is
|
||||
// K.
|
||||
//
|
||||
// We would ordinarily abstract these de-duplicated strings with messages of
|
||||
// their own, but unfortunately, the protobuf compiler does not have a way to
|
||||
// inline a messsage within another message and the child message must be
|
||||
// referenced by pointer. This leads to extra mallocs that we wish to avoid.
|
||||
|
||||
|
||||
package mozilla.devtools.protobuf;
|
||||
|
||||
// A collection of metadata about this core dump.
|
||||
message Metadata {
|
||||
// Number of microseconds since midnight (00:00:00) 1 January 1970 UTC.
|
||||
optional uint64 timeStamp = 1;
|
||||
}
|
||||
|
||||
// A serialized version of `JS::ubi::StackFrame`. Older parent frame tails are
|
||||
// de-duplicated to cut down on [de]serialization and size costs.
|
||||
message StackFrame {
|
||||
oneof StackFrameType {
|
||||
// This is the first time this stack frame has been serialized, and so
|
||||
// here is all of its data.
|
||||
Data data = 1;
|
||||
// A reference to a stack frame that has already been serialized and has
|
||||
// the given number as its id.
|
||||
uint64 ref = 2;
|
||||
}
|
||||
|
||||
message Data {
|
||||
optional uint64 id = 1;
|
||||
optional StackFrame parent = 2;
|
||||
optional uint32 line = 3;
|
||||
optional uint32 column = 4;
|
||||
|
||||
// De-duplicated two-byte string.
|
||||
oneof SourceOrRef {
|
||||
bytes source = 5;
|
||||
uint64 sourceRef = 6;
|
||||
}
|
||||
|
||||
// De-duplicated two-byte string.
|
||||
oneof FunctionDisplayNameOrRef {
|
||||
bytes functionDisplayName = 7;
|
||||
uint64 functionDisplayNameRef = 8;
|
||||
}
|
||||
|
||||
optional bool isSystem = 9;
|
||||
optional bool isSelfHosted = 10;
|
||||
}
|
||||
}
|
||||
|
||||
// A serialized version of `JS::ubi::Node` and its outgoing edges.
|
||||
message Node {
|
||||
optional uint64 id = 1;
|
||||
|
||||
// De-duplicated two-byte string.
|
||||
oneof TypeNameOrRef {
|
||||
bytes typeName = 2;
|
||||
uint64 typeNameRef = 3;
|
||||
}
|
||||
|
||||
optional uint64 size = 4;
|
||||
repeated Edge edges = 5;
|
||||
optional StackFrame allocationStack = 6;
|
||||
|
||||
// De-duplicated one-byte string.
|
||||
oneof JSObjectClassNameOrRef {
|
||||
bytes jsObjectClassName = 7;
|
||||
uint64 jsObjectClassNameRef = 8;
|
||||
}
|
||||
|
||||
// JS::ubi::CoarseType. Defaults to Other.
|
||||
optional uint32 coarseType = 9 [default = 0];
|
||||
|
||||
// De-duplicated one-byte string.
|
||||
oneof ScriptFilenameOrRef {
|
||||
bytes scriptFilename = 10;
|
||||
uint64 scriptFilenameRef = 11;
|
||||
}
|
||||
}
|
||||
|
||||
// A serialized edge from the heap graph.
|
||||
message Edge {
|
||||
optional uint64 referent = 1;
|
||||
|
||||
// De-duplicated two-byte string.
|
||||
oneof EdgeNameOrRef {
|
||||
bytes name = 2;
|
||||
uint64 nameRef = 3;
|
||||
}
|
||||
}
|
||||
150
dom/heapsnapshot/DeserializedNode.cpp
Normal file
150
dom/heapsnapshot/DeserializedNode.cpp
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
/* -*- 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"
|
||||
#include "mozilla/devtools/HeapSnapshot.h"
|
||||
#include "nsCRTGlue.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace devtools {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
JS::ubi::Node
|
||||
DeserializedNode::getEdgeReferent(const DeserializedEdge& edge)
|
||||
{
|
||||
auto ptr = owner->nodes.lookup(edge.referent);
|
||||
MOZ_ASSERT(ptr);
|
||||
|
||||
// `HashSets` only provide const access to their values, because mutating a
|
||||
// value might change its hash, rendering it unfindable in the set.
|
||||
// Unfortunately, the `ubi::Node` constructor requires a non-const pointer to
|
||||
// its referent. However, the only aspect of a `DeserializedNode` we hash on
|
||||
// is its id, which can't be changed via `ubi::Node`, so this cast can't cause
|
||||
// the trouble `HashSet` is concerned a non-const reference would cause.
|
||||
return JS::ubi::Node(const_cast<DeserializedNode*>(&*ptr));
|
||||
}
|
||||
|
||||
JS::ubi::StackFrame
|
||||
DeserializedStackFrame::getParentStackFrame() const
|
||||
{
|
||||
MOZ_ASSERT(parent.isSome());
|
||||
auto ptr = owner->frames.lookup(parent.ref());
|
||||
MOZ_ASSERT(ptr);
|
||||
// See above comment in DeserializedNode::getEdgeReferent about why this
|
||||
// const_cast is needed and safe.
|
||||
return JS::ubi::StackFrame(const_cast<DeserializedStackFrame*>(&*ptr));
|
||||
}
|
||||
|
||||
} // namespace devtools
|
||||
} // namespace mozilla
|
||||
|
||||
namespace JS {
|
||||
namespace ubi {
|
||||
|
||||
using mozilla::devtools::DeserializedEdge;
|
||||
|
||||
const char16_t Concrete<DeserializedNode>::concreteTypeName[] =
|
||||
u"mozilla::devtools::DeserializedNode";
|
||||
|
||||
const char16_t*
|
||||
Concrete<DeserializedNode>::typeName() const
|
||||
{
|
||||
return get().typeName;
|
||||
}
|
||||
|
||||
Node::Size
|
||||
Concrete<DeserializedNode>::size(mozilla::MallocSizeOf mallocSizeof) const
|
||||
{
|
||||
return get().size;
|
||||
}
|
||||
|
||||
class DeserializedEdgeRange : public EdgeRange
|
||||
{
|
||||
DeserializedNode* node;
|
||||
Edge currentEdge;
|
||||
size_t i;
|
||||
|
||||
void settle() {
|
||||
if (i >= node->edges.length()) {
|
||||
front_ = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
auto& edge = node->edges[i];
|
||||
auto referent = node->getEdgeReferent(edge);
|
||||
currentEdge = mozilla::Move(Edge(edge.name ? NS_strdup(edge.name) : nullptr,
|
||||
referent));
|
||||
front_ = ¤tEdge;
|
||||
}
|
||||
|
||||
public:
|
||||
explicit DeserializedEdgeRange(DeserializedNode& node)
|
||||
: node(&node)
|
||||
, i(0)
|
||||
{
|
||||
settle();
|
||||
}
|
||||
|
||||
void popFront() override
|
||||
{
|
||||
i++;
|
||||
settle();
|
||||
}
|
||||
};
|
||||
|
||||
StackFrame
|
||||
Concrete<DeserializedNode>::allocationStack() const
|
||||
{
|
||||
MOZ_ASSERT(hasAllocationStack());
|
||||
auto id = get().allocationStack.ref();
|
||||
auto ptr = get().owner->frames.lookup(id);
|
||||
MOZ_ASSERT(ptr);
|
||||
// See above comment in DeserializedNode::getEdgeReferent about why this
|
||||
// const_cast is needed and safe.
|
||||
return JS::ubi::StackFrame(const_cast<DeserializedStackFrame*>(&*ptr));
|
||||
}
|
||||
|
||||
|
||||
js::UniquePtr<EdgeRange>
|
||||
Concrete<DeserializedNode>::edges(JSContext* cx, bool) const
|
||||
{
|
||||
js::UniquePtr<DeserializedEdgeRange> range(js_new<DeserializedEdgeRange>(get()));
|
||||
|
||||
if (!range)
|
||||
return nullptr;
|
||||
|
||||
return js::UniquePtr<EdgeRange>(range.release());
|
||||
}
|
||||
|
||||
StackFrame
|
||||
ConcreteStackFrame<DeserializedStackFrame>::parent() const
|
||||
{
|
||||
return get().parent.isNothing() ? StackFrame() : get().getParentStackFrame();
|
||||
}
|
||||
|
||||
bool
|
||||
ConcreteStackFrame<DeserializedStackFrame>::constructSavedFrameStack(
|
||||
JSContext* cx,
|
||||
MutableHandleObject outSavedFrameStack) const
|
||||
{
|
||||
StackFrame f(&get());
|
||||
return ConstructSavedFrameStackSlow(cx, f, outSavedFrameStack);
|
||||
}
|
||||
|
||||
} // namespace ubi
|
||||
} // namespace JS
|
||||
317
dom/heapsnapshot/DeserializedNode.h
Normal file
317
dom/heapsnapshot/DeserializedNode.h
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
/* -*- 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 "js/UbiNode.h"
|
||||
#include "js/UniquePtr.h"
|
||||
#include "mozilla/devtools/CoreDump.pb.h"
|
||||
#include "mozilla/Maybe.h"
|
||||
#include "mozilla/Move.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;
|
||||
using StackFrameId = 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(NodeId referent, const char16_t* edgeName = nullptr)
|
||||
: referent(referent)
|
||||
, name(edgeName)
|
||||
{ }
|
||||
DeserializedEdge(DeserializedEdge&& rhs);
|
||||
DeserializedEdge& operator=(DeserializedEdge&& rhs);
|
||||
|
||||
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<DeserializedEdge>;
|
||||
using UniqueStringPtr = UniquePtr<char16_t[]>;
|
||||
|
||||
NodeId id;
|
||||
JS::ubi::CoarseType coarseType;
|
||||
// A borrowed reference to a string owned by this node's owning HeapSnapshot.
|
||||
const char16_t* typeName;
|
||||
uint64_t size;
|
||||
EdgeVector edges;
|
||||
Maybe<StackFrameId> allocationStack;
|
||||
// A borrowed reference to a string owned by this node's owning HeapSnapshot.
|
||||
const char* jsObjectClassName;
|
||||
// A borrowed reference to a string owned by this node's owning HeapSnapshot.
|
||||
const char* scriptFilename;
|
||||
// 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;
|
||||
|
||||
DeserializedNode(NodeId id,
|
||||
JS::ubi::CoarseType coarseType,
|
||||
const char16_t* typeName,
|
||||
uint64_t size,
|
||||
EdgeVector&& edges,
|
||||
Maybe<StackFrameId> allocationStack,
|
||||
const char* className,
|
||||
const char* filename,
|
||||
HeapSnapshot& owner)
|
||||
: id(id)
|
||||
, coarseType(coarseType)
|
||||
, typeName(typeName)
|
||||
, size(size)
|
||||
, edges(Move(edges))
|
||||
, allocationStack(allocationStack)
|
||||
, jsObjectClassName(className)
|
||||
, scriptFilename(filename)
|
||||
, owner(&owner)
|
||||
{ }
|
||||
virtual ~DeserializedNode() { }
|
||||
|
||||
DeserializedNode(DeserializedNode&& rhs)
|
||||
: id(rhs.id)
|
||||
, coarseType(rhs.coarseType)
|
||||
, typeName(rhs.typeName)
|
||||
, size(rhs.size)
|
||||
, edges(Move(rhs.edges))
|
||||
, allocationStack(rhs.allocationStack)
|
||||
, jsObjectClassName(rhs.jsObjectClassName)
|
||||
, scriptFilename(rhs.scriptFilename)
|
||||
, owner(rhs.owner)
|
||||
{ }
|
||||
|
||||
DeserializedNode& operator=(DeserializedNode&& rhs)
|
||||
{
|
||||
MOZ_ASSERT(&rhs != this);
|
||||
this->~DeserializedNode();
|
||||
new(this) DeserializedNode(Move(rhs));
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Get a borrowed reference to the given edge's referent. This method is
|
||||
// virtual to provide a hook for gmock and gtest.
|
||||
virtual JS::ubi::Node getEdgeReferent(const DeserializedEdge& edge);
|
||||
|
||||
struct HashPolicy;
|
||||
|
||||
protected:
|
||||
// This is only for use with `MockDeserializedNode` in testing.
|
||||
DeserializedNode(NodeId id, const char16_t* typeName, uint64_t size)
|
||||
: id(id)
|
||||
, coarseType(JS::ubi::CoarseType::Other)
|
||||
, typeName(typeName)
|
||||
, size(size)
|
||||
, edges()
|
||||
, allocationStack(Nothing())
|
||||
, jsObjectClassName(nullptr)
|
||||
, scriptFilename(nullptr)
|
||||
, owner(nullptr)
|
||||
{ }
|
||||
|
||||
private:
|
||||
DeserializedNode(const DeserializedNode&) = delete;
|
||||
DeserializedNode& operator=(const DeserializedNode&) = delete;
|
||||
};
|
||||
|
||||
static inline js::HashNumber
|
||||
hashIdDerivedFromPtr(uint64_t id)
|
||||
{
|
||||
// NodeIds and StackFrameIds are always 64 bits, but they are derived from
|
||||
// the original referents' addresses, which could have been either 32 or 64
|
||||
// bits long. As such, NodeId and StackFrameId have little entropy in their
|
||||
// bottom three bits, and may or may not have entropy in their upper 32
|
||||
// bits. This hash should manage both cases well.
|
||||
id >>= 3;
|
||||
return js::HashNumber((id >> 32) ^ id);
|
||||
}
|
||||
|
||||
struct DeserializedNode::HashPolicy
|
||||
{
|
||||
using Lookup = NodeId;
|
||||
|
||||
static js::HashNumber hash(const Lookup& lookup) {
|
||||
return hashIdDerivedFromPtr(lookup);
|
||||
}
|
||||
|
||||
static bool match(const DeserializedNode& existing, const Lookup& lookup) {
|
||||
return existing.id == lookup;
|
||||
}
|
||||
};
|
||||
|
||||
// A `DeserializedStackFrame` is a stack frame referred to by a thing in the
|
||||
// heap graph that we deserialized from a core dump.
|
||||
struct DeserializedStackFrame {
|
||||
StackFrameId id;
|
||||
Maybe<StackFrameId> parent;
|
||||
uint32_t line;
|
||||
uint32_t column;
|
||||
// Borrowed references to strings owned by this DeserializedStackFrame's
|
||||
// owning HeapSnapshot.
|
||||
const char16_t* source;
|
||||
const char16_t* functionDisplayName;
|
||||
bool isSystem;
|
||||
bool isSelfHosted;
|
||||
// A weak pointer to this frame's owning `HeapSnapshot`. Safe without
|
||||
// AddRef'ing because this frame's lifetime is equal to that of its owner.
|
||||
HeapSnapshot* owner;
|
||||
|
||||
explicit DeserializedStackFrame(StackFrameId id,
|
||||
const Maybe<StackFrameId>& parent,
|
||||
uint32_t line,
|
||||
uint32_t column,
|
||||
const char16_t* source,
|
||||
const char16_t* functionDisplayName,
|
||||
bool isSystem,
|
||||
bool isSelfHosted,
|
||||
HeapSnapshot& owner)
|
||||
: id(id)
|
||||
, parent(parent)
|
||||
, line(line)
|
||||
, column(column)
|
||||
, source(source)
|
||||
, functionDisplayName(functionDisplayName)
|
||||
, isSystem(isSystem)
|
||||
, isSelfHosted(isSelfHosted)
|
||||
, owner(&owner)
|
||||
{
|
||||
MOZ_ASSERT(source);
|
||||
}
|
||||
|
||||
JS::ubi::StackFrame getParentStackFrame() const;
|
||||
|
||||
struct HashPolicy;
|
||||
|
||||
protected:
|
||||
// This is exposed only for MockDeserializedStackFrame in the gtests.
|
||||
explicit DeserializedStackFrame()
|
||||
: id(0)
|
||||
, parent(Nothing())
|
||||
, line(0)
|
||||
, column(0)
|
||||
, source(nullptr)
|
||||
, functionDisplayName(nullptr)
|
||||
, isSystem(false)
|
||||
, isSelfHosted(false)
|
||||
, owner(nullptr)
|
||||
{ };
|
||||
};
|
||||
|
||||
struct DeserializedStackFrame::HashPolicy {
|
||||
using Lookup = StackFrameId;
|
||||
|
||||
static js::HashNumber hash(const Lookup& lookup) {
|
||||
return hashIdDerivedFromPtr(lookup);
|
||||
}
|
||||
|
||||
static bool match(const DeserializedStackFrame& existing, const Lookup& lookup) {
|
||||
return existing.id == lookup;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace devtools
|
||||
} // namespace mozilla
|
||||
|
||||
namespace JS {
|
||||
namespace ubi {
|
||||
|
||||
using mozilla::devtools::DeserializedNode;
|
||||
using mozilla::devtools::DeserializedStackFrame;
|
||||
|
||||
template<>
|
||||
class Concrete<DeserializedNode> : public Base
|
||||
{
|
||||
protected:
|
||||
explicit Concrete(DeserializedNode* ptr) : Base(ptr) { }
|
||||
DeserializedNode& get() const {
|
||||
return *static_cast<DeserializedNode*>(ptr);
|
||||
}
|
||||
|
||||
public:
|
||||
static void construct(void* storage, DeserializedNode* ptr) {
|
||||
new (storage) Concrete(ptr);
|
||||
}
|
||||
|
||||
CoarseType coarseType() const final { return get().coarseType; }
|
||||
Id identifier() const override { return get().id; }
|
||||
bool isLive() const override { return false; }
|
||||
const char16_t* typeName() const override;
|
||||
Node::Size size(mozilla::MallocSizeOf mallocSizeof) const override;
|
||||
const char* jsObjectClassName() const override { return get().jsObjectClassName; }
|
||||
const char* scriptFilename() const final { return get().scriptFilename; }
|
||||
|
||||
bool hasAllocationStack() const override { return get().allocationStack.isSome(); }
|
||||
StackFrame allocationStack() const override;
|
||||
|
||||
// We ignore the `bool wantNames` parameter because we can't control whether
|
||||
// the core dump was serialized with edge names or not.
|
||||
js::UniquePtr<EdgeRange> edges(JSContext* cx, bool) const override;
|
||||
|
||||
static const char16_t concreteTypeName[];
|
||||
};
|
||||
|
||||
template<>
|
||||
class ConcreteStackFrame<DeserializedStackFrame> : public BaseStackFrame
|
||||
{
|
||||
protected:
|
||||
explicit ConcreteStackFrame(DeserializedStackFrame* ptr)
|
||||
: BaseStackFrame(ptr)
|
||||
{ }
|
||||
|
||||
DeserializedStackFrame& get() const {
|
||||
return *static_cast<DeserializedStackFrame*>(ptr);
|
||||
}
|
||||
|
||||
public:
|
||||
static void construct(void* storage, DeserializedStackFrame* ptr) {
|
||||
new (storage) ConcreteStackFrame(ptr);
|
||||
}
|
||||
|
||||
uint64_t identifier() const override { return get().id; }
|
||||
uint32_t line() const override { return get().line; }
|
||||
uint32_t column() const override { return get().column; }
|
||||
bool isSystem() const override { return get().isSystem; }
|
||||
bool isSelfHosted(JSContext* cx) const override { return get().isSelfHosted; }
|
||||
void trace(JSTracer* trc) override { }
|
||||
AtomOrTwoByteChars source() const override {
|
||||
return AtomOrTwoByteChars(get().source);
|
||||
}
|
||||
AtomOrTwoByteChars functionDisplayName() const override {
|
||||
return AtomOrTwoByteChars(get().functionDisplayName);
|
||||
}
|
||||
|
||||
StackFrame parent() const override;
|
||||
bool constructSavedFrameStack(JSContext* cx,
|
||||
MutableHandleObject outSavedFrameStack)
|
||||
const override;
|
||||
};
|
||||
|
||||
} // namespace ubi
|
||||
} // namespace JS
|
||||
|
||||
#endif // mozilla_devtools_DeserializedNode__
|
||||
140
dom/heapsnapshot/DominatorTree.cpp
Normal file
140
dom/heapsnapshot/DominatorTree.cpp
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
/* -*- 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/DominatorTree.h"
|
||||
#include "mozilla/dom/DominatorTreeBinding.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace devtools {
|
||||
|
||||
dom::Nullable<uint64_t>
|
||||
DominatorTree::GetRetainedSize(uint64_t aNodeId, ErrorResult& aRv)
|
||||
{
|
||||
JS::ubi::Node::Id id(aNodeId);
|
||||
auto node = mHeapSnapshot->getNodeById(id);
|
||||
if (node.isNothing())
|
||||
return dom::Nullable<uint64_t>();
|
||||
|
||||
auto mallocSizeOf = GetCurrentThreadDebuggerMallocSizeOf();
|
||||
JS::ubi::Node::Size size = 0;
|
||||
if (!mDominatorTree.getRetainedSize(*node, mallocSizeOf, size)) {
|
||||
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
|
||||
return dom::Nullable<uint64_t>();
|
||||
}
|
||||
|
||||
MOZ_ASSERT(size != 0,
|
||||
"The node should not have been unknown since we got it from the heap snapshot.");
|
||||
return dom::Nullable<uint64_t>(size);
|
||||
}
|
||||
|
||||
struct NodeAndRetainedSize
|
||||
{
|
||||
JS::ubi::Node mNode;
|
||||
JS::ubi::Node::Size mSize;
|
||||
|
||||
NodeAndRetainedSize(const JS::ubi::Node& aNode, JS::ubi::Node::Size aSize)
|
||||
: mNode(aNode)
|
||||
, mSize(aSize)
|
||||
{ }
|
||||
|
||||
struct Comparator
|
||||
{
|
||||
static bool
|
||||
Equals(const NodeAndRetainedSize& aLhs, const NodeAndRetainedSize& aRhs)
|
||||
{
|
||||
return aLhs.mSize == aRhs.mSize;
|
||||
}
|
||||
|
||||
static bool
|
||||
LessThan(const NodeAndRetainedSize& aLhs, const NodeAndRetainedSize& aRhs)
|
||||
{
|
||||
// Use > because we want to sort from greatest to least retained size.
|
||||
return aLhs.mSize > aRhs.mSize;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
void
|
||||
DominatorTree::GetImmediatelyDominated(uint64_t aNodeId,
|
||||
dom::Nullable<nsTArray<uint64_t>>& aOutResult,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
MOZ_ASSERT(aOutResult.IsNull());
|
||||
|
||||
JS::ubi::Node::Id id(aNodeId);
|
||||
Maybe<JS::ubi::Node> node = mHeapSnapshot->getNodeById(id);
|
||||
if (node.isNothing())
|
||||
return;
|
||||
|
||||
// Get all immediately dominated nodes and their retained sizes.
|
||||
MallocSizeOf mallocSizeOf = GetCurrentThreadDebuggerMallocSizeOf();
|
||||
Maybe<JS::ubi::DominatorTree::DominatedSetRange> range = mDominatorTree.getDominatedSet(*node);
|
||||
MOZ_ASSERT(range.isSome(), "The node should be known, since we got it from the heap snapshot.");
|
||||
size_t length = range->length();
|
||||
nsTArray<NodeAndRetainedSize> dominatedNodes(length);
|
||||
for (const JS::ubi::Node& dominatedNode : *range) {
|
||||
JS::ubi::Node::Size retainedSize = 0;
|
||||
if (NS_WARN_IF(!mDominatorTree.getRetainedSize(dominatedNode, mallocSizeOf, retainedSize))) {
|
||||
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
|
||||
return;
|
||||
}
|
||||
MOZ_ASSERT(retainedSize != 0,
|
||||
"retainedSize should not be zero since we know the node is in the dominator tree.");
|
||||
|
||||
dominatedNodes.AppendElement(NodeAndRetainedSize(dominatedNode, retainedSize));
|
||||
}
|
||||
|
||||
// Sort them by retained size.
|
||||
NodeAndRetainedSize::Comparator comparator;
|
||||
dominatedNodes.Sort(comparator);
|
||||
|
||||
// Fill the result with the nodes' ids.
|
||||
JS::ubi::Node root = mDominatorTree.root();
|
||||
aOutResult.SetValue(nsTArray<uint64_t>(length));
|
||||
for (const NodeAndRetainedSize& entry : dominatedNodes) {
|
||||
// The root dominates itself, but we don't want to expose that to JS.
|
||||
if (entry.mNode == root)
|
||||
continue;
|
||||
|
||||
aOutResult.Value().AppendElement(entry.mNode.identifier());
|
||||
}
|
||||
}
|
||||
|
||||
dom::Nullable<uint64_t>
|
||||
DominatorTree::GetImmediateDominator(uint64_t aNodeId) const
|
||||
{
|
||||
JS::ubi::Node::Id id(aNodeId);
|
||||
Maybe<JS::ubi::Node> node = mHeapSnapshot->getNodeById(id);
|
||||
if (node.isNothing())
|
||||
return dom::Nullable<uint64_t>();
|
||||
|
||||
JS::ubi::Node dominator = mDominatorTree.getImmediateDominator(*node);
|
||||
if (!dominator || dominator == *node)
|
||||
return dom::Nullable<uint64_t>();
|
||||
|
||||
return dom::Nullable<uint64_t>(dominator.identifier());
|
||||
}
|
||||
|
||||
|
||||
/*** Cycle Collection Boilerplate *****************************************************************/
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DominatorTree, mParent, mHeapSnapshot)
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTING_ADDREF(DominatorTree)
|
||||
NS_IMPL_CYCLE_COLLECTING_RELEASE(DominatorTree)
|
||||
|
||||
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DominatorTree)
|
||||
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
|
||||
NS_INTERFACE_MAP_ENTRY(nsISupports)
|
||||
NS_INTERFACE_MAP_END
|
||||
|
||||
/* virtual */ JSObject*
|
||||
DominatorTree::WrapObject(JSContext* aCx, JS::HandleObject aGivenProto)
|
||||
{
|
||||
return dom::DominatorTreeBinding::Wrap(aCx, this, aGivenProto);
|
||||
}
|
||||
|
||||
} // namespace devtools
|
||||
} // namespace mozilla
|
||||
67
dom/heapsnapshot/DominatorTree.h
Normal file
67
dom/heapsnapshot/DominatorTree.h
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/* -*- 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_DominatorTree__
|
||||
#define mozilla_devtools_DominatorTree__
|
||||
|
||||
#include "mozilla/devtools/HeapSnapshot.h"
|
||||
#include "mozilla/dom/BindingDeclarations.h"
|
||||
#include "mozilla/ErrorResult.h"
|
||||
#include "mozilla/RefCounted.h"
|
||||
#include "js/UbiNodeDominatorTree.h"
|
||||
#include "nsWrapperCache.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace devtools {
|
||||
|
||||
class DominatorTree final : public nsISupports
|
||||
, public nsWrapperCache
|
||||
{
|
||||
protected:
|
||||
nsCOMPtr<nsISupports> mParent;
|
||||
|
||||
virtual ~DominatorTree() { }
|
||||
|
||||
private:
|
||||
JS::ubi::DominatorTree mDominatorTree;
|
||||
RefPtr<HeapSnapshot> mHeapSnapshot;
|
||||
|
||||
public:
|
||||
explicit DominatorTree(JS::ubi::DominatorTree&& aDominatorTree, HeapSnapshot* aHeapSnapshot,
|
||||
nsISupports* aParent)
|
||||
: mParent(aParent)
|
||||
, mDominatorTree(Move(aDominatorTree))
|
||||
, mHeapSnapshot(aHeapSnapshot)
|
||||
{
|
||||
MOZ_ASSERT(aParent);
|
||||
MOZ_ASSERT(aHeapSnapshot);
|
||||
};
|
||||
|
||||
NS_DECL_CYCLE_COLLECTING_ISUPPORTS;
|
||||
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(DominatorTree);
|
||||
|
||||
nsISupports* GetParentObject() const { return mParent; }
|
||||
|
||||
virtual JSObject* WrapObject(JSContext* aCx,
|
||||
JS::Handle<JSObject*> aGivenProto) override;
|
||||
|
||||
// readonly attribute NodeId root
|
||||
uint64_t Root() const { return mDominatorTree.root().identifier(); }
|
||||
|
||||
// [Throws] NodeSize getRetainedSize(NodeId node)
|
||||
dom::Nullable<uint64_t> GetRetainedSize(uint64_t aNodeId, ErrorResult& aRv);
|
||||
|
||||
// [Throws] sequence<NodeId>? getImmediatelyDominated(NodeId node);
|
||||
void GetImmediatelyDominated(uint64_t aNodeId, dom::Nullable<nsTArray<uint64_t>>& aOutDominated,
|
||||
ErrorResult& aRv);
|
||||
|
||||
// NodeId? getImmediateDominator(NodeId node);
|
||||
dom::Nullable<uint64_t> GetImmediateDominator(uint64_t aNodeId) const;
|
||||
};
|
||||
|
||||
} // namespace devtools
|
||||
} // namespace mozilla
|
||||
|
||||
#endif // mozilla_devtools_DominatorTree__
|
||||
86
dom/heapsnapshot/FileDescriptorOutputStream.cpp
Normal file
86
dom/heapsnapshot/FileDescriptorOutputStream.cpp
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/* -*- 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/FileDescriptorOutputStream.h"
|
||||
#include "private/pprio.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace devtools {
|
||||
|
||||
/* static */ already_AddRefed<FileDescriptorOutputStream>
|
||||
FileDescriptorOutputStream::Create(const ipc::FileDescriptor& fileDescriptor)
|
||||
{
|
||||
if (NS_WARN_IF(!fileDescriptor.IsValid()))
|
||||
return nullptr;
|
||||
|
||||
auto rawFD = fileDescriptor.ClonePlatformHandle();
|
||||
PRFileDesc* prfd = PR_ImportFile(PROsfd(rawFD.release()));
|
||||
if (NS_WARN_IF(!prfd))
|
||||
return nullptr;
|
||||
|
||||
RefPtr<FileDescriptorOutputStream> stream = new FileDescriptorOutputStream(prfd);
|
||||
return stream.forget();
|
||||
}
|
||||
|
||||
NS_IMPL_ISUPPORTS(FileDescriptorOutputStream, nsIOutputStream);
|
||||
|
||||
NS_IMETHODIMP
|
||||
FileDescriptorOutputStream::Close()
|
||||
{
|
||||
// Repeatedly closing is idempotent.
|
||||
if (!fd)
|
||||
return NS_OK;
|
||||
|
||||
if (PR_Close(fd) != PR_SUCCESS)
|
||||
return NS_ERROR_FAILURE;
|
||||
fd = nullptr;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
FileDescriptorOutputStream::Write(const char* buf, uint32_t count, uint32_t* retval)
|
||||
{
|
||||
if (NS_WARN_IF(!fd))
|
||||
return NS_ERROR_FAILURE;
|
||||
|
||||
auto written = PR_Write(fd, buf, count);
|
||||
if (written < 0)
|
||||
return NS_ERROR_FAILURE;
|
||||
*retval = written;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
FileDescriptorOutputStream::Flush()
|
||||
{
|
||||
if (NS_WARN_IF(!fd))
|
||||
return NS_ERROR_FAILURE;
|
||||
|
||||
return PR_Sync(fd) == PR_SUCCESS ? NS_OK : NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
FileDescriptorOutputStream::WriteFrom(nsIInputStream* fromStream, uint32_t count,
|
||||
uint32_t* retval)
|
||||
{
|
||||
return NS_ERROR_NOT_IMPLEMENTED;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
FileDescriptorOutputStream::WriteSegments(nsReadSegmentFun reader, void* closure,
|
||||
uint32_t count, uint32_t* retval)
|
||||
{
|
||||
return NS_ERROR_NOT_IMPLEMENTED;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
FileDescriptorOutputStream::IsNonBlocking(bool* retval)
|
||||
{
|
||||
*retval = false;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
} // namespace devtools
|
||||
} // namespace mozilla
|
||||
41
dom/heapsnapshot/FileDescriptorOutputStream.h
Normal file
41
dom/heapsnapshot/FileDescriptorOutputStream.h
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/* -*- 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_FileDescriptorOutputStream_h
|
||||
#define mozilla_devtools_FileDescriptorOutputStream_h
|
||||
|
||||
#include <prio.h>
|
||||
|
||||
#include "mozilla/AlreadyAddRefed.h"
|
||||
#include "mozilla/ipc/FileDescriptor.h"
|
||||
#include "nsIOutputStream.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace devtools {
|
||||
|
||||
class FileDescriptorOutputStream final : public nsIOutputStream
|
||||
{
|
||||
private:
|
||||
PRFileDesc* fd;
|
||||
|
||||
public:
|
||||
static already_AddRefed<FileDescriptorOutputStream>
|
||||
Create(const ipc::FileDescriptor& fileDescriptor);
|
||||
|
||||
private:
|
||||
explicit FileDescriptorOutputStream(PRFileDesc* prfd)
|
||||
: fd(prfd)
|
||||
{ }
|
||||
|
||||
virtual ~FileDescriptorOutputStream() { }
|
||||
|
||||
NS_DECL_THREADSAFE_ISUPPORTS
|
||||
NS_DECL_NSIOUTPUTSTREAM
|
||||
};
|
||||
|
||||
} // namespace devtools
|
||||
} // namespace mozilla
|
||||
|
||||
#endif // mozilla_devtools_FileDescriptorOutputStream_h
|
||||
1619
dom/heapsnapshot/HeapSnapshot.cpp
Normal file
1619
dom/heapsnapshot/HeapSnapshot.cpp
Normal file
File diff suppressed because it is too large
Load diff
224
dom/heapsnapshot/HeapSnapshot.h
Normal file
224
dom/heapsnapshot/HeapSnapshot.h
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
/* -*- 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/dom/Nullable.h"
|
||||
#include "mozilla/HashFunctions.h"
|
||||
#include "mozilla/Maybe.h"
|
||||
#include "mozilla/RefCounted.h"
|
||||
#include "mozilla/RefPtr.h"
|
||||
#include "mozilla/TimeStamp.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 {
|
||||
|
||||
class DominatorTree;
|
||||
|
||||
struct NSFreePolicy {
|
||||
void operator()(void* ptr) {
|
||||
NS_Free(ptr);
|
||||
}
|
||||
};
|
||||
|
||||
using UniqueTwoByteString = UniquePtr<char16_t[], NSFreePolicy>;
|
||||
using UniqueOneByteString = UniquePtr<char[], NSFreePolicy>;
|
||||
|
||||
class HeapSnapshot final : public nsISupports
|
||||
, public nsWrapperCache
|
||||
{
|
||||
friend struct DeserializedNode;
|
||||
friend struct DeserializedEdge;
|
||||
friend struct DeserializedStackFrame;
|
||||
friend class JS::ubi::Concrete<JS::ubi::DeserializedNode>;
|
||||
|
||||
explicit HeapSnapshot(JSContext* cx, nsISupports* aParent)
|
||||
: timestamp(Nothing())
|
||||
, rootId(0)
|
||||
, nodes(cx)
|
||||
, frames(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(JSContext* cx, const uint8_t* buffer, uint32_t size);
|
||||
|
||||
using NodeIdSet = js::HashSet<NodeId>;
|
||||
|
||||
// Save the given `protobuf::Node` message in this `HeapSnapshot` as a
|
||||
// `DeserializedNode`.
|
||||
bool saveNode(const protobuf::Node& node, NodeIdSet& edgeReferents);
|
||||
|
||||
// Save the given `protobuf::StackFrame` message in this `HeapSnapshot` as a
|
||||
// `DeserializedStackFrame`. The saved stack frame's id is returned via the
|
||||
// out parameter.
|
||||
bool saveStackFrame(const protobuf::StackFrame& frame,
|
||||
StackFrameId& outFrameId);
|
||||
|
||||
public:
|
||||
// The maximum number of stack frames that we will serialize into a core
|
||||
// dump. This helps prevent over-recursion in the protobuf library when
|
||||
// deserializing stacks.
|
||||
static const size_t MAX_STACK_DEPTH = 60;
|
||||
|
||||
private:
|
||||
// If present, a timestamp in the same units that `PR_Now` gives.
|
||||
Maybe<uint64_t> 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 NodeSet = js::HashSet<DeserializedNode, DeserializedNode::HashPolicy>;
|
||||
NodeSet nodes;
|
||||
|
||||
// The set of stack frames in this deserialized heap graph, keyed by id.
|
||||
using FrameSet = js::HashSet<DeserializedStackFrame,
|
||||
DeserializedStackFrame::HashPolicy>;
|
||||
FrameSet frames;
|
||||
|
||||
Vector<UniqueTwoByteString> internedTwoByteStrings;
|
||||
Vector<UniqueOneByteString> internedOneByteStrings;
|
||||
|
||||
using StringOrRef = Variant<const std::string*, uint64_t>;
|
||||
|
||||
template<typename CharT,
|
||||
typename InternedStringSet>
|
||||
const CharT* getOrInternString(InternedStringSet& internedStrings,
|
||||
Maybe<StringOrRef>& maybeStrOrRef);
|
||||
|
||||
protected:
|
||||
nsCOMPtr<nsISupports> 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<HeapSnapshot> Create(JSContext* cx,
|
||||
dom::GlobalObject& global,
|
||||
const uint8_t* buffer,
|
||||
uint32_t size,
|
||||
ErrorResult& rv);
|
||||
|
||||
// Creates the `$TEMP_DIR/XXXXXX-XXX.fxsnapshot` core dump file that heap
|
||||
// snapshots are serialized into.
|
||||
static already_AddRefed<nsIFile> CreateUniqueCoreDumpFile(ErrorResult& rv,
|
||||
const TimeStamp& now,
|
||||
nsAString& outFilePath);
|
||||
|
||||
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<JSObject*> aGivenProto) override;
|
||||
|
||||
const char16_t* borrowUniqueString(const char16_t* duplicateString,
|
||||
size_t length);
|
||||
|
||||
// Get the root node of this heap snapshot's graph.
|
||||
JS::ubi::Node getRoot() {
|
||||
MOZ_ASSERT(nodes.initialized());
|
||||
auto p = nodes.lookup(rootId);
|
||||
MOZ_ASSERT(p);
|
||||
const DeserializedNode& node = *p;
|
||||
return JS::ubi::Node(const_cast<DeserializedNode*>(&node));
|
||||
}
|
||||
|
||||
Maybe<JS::ubi::Node> getNodeById(JS::ubi::Node::Id nodeId) {
|
||||
auto p = nodes.lookup(nodeId);
|
||||
if (!p)
|
||||
return Nothing();
|
||||
return Some(JS::ubi::Node(const_cast<DeserializedNode*>(&*p)));
|
||||
}
|
||||
|
||||
void TakeCensus(JSContext* cx, JS::HandleObject options,
|
||||
JS::MutableHandleValue rval, ErrorResult& rv);
|
||||
|
||||
void DescribeNode(JSContext* cx, JS::HandleObject breakdown, uint64_t nodeId,
|
||||
JS::MutableHandleValue rval, ErrorResult& rv);
|
||||
|
||||
already_AddRefed<DominatorTree> ComputeDominatorTree(ErrorResult& rv);
|
||||
|
||||
void ComputeShortestPaths(JSContext*cx, uint64_t start,
|
||||
const dom::Sequence<uint64_t>& targets,
|
||||
uint64_t maxNumPaths,
|
||||
JS::MutableHandleObject results,
|
||||
ErrorResult& rv);
|
||||
|
||||
dom::Nullable<uint64_t> GetCreationTime() {
|
||||
static const uint64_t maxTime = uint64_t(1) << 53;
|
||||
if (timestamp.isSome() && timestamp.ref() <= maxTime) {
|
||||
return dom::Nullable<uint64_t>(timestamp.ref());
|
||||
}
|
||||
|
||||
return dom::Nullable<uint64_t>();
|
||||
}
|
||||
};
|
||||
|
||||
// A `CoreDumpWriter` is given the data we wish to save in a core dump and
|
||||
// serializes it to disk, or memory, or a socket, etc.
|
||||
class CoreDumpWriter
|
||||
{
|
||||
public:
|
||||
virtual ~CoreDumpWriter() { };
|
||||
|
||||
// Write the given bits of metadata we would like to associate with this core
|
||||
// dump.
|
||||
virtual bool writeMetadata(uint64_t timestamp) = 0;
|
||||
|
||||
enum EdgePolicy : bool {
|
||||
INCLUDE_EDGES = true,
|
||||
EXCLUDE_EDGES = false
|
||||
};
|
||||
|
||||
// Write the given `JS::ubi::Node` to the core dump. The given `EdgePolicy`
|
||||
// dictates whether its outgoing edges should also be written to the core
|
||||
// dump, or excluded.
|
||||
virtual bool writeNode(const JS::ubi::Node& node,
|
||||
EdgePolicy includeEdges) = 0;
|
||||
};
|
||||
|
||||
// Serialize the heap graph as seen from `node` with the given `CoreDumpWriter`.
|
||||
// If `wantNames` is true, capture edge names. If `zones` is non-null, only
|
||||
// capture the sub-graph within the zone set, otherwise capture the whole heap
|
||||
// graph. Returns false on failure.
|
||||
bool
|
||||
WriteHeapGraph(JSContext* cx,
|
||||
const JS::ubi::Node& node,
|
||||
CoreDumpWriter& writer,
|
||||
bool wantNames,
|
||||
JS::CompartmentSet* compartments,
|
||||
JS::AutoCheckCannotGC& noGC);
|
||||
|
||||
// Get the mozilla::MallocSizeOf for the current thread's JSRuntime.
|
||||
MallocSizeOf GetCurrentThreadDebuggerMallocSizeOf();
|
||||
|
||||
} // namespace devtools
|
||||
} // namespace mozilla
|
||||
|
||||
#endif // mozilla_devtools_HeapSnapshot__
|
||||
32
dom/heapsnapshot/HeapSnapshotTempFileHelperChild.h
Normal file
32
dom/heapsnapshot/HeapSnapshotTempFileHelperChild.h
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
|
||||
/* vim: set sw=4 ts=8 et tw=80 : */
|
||||
/* 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_HeapSnapshotTempFileHelperChild_h
|
||||
#define mozilla_devtools_HeapSnapshotTempFileHelperChild_h
|
||||
|
||||
#include "mozilla/devtools/PHeapSnapshotTempFileHelperChild.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace devtools {
|
||||
|
||||
class HeapSnapshotTempFileHelperChild : public PHeapSnapshotTempFileHelperChild
|
||||
{
|
||||
explicit HeapSnapshotTempFileHelperChild() { }
|
||||
|
||||
public:
|
||||
static inline PHeapSnapshotTempFileHelperChild* Create();
|
||||
};
|
||||
|
||||
/* static */ inline PHeapSnapshotTempFileHelperChild*
|
||||
HeapSnapshotTempFileHelperChild::Create()
|
||||
{
|
||||
return new HeapSnapshotTempFileHelperChild();
|
||||
}
|
||||
|
||||
} // namespace devtools
|
||||
} // namespace mozilla
|
||||
|
||||
#endif // mozilla_devtools_HeapSnapshotTempFileHelperChild_h
|
||||
53
dom/heapsnapshot/HeapSnapshotTempFileHelperParent.cpp
Normal file
53
dom/heapsnapshot/HeapSnapshotTempFileHelperParent.cpp
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
|
||||
/* vim: set sw=4 ts=8 et tw=80 : */
|
||||
/* 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/HeapSnapshot.h"
|
||||
#include "mozilla/devtools/HeapSnapshotTempFileHelperParent.h"
|
||||
#include "mozilla/ErrorResult.h"
|
||||
#include "private/pprio.h"
|
||||
|
||||
#include "nsIFile.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace devtools {
|
||||
|
||||
using ipc::FileDescriptor;
|
||||
|
||||
static bool
|
||||
openFileFailure(ErrorResult& rv,
|
||||
OpenHeapSnapshotTempFileResponse* outResponse)
|
||||
{
|
||||
*outResponse = rv.StealNSResult();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
HeapSnapshotTempFileHelperParent::RecvOpenHeapSnapshotTempFile(
|
||||
OpenHeapSnapshotTempFileResponse* outResponse)
|
||||
{
|
||||
auto start = TimeStamp::Now();
|
||||
ErrorResult rv;
|
||||
nsAutoString filePath;
|
||||
nsCOMPtr<nsIFile> file = HeapSnapshot::CreateUniqueCoreDumpFile(rv,
|
||||
start,
|
||||
filePath);
|
||||
if (NS_WARN_IF(rv.Failed()))
|
||||
return openFileFailure(rv, outResponse);
|
||||
|
||||
PRFileDesc* prfd;
|
||||
rv = file->OpenNSPRFileDesc(PR_WRONLY, 0, &prfd);
|
||||
if (NS_WARN_IF(rv.Failed()))
|
||||
return openFileFailure(rv, outResponse);
|
||||
|
||||
FileDescriptor::PlatformHandleType handle =
|
||||
FileDescriptor::PlatformHandleType(PR_FileDesc2NativeHandle(prfd));
|
||||
FileDescriptor fd(handle);
|
||||
*outResponse = OpenedFile(filePath, fd);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace devtools
|
||||
} // namespace mozilla
|
||||
35
dom/heapsnapshot/HeapSnapshotTempFileHelperParent.h
Normal file
35
dom/heapsnapshot/HeapSnapshotTempFileHelperParent.h
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
|
||||
/* vim: set sw=4 ts=8 et tw=80 : */
|
||||
/* 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_HeapSnapshotTempFileHelperParent_h
|
||||
#define mozilla_devtools_HeapSnapshotTempFileHelperParent_h
|
||||
|
||||
#include "mozilla/devtools/PHeapSnapshotTempFileHelperParent.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace devtools {
|
||||
|
||||
class HeapSnapshotTempFileHelperParent : public PHeapSnapshotTempFileHelperParent
|
||||
{
|
||||
explicit HeapSnapshotTempFileHelperParent() { }
|
||||
void ActorDestroy(ActorDestroyReason why) override { }
|
||||
bool RecvOpenHeapSnapshotTempFile(OpenHeapSnapshotTempFileResponse* outResponse)
|
||||
override;
|
||||
|
||||
public:
|
||||
static inline PHeapSnapshotTempFileHelperParent* Create();
|
||||
};
|
||||
|
||||
/* static */ inline PHeapSnapshotTempFileHelperParent*
|
||||
HeapSnapshotTempFileHelperParent::Create()
|
||||
{
|
||||
return new HeapSnapshotTempFileHelperParent();
|
||||
}
|
||||
|
||||
} // namespace devtools
|
||||
} // namespace mozilla
|
||||
|
||||
#endif // mozilla_devtools_HeapSnapshotTempFileHelperParent_h
|
||||
35
dom/heapsnapshot/PHeapSnapshotTempFileHelper.ipdl
Normal file
35
dom/heapsnapshot/PHeapSnapshotTempFileHelper.ipdl
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
|
||||
* vim: set ts=8 sts=4 et sw=4 tw=99:
|
||||
* 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 protocol PContent;
|
||||
|
||||
namespace mozilla {
|
||||
namespace devtools {
|
||||
|
||||
struct OpenedFile
|
||||
{
|
||||
nsString path;
|
||||
FileDescriptor descriptor;
|
||||
};
|
||||
|
||||
union OpenHeapSnapshotTempFileResponse
|
||||
{
|
||||
nsresult;
|
||||
OpenedFile;
|
||||
};
|
||||
|
||||
sync protocol PHeapSnapshotTempFileHelper
|
||||
{
|
||||
manager PContent;
|
||||
|
||||
parent:
|
||||
sync OpenHeapSnapshotTempFile() returns (OpenHeapSnapshotTempFileResponse response);
|
||||
|
||||
async __delete__();
|
||||
};
|
||||
|
||||
} // namespace devtools
|
||||
} // namespace mozilla
|
||||
100
dom/heapsnapshot/ZeroCopyNSIOutputStream.cpp
Normal file
100
dom/heapsnapshot/ZeroCopyNSIOutputStream.cpp
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
/* -*- 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/ZeroCopyNSIOutputStream.h"
|
||||
|
||||
#include "mozilla/DebugOnly.h"
|
||||
#include "mozilla/Unused.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace devtools {
|
||||
|
||||
ZeroCopyNSIOutputStream::ZeroCopyNSIOutputStream(nsCOMPtr<nsIOutputStream>& out)
|
||||
: out(out)
|
||||
, result_(NS_OK)
|
||||
, amountUsed(0)
|
||||
, writtenCount(0)
|
||||
{
|
||||
DebugOnly<bool> nonBlocking = false;
|
||||
MOZ_ASSERT(out->IsNonBlocking(&nonBlocking) == NS_OK);
|
||||
MOZ_ASSERT(!nonBlocking);
|
||||
}
|
||||
|
||||
ZeroCopyNSIOutputStream::~ZeroCopyNSIOutputStream()
|
||||
{
|
||||
if (!failed())
|
||||
Unused << NS_WARN_IF(NS_FAILED(writeBuffer()));
|
||||
}
|
||||
|
||||
nsresult
|
||||
ZeroCopyNSIOutputStream::writeBuffer()
|
||||
{
|
||||
if (failed())
|
||||
return result_;
|
||||
|
||||
if (amountUsed == 0)
|
||||
return NS_OK;
|
||||
|
||||
int32_t amountWritten = 0;
|
||||
while (amountWritten < amountUsed) {
|
||||
uint32_t justWritten = 0;
|
||||
|
||||
result_ = out->Write(buffer + amountWritten,
|
||||
amountUsed - amountWritten,
|
||||
&justWritten);
|
||||
if (NS_WARN_IF(NS_FAILED(result_)))
|
||||
return result_;
|
||||
|
||||
amountWritten += justWritten;
|
||||
}
|
||||
|
||||
writtenCount += amountUsed;
|
||||
amountUsed = 0;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// ZeroCopyOutputStream Interface
|
||||
|
||||
bool
|
||||
ZeroCopyNSIOutputStream::Next(void** data, int* size)
|
||||
{
|
||||
MOZ_ASSERT(data != nullptr);
|
||||
MOZ_ASSERT(size != nullptr);
|
||||
|
||||
if (failed())
|
||||
return false;
|
||||
|
||||
if (amountUsed == BUFFER_SIZE) {
|
||||
if (NS_FAILED(writeBuffer()))
|
||||
return false;
|
||||
}
|
||||
|
||||
*data = buffer + amountUsed;
|
||||
*size = BUFFER_SIZE - amountUsed;
|
||||
amountUsed = BUFFER_SIZE;
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
ZeroCopyNSIOutputStream::BackUp(int count)
|
||||
{
|
||||
MOZ_ASSERT(count >= 0,
|
||||
"Cannot back up a negative amount of bytes.");
|
||||
MOZ_ASSERT(amountUsed == BUFFER_SIZE,
|
||||
"Can only call BackUp directly after calling Next.");
|
||||
MOZ_ASSERT(count <= amountUsed,
|
||||
"Can't back up further than we've given out.");
|
||||
|
||||
amountUsed -= count;
|
||||
}
|
||||
|
||||
::google::protobuf::int64
|
||||
ZeroCopyNSIOutputStream::ByteCount() const
|
||||
{
|
||||
return writtenCount + amountUsed;
|
||||
}
|
||||
|
||||
} // namespace devtools
|
||||
} // namespace mozilla
|
||||
70
dom/heapsnapshot/ZeroCopyNSIOutputStream.h
Normal file
70
dom/heapsnapshot/ZeroCopyNSIOutputStream.h
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/* -*- 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_ZeroCopyNSIOutputStream__
|
||||
#define mozilla_devtools_ZeroCopyNSIOutputStream__
|
||||
|
||||
#include <google/protobuf/io/zero_copy_stream.h>
|
||||
#include <google/protobuf/stubs/common.h>
|
||||
|
||||
#include "nsCOMPtr.h"
|
||||
#include "nsIOutputStream.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace devtools {
|
||||
|
||||
// A `google::protobuf::io::ZeroCopyOutputStream` implementation that uses an
|
||||
// `nsIOutputStream` under the covers.
|
||||
//
|
||||
// This class will automatically write and flush its data to the
|
||||
// `nsIOutputStream` in its destructor, but if you care whether that call
|
||||
// succeeds or fails, then you should call the `flush` method yourself. Errors
|
||||
// will be logged, however.
|
||||
class MOZ_STACK_CLASS ZeroCopyNSIOutputStream
|
||||
: public ::google::protobuf::io::ZeroCopyOutputStream
|
||||
{
|
||||
static const int BUFFER_SIZE = 8192;
|
||||
|
||||
// The nsIOutputStream we are streaming to.
|
||||
nsCOMPtr<nsIOutputStream>& out;
|
||||
|
||||
// The buffer we write data to before passing it to the output stream.
|
||||
char buffer[BUFFER_SIZE];
|
||||
|
||||
// The status of writing to the underlying output stream.
|
||||
nsresult result_;
|
||||
|
||||
// The number of bytes in the buffer that have been used thus far.
|
||||
int amountUsed;
|
||||
|
||||
// Excluding the amount of the buffer currently used (which hasn't been
|
||||
// written and flushed yet), this is the number of bytes written to the output
|
||||
// stream.
|
||||
int64_t writtenCount;
|
||||
|
||||
// Write the internal buffer to the output stream and flush it.
|
||||
nsresult writeBuffer();
|
||||
|
||||
public:
|
||||
explicit ZeroCopyNSIOutputStream(nsCOMPtr<nsIOutputStream>& out);
|
||||
|
||||
nsresult flush() { return writeBuffer(); }
|
||||
|
||||
// Return true if writing to the underlying output stream ever failed.
|
||||
bool failed() const { return NS_FAILED(result_); }
|
||||
|
||||
nsresult result() const { return result_; }
|
||||
|
||||
// ZeroCopyOutputStream Interface
|
||||
virtual ~ZeroCopyNSIOutputStream() override;
|
||||
virtual bool Next(void** data, int* size) override;
|
||||
virtual void BackUp(int count) override;
|
||||
virtual ::google::protobuf::int64 ByteCount() const override;
|
||||
};
|
||||
|
||||
} // namespace devtools
|
||||
} // namespace mozilla
|
||||
|
||||
#endif // mozilla_devtools_ZeroCopyNSIOutputStream__
|
||||
26
dom/heapsnapshot/generate-core-dump-sources.sh
Normal file
26
dom/heapsnapshot/generate-core-dump-sources.sh
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# A script to generate devtools/server/CoreDump.pb.{h,cc} from
|
||||
# devtools/server/CoreDump.proto. This script assumes you have
|
||||
# downloaded and installed the protocol buffer compiler, and that it is either
|
||||
# on your $PATH or located at $PROTOC_PATH.
|
||||
#
|
||||
# These files were last compiled with libprotoc 2.4.1.
|
||||
|
||||
set -e
|
||||
|
||||
cd $(dirname $0)
|
||||
|
||||
if [ -n $PROTOC_PATH ]; then
|
||||
PROTOC_PATH=`which protoc`
|
||||
fi
|
||||
|
||||
if [ ! -e $PROTOC_PATH ]; then
|
||||
echo You must install the protocol compiler from
|
||||
echo https://code.google.com/p/protobuf/downloads/list
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo Using $PROTOC_PATH as the protocol compiler
|
||||
|
||||
$PROTOC_PATH --cpp_out="." CoreDump.proto
|
||||
52
dom/heapsnapshot/moz.build
Normal file
52
dom/heapsnapshot/moz.build
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
with Files('**'):
|
||||
BUG_COMPONENT = ('Firefox', 'Developer Tools: Memory')
|
||||
|
||||
if CONFIG['ENABLE_TESTS']:
|
||||
DIRS += ['tests/gtest']
|
||||
|
||||
XPCSHELL_TESTS_MANIFESTS += [ 'tests/unit/xpcshell.ini' ]
|
||||
MOCHITEST_MANIFESTS += [ 'tests/mochitest/mochitest.ini' ]
|
||||
MOCHITEST_CHROME_MANIFESTS += [ 'tests/mochitest/chrome.ini' ]
|
||||
|
||||
EXPORTS.mozilla.devtools += [
|
||||
'AutoMemMap.h',
|
||||
'CoreDump.pb.h',
|
||||
'DeserializedNode.h',
|
||||
'DominatorTree.h',
|
||||
'FileDescriptorOutputStream.h',
|
||||
'HeapSnapshot.h',
|
||||
'HeapSnapshotTempFileHelperChild.h',
|
||||
'HeapSnapshotTempFileHelperParent.h',
|
||||
'ZeroCopyNSIOutputStream.h',
|
||||
]
|
||||
|
||||
IPDL_SOURCES += [
|
||||
'PHeapSnapshotTempFileHelper.ipdl',
|
||||
]
|
||||
|
||||
include('/ipc/chromium/chromium-config.mozbuild')
|
||||
|
||||
SOURCES += [
|
||||
'AutoMemMap.cpp',
|
||||
'CoreDump.pb.cc',
|
||||
'DeserializedNode.cpp',
|
||||
'DominatorTree.cpp',
|
||||
'FileDescriptorOutputStream.cpp',
|
||||
'HeapSnapshot.cpp',
|
||||
'HeapSnapshotTempFileHelperParent.cpp',
|
||||
'ZeroCopyNSIOutputStream.cpp',
|
||||
]
|
||||
|
||||
# Disable RTTI in google protocol buffer
|
||||
DEFINES['GOOGLE_PROTOBUF_NO_RTTI'] = True
|
||||
|
||||
FINAL_LIBRARY = 'xul'
|
||||
|
||||
if CONFIG['GNU_CXX']:
|
||||
CXXFLAGS += ['-Wno-error=shadow']
|
||||
100
dom/heapsnapshot/tests/gtest/DeserializedNodeUbiNodes.cpp
Normal file
100
dom/heapsnapshot/tests/gtest/DeserializedNodeUbiNodes.cpp
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
/* -*- 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/. */
|
||||
|
||||
// Test that the `JS::ubi::Node`s we create from
|
||||
// `mozilla::devtools::DeserializedNode` instances look and behave as we would
|
||||
// like.
|
||||
|
||||
#include "DevTools.h"
|
||||
#include "js/TypeDecls.h"
|
||||
#include "mozilla/devtools/DeserializedNode.h"
|
||||
|
||||
using testing::Field;
|
||||
using testing::ReturnRef;
|
||||
|
||||
// A mock DeserializedNode for testing.
|
||||
struct MockDeserializedNode : public DeserializedNode
|
||||
{
|
||||
MockDeserializedNode(NodeId id, const char16_t* typeName, uint64_t size)
|
||||
: DeserializedNode(id, typeName, size)
|
||||
{ }
|
||||
|
||||
bool addEdge(DeserializedEdge&& edge)
|
||||
{
|
||||
return edges.append(Move(edge));
|
||||
}
|
||||
|
||||
MOCK_METHOD1(getEdgeReferent, JS::ubi::Node(const DeserializedEdge&));
|
||||
};
|
||||
|
||||
size_t fakeMallocSizeOf(const void*) {
|
||||
EXPECT_TRUE(false);
|
||||
MOZ_ASSERT_UNREACHABLE("fakeMallocSizeOf should never be called because "
|
||||
"DeserializedNodes report the deserialized size.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
DEF_TEST(DeserializedNodeUbiNodes, {
|
||||
const char16_t* typeName = u"TestTypeName";
|
||||
const char* className = "MyObjectClassName";
|
||||
const char* filename = "my-cool-filename.js";
|
||||
|
||||
NodeId id = uint64_t(1) << 33;
|
||||
uint64_t size = uint64_t(1) << 60;
|
||||
MockDeserializedNode mocked(id, typeName, size);
|
||||
mocked.coarseType = JS::ubi::CoarseType::Script;
|
||||
mocked.jsObjectClassName = className;
|
||||
mocked.scriptFilename = filename;
|
||||
|
||||
DeserializedNode& deserialized = mocked;
|
||||
JS::ubi::Node ubi(&deserialized);
|
||||
|
||||
// Test the ubi::Node accessors.
|
||||
|
||||
EXPECT_EQ(size, ubi.size(fakeMallocSizeOf));
|
||||
EXPECT_EQ(typeName, ubi.typeName());
|
||||
EXPECT_EQ(JS::ubi::CoarseType::Script, ubi.coarseType());
|
||||
EXPECT_EQ(id, ubi.identifier());
|
||||
EXPECT_FALSE(ubi.isLive());
|
||||
EXPECT_EQ(ubi.jsObjectClassName(), className);
|
||||
EXPECT_EQ(ubi.scriptFilename(), filename);
|
||||
|
||||
// Test the ubi::Node's edges.
|
||||
|
||||
UniquePtr<DeserializedNode> referent1(new MockDeserializedNode(1,
|
||||
nullptr,
|
||||
10));
|
||||
DeserializedEdge edge1(referent1->id);
|
||||
mocked.addEdge(Move(edge1));
|
||||
EXPECT_CALL(mocked, getEdgeReferent(EdgeTo(referent1->id)))
|
||||
.Times(1)
|
||||
.WillOnce(Return(JS::ubi::Node(referent1.get())));
|
||||
|
||||
UniquePtr<DeserializedNode> referent2(new MockDeserializedNode(2,
|
||||
nullptr,
|
||||
20));
|
||||
DeserializedEdge edge2(referent2->id);
|
||||
mocked.addEdge(Move(edge2));
|
||||
EXPECT_CALL(mocked, getEdgeReferent(EdgeTo(referent2->id)))
|
||||
.Times(1)
|
||||
.WillOnce(Return(JS::ubi::Node(referent2.get())));
|
||||
|
||||
UniquePtr<DeserializedNode> referent3(new MockDeserializedNode(3,
|
||||
nullptr,
|
||||
30));
|
||||
DeserializedEdge edge3(referent3->id);
|
||||
mocked.addEdge(Move(edge3));
|
||||
EXPECT_CALL(mocked, getEdgeReferent(EdgeTo(referent3->id)))
|
||||
.Times(1)
|
||||
.WillOnce(Return(JS::ubi::Node(referent3.get())));
|
||||
|
||||
auto range = ubi.edges(cx);
|
||||
ASSERT_TRUE(!!range);
|
||||
|
||||
for ( ; !range->empty(); range->popFront()) {
|
||||
// Nothing to do here. This loop ensures that we get each edge referent
|
||||
// that we expect above.
|
||||
}
|
||||
});
|
||||
|
|
@ -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/. */
|
||||
|
||||
// Test that the `JS::ubi::StackFrame`s we create from
|
||||
// `mozilla::devtools::DeserializedStackFrame` instances look and behave as we would
|
||||
// like.
|
||||
|
||||
#include "DevTools.h"
|
||||
#include "js/TypeDecls.h"
|
||||
#include "mozilla/devtools/DeserializedNode.h"
|
||||
|
||||
using testing::Field;
|
||||
using testing::ReturnRef;
|
||||
|
||||
// A mock DeserializedStackFrame for testing.
|
||||
struct MockDeserializedStackFrame : public DeserializedStackFrame
|
||||
{
|
||||
MockDeserializedStackFrame() : DeserializedStackFrame() { }
|
||||
};
|
||||
|
||||
DEF_TEST(DeserializedStackFrameUbiStackFrames, {
|
||||
StackFrameId id = uint64_t(1) << 42;
|
||||
uint32_t line = 1337;
|
||||
uint32_t column = 9; // 3 space tabs!?
|
||||
const char16_t* source = u"my-javascript-file.js";
|
||||
const char16_t* functionDisplayName = u"myFunctionName";
|
||||
|
||||
MockDeserializedStackFrame mocked;
|
||||
mocked.id = id;
|
||||
mocked.line = line;
|
||||
mocked.column = column;
|
||||
mocked.source = source;
|
||||
mocked.functionDisplayName = functionDisplayName;
|
||||
|
||||
DeserializedStackFrame& deserialized = mocked;
|
||||
JS::ubi::StackFrame ubiFrame(&deserialized);
|
||||
|
||||
// Test the JS::ubi::StackFrame accessors.
|
||||
|
||||
EXPECT_EQ(id, ubiFrame.identifier());
|
||||
EXPECT_EQ(JS::ubi::StackFrame(), ubiFrame.parent());
|
||||
EXPECT_EQ(line, ubiFrame.line());
|
||||
EXPECT_EQ(column, ubiFrame.column());
|
||||
EXPECT_EQ(JS::ubi::AtomOrTwoByteChars(source), ubiFrame.source());
|
||||
EXPECT_EQ(JS::ubi::AtomOrTwoByteChars(functionDisplayName),
|
||||
ubiFrame.functionDisplayName());
|
||||
EXPECT_FALSE(ubiFrame.isSelfHosted(cx));
|
||||
EXPECT_FALSE(ubiFrame.isSystem());
|
||||
|
||||
JS::RootedObject savedFrame(cx);
|
||||
EXPECT_TRUE(ubiFrame.constructSavedFrameStack(cx, &savedFrame));
|
||||
|
||||
uint32_t frameLine;
|
||||
ASSERT_EQ(JS::SavedFrameResult::Ok, JS::GetSavedFrameLine(cx, savedFrame, &frameLine));
|
||||
EXPECT_EQ(line, frameLine);
|
||||
|
||||
uint32_t frameColumn;
|
||||
ASSERT_EQ(JS::SavedFrameResult::Ok, JS::GetSavedFrameColumn(cx, savedFrame, &frameColumn));
|
||||
EXPECT_EQ(column, frameColumn);
|
||||
|
||||
JS::RootedObject parent(cx);
|
||||
ASSERT_EQ(JS::SavedFrameResult::Ok, JS::GetSavedFrameParent(cx, savedFrame, &parent));
|
||||
EXPECT_EQ(nullptr, parent);
|
||||
|
||||
ASSERT_EQ(NS_strlen(source), 21U);
|
||||
char16_t sourceBuf[21] = {};
|
||||
|
||||
// Test when the length is shorter than the string length.
|
||||
auto written = ubiFrame.source(RangedPtr<char16_t>(sourceBuf), 3);
|
||||
EXPECT_EQ(written, 3U);
|
||||
for (size_t i = 0; i < 3; i++) {
|
||||
EXPECT_EQ(sourceBuf[i], source[i]);
|
||||
}
|
||||
|
||||
written = ubiFrame.source(RangedPtr<char16_t>(sourceBuf), 21);
|
||||
EXPECT_EQ(written, 21U);
|
||||
for (size_t i = 0; i < 21; i++) {
|
||||
EXPECT_EQ(sourceBuf[i], source[i]);
|
||||
}
|
||||
|
||||
ASSERT_EQ(NS_strlen(functionDisplayName), 14U);
|
||||
char16_t nameBuf[14] = {};
|
||||
|
||||
written = ubiFrame.functionDisplayName(RangedPtr<char16_t>(nameBuf), 14);
|
||||
EXPECT_EQ(written, 14U);
|
||||
for (size_t i = 0; i < 14; i++) {
|
||||
EXPECT_EQ(nameBuf[i], functionDisplayName[i]);
|
||||
}
|
||||
});
|
||||
276
dom/heapsnapshot/tests/gtest/DevTools.h
Normal file
276
dom/heapsnapshot/tests/gtest/DevTools.h
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
/* -*- 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_gtest_DevTools__
|
||||
#define mozilla_devtools_gtest_DevTools__
|
||||
|
||||
#include "CoreDump.pb.h"
|
||||
#include "jsapi.h"
|
||||
#include "jspubtd.h"
|
||||
#include "nsCRTGlue.h"
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include "mozilla/devtools/HeapSnapshot.h"
|
||||
#include "mozilla/dom/ChromeUtils.h"
|
||||
#include "mozilla/CycleCollectedJSContext.h"
|
||||
#include "mozilla/Move.h"
|
||||
#include "js/Principals.h"
|
||||
#include "js/UbiNode.h"
|
||||
#include "js/UniquePtr.h"
|
||||
|
||||
using namespace mozilla;
|
||||
using namespace mozilla::devtools;
|
||||
using namespace mozilla::dom;
|
||||
using namespace testing;
|
||||
|
||||
// GTest fixture class that all of our tests derive from.
|
||||
struct DevTools : public ::testing::Test {
|
||||
bool _initialized;
|
||||
JSContext* cx;
|
||||
JSCompartment* compartment;
|
||||
JS::Zone* zone;
|
||||
JS::PersistentRootedObject global;
|
||||
|
||||
DevTools()
|
||||
: _initialized(false),
|
||||
cx(nullptr)
|
||||
{ }
|
||||
|
||||
virtual void SetUp() {
|
||||
MOZ_ASSERT(!_initialized);
|
||||
|
||||
cx = getContext();
|
||||
if (!cx)
|
||||
return;
|
||||
|
||||
JS_BeginRequest(cx);
|
||||
|
||||
global.init(cx, createGlobal());
|
||||
if (!global)
|
||||
return;
|
||||
JS_EnterCompartment(cx, global);
|
||||
|
||||
compartment = js::GetContextCompartment(cx);
|
||||
zone = js::GetContextZone(cx);
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
JSContext* getContext() {
|
||||
return CycleCollectedJSContext::Get()->Context();
|
||||
}
|
||||
|
||||
static void reportError(JSContext* cx, const char* message, JSErrorReport* report) {
|
||||
fprintf(stderr, "%s:%u:%s\n",
|
||||
report->filename ? report->filename : "<no filename>",
|
||||
(unsigned int) report->lineno,
|
||||
message);
|
||||
}
|
||||
|
||||
static const JSClass* getGlobalClass() {
|
||||
static const JSClassOps globalClassOps = {
|
||||
nullptr, nullptr, nullptr, nullptr,
|
||||
nullptr, nullptr, nullptr, nullptr,
|
||||
nullptr, nullptr, nullptr,
|
||||
JS_GlobalObjectTraceHook
|
||||
};
|
||||
static const JSClass globalClass = {
|
||||
"global", JSCLASS_GLOBAL_FLAGS,
|
||||
&globalClassOps
|
||||
};
|
||||
return &globalClass;
|
||||
}
|
||||
|
||||
JSObject* createGlobal()
|
||||
{
|
||||
/* Create the global object. */
|
||||
JS::RootedObject newGlobal(cx);
|
||||
JS::CompartmentOptions options;
|
||||
options.behaviors().setVersion(JSVERSION_LATEST);
|
||||
newGlobal = JS_NewGlobalObject(cx, getGlobalClass(), nullptr,
|
||||
JS::FireOnNewGlobalHook, options);
|
||||
if (!newGlobal)
|
||||
return nullptr;
|
||||
|
||||
JSAutoCompartment ac(cx, newGlobal);
|
||||
|
||||
/* Populate the global object with the standard globals, like Object and
|
||||
Array. */
|
||||
if (!JS_InitStandardClasses(cx, newGlobal))
|
||||
return nullptr;
|
||||
|
||||
return newGlobal;
|
||||
}
|
||||
|
||||
virtual void TearDown() {
|
||||
_initialized = false;
|
||||
|
||||
if (global) {
|
||||
JS_LeaveCompartment(cx, nullptr);
|
||||
global = nullptr;
|
||||
}
|
||||
if (cx)
|
||||
JS_EndRequest(cx);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Helper to define a test and ensure that the fixture is initialized properly.
|
||||
#define DEF_TEST(name, body) \
|
||||
TEST_F(DevTools, name) { \
|
||||
ASSERT_TRUE(_initialized); \
|
||||
body \
|
||||
}
|
||||
|
||||
|
||||
// Fake JS::ubi::Node implementation
|
||||
class MOZ_STACK_CLASS FakeNode
|
||||
{
|
||||
public:
|
||||
JS::ubi::EdgeVector edges;
|
||||
JSCompartment* compartment;
|
||||
JS::Zone* zone;
|
||||
size_t size;
|
||||
|
||||
explicit FakeNode()
|
||||
: edges(),
|
||||
compartment(nullptr),
|
||||
zone(nullptr),
|
||||
size(1)
|
||||
{ }
|
||||
};
|
||||
|
||||
namespace JS {
|
||||
namespace ubi {
|
||||
|
||||
template<>
|
||||
class Concrete<FakeNode> : public Base
|
||||
{
|
||||
const char16_t* typeName() const override {
|
||||
return concreteTypeName;
|
||||
}
|
||||
|
||||
js::UniquePtr<EdgeRange> edges(JSContext*, bool) const override {
|
||||
return js::UniquePtr<EdgeRange>(js_new<PreComputedEdgeRange>(get().edges));
|
||||
}
|
||||
|
||||
Size size(mozilla::MallocSizeOf) const override {
|
||||
return get().size;
|
||||
}
|
||||
|
||||
JS::Zone* zone() const override {
|
||||
return get().zone;
|
||||
}
|
||||
|
||||
JSCompartment* compartment() const override {
|
||||
return get().compartment;
|
||||
}
|
||||
|
||||
protected:
|
||||
explicit Concrete(FakeNode* ptr) : Base(ptr) { }
|
||||
FakeNode& get() const { return *static_cast<FakeNode*>(ptr); }
|
||||
|
||||
public:
|
||||
static const char16_t concreteTypeName[];
|
||||
static void construct(void* storage, FakeNode* ptr) {
|
||||
new (storage) Concrete(ptr);
|
||||
}
|
||||
};
|
||||
|
||||
const char16_t Concrete<FakeNode>::concreteTypeName[] = u"FakeNode";
|
||||
|
||||
} // namespace ubi
|
||||
} // namespace JS
|
||||
|
||||
void AddEdge(FakeNode& node, FakeNode& referent, const char16_t* edgeName = nullptr) {
|
||||
char16_t* ownedEdgeName = nullptr;
|
||||
if (edgeName) {
|
||||
ownedEdgeName = NS_strdup(edgeName);
|
||||
ASSERT_NE(ownedEdgeName, nullptr);
|
||||
}
|
||||
|
||||
JS::ubi::Edge edge(ownedEdgeName, &referent);
|
||||
ASSERT_TRUE(node.edges.append(mozilla::Move(edge)));
|
||||
}
|
||||
|
||||
|
||||
// Custom GMock Matchers
|
||||
|
||||
// Use the testing namespace to avoid static analysis failures in the gmock
|
||||
// matcher classes that get generated from MATCHER_P macros.
|
||||
namespace testing {
|
||||
|
||||
// Ensure that given node has the expected number of edges.
|
||||
MATCHER_P2(EdgesLength, cx, expectedLength, "") {
|
||||
auto edges = arg.edges(cx);
|
||||
if (!edges)
|
||||
return false;
|
||||
|
||||
int actualLength = 0;
|
||||
for ( ; !edges->empty(); edges->popFront())
|
||||
actualLength++;
|
||||
|
||||
return Matcher<int>(Eq(expectedLength))
|
||||
.MatchAndExplain(actualLength, result_listener);
|
||||
}
|
||||
|
||||
// Get the nth edge and match it with the given matcher.
|
||||
MATCHER_P3(Edge, cx, n, matcher, "") {
|
||||
auto edges = arg.edges(cx);
|
||||
if (!edges)
|
||||
return false;
|
||||
|
||||
int i = 0;
|
||||
for ( ; !edges->empty(); edges->popFront()) {
|
||||
if (i == n) {
|
||||
return Matcher<const JS::ubi::Edge&>(matcher)
|
||||
.MatchAndExplain(edges->front(), result_listener);
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensures that two char16_t* strings are equal.
|
||||
MATCHER_P(UTF16StrEq, str, "") {
|
||||
return NS_strcmp(arg, str) == 0;
|
||||
}
|
||||
|
||||
MATCHER_P(UniqueUTF16StrEq, str, "") {
|
||||
return NS_strcmp(arg.get(), str) == 0;
|
||||
}
|
||||
|
||||
MATCHER(UniqueIsNull, "") {
|
||||
return arg.get() == nullptr;
|
||||
}
|
||||
|
||||
// Matches an edge whose referent is the node with the given id.
|
||||
MATCHER_P(EdgeTo, id, "") {
|
||||
return Matcher<const DeserializedEdge&>(Field(&DeserializedEdge::referent, id))
|
||||
.MatchAndExplain(arg, result_listener);
|
||||
}
|
||||
|
||||
} // namespace testing
|
||||
|
||||
|
||||
// A mock `Writer` class to be used with testing `WriteHeapGraph`.
|
||||
class MockWriter : public CoreDumpWriter
|
||||
{
|
||||
public:
|
||||
virtual ~MockWriter() override { }
|
||||
MOCK_METHOD2(writeNode, bool(const JS::ubi::Node&, CoreDumpWriter::EdgePolicy));
|
||||
MOCK_METHOD1(writeMetadata, bool(uint64_t));
|
||||
};
|
||||
|
||||
void ExpectWriteNode(MockWriter& writer, FakeNode& node) {
|
||||
EXPECT_CALL(writer, writeNode(Eq(JS::ubi::Node(&node)), _))
|
||||
.Times(1)
|
||||
.WillOnce(Return(true));
|
||||
}
|
||||
|
||||
#endif // mozilla_devtools_gtest_DevTools__
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
/* -*- 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/. */
|
||||
|
||||
// Test that heap snapshots cross compartment boundaries when expected.
|
||||
|
||||
#include "DevTools.h"
|
||||
|
||||
DEF_TEST(DoesCrossCompartmentBoundaries, {
|
||||
// Create a new global to get a new compartment.
|
||||
JS::CompartmentOptions options;
|
||||
JS::RootedObject newGlobal(cx, JS_NewGlobalObject(cx,
|
||||
getGlobalClass(),
|
||||
nullptr,
|
||||
JS::FireOnNewGlobalHook,
|
||||
options));
|
||||
ASSERT_TRUE(newGlobal);
|
||||
JSCompartment* newCompartment = nullptr;
|
||||
{
|
||||
JSAutoCompartment ac(cx, newGlobal);
|
||||
ASSERT_TRUE(JS_InitStandardClasses(cx, newGlobal));
|
||||
newCompartment = js::GetContextCompartment(cx);
|
||||
}
|
||||
ASSERT_TRUE(newCompartment);
|
||||
ASSERT_NE(newCompartment, compartment);
|
||||
|
||||
// Our set of target compartments is both the old and new compartments.
|
||||
JS::CompartmentSet targetCompartments;
|
||||
ASSERT_TRUE(targetCompartments.init());
|
||||
ASSERT_TRUE(targetCompartments.put(compartment));
|
||||
ASSERT_TRUE(targetCompartments.put(newCompartment));
|
||||
|
||||
FakeNode nodeA;
|
||||
FakeNode nodeB;
|
||||
FakeNode nodeC;
|
||||
FakeNode nodeD;
|
||||
|
||||
nodeA.compartment = compartment;
|
||||
nodeB.compartment = nullptr;
|
||||
nodeC.compartment = newCompartment;
|
||||
nodeD.compartment = nullptr;
|
||||
|
||||
AddEdge(nodeA, nodeB);
|
||||
AddEdge(nodeA, nodeC);
|
||||
AddEdge(nodeB, nodeD);
|
||||
|
||||
::testing::NiceMock<MockWriter> writer;
|
||||
|
||||
// Should serialize nodeA, because it is in one of our target compartments.
|
||||
ExpectWriteNode(writer, nodeA);
|
||||
|
||||
// Should serialize nodeB, because it doesn't belong to a compartment and is
|
||||
// therefore assumed to be shared.
|
||||
ExpectWriteNode(writer, nodeB);
|
||||
|
||||
// Should also serialize nodeC, which is in our target compartments, but a
|
||||
// different compartment than A.
|
||||
ExpectWriteNode(writer, nodeC);
|
||||
|
||||
// Should serialize nodeD because it's reachable via B and both nodes B and D
|
||||
// don't belong to a specific compartment.
|
||||
ExpectWriteNode(writer, nodeD);
|
||||
|
||||
JS::AutoCheckCannotGC noGC(cx);
|
||||
|
||||
ASSERT_TRUE(WriteHeapGraph(cx,
|
||||
JS::ubi::Node(&nodeA),
|
||||
writer,
|
||||
/* wantNames = */ false,
|
||||
&targetCompartments,
|
||||
noGC));
|
||||
});
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/* -*- 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/. */
|
||||
|
||||
// Test that heap snapshots walk the compartment boundaries correctly.
|
||||
|
||||
#include "DevTools.h"
|
||||
|
||||
DEF_TEST(DoesntCrossCompartmentBoundaries, {
|
||||
// Create a new global to get a new compartment.
|
||||
JS::CompartmentOptions options;
|
||||
JS::RootedObject newGlobal(cx, JS_NewGlobalObject(cx,
|
||||
getGlobalClass(),
|
||||
nullptr,
|
||||
JS::FireOnNewGlobalHook,
|
||||
options));
|
||||
ASSERT_TRUE(newGlobal);
|
||||
JSCompartment* newCompartment = nullptr;
|
||||
{
|
||||
JSAutoCompartment ac(cx, newGlobal);
|
||||
ASSERT_TRUE(JS_InitStandardClasses(cx, newGlobal));
|
||||
newCompartment = js::GetContextCompartment(cx);
|
||||
}
|
||||
ASSERT_TRUE(newCompartment);
|
||||
ASSERT_NE(newCompartment, compartment);
|
||||
|
||||
// Our set of target compartments is only the pre-existing compartment and
|
||||
// does not include the new compartment.
|
||||
JS::CompartmentSet targetCompartments;
|
||||
ASSERT_TRUE(targetCompartments.init());
|
||||
ASSERT_TRUE(targetCompartments.put(compartment));
|
||||
|
||||
FakeNode nodeA;
|
||||
FakeNode nodeB;
|
||||
FakeNode nodeC;
|
||||
|
||||
nodeA.compartment = compartment;
|
||||
nodeB.compartment = nullptr;
|
||||
nodeC.compartment = newCompartment;
|
||||
|
||||
AddEdge(nodeA, nodeB);
|
||||
AddEdge(nodeB, nodeC);
|
||||
|
||||
::testing::NiceMock<MockWriter> writer;
|
||||
|
||||
// Should serialize nodeA, because it is in our target compartments.
|
||||
ExpectWriteNode(writer, nodeA);
|
||||
|
||||
// Should serialize nodeB, because it doesn't belong to a compartment and is
|
||||
// therefore assumed to be shared.
|
||||
ExpectWriteNode(writer, nodeB);
|
||||
|
||||
// But we shouldn't ever serialize nodeC.
|
||||
|
||||
JS::AutoCheckCannotGC noGC(cx);
|
||||
|
||||
ASSERT_TRUE(WriteHeapGraph(cx,
|
||||
JS::ubi::Node(&nodeA),
|
||||
writer,
|
||||
/* wantNames = */ false,
|
||||
&targetCompartments,
|
||||
noGC));
|
||||
});
|
||||
53
dom/heapsnapshot/tests/gtest/SerializesEdgeNames.cpp
Normal file
53
dom/heapsnapshot/tests/gtest/SerializesEdgeNames.cpp
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/* -*- 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/. */
|
||||
|
||||
// Test that edge names get serialized correctly.
|
||||
|
||||
#include "DevTools.h"
|
||||
|
||||
using testing::Field;
|
||||
using testing::IsNull;
|
||||
using testing::Property;
|
||||
using testing::Return;
|
||||
|
||||
DEF_TEST(SerializesEdgeNames, {
|
||||
FakeNode node;
|
||||
FakeNode referent;
|
||||
|
||||
const char16_t edgeName[] = u"edge name";
|
||||
const char16_t emptyStr[] = u"";
|
||||
|
||||
AddEdge(node, referent, edgeName);
|
||||
AddEdge(node, referent, emptyStr);
|
||||
AddEdge(node, referent, nullptr);
|
||||
|
||||
::testing::NiceMock<MockWriter> writer;
|
||||
|
||||
// Should get the node with edges once.
|
||||
EXPECT_CALL(
|
||||
writer,
|
||||
writeNode(AllOf(EdgesLength(cx, 3),
|
||||
Edge(cx, 0, Field(&JS::ubi::Edge::name,
|
||||
UniqueUTF16StrEq(edgeName))),
|
||||
Edge(cx, 1, Field(&JS::ubi::Edge::name,
|
||||
UniqueUTF16StrEq(emptyStr))),
|
||||
Edge(cx, 2, Field(&JS::ubi::Edge::name,
|
||||
UniqueIsNull()))),
|
||||
_)
|
||||
)
|
||||
.Times(1)
|
||||
.WillOnce(Return(true));
|
||||
|
||||
// Should get the referent node that doesn't have any edges once.
|
||||
ExpectWriteNode(writer, referent);
|
||||
|
||||
JS::AutoCheckCannotGC noGC(cx);
|
||||
ASSERT_TRUE(WriteHeapGraph(cx,
|
||||
JS::ubi::Node(&node),
|
||||
writer,
|
||||
/* wantNames = */ true,
|
||||
/* zones = */ nullptr,
|
||||
noGC));
|
||||
});
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/* -*- 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/. */
|
||||
|
||||
// Test that everything in the heap graph gets serialized once, and only once.
|
||||
|
||||
#include "DevTools.h"
|
||||
|
||||
DEF_TEST(SerializesEverythingInHeapGraphOnce, {
|
||||
FakeNode nodeA;
|
||||
FakeNode nodeB;
|
||||
FakeNode nodeC;
|
||||
FakeNode nodeD;
|
||||
|
||||
AddEdge(nodeA, nodeB);
|
||||
AddEdge(nodeB, nodeC);
|
||||
AddEdge(nodeC, nodeD);
|
||||
AddEdge(nodeD, nodeA);
|
||||
|
||||
::testing::NiceMock<MockWriter> writer;
|
||||
|
||||
// Should serialize each node once.
|
||||
ExpectWriteNode(writer, nodeA);
|
||||
ExpectWriteNode(writer, nodeB);
|
||||
ExpectWriteNode(writer, nodeC);
|
||||
ExpectWriteNode(writer, nodeD);
|
||||
|
||||
JS::AutoCheckCannotGC noGC(cx);
|
||||
|
||||
ASSERT_TRUE(WriteHeapGraph(cx,
|
||||
JS::ubi::Node(&nodeA),
|
||||
writer,
|
||||
/* wantNames = */ false,
|
||||
/* zones = */ nullptr,
|
||||
noGC));
|
||||
});
|
||||
30
dom/heapsnapshot/tests/gtest/SerializesTypeNames.cpp
Normal file
30
dom/heapsnapshot/tests/gtest/SerializesTypeNames.cpp
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/* -*- 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/. */
|
||||
|
||||
// Test that a ubi::Node's typeName gets properly serialized into a core dump.
|
||||
|
||||
#include "DevTools.h"
|
||||
|
||||
using testing::Property;
|
||||
using testing::Return;
|
||||
|
||||
DEF_TEST(SerializesTypeNames, {
|
||||
FakeNode node;
|
||||
|
||||
::testing::NiceMock<MockWriter> writer;
|
||||
EXPECT_CALL(writer, writeNode(Property(&JS::ubi::Node::typeName,
|
||||
UTF16StrEq(u"FakeNode")),
|
||||
_))
|
||||
.Times(1)
|
||||
.WillOnce(Return(true));
|
||||
|
||||
JS::AutoCheckCannotGC noGC(cx);
|
||||
ASSERT_TRUE(WriteHeapGraph(cx,
|
||||
JS::ubi::Node(&node),
|
||||
writer,
|
||||
/* wantNames = */ true,
|
||||
/* zones = */ nullptr,
|
||||
noGC));
|
||||
});
|
||||
31
dom/heapsnapshot/tests/gtest/moz.build
Normal file
31
dom/heapsnapshot/tests/gtest/moz.build
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
Library('devtoolstests')
|
||||
|
||||
LOCAL_INCLUDES += [
|
||||
'../..',
|
||||
]
|
||||
|
||||
UNIFIED_SOURCES = [
|
||||
'DeserializedNodeUbiNodes.cpp',
|
||||
'DeserializedStackFrameUbiStackFrames.cpp',
|
||||
'DoesCrossCompartmentBoundaries.cpp',
|
||||
'DoesntCrossCompartmentBoundaries.cpp',
|
||||
'SerializesEdgeNames.cpp',
|
||||
'SerializesEverythingInHeapGraphOnce.cpp',
|
||||
'SerializesTypeNames.cpp',
|
||||
]
|
||||
|
||||
if CONFIG['GNU_CXX']:
|
||||
CXXFLAGS += ['-Wno-error=shadow']
|
||||
|
||||
# THE MOCK_METHOD2 macro from gtest triggers this clang warning and it's hard
|
||||
# to work around, so we just ignore it.
|
||||
if CONFIG['CLANG_CXX']:
|
||||
CXXFLAGS += ['-Wno-inconsistent-missing-override']
|
||||
|
||||
FINAL_LIBRARY = 'xul-gtest'
|
||||
8
dom/heapsnapshot/tests/mochitest/chrome.ini
Normal file
8
dom/heapsnapshot/tests/mochitest/chrome.ini
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
[DEFAULT]
|
||||
tags = devtools devtools-memory
|
||||
skip-if = os == 'android'
|
||||
support-files =
|
||||
|
||||
[test_DominatorTree_01.html]
|
||||
[test_SaveHeapSnapshot.html]
|
||||
|
||||
6
dom/heapsnapshot/tests/mochitest/mochitest.ini
Normal file
6
dom/heapsnapshot/tests/mochitest/mochitest.ini
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[DEFAULT]
|
||||
tags = devtools devtools-memory
|
||||
support-files =
|
||||
|
||||
[test_saveHeapSnapshot_e10s_01.html]
|
||||
|
||||
37
dom/heapsnapshot/tests/mochitest/test_DominatorTree_01.html
Normal file
37
dom/heapsnapshot/tests/mochitest/test_DominatorTree_01.html
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Sanity test that we can compute dominator trees from a heap snapshot in a web window.
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>ChromeUtils.saveHeapSnapshot test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
<body>
|
||||
<pre id="test">
|
||||
<script>
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
window.onload = function() {
|
||||
const path = ChromeUtils.saveHeapSnapshot({ runtime: true });
|
||||
const snapshot = ChromeUtils.readHeapSnapshot(path);
|
||||
|
||||
const dominatorTree = snapshot.computeDominatorTree();
|
||||
ok(dominatorTree);
|
||||
ok(dominatorTree instanceof DominatorTree);
|
||||
|
||||
let threw = false;
|
||||
try {
|
||||
new DominatorTree();
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
}
|
||||
ok(threw, "Constructor shouldn't be usable");
|
||||
|
||||
SimpleTest.finish();
|
||||
};
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
25
dom/heapsnapshot/tests/mochitest/test_SaveHeapSnapshot.html
Normal file
25
dom/heapsnapshot/tests/mochitest/test_SaveHeapSnapshot.html
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Bug 1024774 - Sanity test that we can take a heap snapshot in a web window.
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>ChromeUtils.saveHeapSnapshot test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
<body>
|
||||
<pre id="test">
|
||||
<script>
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
window.onload = function() {
|
||||
ok(ChromeUtils, "The ChromeUtils interface should be exposed in chrome windows.");
|
||||
ChromeUtils.saveHeapSnapshot({ runtime: true });
|
||||
ok(true, "Should save a heap snapshot and shouldn't throw.");
|
||||
SimpleTest.finish();
|
||||
};
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
<!DOCTYPE HTML>
|
||||
<!--
|
||||
Bug 1201597 - Sanity test that we can take a heap snapshot in an e10s child process.
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<title>saveHeapSnapshot in e10s child processes</title>
|
||||
<script type="application/javascript"
|
||||
src="/tests/SimpleTest/SimpleTest.js">
|
||||
</script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<script type="application/javascript">
|
||||
window.onerror = function (msg, url, line, col, err) {
|
||||
ok(false, "@" + url + ":" + line + ":" + col + ": " + msg + "\n" + err.stack);
|
||||
};
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
var childFrameURL = "data:text/html,<!DOCTYPE HTML><html><body></body></html>";
|
||||
|
||||
// This function is stringified and loaded in the child process as a frame
|
||||
// script.
|
||||
function childFrameScript() {
|
||||
try {
|
||||
ChromeUtils.saveHeapSnapshot({ runtime: true });
|
||||
} catch (err) {
|
||||
sendAsyncMessage("testSaveHeapSnapshot:error",
|
||||
{ error: err.toString() });
|
||||
return;
|
||||
}
|
||||
|
||||
sendAsyncMessage("testSaveHeapSnapshot:done", {});
|
||||
}
|
||||
|
||||
// Kick everything off on load.
|
||||
window.onload = function () {
|
||||
info("window.onload fired");
|
||||
SpecialPowers.addPermission("browser", true, document);
|
||||
SpecialPowers.pushPrefEnv({
|
||||
"set": [
|
||||
["dom.ipc.browser_frames.oop_by_default", true],
|
||||
["dom.mozBrowserFramesEnabled", true],
|
||||
["browser.pagethumbnails.capturing_disabled", true]
|
||||
]
|
||||
}, function () {
|
||||
var iframe = document.createElement("iframe");
|
||||
SpecialPowers.wrap(iframe).mozbrowser = true;
|
||||
iframe.id = "iframe";
|
||||
iframe.src = childFrameURL;
|
||||
|
||||
|
||||
iframe.addEventListener("mozbrowserloadend", function onLoadEnd() {
|
||||
iframe.removeEventListener("mozbrowserloadend", onLoadEnd);
|
||||
info("iframe done loading");
|
||||
|
||||
var mm = SpecialPowers.getBrowserFrameMessageManager(iframe);
|
||||
|
||||
function onError(e) {
|
||||
ok(false, e.data.error);
|
||||
}
|
||||
mm.addMessageListener("testSaveHeapSnapshot:error", onError);
|
||||
|
||||
mm.addMessageListener("testSaveHeapSnapshot:done", function onMsg() {
|
||||
mm.removeMessageListener("testSaveHeapSnapshot:done", onMsg);
|
||||
mm.removeMessageListener("testSaveHeapSnapshot:error", onError);
|
||||
ok(true, "Saved heap snapshot in child process");
|
||||
SimpleTest.finish();
|
||||
});
|
||||
|
||||
info("Loading frame script to save heap snapshot");
|
||||
mm.loadFrameScript("data:,(" + encodeURI(childFrameScript.toString()) + ")();",
|
||||
false);
|
||||
});
|
||||
|
||||
info("Loading iframe");
|
||||
document.body.appendChild(iframe);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</window>
|
||||
6
dom/heapsnapshot/tests/unit/.eslintrc.js
Normal file
6
dom/heapsnapshot/tests/unit/.eslintrc.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
// Extend from the common devtools xpcshell eslintrc config.
|
||||
"extends": "../../../../.eslintrc.xpcshell.js"
|
||||
};
|
||||
165
dom/heapsnapshot/tests/unit/Census.jsm
Normal file
165
dom/heapsnapshot/tests/unit/Census.jsm
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
// Functions for checking results returned by
|
||||
// Debugger.Memory.prototype.takeCensus and
|
||||
// HeapSnapshot.prototype.takeCensus. Adapted from js/src/jit-test/lib/census.js.
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["Census"];
|
||||
|
||||
this.Census = (function () {
|
||||
const Census = {};
|
||||
|
||||
function dumpn(msg) {
|
||||
dump("DBG-TEST: Census.jsm: " + msg + "\n");
|
||||
}
|
||||
|
||||
// Census.walkCensus(subject, name, walker)
|
||||
//
|
||||
// Use |walker| to check |subject|, a census object of the sort returned by
|
||||
// Debugger.Memory.prototype.takeCensus: a tree of objects with integers at the
|
||||
// leaves. Use |name| as the name for |subject| in diagnostic messages. Return
|
||||
// the number of leaves of |subject| we visited.
|
||||
//
|
||||
// A walker is an object with three methods:
|
||||
//
|
||||
// - enter(prop): Return the walker we should use to check the property of the
|
||||
// subject census named |prop|. This is for recursing into the subobjects of
|
||||
// the subject.
|
||||
//
|
||||
// - done(): Called after we have called 'enter' on every property of the
|
||||
// subject.
|
||||
//
|
||||
// - check(value): Check |value|, a leaf in the subject.
|
||||
//
|
||||
// Walker methods are expected to simply throw if a node we visit doesn't look
|
||||
// right.
|
||||
Census.walkCensus = (subject, name, walker) => walk(subject, name, walker, 0);
|
||||
function walk(subject, name, walker, count) {
|
||||
if (typeof subject === "object") {
|
||||
dumpn(name);
|
||||
for (let prop in subject) {
|
||||
count = walk(subject[prop],
|
||||
name + "[" + uneval(prop) + "]",
|
||||
walker.enter(prop),
|
||||
count);
|
||||
}
|
||||
walker.done();
|
||||
} else {
|
||||
dumpn(name + " = " + uneval(subject));
|
||||
walker.check(subject);
|
||||
count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
// A walker that doesn't check anything.
|
||||
Census.walkAnything = {
|
||||
enter: () => Census.walkAnything,
|
||||
done: () => undefined,
|
||||
check: () => undefined
|
||||
};
|
||||
|
||||
// A walker that requires all leaves to be zeros.
|
||||
Census.assertAllZeros = {
|
||||
enter: () => Census.assertAllZeros,
|
||||
done: () => undefined,
|
||||
check: elt => { if (elt !== 0) throw new Error("Census mismatch: expected zero, found " + elt); }
|
||||
};
|
||||
|
||||
function expectedObject() {
|
||||
throw new Error("Census mismatch: subject has leaf where basis has nested object");
|
||||
}
|
||||
|
||||
function expectedLeaf() {
|
||||
throw new Error("Census mismatch: subject has nested object where basis has leaf");
|
||||
}
|
||||
|
||||
// Return a function that, given a 'basis' census, returns a census walker that
|
||||
// compares the subject census against the basis. The returned walker calls the
|
||||
// given |compare|, |missing|, and |extra| functions as follows:
|
||||
//
|
||||
// - compare(subjectLeaf, basisLeaf): Check a leaf of the subject against the
|
||||
// corresponding leaf of the basis.
|
||||
//
|
||||
// - missing(prop, value): Called when the subject is missing a property named
|
||||
// |prop| which is present in the basis with value |value|.
|
||||
//
|
||||
// - extra(prop): Called when the subject has a property named |prop|, but the
|
||||
// basis has no such property. This should return a walker that can check
|
||||
// the subject's value.
|
||||
function makeBasisChecker({compare, missing, extra}) {
|
||||
return function makeWalker(basis) {
|
||||
if (typeof basis === "object") {
|
||||
var unvisited = new Set(Object.getOwnPropertyNames(basis));
|
||||
return {
|
||||
enter: prop => {
|
||||
unvisited.delete(prop);
|
||||
if (prop in basis) {
|
||||
return makeWalker(basis[prop]);
|
||||
} else {
|
||||
return extra(prop);
|
||||
}
|
||||
},
|
||||
|
||||
done: () => unvisited.forEach(prop => missing(prop, basis[prop])),
|
||||
check: expectedObject
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
enter: expectedLeaf,
|
||||
done: expectedLeaf,
|
||||
check: elt => compare(elt, basis)
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function missingProp(prop) {
|
||||
throw new Error("Census mismatch: subject lacks property present in basis: " + prop);
|
||||
}
|
||||
|
||||
function extraProp(prop) {
|
||||
throw new Error("Census mismatch: subject has property not present in basis: " + prop);
|
||||
}
|
||||
|
||||
// Return a walker that checks that the subject census has counts all equal to
|
||||
// |basis|.
|
||||
Census.assertAllEqual = makeBasisChecker({
|
||||
compare: (a, b) => { if (a !== b) throw new Error("Census mismatch: expected " + a + " got " + b);},
|
||||
missing: missingProp,
|
||||
extra: extraProp
|
||||
});
|
||||
|
||||
function ok(val) {
|
||||
if (!val) {
|
||||
throw new Error("Census mismatch: expected truthy, got " + val);
|
||||
}
|
||||
}
|
||||
|
||||
// Return a walker that checks that the subject census has at least as many
|
||||
// items of each category as |basis|.
|
||||
Census.assertAllNotLessThan = makeBasisChecker({
|
||||
compare: (subject, basis) => ok(subject >= basis),
|
||||
missing: missingProp,
|
||||
extra: () => Census.walkAnything
|
||||
});
|
||||
|
||||
// Return a walker that checks that the subject census has at most as many
|
||||
// items of each category as |basis|.
|
||||
Census.assertAllNotMoreThan = makeBasisChecker({
|
||||
compare: (subject, basis) => ok(subject <= basis),
|
||||
missing: missingProp,
|
||||
extra: () => Census.walkAnything
|
||||
});
|
||||
|
||||
// Return a walker that checks that the subject census has within |fudge|
|
||||
// items of each category of the count in |basis|.
|
||||
Census.assertAllWithin = function (fudge, basis) {
|
||||
return makeBasisChecker({
|
||||
compare: (subject, basis) => ok(Math.abs(subject - basis) <= fudge),
|
||||
missing: missingProp,
|
||||
extra: () => Census.walkAnything
|
||||
})(basis);
|
||||
};
|
||||
|
||||
return Census;
|
||||
}());
|
||||
190
dom/heapsnapshot/tests/unit/Match.jsm
Normal file
190
dom/heapsnapshot/tests/unit/Match.jsm
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
// A little pattern-matching library.
|
||||
//
|
||||
// Ported from js/src/tests/js1_8_5/reflect-parse/Match.js for use with devtools
|
||||
// server xpcshell tests.
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["Match"];
|
||||
|
||||
this.Match = (function() {
|
||||
|
||||
function Pattern(template) {
|
||||
// act like a constructor even as a function
|
||||
if (!(this instanceof Pattern))
|
||||
return new Pattern(template);
|
||||
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
Pattern.prototype = {
|
||||
match: function(act) {
|
||||
return match(act, this.template);
|
||||
},
|
||||
|
||||
matches: function(act) {
|
||||
try {
|
||||
return this.match(act);
|
||||
}
|
||||
catch (e if e instanceof MatchError) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
assert: function(act, message) {
|
||||
try {
|
||||
return this.match(act);
|
||||
}
|
||||
catch (e if e instanceof MatchError) {
|
||||
throw new Error((message || "failed match") + ": " + e.message);
|
||||
}
|
||||
},
|
||||
|
||||
toString: () => "[object Pattern]"
|
||||
};
|
||||
|
||||
Pattern.ANY = new Pattern;
|
||||
Pattern.ANY.template = Pattern.ANY;
|
||||
|
||||
Pattern.NUMBER = new Pattern;
|
||||
Pattern.NUMBER.match = function (act) {
|
||||
if (typeof act !== 'number') {
|
||||
throw new MatchError("Expected number, got: " + quote(act));
|
||||
}
|
||||
}
|
||||
|
||||
Pattern.NATURAL = new Pattern
|
||||
Pattern.NATURAL.match = function (act) {
|
||||
if (typeof act !== 'number' || act !== Math.floor(act) || act < 0) {
|
||||
throw new MatchError("Expected natural number, got: " + quote(act));
|
||||
}
|
||||
}
|
||||
|
||||
var quote = uneval;
|
||||
|
||||
function MatchError(msg) {
|
||||
this.message = msg;
|
||||
}
|
||||
|
||||
MatchError.prototype = {
|
||||
toString: function() {
|
||||
return "match error: " + this.message;
|
||||
}
|
||||
};
|
||||
|
||||
function isAtom(x) {
|
||||
return (typeof x === "number") ||
|
||||
(typeof x === "string") ||
|
||||
(typeof x === "boolean") ||
|
||||
(x === null) ||
|
||||
(typeof x === "object" && x instanceof RegExp);
|
||||
}
|
||||
|
||||
function isObject(x) {
|
||||
return (x !== null) && (typeof x === "object");
|
||||
}
|
||||
|
||||
function isFunction(x) {
|
||||
return typeof x === "function";
|
||||
}
|
||||
|
||||
function isArrayLike(x) {
|
||||
return isObject(x) && ("length" in x);
|
||||
}
|
||||
|
||||
function matchAtom(act, exp) {
|
||||
if ((typeof exp) === "number" && isNaN(exp)) {
|
||||
if ((typeof act) !== "number" || !isNaN(act))
|
||||
throw new MatchError("expected NaN, got: " + quote(act));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (exp === null) {
|
||||
if (act !== null)
|
||||
throw new MatchError("expected null, got: " + quote(act));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (exp instanceof RegExp) {
|
||||
if (!(act instanceof RegExp) || exp.source !== act.source)
|
||||
throw new MatchError("expected " + quote(exp) + ", got: " + quote(act));
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (typeof exp) {
|
||||
case "string":
|
||||
if (act !== exp)
|
||||
throw new MatchError("expected " + quote(exp) + ", got " + quote(act));
|
||||
return true;
|
||||
case "boolean":
|
||||
case "number":
|
||||
if (exp !== act)
|
||||
throw new MatchError("expected " + exp + ", got " + quote(act));
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new Error("bad pattern: " + exp.toSource());
|
||||
}
|
||||
|
||||
function matchObject(act, exp) {
|
||||
if (!isObject(act))
|
||||
throw new MatchError("expected object, got " + quote(act));
|
||||
|
||||
for (var key in exp) {
|
||||
if (!(key in act))
|
||||
throw new MatchError("expected property " + quote(key) + " not found in " + quote(act));
|
||||
match(act[key], exp[key]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function matchFunction(act, exp) {
|
||||
if (!isFunction(act))
|
||||
throw new MatchError("expected function, got " + quote(act));
|
||||
|
||||
if (act !== exp)
|
||||
throw new MatchError("expected function: " + exp +
|
||||
"\nbut got different function: " + act);
|
||||
}
|
||||
|
||||
function matchArray(act, exp) {
|
||||
if (!isObject(act) || !("length" in act))
|
||||
throw new MatchError("expected array-like object, got " + quote(act));
|
||||
|
||||
var length = exp.length;
|
||||
if (act.length !== exp.length)
|
||||
throw new MatchError("expected array-like object of length " + length + ", got " + quote(act));
|
||||
|
||||
for (var i = 0; i < length; i++) {
|
||||
if (i in exp) {
|
||||
if (!(i in act))
|
||||
throw new MatchError("expected array property " + i + " not found in " + quote(act));
|
||||
match(act[i], exp[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function match(act, exp) {
|
||||
if (exp === Pattern.ANY)
|
||||
return true;
|
||||
|
||||
if (exp instanceof Pattern)
|
||||
return exp.match(act);
|
||||
|
||||
if (isAtom(exp))
|
||||
return matchAtom(act, exp);
|
||||
|
||||
if (isArrayLike(exp))
|
||||
return matchArray(act, exp);
|
||||
|
||||
if (isFunction(exp))
|
||||
return matchFunction(act, exp);
|
||||
|
||||
return matchObject(act, exp);
|
||||
}
|
||||
|
||||
return { Pattern: Pattern,
|
||||
MatchError: MatchError };
|
||||
|
||||
})();
|
||||
47
dom/heapsnapshot/tests/unit/dominator-tree-worker.js
Normal file
47
dom/heapsnapshot/tests/unit/dominator-tree-worker.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
console.log("Initializing worker.");
|
||||
|
||||
self.onmessage = e => {
|
||||
console.log("Starting test.");
|
||||
try {
|
||||
const path = ThreadSafeChromeUtils.saveHeapSnapshot({ runtime: true });
|
||||
const snapshot = ThreadSafeChromeUtils.readHeapSnapshot(path);
|
||||
|
||||
const dominatorTree = snapshot.computeDominatorTree();
|
||||
ok(dominatorTree);
|
||||
ok(dominatorTree instanceof DominatorTree);
|
||||
|
||||
let threw = false;
|
||||
try {
|
||||
new DominatorTree();
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
}
|
||||
ok(threw, "Constructor shouldn't be usable");
|
||||
} catch (e) {
|
||||
ok(false, "Unexpected error inside worker:\n" + e.toString() + "\n" + e.stack);
|
||||
} finally {
|
||||
done();
|
||||
}
|
||||
};
|
||||
|
||||
// Proxy assertions to the main thread.
|
||||
function ok(val, msg) {
|
||||
console.log("ok(" + !!val + ", \"" + msg + "\")");
|
||||
self.postMessage({
|
||||
type: "assertion",
|
||||
passed: !!val,
|
||||
msg,
|
||||
stack: Error().stack
|
||||
});
|
||||
}
|
||||
|
||||
// Tell the main thread we are done with the tests.
|
||||
function done() {
|
||||
console.log("done()");
|
||||
self.postMessage({
|
||||
type: "done"
|
||||
});
|
||||
}
|
||||
448
dom/heapsnapshot/tests/unit/head_heapsnapshot.js
Normal file
448
dom/heapsnapshot/tests/unit/head_heapsnapshot.js
Normal file
|
|
@ -0,0 +1,448 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
var Cc = Components.classes;
|
||||
var Ci = Components.interfaces;
|
||||
var Cu = Components.utils;
|
||||
var Cr = Components.results;
|
||||
var CC = Components.Constructor;
|
||||
|
||||
const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
|
||||
const { Match } = Cu.import("resource://test/Match.jsm", {});
|
||||
const { Census } = Cu.import("resource://test/Census.jsm", {});
|
||||
const { addDebuggerToGlobal } =
|
||||
Cu.import("resource://gre/modules/jsdebugger.jsm", {});
|
||||
const { Task } = require("devtools/shared/task");
|
||||
|
||||
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
|
||||
const flags = require("devtools/shared/flags");
|
||||
const HeapAnalysesClient =
|
||||
require("devtools/shared/heapsnapshot/HeapAnalysesClient");
|
||||
const Services = require("Services");
|
||||
const { censusReportToCensusTreeNode } = require("devtools/shared/heapsnapshot/census-tree-node");
|
||||
const CensusUtils = require("devtools/shared/heapsnapshot/CensusUtils");
|
||||
const DominatorTreeNode = require("devtools/shared/heapsnapshot/DominatorTreeNode");
|
||||
const { deduplicatePaths } = require("devtools/shared/heapsnapshot/shortest-paths");
|
||||
const { LabelAndShallowSizeVisitor } = DominatorTreeNode;
|
||||
|
||||
|
||||
// Always log packets when running tests. runxpcshelltests.py will throw
|
||||
// the output away anyway, unless you give it the --verbose flag.
|
||||
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) {
|
||||
Services.prefs.setBoolPref("devtools.debugger.log", true);
|
||||
}
|
||||
flags.wantLogging = true;
|
||||
|
||||
const SYSTEM_PRINCIPAL = Cc["@mozilla.org/systemprincipal;1"]
|
||||
.createInstance(Ci.nsIPrincipal);
|
||||
|
||||
function dumpn(msg) {
|
||||
dump("HEAPSNAPSHOT-TEST: " + msg + "\n");
|
||||
}
|
||||
|
||||
function addTestingFunctionsToGlobal(global) {
|
||||
global.eval(
|
||||
`
|
||||
const testingFunctions = Components.utils.getJSTestingFunctions();
|
||||
for (let k in testingFunctions) {
|
||||
this[k] = testingFunctions[k];
|
||||
}
|
||||
`
|
||||
);
|
||||
if (!global.print) {
|
||||
global.print = do_print;
|
||||
}
|
||||
if (!global.newGlobal) {
|
||||
global.newGlobal = newGlobal;
|
||||
}
|
||||
if (!global.Debugger) {
|
||||
addDebuggerToGlobal(global);
|
||||
}
|
||||
}
|
||||
|
||||
addTestingFunctionsToGlobal(this);
|
||||
|
||||
/**
|
||||
* Create a new global, with all the JS shell testing functions. Similar to the
|
||||
* newGlobal function exposed to JS shells, and useful for porting JS shell
|
||||
* tests to xpcshell tests.
|
||||
*/
|
||||
function newGlobal() {
|
||||
const global = new Cu.Sandbox(SYSTEM_PRINCIPAL, { freshZone: true });
|
||||
addTestingFunctionsToGlobal(global);
|
||||
return global;
|
||||
}
|
||||
|
||||
function assertThrowsValue(f, val, msg) {
|
||||
var fullmsg;
|
||||
try {
|
||||
f();
|
||||
} catch (exc) {
|
||||
if ((exc === val) === (val === val) && (val !== 0 || 1 / exc === 1 / val))
|
||||
return;
|
||||
fullmsg = "Assertion failed: expected exception " + val + ", got " + exc;
|
||||
}
|
||||
if (fullmsg === undefined)
|
||||
fullmsg = "Assertion failed: expected exception " + val + ", no exception thrown";
|
||||
if (msg !== undefined)
|
||||
fullmsg += " - " + msg;
|
||||
throw new Error(fullmsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full path of the file with the specified name in a
|
||||
* platform-independent and URL-like form.
|
||||
*/
|
||||
function getFilePath(aName, aAllowMissing = false, aUsePlatformPathSeparator = false)
|
||||
{
|
||||
let file = do_get_file(aName, aAllowMissing);
|
||||
let path = Services.io.newFileURI(file).spec;
|
||||
let filePrePath = "file://";
|
||||
if ("nsILocalFileWin" in Ci &&
|
||||
file instanceof Ci.nsILocalFileWin) {
|
||||
filePrePath += "/";
|
||||
}
|
||||
|
||||
path = path.slice(filePrePath.length);
|
||||
|
||||
if (aUsePlatformPathSeparator && path.match(/^\w:/)) {
|
||||
path = path.replace(/\//g, "\\");
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
function saveNewHeapSnapshot(opts = { runtime: true }) {
|
||||
const filePath = ChromeUtils.saveHeapSnapshot(opts);
|
||||
ok(filePath, "Should get a file path to save the core dump to.");
|
||||
ok(true, "Saved a heap snapshot to " + filePath);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function readHeapSnapshot(filePath) {
|
||||
const snapshot = ChromeUtils.readHeapSnapshot(filePath);
|
||||
ok(snapshot, "Should have read a heap snapshot back from " + filePath);
|
||||
ok(snapshot instanceof HeapSnapshot, "snapshot should be an instance of HeapSnapshot");
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a heap snapshot to the file with the given name in the current
|
||||
* directory, read it back as a HeapSnapshot instance, and then take a census of
|
||||
* the heap snapshot's serialized heap graph with the provided census options.
|
||||
*
|
||||
* @param {Object|undefined} censusOptions
|
||||
* Options that should be passed through to the takeCensus method. See
|
||||
* js/src/doc/Debugger/Debugger.Memory.md for details.
|
||||
*
|
||||
* @param {Debugger|null} dbg
|
||||
* If a Debugger object is given, only serialize the subgraph covered by
|
||||
* the Debugger's debuggees. If null, serialize the whole heap graph.
|
||||
*
|
||||
* @param {String} fileName
|
||||
* The file name to save the heap snapshot's core dump file to, within
|
||||
* the current directory.
|
||||
*
|
||||
* @returns Census
|
||||
*/
|
||||
function saveHeapSnapshotAndTakeCensus(dbg = null, censusOptions = undefined) {
|
||||
const snapshotOptions = dbg ? { debugger: dbg } : { runtime: true };
|
||||
const filePath = saveNewHeapSnapshot(snapshotOptions);
|
||||
const snapshot = readHeapSnapshot(filePath);
|
||||
|
||||
equal(typeof snapshot.takeCensus, "function", "snapshot should have a takeCensus method");
|
||||
|
||||
return snapshot.takeCensus(censusOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a heap snapshot to disk, read it back as a HeapSnapshot instance, and
|
||||
* then compute its dominator tree.
|
||||
*
|
||||
* @param {Debugger|null} dbg
|
||||
* If a Debugger object is given, only serialize the subgraph covered by
|
||||
* the Debugger's debuggees. If null, serialize the whole heap graph.
|
||||
*
|
||||
* @returns {DominatorTree}
|
||||
*/
|
||||
function saveHeapSnapshotAndComputeDominatorTree(dbg = null) {
|
||||
const snapshotOptions = dbg ? { debugger: dbg } : { runtime: true };
|
||||
const filePath = saveNewHeapSnapshot(snapshotOptions);
|
||||
const snapshot = readHeapSnapshot(filePath);
|
||||
|
||||
equal(typeof snapshot.computeDominatorTree, "function",
|
||||
"snapshot should have a `computeDominatorTree` method");
|
||||
|
||||
const dominatorTree = snapshot.computeDominatorTree();
|
||||
|
||||
ok(dominatorTree, "Should be able to compute a dominator tree");
|
||||
ok(dominatorTree instanceof DominatorTree, "Should be an instance of DominatorTree");
|
||||
|
||||
return dominatorTree;
|
||||
}
|
||||
|
||||
function isSavedFrame(obj) {
|
||||
return Object.prototype.toString.call(obj) === "[object SavedFrame]";
|
||||
}
|
||||
|
||||
function savedFrameReplacer(key, val) {
|
||||
if (isSavedFrame(val)) {
|
||||
return `<SavedFrame '${val.toString().split(/\n/g).shift()}'>`;
|
||||
} else {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that creating a CensusTreeNode from the given `report` with the
|
||||
* specified `breakdown` creates the given `expected` CensusTreeNode.
|
||||
*
|
||||
* @param {Object} breakdown
|
||||
* The census breakdown.
|
||||
*
|
||||
* @param {Object} report
|
||||
* The census report.
|
||||
*
|
||||
* @param {Object} expected
|
||||
* The expected CensusTreeNode result.
|
||||
*
|
||||
* @param {Object} options
|
||||
* The options to pass through to `censusReportToCensusTreeNode`.
|
||||
*/
|
||||
function compareCensusViewData(breakdown, report, expected, options) {
|
||||
dumpn("Generating CensusTreeNode from report:");
|
||||
dumpn("breakdown: " + JSON.stringify(breakdown, null, 4));
|
||||
dumpn("report: " + JSON.stringify(report, null, 4));
|
||||
dumpn("expected: " + JSON.stringify(expected, savedFrameReplacer, 4));
|
||||
|
||||
const actual = censusReportToCensusTreeNode(breakdown, report, options);
|
||||
dumpn("actual: " + JSON.stringify(actual, savedFrameReplacer, 4));
|
||||
|
||||
assertStructurallyEquivalent(actual, expected);
|
||||
}
|
||||
|
||||
// Deep structural equivalence that can handle Map objects in addition to plain
|
||||
// objects.
|
||||
function assertStructurallyEquivalent(actual, expected, path = "root") {
|
||||
if (actual === expected) {
|
||||
equal(actual, expected, "actual and expected are the same");
|
||||
return;
|
||||
}
|
||||
|
||||
equal(typeof actual, typeof expected, `${path}: typeof should be the same`);
|
||||
|
||||
if (actual && typeof actual === "object") {
|
||||
const actualProtoString = Object.prototype.toString.call(actual);
|
||||
const expectedProtoString = Object.prototype.toString.call(expected);
|
||||
equal(actualProtoString, expectedProtoString,
|
||||
`${path}: Object.prototype.toString.call() should be the same`);
|
||||
|
||||
if (actualProtoString === "[object Map]") {
|
||||
const expectedKeys = new Set([...expected.keys()]);
|
||||
|
||||
for (let key of actual.keys()) {
|
||||
ok(expectedKeys.has(key),
|
||||
`${path}: every key in actual should exist in expected: ${String(key).slice(0, 10)}`);
|
||||
expectedKeys.delete(key);
|
||||
|
||||
assertStructurallyEquivalent(actual.get(key), expected.get(key),
|
||||
path + ".get(" + String(key).slice(0, 20) + ")");
|
||||
}
|
||||
|
||||
equal(expectedKeys.size, 0,
|
||||
`${path}: every key in expected should also exist in actual, did not see ${[...expectedKeys]}`);
|
||||
} else if (actualProtoString === "[object Set]") {
|
||||
const expectedItems = new Set([...expected]);
|
||||
|
||||
for (let item of actual) {
|
||||
ok(expectedItems.has(item),
|
||||
`${path}: every set item in actual should exist in expected: ${item}`);
|
||||
expectedItems.delete(item);
|
||||
}
|
||||
|
||||
equal(expectedItems.size, 0,
|
||||
`${path}: every set item in expected should also exist in actual, did not see ${[...expectedItems]}`);
|
||||
} else {
|
||||
const expectedKeys = new Set(Object.keys(expected));
|
||||
|
||||
for (let key of Object.keys(actual)) {
|
||||
ok(expectedKeys.has(key),
|
||||
`${path}: every key in actual should exist in expected: ${key}`);
|
||||
expectedKeys.delete(key);
|
||||
|
||||
assertStructurallyEquivalent(actual[key], expected[key], path + "." + key);
|
||||
}
|
||||
|
||||
equal(expectedKeys.size, 0,
|
||||
`${path}: every key in expected should also exist in actual, did not see ${[...expectedKeys]}`);
|
||||
}
|
||||
} else {
|
||||
equal(actual, expected, `${path}: primitives should be equal`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that creating a diff of the `first` and `second` census reports
|
||||
* creates the `expected` delta-report.
|
||||
*
|
||||
* @param {Object} breakdown
|
||||
* The census breakdown.
|
||||
*
|
||||
* @param {Object} first
|
||||
* The first census report.
|
||||
*
|
||||
* @param {Object} second
|
||||
* The second census report.
|
||||
*
|
||||
* @param {Object} expected
|
||||
* The expected delta-report.
|
||||
*/
|
||||
function assertDiff(breakdown, first, second, expected) {
|
||||
dumpn("Diffing census reports:");
|
||||
dumpn("Breakdown: " + JSON.stringify(breakdown, null, 4));
|
||||
dumpn("First census report: " + JSON.stringify(first, null, 4));
|
||||
dumpn("Second census report: " + JSON.stringify(second, null, 4));
|
||||
dumpn("Expected delta-report: " + JSON.stringify(expected, null, 4));
|
||||
|
||||
const actual = CensusUtils.diff(breakdown, first, second);
|
||||
dumpn("Actual delta-report: " + JSON.stringify(actual, null, 4));
|
||||
|
||||
assertStructurallyEquivalent(actual, expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that creating a label and getting a shallow size from the given node
|
||||
* description with the specified breakdown is as expected.
|
||||
*
|
||||
* @param {Object} breakdown
|
||||
* @param {Object} givenDescription
|
||||
* @param {Number} expectedShallowSize
|
||||
* @param {Object} expectedLabel
|
||||
*/
|
||||
function assertLabelAndShallowSize(breakdown, givenDescription, expectedShallowSize, expectedLabel) {
|
||||
dumpn("Computing label and shallow size from node description:");
|
||||
dumpn("Breakdown: " + JSON.stringify(breakdown, null, 4));
|
||||
dumpn("Given description: " + JSON.stringify(givenDescription, null, 4));
|
||||
|
||||
const visitor = new LabelAndShallowSizeVisitor();
|
||||
CensusUtils.walk(breakdown, description, visitor);
|
||||
|
||||
dumpn("Expected shallow size: " + expectedShallowSize);
|
||||
dumpn("Actual shallow size: " + visitor.shallowSize());
|
||||
equal(visitor.shallowSize(), expectedShallowSize, "Shallow size should be correct");
|
||||
|
||||
dumpn("Expected label: " + JSON.stringify(expectedLabel, null, 4));
|
||||
dumpn("Actual label: " + JSON.stringify(visitor.label(), null, 4));
|
||||
assertStructurallyEquivalent(visitor.label(), expectedLabel);
|
||||
}
|
||||
|
||||
// Counter for mock DominatorTreeNode ids.
|
||||
let TEST_NODE_ID_COUNTER = 0;
|
||||
|
||||
/**
|
||||
* Create a mock DominatorTreeNode for testing, with sane defaults. Override any
|
||||
* property by providing it on `opts`. Optionally pass child nodes as well.
|
||||
*
|
||||
* @param {Object} opts
|
||||
* @param {Array<DominatorTreeNode>?} children
|
||||
*
|
||||
* @returns {DominatorTreeNode}
|
||||
*/
|
||||
function makeTestDominatorTreeNode(opts, children) {
|
||||
const nodeId = TEST_NODE_ID_COUNTER++;
|
||||
|
||||
const node = Object.assign({
|
||||
nodeId,
|
||||
label: undefined,
|
||||
shallowSize: 1,
|
||||
retainedSize: (children || []).reduce((size, c) => size + c.retainedSize, 1),
|
||||
parentId: undefined,
|
||||
children,
|
||||
moreChildrenAvailable: true,
|
||||
}, opts);
|
||||
|
||||
if (children && children.length) {
|
||||
children.map(c => c.parentId = node.nodeId);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert `newChildren` into the given dominator `tree` as specified by the
|
||||
* `path` from the root to the node the `newChildren` should be inserted
|
||||
* beneath. Assert that the resulting tree matches `expected`.
|
||||
*/
|
||||
function assertDominatorTreeNodeInsertion(tree, path, newChildren, moreChildrenAvailable, expected) {
|
||||
dumpn("Inserting new children into a dominator tree:");
|
||||
dumpn("Dominator tree: " + JSON.stringify(tree, null, 2));
|
||||
dumpn("Path: " + JSON.stringify(path, null, 2));
|
||||
dumpn("New children: " + JSON.stringify(newChildren, null, 2));
|
||||
dumpn("Expected resulting tree: " + JSON.stringify(expected, null, 2));
|
||||
|
||||
const actual = DominatorTreeNode.insert(tree, path, newChildren, moreChildrenAvailable);
|
||||
dumpn("Actual resulting tree: " + JSON.stringify(actual, null, 2));
|
||||
|
||||
assertStructurallyEquivalent(actual, expected);
|
||||
}
|
||||
|
||||
function assertDeduplicatedPaths({ target, paths, expectedNodes, expectedEdges }) {
|
||||
dumpn("Deduplicating paths:");
|
||||
dumpn("target = " + target);
|
||||
dumpn("paths = " + JSON.stringify(paths, null, 2));
|
||||
dumpn("expectedNodes = " + expectedNodes);
|
||||
dumpn("expectedEdges = " + JSON.stringify(expectedEdges, null, 2));
|
||||
|
||||
const { nodes, edges } = deduplicatePaths(target, paths);
|
||||
|
||||
dumpn("Actual nodes = " + nodes);
|
||||
dumpn("Actual edges = " + JSON.stringify(edges, null, 2));
|
||||
|
||||
equal(nodes.length, expectedNodes.length,
|
||||
"actual number of nodes is equal to the expected number of nodes");
|
||||
|
||||
equal(edges.length, expectedEdges.length,
|
||||
"actual number of edges is equal to the expected number of edges");
|
||||
|
||||
const expectedNodeSet = new Set(expectedNodes);
|
||||
const nodeSet = new Set(nodes);
|
||||
ok(nodeSet.size === nodes.length,
|
||||
"each returned node should be unique");
|
||||
|
||||
for (let node of nodes) {
|
||||
ok(expectedNodeSet.has(node), `the ${node} node was expected`);
|
||||
}
|
||||
|
||||
for (let expectedEdge of expectedEdges) {
|
||||
let count = 0;
|
||||
for (let edge of edges) {
|
||||
if (edge.from === expectedEdge.from &&
|
||||
edge.to === expectedEdge.to &&
|
||||
edge.name === expectedEdge.name) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
equal(count, 1,
|
||||
"should have exactly one matching edge for the expected edge = " + JSON.stringify(edge));
|
||||
}
|
||||
}
|
||||
|
||||
function assertCountToBucketBreakdown(breakdown, expected) {
|
||||
dumpn("count => bucket breakdown");
|
||||
dumpn("Initial breakdown = ", JSON.stringify(breakdown, null, 2));
|
||||
dumpn("Expected results = ", JSON.stringify(expected, null, 2));
|
||||
|
||||
const actual = CensusUtils.countToBucketBreakdown(breakdown);
|
||||
dumpn("Actual results = ", JSON.stringify(actual, null, 2));
|
||||
|
||||
assertStructurallyEquivalent(actual, expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mock path entry for the given predecessor and edge.
|
||||
*/
|
||||
function pathEntry(predecessor, edge) {
|
||||
return { predecessor, edge };
|
||||
}
|
||||
46
dom/heapsnapshot/tests/unit/heap-snapshot-worker.js
Normal file
46
dom/heapsnapshot/tests/unit/heap-snapshot-worker.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
console.log("Initializing worker.");
|
||||
|
||||
self.onmessage = e => {
|
||||
console.log("Starting test.");
|
||||
try {
|
||||
ok(typeof ChromeUtils === "undefined",
|
||||
"Should not have access to ChromeUtils in a worker.");
|
||||
ok(ThreadSafeChromeUtils,
|
||||
"Should have access to ThreadSafeChromeUtils in a worker.");
|
||||
ok(HeapSnapshot,
|
||||
"Should have access to HeapSnapshot in a worker.");
|
||||
|
||||
const filePath = ThreadSafeChromeUtils.saveHeapSnapshot({ globals: [this] });
|
||||
ok(true, "Should be able to save a snapshot.");
|
||||
|
||||
const snapshot = ThreadSafeChromeUtils.readHeapSnapshot(filePath);
|
||||
ok(snapshot, "Should be able to read a heap snapshot");
|
||||
ok(snapshot instanceof HeapSnapshot, "Should be an instanceof HeapSnapshot");
|
||||
} catch (e) {
|
||||
ok(false, "Unexpected error inside worker:\n" + e.toString() + "\n" + e.stack);
|
||||
} finally {
|
||||
done();
|
||||
}
|
||||
};
|
||||
|
||||
// Proxy assertions to the main thread.
|
||||
function ok(val, msg) {
|
||||
console.log("ok(" + !!val + ", \"" + msg + "\")");
|
||||
self.postMessage({
|
||||
type: "assertion",
|
||||
passed: !!val,
|
||||
msg,
|
||||
stack: Error().stack
|
||||
});
|
||||
}
|
||||
|
||||
// Tell the main thread we are done with the tests.
|
||||
function done() {
|
||||
console.log("done()");
|
||||
self.postMessage({
|
||||
type: "done"
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that we can generate label structures from node description reports.
|
||||
|
||||
const breakdown = {
|
||||
by: "coarseType",
|
||||
objects: {
|
||||
by: "objectClass",
|
||||
then: { by: "count", count: true, bytes: true },
|
||||
other: { by: "count", count: true, bytes: true },
|
||||
},
|
||||
strings: {
|
||||
by: "internalType",
|
||||
then: { by: "count", count: true, bytes: true },
|
||||
},
|
||||
scripts: {
|
||||
by: "internalType",
|
||||
then: { by: "count", count: true, bytes: true },
|
||||
},
|
||||
other: {
|
||||
by: "internalType",
|
||||
then: { by: "count", count: true, bytes: true },
|
||||
},
|
||||
};
|
||||
|
||||
const description = {
|
||||
objects: {
|
||||
Function: { count: 1, bytes: 32 },
|
||||
other: { count: 0, bytes: 0 }
|
||||
},
|
||||
strings: {},
|
||||
scripts: {},
|
||||
other: {}
|
||||
};
|
||||
|
||||
const expected = [
|
||||
"objects",
|
||||
"Function"
|
||||
];
|
||||
|
||||
const shallowSize = 32;
|
||||
|
||||
function run_test() {
|
||||
assertLabelAndShallowSize(breakdown, description, shallowSize, expected);
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that we can generate label structures from node description reports.
|
||||
|
||||
const breakdown = {
|
||||
by: "coarseType",
|
||||
objects: {
|
||||
by: "objectClass",
|
||||
then: { by: "count", count: true, bytes: true },
|
||||
other: { by: "count", count: true, bytes: true },
|
||||
},
|
||||
strings: {
|
||||
by: "internalType",
|
||||
then: { by: "count", count: true, bytes: true },
|
||||
},
|
||||
scripts: {
|
||||
by: "internalType",
|
||||
then: { by: "count", count: true, bytes: true },
|
||||
},
|
||||
other: {
|
||||
by: "internalType",
|
||||
then: { by: "count", count: true, bytes: true },
|
||||
},
|
||||
};
|
||||
|
||||
const description = {
|
||||
objects: {
|
||||
other: { count: 1, bytes: 10 }
|
||||
},
|
||||
strings: {},
|
||||
scripts: {},
|
||||
other: {}
|
||||
};
|
||||
|
||||
const expected = [
|
||||
"objects",
|
||||
"other"
|
||||
];
|
||||
|
||||
const shallowSize = 10;
|
||||
|
||||
function run_test() {
|
||||
assertLabelAndShallowSize(breakdown, description, shallowSize, expected);
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that we can generate label structures from node description reports.
|
||||
|
||||
const breakdown = {
|
||||
by: "coarseType",
|
||||
objects: {
|
||||
by: "objectClass",
|
||||
then: { by: "count", count: true, bytes: true },
|
||||
other: { by: "count", count: true, bytes: true },
|
||||
},
|
||||
strings: {
|
||||
by: "internalType",
|
||||
then: { by: "count", count: true, bytes: true },
|
||||
},
|
||||
scripts: {
|
||||
by: "internalType",
|
||||
then: { by: "count", count: true, bytes: true },
|
||||
},
|
||||
other: {
|
||||
by: "internalType",
|
||||
then: { by: "count", count: true, bytes: true },
|
||||
},
|
||||
};
|
||||
|
||||
const description = {
|
||||
objects: {
|
||||
other: { count: 0, bytes: 0 }
|
||||
},
|
||||
strings: {
|
||||
"JSString": { count: 1, bytes: 42 },
|
||||
},
|
||||
scripts: {},
|
||||
other: {}
|
||||
};
|
||||
|
||||
const expected = [
|
||||
"strings",
|
||||
"JSString"
|
||||
];
|
||||
|
||||
const shallowSize = 42;
|
||||
|
||||
function run_test() {
|
||||
assertLabelAndShallowSize(breakdown, description, shallowSize, expected);
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that we can generate label structures from node description reports.
|
||||
|
||||
const breakdown = {
|
||||
by: "coarseType",
|
||||
objects: {
|
||||
by: "objectClass",
|
||||
then: {
|
||||
by: "allocationStack",
|
||||
then: { by: "count", count: true, bytes: true },
|
||||
noStack: { by: "count", count: true, bytes: true },
|
||||
},
|
||||
other: { by: "count", count: true, bytes: true },
|
||||
},
|
||||
strings: {
|
||||
by: "internalType",
|
||||
then: { by: "count", count: true, bytes: true },
|
||||
},
|
||||
scripts: {
|
||||
by: "internalType",
|
||||
then: { by: "count", count: true, bytes: true },
|
||||
},
|
||||
other: {
|
||||
by: "internalType",
|
||||
then: { by: "count", count: true, bytes: true },
|
||||
},
|
||||
};
|
||||
|
||||
const stack = saveStack();
|
||||
|
||||
const description = {
|
||||
objects: {
|
||||
Array: new Map([[stack, { count: 1, bytes: 512 }]]),
|
||||
other: { count: 0, bytes: 0 }
|
||||
},
|
||||
strings: {},
|
||||
scripts: {},
|
||||
other: {}
|
||||
};
|
||||
|
||||
const expected = [
|
||||
"objects",
|
||||
"Array",
|
||||
stack
|
||||
];
|
||||
|
||||
const shallowSize = 512;
|
||||
|
||||
function run_test() {
|
||||
assertLabelAndShallowSize(breakdown, description, shallowSize, expected);
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
// Test that the DominatorTreeNode.attachShortestPaths function can correctly
|
||||
// attach the deduplicated shortest retaining paths for each node it is given.
|
||||
|
||||
const startNodeId = 9999;
|
||||
const maxNumPaths = 2;
|
||||
|
||||
// Mock data mapping node id to shortest paths to that node id.
|
||||
const shortestPaths = new Map([
|
||||
[1000, [
|
||||
[pathEntry(1100, "a"), pathEntry(1200, "b")],
|
||||
[pathEntry(1100, "c"), pathEntry(1300, "d")],
|
||||
]],
|
||||
[2000, [
|
||||
[pathEntry(2100, "e"), pathEntry(2200, "f"), pathEntry(2300, "g")]
|
||||
]],
|
||||
[3000, [
|
||||
[pathEntry(3100, "h")],
|
||||
[pathEntry(3100, "i")],
|
||||
[pathEntry(3100, "j")],
|
||||
[pathEntry(3200, "k")],
|
||||
[pathEntry(3300, "l")],
|
||||
[pathEntry(3400, "m")],
|
||||
]],
|
||||
]);
|
||||
|
||||
const actual = [
|
||||
makeTestDominatorTreeNode({ nodeId: 1000 }),
|
||||
makeTestDominatorTreeNode({ nodeId: 2000 }),
|
||||
makeTestDominatorTreeNode({ nodeId: 3000 }),
|
||||
];
|
||||
|
||||
const expected = [
|
||||
makeTestDominatorTreeNode({
|
||||
nodeId: 1000,
|
||||
shortestPaths: {
|
||||
nodes: [
|
||||
{ id: 1000, label: ["SomeType-1000"] },
|
||||
{ id: 1100, label: ["SomeType-1100"] },
|
||||
{ id: 1200, label: ["SomeType-1200"] },
|
||||
{ id: 1300, label: ["SomeType-1300"] },
|
||||
],
|
||||
edges: [
|
||||
{ from: 1100, to: 1200, name: "a" },
|
||||
{ from: 1100, to: 1300, name: "c" },
|
||||
{ from: 1200, to: 1000, name: "b" },
|
||||
{ from: 1300, to: 1000, name: "d" },
|
||||
]
|
||||
}
|
||||
}),
|
||||
|
||||
makeTestDominatorTreeNode({
|
||||
nodeId: 2000,
|
||||
shortestPaths: {
|
||||
nodes: [
|
||||
{ id: 2000, label: ["SomeType-2000"] },
|
||||
{ id: 2100, label: ["SomeType-2100"] },
|
||||
{ id: 2200, label: ["SomeType-2200"] },
|
||||
{ id: 2300, label: ["SomeType-2300"] },
|
||||
],
|
||||
edges: [
|
||||
{ from: 2100, to: 2200, name: "e" },
|
||||
{ from: 2200, to: 2300, name: "f" },
|
||||
{ from: 2300, to: 2000, name: "g" },
|
||||
]
|
||||
}
|
||||
}),
|
||||
|
||||
makeTestDominatorTreeNode({ nodeId: 3000,
|
||||
shortestPaths: {
|
||||
nodes: [
|
||||
{ id: 3000, label: ["SomeType-3000"] },
|
||||
{ id: 3100, label: ["SomeType-3100"] },
|
||||
{ id: 3200, label: ["SomeType-3200"] },
|
||||
{ id: 3300, label: ["SomeType-3300"] },
|
||||
{ id: 3400, label: ["SomeType-3400"] },
|
||||
],
|
||||
edges: [
|
||||
{ from: 3100, to: 3000, name: "h" },
|
||||
{ from: 3100, to: 3000, name: "i" },
|
||||
{ from: 3100, to: 3000, name: "j" },
|
||||
{ from: 3200, to: 3000, name: "k" },
|
||||
{ from: 3300, to: 3000, name: "l" },
|
||||
{ from: 3400, to: 3000, name: "m" },
|
||||
]
|
||||
}
|
||||
}),
|
||||
];
|
||||
|
||||
const breakdown = {
|
||||
by: "internalType",
|
||||
then: { by: "count", count: true, bytes: true }
|
||||
};
|
||||
|
||||
const mockSnapshot = {
|
||||
computeShortestPaths: (start, nodeIds, max) => {
|
||||
equal(start, startNodeId);
|
||||
equal(max, maxNumPaths);
|
||||
|
||||
return new Map(nodeIds.map(nodeId => {
|
||||
const paths = shortestPaths.get(nodeId);
|
||||
ok(paths, "Expected computeShortestPaths call for node id = " + nodeId);
|
||||
return [nodeId, paths];
|
||||
}));
|
||||
},
|
||||
|
||||
describeNode: (bd, nodeId) => {
|
||||
equal(bd, breakdown);
|
||||
return {
|
||||
["SomeType-" + nodeId]: {
|
||||
count: 1,
|
||||
bytes: 10,
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
function run_test() {
|
||||
DominatorTreeNode.attachShortestPaths(mockSnapshot,
|
||||
breakdown,
|
||||
startNodeId,
|
||||
actual,
|
||||
maxNumPaths);
|
||||
|
||||
dumpn("Expected = " + JSON.stringify(expected, null, 2));
|
||||
dumpn("Actual = " + JSON.stringify(actual, null, 2));
|
||||
|
||||
assertStructurallyEquivalent(expected, actual);
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that we can find the node with the given id along the specified path.
|
||||
|
||||
const node3000 = makeTestDominatorTreeNode({ nodeId: 3000 });
|
||||
|
||||
const node2000 = makeTestDominatorTreeNode({ nodeId: 2000 }, [
|
||||
makeTestDominatorTreeNode({}),
|
||||
node3000,
|
||||
makeTestDominatorTreeNode({}),
|
||||
]);
|
||||
|
||||
const node1000 = makeTestDominatorTreeNode({ nodeId: 1000 }, [
|
||||
makeTestDominatorTreeNode({}),
|
||||
node2000,
|
||||
makeTestDominatorTreeNode({}),
|
||||
]);
|
||||
|
||||
const tree = node1000;
|
||||
|
||||
const path = [1000, 2000, 3000];
|
||||
|
||||
const tests = [
|
||||
{ id: 1000, expected: node1000 },
|
||||
{ id: 2000, expected: node2000 },
|
||||
{ id: 3000, expected: node3000 },
|
||||
];
|
||||
|
||||
function run_test() {
|
||||
for (let { id, expected } of tests) {
|
||||
const actual = DominatorTreeNode.getNodeByIdAlongPath(id, tree, path);
|
||||
equal(actual, expected, `We should have got the node with id = ${id}`);
|
||||
}
|
||||
|
||||
equal(null,
|
||||
DominatorTreeNode.getNodeByIdAlongPath(999999999999, tree, path),
|
||||
"null is returned for nodes that are not even in the tree");
|
||||
|
||||
const lastNodeId = tree.children[tree.children.length - 1].nodeId;
|
||||
equal(null,
|
||||
DominatorTreeNode.getNodeByIdAlongPath(lastNodeId, tree, path),
|
||||
"null is returned for nodes that are not along the path");
|
||||
}
|
||||
112
dom/heapsnapshot/tests/unit/test_DominatorTreeNode_insert_01.js
Normal file
112
dom/heapsnapshot/tests/unit/test_DominatorTreeNode_insert_01.js
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that we can insert new children into an existing DominatorTreeNode tree.
|
||||
|
||||
const tree = makeTestDominatorTreeNode({ nodeId: 1000 }, [
|
||||
makeTestDominatorTreeNode({}),
|
||||
makeTestDominatorTreeNode({ nodeId: 2000 }, [
|
||||
makeTestDominatorTreeNode({}),
|
||||
makeTestDominatorTreeNode({ nodeId: 3000 }),
|
||||
makeTestDominatorTreeNode({}),
|
||||
]),
|
||||
makeTestDominatorTreeNode({}),
|
||||
]);
|
||||
|
||||
const path = [1000, 2000, 3000];
|
||||
|
||||
const newChildren = [
|
||||
makeTestDominatorTreeNode({ parentId: 3000 }),
|
||||
makeTestDominatorTreeNode({ parentId: 3000 }),
|
||||
];
|
||||
|
||||
const moreChildrenAvailable = false;
|
||||
|
||||
const expected = {
|
||||
nodeId: 1000,
|
||||
parentId: undefined,
|
||||
label: undefined,
|
||||
shallowSize: 1,
|
||||
retainedSize: 7,
|
||||
children: [
|
||||
{
|
||||
nodeId: 0,
|
||||
label: undefined,
|
||||
shallowSize: 1,
|
||||
retainedSize: 1,
|
||||
parentId: 1000,
|
||||
moreChildrenAvailable: true,
|
||||
children: undefined,
|
||||
},
|
||||
{
|
||||
nodeId: 2000,
|
||||
label: undefined,
|
||||
shallowSize: 1,
|
||||
retainedSize: 4,
|
||||
parentId: 1000,
|
||||
children: [
|
||||
{
|
||||
nodeId: 1,
|
||||
label: undefined,
|
||||
shallowSize: 1,
|
||||
retainedSize: 1,
|
||||
parentId: 2000,
|
||||
moreChildrenAvailable: true,
|
||||
children: undefined,
|
||||
},
|
||||
{
|
||||
nodeId: 3000,
|
||||
label: undefined,
|
||||
shallowSize: 1,
|
||||
retainedSize: 1,
|
||||
parentId: 2000,
|
||||
children: [
|
||||
{
|
||||
nodeId: 7,
|
||||
parentId: 3000,
|
||||
label: undefined,
|
||||
shallowSize: 1,
|
||||
retainedSize: 1,
|
||||
moreChildrenAvailable: true,
|
||||
children: undefined,
|
||||
},
|
||||
{
|
||||
nodeId: 8,
|
||||
parentId: 3000,
|
||||
label: undefined,
|
||||
shallowSize: 1,
|
||||
retainedSize: 1,
|
||||
moreChildrenAvailable: true,
|
||||
children: undefined,
|
||||
},
|
||||
],
|
||||
moreChildrenAvailable: false
|
||||
},
|
||||
{
|
||||
nodeId: 3,
|
||||
label: undefined,
|
||||
shallowSize: 1,
|
||||
retainedSize: 1,
|
||||
parentId: 2000,
|
||||
moreChildrenAvailable: true,
|
||||
children: undefined,
|
||||
},
|
||||
],
|
||||
moreChildrenAvailable: true
|
||||
},
|
||||
{
|
||||
nodeId: 5,
|
||||
label: undefined,
|
||||
shallowSize: 1,
|
||||
retainedSize: 1,
|
||||
parentId: 1000,
|
||||
moreChildrenAvailable: true,
|
||||
children: undefined,
|
||||
}
|
||||
],
|
||||
moreChildrenAvailable: true
|
||||
};
|
||||
|
||||
function run_test() {
|
||||
assertDominatorTreeNodeInsertion(tree, path, newChildren, moreChildrenAvailable, expected);
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test attempting to insert new children into an existing DominatorTreeNode
|
||||
// tree with a bad path.
|
||||
|
||||
const tree = makeTestDominatorTreeNode({}, [
|
||||
makeTestDominatorTreeNode({}),
|
||||
makeTestDominatorTreeNode({}, [
|
||||
makeTestDominatorTreeNode({}),
|
||||
makeTestDominatorTreeNode({}),
|
||||
makeTestDominatorTreeNode({}),
|
||||
]),
|
||||
makeTestDominatorTreeNode({}),
|
||||
]);
|
||||
|
||||
const path = [111111, 222222, 333333];
|
||||
|
||||
const newChildren = [
|
||||
makeTestDominatorTreeNode({ parentId: 333333 }),
|
||||
makeTestDominatorTreeNode({ parentId: 333333 }),
|
||||
];
|
||||
|
||||
const moreChildrenAvailable = false;
|
||||
|
||||
const expected = tree;
|
||||
|
||||
function run_test() {
|
||||
assertDominatorTreeNodeInsertion(tree, path, newChildren, moreChildrenAvailable, expected);
|
||||
}
|
||||
117
dom/heapsnapshot/tests/unit/test_DominatorTreeNode_insert_03.js
Normal file
117
dom/heapsnapshot/tests/unit/test_DominatorTreeNode_insert_03.js
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test inserting new children into an existing DominatorTreeNode at the root.
|
||||
|
||||
const tree = makeTestDominatorTreeNode({ nodeId: 666 }, [
|
||||
makeTestDominatorTreeNode({}),
|
||||
makeTestDominatorTreeNode({}, [
|
||||
makeTestDominatorTreeNode({}),
|
||||
makeTestDominatorTreeNode({}),
|
||||
makeTestDominatorTreeNode({}),
|
||||
]),
|
||||
makeTestDominatorTreeNode({}),
|
||||
]);
|
||||
|
||||
const path = [666];
|
||||
|
||||
const newChildren = [
|
||||
makeTestDominatorTreeNode({
|
||||
nodeId: 777,
|
||||
parentId: 666
|
||||
}),
|
||||
makeTestDominatorTreeNode({
|
||||
nodeId: 888,
|
||||
parentId: 666
|
||||
}),
|
||||
];
|
||||
|
||||
const moreChildrenAvailable = false;
|
||||
|
||||
const expected = {
|
||||
nodeId: 666,
|
||||
label: undefined,
|
||||
parentId: undefined,
|
||||
shallowSize: 1,
|
||||
retainedSize: 7,
|
||||
children: [
|
||||
{
|
||||
nodeId: 0,
|
||||
label: undefined,
|
||||
shallowSize: 1,
|
||||
retainedSize: 1,
|
||||
parentId: 666,
|
||||
moreChildrenAvailable: true,
|
||||
children: undefined
|
||||
},
|
||||
{
|
||||
nodeId: 4,
|
||||
label: undefined,
|
||||
shallowSize: 1,
|
||||
retainedSize: 4,
|
||||
parentId: 666,
|
||||
children: [
|
||||
{
|
||||
nodeId: 1,
|
||||
label: undefined,
|
||||
shallowSize: 1,
|
||||
retainedSize: 1,
|
||||
parentId: 4,
|
||||
moreChildrenAvailable: true,
|
||||
children: undefined
|
||||
},
|
||||
{
|
||||
nodeId: 2,
|
||||
label: undefined,
|
||||
shallowSize: 1,
|
||||
retainedSize: 1,
|
||||
parentId: 4,
|
||||
moreChildrenAvailable: true,
|
||||
children: undefined
|
||||
},
|
||||
{
|
||||
nodeId: 3,
|
||||
label: undefined,
|
||||
shallowSize: 1,
|
||||
retainedSize: 1,
|
||||
parentId: 4,
|
||||
moreChildrenAvailable: true,
|
||||
children: undefined
|
||||
}
|
||||
],
|
||||
moreChildrenAvailable: true
|
||||
},
|
||||
{
|
||||
nodeId: 5,
|
||||
label: undefined,
|
||||
shallowSize: 1,
|
||||
retainedSize: 1,
|
||||
parentId: 666,
|
||||
moreChildrenAvailable: true,
|
||||
children: undefined
|
||||
},
|
||||
{
|
||||
nodeId: 777,
|
||||
label: undefined,
|
||||
shallowSize: 1,
|
||||
retainedSize: 1,
|
||||
parentId: 666,
|
||||
moreChildrenAvailable: true,
|
||||
children: undefined
|
||||
},
|
||||
{
|
||||
nodeId: 888,
|
||||
label: undefined,
|
||||
shallowSize: 1,
|
||||
retainedSize: 1,
|
||||
parentId: 666,
|
||||
moreChildrenAvailable: true,
|
||||
children: undefined
|
||||
}
|
||||
],
|
||||
moreChildrenAvailable: false
|
||||
};
|
||||
|
||||
function run_test() {
|
||||
assertDominatorTreeNodeInsertion(tree, path, newChildren, moreChildrenAvailable, expected);
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that we correctly set `moreChildrenAvailable` when doing a partial
|
||||
// traversal of a dominator tree to create the initial incrementally loaded
|
||||
// `DominatorTreeNode` tree.
|
||||
|
||||
// `tree` maps parent to children:
|
||||
//
|
||||
// 100
|
||||
// |- 200
|
||||
// | |- 500
|
||||
// | |- 600
|
||||
// | `- 700
|
||||
// |- 300
|
||||
// | |- 800
|
||||
// | |- 900
|
||||
// `- 400
|
||||
// |- 1000
|
||||
// |- 1100
|
||||
// `- 1200
|
||||
const tree = new Map([
|
||||
[100, [200, 300, 400]],
|
||||
[200, [500, 600, 700]],
|
||||
[300, [800, 900]],
|
||||
[400, [1000, 1100, 1200]]
|
||||
]);
|
||||
|
||||
const mockDominatorTree = {
|
||||
root: 100,
|
||||
getRetainedSize: _ => 10,
|
||||
getImmediatelyDominated: id => (tree.get(id) || []).slice()
|
||||
};
|
||||
|
||||
const mockSnapshot = {
|
||||
describeNode: _ => ({
|
||||
objects: { count: 0, bytes: 0 },
|
||||
strings: { count: 0, bytes: 0 },
|
||||
scripts: { count: 0, bytes: 0 },
|
||||
other: { SomeType: { count: 1, bytes: 10 } }
|
||||
})
|
||||
};
|
||||
|
||||
const breakdown = {
|
||||
by: "coarseType",
|
||||
objects: { by: "count", count: true, bytes: true },
|
||||
strings: { by: "count", count: true, bytes: true },
|
||||
scripts: { by: "count", count: true, bytes: true },
|
||||
other: {
|
||||
by: "internalType",
|
||||
then: { by: "count", count: true, bytes: true }
|
||||
},
|
||||
};
|
||||
|
||||
const expected = {
|
||||
nodeId: 100,
|
||||
label: [
|
||||
"other",
|
||||
"SomeType"
|
||||
],
|
||||
shallowSize: 10,
|
||||
retainedSize: 10,
|
||||
shortestPaths: undefined,
|
||||
children: [
|
||||
{
|
||||
nodeId: 200,
|
||||
label: [
|
||||
"other",
|
||||
"SomeType"
|
||||
],
|
||||
shallowSize: 10,
|
||||
retainedSize: 10,
|
||||
parentId: 100,
|
||||
shortestPaths: undefined,
|
||||
children: [
|
||||
{
|
||||
nodeId: 500,
|
||||
label: [
|
||||
"other",
|
||||
"SomeType"
|
||||
],
|
||||
shallowSize: 10,
|
||||
retainedSize: 10,
|
||||
parentId: 200,
|
||||
moreChildrenAvailable: false,
|
||||
shortestPaths: undefined,
|
||||
children: undefined
|
||||
},
|
||||
{
|
||||
nodeId: 600,
|
||||
label: [
|
||||
"other",
|
||||
"SomeType"
|
||||
],
|
||||
shallowSize: 10,
|
||||
retainedSize: 10,
|
||||
parentId: 200,
|
||||
moreChildrenAvailable: false,
|
||||
shortestPaths: undefined,
|
||||
children: undefined
|
||||
}
|
||||
],
|
||||
moreChildrenAvailable: true
|
||||
},
|
||||
{
|
||||
nodeId: 300,
|
||||
label: [
|
||||
"other",
|
||||
"SomeType"
|
||||
],
|
||||
shallowSize: 10,
|
||||
retainedSize: 10,
|
||||
parentId: 100,
|
||||
shortestPaths: undefined,
|
||||
children: [
|
||||
{
|
||||
nodeId: 800,
|
||||
label: [
|
||||
"other",
|
||||
"SomeType"
|
||||
],
|
||||
shallowSize: 10,
|
||||
retainedSize: 10,
|
||||
parentId: 300,
|
||||
moreChildrenAvailable: false,
|
||||
shortestPaths: undefined,
|
||||
children: undefined
|
||||
},
|
||||
{
|
||||
nodeId: 900,
|
||||
label: [
|
||||
"other",
|
||||
"SomeType"
|
||||
],
|
||||
shallowSize: 10,
|
||||
retainedSize: 10,
|
||||
parentId: 300,
|
||||
moreChildrenAvailable: false,
|
||||
shortestPaths: undefined,
|
||||
children: undefined
|
||||
}
|
||||
],
|
||||
moreChildrenAvailable: false
|
||||
}
|
||||
],
|
||||
moreChildrenAvailable: true,
|
||||
parentId: undefined,
|
||||
};
|
||||
|
||||
function run_test() {
|
||||
// Traverse the whole depth of the test tree, but one short of the number of
|
||||
// siblings. This will exercise the moreChildrenAvailable handling for
|
||||
// siblings.
|
||||
const actual = DominatorTreeNode.partialTraversal(mockDominatorTree,
|
||||
mockSnapshot,
|
||||
breakdown,
|
||||
/* maxDepth = */ 4,
|
||||
/* siblings = */ 2);
|
||||
|
||||
dumpn("Expected = " + JSON.stringify(expected, null, 2));
|
||||
dumpn("Actual = " + JSON.stringify(actual, null, 2));
|
||||
|
||||
assertStructurallyEquivalent(expected, actual);
|
||||
}
|
||||
23
dom/heapsnapshot/tests/unit/test_DominatorTree_01.js
Normal file
23
dom/heapsnapshot/tests/unit/test_DominatorTree_01.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Sanity test that we can compute dominator trees.
|
||||
|
||||
function run_test() {
|
||||
const path = ChromeUtils.saveHeapSnapshot({ runtime: true });
|
||||
const snapshot = ChromeUtils.readHeapSnapshot(path);
|
||||
|
||||
const dominatorTree = snapshot.computeDominatorTree();
|
||||
ok(dominatorTree);
|
||||
ok(dominatorTree instanceof DominatorTree);
|
||||
|
||||
let threw = false;
|
||||
try {
|
||||
new DominatorTree();
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
}
|
||||
ok(threw, "Constructor shouldn't be usable");
|
||||
|
||||
do_test_finished();
|
||||
}
|
||||
40
dom/heapsnapshot/tests/unit/test_DominatorTree_02.js
Normal file
40
dom/heapsnapshot/tests/unit/test_DominatorTree_02.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that we can compute dominator trees from a snapshot in a worker.
|
||||
|
||||
add_task(function* () {
|
||||
const worker = new ChromeWorker("resource://test/dominator-tree-worker.js");
|
||||
worker.postMessage({});
|
||||
|
||||
let assertionCount = 0;
|
||||
worker.onmessage = e => {
|
||||
if (e.data.type !== "assertion") {
|
||||
return;
|
||||
}
|
||||
|
||||
ok(e.data.passed, e.data.msg + "\n" + e.data.stack);
|
||||
assertionCount++;
|
||||
};
|
||||
|
||||
yield waitForDone(worker);
|
||||
|
||||
ok(assertionCount > 0);
|
||||
worker.terminate();
|
||||
});
|
||||
|
||||
function waitForDone(w) {
|
||||
return new Promise((resolve, reject) => {
|
||||
w.onerror = e => {
|
||||
reject();
|
||||
ok(false, "Error in worker: " + e);
|
||||
};
|
||||
|
||||
w.addEventListener("message", function listener(e) {
|
||||
if (e.data.type === "done") {
|
||||
w.removeEventListener("message", listener, false);
|
||||
resolve();
|
||||
}
|
||||
}, false);
|
||||
});
|
||||
}
|
||||
13
dom/heapsnapshot/tests/unit/test_DominatorTree_03.js
Normal file
13
dom/heapsnapshot/tests/unit/test_DominatorTree_03.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that we can get the root of dominator trees.
|
||||
|
||||
function run_test() {
|
||||
const dominatorTree = saveHeapSnapshotAndComputeDominatorTree();
|
||||
equal(typeof dominatorTree.root, "number", "root should be a number");
|
||||
equal(Math.floor(dominatorTree.root), dominatorTree.root, "root should be an integer");
|
||||
ok(dominatorTree.root >= 0, "root should be positive");
|
||||
ok(dominatorTree.root <= Math.pow(2, 48), "root should be less than or equal to 2^48");
|
||||
do_test_finished();
|
||||
}
|
||||
22
dom/heapsnapshot/tests/unit/test_DominatorTree_04.js
Normal file
22
dom/heapsnapshot/tests/unit/test_DominatorTree_04.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that we can get the retained sizes of dominator trees.
|
||||
|
||||
function run_test() {
|
||||
const dominatorTree = saveHeapSnapshotAndComputeDominatorTree();
|
||||
equal(typeof dominatorTree.getRetainedSize, "function",
|
||||
"getRetainedSize should be a function");
|
||||
|
||||
const size = dominatorTree.getRetainedSize(dominatorTree.root);
|
||||
ok(size, "should get a size for the root");
|
||||
equal(typeof size, "number", "retained sizes should be a number");
|
||||
equal(Math.floor(size), size, "size should be an integer");
|
||||
ok(size > 0, "size should be positive");
|
||||
ok(size <= Math.pow(2, 64), "size should be less than or equal to 2^64");
|
||||
|
||||
const bad = dominatorTree.getRetainedSize(1);
|
||||
equal(bad, null, "null is returned for unknown node ids");
|
||||
|
||||
do_test_finished();
|
||||
}
|
||||
75
dom/heapsnapshot/tests/unit/test_DominatorTree_05.js
Normal file
75
dom/heapsnapshot/tests/unit/test_DominatorTree_05.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that we can get the set of immediately dominated nodes for any given
|
||||
// node and that this forms a tree.
|
||||
|
||||
function run_test() {
|
||||
var dominatorTree = saveHeapSnapshotAndComputeDominatorTree();
|
||||
equal(typeof dominatorTree.getImmediatelyDominated, "function",
|
||||
"getImmediatelyDominated should be a function");
|
||||
|
||||
// Do a traversal of the dominator tree.
|
||||
//
|
||||
// Note that we don't assert directly, only if we get an unexpected
|
||||
// value. There are just way too many nodes in the heap graph to assert for
|
||||
// every one. This test would constantly time out and assertion messages would
|
||||
// overflow the log size.
|
||||
|
||||
var root = dominatorTree.root;
|
||||
equal(dominatorTree.getImmediateDominator(root), null,
|
||||
"The root should not have a parent");
|
||||
|
||||
var seen = new Set();
|
||||
var stack = [root];
|
||||
while (stack.length > 0) {
|
||||
var top = stack.pop();
|
||||
|
||||
if (seen.has(top)) {
|
||||
ok(false,
|
||||
"This is a tree, not a graph: we shouldn't have multiple edges to the same node");
|
||||
}
|
||||
seen.add(top);
|
||||
if (seen.size % 1000 === 0) {
|
||||
dumpn("Progress update: seen size = " + seen.size);
|
||||
}
|
||||
|
||||
var newNodes = dominatorTree.getImmediatelyDominated(top);
|
||||
if (Object.prototype.toString.call(newNodes) !== "[object Array]") {
|
||||
ok(false, "getImmediatelyDominated should return an array for known node ids");
|
||||
}
|
||||
|
||||
var topSize = dominatorTree.getRetainedSize(top);
|
||||
|
||||
var lastSize = Infinity;
|
||||
for (var i = 0; i < newNodes.length; i++) {
|
||||
if (typeof newNodes[i] !== "number") {
|
||||
ok(false, "Every dominated id should be a number");
|
||||
}
|
||||
|
||||
if (dominatorTree.getImmediateDominator(newNodes[i]) !== top) {
|
||||
ok(false, "child's parent should be the expected parent");
|
||||
}
|
||||
|
||||
var thisSize = dominatorTree.getRetainedSize(newNodes[i]);
|
||||
|
||||
if (thisSize >= topSize) {
|
||||
ok(false, "the size of children in the dominator tree should always be less than that of their parent");
|
||||
}
|
||||
|
||||
if (thisSize > lastSize) {
|
||||
ok(false,
|
||||
"children should be sorted by greatest to least retained size, "
|
||||
+ "lastSize = " + lastSize + ", thisSize = " + thisSize);
|
||||
}
|
||||
|
||||
lastSize = thisSize;
|
||||
stack.push(newNodes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
ok(true, "Successfully walked the tree");
|
||||
dumpn("Walked " + seen.size + " nodes");
|
||||
|
||||
do_test_finished();
|
||||
}
|
||||
56
dom/heapsnapshot/tests/unit/test_DominatorTree_06.js
Normal file
56
dom/heapsnapshot/tests/unit/test_DominatorTree_06.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that the retained size of a node is the sum of its children retained
|
||||
// sizes plus its shallow size.
|
||||
|
||||
// Note that we don't assert directly, only if we get an unexpected
|
||||
// value. There are just way too many nodes in the heap graph to assert for
|
||||
// every one. This test would constantly time out and assertion messages would
|
||||
// overflow the log size.
|
||||
function fastAssert(cond, msg) {
|
||||
if (!cond) {
|
||||
ok(false, msg);
|
||||
}
|
||||
}
|
||||
|
||||
var COUNT = { by: "count", count: false, bytes: true };
|
||||
|
||||
function run_test() {
|
||||
var path = saveNewHeapSnapshot();
|
||||
var snapshot = ChromeUtils.readHeapSnapshot(path);
|
||||
var dominatorTree = snapshot.computeDominatorTree();
|
||||
|
||||
// Do a traversal of the dominator tree and assert the relationship between
|
||||
// retained size, shallow size, and children's retained sizes.
|
||||
|
||||
var root = dominatorTree.root;
|
||||
var stack = [root];
|
||||
while (stack.length > 0) {
|
||||
var top = stack.pop();
|
||||
|
||||
var children = dominatorTree.getImmediatelyDominated(top);
|
||||
|
||||
var topRetainedSize = dominatorTree.getRetainedSize(top);
|
||||
var topShallowSize = snapshot.describeNode(COUNT, top).bytes;
|
||||
fastAssert(topShallowSize <= topRetainedSize,
|
||||
"The shallow size should be less than or equal to the " +
|
||||
"retained size");
|
||||
|
||||
var sumOfChildrensRetainedSizes = 0;
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
sumOfChildrensRetainedSizes += dominatorTree.getRetainedSize(children[i]);
|
||||
stack.push(children[i]);
|
||||
}
|
||||
|
||||
fastAssert(sumOfChildrensRetainedSizes <= topRetainedSize,
|
||||
"The sum of the children's retained sizes should be less than " +
|
||||
"or equal to the retained size");
|
||||
fastAssert(sumOfChildrensRetainedSizes + topShallowSize === topRetainedSize,
|
||||
"The sum of the children's retained sizes plus the shallow " +
|
||||
"size should be equal to the retained size");
|
||||
}
|
||||
|
||||
ok(true, "Successfully walked the tree");
|
||||
do_test_finished();
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test the HeapAnalyses{Client,Worker} "computeDominatorTree" request.
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* () {
|
||||
const client = new HeapAnalysesClient();
|
||||
|
||||
const snapshotFilePath = saveNewHeapSnapshot();
|
||||
yield client.readHeapSnapshot(snapshotFilePath);
|
||||
ok(true, "Should have read the heap snapshot");
|
||||
|
||||
const dominatorTreeId = yield client.computeDominatorTree(snapshotFilePath);
|
||||
equal(typeof dominatorTreeId, "number",
|
||||
"should get a dominator tree id, and it should be a number");
|
||||
|
||||
client.destroy();
|
||||
});
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test the HeapAnalyses{Client,Worker} "computeDominatorTree" request with bad
|
||||
// file paths.
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* () {
|
||||
const client = new HeapAnalysesClient();
|
||||
|
||||
let threw = false;
|
||||
try {
|
||||
yield client.computeDominatorTree("/etc/passwd");
|
||||
} catch (_) {
|
||||
threw = true;
|
||||
}
|
||||
ok(threw, "should throw when given a bad path");
|
||||
|
||||
client.destroy();
|
||||
});
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that the HeapAnalyses{Client,Worker} can delete heap snapshots.
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
const breakdown = {
|
||||
by: "coarseType",
|
||||
objects: { by: "count", count: true, bytes: true },
|
||||
scripts: { by: "count", count: true, bytes: true },
|
||||
strings: { by: "count", count: true, bytes: true },
|
||||
other: { by: "count", count: true, bytes: true },
|
||||
};
|
||||
|
||||
add_task(function* () {
|
||||
const client = new HeapAnalysesClient();
|
||||
|
||||
const snapshotFilePath = saveNewHeapSnapshot();
|
||||
yield client.readHeapSnapshot(snapshotFilePath);
|
||||
ok(true, "Should have read the heap snapshot");
|
||||
|
||||
let dominatorTreeId = yield client.computeDominatorTree(snapshotFilePath);
|
||||
ok(true, "Should have computed the dominator tree");
|
||||
|
||||
yield client.deleteHeapSnapshot(snapshotFilePath);
|
||||
ok(true, "Should have deleted the snapshot");
|
||||
|
||||
let threw = false;
|
||||
try {
|
||||
yield client.getDominatorTree({
|
||||
dominatorTreeId: dominatorTreeId,
|
||||
breakdown
|
||||
});
|
||||
} catch (_) {
|
||||
threw = true;
|
||||
}
|
||||
ok(threw, "getDominatorTree on deleted tree should throw an error");
|
||||
|
||||
threw = false;
|
||||
try {
|
||||
yield client.computeDominatorTree(snapshotFilePath);
|
||||
} catch (_) {
|
||||
threw = true;
|
||||
}
|
||||
ok(threw, "computeDominatorTree on deleted snapshot should throw an error");
|
||||
|
||||
threw = false;
|
||||
try {
|
||||
yield client.takeCensus(snapshotFilePath);
|
||||
} catch (_) {
|
||||
threw = true;
|
||||
}
|
||||
ok(threw, "takeCensus on deleted tree should throw an error");
|
||||
|
||||
client.destroy();
|
||||
});
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test deleteHeapSnapshot is a noop if the provided path matches no snapshot
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* () {
|
||||
const client = new HeapAnalysesClient();
|
||||
|
||||
let threw = false;
|
||||
try {
|
||||
yield client.deleteHeapSnapshot("path-does-not-exist");
|
||||
} catch (_) {
|
||||
threw = true;
|
||||
}
|
||||
ok(threw, "deleteHeapSnapshot on non-existant path should throw an error");
|
||||
|
||||
client.destroy();
|
||||
});
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test other dominatorTrees can still be retrieved after deleting a snapshot
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
const breakdown = {
|
||||
by: "coarseType",
|
||||
objects: { by: "count", count: true, bytes: true },
|
||||
scripts: { by: "count", count: true, bytes: true },
|
||||
strings: { by: "count", count: true, bytes: true },
|
||||
other: { by: "count", count: true, bytes: true },
|
||||
};
|
||||
|
||||
function* createSnapshotAndDominatorTree(client) {
|
||||
let snapshotFilePath = saveNewHeapSnapshot();
|
||||
yield client.readHeapSnapshot(snapshotFilePath);
|
||||
let dominatorTreeId = yield client.computeDominatorTree(snapshotFilePath);
|
||||
return { dominatorTreeId, snapshotFilePath };
|
||||
}
|
||||
|
||||
add_task(function* () {
|
||||
const client = new HeapAnalysesClient();
|
||||
|
||||
let savedSnapshots = [
|
||||
yield createSnapshotAndDominatorTree(client),
|
||||
yield createSnapshotAndDominatorTree(client),
|
||||
yield createSnapshotAndDominatorTree(client)
|
||||
];
|
||||
ok(true, "Create 3 snapshots and dominator trees");
|
||||
|
||||
yield client.deleteHeapSnapshot(savedSnapshots[1].snapshotFilePath);
|
||||
ok(true, "Snapshot deleted");
|
||||
|
||||
let tree = yield client.getDominatorTree({
|
||||
dominatorTreeId: savedSnapshots[0].dominatorTreeId,
|
||||
breakdown
|
||||
});
|
||||
ok(tree, "Should get a valid tree for first snapshot");
|
||||
|
||||
let threw = false;
|
||||
try {
|
||||
yield client.getDominatorTree({
|
||||
dominatorTreeId: savedSnapshots[1].dominatorTreeId,
|
||||
breakdown
|
||||
});
|
||||
} catch (_) {
|
||||
threw = true;
|
||||
}
|
||||
ok(threw, "getDominatorTree on a deleted snapshot should throw an error");
|
||||
|
||||
tree = yield client.getDominatorTree({
|
||||
dominatorTreeId: savedSnapshots[2].dominatorTreeId,
|
||||
breakdown
|
||||
});
|
||||
ok(tree, "Should get a valid tree for third snapshot");
|
||||
|
||||
client.destroy();
|
||||
});
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that the HeapAnalyses{Client,Worker} can get census individuals.
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
const COUNT = { by: "count", count: true, bytes: true };
|
||||
|
||||
const CENSUS_BREAKDOWN = {
|
||||
by: "coarseType",
|
||||
objects: COUNT,
|
||||
strings: COUNT,
|
||||
scripts: COUNT,
|
||||
other: COUNT,
|
||||
};
|
||||
|
||||
const LABEL_BREAKDOWN = {
|
||||
by: "internalType",
|
||||
then: COUNT,
|
||||
};
|
||||
|
||||
const MAX_INDIVIDUALS = 10;
|
||||
|
||||
add_task(function* () {
|
||||
const client = new HeapAnalysesClient();
|
||||
|
||||
const snapshotFilePath = saveNewHeapSnapshot();
|
||||
yield client.readHeapSnapshot(snapshotFilePath);
|
||||
ok(true, "Should have read the heap snapshot");
|
||||
|
||||
const dominatorTreeId = yield client.computeDominatorTree(snapshotFilePath);
|
||||
ok(true, "Should have computed dominator tree");
|
||||
|
||||
const { report } = yield client.takeCensus(snapshotFilePath,
|
||||
{ breakdown: CENSUS_BREAKDOWN },
|
||||
{ asTreeNode: true });
|
||||
ok(report, "Should get a report");
|
||||
|
||||
let nodesWithLeafIndicesFound = 0;
|
||||
|
||||
yield* (function* assertCanGetIndividuals(censusNode) {
|
||||
if (censusNode.reportLeafIndex !== undefined) {
|
||||
nodesWithLeafIndicesFound++;
|
||||
|
||||
const response = yield client.getCensusIndividuals({
|
||||
dominatorTreeId,
|
||||
indices: DevToolsUtils.isSet(censusNode.reportLeafIndex)
|
||||
? censusNode.reportLeafIndex
|
||||
: new Set([censusNode.reportLeafIndex]),
|
||||
censusBreakdown: CENSUS_BREAKDOWN,
|
||||
labelBreakdown: LABEL_BREAKDOWN,
|
||||
maxRetainingPaths: 1,
|
||||
maxIndividuals: MAX_INDIVIDUALS,
|
||||
});
|
||||
|
||||
dumpn(`response = ${JSON.stringify(response, null, 4)}`);
|
||||
|
||||
equal(response.nodes.length, Math.min(MAX_INDIVIDUALS, censusNode.count),
|
||||
"response.nodes.length === Math.min(MAX_INDIVIDUALS, censusNode.count)");
|
||||
|
||||
let lastRetainedSize = Infinity;
|
||||
for (let individual of response.nodes) {
|
||||
equal(typeof individual.nodeId, "number",
|
||||
"individual.nodeId should be a number");
|
||||
ok(individual.retainedSize <= lastRetainedSize,
|
||||
"individual.retainedSize <= lastRetainedSize");
|
||||
lastRetainedSize = individual.retainedSize;
|
||||
ok(individual.shallowSize, "individual.shallowSize should exist and be non-zero");
|
||||
ok(individual.shortestPaths, "individual.shortestPaths should exist");
|
||||
ok(individual.shortestPaths.nodes, "individual.shortestPaths.nodes should exist");
|
||||
ok(individual.shortestPaths.edges, "individual.shortestPaths.edges should exist");
|
||||
ok(individual.label, "individual.label should exist");
|
||||
}
|
||||
}
|
||||
|
||||
if (censusNode.children) {
|
||||
for (let child of censusNode.children) {
|
||||
yield* assertCanGetIndividuals(child);
|
||||
}
|
||||
}
|
||||
}(report));
|
||||
|
||||
equal(nodesWithLeafIndicesFound, 4, "Should have found a leaf for each coarse type");
|
||||
|
||||
client.destroy();
|
||||
});
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that the HeapAnalyses{Client,Worker} can get a HeapSnapshot's
|
||||
// creation time.
|
||||
|
||||
function waitForThirtyMilliseconds() {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < 30) ;
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
const BREAKDOWN = {
|
||||
by: "internalType",
|
||||
then: { by: "count", count: true, bytes: true }
|
||||
};
|
||||
|
||||
add_task(function* () {
|
||||
const client = new HeapAnalysesClient();
|
||||
const start = Date.now() * 1000;
|
||||
|
||||
// Because Date.now() is less precise than the snapshot's time stamp, give it
|
||||
// a little bit of head room. Additionally, WinXP's timers have a granularity
|
||||
// of only +/-15 ms.
|
||||
waitForThirtyMilliseconds();
|
||||
const snapshotFilePath = saveNewHeapSnapshot();
|
||||
waitForThirtyMilliseconds();
|
||||
const end = Date.now() * 1000;
|
||||
|
||||
yield client.readHeapSnapshot(snapshotFilePath);
|
||||
ok(true, "Should have read the heap snapshot");
|
||||
|
||||
let threw = false;
|
||||
try {
|
||||
yield client.getCreationTime("/not/a/real/path", {
|
||||
breakdown: BREAKDOWN
|
||||
});
|
||||
} catch (_) {
|
||||
threw = true;
|
||||
}
|
||||
ok(threw, "getCreationTime should throw when snapshot does not exist");
|
||||
|
||||
let time = yield client.getCreationTime(snapshotFilePath, {
|
||||
breakdown: BREAKDOWN
|
||||
});
|
||||
|
||||
dumpn("Start = " + start);
|
||||
dumpn("End = " + end);
|
||||
dumpn("Time = " + time);
|
||||
|
||||
ok(time >= start, "creation time occurred after start");
|
||||
ok(time <= end, "creation time occurred before end");
|
||||
|
||||
client.destroy();
|
||||
});
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test the HeapAnalyses{Client,Worker} "getDominatorTree" request.
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
const breakdown = {
|
||||
by: "coarseType",
|
||||
objects: { by: "count", count: true, bytes: true },
|
||||
scripts: { by: "count", count: true, bytes: true },
|
||||
strings: { by: "count", count: true, bytes: true },
|
||||
other: { by: "count", count: true, bytes: true },
|
||||
};
|
||||
|
||||
add_task(function* () {
|
||||
const client = new HeapAnalysesClient();
|
||||
|
||||
const snapshotFilePath = saveNewHeapSnapshot();
|
||||
yield client.readHeapSnapshot(snapshotFilePath);
|
||||
ok(true, "Should have read the heap snapshot");
|
||||
|
||||
const dominatorTreeId = yield client.computeDominatorTree(snapshotFilePath);
|
||||
equal(typeof dominatorTreeId, "number",
|
||||
"should get a dominator tree id, and it should be a number");
|
||||
|
||||
const partialTree = yield client.getDominatorTree({
|
||||
dominatorTreeId,
|
||||
breakdown
|
||||
});
|
||||
ok(partialTree, "Should get a partial tree");
|
||||
equal(typeof partialTree, "object",
|
||||
"partialTree should be an object");
|
||||
|
||||
function checkTree(node) {
|
||||
equal(typeof node.nodeId, "number", "each node should have an id");
|
||||
|
||||
if (node === partialTree) {
|
||||
equal(node.parentId, undefined, "the root has no parent");
|
||||
} else {
|
||||
equal(typeof node.parentId, "number", "each node should have a parent id");
|
||||
}
|
||||
|
||||
equal(typeof node.retainedSize, "number",
|
||||
"each node should have a retained size");
|
||||
|
||||
ok(node.children === undefined || Array.isArray(node.children),
|
||||
"each node either has a list of children, or undefined meaning no children loaded");
|
||||
equal(typeof node.moreChildrenAvailable, "boolean",
|
||||
"each node should indicate if there are more children available or not");
|
||||
|
||||
equal(typeof node.shortestPaths, "object",
|
||||
"Should have shortest paths");
|
||||
equal(typeof node.shortestPaths.nodes, "object",
|
||||
"Should have shortest paths' nodes");
|
||||
equal(typeof node.shortestPaths.edges, "object",
|
||||
"Should have shortest paths' edges");
|
||||
|
||||
if (node.children) {
|
||||
node.children.forEach(checkTree);
|
||||
}
|
||||
}
|
||||
|
||||
checkTree(partialTree);
|
||||
|
||||
client.destroy();
|
||||
});
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test the HeapAnalyses{Client,Worker} "getDominatorTree" request with bad
|
||||
// dominator tree ids.
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
const breakdown = {
|
||||
by: "coarseType",
|
||||
objects: { by: "count", count: true, bytes: true },
|
||||
scripts: { by: "count", count: true, bytes: true },
|
||||
strings: { by: "count", count: true, bytes: true },
|
||||
other: { by: "count", count: true, bytes: true },
|
||||
};
|
||||
|
||||
add_task(function* () {
|
||||
const client = new HeapAnalysesClient();
|
||||
|
||||
let threw = false;
|
||||
try {
|
||||
yield client.getDominatorTree({ dominatorTreeId: 42, breakdown });
|
||||
} catch (_) {
|
||||
threw = true;
|
||||
}
|
||||
ok(threw, "should throw when given a bad id");
|
||||
|
||||
client.destroy();
|
||||
});
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test the HeapAnalyses{Client,Worker} "getImmediatelyDominated" request.
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
const breakdown = {
|
||||
by: "coarseType",
|
||||
objects: { by: "count", count: true, bytes: true },
|
||||
scripts: { by: "count", count: true, bytes: true },
|
||||
strings: { by: "count", count: true, bytes: true },
|
||||
other: { by: "count", count: true, bytes: true },
|
||||
};
|
||||
|
||||
add_task(function* () {
|
||||
const client = new HeapAnalysesClient();
|
||||
|
||||
const snapshotFilePath = saveNewHeapSnapshot();
|
||||
yield client.readHeapSnapshot(snapshotFilePath);
|
||||
const dominatorTreeId = yield client.computeDominatorTree(snapshotFilePath);
|
||||
|
||||
const partialTree = yield client.getDominatorTree({
|
||||
dominatorTreeId,
|
||||
breakdown
|
||||
});
|
||||
ok(partialTree.children.length > 0,
|
||||
"root should immediately dominate some nodes");
|
||||
|
||||
// First, test getting a subset of children available.
|
||||
const response = yield client.getImmediatelyDominated({
|
||||
dominatorTreeId,
|
||||
breakdown,
|
||||
nodeId: partialTree.nodeId,
|
||||
startIndex: 0,
|
||||
maxCount: partialTree.children.length - 1
|
||||
});
|
||||
|
||||
ok(Array.isArray(response.nodes));
|
||||
ok(response.nodes.every(node => node.parentId === partialTree.nodeId));
|
||||
ok(response.moreChildrenAvailable);
|
||||
equal(response.path.length, 1);
|
||||
equal(response.path[0], partialTree.nodeId);
|
||||
|
||||
for (let node of response.nodes) {
|
||||
equal(typeof node.shortestPaths, "object",
|
||||
"Should have shortest paths");
|
||||
equal(typeof node.shortestPaths.nodes, "object",
|
||||
"Should have shortest paths' nodes");
|
||||
equal(typeof node.shortestPaths.edges, "object",
|
||||
"Should have shortest paths' edges");
|
||||
}
|
||||
|
||||
// Next, test getting a subset of children available.
|
||||
const secondResponse = yield client.getImmediatelyDominated({
|
||||
dominatorTreeId,
|
||||
breakdown,
|
||||
nodeId: partialTree.nodeId,
|
||||
startIndex: 0,
|
||||
maxCount: Infinity
|
||||
});
|
||||
|
||||
ok(Array.isArray(secondResponse.nodes));
|
||||
ok(secondResponse.nodes.every(node => node.parentId === partialTree.nodeId));
|
||||
ok(!secondResponse.moreChildrenAvailable);
|
||||
equal(secondResponse.path.length, 1);
|
||||
equal(secondResponse.path[0], partialTree.nodeId);
|
||||
|
||||
for (let node of secondResponse.nodes) {
|
||||
equal(typeof node.shortestPaths, "object",
|
||||
"Should have shortest paths");
|
||||
equal(typeof node.shortestPaths.nodes, "object",
|
||||
"Should have shortest paths' nodes");
|
||||
equal(typeof node.shortestPaths.edges, "object",
|
||||
"Should have shortest paths' edges");
|
||||
}
|
||||
|
||||
client.destroy();
|
||||
});
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that the HeapAnalyses{Client,Worker} can read heap snapshots.
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* () {
|
||||
const client = new HeapAnalysesClient();
|
||||
|
||||
const snapshotFilePath = saveNewHeapSnapshot();
|
||||
yield client.readHeapSnapshot(snapshotFilePath);
|
||||
ok(true, "Should have read the heap snapshot");
|
||||
|
||||
client.destroy();
|
||||
});
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that the HeapAnalyses{Client,Worker} can take diffs between censuses.
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
const BREAKDOWN = {
|
||||
by: "objectClass",
|
||||
then: { by: "count", count: true, bytes: false },
|
||||
other: { by: "count", count: true, bytes: false },
|
||||
};
|
||||
|
||||
add_task(function* () {
|
||||
const client = new HeapAnalysesClient();
|
||||
|
||||
const markers = [allocationMarker()];
|
||||
|
||||
const firstSnapshotFilePath = saveNewHeapSnapshot();
|
||||
|
||||
// Allocate and hold an additional AllocationMarker object so we can see it in
|
||||
// the next heap snapshot.
|
||||
markers.push(allocationMarker());
|
||||
|
||||
const secondSnapshotFilePath = saveNewHeapSnapshot();
|
||||
|
||||
yield client.readHeapSnapshot(firstSnapshotFilePath);
|
||||
yield client.readHeapSnapshot(secondSnapshotFilePath);
|
||||
ok(true, "Should have read both heap snapshot files");
|
||||
|
||||
const { delta } = yield client.takeCensusDiff(firstSnapshotFilePath,
|
||||
secondSnapshotFilePath,
|
||||
{ breakdown: BREAKDOWN });
|
||||
|
||||
equal(delta.AllocationMarker.count, 1,
|
||||
"There exists one new AllocationMarker in the second heap snapshot");
|
||||
|
||||
const { delta: deltaTreeNode } = yield client.takeCensusDiff(firstSnapshotFilePath,
|
||||
secondSnapshotFilePath,
|
||||
{ breakdown: BREAKDOWN },
|
||||
{ asTreeNode: true });
|
||||
|
||||
// Have to manually set these because symbol properties aren't structured
|
||||
// cloned.
|
||||
delta[CensusUtils.basisTotalBytes] = deltaTreeNode.totalBytes;
|
||||
delta[CensusUtils.basisTotalCount] = deltaTreeNode.totalCount;
|
||||
|
||||
compareCensusViewData(BREAKDOWN, delta, deltaTreeNode,
|
||||
"Returning delta-census as a tree node represents same data as the report");
|
||||
|
||||
client.destroy();
|
||||
});
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that the HeapAnalyses{Client,Worker} can take diffs between censuses as
|
||||
// inverted trees.
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
const BREAKDOWN = {
|
||||
by: "coarseType",
|
||||
objects: {
|
||||
by: "objectClass",
|
||||
then: { by: "count", count: true, bytes: true },
|
||||
other: { by: "count", count: true, bytes: true },
|
||||
},
|
||||
scripts: {
|
||||
by: "internalType",
|
||||
then: { by: "count", count: true, bytes: true },
|
||||
},
|
||||
strings: {
|
||||
by: "internalType",
|
||||
then: { by: "count", count: true, bytes: true },
|
||||
},
|
||||
other: {
|
||||
by: "internalType",
|
||||
then: { by: "count", count: true, bytes: true },
|
||||
},
|
||||
};
|
||||
|
||||
add_task(function* () {
|
||||
const firstSnapshotFilePath = saveNewHeapSnapshot();
|
||||
const secondSnapshotFilePath = saveNewHeapSnapshot();
|
||||
|
||||
const client = new HeapAnalysesClient();
|
||||
yield client.readHeapSnapshot(firstSnapshotFilePath);
|
||||
yield client.readHeapSnapshot(secondSnapshotFilePath);
|
||||
|
||||
ok(true, "Should have read both heap snapshot files");
|
||||
|
||||
const { delta } = yield client.takeCensusDiff(firstSnapshotFilePath,
|
||||
secondSnapshotFilePath,
|
||||
{ breakdown: BREAKDOWN });
|
||||
|
||||
const { delta: deltaTreeNode } = yield client.takeCensusDiff(firstSnapshotFilePath,
|
||||
secondSnapshotFilePath,
|
||||
{ breakdown: BREAKDOWN },
|
||||
{ asInvertedTreeNode: true });
|
||||
|
||||
// Have to manually set these because symbol properties aren't structured
|
||||
// cloned.
|
||||
delta[CensusUtils.basisTotalBytes] = deltaTreeNode.totalBytes;
|
||||
delta[CensusUtils.basisTotalCount] = deltaTreeNode.totalCount;
|
||||
|
||||
compareCensusViewData(BREAKDOWN, delta, deltaTreeNode, { invert: true });
|
||||
|
||||
client.destroy();
|
||||
});
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that the HeapAnalyses{Client,Worker} can take censuses.
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* () {
|
||||
const client = new HeapAnalysesClient();
|
||||
|
||||
const snapshotFilePath = saveNewHeapSnapshot();
|
||||
yield client.readHeapSnapshot(snapshotFilePath);
|
||||
ok(true, "Should have read the heap snapshot");
|
||||
|
||||
const { report } = yield client.takeCensus(snapshotFilePath);
|
||||
ok(report, "Should get a report");
|
||||
equal(typeof report, "object", "report should be an object");
|
||||
|
||||
ok(report.objects);
|
||||
ok(report.scripts);
|
||||
ok(report.strings);
|
||||
ok(report.other);
|
||||
|
||||
client.destroy();
|
||||
});
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that the HeapAnalyses{Client,Worker} can take censuses with breakdown
|
||||
// options.
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* () {
|
||||
const client = new HeapAnalysesClient();
|
||||
|
||||
const snapshotFilePath = saveNewHeapSnapshot();
|
||||
yield client.readHeapSnapshot(snapshotFilePath);
|
||||
ok(true, "Should have read the heap snapshot");
|
||||
|
||||
const { report } = yield client.takeCensus(snapshotFilePath, {
|
||||
breakdown: { by: "count", count: true, bytes: true }
|
||||
});
|
||||
|
||||
ok(report, "Should get a report");
|
||||
equal(typeof report, "object", "report should be an object");
|
||||
|
||||
ok(report.count);
|
||||
ok(report.bytes);
|
||||
|
||||
client.destroy();
|
||||
});
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that the HeapAnalyses{Client,Worker} bubbles errors properly when things
|
||||
// go wrong.
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* () {
|
||||
const client = new HeapAnalysesClient();
|
||||
|
||||
// Snapshot file path to a file that doesn't exist.
|
||||
let failed = false;
|
||||
try {
|
||||
yield client.readHeapSnapshot(getFilePath("foo-bar-baz" + Math.random(), true));
|
||||
} catch (e) {
|
||||
failed = true;
|
||||
}
|
||||
ok(failed, "should not read heap snapshots that do not exist");
|
||||
|
||||
// Snapshot file path to a file that is not a heap snapshot.
|
||||
failed = false;
|
||||
try {
|
||||
yield client.readHeapSnapshot(getFilePath("test_HeapAnalyses_takeCensus_03.js"));
|
||||
} catch (e) {
|
||||
failed = true;
|
||||
}
|
||||
ok(failed, "should not be able to read a file that is not a heap snapshot as a heap snapshot");
|
||||
|
||||
const snapshotFilePath = saveNewHeapSnapshot();
|
||||
yield client.readHeapSnapshot(snapshotFilePath);
|
||||
ok(true, "Should have read the heap snapshot");
|
||||
|
||||
// Bad census breakdown options.
|
||||
failed = false;
|
||||
try {
|
||||
yield client.takeCensus(snapshotFilePath, {
|
||||
breakdown: { by: "some classification that we do not have" }
|
||||
});
|
||||
} catch (e) {
|
||||
failed = true;
|
||||
}
|
||||
ok(failed, "should not be able to breakdown by an unknown classification");
|
||||
|
||||
client.destroy();
|
||||
});
|
||||
118
dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_04.js
Normal file
118
dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_04.js
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that the HeapAnalyses{Client,Worker} can send SavedFrame stacks from
|
||||
// by-allocation-stack reports from the worker.
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test() {
|
||||
const client = new HeapAnalysesClient();
|
||||
|
||||
// Track some allocation stacks.
|
||||
|
||||
const g = newGlobal();
|
||||
const dbg = new Debugger(g);
|
||||
g.eval(` // 1
|
||||
this.log = []; // 2
|
||||
function f() { this.log.push(allocationMarker()); } // 3
|
||||
function g() { this.log.push(allocationMarker()); } // 4
|
||||
function h() { this.log.push(allocationMarker()); } // 5
|
||||
`); // 6
|
||||
|
||||
// Create one allocationMarker with tracking turned off,
|
||||
// so it will have no associated stack.
|
||||
g.f();
|
||||
|
||||
dbg.memory.allocationSamplingProbability = 1;
|
||||
|
||||
for (let [func, n] of [ [g.f, 20],
|
||||
[g.g, 10],
|
||||
[g.h, 5] ]) {
|
||||
for (let i = 0; i < n; i++) {
|
||||
dbg.memory.trackingAllocationSites = true;
|
||||
// All allocations of allocationMarker occur with this line as the oldest
|
||||
// stack frame.
|
||||
func();
|
||||
dbg.memory.trackingAllocationSites = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Take a heap snapshot.
|
||||
|
||||
const snapshotFilePath = saveNewHeapSnapshot({ debugger: dbg });
|
||||
yield client.readHeapSnapshot(snapshotFilePath);
|
||||
ok(true, "Should have read the heap snapshot");
|
||||
|
||||
// Run a census broken down by class name -> allocation stack so we can grab
|
||||
// only the AllocationMarker objects we have complete control over.
|
||||
|
||||
const { report } = yield client.takeCensus(snapshotFilePath, {
|
||||
breakdown: { by: "objectClass",
|
||||
then: { by: "allocationStack",
|
||||
then: { by: "count",
|
||||
bytes: true,
|
||||
count: true
|
||||
},
|
||||
noStack: { by: "count",
|
||||
bytes: true,
|
||||
count: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Test the generated report.
|
||||
|
||||
ok(report, "Should get a report");
|
||||
|
||||
const map = report.AllocationMarker;
|
||||
ok(map, "Should get AllocationMarkers in the report.");
|
||||
// From a module with a different global, and therefore a different Map
|
||||
// constructor, so we can't use instanceof.
|
||||
equal(map.__proto__.constructor.name, "Map");
|
||||
|
||||
equal(map.size, 4, "Should have 4 allocation stacks (including the lack of a stack)");
|
||||
|
||||
// Gather the stacks we are expecting to appear as keys, and
|
||||
// check that there are no unexpected keys.
|
||||
let stacks = {};
|
||||
|
||||
map.forEach((v, k) => {
|
||||
if (k === "noStack") {
|
||||
// No need to save this key.
|
||||
} else if (k.functionDisplayName === "f" &&
|
||||
k.parent.functionDisplayName === "test") {
|
||||
stacks.f = k;
|
||||
} else if (k.functionDisplayName === "g" &&
|
||||
k.parent.functionDisplayName === "test") {
|
||||
stacks.g = k;
|
||||
} else if (k.functionDisplayName === "h" &&
|
||||
k.parent.functionDisplayName === "test") {
|
||||
stacks.h = k;
|
||||
} else {
|
||||
dumpn("Unexpected allocation stack:");
|
||||
k.toString().split(/\n/g).forEach(s => dumpn(s));
|
||||
ok(false);
|
||||
}
|
||||
});
|
||||
|
||||
ok(map.get("noStack"));
|
||||
equal(map.get("noStack").count, 1);
|
||||
|
||||
ok(stacks.f);
|
||||
ok(map.get(stacks.f));
|
||||
equal(map.get(stacks.f).count, 20);
|
||||
|
||||
ok(stacks.g);
|
||||
ok(map.get(stacks.g));
|
||||
equal(map.get(stacks.g).count, 10);
|
||||
|
||||
ok(stacks.h);
|
||||
ok(map.get(stacks.h));
|
||||
equal(map.get(stacks.h).count, 5);
|
||||
|
||||
client.destroy();
|
||||
});
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that the HeapAnalyses{Client,Worker} can take censuses and return
|
||||
// a CensusTreeNode.
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
const BREAKDOWN = {
|
||||
by: "internalType",
|
||||
then: { by: "count", count: true, bytes: true }
|
||||
};
|
||||
|
||||
add_task(function* () {
|
||||
const client = new HeapAnalysesClient();
|
||||
|
||||
const snapshotFilePath = saveNewHeapSnapshot();
|
||||
yield client.readHeapSnapshot(snapshotFilePath);
|
||||
ok(true, "Should have read the heap snapshot");
|
||||
|
||||
const { report } = yield client.takeCensus(snapshotFilePath, {
|
||||
breakdown: BREAKDOWN
|
||||
});
|
||||
|
||||
const { report: treeNode } = yield client.takeCensus(snapshotFilePath, {
|
||||
breakdown: BREAKDOWN
|
||||
}, {
|
||||
asTreeNode: true
|
||||
});
|
||||
|
||||
ok(treeNode.children.length > 0, "treeNode has children");
|
||||
ok(treeNode.children.every(type => {
|
||||
return "name" in type &&
|
||||
"bytes" in type &&
|
||||
"count" in type;
|
||||
}), "all of tree node's children have name, bytes, count");
|
||||
|
||||
compareCensusViewData(BREAKDOWN, report, treeNode,
|
||||
"Returning census as a tree node represents same data as the report");
|
||||
|
||||
client.destroy();
|
||||
});
|
||||
109
dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_06.js
Normal file
109
dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_06.js
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that the HeapAnalyses{Client,Worker} can take censuses by
|
||||
// "allocationStack" and return a CensusTreeNode.
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
const BREAKDOWN = {
|
||||
by: "objectClass",
|
||||
then: {
|
||||
by: "allocationStack",
|
||||
then: { by: "count", count: true, bytes: true },
|
||||
noStack: { by: "count", count: true, bytes: true }
|
||||
},
|
||||
other: { by: "count", count: true, bytes: true }
|
||||
};
|
||||
|
||||
add_task(function* () {
|
||||
const g = newGlobal();
|
||||
const dbg = new Debugger(g);
|
||||
|
||||
// 5 allocation markers with no stack.
|
||||
g.eval(`
|
||||
this.markers = [];
|
||||
for (var i = 0; i < 5; i++) {
|
||||
markers.push(allocationMarker());
|
||||
}
|
||||
`);
|
||||
|
||||
dbg.memory.allocationSamplingProbability = 1;
|
||||
dbg.memory.trackingAllocationSites = true;
|
||||
|
||||
// 5 allocation markers at 5 stacks.
|
||||
g.eval(`
|
||||
(function shouldHaveCountOfOne() {
|
||||
markers.push(allocationMarker());
|
||||
markers.push(allocationMarker());
|
||||
markers.push(allocationMarker());
|
||||
markers.push(allocationMarker());
|
||||
markers.push(allocationMarker());
|
||||
}());
|
||||
`);
|
||||
|
||||
// 5 allocation markers at 1 stack.
|
||||
g.eval(`
|
||||
(function shouldHaveCountOfFive() {
|
||||
for (var i = 0; i < 5; i++) {
|
||||
markers.push(allocationMarker());
|
||||
}
|
||||
}());
|
||||
`);
|
||||
|
||||
const snapshotFilePath = saveNewHeapSnapshot({ debugger: dbg });
|
||||
|
||||
const client = new HeapAnalysesClient();
|
||||
yield client.readHeapSnapshot(snapshotFilePath);
|
||||
ok(true, "Should have read the heap snapshot");
|
||||
|
||||
const { report } = yield client.takeCensus(snapshotFilePath, {
|
||||
breakdown: BREAKDOWN
|
||||
});
|
||||
|
||||
const { report: treeNode } = yield client.takeCensus(snapshotFilePath, {
|
||||
breakdown: BREAKDOWN
|
||||
}, {
|
||||
asTreeNode: true
|
||||
});
|
||||
|
||||
const markers = treeNode.children.find(c => c.name === "AllocationMarker");
|
||||
ok(markers);
|
||||
|
||||
const noStack = markers.children.find(c => c.name === "noStack");
|
||||
equal(noStack.count, 5);
|
||||
|
||||
let numShouldHaveFiveFound = 0;
|
||||
let numShouldHaveOneFound = 0;
|
||||
|
||||
function walk(node) {
|
||||
if (node.children) {
|
||||
node.children.forEach(walk);
|
||||
}
|
||||
|
||||
if (!isSavedFrame(node.name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.name.functionDisplayName === "shouldHaveCountOfFive") {
|
||||
equal(node.count, 5, "shouldHaveCountOfFive should have count of five");
|
||||
numShouldHaveFiveFound++;
|
||||
}
|
||||
|
||||
if (node.name.functionDisplayName === "shouldHaveCountOfOne") {
|
||||
equal(node.count, 1, "shouldHaveCountOfOne should have count of one");
|
||||
numShouldHaveOneFound++;
|
||||
}
|
||||
}
|
||||
markers.children.forEach(walk);
|
||||
|
||||
equal(numShouldHaveFiveFound, 1);
|
||||
equal(numShouldHaveOneFound, 5);
|
||||
|
||||
compareCensusViewData(BREAKDOWN, report, treeNode,
|
||||
"Returning census as a tree node represents same data as the report");
|
||||
|
||||
client.destroy();
|
||||
});
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that the HeapAnalyses{Client,Worker} can take censuses and return
|
||||
// an inverted CensusTreeNode.
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
const BREAKDOWN = {
|
||||
by: "coarseType",
|
||||
objects: {
|
||||
by: "objectClass",
|
||||
then: { by: "count", count: true, bytes: true },
|
||||
other: { by: "count", count: true, bytes: true },
|
||||
},
|
||||
scripts: {
|
||||
by: "internalType",
|
||||
then: { by: "count", count: true, bytes: true },
|
||||
},
|
||||
strings: {
|
||||
by: "internalType",
|
||||
then: { by: "count", count: true, bytes: true },
|
||||
},
|
||||
other: {
|
||||
by: "internalType",
|
||||
then: { by: "count", count: true, bytes: true },
|
||||
},
|
||||
};
|
||||
|
||||
add_task(function* () {
|
||||
const client = new HeapAnalysesClient();
|
||||
|
||||
const snapshotFilePath = saveNewHeapSnapshot();
|
||||
yield client.readHeapSnapshot(snapshotFilePath);
|
||||
ok(true, "Should have read the heap snapshot");
|
||||
|
||||
const { report } = yield client.takeCensus(snapshotFilePath, {
|
||||
breakdown: BREAKDOWN
|
||||
});
|
||||
|
||||
const { report: treeNode } = yield client.takeCensus(snapshotFilePath, {
|
||||
breakdown: BREAKDOWN
|
||||
}, {
|
||||
asInvertedTreeNode: true
|
||||
});
|
||||
|
||||
compareCensusViewData(BREAKDOWN, report, treeNode, { invert: true });
|
||||
|
||||
client.destroy();
|
||||
});
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Sanity test that we can compute shortest paths.
|
||||
//
|
||||
// Because the actual heap graph is too unpredictable and likely to drastically
|
||||
// change as various implementation bits change, we don't test exact paths
|
||||
// here. See js/src/jsapi-tests/testUbiNode.cpp for such tests, where we can
|
||||
// control the specific graph shape and structure and so testing exact paths is
|
||||
// reliable.
|
||||
|
||||
function run_test() {
|
||||
const path = ChromeUtils.saveHeapSnapshot({ runtime: true });
|
||||
const snapshot = ChromeUtils.readHeapSnapshot(path);
|
||||
|
||||
const dominatorTree = snapshot.computeDominatorTree();
|
||||
const dominatedByRoot = dominatorTree.getImmediatelyDominated(dominatorTree.root)
|
||||
.slice(0, 10);
|
||||
ok(dominatedByRoot);
|
||||
ok(dominatedByRoot.length);
|
||||
|
||||
const targetSet = new Set(dominatedByRoot);
|
||||
|
||||
const shortestPaths = snapshot.computeShortestPaths(dominatorTree.root, dominatedByRoot, 2);
|
||||
ok(shortestPaths);
|
||||
ok(shortestPaths instanceof Map);
|
||||
ok(shortestPaths.size === targetSet.size);
|
||||
|
||||
for (let [target, paths] of shortestPaths) {
|
||||
ok(targetSet.has(target),
|
||||
"We should only get paths for our targets");
|
||||
targetSet.delete(target);
|
||||
|
||||
ok(paths.length > 0,
|
||||
"We must have at least one path, since the target is dominated by the root");
|
||||
ok(paths.length <= 2,
|
||||
"Should not have recorded more paths than the max requested");
|
||||
|
||||
dumpn("---------------------");
|
||||
dumpn("Shortest paths for 0x" + target.toString(16) + ":");
|
||||
for (let path of paths) {
|
||||
dumpn(" path =");
|
||||
for (let part of path) {
|
||||
dumpn(" predecessor: 0x" + part.predecessor.toString(16) +
|
||||
"; edge: " + part.edge);
|
||||
}
|
||||
}
|
||||
dumpn("---------------------");
|
||||
|
||||
for (let path of paths) {
|
||||
ok(path.length > 0, "Cannot have zero length paths");
|
||||
ok(path[0].predecessor === dominatorTree.root,
|
||||
"The first predecessor is always our start node");
|
||||
|
||||
for (let part of path) {
|
||||
ok(part.predecessor, "Each part of a path has a predecessor");
|
||||
ok(!!snapshot.describeNode({ by: "count", count: true, bytes: true},
|
||||
part.predecessor),
|
||||
"The predecessor is in the heap snapshot");
|
||||
ok("edge" in part, "Each part has an (potentially null) edge property");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ok(targetSet.size === 0,
|
||||
"We found paths for all of our targets");
|
||||
|
||||
do_test_finished();
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test computing shortest paths with invalid arguments.
|
||||
|
||||
function run_test() {
|
||||
const path = ChromeUtils.saveHeapSnapshot({ runtime: true });
|
||||
const snapshot = ChromeUtils.readHeapSnapshot(path);
|
||||
|
||||
const dominatorTree = snapshot.computeDominatorTree();
|
||||
const target = dominatorTree.getImmediatelyDominated(dominatorTree.root).pop();
|
||||
ok(target);
|
||||
|
||||
let threw = false;
|
||||
try {
|
||||
snapshot.computeShortestPaths(0, [target], 2);
|
||||
} catch (_) {
|
||||
threw = true;
|
||||
}
|
||||
ok(threw, "invalid start node should throw");
|
||||
|
||||
threw = false;
|
||||
try {
|
||||
snapshot.computeShortestPaths(dominatorTree.root, [0], 2);
|
||||
} catch (_) {
|
||||
threw = true;
|
||||
}
|
||||
ok(threw, "invalid target nodes should throw");
|
||||
|
||||
threw = false;
|
||||
try {
|
||||
snapshot.computeShortestPaths(dominatorTree.root, [], 2);
|
||||
} catch (_) {
|
||||
threw = true;
|
||||
}
|
||||
ok(threw, "empty target nodes should throw");
|
||||
|
||||
threw = false;
|
||||
try {
|
||||
snapshot.computeShortestPaths(dominatorTree.root, [target], 0);
|
||||
} catch (_) {
|
||||
threw = true;
|
||||
}
|
||||
ok(threw, "0 max paths should throw");
|
||||
|
||||
do_test_finished();
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// HeapSnapshot.prototype.creationTime returns the expected time.
|
||||
|
||||
function waitForThirtyMilliseconds() {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < 30) ;
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
const start = Date.now() * 1000;
|
||||
do_print("start = " + start);
|
||||
|
||||
// Because Date.now() is less precise than the snapshot's time stamp, give it
|
||||
// a little bit of head room. Additionally, WinXP's timer only has granularity
|
||||
// of +/- 15ms.
|
||||
waitForThirtyMilliseconds();
|
||||
const path = ChromeUtils.saveHeapSnapshot({ runtime: true });
|
||||
waitForThirtyMilliseconds();
|
||||
|
||||
const end = Date.now() * 1000;
|
||||
do_print("end = " + end);
|
||||
|
||||
const snapshot = ChromeUtils.readHeapSnapshot(path);
|
||||
do_print("snapshot.creationTime = " + snapshot.creationTime);
|
||||
|
||||
ok(snapshot.creationTime >= start);
|
||||
ok(snapshot.creationTime <= end);
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that we can save a core dump with very deep allocation stacks and read
|
||||
// it back into a HeapSnapshot.
|
||||
|
||||
function stackDepth(stack) {
|
||||
return stack ? 1 + stackDepth(stack.parent) : 0;
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
// Create a Debugger observing a debuggee's allocations.
|
||||
const debuggee = new Cu.Sandbox(null);
|
||||
const dbg = new Debugger(debuggee);
|
||||
dbg.memory.trackingAllocationSites = true;
|
||||
|
||||
// Allocate some objects in the debuggee that will have their allocation
|
||||
// stacks recorded by the Debugger.
|
||||
|
||||
debuggee.eval("this.objects = []");
|
||||
debuggee.eval(
|
||||
(function recursiveAllocate(n) {
|
||||
if (n <= 0)
|
||||
return;
|
||||
|
||||
// Make sure to recurse before pushing the object so that when TCO is
|
||||
// implemented sometime in the future, it doesn't invalidate this test.
|
||||
recursiveAllocate(n - 1);
|
||||
this.objects.push({});
|
||||
}).toString()
|
||||
);
|
||||
debuggee.eval("recursiveAllocate = recursiveAllocate.bind(this);");
|
||||
debuggee.eval("recursiveAllocate(200);");
|
||||
|
||||
// Now save a snapshot that will include the allocation stacks and read it
|
||||
// back again.
|
||||
|
||||
const filePath = ChromeUtils.saveHeapSnapshot({ runtime: true });
|
||||
ok(true, "Should be able to save a snapshot.");
|
||||
|
||||
const snapshot = ChromeUtils.readHeapSnapshot(filePath);
|
||||
ok(snapshot, "Should be able to read a heap snapshot");
|
||||
ok(snapshot instanceof HeapSnapshot, "Should be an instanceof HeapSnapshot");
|
||||
|
||||
const report = snapshot.takeCensus({
|
||||
breakdown: { by: "allocationStack",
|
||||
then: { by: "count", bytes: true, count: true },
|
||||
noStack: { by: "count", bytes: true, count: true }
|
||||
}
|
||||
});
|
||||
|
||||
// Keep this synchronized with `HeapSnapshot::MAX_STACK_DEPTH`!
|
||||
const MAX_STACK_DEPTH = 60;
|
||||
|
||||
let foundStacks = false;
|
||||
report.forEach((v, k) => {
|
||||
if (k === "noStack") {
|
||||
return;
|
||||
}
|
||||
|
||||
foundStacks = true;
|
||||
const depth = stackDepth(k);
|
||||
dumpn("Stack depth is " + depth);
|
||||
ok(depth <= MAX_STACK_DEPTH,
|
||||
"Every stack should have depth less than or equal to the maximum stack depth");
|
||||
});
|
||||
ok(foundStacks);
|
||||
|
||||
do_test_finished();
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that we can describe nodes with a breakdown.
|
||||
|
||||
function run_test() {
|
||||
const path = saveNewHeapSnapshot();
|
||||
const snapshot = ChromeUtils.readHeapSnapshot(path);
|
||||
ok(snapshot.describeNode);
|
||||
equal(typeof snapshot.describeNode, "function");
|
||||
|
||||
const dt = snapshot.computeDominatorTree();
|
||||
|
||||
let threw = false;
|
||||
try {
|
||||
snapshot.describeNode(undefined, dt.root);
|
||||
} catch (_) {
|
||||
threw = true;
|
||||
}
|
||||
ok(threw, "Should require a breakdown");
|
||||
|
||||
const breakdown = {
|
||||
by: "coarseType",
|
||||
objects: { by: "objectClass" },
|
||||
scripts: { by: "internalType" },
|
||||
strings: { by: "internalType" },
|
||||
other: { by: "internalType" }
|
||||
};
|
||||
|
||||
threw = false;
|
||||
try {
|
||||
snapshot.describeNode(breakdown, 0);
|
||||
} catch (_) {
|
||||
threw = true;
|
||||
}
|
||||
ok(threw, "Should throw when given an invalid node id");
|
||||
|
||||
const description = snapshot.describeNode(breakdown, dt.root);
|
||||
ok(description);
|
||||
ok(description.other);
|
||||
ok(description.other["JS::ubi::RootList"]);
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// HeapSnapshot.prototype.takeCensus returns a value of an appropriate
|
||||
// shape. Ported from js/src/jit-tests/debug/Memory-takeCensus-01.js
|
||||
|
||||
function run_test() {
|
||||
var dbg = new Debugger;
|
||||
|
||||
function checkProperties(census) {
|
||||
equal(typeof census, "object");
|
||||
for (prop of Object.getOwnPropertyNames(census)) {
|
||||
var desc = Object.getOwnPropertyDescriptor(census, prop);
|
||||
equal(desc.enumerable, true);
|
||||
equal(desc.configurable, true);
|
||||
equal(desc.writable, true);
|
||||
if (typeof desc.value === "object")
|
||||
checkProperties(desc.value);
|
||||
else
|
||||
equal(typeof desc.value, "number");
|
||||
}
|
||||
}
|
||||
|
||||
checkProperties(saveHeapSnapshotAndTakeCensus(dbg));
|
||||
|
||||
var g = newGlobal();
|
||||
dbg.addDebuggee(g);
|
||||
checkProperties(saveHeapSnapshotAndTakeCensus(dbg));
|
||||
|
||||
do_test_finished();
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// HeapSnapshot.prototype.takeCensus behaves plausibly as we allocate objects.
|
||||
//
|
||||
// Exact object counts vary in ways we can't predict. For example,
|
||||
// BaselineScripts can hold onto "template objects", which exist only to hold
|
||||
// the shape and type for newly created objects. When BaselineScripts are
|
||||
// discarded, these template objects go with them.
|
||||
//
|
||||
// So instead of expecting precise counts, we expect counts that are at least as
|
||||
// many as we would expect given the object graph we've built.
|
||||
//
|
||||
// Ported from js/src/jit-tests/debug/Memory-takeCensus-02.js
|
||||
|
||||
function run_test() {
|
||||
// A Debugger with no debuggees had better not find anything.
|
||||
var dbg = new Debugger;
|
||||
var census0 = saveHeapSnapshotAndTakeCensus(dbg);
|
||||
Census.walkCensus(census0, "census0", Census.assertAllZeros);
|
||||
|
||||
function newGlobalWithDefs() {
|
||||
var g = newGlobal();
|
||||
g.eval(`
|
||||
function times(n, fn) {
|
||||
var a=[];
|
||||
for (var i = 0; i<n; i++)
|
||||
a.push(fn());
|
||||
return a;
|
||||
}
|
||||
`);
|
||||
return g;
|
||||
}
|
||||
|
||||
// Allocate a large number of various types of objects, and check that census
|
||||
// finds them.
|
||||
var g = newGlobalWithDefs();
|
||||
dbg.addDebuggee(g);
|
||||
|
||||
g.eval("var objs = times(100, () => ({}));");
|
||||
g.eval("var rxs = times(200, () => /foo/);");
|
||||
g.eval("var ars = times(400, () => []);");
|
||||
g.eval("var fns = times(800, () => () => {});");
|
||||
|
||||
var census1 = dbg.memory.takeCensus(dbg);
|
||||
Census.walkCensus(census1, "census1",
|
||||
Census.assertAllNotLessThan(
|
||||
{ "objects":
|
||||
{ "Object": { count: 100 },
|
||||
"RegExp": { count: 200 },
|
||||
"Array": { count: 400 },
|
||||
"Function": { count: 800 }
|
||||
}
|
||||
}));
|
||||
|
||||
do_test_finished();
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// HeapSnapshot.prototype.takeCensus behaves plausibly as we add and remove
|
||||
// debuggees.
|
||||
//
|
||||
// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-03.js
|
||||
|
||||
function run_test() {
|
||||
var dbg = new Debugger;
|
||||
|
||||
var census0 = saveHeapSnapshotAndTakeCensus(dbg);
|
||||
Census.walkCensus(census0, "census0", Census.assertAllZeros);
|
||||
|
||||
var g1 = newGlobal();
|
||||
dbg.addDebuggee(g1);
|
||||
var census1 = saveHeapSnapshotAndTakeCensus(dbg);
|
||||
Census.walkCensus(census1, "census1", Census.assertAllNotLessThan(census0));
|
||||
|
||||
var g2 = newGlobal();
|
||||
dbg.addDebuggee(g2);
|
||||
var census2 = saveHeapSnapshotAndTakeCensus(dbg);
|
||||
Census.walkCensus(census2, "census2", Census.assertAllNotLessThan(census1));
|
||||
|
||||
dbg.removeDebuggee(g2);
|
||||
var census3 = saveHeapSnapshotAndTakeCensus(dbg);
|
||||
Census.walkCensus(census3, "census3", Census.assertAllEqual(census1));
|
||||
|
||||
dbg.removeDebuggee(g1);
|
||||
var census4 = saveHeapSnapshotAndTakeCensus(dbg);
|
||||
Census.walkCensus(census4, "census4", Census.assertAllEqual(census0));
|
||||
|
||||
do_test_finished();
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that HeapSnapshot.prototype.takeCensus finds GC roots that are on the
|
||||
// stack.
|
||||
//
|
||||
// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-04.js
|
||||
|
||||
function run_test() {
|
||||
var g = newGlobal();
|
||||
var dbg = new Debugger(g);
|
||||
|
||||
g.eval(`
|
||||
function withAllocationMarkerOnStack(f) {
|
||||
(function () {
|
||||
var onStack = allocationMarker();
|
||||
f();
|
||||
}());
|
||||
}
|
||||
`);
|
||||
|
||||
equal("AllocationMarker" in saveHeapSnapshotAndTakeCensus(dbg).objects, false,
|
||||
"There shouldn't exist any allocation markers in the census.");
|
||||
|
||||
var allocationMarkerCount;
|
||||
g.withAllocationMarkerOnStack(() => {
|
||||
const census = saveHeapSnapshotAndTakeCensus(dbg);
|
||||
allocationMarkerCount = census.objects.AllocationMarker.count;
|
||||
});
|
||||
|
||||
equal(allocationMarkerCount, 1,
|
||||
"Should have one allocation marker in the census, because there " +
|
||||
"was one on the stack.");
|
||||
|
||||
do_test_finished();
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that HeapSnapshot.prototype.takeCensus finds cross compartment
|
||||
// wrapper GC roots.
|
||||
//
|
||||
// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-05.js
|
||||
|
||||
function run_test() {
|
||||
var g = newGlobal();
|
||||
var dbg = new Debugger(g);
|
||||
|
||||
equal("AllocationMarker" in saveHeapSnapshotAndTakeCensus(dbg).objects, false,
|
||||
"No allocation markers should exist in the census.");
|
||||
|
||||
this.ccw = g.allocationMarker();
|
||||
|
||||
const census = saveHeapSnapshotAndTakeCensus(dbg);
|
||||
equal(census.objects.AllocationMarker.count, 1,
|
||||
"Should have one allocation marker in the census, because there " +
|
||||
"is one cross-compartment wrapper referring to it.");
|
||||
|
||||
do_test_finished();
|
||||
}
|
||||
125
dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_06.js
Normal file
125
dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_06.js
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Check HeapSnapshot.prototype.takeCensus handling of 'breakdown' argument.
|
||||
//
|
||||
// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-06.js
|
||||
|
||||
function run_test() {
|
||||
var Pattern = Match.Pattern;
|
||||
|
||||
var g = newGlobal();
|
||||
var dbg = new Debugger(g);
|
||||
|
||||
Pattern({ count: Pattern.NATURAL,
|
||||
bytes: Pattern.NATURAL })
|
||||
.assert(saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "count" } }));
|
||||
|
||||
let census = saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "count", count: false, bytes: false } });
|
||||
equal("count" in census, false);
|
||||
equal("bytes" in census, false);
|
||||
|
||||
census = saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "count", count: true, bytes: false } });
|
||||
equal("count" in census, true);
|
||||
equal("bytes" in census, false);
|
||||
|
||||
census = saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "count", count: false, bytes: true } });
|
||||
equal("count" in census, false);
|
||||
equal("bytes" in census, true);
|
||||
|
||||
census = saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "count", count: true, bytes: true } });
|
||||
equal("count" in census, true);
|
||||
equal("bytes" in census, true);
|
||||
|
||||
|
||||
// Pattern doesn't mind objects with extra properties, so we'll restrict this
|
||||
// list to the object classes we're pretty sure are going to stick around for
|
||||
// the forseeable future.
|
||||
Pattern({
|
||||
Function: { count: Pattern.NATURAL },
|
||||
Object: { count: Pattern.NATURAL },
|
||||
Debugger: { count: Pattern.NATURAL },
|
||||
Sandbox: { count: Pattern.NATURAL },
|
||||
|
||||
// The below are all Debugger prototype objects.
|
||||
Source: { count: Pattern.NATURAL },
|
||||
Environment: { count: Pattern.NATURAL },
|
||||
Script: { count: Pattern.NATURAL },
|
||||
Memory: { count: Pattern.NATURAL },
|
||||
Frame: { count: Pattern.NATURAL }
|
||||
})
|
||||
.assert(saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "objectClass" } }));
|
||||
|
||||
Pattern({
|
||||
objects: { count: Pattern.NATURAL },
|
||||
scripts: { count: Pattern.NATURAL },
|
||||
strings: { count: Pattern.NATURAL },
|
||||
other: { count: Pattern.NATURAL }
|
||||
})
|
||||
.assert(saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "coarseType" } }));
|
||||
|
||||
// As for { by: 'objectClass' }, restrict our pattern to the types
|
||||
// we predict will stick around for a long time.
|
||||
Pattern({
|
||||
JSString: { count: Pattern.NATURAL },
|
||||
"js::Shape": { count: Pattern.NATURAL },
|
||||
JSObject: { count: Pattern.NATURAL },
|
||||
JSScript: { count: Pattern.NATURAL }
|
||||
})
|
||||
.assert(saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "internalType" } }));
|
||||
|
||||
|
||||
// Nested breakdowns.
|
||||
|
||||
let coarseTypePattern = {
|
||||
objects: { count: Pattern.NATURAL },
|
||||
scripts: { count: Pattern.NATURAL },
|
||||
strings: { count: Pattern.NATURAL },
|
||||
other: { count: Pattern.NATURAL }
|
||||
};
|
||||
|
||||
Pattern({
|
||||
JSString: coarseTypePattern,
|
||||
"js::Shape": coarseTypePattern,
|
||||
JSObject: coarseTypePattern,
|
||||
JSScript: coarseTypePattern,
|
||||
})
|
||||
.assert(saveHeapSnapshotAndTakeCensus(dbg, {
|
||||
breakdown: { by: "internalType",
|
||||
then: { by: "coarseType" }
|
||||
}
|
||||
}));
|
||||
|
||||
Pattern({
|
||||
Function: { count: Pattern.NATURAL },
|
||||
Object: { count: Pattern.NATURAL },
|
||||
Debugger: { count: Pattern.NATURAL },
|
||||
Sandbox: { count: Pattern.NATURAL },
|
||||
other: coarseTypePattern
|
||||
})
|
||||
.assert(saveHeapSnapshotAndTakeCensus(dbg, {
|
||||
breakdown: {
|
||||
by: "objectClass",
|
||||
then: { by: "count" },
|
||||
other: { by: "coarseType" }
|
||||
}
|
||||
}));
|
||||
|
||||
Pattern({
|
||||
objects: { count: Pattern.NATURAL, label: "object" },
|
||||
scripts: { count: Pattern.NATURAL, label: "scripts" },
|
||||
strings: { count: Pattern.NATURAL, label: "strings" },
|
||||
other: { count: Pattern.NATURAL, label: "other" }
|
||||
})
|
||||
.assert(saveHeapSnapshotAndTakeCensus(dbg, {
|
||||
breakdown: {
|
||||
by: "coarseType",
|
||||
objects: { by: "count", label: "object" },
|
||||
scripts: { by: "count", label: "scripts" },
|
||||
strings: { by: "count", label: "strings" },
|
||||
other: { by: "count", label: "other" }
|
||||
}
|
||||
}));
|
||||
|
||||
do_test_finished();
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// HeapSnapshot.prototype.takeCensus breakdown: check error handling on property
|
||||
// gets.
|
||||
//
|
||||
// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-07.js
|
||||
|
||||
function run_test() {
|
||||
var g = newGlobal();
|
||||
var dbg = new Debugger(g);
|
||||
|
||||
assertThrowsValue(() => {
|
||||
saveHeapSnapshotAndTakeCensus(dbg, {
|
||||
breakdown: { get by() { throw "ಠ_ಠ"; } }
|
||||
});
|
||||
}, "ಠ_ಠ");
|
||||
|
||||
|
||||
|
||||
assertThrowsValue(() => {
|
||||
saveHeapSnapshotAndTakeCensus(dbg, {
|
||||
breakdown: { by: "count", get count() { throw "ಠ_ಠ"; } }
|
||||
});
|
||||
}, "ಠ_ಠ");
|
||||
|
||||
assertThrowsValue(() => {
|
||||
saveHeapSnapshotAndTakeCensus(dbg, {
|
||||
breakdown: { by: "count", get bytes() { throw "ಠ_ಠ"; } }
|
||||
});
|
||||
}, "ಠ_ಠ");
|
||||
|
||||
|
||||
|
||||
assertThrowsValue(() => {
|
||||
saveHeapSnapshotAndTakeCensus(dbg, {
|
||||
breakdown: { by: "objectClass", get then() { throw "ಠ_ಠ"; } }
|
||||
});
|
||||
}, "ಠ_ಠ");
|
||||
|
||||
assertThrowsValue(() => {
|
||||
saveHeapSnapshotAndTakeCensus(dbg, {
|
||||
breakdown: { by: "objectClass", get other() { throw "ಠ_ಠ"; } }
|
||||
});
|
||||
}, "ಠ_ಠ");
|
||||
|
||||
|
||||
|
||||
assertThrowsValue(() => {
|
||||
saveHeapSnapshotAndTakeCensus(dbg, {
|
||||
breakdown: { by: "coarseType", get objects() { throw "ಠ_ಠ"; } }
|
||||
});
|
||||
}, "ಠ_ಠ");
|
||||
|
||||
assertThrowsValue(() => {
|
||||
saveHeapSnapshotAndTakeCensus(dbg, {
|
||||
breakdown: { by: "coarseType", get scripts() { throw "ಠ_ಠ"; } }
|
||||
});
|
||||
}, "ಠ_ಠ");
|
||||
|
||||
assertThrowsValue(() => {
|
||||
saveHeapSnapshotAndTakeCensus(dbg, {
|
||||
breakdown: { by: "coarseType", get strings() { throw "ಠ_ಠ"; } }
|
||||
});
|
||||
}, "ಠ_ಠ");
|
||||
|
||||
assertThrowsValue(() => {
|
||||
saveHeapSnapshotAndTakeCensus(dbg, {
|
||||
breakdown: { by: "coarseType", get other() { throw "ಠ_ಠ"; } }
|
||||
});
|
||||
}, "ಠ_ಠ");
|
||||
|
||||
|
||||
|
||||
assertThrowsValue(() => {
|
||||
saveHeapSnapshotAndTakeCensus(dbg, {
|
||||
breakdown: { by: "internalType", get then() { throw "ಠ_ಠ"; } }
|
||||
});
|
||||
}, "ಠ_ಠ");
|
||||
|
||||
do_test_finished();
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// HeapSnapshot.prototype.takeCensus: test by: 'count' breakdown
|
||||
//
|
||||
// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-08.js
|
||||
|
||||
function run_test() {
|
||||
let g = newGlobal();
|
||||
let dbg = new Debugger(g);
|
||||
|
||||
g.eval(`
|
||||
var stuff = [];
|
||||
function add(n, c) {
|
||||
for (let i = 0; i < n; i++)
|
||||
stuff.push(c());
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
|
||||
function obj() { return { count: count++ }; }
|
||||
obj.factor = 1;
|
||||
|
||||
// This creates a closure (a function JSObject) that has captured
|
||||
// a Call object. So each call creates two items.
|
||||
function fun() { let v = count; return () => { return v; } }
|
||||
fun.factor = 2;
|
||||
|
||||
function str() { return 'perambulator' + count++; }
|
||||
str.factor = 1;
|
||||
|
||||
// Eval a fresh text each time, allocating:
|
||||
// - a fresh ScriptSourceObject
|
||||
// - a new JSScripts, not an eval cache hits
|
||||
// - a fresh prototype object
|
||||
// - a fresh Call object, since the eval makes 'ev' heavyweight
|
||||
// - the new function itself
|
||||
function ev() {
|
||||
return eval(\`(function () { return \${ count++ } })\`);
|
||||
}
|
||||
ev.factor = 5;
|
||||
|
||||
// A new object (1) with a new shape (2) with a new atom (3)
|
||||
function shape() { return { [ 'theobroma' + count++ ]: count }; }
|
||||
shape.factor = 3;
|
||||
`);
|
||||
|
||||
let baseline = 0;
|
||||
function countIncreasedByAtLeast(n) {
|
||||
let oldBaseline = baseline;
|
||||
|
||||
// Since a census counts only reachable objects, one might assume that calling
|
||||
// GC here would have no effect on the census results. But GC also throws away
|
||||
// JIT code and any objects it might be holding (template objects, say);
|
||||
// takeCensus reaches those. Shake everything loose that we can, to make the
|
||||
// census approximate reachability a bit more closely, and make our results a
|
||||
// bit more predictable.
|
||||
gc(g, "shrinking");
|
||||
|
||||
baseline = saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "count" } }).count;
|
||||
return baseline >= oldBaseline + n;
|
||||
}
|
||||
|
||||
countIncreasedByAtLeast(0);
|
||||
|
||||
g.add(100, g.obj);
|
||||
ok(countIncreasedByAtLeast(g.obj.factor * 100));
|
||||
|
||||
g.add(100, g.fun);
|
||||
ok(countIncreasedByAtLeast(g.fun.factor * 100));
|
||||
|
||||
g.add(100, g.str);
|
||||
ok(countIncreasedByAtLeast(g.str.factor * 100));
|
||||
|
||||
g.add(100, g.ev);
|
||||
ok(countIncreasedByAtLeast(g.ev.factor * 100));
|
||||
|
||||
g.add(100, g.shape);
|
||||
ok(countIncreasedByAtLeast(g.shape.factor * 100));
|
||||
|
||||
do_test_finished();
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// HeapSnapshot.prototype.takeCensus: by: allocationStack breakdown
|
||||
//
|
||||
// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-09.js
|
||||
|
||||
function run_test() {
|
||||
var g = newGlobal();
|
||||
var dbg = new Debugger(g);
|
||||
|
||||
g.eval(` // 1
|
||||
var log = []; // 2
|
||||
function f() { log.push(allocationMarker()); } // 3
|
||||
function g() { f(); } // 4
|
||||
function h() { f(); } // 5
|
||||
`); // 6
|
||||
|
||||
// Create one allocationMarker with tracking turned off,
|
||||
// so it will have no associated stack.
|
||||
g.f();
|
||||
|
||||
dbg.memory.allocationSamplingProbability = 1;
|
||||
|
||||
for ([func, n] of [[g.f, 20], [g.g, 10], [g.h, 5]]) {
|
||||
for (let i = 0; i < n; i++) {
|
||||
dbg.memory.trackingAllocationSites = true;
|
||||
// All allocations of allocationMarker occur with this line as the oldest
|
||||
// stack frame.
|
||||
func();
|
||||
dbg.memory.trackingAllocationSites = false;
|
||||
}
|
||||
}
|
||||
|
||||
let census = saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "objectClass",
|
||||
then: { by: "allocationStack",
|
||||
then: { by: "count",
|
||||
label: "haz stack"
|
||||
},
|
||||
noStack: { by: "count",
|
||||
label: "no haz stack"
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let map = census.AllocationMarker;
|
||||
ok(map instanceof Map, "Should be a Map instance");
|
||||
equal(map.size, 4, "Should have 4 allocation stacks (including the lack of a stack)");
|
||||
|
||||
// Gather the stacks we are expecting to appear as keys, and
|
||||
// check that there are no unexpected keys.
|
||||
let stacks = { };
|
||||
|
||||
map.forEach((v, k) => {
|
||||
if (k === "noStack") {
|
||||
// No need to save this key.
|
||||
} else if (k.functionDisplayName === "f" &&
|
||||
k.parent.functionDisplayName === "run_test") {
|
||||
stacks.f = k;
|
||||
} else if (k.functionDisplayName === "f" &&
|
||||
k.parent.functionDisplayName === "g" &&
|
||||
k.parent.parent.functionDisplayName === "run_test") {
|
||||
stacks.fg = k;
|
||||
} else if (k.functionDisplayName === "f" &&
|
||||
k.parent.functionDisplayName === "h" &&
|
||||
k.parent.parent.functionDisplayName === "run_test") {
|
||||
stacks.fh = k;
|
||||
} else {
|
||||
dumpn("Unexpected allocation stack:");
|
||||
k.toString().split(/\n/g).forEach(s => dumpn(s));
|
||||
ok(false);
|
||||
}
|
||||
});
|
||||
|
||||
equal(map.get("noStack").label, "no haz stack");
|
||||
equal(map.get("noStack").count, 1);
|
||||
|
||||
ok(stacks.f);
|
||||
equal(map.get(stacks.f).label, "haz stack");
|
||||
equal(map.get(stacks.f).count, 20);
|
||||
|
||||
ok(stacks.fg);
|
||||
equal(map.get(stacks.fg).label, "haz stack");
|
||||
equal(map.get(stacks.fg).count, 10);
|
||||
|
||||
ok(stacks.fh);
|
||||
equal(map.get(stacks.fh).label, "haz stack");
|
||||
equal(map.get(stacks.fh).count, 5);
|
||||
|
||||
do_test_finished();
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Check byte counts produced by takeCensus.
|
||||
//
|
||||
// Ported from js/src/jit-test/tests/debug/Memory-take Census-10.js
|
||||
|
||||
function run_test() {
|
||||
let g = newGlobal();
|
||||
let dbg = new Debugger(g);
|
||||
|
||||
let sizeOfAM = byteSize(allocationMarker());
|
||||
|
||||
// Allocate a single allocation marker, and check that we can find it.
|
||||
g.eval("var hold = allocationMarker();");
|
||||
let census = saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "objectClass" } });
|
||||
equal(census.AllocationMarker.count, 1);
|
||||
equal(census.AllocationMarker.bytes, sizeOfAM);
|
||||
g.hold = null;
|
||||
|
||||
g.eval(` // 1
|
||||
var objs = []; // 2
|
||||
function fnerd() { // 3
|
||||
objs.push(allocationMarker()); // 4
|
||||
for (let i = 0; i < 10; i++) // 5
|
||||
objs.push(allocationMarker()); // 6
|
||||
} // 7
|
||||
`); // 8
|
||||
|
||||
dbg.memory.allocationSamplingProbability = 1;
|
||||
dbg.memory.trackingAllocationSites = true;
|
||||
g.fnerd();
|
||||
dbg.memory.trackingAllocationSites = false;
|
||||
|
||||
census = saveHeapSnapshotAndTakeCensus(dbg, {
|
||||
breakdown: { by: "objectClass",
|
||||
then: { by: "allocationStack" }
|
||||
}
|
||||
});
|
||||
|
||||
let seen = 0;
|
||||
census.AllocationMarker.forEach((v, k) => {
|
||||
equal(k.functionDisplayName, "fnerd");
|
||||
switch (k.line) {
|
||||
case 4:
|
||||
equal(v.count, 1);
|
||||
equal(v.bytes, sizeOfAM);
|
||||
seen++;
|
||||
break;
|
||||
|
||||
case 6:
|
||||
equal(v.count, 10);
|
||||
equal(v.bytes, 10 * sizeOfAM);
|
||||
seen++;
|
||||
break;
|
||||
|
||||
default:
|
||||
dumpn("Unexpected stack:");
|
||||
k.toString().split(/\n/g).forEach(s => dumpn(s));
|
||||
ok(false);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
equal(seen, 2);
|
||||
|
||||
do_test_finished();
|
||||
}
|
||||
116
dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_11.js
Normal file
116
dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_11.js
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that Debugger.Memory.prototype.takeCensus and
|
||||
// HeapSnapshot.prototype.takeCensus return the same data for the same heap
|
||||
// graph.
|
||||
|
||||
function doLiveAndOfflineCensus(g, dbg, opts) {
|
||||
dbg.memory.allocationSamplingProbability = 1;
|
||||
dbg.memory.trackingAllocationSites = true;
|
||||
g.eval(` // 1
|
||||
(function unsafeAtAnySpeed() { // 2
|
||||
for (var i = 0; i < 100; i++) { // 3
|
||||
this.markers.push(allocationMarker()); // 4
|
||||
} // 5
|
||||
}()); // 6
|
||||
`); // 7
|
||||
dbg.memory.trackingAllocationSites = false;
|
||||
|
||||
return {
|
||||
live: dbg.memory.takeCensus(opts),
|
||||
offline: saveHeapSnapshotAndTakeCensus(dbg, opts)
|
||||
};
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
var g = newGlobal();
|
||||
var dbg = new Debugger(g);
|
||||
|
||||
g.eval("this.markers = []");
|
||||
const markerSize = byteSize(allocationMarker());
|
||||
|
||||
// First, test that we get the same counts and sizes as we allocate and retain
|
||||
// more things.
|
||||
|
||||
let prevCount = 0;
|
||||
let prevBytes = 0;
|
||||
|
||||
for (var i = 0; i < 10; i++) {
|
||||
const { live, offline } = doLiveAndOfflineCensus(g, dbg, {
|
||||
breakdown: { by: "objectClass",
|
||||
then: { by: "count"} }
|
||||
});
|
||||
|
||||
equal(live.AllocationMarker.count, offline.AllocationMarker.count);
|
||||
equal(live.AllocationMarker.bytes, offline.AllocationMarker.bytes);
|
||||
equal(live.AllocationMarker.count, prevCount + 100);
|
||||
equal(live.AllocationMarker.bytes, prevBytes + 100 * markerSize);
|
||||
|
||||
prevCount = live.AllocationMarker.count;
|
||||
prevBytes = live.AllocationMarker.bytes;
|
||||
}
|
||||
|
||||
// Second, test that the reported allocation stacks and counts and sizes at
|
||||
// those allocation stacks match up.
|
||||
|
||||
const { live, offline } = doLiveAndOfflineCensus(g, dbg, {
|
||||
breakdown: { by: "objectClass",
|
||||
then: { by: "allocationStack"} }
|
||||
});
|
||||
|
||||
equal(live.AllocationMarker.size, offline.AllocationMarker.size);
|
||||
// One stack with the loop further above, and another stack featuring the call
|
||||
// right above.
|
||||
equal(live.AllocationMarker.size, 2);
|
||||
|
||||
// Note that because SavedFrame stacks reconstructed from an offline heap
|
||||
// snapshot don't have the same principals as SavedFrame stacks captured from
|
||||
// a live stack, the live and offline allocation stacks won't be identity
|
||||
// equal, but should be structurally the same.
|
||||
|
||||
const liveEntries = [];
|
||||
live.AllocationMarker.forEach((v, k) => {
|
||||
dumpn("Allocation stack:");
|
||||
k.toString().split(/\n/g).forEach(s => dumpn(s));
|
||||
|
||||
equal(k.functionDisplayName, "unsafeAtAnySpeed");
|
||||
equal(k.line, 4);
|
||||
|
||||
liveEntries.push([k.toString(), v]);
|
||||
});
|
||||
|
||||
const offlineEntries = [];
|
||||
offline.AllocationMarker.forEach((v, k) => {
|
||||
dumpn("Allocation stack:");
|
||||
k.toString().split(/\n/g).forEach(s => dumpn(s));
|
||||
|
||||
equal(k.functionDisplayName, "unsafeAtAnySpeed");
|
||||
equal(k.line, 4);
|
||||
|
||||
offlineEntries.push([k.toString(), v]);
|
||||
});
|
||||
|
||||
const sortEntries = (a, b) => {
|
||||
if (a[0] < b[0]) {
|
||||
return -1;
|
||||
} else if (a[0] > b[0]) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
liveEntries.sort(sortEntries);
|
||||
offlineEntries.sort(sortEntries);
|
||||
|
||||
equal(liveEntries.length, live.AllocationMarker.size);
|
||||
equal(liveEntries.length, offlineEntries.length);
|
||||
|
||||
for (let i = 0; i < liveEntries.length; i++) {
|
||||
equal(liveEntries[i][0], offlineEntries[i][0]);
|
||||
equal(liveEntries[i][1].count, offlineEntries[i][1].count);
|
||||
equal(liveEntries[i][1].bytes, offlineEntries[i][1].bytes);
|
||||
}
|
||||
|
||||
do_test_finished();
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that when we take a census and get a bucket list of ids that matched the
|
||||
// given category, that the returned ids are all in the snapshot and their
|
||||
// reported category.
|
||||
|
||||
function run_test() {
|
||||
const g = newGlobal();
|
||||
const dbg = new Debugger(g);
|
||||
|
||||
const path = saveNewHeapSnapshot({ debugger: dbg });
|
||||
const snapshot = readHeapSnapshot(path);
|
||||
|
||||
const bucket = { by: "bucket" };
|
||||
const count = { by: "count", count: true, bytes: false };
|
||||
const objectClassCount = { by: "objectClass", then: count, other: count };
|
||||
|
||||
const byClassName = snapshot.takeCensus({
|
||||
breakdown: {
|
||||
by: "objectClass",
|
||||
then: bucket,
|
||||
other: bucket,
|
||||
}
|
||||
});
|
||||
|
||||
const byClassNameCount = snapshot.takeCensus({
|
||||
breakdown: objectClassCount
|
||||
});
|
||||
|
||||
const keys = new Set(Object.keys(byClassName));
|
||||
equal(keys.size, Object.keys(byClassNameCount).length,
|
||||
"Should have the same number of keys.");
|
||||
for (let k of Object.keys(byClassNameCount)) {
|
||||
ok(keys.has(k), "Should not have any unexpected class names");
|
||||
}
|
||||
|
||||
for (let key of Object.keys(byClassName)) {
|
||||
equal(byClassNameCount[key].count, byClassName[key].length,
|
||||
"Length of the bucket and count should be equal");
|
||||
|
||||
for (let id of byClassName[key]) {
|
||||
const desc = snapshot.describeNode(objectClassCount, id);
|
||||
equal(desc[key].count, 1,
|
||||
"Describing the bucketed node confirms that it belongs to the category");
|
||||
}
|
||||
}
|
||||
|
||||
do_test_finished();
|
||||
}
|
||||
20
dom/heapsnapshot/tests/unit/test_ReadHeapSnapshot.js
Normal file
20
dom/heapsnapshot/tests/unit/test_ReadHeapSnapshot.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that we can read core dumps into HeapSnapshot instances.
|
||||
|
||||
if (typeof Debugger != "function") {
|
||||
const { addDebuggerToGlobal } = Cu.import("resource://gre/modules/jsdebugger.jsm", {});
|
||||
addDebuggerToGlobal(this);
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
const filePath = ChromeUtils.saveHeapSnapshot({ globals: [this] });
|
||||
ok(true, "Should be able to save a snapshot.");
|
||||
|
||||
const snapshot = ChromeUtils.readHeapSnapshot(filePath);
|
||||
ok(snapshot, "Should be able to read a heap snapshot");
|
||||
ok(snapshot instanceof HeapSnapshot, "Should be an instanceof HeapSnapshot");
|
||||
|
||||
do_test_finished();
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that we can save a core dump with allocation stacks and read it back
|
||||
// into a HeapSnapshot.
|
||||
|
||||
if (typeof Debugger != "function") {
|
||||
const { addDebuggerToGlobal } = Cu.import("resource://gre/modules/jsdebugger.jsm", {});
|
||||
addDebuggerToGlobal(this);
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
// Create a Debugger observing a debuggee's allocations.
|
||||
const debuggee = new Cu.Sandbox(null);
|
||||
const dbg = new Debugger(debuggee);
|
||||
dbg.memory.trackingAllocationSites = true;
|
||||
|
||||
// Allocate some objects in the debuggee that will have their allocation
|
||||
// stacks recorded by the Debugger.
|
||||
debuggee.eval("this.objects = []");
|
||||
for (let i = 0; i < 100; i++) {
|
||||
debuggee.eval("this.objects.push({})");
|
||||
}
|
||||
|
||||
// Now save a snapshot that will include the allocation stacks and read it
|
||||
// back again.
|
||||
|
||||
const filePath = ChromeUtils.saveHeapSnapshot({ runtime: true });
|
||||
ok(true, "Should be able to save a snapshot.");
|
||||
|
||||
const snapshot = ChromeUtils.readHeapSnapshot(filePath);
|
||||
ok(snapshot, "Should be able to read a heap snapshot");
|
||||
ok(snapshot instanceof HeapSnapshot, "Should be an instanceof HeapSnapshot");
|
||||
|
||||
do_test_finished();
|
||||
}
|
||||
40
dom/heapsnapshot/tests/unit/test_ReadHeapSnapshot_worker.js
Normal file
40
dom/heapsnapshot/tests/unit/test_ReadHeapSnapshot_worker.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that we can read core dumps into HeapSnapshot instances in a worker.
|
||||
|
||||
add_task(function* () {
|
||||
const worker = new ChromeWorker("resource://test/heap-snapshot-worker.js");
|
||||
worker.postMessage({});
|
||||
|
||||
let assertionCount = 0;
|
||||
worker.onmessage = e => {
|
||||
if (e.data.type !== "assertion") {
|
||||
return;
|
||||
}
|
||||
|
||||
ok(e.data.passed, e.data.msg + "\n" + e.data.stack);
|
||||
assertionCount++;
|
||||
};
|
||||
|
||||
yield waitForDone(worker);
|
||||
|
||||
ok(assertionCount > 0);
|
||||
worker.terminate();
|
||||
});
|
||||
|
||||
function waitForDone(w) {
|
||||
return new Promise((resolve, reject) => {
|
||||
w.onerror = e => {
|
||||
reject();
|
||||
ok(false, "Error in worker: " + e);
|
||||
};
|
||||
|
||||
w.addEventListener("message", function listener(e) {
|
||||
if (e.data.type === "done") {
|
||||
w.removeEventListener("message", listener, false);
|
||||
resolve();
|
||||
}
|
||||
}, false);
|
||||
});
|
||||
}
|
||||
82
dom/heapsnapshot/tests/unit/test_SaveHeapSnapshot.js
Normal file
82
dom/heapsnapshot/tests/unit/test_SaveHeapSnapshot.js
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test the ChromeUtils interface.
|
||||
|
||||
if (typeof Debugger != "function") {
|
||||
const { addDebuggerToGlobal } = Cu.import("resource://gre/modules/jsdebugger.jsm", {});
|
||||
addDebuggerToGlobal(this);
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
ok(ChromeUtils, "Should be able to get the ChromeUtils interface");
|
||||
|
||||
testBadParameters();
|
||||
testGoodParameters();
|
||||
|
||||
do_test_finished();
|
||||
}
|
||||
|
||||
function testBadParameters() {
|
||||
throws(() => ChromeUtils.saveHeapSnapshot(),
|
||||
"Should throw if arguments aren't passed in.");
|
||||
|
||||
throws(() => ChromeUtils.saveHeapSnapshot(null),
|
||||
"Should throw if boundaries isn't an object.");
|
||||
|
||||
throws(() => ChromeUtils.saveHeapSnapshot({}),
|
||||
"Should throw if the boundaries object doesn't have any properties.");
|
||||
|
||||
throws(() => ChromeUtils.saveHeapSnapshot({ runtime: true,
|
||||
globals: [this] }),
|
||||
"Should throw if the boundaries object has more than one property.");
|
||||
|
||||
throws(() => ChromeUtils.saveHeapSnapshot({ debugger: {} }),
|
||||
"Should throw if the debuggees object is not a Debugger object");
|
||||
|
||||
throws(() => ChromeUtils.saveHeapSnapshot({ globals: [{}] }),
|
||||
"Should throw if the globals array contains non-global objects.");
|
||||
|
||||
throws(() => ChromeUtils.saveHeapSnapshot({ runtime: false }),
|
||||
"Should throw if runtime is supplied and is not true.");
|
||||
|
||||
throws(() => ChromeUtils.saveHeapSnapshot({ globals: null }),
|
||||
"Should throw if globals is not an object.");
|
||||
|
||||
throws(() => ChromeUtils.saveHeapSnapshot({ globals: {} }),
|
||||
"Should throw if globals is not an array.");
|
||||
|
||||
throws(() => ChromeUtils.saveHeapSnapshot({ debugger: Debugger.prototype }),
|
||||
"Should throw if debugger is the Debugger.prototype object.");
|
||||
|
||||
throws(() => ChromeUtils.saveHeapSnapshot({ get globals() { return [this]; } }),
|
||||
"Should throw if boundaries property is a getter.");
|
||||
}
|
||||
|
||||
const makeNewSandbox = () =>
|
||||
Cu.Sandbox(CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")());
|
||||
|
||||
function testGoodParameters() {
|
||||
let sandbox = makeNewSandbox();
|
||||
let dbg = new Debugger(sandbox);
|
||||
|
||||
ChromeUtils.saveHeapSnapshot({ debugger: dbg });
|
||||
ok(true, "Should be able to save a snapshot for a debuggee global.");
|
||||
|
||||
dbg = new Debugger;
|
||||
let sandboxes = Array(10).fill(null).map(makeNewSandbox);
|
||||
sandboxes.forEach(sb => dbg.addDebuggee(sb));
|
||||
|
||||
ChromeUtils.saveHeapSnapshot({ debugger: dbg });
|
||||
ok(true, "Should be able to save a snapshot for many debuggee globals.");
|
||||
|
||||
dbg = new Debugger;
|
||||
ChromeUtils.saveHeapSnapshot({ debugger: dbg });
|
||||
ok(true, "Should be able to save a snapshot with no debuggee globals.");
|
||||
|
||||
ChromeUtils.saveHeapSnapshot({ globals: [this] });
|
||||
ok(true, "Should be able to save a snapshot for a specific global.");
|
||||
|
||||
ChromeUtils.saveHeapSnapshot({ runtime: true });
|
||||
ok(true, "Should be able to save a snapshot of the full runtime.");
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue