mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-06 15:58:39 +09:00
Reclassify heapsnapshot and nsJSInspector as not part of devtools
This resolves Issue #316
This commit is contained in:
parent
28e0b0cd57
commit
95d5dcd8c5
142 changed files with 19 additions and 15 deletions
1
devtools/shared/heapsnapshot/.gitattributes
vendored
1
devtools/shared/heapsnapshot/.gitattributes
vendored
|
|
@ -1 +0,0 @@
|
|||
CoreDump.pb.* binary
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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_ = ¤tEdge;
|
||||
}
|
||||
|
||||
public:
|
||||
explicit DeserializedEdgeRange(DeserializedNode& node)
|
||||
: node(&node)
|
||||
, i(0)
|
||||
{
|
||||
settle();
|
||||
}
|
||||
|
||||
void popFront() override
|
||||
{
|
||||
i++;
|
||||
settle();
|
||||
}
|
||||
};
|
||||
|
||||
StackFrame
|
||||
Concrete<DeserializedNode>::allocationStack() const
|
||||
{
|
||||
MOZ_ASSERT(hasAllocationStack());
|
||||
auto id = get().allocationStack.ref();
|
||||
auto ptr = get().owner->frames.lookup(id);
|
||||
MOZ_ASSERT(ptr);
|
||||
// See above comment in DeserializedNode::getEdgeReferent about why this
|
||||
// const_cast is needed and safe.
|
||||
return JS::ubi::StackFrame(const_cast<DeserializedStackFrame*>(&*ptr));
|
||||
}
|
||||
|
||||
|
||||
js::UniquePtr<EdgeRange>
|
||||
Concrete<DeserializedNode>::edges(JSContext* cx, bool) const
|
||||
{
|
||||
js::UniquePtr<DeserializedEdgeRange> range(js_new<DeserializedEdgeRange>(get()));
|
||||
|
||||
if (!range)
|
||||
return nullptr;
|
||||
|
||||
return js::UniquePtr<EdgeRange>(range.release());
|
||||
}
|
||||
|
||||
StackFrame
|
||||
ConcreteStackFrame<DeserializedStackFrame>::parent() const
|
||||
{
|
||||
return get().parent.isNothing() ? StackFrame() : get().getParentStackFrame();
|
||||
}
|
||||
|
||||
bool
|
||||
ConcreteStackFrame<DeserializedStackFrame>::constructSavedFrameStack(
|
||||
JSContext* cx,
|
||||
MutableHandleObject outSavedFrameStack) const
|
||||
{
|
||||
StackFrame f(&get());
|
||||
return ConstructSavedFrameStackSlow(cx, f, outSavedFrameStack);
|
||||
}
|
||||
|
||||
} // namespace ubi
|
||||
} // namespace JS
|
||||
|
|
@ -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__
|
||||
|
|
@ -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
|
||||
|
|
@ -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__
|
||||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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
|
|
@ -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__
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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__
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
@ -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']
|
||||
|
|
@ -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 };
|
||||
};
|
||||
|
|
@ -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.
|
||||
}
|
||||
});
|
||||
|
|
@ -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]);
|
||||
}
|
||||
});
|
||||
|
|
@ -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__
|
||||
|
|
@ -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));
|
||||
});
|
||||
|
|
@ -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));
|
||||
});
|
||||
|
|
@ -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));
|
||||
});
|
||||
|
|
@ -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));
|
||||
});
|
||||
|
|
@ -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));
|
||||
});
|
||||
|
|
@ -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'
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
[DEFAULT]
|
||||
tags = devtools devtools-memory
|
||||
skip-if = os == 'android'
|
||||
support-files =
|
||||
|
||||
[test_DominatorTree_01.html]
|
||||
[test_SaveHeapSnapshot.html]
|
||||
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
[DEFAULT]
|
||||
tags = devtools devtools-memory
|
||||
support-files =
|
||||
|
||||
[test_saveHeapSnapshot_e10s_01.html]
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
// Extend from the common devtools xpcshell eslintrc config.
|
||||
"extends": "../../../../.eslintrc.xpcshell.js"
|
||||
};
|
||||
|
|
@ -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;
|
||||
}());
|
||||
|
|
@ -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 };
|
||||
|
||||
})();
|
||||
|
|
@ -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"
|
||||
});
|
||||
}
|
||||
|
|
@ -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 };
|
||||
}
|
||||
|
|
@ -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"
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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"]);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue