Reclassify heapsnapshot and nsJSInspector as not part of devtools

This resolves Issue #316
This commit is contained in:
Matt A. Tobin 2020-02-22 17:32:39 -05:00 committed by Roy Tam
commit 95d5dcd8c5
142 changed files with 19 additions and 15 deletions

View file

@ -1 +0,0 @@
CoreDump.pb.* binary

View file

@ -1,64 +0,0 @@
/* -*- 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

View file

@ -1,75 +0,0 @@
/* -*- 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

View file

@ -1,489 +0,0 @@
/* 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/. */
"use strict";
const { flatten } = require("resource://devtools/shared/ThreadSafeDevToolsUtils.js");
/** * Visitor ****************************************************************/
/**
* A Visitor visits each node and edge of a census report tree as the census
* report is being traversed by `walk`.
*/
function Visitor() { }
exports.Visitor = Visitor;
/**
* The `enter` method is called when a new sub-report is entered in traversal.
*
* @param {Object} breakdown
* The breakdown for the sub-report that is being entered by traversal.
*
* @param {Object} report
* The report generated by the given breakdown.
*
* @param {any} edge
* The edge leading to this sub-report. The edge is null if (but not iff!
* eg, null allocation stack edges) we are entering the root report.
*/
Visitor.prototype.enter = function (breakdown, report, edge) { };
/**
* The `exit` method is called when traversal of a sub-report has finished.
*
* @param {Object} breakdown
* The breakdown for the sub-report whose traversal has finished.
*
* @param {Object} report
* The report generated by the given breakdown.
*
* @param {any} edge
* The edge leading to this sub-report. The edge is null if (but not iff!
* eg, null allocation stack edges) we are entering the root report.
*/
Visitor.prototype.exit = function (breakdown, report, edge) { };
/**
* The `count` method is called when leaf nodes (reports whose breakdown is
* by: "count") in the report tree are encountered.
*
* @param {Object} breakdown
* The count breakdown for this report.
*
* @param {Object} report
* The report generated by a breakdown by "count".
*
* @param {any|null} edge
* The edge leading to this count report. The edge is null if we are
* entering the root report.
*/
Visitor.prototype.count = function (breakdown, report, edge) { };
/** * getReportEdges *********************************************************/
const EDGES = Object.create(null);
EDGES.count = function (breakdown, report) {
return [];
};
EDGES.bucket = function (breakdown, report) {
return [];
};
EDGES.internalType = function (breakdown, report) {
return Object.keys(report).map(key => ({
edge: key,
referent: report[key],
breakdown: breakdown.then
}));
};
EDGES.objectClass = function (breakdown, report) {
return Object.keys(report).map(key => ({
edge: key,
referent: report[key],
breakdown: key === "other" ? breakdown.other : breakdown.then
}));
};
EDGES.coarseType = function (breakdown, report) {
return [
{ edge: "objects", referent: report.objects, breakdown: breakdown.objects },
{ edge: "scripts", referent: report.scripts, breakdown: breakdown.scripts },
{ edge: "strings", referent: report.strings, breakdown: breakdown.strings },
{ edge: "other", referent: report.other, breakdown: breakdown.other },
];
};
EDGES.allocationStack = function (breakdown, report) {
const edges = [];
report.forEach((value, key) => {
edges.push({
edge: key,
referent: value,
breakdown: key === "noStack" ? breakdown.noStack : breakdown.then
});
});
return edges;
};
EDGES.filename = function (breakdown, report) {
return Object.keys(report).map(key => ({
edge: key,
referent: report[key],
breakdown: key === "noFilename" ? breakdown.noFilename : breakdown.then
}));
};
/**
* Get the set of outgoing edges from `report` as specified by the given
* breakdown.
*
* @param {Object} breakdown
* The census breakdown.
*
* @param {Object} report
* The census report.
*/
function getReportEdges(breakdown, report) {
return EDGES[breakdown.by](breakdown, report);
}
exports.getReportEdges = getReportEdges;
/** * walk *******************************************************************/
function recursiveWalk(breakdown, edge, report, visitor) {
if (breakdown.by === "count") {
visitor.enter(breakdown, report, edge);
visitor.count(breakdown, report, edge);
visitor.exit(breakdown, report, edge);
} else {
visitor.enter(breakdown, report, edge);
for (let { edge, referent, breakdown: subBreakdown } of getReportEdges(breakdown, report)) {
recursiveWalk(subBreakdown, edge, referent, visitor);
}
visitor.exit(breakdown, report, edge);
}
}
/**
* Walk the given `report` that was generated by taking a census with the
* specified `breakdown`.
*
* @param {Object} breakdown
* The census breakdown.
*
* @param {Object} report
* The census report.
*
* @param {Visitor} visitor
* The Visitor instance to call into while traversing.
*/
function walk(breakdown, report, visitor) {
recursiveWalk(breakdown, null, report, visitor);
}
exports.walk = walk;
/** * diff *******************************************************************/
/**
* Return true if the object is a Map, false otherwise. Works with Map objects
* from other globals, unlike `instanceof`.
*
* @returns {Boolean}
*/
function isMap(obj) {
return Object.prototype.toString.call(obj) === "[object Map]";
}
/**
* A Visitor for computing the difference between the census report being
* traversed and the given other census.
*
* @param {Object} otherCensus
* The other census report.
*/
function DiffVisitor(otherCensus) {
// The other census we are comparing against.
this._otherCensus = otherCensus;
// The total bytes and count of the basis census we are traversing.
this._totalBytes = 0;
this._totalCount = 0;
// Stack maintaining the current corresponding sub-report for the other
// census we are comparing against.
this._otherCensusStack = [];
// Stack maintaining the set of edges visited at each sub-report.
this._edgesVisited = [new Set()];
// The final delta census. Valid only after traversal.
this._results = null;
// Stack maintaining the results corresponding to each sub-report we are
// currently traversing.
this._resultsStack = [];
}
DiffVisitor.prototype = Object.create(Visitor.prototype);
/**
* Given a report and an outgoing edge, get the edge's referent.
*/
DiffVisitor.prototype._get = function (report, edge) {
if (!report) {
return undefined;
}
return isMap(report) ? report.get(edge) : report[edge];
};
/**
* Given a report, an outgoing edge, and a value, set the edge's referent to
* the given value.
*/
DiffVisitor.prototype._set = function (report, edge, val) {
if (isMap(report)) {
report.set(edge, val);
} else {
report[edge] = val;
}
};
/**
* @overrides Visitor.prototype.enter
*/
DiffVisitor.prototype.enter = function (breakdown, report, edge) {
const isFirstTimeEntering = this._results === null;
const newResults = breakdown.by === "allocationStack" ? new Map() : {};
let newOther;
if (!this._results) {
// This is the first time we have entered a sub-report.
this._results = newResults;
newOther = this._otherCensus;
} else {
const topResults = this._resultsStack[this._resultsStack.length - 1];
this._set(topResults, edge, newResults);
const topOther = this._otherCensusStack[this._otherCensusStack.length - 1];
newOther = this._get(topOther, edge);
}
this._resultsStack.push(newResults);
this._otherCensusStack.push(newOther);
const visited = this._edgesVisited[this._edgesVisited.length - 1];
visited.add(edge);
this._edgesVisited.push(new Set());
};
/**
* @overrides Visitor.prototype.exit
*/
DiffVisitor.prototype.exit = function (breakdown, report, edge) {
// Find all the edges in the other census report that were not traversed and
// add them to the results directly.
const other = this._otherCensusStack[this._otherCensusStack.length - 1];
if (other) {
const visited = this._edgesVisited[this._edgesVisited.length - 1];
const unvisited = getReportEdges(breakdown, other)
.map(e => e.edge)
.filter(e => !visited.has(e));
const results = this._resultsStack[this._resultsStack.length - 1];
for (let edge of unvisited) {
this._set(results, edge, this._get(other, edge));
}
}
this._otherCensusStack.pop();
this._resultsStack.pop();
this._edgesVisited.pop();
};
/**
* @overrides Visitor.prototype.count
*/
DiffVisitor.prototype.count = function (breakdown, report, edge) {
const other = this._otherCensusStack[this._otherCensusStack.length - 1];
const results = this._resultsStack[this._resultsStack.length - 1];
if (breakdown.count) {
this._totalCount += report.count;
}
if (breakdown.bytes) {
this._totalBytes += report.bytes;
}
if (other) {
if (breakdown.count) {
results.count = other.count - report.count;
}
if (breakdown.bytes) {
results.bytes = other.bytes - report.bytes;
}
} else {
if (breakdown.count) {
results.count = -report.count;
}
if (breakdown.bytes) {
results.bytes = -report.bytes;
}
}
};
const basisTotalBytes = exports.basisTotalBytes = Symbol("basisTotalBytes");
const basisTotalCount = exports.basisTotalCount = Symbol("basisTotalCount");
/**
* Get the resulting report of the difference between the traversed census
* report and the other census report.
*
* @returns {Object}
* The delta census report.
*/
DiffVisitor.prototype.results = function () {
if (!this._results) {
throw new Error("Attempt to get results before computing diff!");
}
if (this._resultsStack.length) {
throw new Error("Attempt to get results while still computing diff!");
}
this._results[basisTotalBytes] = this._totalBytes;
this._results[basisTotalCount] = this._totalCount;
return this._results;
};
/**
* Take the difference between two censuses. The resulting delta report
* contains the number/size of things that are in the `endCensus` that are not
* in the `startCensus`.
*
* @param {Object} breakdown
* The breakdown used to generate both census reports.
*
* @param {Object} startCensus
* The first census report.
*
* @param {Object} endCensus
* The second census report.
*
* @returns {Object}
* A delta report mirroring the structure of the two census reports (as
* specified by the given breakdown). Has two additional properties:
* - {Number} basisTotalBytes: the total number of bytes in the start
* census.
* - {Number} basisTotalCount: the total count in the start census.
*/
function diff(breakdown, startCensus, endCensus) {
const visitor = new DiffVisitor(endCensus);
walk(breakdown, startCensus, visitor);
return visitor.results();
}
exports.diff = diff;
/**
* Creates a hash map mapping node IDs to its parent node.
*
* @param {CensusTreeNode} node
* @param {Object<number, TreeNode>} aggregator
*
* @return {Object<number, TreeNode>}
*/
const createParentMap = exports.createParentMap = function (node,
getId = node => node.id,
aggregator = Object.create(null)) {
if (node.children) {
for (let i = 0, length = node.children.length; i < length; i++) {
const child = node.children[i];
aggregator[getId(child)] = node;
createParentMap(child, getId, aggregator);
}
}
return aggregator;
};
const BUCKET = Object.freeze({ by: "bucket" });
/**
* Convert a breakdown whose leaves are { by: "count" } to an identical
* breakdown, except with { by: "bucket" } leaves.
*
* @param {Object} breakdown
* @returns {Object}
*/
exports.countToBucketBreakdown = function (breakdown) {
if (typeof breakdown !== "object" || !breakdown) {
return breakdown;
}
if (breakdown.by === "count") {
return BUCKET;
}
const keys = Object.keys(breakdown);
const vals = keys.reduce((vs, k) => {
vs.push(exports.countToBucketBreakdown(breakdown[k]));
return vs;
}, []);
const result = {};
for (let i = 0, length = keys.length; i < length; i++) {
result[keys[i]] = vals[i];
}
return Object.freeze(result);
};
/**
* A Visitor for finding report leaves by their DFS index.
*/
function GetLeavesVisitor(targetIndices) {
this._index = -1;
this._targetIndices = targetIndices;
this._leaves = [];
}
GetLeavesVisitor.prototype = Object.create(Visitor.prototype);
/**
* @overrides Visitor.prototype.enter
*/
GetLeavesVisitor.prototype.enter = function (breakdown, report, edge) {
this._index++;
if (this._targetIndices.has(this._index)) {
this._leaves.push(report);
}
};
/**
* Get the accumulated report leaves after traversal.
*/
GetLeavesVisitor.prototype.leaves = function () {
if (this._index === -1) {
throw new Error("Attempt to call `leaves` before traversing report!");
}
return this._leaves;
};
/**
* Given a set of indices of leaves in a pre-order depth-first traversal of the
* given census report, return the leaves.
*
* @param {Set<Number>} indices
* @param {Object} breakdown
* @param {Object} report
*
* @returns {Array<Object>}
*/
exports.getReportLeaves = function (indices, breakdown, report) {
const visitor = new GetLeavesVisitor(indices);
walk(breakdown, report, visitor);
return visitor.leaves();
};
/**
* Get a list of the individual node IDs that belong to the census report leaves
* of the given indices.
*
* @param {Set<Number>} indices
* @param {Object} breakdown
* @param {HeapSnapshot} snapshot
*
* @returns {Array<NodeId>}
*/
exports.getCensusIndividuals = function (indices, countBreakdown, snapshot) {
const bucketBreakdown = exports.countToBucketBreakdown(countBreakdown);
const bucketReport = snapshot.takeCensus({ breakdown: bucketBreakdown });
const buckets = exports.getReportLeaves(indices,
bucketBreakdown,
bucketReport);
return flatten(buckets);
};

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,143 +0,0 @@
/* -*- 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;
}
}

View file

@ -1,150 +0,0 @@
/* -*- 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_ = &currentEdge;
}
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

View file

@ -1,317 +0,0 @@
/* -*- 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__

View file

@ -1,140 +0,0 @@
/* -*- 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

View file

@ -1,67 +0,0 @@
/* -*- 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__

View file

@ -1,336 +0,0 @@
/* 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/. */
"use strict";
const { immutableUpdate } = require("resource://devtools/shared/ThreadSafeDevToolsUtils.js");
const { Visitor, walk } = require("resource://devtools/shared/heapsnapshot/CensusUtils.js");
const { deduplicatePaths } = require("resource://devtools/shared/heapsnapshot/shortest-paths");
const DEFAULT_MAX_DEPTH = 4;
const DEFAULT_MAX_SIBLINGS = 15;
const DEFAULT_MAX_NUM_PATHS = 5;
/**
* A single node in a dominator tree.
*
* @param {NodeId} nodeId
* @param {NodeSize} retainedSize
*/
function DominatorTreeNode(nodeId, label, shallowSize, retainedSize) {
// The id of this node.
this.nodeId = nodeId;
// The label structure generated by describing the given node.
this.label = label;
// The shallow size of this node.
this.shallowSize = shallowSize;
// The retained size of this node.
this.retainedSize = retainedSize;
// The id of this node's parent or undefined if this node is the root.
this.parentId = undefined;
// An array of immediately dominated child `DominatorTreeNode`s, or undefined.
this.children = undefined;
// An object of the form returned by `deduplicatePaths`, encoding the set of
// the N shortest retaining paths for this node as a graph.
this.shortestPaths = undefined;
// True iff the `children` property does not contain every immediately
// dominated node.
//
// * If children is an array and this property is true: the array does not
// contain the complete set of immediately dominated children.
// * If children is an array and this property is false: the array contains
// the complete set of immediately dominated children.
// * If children is undefined and this property is true: there exist
// immediately dominated children for this node, but they have not been
// loaded yet.
// * If children is undefined and this property is false: this node does not
// dominate any others and therefore has no children.
this.moreChildrenAvailable = true;
}
DominatorTreeNode.prototype = null;
module.exports = DominatorTreeNode;
/**
* Add `child` to the `parent`'s set of children.
*
* @param {DominatorTreeNode} parent
* @param {DominatorTreeNode} child
*/
DominatorTreeNode.addChild = function (parent, child) {
if (parent.children === undefined) {
parent.children = [];
}
parent.children.push(child);
child.parentId = parent.nodeId;
};
/**
* A Visitor that is used to generate a label for a node in the heap snapshot
* and get its shallow size as well while we are at it.
*/
function LabelAndShallowSizeVisitor() {
// As we walk the description, we accumulate edges in this array.
this._labelPieces = [];
// Once we reach the non-zero count leaf node in the description, we move the
// labelPieces here to signify that we no longer need to accumulate edges.
this._label = undefined;
// Once we reach the non-zero count leaf node in the description, we grab the
// shallow size and place it here.
this._shallowSize = 0;
}
DominatorTreeNode.LabelAndShallowSizeVisitor = LabelAndShallowSizeVisitor;
LabelAndShallowSizeVisitor.prototype = Object.create(Visitor);
/**
* @overrides Visitor.prototype.enter
*/
LabelAndShallowSizeVisitor.prototype.enter = function (breakdown, report, edge) {
if (this._labelPieces && edge) {
this._labelPieces.push(edge);
}
};
/**
* @overrides Visitor.prototype.exit
*/
LabelAndShallowSizeVisitor.prototype.exit = function (breakdown, report, edge) {
if (this._labelPieces && edge) {
this._labelPieces.pop();
}
};
/**
* @overrides Visitor.prototype.count
*/
LabelAndShallowSizeVisitor.prototype.count = function (breakdown, report, edge) {
if (report.count === 0) {
return;
}
this._label = this._labelPieces;
this._labelPieces = undefined;
this._shallowSize = report.bytes;
};
/**
* Get the generated label structure accumulated by this visitor.
*
* @returns {Object}
*/
LabelAndShallowSizeVisitor.prototype.label = function () {
return this._label;
};
/**
* Get the shallow size of the node this visitor visited.
*
* @returns {Number}
*/
LabelAndShallowSizeVisitor.prototype.shallowSize = function () {
return this._shallowSize;
};
/**
* Generate a label structure for the node with the given id and grab its
* shallow size.
*
* What is a "label" structure? HeapSnapshot.describeNode essentially takes a
* census of a single node rather than the whole heap graph. The resulting
* report has only one count leaf that is non-zero. The label structure is the
* path in this report from the root to the non-zero count leaf.
*
* @param {Number} nodeId
* @param {HeapSnapshot} snapshot
* @param {Object} breakdown
*
* @returns {Object}
* An object with the following properties:
* - {Number} shallowSize
* - {Object} label
*/
DominatorTreeNode.getLabelAndShallowSize = function (nodeId,
snapshot,
breakdown) {
const description = snapshot.describeNode(breakdown, nodeId);
const visitor = new LabelAndShallowSizeVisitor();
walk(breakdown, description, visitor);
return {
label: visitor.label(),
shallowSize: visitor.shallowSize(),
};
};
/**
* Do a partial traversal of the given dominator tree and convert it into a tree
* of `DominatorTreeNode`s. Dominator trees have a node for every node in the
* snapshot's heap graph, so we must not allocate a JS object for every node. It
* would be way too many and the node count is effectively unbounded.
*
* Go no deeper down the tree than `maxDepth` and only consider at most
* `maxSiblings` within any single node's children.
*
* @param {DominatorTree} dominatorTree
* @param {HeapSnapshot} snapshot
* @param {Object} breakdown
* @param {Number} maxDepth
* @param {Number} maxSiblings
*
* @returns {DominatorTreeNode}
*/
DominatorTreeNode.partialTraversal = function (dominatorTree,
snapshot,
breakdown,
maxDepth = DEFAULT_MAX_DEPTH,
maxSiblings = DEFAULT_MAX_SIBLINGS) {
function dfs(nodeId, depth) {
const { label, shallowSize } =
DominatorTreeNode.getLabelAndShallowSize(nodeId, snapshot, breakdown);
const retainedSize = dominatorTree.getRetainedSize(nodeId);
const node = new DominatorTreeNode(nodeId, label, shallowSize, retainedSize);
const childNodeIds = dominatorTree.getImmediatelyDominated(nodeId);
const newDepth = depth + 1;
if (newDepth < maxDepth) {
const endIdx = Math.min(childNodeIds.length, maxSiblings);
for (let i = 0; i < endIdx; i++) {
DominatorTreeNode.addChild(node, dfs(childNodeIds[i], newDepth));
}
node.moreChildrenAvailable = endIdx < childNodeIds.length;
} else {
node.moreChildrenAvailable = childNodeIds.length > 0;
}
return node;
}
return dfs(dominatorTree.root, 0);
};
/**
* Insert more children into the given (partially complete) dominator tree.
*
* The tree is updated in an immutable and persistent manner: a new tree is
* returned, but all unmodified subtrees (which is most) are shared with the
* original tree. Only the modified nodes are re-allocated.
*
* @param {DominatorTreeNode} tree
* @param {Array<NodeId>} path
* @param {Array<DominatorTreeNode>} newChildren
* @param {Boolean} moreChildrenAvailable
*
* @returns {DominatorTreeNode}
*/
DominatorTreeNode.insert = function (tree, path, newChildren, moreChildrenAvailable) {
function insert(tree, i) {
if (tree.nodeId !== path[i]) {
return tree;
}
if (i == path.length - 1) {
return immutableUpdate(tree, {
children: (tree.children || []).concat(newChildren),
moreChildrenAvailable,
});
}
return tree.children
? immutableUpdate(tree, {
children: tree.children.map(c => insert(c, i + 1))
})
: tree;
}
return insert(tree, 0);
};
/**
* Get the new canonical node with the given `id` in `tree` that exists along
* `path`. If there is no such node along `path`, return null.
*
* This is useful if we have a reference to a now-outdated DominatorTreeNode due
* to a recent call to DominatorTreeNode.insert and want to get the up-to-date
* version. We don't have to walk the whole tree: if there is an updated version
* of the node then it *must* be along the path.
*
* @param {NodeId} id
* @param {DominatorTreeNode} tree
* @param {Array<NodeId>} path
*
* @returns {DominatorTreeNode|null}
*/
DominatorTreeNode.getNodeByIdAlongPath = function (id, tree, path) {
function find(node, i) {
if (!node || node.nodeId !== path[i]) {
return null;
}
if (node.nodeId === id) {
return node;
}
if (i === path.length - 1 || !node.children) {
return null;
}
const nextId = path[i + 1];
return find(node.children.find(c => c.nodeId === nextId), i + 1);
}
return find(tree, 0);
};
/**
* Find the shortest retaining paths for the given set of DominatorTreeNodes,
* and populate each node's `shortestPaths` property with them in place.
*
* @param {HeapSnapshot} snapshot
* @param {Object} breakdown
* @param {NodeId} start
* @param {Array<DominatorTreeNode>} treeNodes
* @param {Number} maxNumPaths
*/
DominatorTreeNode.attachShortestPaths = function (snapshot,
breakdown,
start,
treeNodes,
maxNumPaths = DEFAULT_MAX_NUM_PATHS) {
const idToTreeNode = new Map();
const targets = [];
for (let node of treeNodes) {
const id = node.nodeId;
idToTreeNode.set(id, node);
targets.push(id);
}
const shortestPaths = snapshot.computeShortestPaths(start,
targets,
maxNumPaths);
for (let [target, paths] of shortestPaths) {
const deduped = deduplicatePaths(target, paths);
deduped.nodes = deduped.nodes.map(id => {
const { label } =
DominatorTreeNode.getLabelAndShallowSize(id, snapshot, breakdown);
return { id, label };
});
idToTreeNode.get(target).shortestPaths = deduped;
}
};

View file

@ -1,86 +0,0 @@
/* -*- 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

View file

@ -1,41 +0,0 @@
/* -*- 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

View file

@ -1,277 +0,0 @@
/* 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/. */
"use strict";
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
const { DevToolsWorker } = require("devtools/shared/worker/worker");
const WORKER_URL =
"resource://devtools/shared/heapsnapshot/HeapAnalysesWorker.js";
var workerCounter = 0;
/**
* A HeapAnalysesClient instance provides a developer-friendly interface for
* interacting with a HeapAnalysesWorker. This enables users to be ignorant of
* the message passing protocol used to communicate with the worker. The
* HeapAnalysesClient owns the worker, and terminating the worker is done by
* terminating the client (see the `destroy` method).
*/
const HeapAnalysesClient = module.exports = function () {
this._worker = new DevToolsWorker(WORKER_URL, {
name: `HeapAnalyses-${workerCounter++}`,
verbose: DevToolsUtils.dumpv.wantVerbose
});
};
/**
* Destroy the worker, causing it to release its resources (such as heap
* snapshots it has deserialized and read into memory). The client is no longer
* usable after calling this method.
*/
HeapAnalysesClient.prototype.destroy = function () {
this._worker.destroy();
this._worker = null;
};
/**
* Tell the worker to read into memory the heap snapshot at the given file
* path. This is a prerequisite for asking the worker to perform various
* analyses on a heap snapshot.
*
* @param {String} snapshotFilePath
*
* @returns Promise
* The promise is fulfilled if the heap snapshot is successfully
* deserialized and read into memory. The promise is rejected if that
* does not happen, eg due to a bad file path or malformed heap
* snapshot file.
*/
HeapAnalysesClient.prototype.readHeapSnapshot = function (snapshotFilePath) {
return this._worker.performTask("readHeapSnapshot", { snapshotFilePath });
};
/**
* Tell the worker to delete all references to the snapshot and dominator trees
* linked to the provided snapshot file path.
*
* @param {String} snapshotFilePath
* @return Promise<undefined>
*/
HeapAnalysesClient.prototype.deleteHeapSnapshot = function (snapshotFilePath) {
return this._worker.performTask("deleteHeapSnapshot", { snapshotFilePath });
};
/**
* Request the creation time given a snapshot file path. Returns `null`
* if snapshot does not exist.
*
* @param {String} snapshotFilePath
* The path to the snapshot.
* @return {Number?}
* The unix timestamp of the creation time of the snapshot, or null if
* snapshot does not exist.
*/
HeapAnalysesClient.prototype.getCreationTime = function (snapshotFilePath) {
return this._worker.performTask("getCreationTime", snapshotFilePath);
};
/** * Censuses *****************************************************************/
/**
* Ask the worker to perform a census analysis on the heap snapshot with the
* given path. The heap snapshot at the given path must have already been read
* into memory by the worker (see `readHeapSnapshot`).
*
* @param {String} snapshotFilePath
*
* @param {Object} censusOptions
* A structured-cloneable object specifying the requested census's
* breakdown. See the "takeCensus" section of
* `js/src/doc/Debugger/Debugger.Memory.md` for detailed documentation.
*
* @param {Object} requestOptions
* An object specifying options of this worker request.
* - {Boolean} asTreeNode
* Whether or not the census is returned as a CensusTreeNode,
* or just a breakdown report. Defaults to false.
* @see `devtools/shared/heapsnapshot/census-tree-node.js`
* - {Boolean} asInvertedTreeNode
* Whether or not the census is returned as an inverted
* CensusTreeNode. Defaults to false.
* - {String} filter
* A filter string to prune the resulting tree with. Only applies if
* either asTreeNode or asInvertedTreeNode is true.
*
* @returns Promise<Object>
* An object with the following properties:
* - report:
* The report generated by the given census breakdown, or a
* CensusTreeNode generated by the given census breakdown if
* `asTreeNode` is true.
* - parentMap:
* The result of calling CensusUtils.createParentMap on the generated
* report. Only exists if asTreeNode or asInvertedTreeNode are set.
*/
HeapAnalysesClient.prototype.takeCensus = function (snapshotFilePath,
censusOptions,
requestOptions = {}) {
return this._worker.performTask("takeCensus", {
snapshotFilePath,
censusOptions,
requestOptions,
});
};
/**
* Get the individual nodes that correspond to the given census report leaf
* indices.
*
* @param {Object} opts
* An object with the following properties:
* - {DominatorTreeId} dominatorTreeId: The id of the dominator tree.
* - {Set<Number>} indices: The indices of the census report leaves we
* would like to get the individuals for.
* - {Object} censusBreakdown: The breakdown used to generate the census.
* - {Object} labelBreakdown: The breakdown we would like to use when
* labeling the resulting nodes.
* - {Number} maxRetainingPaths: The maximum number of retaining paths to
* compute for each node.
* - {Number} maxIndividuals: The maximum number of individual nodes to
* return.
*
* @returns {Promise<Object>}
* A promise of an object with the following properties:
* - {Array<DominatorTreeNode>} nodes: An array of `DominatorTreeNode`s
* with their shortest paths attached, and without any dominator tree
* child/parent information attached. The results are sorted by
* retained size.
*
*/
HeapAnalysesClient.prototype.getCensusIndividuals = function (opts) {
return this._worker.performTask("getCensusIndividuals", opts);
};
/**
* Request that the worker take a census on the heap snapshots with the given
* paths and then return the difference between them. Both heap snapshots must
* have already been read into memory by the worker (see `readHeapSnapshot`).
*
* @param {String} firstSnapshotFilePath
* The first snapshot file path.
*
* @param {String} secondSnapshotFilePath
* The second snapshot file path.
*
* @param {Object} censusOptions
* A structured-cloneable object specifying the requested census's
* breakdown. See the "takeCensus" section of
* `js/src/doc/Debugger/Debugger.Memory.md` for detailed documentation.
*
* @param {Object} requestOptions
* An object specifying options for this request.
* - {Boolean} asTreeNode
* Whether the resulting delta report should be converted to a census
* tree node before returned. Defaults to false.
* - {Boolean} asInvertedTreeNode
* Whether or not the census is returned as an inverted
* CensusTreeNode. Defaults to false.
* - {String} filter
* A filter string to prune the resulting tree with. Only applies if
* either asTreeNode or asInvertedTreeNode is true.
*
* @returns Promise<Object>
* - delta:
* The delta report generated by diffing the two census reports, or a
* CensusTreeNode generated from the delta report if
* `requestOptions.asTreeNode` was true.
* - parentMap:
* The result of calling CensusUtils.createParentMap on the generated
* delta. Only exists if asTreeNode or asInvertedTreeNode are set.
*/
HeapAnalysesClient.prototype.takeCensusDiff = function (firstSnapshotFilePath,
secondSnapshotFilePath,
censusOptions,
requestOptions = {}) {
return this._worker.performTask("takeCensusDiff", {
firstSnapshotFilePath,
secondSnapshotFilePath,
censusOptions,
requestOptions
});
};
/** * Dominator Trees **********************************************************/
/**
* Compute the dominator tree of the heap snapshot loaded from the given file
* path. Returns the id of the computed dominator tree.
*
* @param {String} snapshotFilePath
*
* @returns {Promise<DominatorTreeId>}
*/
HeapAnalysesClient.prototype.computeDominatorTree = function (snapshotFilePath) {
return this._worker.performTask("computeDominatorTree", snapshotFilePath);
};
/**
* Get the initial, partial view of the dominator tree with the given id.
*
* @param {Object} opts
* An object specifying options for this request.
* - {DominatorTreeId} dominatorTreeId
* The id of the dominator tree.
* - {Object} breakdown
* The breakdown used to generate node labels.
* - {Number} maxDepth
* The maximum depth to traverse down the tree to create this initial
* view.
* - {Number} maxSiblings
* The maximum number of siblings to visit within each traversed node's
* children.
* - {Number} maxRetainingPaths
* The maximum number of retaining paths to find for each node.
*
* @returns {Promise<DominatorTreeNode>}
*/
HeapAnalysesClient.prototype.getDominatorTree = function (opts) {
return this._worker.performTask("getDominatorTree", opts);
};
/**
* Get a subset of a nodes children in the dominator tree.
*
* @param {Object} opts
* An object specifying options for this request.
* - {DominatorTreeId} dominatorTreeId
* The id of the dominator tree.
* - {NodeId} nodeId
* The id of the node whose children are being found.
* - {Object} breakdown
* The breakdown used to generate node labels.
* - {Number} startIndex
* The starting index within the full set of immediately dominated
* children of the children being requested. Children are always sorted
* by greatest to least retained size.
* - {Number} maxCount
* The maximum number of children to return.
* - {Number} maxRetainingPaths
* The maximum number of retaining paths to find for each node.
*
* @returns {Promise<Object>}
* A promise of an object with the following properties:
* - {Array<DominatorTreeNode>} nodes
* The requested nodes that are immediately dominated by the node
* identified by `opts.nodeId`.
* - {Boolean} moreChildrenAvailable
* True iff there are more children available after the returned
* nodes.
* - {Array<NodeId>} path
* The path through the tree from the root to these node's parent, eg
* [root's id, child of root's id, child of child of root's id, ..., `nodeId`].
*/
HeapAnalysesClient.prototype.getImmediatelyDominated = function (opts) {
return this._worker.performTask("getImmediatelyDominated", opts);
};

View file

@ -1,303 +0,0 @@
/* 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/. */
/* global ThreadSafeChromeUtils*/
// This is a worker which reads offline heap snapshots into memory and performs
// heavyweight analyses on them without blocking the main thread. A
// HeapAnalysesWorker is owned and communicated with by a HeapAnalysesClient
// instance. See HeapAnalysesClient.js.
"use strict";
importScripts("resource://gre/modules/workers/require.js");
importScripts("resource://devtools/shared/worker/helper.js");
const { censusReportToCensusTreeNode } = require("resource://devtools/shared/heapsnapshot/census-tree-node.js");
const DominatorTreeNode = require("resource://devtools/shared/heapsnapshot/DominatorTreeNode.js");
const CensusUtils = require("resource://devtools/shared/heapsnapshot/CensusUtils.js");
const DEFAULT_START_INDEX = 0;
const DEFAULT_MAX_COUNT = 50;
/**
* The set of HeapSnapshot instances this worker has read into memory. Keyed by
* snapshot file path.
*/
const snapshots = Object.create(null);
/**
* The set of `DominatorTree`s that have been computed, mapped by their id (aka
* the index into this array).
*
* @see /dom/webidl/DominatorTree.webidl
*/
const dominatorTrees = [];
/**
* The i^th HeapSnapshot in this array is the snapshot used to generate the i^th
* dominator tree in `dominatorTrees` above. This lets us map from a dominator
* tree id to the snapshot it came from.
*/
const dominatorTreeSnapshots = [];
/**
* @see HeapAnalysesClient.prototype.readHeapSnapshot
*/
workerHelper.createTask(self, "readHeapSnapshot", ({ snapshotFilePath }) => {
snapshots[snapshotFilePath] =
ThreadSafeChromeUtils.readHeapSnapshot(snapshotFilePath);
return true;
});
/**
* @see HeapAnalysesClient.prototype.deleteHeapSnapshot
*/
workerHelper.createTask(self, "deleteHeapSnapshot", ({ snapshotFilePath }) => {
let snapshot = snapshots[snapshotFilePath];
if (!snapshot) {
throw new Error(`No known heap snapshot for '${snapshotFilePath}'`);
}
snapshots[snapshotFilePath] = undefined;
let dominatorTreeId = dominatorTreeSnapshots.indexOf(snapshot);
if (dominatorTreeId != -1) {
dominatorTreeSnapshots[dominatorTreeId] = undefined;
dominatorTrees[dominatorTreeId] = undefined;
}
});
/**
* @see HeapAnalysesClient.prototype.takeCensus
*/
workerHelper.createTask(self, "takeCensus", ({ snapshotFilePath, censusOptions, requestOptions }) => {
if (!snapshots[snapshotFilePath]) {
throw new Error(`No known heap snapshot for '${snapshotFilePath}'`);
}
let report = snapshots[snapshotFilePath].takeCensus(censusOptions);
let parentMap;
if (requestOptions.asTreeNode || requestOptions.asInvertedTreeNode) {
const opts = { filter: requestOptions.filter || null };
if (requestOptions.asInvertedTreeNode) {
opts.invert = true;
}
report = censusReportToCensusTreeNode(censusOptions.breakdown, report, opts);
parentMap = CensusUtils.createParentMap(report);
}
return { report, parentMap };
});
/**
* @see HeapAnalysesClient.prototype.getCensusIndividuals
*/
workerHelper.createTask(self, "getCensusIndividuals", request => {
const {
dominatorTreeId,
indices,
censusBreakdown,
labelBreakdown,
maxRetainingPaths,
maxIndividuals,
} = request;
const dominatorTree = dominatorTrees[dominatorTreeId];
if (!dominatorTree) {
throw new Error(
`There does not exist a DominatorTree with the id ${dominatorTreeId}`);
}
const snapshot = dominatorTreeSnapshots[dominatorTreeId];
const nodeIds = CensusUtils.getCensusIndividuals(indices, censusBreakdown, snapshot);
const nodes = nodeIds
.sort((a, b) => dominatorTree.getRetainedSize(b) - dominatorTree.getRetainedSize(a))
.slice(0, maxIndividuals)
.map(id => {
const { label, shallowSize } =
DominatorTreeNode.getLabelAndShallowSize(id, snapshot, labelBreakdown);
const retainedSize = dominatorTree.getRetainedSize(id);
const node = new DominatorTreeNode(id, label, shallowSize, retainedSize);
node.moreChildrenAvailable = false;
return node;
});
DominatorTreeNode.attachShortestPaths(snapshot,
labelBreakdown,
dominatorTree.root,
nodes,
maxRetainingPaths);
return { nodes };
});
/**
* @see HeapAnalysesClient.prototype.takeCensusDiff
*/
workerHelper.createTask(self, "takeCensusDiff", request => {
const {
firstSnapshotFilePath,
secondSnapshotFilePath,
censusOptions,
requestOptions
} = request;
if (!snapshots[firstSnapshotFilePath]) {
throw new Error(`No known heap snapshot for '${firstSnapshotFilePath}'`);
}
if (!snapshots[secondSnapshotFilePath]) {
throw new Error(`No known heap snapshot for '${secondSnapshotFilePath}'`);
}
const first = snapshots[firstSnapshotFilePath].takeCensus(censusOptions);
const second = snapshots[secondSnapshotFilePath].takeCensus(censusOptions);
let delta = CensusUtils.diff(censusOptions.breakdown, first, second);
let parentMap;
if (requestOptions.asTreeNode || requestOptions.asInvertedTreeNode) {
const opts = { filter: requestOptions.filter || null };
if (requestOptions.asInvertedTreeNode) {
opts.invert = true;
}
delta = censusReportToCensusTreeNode(censusOptions.breakdown, delta, opts);
parentMap = CensusUtils.createParentMap(delta);
}
return { delta, parentMap };
});
/**
* @see HeapAnalysesClient.prototype.getCreationTime
*/
workerHelper.createTask(self, "getCreationTime", snapshotFilePath => {
if (!snapshots[snapshotFilePath]) {
throw new Error(`No known heap snapshot for '${snapshotFilePath}'`);
}
return snapshots[snapshotFilePath].creationTime;
});
/**
* @see HeapAnalysesClient.prototype.computeDominatorTree
*/
workerHelper.createTask(self, "computeDominatorTree", snapshotFilePath => {
const snapshot = snapshots[snapshotFilePath];
if (!snapshot) {
throw new Error(`No known heap snapshot for '${snapshotFilePath}'`);
}
const id = dominatorTrees.length;
dominatorTrees.push(snapshot.computeDominatorTree());
dominatorTreeSnapshots.push(snapshot);
return id;
});
/**
* @see HeapAnalysesClient.prototype.getDominatorTree
*/
workerHelper.createTask(self, "getDominatorTree", request => {
const {
dominatorTreeId,
breakdown,
maxDepth,
maxSiblings,
maxRetainingPaths,
} = request;
if (!(0 <= dominatorTreeId && dominatorTreeId < dominatorTrees.length)) {
throw new Error(
`There does not exist a DominatorTree with the id ${dominatorTreeId}`);
}
const dominatorTree = dominatorTrees[dominatorTreeId];
const snapshot = dominatorTreeSnapshots[dominatorTreeId];
const tree = DominatorTreeNode.partialTraversal(dominatorTree,
snapshot,
breakdown,
maxDepth,
maxSiblings);
const nodes = [];
(function getNodes(node) {
nodes.push(node);
if (node.children) {
for (let i = 0, length = node.children.length; i < length; i++) {
getNodes(node.children[i]);
}
}
}(tree));
DominatorTreeNode.attachShortestPaths(snapshot,
breakdown,
dominatorTree.root,
nodes,
maxRetainingPaths);
return tree;
});
/**
* @see HeapAnalysesClient.prototype.getImmediatelyDominated
*/
workerHelper.createTask(self, "getImmediatelyDominated", request => {
const {
dominatorTreeId,
nodeId,
breakdown,
startIndex,
maxCount,
maxRetainingPaths,
} = request;
if (!(0 <= dominatorTreeId && dominatorTreeId < dominatorTrees.length)) {
throw new Error(
`There does not exist a DominatorTree with the id ${dominatorTreeId}`);
}
const dominatorTree = dominatorTrees[dominatorTreeId];
const snapshot = dominatorTreeSnapshots[dominatorTreeId];
const childIds = dominatorTree.getImmediatelyDominated(nodeId);
if (!childIds) {
throw new Error(`${nodeId} is not a node id in the dominator tree`);
}
const start = startIndex || DEFAULT_START_INDEX;
const count = maxCount || DEFAULT_MAX_COUNT;
const end = start + count;
const nodes = childIds
.slice(start, end)
.map(id => {
const { label, shallowSize } =
DominatorTreeNode.getLabelAndShallowSize(id, snapshot, breakdown);
const retainedSize = dominatorTree.getRetainedSize(id);
const node = new DominatorTreeNode(id, label, shallowSize, retainedSize);
node.parentId = nodeId;
// DominatorTree.getImmediatelyDominated will always return non-null here
// because we got the id directly from the dominator tree.
node.moreChildrenAvailable = dominatorTree.getImmediatelyDominated(id).length > 0;
return node;
});
const path = [];
let id = nodeId;
do {
path.push(id);
id = dominatorTree.getImmediateDominator(id);
} while (id !== null);
path.reverse();
const moreChildrenAvailable = childIds.length > end;
DominatorTreeNode.attachShortestPaths(snapshot,
breakdown,
dominatorTree.root,
nodes,
maxRetainingPaths);
return { nodes, moreChildrenAvailable, path };
});

File diff suppressed because it is too large Load diff

View file

@ -1,224 +0,0 @@
/* -*- 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__

View file

@ -1,95 +0,0 @@
/* 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/. */
// Heap snapshots are always saved in the temp directory, and have a regular
// naming convention. This module provides helpers for working with heap
// snapshot files in a safe manner. Because we attempt to avoid unnecessary
// copies of the heap snapshot files by checking the local filesystem for a heap
// snapshot file with the given snapshot id, we want to ensure that we are only
// attempting to open heap snapshot files and not `~/.ssh/id_rsa`, for
// example. Therefore, the RDP only talks about snapshot ids, or transfering the
// bulk file data. A file path can be recovered from a snapshot id, which allows
// one to check for the presence of the heap snapshot file on the local file
// system, but we don't have to worry about opening arbitrary files.
//
// The heap snapshot file path conventions permits the following forms:
//
// $TEMP_DIRECTORY/XXXXXXXXXX.fxsnapshot
// $TEMP_DIRECTORY/XXXXXXXXXX-XXXXX.fxsnapshot
//
// Where the strings of "X" are zero or more digits.
"use strict";
const { Ci } = require("chrome");
loader.lazyRequireGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm", true);
loader.lazyRequireGetter(this, "OS", "resource://gre/modules/osfile.jsm", true);
function getHeapSnapshotFileTemplate() {
return OS.Path.join(OS.Constants.Path.tmpDir, `${Date.now()}.fxsnapshot`);
}
/**
* Get a unique temp file path for a new heap snapshot. The file is guaranteed
* not to exist before this call.
*
* @returns String
*/
exports.getNewUniqueHeapSnapshotTempFilePath = function () {
let file = new FileUtils.File(getHeapSnapshotFileTemplate());
// The call to createUnique will append "-N" after the leaf name (but before
// the extension) until a new file is found and create it. This guarantees we
// won't accidentally choose the same file twice.
file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
return file.path;
};
function isValidSnapshotFileId(snapshotId) {
return /^\d+(\-\d+)?$/.test(snapshotId);
}
/**
* Get the file path for the given snapshot id.
*
* @param {String} snapshotId
*
* @returns String | null
*/
exports.getHeapSnapshotTempFilePath = function (snapshotId) {
// Don't want anyone sneaking "../../../.." strings into the snapshot id and
// trying to make us open arbitrary files.
if (!isValidSnapshotFileId(snapshotId)) {
return null;
}
return OS.Path.join(OS.Constants.Path.tmpDir, snapshotId + ".fxsnapshot");
};
/**
* Return true if we have the heap snapshot file for the given snapshot id on
* the local file system. False is returned otherwise.
*
* @returns Promise<Boolean>
*/
exports.haveHeapSnapshotTempFile = function (snapshotId) {
const path = exports.getHeapSnapshotTempFilePath(snapshotId);
if (!path) {
return Promise.resolve(false);
}
return OS.File.stat(path).then(() => true,
() => false);
};
/**
* Given a heap snapshot's file path, extricate the snapshot id.
*
* @param {String} path
*
* @returns String
*/
exports.getSnapshotIdFromPath = function (path) {
return path.slice(OS.Constants.Path.tmpDir.length + 1,
path.length - ".fxsnapshot".length);
};

View file

@ -1,32 +0,0 @@
/* -*- 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

View file

@ -1,53 +0,0 @@
/* -*- 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

View file

@ -1,35 +0,0 @@
/* -*- 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

View file

@ -1,35 +0,0 @@
/* -*- 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

View file

@ -1,100 +0,0 @@
/* -*- 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

View file

@ -1,70 +0,0 @@
/* -*- 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__

View file

@ -1,748 +0,0 @@
/* 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/. */
"use strict";
// CensusTreeNode is an intermediate representation of a census report that
// exists between after a report is generated by taking a census and before the
// report is rendered in the DOM. It must be dead simple to render, with no
// further data processing or massaging needed before rendering DOM nodes. Our
// goal is to do the census report to CensusTreeNode transformation in the
// HeapAnalysesWorker, and ensure that the **only** work that the main thread
// has to do is strictly DOM rendering work.
const {
Visitor,
walk,
basisTotalBytes,
basisTotalCount,
} = require("resource://devtools/shared/heapsnapshot/CensusUtils.js");
// Monotonically increasing integer for CensusTreeNode `id`s.
let censusTreeNodeIdCounter = 0;
/**
* Return true if the given object is a SavedFrame stack object, false otherwise.
*
* @param {any} obj
* @returns {Boolean}
*/
function isSavedFrame(obj) {
return Object.prototype.toString.call(obj) === "[object SavedFrame]";
}
/**
* A CensusTreeNodeCache maps from SavedFrames to CensusTreeNodes. It is used when
* aggregating multiple SavedFrame allocation stack keys into a tree of many
* CensusTreeNodes. Each stack may share older frames, and we want to preserve
* this sharing when converting to CensusTreeNode, so before creating a new
* CensusTreeNode, we look for an existing one in one of our CensusTreeNodeCaches.
*/
function CensusTreeNodeCache() {}
CensusTreeNodeCache.prototype = null;
/**
* The value of a single entry stored in a CensusTreeNodeCache. It is a pair of
* the CensusTreeNode for this cache value, and the subsequent
* CensusTreeNodeCache for this node's children.
*
* @param {SavedFrame} frame
* The frame being cached.
*/
function CensusTreeNodeCacheValue() {
// The CensusTreeNode for this cache value.
this.node = undefined;
// The CensusTreeNodeCache for this frame's children.
this.children = undefined;
}
CensusTreeNodeCacheValue.prototype = null;
/**
* Create a unique string for the given SavedFrame (ignoring the frame's parent
* chain) that can be used as a hash to key this frame within a CensusTreeNodeCache.
*
* NB: We manually hash rather than using an ES6 Map because we are purposely
* ignoring the parent chain and wish to consider frames with everything the
* same except their parents as the same.
*
* @param {SavedFrame} frame
* The SavedFrame object we would like to lookup in or insert into a
* CensusTreeNodeCache.
*
* @returns {String}
* The unique string that can be used as a key in a CensusTreeNodeCache.
*/
CensusTreeNodeCache.hashFrame = function (frame) {
return `FRAME,${frame.functionDisplayName},${frame.source},${frame.line},${frame.column},${frame.asyncCause}`;
};
/**
* Create a unique string for the given CensusTreeNode **with regards to
* siblings at the current depth of the tree, not within the whole tree.** It
* can be used as a hash to key this node within a CensusTreeNodeCache.
*
* @param {CensusTreeNode} node
* The node we would like to lookup in or insert into a cache.
*
* @returns {String}
* The unique string that can be used as a key in a CensusTreeNodeCache.
*/
CensusTreeNodeCache.hashNode = function (node) {
return isSavedFrame(node.name)
? CensusTreeNodeCache.hashFrame(node.name)
: `NODE,${node.name}`;
};
/**
* Insert the given CensusTreeNodeCacheValue whose node.name is a SavedFrame
* object in the given cache.
*
* @param {CensusTreeNodeCache} cache
* @param {CensusTreeNodeCacheValue} value
*/
CensusTreeNodeCache.insertFrame = function (cache, value) {
cache[CensusTreeNodeCache.hashFrame(value.node.name)] = value;
};
/**
* Insert the given value in the cache.
*
* @param {CensusTreeNodeCache} cache
* @param {CensusTreeNodeCacheValue} value
*/
CensusTreeNodeCache.insertNode = function (cache, value) {
if (isSavedFrame(value.node.name)) {
CensusTreeNodeCache.insertFrame(cache, value);
} else {
cache[CensusTreeNodeCache.hashNode(value.node)] = value;
}
};
/**
* Lookup `frame` in `cache` and return its value if it exists.
*
* @param {CensusTreeNodeCache} cache
* @param {SavedFrame} frame
*
* @returns {undefined|CensusTreeNodeCacheValue}
*/
CensusTreeNodeCache.lookupFrame = function (cache, frame) {
return cache[CensusTreeNodeCache.hashFrame(frame)];
};
/**
* Lookup `node` in `cache` and return its value if it exists.
*
* @param {CensusTreeNodeCache} cache
* @param {CensusTreeNode} node
*
* @returns {undefined|CensusTreeNodeCacheValue}
*/
CensusTreeNodeCache.lookupNode = function (cache, node) {
return isSavedFrame(node.name)
? CensusTreeNodeCache.lookupFrame(cache, node.name)
: cache[CensusTreeNodeCache.hashNode(node)];
};
/**
* Add `child` to `parent`'s set of children and store the parent ID
* on the child.
*
* @param {CensusTreeNode} parent
* @param {CensusTreeNode} child
*/
function addChild(parent, child) {
if (!parent.children) {
parent.children = [];
}
child.parent = parent.id;
parent.children.push(child);
}
/**
* Get an array of each frame in the provided stack.
*
* @param {SavedFrame} stack
* @returns {Array<SavedFrame>}
*/
function getArrayOfFrames(stack) {
const frames = [];
let frame = stack;
while (frame) {
frames.push(frame);
frame = frame.parent;
}
frames.reverse();
return frames;
}
/**
* Given an `edge` to a sub-`report` whose structure is described by
* `breakdown`, create a CensusTreeNode tree.
*
* @param {Object} breakdown
* The breakdown specifying the structure of the given report.
*
* @param {Object} report
* The census report.
*
* @param {null|String|SavedFrame} edge
* The edge leading to this report from the parent report.
*
* @param {CensusTreeNodeCache} cache
* The cache of CensusTreeNodes we have already made for the siblings of
* the node being created. The existing nodes are reused when possible.
*
* @param {Object} outParams
* The return values are attached to this object after this function
* returns. Because we create a CensusTreeNode for each frame in a
* SavedFrame stack edge, there may multiple nodes per sub-report.
*
* - top: The deepest node in the CensusTreeNode subtree created.
*
* - bottom: The shallowest node in the CensusTreeNode subtree created.
* This is null if the shallowest node in the subtree was
* found in the `cache` and reused.
*
* Note that top and bottom are not necessarily different. In the case
* where there is a 1:1 correspondence between an edge in the report and
* a CensusTreeNode, top and bottom refer to the same node.
*/
function makeCensusTreeNodeSubTree(breakdown, report, edge, cache, outParams) {
if (!isSavedFrame(edge)) {
const node = new CensusTreeNode(edge);
outParams.top = outParams.bottom = node;
return;
}
const frames = getArrayOfFrames(edge);
let currentCache = cache;
let prevNode;
for (let i = 0, length = frames.length; i < length; i++) {
const frame = frames[i];
// Get or create the CensusTreeNodeCacheValue for this frame. If we already
// have a CensusTreeNodeCacheValue (and hence a CensusTreeNode) for this
// frame, we don't need to add the node to the previous node's children as
// we have already done that. If we don't have a CensusTreeNodeCacheValue
// and CensusTreeNode for this frame, then create one and make sure to hook
// it up as a child of the previous node.
let isNewNode = false;
let val = CensusTreeNodeCache.lookupFrame(currentCache, frame);
if (!val) {
isNewNode = true;
val = new CensusTreeNodeCacheValue();
val.node = new CensusTreeNode(frame);
CensusTreeNodeCache.insertFrame(currentCache, val);
if (prevNode) {
addChild(prevNode, val.node);
}
}
if (i === 0) {
outParams.bottom = isNewNode ? val.node : null;
}
if (i === length - 1) {
outParams.top = val.node;
}
prevNode = val.node;
if (i !== length - 1 && !val.children) {
// This is not the last frame and therefore this node will have
// children, which we must cache.
val.children = new CensusTreeNodeCache();
}
currentCache = val.children;
}
}
/**
* A Visitor that walks a census report and creates the corresponding
* CensusTreeNode tree.
*/
function CensusTreeNodeVisitor() {
// The root of the resulting CensusTreeNode tree.
this._root = null;
// The stack of CensusTreeNodes that we are in the process of building while
// walking the census report.
this._nodeStack = [];
// To avoid unnecessary allocations, we reuse the same out parameter object
// passed to `makeCensusTreeNodeSubTree` every time we call it.
this._outParams = {
top: null,
bottom: null,
};
// The stack of `CensusTreeNodeCache`s that we use to aggregate many
// SavedFrame stacks into a single CensusTreeNode tree.
this._cacheStack = [new CensusTreeNodeCache()];
// The current index in the DFS of the census report tree.
this._index = -1;
}
CensusTreeNodeVisitor.prototype = Object.create(Visitor);
/**
* Create the CensusTreeNode subtree for this sub-report and link it to the
* parent CensusTreeNode.
*
* @overrides Visitor.prototype.enter
*/
CensusTreeNodeVisitor.prototype.enter = function (breakdown, report, edge) {
this._index++;
const cache = this._cacheStack[this._cacheStack.length - 1];
makeCensusTreeNodeSubTree(breakdown, report, edge, cache, this._outParams);
const { top, bottom } = this._outParams;
if (!this._root) {
this._root = bottom;
} else if (bottom) {
addChild(this._nodeStack[this._nodeStack.length - 1], bottom);
}
this._cacheStack.push(new CensusTreeNodeCache());
this._nodeStack.push(top);
};
function values(cache) {
return Object.keys(cache).map(k => cache[k]);
}
function isNonEmpty(node) {
return (node.children !== undefined && node.children.length)
|| node.bytes !== 0
|| node.count !== 0;
}
/**
* We have finished adding children to the CensusTreeNode subtree for the
* current sub-report. Make sure that the children are sorted for every node in
* the subtree.
*
* @overrides Visitor.prototype.exit
*/
CensusTreeNodeVisitor.prototype.exit = function (breakdown, report, edge) {
// Ensure all children are sorted and have their counts/bytes aggregated. We
// only need to consider cache children here, because other children
// correspond to other sub-reports and we already fixed them up in an earlier
// invocation of `exit`.
function dfs(node, childrenCache) {
if (childrenCache) {
const childValues = values(childrenCache);
for (let i = 0, length = childValues.length; i < length; i++) {
dfs(childValues[i].node, childValues[i].children);
}
}
node.totalCount = node.count;
node.totalBytes = node.bytes;
if (node.children) {
// Prune empty leaves.
node.children = node.children.filter(isNonEmpty);
node.children.sort(compareByTotal);
for (let i = 0, length = node.children.length; i < length; i++) {
node.totalCount += node.children[i].totalCount;
node.totalBytes += node.children[i].totalBytes;
}
}
}
const top = this._nodeStack.pop();
const cache = this._cacheStack.pop();
dfs(top, cache);
};
/**
* @overrides Visitor.prototype.count
*/
CensusTreeNodeVisitor.prototype.count = function (breakdown, report, edge) {
const node = this._nodeStack[this._nodeStack.length - 1];
node.reportLeafIndex = this._index;
if (breakdown.count) {
node.count = report.count;
}
if (breakdown.bytes) {
node.bytes = report.bytes;
}
};
/**
* Get the root of the resulting CensusTreeNode tree.
*
* @returns {CensusTreeNode}
*/
CensusTreeNodeVisitor.prototype.root = function () {
if (!this._root) {
throw new Error("Attempt to get the root before walking the census report!");
}
if (this._nodeStack.length) {
throw new Error("Attempt to get the root while walking the census report!");
}
return this._root;
};
/**
* Create a single, uninitialized CensusTreeNode.
*
* @param {null|String|SavedFrame} name
*/
function CensusTreeNode(name) {
// Display name for this CensusTreeNode. Either null, a string, or a
// SavedFrame.
this.name = name;
// The number of bytes occupied by matching things in the heap snapshot.
this.bytes = 0;
// The sum of `this.bytes` and `child.totalBytes` for each child in
// `this.children`.
this.totalBytes = 0;
// The number of things in the heap snapshot that match this node in the
// census tree.
this.count = 0;
// The sum of `this.count` and `child.totalCount` for each child in
// `this.children`.
this.totalCount = 0;
// An array of this node's children, or undefined if it has no children.
this.children = undefined;
// The unique ID of this node.
this.id = ++censusTreeNodeIdCounter;
// If present, the unique ID of this node's parent. If this node does not have
// a parent, then undefined.
this.parent = undefined;
// The `reportLeafIndex` property allows mapping a CensusTreeNode node back to
// a leaf in the census report it was generated from. It is always one of the
// following variants:
//
// * A `Number` index pointing a leaf report in a pre-order DFS traversal of
// this CensusTreeNode's census report.
//
// * A `Set` object containing such indices, when this is part of an inverted
// CensusTreeNode tree and multiple leaves in the report map onto this node.
//
// * Finally, `undefined` when no leaves in the census report correspond with
// this node.
//
// The first and third cases are the common cases. The second case is rather
// uncommon, and to avoid doubling the number of allocations when creating
// CensusTreeNode trees, and objects that get structured cloned when sending
// such trees from the HeapAnalysesWorker to the main thread, we only allocate
// a Set object once a node actually does have multiple leaves it corresponds
// to.
this.reportLeafIndex = undefined;
}
CensusTreeNode.prototype = null;
/**
* Compare the given nodes by their `totalBytes` properties, and breaking ties
* with the `totalCount`, `bytes`, and `count` properties (in that order).
*
* @param {CensusTreeNode} node1
* @param {CensusTreeNode} node2
*
* @returns {Number}
* A number suitable for using with Array.prototype.sort.
*/
function compareByTotal(node1, node2) {
return Math.abs(node2.totalBytes) - Math.abs(node1.totalBytes)
|| Math.abs(node2.totalCount) - Math.abs(node1.totalCount)
|| Math.abs(node2.bytes) - Math.abs(node1.bytes)
|| Math.abs(node2.count) - Math.abs(node1.count);
}
/**
* Compare the given nodes by their `bytes` properties, and breaking ties with
* the `count`, `totalBytes`, and `totalCount` properties (in that order).
*
* @param {CensusTreeNode} node1
* @param {CensusTreeNode} node2
*
* @returns {Number}
* A number suitable for using with Array.prototype.sort.
*/
function compareBySelf(node1, node2) {
return Math.abs(node2.bytes) - Math.abs(node1.bytes)
|| Math.abs(node2.count) - Math.abs(node1.count)
|| Math.abs(node2.totalBytes) - Math.abs(node1.totalBytes)
|| Math.abs(node2.totalCount) - Math.abs(node1.totalCount);
}
/**
* Given a parent cache value from a tree we are building and a child node from
* a tree we are basing the new tree off of, if we already have a corresponding
* node in the parent's children cache, merge this node's counts with
* it. Otherwise, create the corresponding node, add it to the parent's children
* cache, and create the parent->child edge.
*
* @param {CensusTreeNodeCacheValue} parentCachevalue
* @param {CensusTreeNode} node
*
* @returns {CensusTreeNodeCacheValue}
* The new or extant child node's corresponding cache value.
*/
function insertOrMergeNode(parentCacheValue, node) {
if (!parentCacheValue.children) {
parentCacheValue.children = new CensusTreeNodeCache();
}
let val = CensusTreeNodeCache.lookupNode(parentCacheValue.children, node);
if (val) {
// When inverting, it is possible that multiple leaves in the census report
// get merged into a single CensusTreeNode node. When this occurs, switch
// from a single index to a set of indices.
if (val.node.reportLeafIndex !== undefined &&
val.node.reportLeafIndex !== node.reportLeafIndex) {
if (typeof val.node.reportLeafIndex === "number") {
const oldIndex = val.node.reportLeafIndex;
val.node.reportLeafIndex = new Set();
val.node.reportLeafIndex.add(oldIndex);
val.node.reportLeafIndex.add(node.reportLeafIndex);
} else {
val.node.reportLeafIndex.add(node.reportLeafIndex);
}
}
val.node.count += node.count;
val.node.bytes += node.bytes;
} else {
val = new CensusTreeNodeCacheValue();
val.node = new CensusTreeNode(node.name);
val.node.reportLeafIndex = node.reportLeafIndex;
val.node.count = node.count;
val.node.totalCount = node.totalCount;
val.node.bytes = node.bytes;
val.node.totalBytes = node.totalBytes;
addChild(parentCacheValue.node, val.node);
CensusTreeNodeCache.insertNode(parentCacheValue.children, val);
}
return val;
}
/**
* Given an un-inverted CensusTreeNode tree, return the corresponding inverted
* CensusTreeNode tree. The input tree is not modified. The resulting inverted
* tree is sorted by self bytes rather than by total bytes.
*
* @param {CensusTreeNode} tree
* The un-inverted tree.
*
* @returns {CensusTreeNode}
* The corresponding inverted tree.
*/
function invert(tree) {
const inverted = new CensusTreeNodeCacheValue();
inverted.node = new CensusTreeNode(null);
// Do a depth-first search of the un-inverted tree. As we reach each leaf,
// take the path from the old root to the leaf, reverse that path, and add it
// to the new, inverted tree's root.
const path = [];
(function addInvertedPaths(node) {
path.push(node);
if (node.children) {
for (let i = 0, length = node.children.length; i < length; i++) {
addInvertedPaths(node.children[i]);
}
} else {
// We found a leaf node, add the reverse path to the inverted tree.
let currentCacheValue = inverted;
for (let i = path.length - 1; i >= 0; i--) {
currentCacheValue = insertOrMergeNode(currentCacheValue, path[i]);
}
}
path.pop();
}(tree));
// Ensure that the root node always has the totals.
inverted.node.totalBytes = tree.totalBytes;
inverted.node.totalCount = tree.totalCount;
return inverted.node;
}
/**
* Given a CensusTreeNode tree and predicate function, create the tree
* containing only the nodes in any path `(node_0, node_1, ..., node_n-1)` in
* the given tree where `predicate(node_j)` is true for `0 <= j < n`, `node_0`
* is the given tree's root, and `node_n-1` is a leaf in the given tree. The
* given tree is left unmodified.
*
* @param {CensusTreeNode} tree
* @param {Function} predicate
*
* @returns {CensusTreeNode}
*/
function filter(tree, predicate) {
const filtered = new CensusTreeNodeCacheValue();
filtered.node = new CensusTreeNode(null);
// Do a DFS over the given tree. If the predicate returns true for any node,
// add that node and its whole subtree to the filtered tree.
const path = [];
let match = false;
function addMatchingNodes(node) {
path.push(node);
let oldMatch = match;
if (!match && predicate(node)) {
match = true;
}
if (node.children) {
for (let i = 0, length = node.children.length; i < length; i++) {
addMatchingNodes(node.children[i]);
}
} else if (match) {
// We found a matching leaf node, add it to the filtered tree.
let currentCacheValue = filtered;
for (let i = 0, length = path.length; i < length; i++) {
currentCacheValue = insertOrMergeNode(currentCacheValue, path[i]);
}
}
match = oldMatch;
path.pop();
}
if (tree.children) {
for (let i = 0, length = tree.children.length; i < length; i++) {
addMatchingNodes(tree.children[i]);
}
}
filtered.node.count = tree.count;
filtered.node.totalCount = tree.totalCount;
filtered.node.bytes = tree.bytes;
filtered.node.totalBytes = tree.totalBytes;
return filtered.node;
}
/**
* Given a filter string, return a predicate function that takes a node and
* returns true iff the node matches the filter.
*
* @param {String} filterString
* @returns {Function}
*/
function makeFilterPredicate(filterString) {
return function (node) {
if (!node.name) {
return false;
}
if (isSavedFrame(node.name)) {
return node.name.source.includes(filterString)
|| (node.name.functionDisplayName || "").includes(filterString)
|| (node.name.asyncCause || "").includes(filterString);
}
return String(node.name).includes(filterString);
};
}
/**
* Takes a report from a census (`dbg.memory.takeCensus()`) and the breakdown
* used to generate the census and returns a structure used to render
* a tree to display the data.
*
* Returns a recursive "CensusTreeNode" object, looking like:
*
* CensusTreeNode = {
* // `children` if it exists, is sorted by `bytes`, if they are leaf nodes.
* children: ?[<CensusTreeNode...>],
* name: <?String>
* count: <?Number>
* bytes: <?Number>
* id: <?Number>
* parent: <?Number>
* }
*
* @param {Object} breakdown
* The breakdown used to generate the census report.
*
* @param {Object} report
* The census report generated with the specified breakdown.
*
* @param {Object} options
* Configuration options.
* - invert: Whether to invert the resulting tree or not. Defaults to
* false, ie uninverted.
*
* @returns {CensusTreeNode}
*/
exports.censusReportToCensusTreeNode = function (breakdown, report,
options = {
invert: false,
filter: null
}) {
// Reset the counter so that turning the same census report into a
// CensusTreeNode tree repeatedly is idempotent.
censusTreeNodeIdCounter = 0;
const visitor = new CensusTreeNodeVisitor();
walk(breakdown, report, visitor);
let result = visitor.root();
if (options.invert) {
result = invert(result);
}
if (typeof options.filter === "string") {
result = filter(result, makeFilterPredicate(options.filter));
}
// If the report is a delta report that was generated by diffing two other
// reports, make sure to use the basis totals rather than the totals of the
// difference.
if (typeof report[basisTotalBytes] === "number") {
result.totalBytes = report[basisTotalBytes];
result.totalCount = report[basisTotalCount];
}
// Inverting and filtering could have messed up the sort order, so do a
// depth-first search of the tree and ensure that siblings are sorted.
const comparator = options.invert ? compareBySelf : compareByTotal;
(function ensureSorted(node) {
if (node.children) {
node.children.sort(comparator);
for (let i = 0, length = node.children.length; i < length; i++) {
ensureSorted(node.children[i]);
}
}
}(result));
return result;
};

View file

@ -1,26 +0,0 @@
#!/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

View file

@ -1,63 +0,0 @@
# -*- 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['MOZ_DEVTOOLS_SERVER']:
DevToolsModules(
'census-tree-node.js',
'CensusUtils.js',
'DominatorTreeNode.js',
'HeapAnalysesClient.js',
'HeapAnalysesWorker.js',
'HeapSnapshotFileUtils.js',
'shortest-paths.js',
)
if CONFIG['GNU_CXX']:
CXXFLAGS += ['-Wno-error=shadow']

View file

@ -1,91 +0,0 @@
/* 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/. */
"use strict";
/**
* Compress a set of paths leading to `target` into a single graph, returned as
* a set of nodes and a set of edges.
*
* @param {NodeId} target
* The target node passed to `HeapSnapshot.computeShortestPaths`.
*
* @param {Array<Path>} paths
* An array of paths to `target`, as returned by
* `HeapSnapshot.computeShortestPaths`.
*
* @returns {Object}
* An object with two properties:
* - edges: An array of unique objects of the form:
* {
* from: <node ID>,
* to: <node ID>,
* name: <string or null>
* }
* - nodes: An array of unique node IDs. Every `from` and `to` id is
* guaranteed to be in this array exactly once.
*/
exports.deduplicatePaths = function (target, paths) {
// Use this structure to de-duplicate edges among many retaining paths from
// start to target.
//
// Map<FromNodeId, Map<ToNodeId, Set<EdgeName>>>
const deduped = new Map();
function insert(from, to, name) {
let toMap = deduped.get(from);
if (!toMap) {
toMap = new Map();
deduped.set(from, toMap);
}
let nameSet = toMap.get(to);
if (!nameSet) {
nameSet = new Set();
toMap.set(to, nameSet);
}
nameSet.add(name);
}
outer: for (let path of paths) {
const pathLength = path.length;
// Check for duplicate predecessors in the path, and skip paths that contain
// them.
const predecessorsSeen = new Set();
predecessorsSeen.add(target);
for (let i = 0; i < pathLength; i++) {
if (predecessorsSeen.has(path[i].predecessor)) {
continue outer;
}
predecessorsSeen.add(path[i].predecessor);
}
for (let i = 0; i < pathLength - 1; i++) {
insert(path[i].predecessor, path[i + 1].predecessor, path[i].edge);
}
insert(path[pathLength - 1].predecessor, target, path[pathLength - 1].edge);
}
const nodes = [target];
const edges = [];
for (let [from, toMap] of deduped) {
// If the second/third/etc shortest path contains the `target` anywhere
// other than the very last node, we could accidentally put the `target` in
// `nodes` more than once.
if (from !== target) {
nodes.push(from);
}
for (let [to, edgeNameSet] of toMap) {
for (let name of edgeNameSet) {
edges.push({ from, to, name });
}
}
}
return { nodes, edges };
};

View file

@ -1,100 +0,0 @@
/* -*- 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.
}
});

View file

@ -1,91 +0,0 @@
/* -*- 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]);
}
});

View file

@ -1,276 +0,0 @@
/* -*- 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__

View file

@ -1,73 +0,0 @@
/* -*- 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));
});

View file

@ -1,64 +0,0 @@
/* -*- 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));
});

View file

@ -1,53 +0,0 @@
/* -*- 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));
});

View file

@ -1,37 +0,0 @@
/* -*- 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));
});

View file

@ -1,30 +0,0 @@
/* -*- 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));
});

View file

@ -1,31 +0,0 @@
# -*- 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'

View file

@ -1,8 +0,0 @@
[DEFAULT]
tags = devtools devtools-memory
skip-if = os == 'android'
support-files =
[test_DominatorTree_01.html]
[test_SaveHeapSnapshot.html]

View file

@ -1,6 +0,0 @@
[DEFAULT]
tags = devtools devtools-memory
support-files =
[test_saveHeapSnapshot_e10s_01.html]

View file

@ -1,37 +0,0 @@
<!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>

View file

@ -1,25 +0,0 @@
<!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>

View file

@ -1,82 +0,0 @@
<!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>

View file

@ -1,6 +0,0 @@
"use strict";
module.exports = {
// Extend from the common devtools xpcshell eslintrc config.
"extends": "../../../../.eslintrc.xpcshell.js"
};

View file

@ -1,165 +0,0 @@
// 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;
}());

View file

@ -1,190 +0,0 @@
// 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 };
})();

View file

@ -1,47 +0,0 @@
/* 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"
});
}

View file

@ -1,448 +0,0 @@
/* 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 };
}

View file

@ -1,46 +0,0 @@
/* 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"
});
}

View file

@ -1,46 +0,0 @@
/* 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);
}

View file

@ -1,45 +0,0 @@
/* 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);
}

View file

@ -1,47 +0,0 @@
/* 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);
}

View file

@ -1,53 +0,0 @@
/* 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);
}

View file

@ -1,132 +0,0 @@
/* 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);
}

View file

@ -1,44 +0,0 @@
/* 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");
}

View file

@ -1,112 +0,0 @@
/* 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);
}

View file

@ -1,30 +0,0 @@
/* 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);
}

View file

@ -1,117 +0,0 @@
/* 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);
}

View file

@ -1,164 +0,0 @@
/* 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);
}

View file

@ -1,23 +0,0 @@
/* 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();
}

View file

@ -1,40 +0,0 @@
/* 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);
});
}

View file

@ -1,13 +0,0 @@
/* 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();
}

View file

@ -1,22 +0,0 @@
/* 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();
}

View file

@ -1,75 +0,0 @@
/* 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();
}

View file

@ -1,56 +0,0 @@
/* 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();
}

View file

@ -1,22 +0,0 @@
/* 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();
});

View file

@ -1,23 +0,0 @@
/* 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();
});

View file

@ -1,59 +0,0 @@
/* 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();
});

View file

@ -1,22 +0,0 @@
/* 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();
});

View file

@ -1,62 +0,0 @@
/* 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();
});

View file

@ -1,89 +0,0 @@
/* 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();
});

View file

@ -1,58 +0,0 @@
/* 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();
});

View file

@ -1,69 +0,0 @@
/* 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();
});

View file

@ -1,31 +0,0 @@
/* 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();
});

View file

@ -1,81 +0,0 @@
/* 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();
});

View file

@ -1,18 +0,0 @@
/* 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();
});

View file

@ -1,54 +0,0 @@
/* 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();
});

View file

@ -1,59 +0,0 @@
/* 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();
});

View file

@ -1,27 +0,0 @@
/* 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();
});

View file

@ -1,29 +0,0 @@
/* 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();
});

View file

@ -1,48 +0,0 @@
/* 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();
});

View file

@ -1,118 +0,0 @@
/* 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();
});

View file

@ -1,44 +0,0 @@
/* 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();
});

View file

@ -1,109 +0,0 @@
/* 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();
});

View file

@ -1,52 +0,0 @@
/* 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();
});

View file

@ -1,69 +0,0 @@
/* 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();
}

View file

@ -1,47 +0,0 @@
/* 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();
}

View file

@ -1,30 +0,0 @@
/* 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);
}

View file

@ -1,70 +0,0 @@
/* 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();
}

View file

@ -1,42 +0,0 @@
/* 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"]);
}

View file

@ -1,31 +0,0 @@
/* 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();
}

View file

@ -1,57 +0,0 @@
/* 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();
}

View file

@ -1,34 +0,0 @@
/* 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();
}

View file

@ -1,36 +0,0 @@
/* 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();
}

View file

@ -1,24 +0,0 @@
/* 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();
}

View file

@ -1,125 +0,0 @@
/* 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();
}

View file

@ -1,82 +0,0 @@
/* 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();
}

View file

@ -1,82 +0,0 @@
/* 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();
}

View file

@ -1,92 +0,0 @@
/* 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();
}

View file

@ -1,68 +0,0 @@
/* 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();
}

Some files were not shown because too many files have changed in this diff Show more