diff options
Diffstat (limited to 'dom')
134 files changed, 17795 insertions, 0 deletions
diff --git a/dom/heapsnapshot/.gitattributes b/dom/heapsnapshot/.gitattributes new file mode 100644 index 000000000..44e248a8d --- /dev/null +++ b/dom/heapsnapshot/.gitattributes @@ -0,0 +1 @@ +CoreDump.pb.* binary diff --git a/dom/heapsnapshot/AutoMemMap.cpp b/dom/heapsnapshot/AutoMemMap.cpp new file mode 100644 index 000000000..e725a99c6 --- /dev/null +++ b/dom/heapsnapshot/AutoMemMap.cpp @@ -0,0 +1,64 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/devtools/AutoMemMap.h" + +#include "mozilla/Unused.h" +#include "nsDebug.h" + +namespace mozilla { +namespace devtools { + +AutoMemMap::~AutoMemMap() +{ + if (addr) { + Unused << NS_WARN_IF(PR_MemUnmap(addr, size()) != PR_SUCCESS); + addr = nullptr; + } + + if (fileMap) { + Unused << NS_WARN_IF(PR_CloseFileMap(fileMap) != PR_SUCCESS); + fileMap = nullptr; + } + + if (fd) { + Unused << NS_WARN_IF(PR_Close(fd) != PR_SUCCESS); + fd = nullptr; + } +} + +nsresult +AutoMemMap::init(const char* filePath, int flags, int mode, PRFileMapProtect prot) +{ + MOZ_ASSERT(!fd); + MOZ_ASSERT(!fileMap); + MOZ_ASSERT(!addr); + + if (PR_GetFileInfo64(filePath, &fileInfo) != PR_SUCCESS) + return NS_ERROR_FILE_NOT_FOUND; + + // Check if the file is too big to memmap. + if (fileInfo.size > int64_t(UINT32_MAX)) + return NS_ERROR_INVALID_ARG; + auto length = uint32_t(fileInfo.size); + + fd = PR_Open(filePath, flags, flags); + if (!fd) + return NS_ERROR_UNEXPECTED; + + fileMap = PR_CreateFileMap(fd, fileInfo.size, prot); + if (!fileMap) + return NS_ERROR_UNEXPECTED; + + addr = PR_MemMap(fileMap, 0, length); + if (!addr) + return NS_ERROR_UNEXPECTED; + + return NS_OK; +} + +} // namespace devtools +} // namespace mozilla diff --git a/dom/heapsnapshot/AutoMemMap.h b/dom/heapsnapshot/AutoMemMap.h new file mode 100644 index 000000000..537d68004 --- /dev/null +++ b/dom/heapsnapshot/AutoMemMap.h @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_devtools_AutoMemMap_h +#define mozilla_devtools_AutoMemMap_h + +#include <prio.h> +#include "mozilla/GuardObjects.h" + +namespace mozilla { +namespace devtools { + +// # AutoMemMap +// +// AutoMemMap is an RAII class to manage mapping a file to memory. It is a +// wrapper aorund managing opening and closing a file and calling PR_MemMap and +// PR_MemUnmap. +// +// Example usage: +// +// { +// AutoMemMap mm; +// if (NS_FAILED(mm.init("/path/to/desired/file"))) { +// // Handle the error however you see fit. +// return false; +// } +// +// doStuffWithMappedMemory(mm.address()); +// } +// // The memory is automatically unmapped when the AutoMemMap leaves scope. +class MOZ_RAII AutoMemMap +{ + MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER; + + PRFileInfo64 fileInfo; + PRFileDesc* fd; + PRFileMap* fileMap; + void* addr; + + AutoMemMap(const AutoMemMap& aOther) = delete; + void operator=(const AutoMemMap& aOther) = delete; + +public: + explicit AutoMemMap(MOZ_GUARD_OBJECT_NOTIFIER_ONLY_PARAM) + : fd(nullptr) + , fileMap(nullptr) + , addr(nullptr) + { + MOZ_GUARD_OBJECT_NOTIFIER_INIT; + }; + ~AutoMemMap(); + + // Initialize this AutoMemMap. + nsresult init(const char* filePath, int flags = PR_RDONLY, int mode = 0, + PRFileMapProtect prot = PR_PROT_READONLY); + + // Get the size of the memory mapped file. + uint32_t size() const { + MOZ_ASSERT(fileInfo.size <= UINT32_MAX, + "Should only call size() if init() succeeded."); + return uint32_t(fileInfo.size); + } + + // Get the mapped memory. + void* address() { MOZ_ASSERT(addr); return addr; } + const void* address() const { MOZ_ASSERT(addr); return addr; } +}; + +} // namespace devtools +} // namespace mozilla + +#endif // mozilla_devtools_AutoMemMap_h diff --git a/dom/heapsnapshot/CensusUtils.js b/dom/heapsnapshot/CensusUtils.js new file mode 100644 index 000000000..36bdd2d82 --- /dev/null +++ b/dom/heapsnapshot/CensusUtils.js @@ -0,0 +1,489 @@ +/* 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); +}; diff --git a/dom/heapsnapshot/CoreDump.pb.cc b/dom/heapsnapshot/CoreDump.pb.cc new file mode 100644 index 000000000..6c7c0e8a4 --- /dev/null +++ b/dom/heapsnapshot/CoreDump.pb.cc @@ -0,0 +1,2542 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: CoreDump.proto + +#define INTERNAL_SUPPRESS_PROTOBUF_FIELD_DEPRECATION +#include "CoreDump.pb.h" + +#include <algorithm> + +#include <google/protobuf/stubs/common.h> +#include <google/protobuf/stubs/once.h> +#include <google/protobuf/io/coded_stream.h> +#include <google/protobuf/wire_format_lite_inl.h> +#include <google/protobuf/descriptor.h> +#include <google/protobuf/generated_message_reflection.h> +#include <google/protobuf/reflection_ops.h> +#include <google/protobuf/wire_format.h> +// @@protoc_insertion_point(includes) + +namespace mozilla { +namespace devtools { +namespace protobuf { + +namespace { + +const ::google::protobuf::Descriptor* Metadata_descriptor_ = NULL; +const ::google::protobuf::internal::GeneratedMessageReflection* + Metadata_reflection_ = NULL; +const ::google::protobuf::Descriptor* StackFrame_descriptor_ = NULL; +const ::google::protobuf::internal::GeneratedMessageReflection* + StackFrame_reflection_ = NULL; +struct StackFrameOneofInstance { + const ::mozilla::devtools::protobuf::StackFrame_Data* data_; + ::google::protobuf::uint64 ref_; +}* StackFrame_default_oneof_instance_ = NULL; +const ::google::protobuf::Descriptor* StackFrame_Data_descriptor_ = NULL; +const ::google::protobuf::internal::GeneratedMessageReflection* + StackFrame_Data_reflection_ = NULL; +struct StackFrame_DataOneofInstance { + const ::std::string* source_; + ::google::protobuf::uint64 sourceref_; + const ::std::string* functiondisplayname_; + ::google::protobuf::uint64 functiondisplaynameref_; +}* StackFrame_Data_default_oneof_instance_ = NULL; +const ::google::protobuf::Descriptor* Node_descriptor_ = NULL; +const ::google::protobuf::internal::GeneratedMessageReflection* + Node_reflection_ = NULL; +struct NodeOneofInstance { + const ::std::string* typename__; + ::google::protobuf::uint64 typenameref_; + const ::std::string* jsobjectclassname_; + ::google::protobuf::uint64 jsobjectclassnameref_; + const ::std::string* scriptfilename_; + ::google::protobuf::uint64 scriptfilenameref_; +}* Node_default_oneof_instance_ = NULL; +const ::google::protobuf::Descriptor* Edge_descriptor_ = NULL; +const ::google::protobuf::internal::GeneratedMessageReflection* + Edge_reflection_ = NULL; +struct EdgeOneofInstance { + const ::std::string* name_; + ::google::protobuf::uint64 nameref_; +}* Edge_default_oneof_instance_ = NULL; + +} // namespace + + +void protobuf_AssignDesc_CoreDump_2eproto() { + protobuf_AddDesc_CoreDump_2eproto(); + const ::google::protobuf::FileDescriptor* file = + ::google::protobuf::DescriptorPool::generated_pool()->FindFileByName( + "CoreDump.proto"); + GOOGLE_CHECK(file != NULL); + Metadata_descriptor_ = file->message_type(0); + static const int Metadata_offsets_[1] = { + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Metadata, timestamp_), + }; + Metadata_reflection_ = + new ::google::protobuf::internal::GeneratedMessageReflection( + Metadata_descriptor_, + Metadata::default_instance_, + Metadata_offsets_, + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Metadata, _has_bits_[0]), + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Metadata, _unknown_fields_), + -1, + ::google::protobuf::DescriptorPool::generated_pool(), + ::google::protobuf::MessageFactory::generated_factory(), + sizeof(Metadata)); + StackFrame_descriptor_ = file->message_type(1); + static const int StackFrame_offsets_[3] = { + PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(StackFrame_default_oneof_instance_, data_), + PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(StackFrame_default_oneof_instance_, ref_), + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame, StackFrameType_), + }; + StackFrame_reflection_ = + new ::google::protobuf::internal::GeneratedMessageReflection( + StackFrame_descriptor_, + StackFrame::default_instance_, + StackFrame_offsets_, + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame, _has_bits_[0]), + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame, _unknown_fields_), + -1, + StackFrame_default_oneof_instance_, + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame, _oneof_case_[0]), + ::google::protobuf::DescriptorPool::generated_pool(), + ::google::protobuf::MessageFactory::generated_factory(), + sizeof(StackFrame)); + StackFrame_Data_descriptor_ = StackFrame_descriptor_->nested_type(0); + static const int StackFrame_Data_offsets_[12] = { + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame_Data, id_), + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame_Data, parent_), + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame_Data, line_), + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame_Data, column_), + PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(StackFrame_Data_default_oneof_instance_, source_), + PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(StackFrame_Data_default_oneof_instance_, sourceref_), + PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(StackFrame_Data_default_oneof_instance_, functiondisplayname_), + PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(StackFrame_Data_default_oneof_instance_, functiondisplaynameref_), + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame_Data, issystem_), + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame_Data, isselfhosted_), + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame_Data, SourceOrRef_), + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame_Data, FunctionDisplayNameOrRef_), + }; + StackFrame_Data_reflection_ = + new ::google::protobuf::internal::GeneratedMessageReflection( + StackFrame_Data_descriptor_, + StackFrame_Data::default_instance_, + StackFrame_Data_offsets_, + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame_Data, _has_bits_[0]), + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame_Data, _unknown_fields_), + -1, + StackFrame_Data_default_oneof_instance_, + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame_Data, _oneof_case_[0]), + ::google::protobuf::DescriptorPool::generated_pool(), + ::google::protobuf::MessageFactory::generated_factory(), + sizeof(StackFrame_Data)); + Node_descriptor_ = file->message_type(2); + static const int Node_offsets_[14] = { + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Node, id_), + PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(Node_default_oneof_instance_, typename__), + PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(Node_default_oneof_instance_, typenameref_), + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Node, size_), + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Node, edges_), + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Node, allocationstack_), + PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(Node_default_oneof_instance_, jsobjectclassname_), + PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(Node_default_oneof_instance_, jsobjectclassnameref_), + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Node, coarsetype_), + PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(Node_default_oneof_instance_, scriptfilename_), + PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(Node_default_oneof_instance_, scriptfilenameref_), + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Node, TypeNameOrRef_), + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Node, JSObjectClassNameOrRef_), + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Node, ScriptFilenameOrRef_), + }; + Node_reflection_ = + new ::google::protobuf::internal::GeneratedMessageReflection( + Node_descriptor_, + Node::default_instance_, + Node_offsets_, + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Node, _has_bits_[0]), + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Node, _unknown_fields_), + -1, + Node_default_oneof_instance_, + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Node, _oneof_case_[0]), + ::google::protobuf::DescriptorPool::generated_pool(), + ::google::protobuf::MessageFactory::generated_factory(), + sizeof(Node)); + Edge_descriptor_ = file->message_type(3); + static const int Edge_offsets_[4] = { + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Edge, referent_), + PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(Edge_default_oneof_instance_, name_), + PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(Edge_default_oneof_instance_, nameref_), + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Edge, EdgeNameOrRef_), + }; + Edge_reflection_ = + new ::google::protobuf::internal::GeneratedMessageReflection( + Edge_descriptor_, + Edge::default_instance_, + Edge_offsets_, + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Edge, _has_bits_[0]), + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Edge, _unknown_fields_), + -1, + Edge_default_oneof_instance_, + GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Edge, _oneof_case_[0]), + ::google::protobuf::DescriptorPool::generated_pool(), + ::google::protobuf::MessageFactory::generated_factory(), + sizeof(Edge)); +} + +namespace { + +GOOGLE_PROTOBUF_DECLARE_ONCE(protobuf_AssignDescriptors_once_); +inline void protobuf_AssignDescriptorsOnce() { + ::google::protobuf::GoogleOnceInit(&protobuf_AssignDescriptors_once_, + &protobuf_AssignDesc_CoreDump_2eproto); +} + +void protobuf_RegisterTypes(const ::std::string&) { + protobuf_AssignDescriptorsOnce(); + ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage( + Metadata_descriptor_, &Metadata::default_instance()); + ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage( + StackFrame_descriptor_, &StackFrame::default_instance()); + ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage( + StackFrame_Data_descriptor_, &StackFrame_Data::default_instance()); + ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage( + Node_descriptor_, &Node::default_instance()); + ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage( + Edge_descriptor_, &Edge::default_instance()); +} + +} // namespace + +void protobuf_ShutdownFile_CoreDump_2eproto() { + delete Metadata::default_instance_; + delete Metadata_reflection_; + delete StackFrame::default_instance_; + delete StackFrame_default_oneof_instance_; + delete StackFrame_reflection_; + delete StackFrame_Data::default_instance_; + delete StackFrame_Data_default_oneof_instance_; + delete StackFrame_Data_reflection_; + delete Node::default_instance_; + delete Node_default_oneof_instance_; + delete Node_reflection_; + delete Edge::default_instance_; + delete Edge_default_oneof_instance_; + delete Edge_reflection_; +} + +void protobuf_AddDesc_CoreDump_2eproto() { + static bool already_here = false; + if (already_here) return; + already_here = true; + GOOGLE_PROTOBUF_VERIFY_VERSION; + + ::google::protobuf::DescriptorPool::InternalAddGeneratedFile( + "\n\016CoreDump.proto\022\031mozilla.devtools.proto" + "buf\"\035\n\010Metadata\022\021\n\ttimeStamp\030\001 \001(\004\"\216\003\n\nS" + "tackFrame\022:\n\004data\030\001 \001(\0132*.mozilla.devtoo" + "ls.protobuf.StackFrame.DataH\000\022\r\n\003ref\030\002 \001" + "(\004H\000\032\242\002\n\004Data\022\n\n\002id\030\001 \001(\004\0225\n\006parent\030\002 \001(" + "\0132%.mozilla.devtools.protobuf.StackFrame" + "\022\014\n\004line\030\003 \001(\r\022\016\n\006column\030\004 \001(\r\022\020\n\006source" + "\030\005 \001(\014H\000\022\023\n\tsourceRef\030\006 \001(\004H\000\022\035\n\023functio" + "nDisplayName\030\007 \001(\014H\001\022 \n\026functionDisplayN" + "ameRef\030\010 \001(\004H\001\022\020\n\010isSystem\030\t \001(\010\022\024\n\014isSe" + "lfHosted\030\n \001(\010B\r\n\013SourceOrRefB\032\n\030Functio" + "nDisplayNameOrRefB\020\n\016StackFrameType\"\210\003\n\004" + "Node\022\n\n\002id\030\001 \001(\004\022\022\n\010typeName\030\002 \001(\014H\000\022\025\n\013" + "typeNameRef\030\003 \001(\004H\000\022\014\n\004size\030\004 \001(\004\022.\n\005edg" + "es\030\005 \003(\0132\037.mozilla.devtools.protobuf.Edg" + "e\022>\n\017allocationStack\030\006 \001(\0132%.mozilla.dev" + "tools.protobuf.StackFrame\022\033\n\021jsObjectCla" + "ssName\030\007 \001(\014H\001\022\036\n\024jsObjectClassNameRef\030\010" + " \001(\004H\001\022\025\n\ncoarseType\030\t \001(\r:\0010\022\030\n\016scriptF" + "ilename\030\n \001(\014H\002\022\033\n\021scriptFilenameRef\030\013 \001" + "(\004H\002B\017\n\rTypeNameOrRefB\030\n\026JSObjectClassNa" + "meOrRefB\025\n\023ScriptFilenameOrRef\"L\n\004Edge\022\020" + "\n\010referent\030\001 \001(\004\022\016\n\004name\030\002 \001(\014H\000\022\021\n\007name" + "Ref\030\003 \001(\004H\000B\017\n\rEdgeNameOrRef", 948); + ::google::protobuf::MessageFactory::InternalRegisterGeneratedFile( + "CoreDump.proto", &protobuf_RegisterTypes); + Metadata::default_instance_ = new Metadata(); + StackFrame::default_instance_ = new StackFrame(); + StackFrame_default_oneof_instance_ = new StackFrameOneofInstance; + StackFrame_Data::default_instance_ = new StackFrame_Data(); + StackFrame_Data_default_oneof_instance_ = new StackFrame_DataOneofInstance; + Node::default_instance_ = new Node(); + Node_default_oneof_instance_ = new NodeOneofInstance; + Edge::default_instance_ = new Edge(); + Edge_default_oneof_instance_ = new EdgeOneofInstance; + Metadata::default_instance_->InitAsDefaultInstance(); + StackFrame::default_instance_->InitAsDefaultInstance(); + StackFrame_Data::default_instance_->InitAsDefaultInstance(); + Node::default_instance_->InitAsDefaultInstance(); + Edge::default_instance_->InitAsDefaultInstance(); + ::google::protobuf::internal::OnShutdown(&protobuf_ShutdownFile_CoreDump_2eproto); +} + +// Force AddDescriptors() to be called at static initialization time. +struct StaticDescriptorInitializer_CoreDump_2eproto { + StaticDescriptorInitializer_CoreDump_2eproto() { + protobuf_AddDesc_CoreDump_2eproto(); + } +} static_descriptor_initializer_CoreDump_2eproto_; + +// =================================================================== + +#ifndef _MSC_VER +const int Metadata::kTimeStampFieldNumber; +#endif // !_MSC_VER + +Metadata::Metadata() + : ::google::protobuf::Message() { + SharedCtor(); + // @@protoc_insertion_point(constructor:mozilla.devtools.protobuf.Metadata) +} + +void Metadata::InitAsDefaultInstance() { +} + +Metadata::Metadata(const Metadata& from) + : ::google::protobuf::Message() { + SharedCtor(); + MergeFrom(from); + // @@protoc_insertion_point(copy_constructor:mozilla.devtools.protobuf.Metadata) +} + +void Metadata::SharedCtor() { + _cached_size_ = 0; + timestamp_ = GOOGLE_ULONGLONG(0); + ::memset(_has_bits_, 0, sizeof(_has_bits_)); +} + +Metadata::~Metadata() { + // @@protoc_insertion_point(destructor:mozilla.devtools.protobuf.Metadata) + SharedDtor(); +} + +void Metadata::SharedDtor() { + if (this != default_instance_) { + } +} + +void Metadata::SetCachedSize(int size) const { + GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN(); + _cached_size_ = size; + GOOGLE_SAFE_CONCURRENT_WRITES_END(); +} +const ::google::protobuf::Descriptor* Metadata::descriptor() { + protobuf_AssignDescriptorsOnce(); + return Metadata_descriptor_; +} + +const Metadata& Metadata::default_instance() { + if (default_instance_ == NULL) protobuf_AddDesc_CoreDump_2eproto(); + return *default_instance_; +} + +Metadata* Metadata::default_instance_ = NULL; + +Metadata* Metadata::New() const { + return new Metadata; +} + +void Metadata::Clear() { + timestamp_ = GOOGLE_ULONGLONG(0); + ::memset(_has_bits_, 0, sizeof(_has_bits_)); + mutable_unknown_fields()->Clear(); +} + +bool Metadata::MergePartialFromCodedStream( + ::google::protobuf::io::CodedInputStream* input) { +#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure + ::google::protobuf::uint32 tag; + // @@protoc_insertion_point(parse_start:mozilla.devtools.protobuf.Metadata) + for (;;) { + ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127); + tag = p.first; + if (!p.second) goto handle_unusual; + switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) { + // optional uint64 timeStamp = 1; + case 1: { + if (tag == 8) { + DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive< + ::google::protobuf::uint64, ::google::protobuf::internal::WireFormatLite::TYPE_UINT64>( + input, ×tamp_))); + set_has_timestamp(); + } else { + goto handle_unusual; + } + if (input->ExpectAtEnd()) goto success; + break; + } + + default: { + handle_unusual: + if (tag == 0 || + ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) == + ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) { + goto success; + } + DO_(::google::protobuf::internal::WireFormat::SkipField( + input, tag, mutable_unknown_fields())); + break; + } + } + } +success: + // @@protoc_insertion_point(parse_success:mozilla.devtools.protobuf.Metadata) + return true; +failure: + // @@protoc_insertion_point(parse_failure:mozilla.devtools.protobuf.Metadata) + return false; +#undef DO_ +} + +void Metadata::SerializeWithCachedSizes( + ::google::protobuf::io::CodedOutputStream* output) const { + // @@protoc_insertion_point(serialize_start:mozilla.devtools.protobuf.Metadata) + // optional uint64 timeStamp = 1; + if (has_timestamp()) { + ::google::protobuf::internal::WireFormatLite::WriteUInt64(1, this->timestamp(), output); + } + + if (!unknown_fields().empty()) { + ::google::protobuf::internal::WireFormat::SerializeUnknownFields( + unknown_fields(), output); + } + // @@protoc_insertion_point(serialize_end:mozilla.devtools.protobuf.Metadata) +} + +::google::protobuf::uint8* Metadata::SerializeWithCachedSizesToArray( + ::google::protobuf::uint8* target) const { + // @@protoc_insertion_point(serialize_to_array_start:mozilla.devtools.protobuf.Metadata) + // optional uint64 timeStamp = 1; + if (has_timestamp()) { + target = ::google::protobuf::internal::WireFormatLite::WriteUInt64ToArray(1, this->timestamp(), target); + } + + if (!unknown_fields().empty()) { + target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray( + unknown_fields(), target); + } + // @@protoc_insertion_point(serialize_to_array_end:mozilla.devtools.protobuf.Metadata) + return target; +} + +int Metadata::ByteSize() const { + int total_size = 0; + + if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) { + // optional uint64 timeStamp = 1; + if (has_timestamp()) { + total_size += 1 + + ::google::protobuf::internal::WireFormatLite::UInt64Size( + this->timestamp()); + } + + } + if (!unknown_fields().empty()) { + total_size += + ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize( + unknown_fields()); + } + GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN(); + _cached_size_ = total_size; + GOOGLE_SAFE_CONCURRENT_WRITES_END(); + return total_size; +} + +void Metadata::MergeFrom(const ::google::protobuf::Message& from) { + GOOGLE_CHECK_NE(&from, this); + const Metadata* source = + ::google::protobuf::internal::dynamic_cast_if_available<const Metadata*>( + &from); + if (source == NULL) { + ::google::protobuf::internal::ReflectionOps::Merge(from, this); + } else { + MergeFrom(*source); + } +} + +void Metadata::MergeFrom(const Metadata& from) { + GOOGLE_CHECK_NE(&from, this); + if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) { + if (from.has_timestamp()) { + set_timestamp(from.timestamp()); + } + } + mutable_unknown_fields()->MergeFrom(from.unknown_fields()); +} + +void Metadata::CopyFrom(const ::google::protobuf::Message& from) { + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +void Metadata::CopyFrom(const Metadata& from) { + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool Metadata::IsInitialized() const { + + return true; +} + +void Metadata::Swap(Metadata* other) { + if (other != this) { + std::swap(timestamp_, other->timestamp_); + std::swap(_has_bits_[0], other->_has_bits_[0]); + _unknown_fields_.Swap(&other->_unknown_fields_); + std::swap(_cached_size_, other->_cached_size_); + } +} + +::google::protobuf::Metadata Metadata::GetMetadata() const { + protobuf_AssignDescriptorsOnce(); + ::google::protobuf::Metadata metadata; + metadata.descriptor = Metadata_descriptor_; + metadata.reflection = Metadata_reflection_; + return metadata; +} + + +// =================================================================== + +#ifndef _MSC_VER +const int StackFrame_Data::kIdFieldNumber; +const int StackFrame_Data::kParentFieldNumber; +const int StackFrame_Data::kLineFieldNumber; +const int StackFrame_Data::kColumnFieldNumber; +const int StackFrame_Data::kSourceFieldNumber; +const int StackFrame_Data::kSourceRefFieldNumber; +const int StackFrame_Data::kFunctionDisplayNameFieldNumber; +const int StackFrame_Data::kFunctionDisplayNameRefFieldNumber; +const int StackFrame_Data::kIsSystemFieldNumber; +const int StackFrame_Data::kIsSelfHostedFieldNumber; +#endif // !_MSC_VER + +StackFrame_Data::StackFrame_Data() + : ::google::protobuf::Message() { + SharedCtor(); + // @@protoc_insertion_point(constructor:mozilla.devtools.protobuf.StackFrame.Data) +} + +void StackFrame_Data::InitAsDefaultInstance() { + parent_ = const_cast< ::mozilla::devtools::protobuf::StackFrame*>(&::mozilla::devtools::protobuf::StackFrame::default_instance()); + StackFrame_Data_default_oneof_instance_->source_ = &::google::protobuf::internal::GetEmptyStringAlreadyInited(); + StackFrame_Data_default_oneof_instance_->sourceref_ = GOOGLE_ULONGLONG(0); + StackFrame_Data_default_oneof_instance_->functiondisplayname_ = &::google::protobuf::internal::GetEmptyStringAlreadyInited(); + StackFrame_Data_default_oneof_instance_->functiondisplaynameref_ = GOOGLE_ULONGLONG(0); +} + +StackFrame_Data::StackFrame_Data(const StackFrame_Data& from) + : ::google::protobuf::Message() { + SharedCtor(); + MergeFrom(from); + // @@protoc_insertion_point(copy_constructor:mozilla.devtools.protobuf.StackFrame.Data) +} + +void StackFrame_Data::SharedCtor() { + ::google::protobuf::internal::GetEmptyString(); + _cached_size_ = 0; + id_ = GOOGLE_ULONGLONG(0); + parent_ = NULL; + line_ = 0u; + column_ = 0u; + issystem_ = false; + isselfhosted_ = false; + ::memset(_has_bits_, 0, sizeof(_has_bits_)); + clear_has_SourceOrRef(); + clear_has_FunctionDisplayNameOrRef(); +} + +StackFrame_Data::~StackFrame_Data() { + // @@protoc_insertion_point(destructor:mozilla.devtools.protobuf.StackFrame.Data) + SharedDtor(); +} + +void StackFrame_Data::SharedDtor() { + if (has_SourceOrRef()) { + clear_SourceOrRef(); + } + if (has_FunctionDisplayNameOrRef()) { + clear_FunctionDisplayNameOrRef(); + } + if (this != default_instance_) { + delete parent_; + } +} + +void StackFrame_Data::SetCachedSize(int size) const { + GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN(); + _cached_size_ = size; + GOOGLE_SAFE_CONCURRENT_WRITES_END(); +} +const ::google::protobuf::Descriptor* StackFrame_Data::descriptor() { + protobuf_AssignDescriptorsOnce(); + return StackFrame_Data_descriptor_; +} + +const StackFrame_Data& StackFrame_Data::default_instance() { + if (default_instance_ == NULL) protobuf_AddDesc_CoreDump_2eproto(); + return *default_instance_; +} + +StackFrame_Data* StackFrame_Data::default_instance_ = NULL; + +StackFrame_Data* StackFrame_Data::New() const { + return new StackFrame_Data; +} + +void StackFrame_Data::clear_SourceOrRef() { + switch(SourceOrRef_case()) { + case kSource: { + delete SourceOrRef_.source_; + break; + } + case kSourceRef: { + // No need to clear + break; + } + case SOURCEORREF_NOT_SET: { + break; + } + } + _oneof_case_[0] = SOURCEORREF_NOT_SET; +} + +void StackFrame_Data::clear_FunctionDisplayNameOrRef() { + switch(FunctionDisplayNameOrRef_case()) { + case kFunctionDisplayName: { + delete FunctionDisplayNameOrRef_.functiondisplayname_; + break; + } + case kFunctionDisplayNameRef: { + // No need to clear + break; + } + case FUNCTIONDISPLAYNAMEORREF_NOT_SET: { + break; + } + } + _oneof_case_[1] = FUNCTIONDISPLAYNAMEORREF_NOT_SET; +} + + +void StackFrame_Data::Clear() { +#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>( \ + &reinterpret_cast<StackFrame_Data*>(16)->f) - \ + reinterpret_cast<char*>(16)) + +#define ZR_(first, last) do { \ + size_t f = OFFSET_OF_FIELD_(first); \ + size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last); \ + ::memset(&first, 0, n); \ + } while (0) + + if (_has_bits_[0 / 32] & 15) { + ZR_(line_, column_); + id_ = GOOGLE_ULONGLONG(0); + if (has_parent()) { + if (parent_ != NULL) parent_->::mozilla::devtools::protobuf::StackFrame::Clear(); + } + } + ZR_(issystem_, isselfhosted_); + +#undef OFFSET_OF_FIELD_ +#undef ZR_ + + clear_SourceOrRef(); + clear_FunctionDisplayNameOrRef(); + ::memset(_has_bits_, 0, sizeof(_has_bits_)); + mutable_unknown_fields()->Clear(); +} + +bool StackFrame_Data::MergePartialFromCodedStream( + ::google::protobuf::io::CodedInputStream* input) { +#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure + ::google::protobuf::uint32 tag; + // @@protoc_insertion_point(parse_start:mozilla.devtools.protobuf.StackFrame.Data) + for (;;) { + ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127); + tag = p.first; + if (!p.second) goto handle_unusual; + switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) { + // optional uint64 id = 1; + case 1: { + if (tag == 8) { + DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive< + ::google::protobuf::uint64, ::google::protobuf::internal::WireFormatLite::TYPE_UINT64>( + input, &id_))); + set_has_id(); + } else { + goto handle_unusual; + } + if (input->ExpectTag(18)) goto parse_parent; + break; + } + + // optional .mozilla.devtools.protobuf.StackFrame parent = 2; + case 2: { + if (tag == 18) { + parse_parent: + DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual( + input, mutable_parent())); + } else { + goto handle_unusual; + } + if (input->ExpectTag(24)) goto parse_line; + break; + } + + // optional uint32 line = 3; + case 3: { + if (tag == 24) { + parse_line: + DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive< + ::google::protobuf::uint32, ::google::protobuf::internal::WireFormatLite::TYPE_UINT32>( + input, &line_))); + set_has_line(); + } else { + goto handle_unusual; + } + if (input->ExpectTag(32)) goto parse_column; + break; + } + + // optional uint32 column = 4; + case 4: { + if (tag == 32) { + parse_column: + DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive< + ::google::protobuf::uint32, ::google::protobuf::internal::WireFormatLite::TYPE_UINT32>( + input, &column_))); + set_has_column(); + } else { + goto handle_unusual; + } + if (input->ExpectTag(42)) goto parse_source; + break; + } + + // optional bytes source = 5; + case 5: { + if (tag == 42) { + parse_source: + DO_(::google::protobuf::internal::WireFormatLite::ReadBytes( + input, this->mutable_source())); + } else { + goto handle_unusual; + } + if (input->ExpectTag(48)) goto parse_sourceRef; + break; + } + + // optional uint64 sourceRef = 6; + case 6: { + if (tag == 48) { + parse_sourceRef: + clear_SourceOrRef(); + DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive< + ::google::protobuf::uint64, ::google::protobuf::internal::WireFormatLite::TYPE_UINT64>( + input, &SourceOrRef_.sourceref_))); + set_has_sourceref(); + } else { + goto handle_unusual; + } + if (input->ExpectTag(58)) goto parse_functionDisplayName; + break; + } + + // optional bytes functionDisplayName = 7; + case 7: { + if (tag == 58) { + parse_functionDisplayName: + DO_(::google::protobuf::internal::WireFormatLite::ReadBytes( + input, this->mutable_functiondisplayname())); + } else { + goto handle_unusual; + } + if (input->ExpectTag(64)) goto parse_functionDisplayNameRef; + break; + } + + // optional uint64 functionDisplayNameRef = 8; + case 8: { + if (tag == 64) { + parse_functionDisplayNameRef: + clear_FunctionDisplayNameOrRef(); + DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive< + ::google::protobuf::uint64, ::google::protobuf::internal::WireFormatLite::TYPE_UINT64>( + input, &FunctionDisplayNameOrRef_.functiondisplaynameref_))); + set_has_functiondisplaynameref(); + } else { + goto handle_unusual; + } + if (input->ExpectTag(72)) goto parse_isSystem; + break; + } + + // optional bool isSystem = 9; + case 9: { + if (tag == 72) { + parse_isSystem: + DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive< + bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>( + input, &issystem_))); + set_has_issystem(); + } else { + goto handle_unusual; + } + if (input->ExpectTag(80)) goto parse_isSelfHosted; + break; + } + + // optional bool isSelfHosted = 10; + case 10: { + if (tag == 80) { + parse_isSelfHosted: + DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive< + bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>( + input, &isselfhosted_))); + set_has_isselfhosted(); + } else { + goto handle_unusual; + } + if (input->ExpectAtEnd()) goto success; + break; + } + + default: { + handle_unusual: + if (tag == 0 || + ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) == + ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) { + goto success; + } + DO_(::google::protobuf::internal::WireFormat::SkipField( + input, tag, mutable_unknown_fields())); + break; + } + } + } +success: + // @@protoc_insertion_point(parse_success:mozilla.devtools.protobuf.StackFrame.Data) + return true; +failure: + // @@protoc_insertion_point(parse_failure:mozilla.devtools.protobuf.StackFrame.Data) + return false; +#undef DO_ +} + +void StackFrame_Data::SerializeWithCachedSizes( + ::google::protobuf::io::CodedOutputStream* output) const { + // @@protoc_insertion_point(serialize_start:mozilla.devtools.protobuf.StackFrame.Data) + // optional uint64 id = 1; + if (has_id()) { + ::google::protobuf::internal::WireFormatLite::WriteUInt64(1, this->id(), output); + } + + // optional .mozilla.devtools.protobuf.StackFrame parent = 2; + if (has_parent()) { + ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray( + 2, this->parent(), output); + } + + // optional uint32 line = 3; + if (has_line()) { + ::google::protobuf::internal::WireFormatLite::WriteUInt32(3, this->line(), output); + } + + // optional uint32 column = 4; + if (has_column()) { + ::google::protobuf::internal::WireFormatLite::WriteUInt32(4, this->column(), output); + } + + // optional bytes source = 5; + if (has_source()) { + ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased( + 5, this->source(), output); + } + + // optional uint64 sourceRef = 6; + if (has_sourceref()) { + ::google::protobuf::internal::WireFormatLite::WriteUInt64(6, this->sourceref(), output); + } + + // optional bytes functionDisplayName = 7; + if (has_functiondisplayname()) { + ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased( + 7, this->functiondisplayname(), output); + } + + // optional uint64 functionDisplayNameRef = 8; + if (has_functiondisplaynameref()) { + ::google::protobuf::internal::WireFormatLite::WriteUInt64(8, this->functiondisplaynameref(), output); + } + + // optional bool isSystem = 9; + if (has_issystem()) { + ::google::protobuf::internal::WireFormatLite::WriteBool(9, this->issystem(), output); + } + + // optional bool isSelfHosted = 10; + if (has_isselfhosted()) { + ::google::protobuf::internal::WireFormatLite::WriteBool(10, this->isselfhosted(), output); + } + + if (!unknown_fields().empty()) { + ::google::protobuf::internal::WireFormat::SerializeUnknownFields( + unknown_fields(), output); + } + // @@protoc_insertion_point(serialize_end:mozilla.devtools.protobuf.StackFrame.Data) +} + +::google::protobuf::uint8* StackFrame_Data::SerializeWithCachedSizesToArray( + ::google::protobuf::uint8* target) const { + // @@protoc_insertion_point(serialize_to_array_start:mozilla.devtools.protobuf.StackFrame.Data) + // optional uint64 id = 1; + if (has_id()) { + target = ::google::protobuf::internal::WireFormatLite::WriteUInt64ToArray(1, this->id(), target); + } + + // optional .mozilla.devtools.protobuf.StackFrame parent = 2; + if (has_parent()) { + target = ::google::protobuf::internal::WireFormatLite:: + WriteMessageNoVirtualToArray( + 2, this->parent(), target); + } + + // optional uint32 line = 3; + if (has_line()) { + target = ::google::protobuf::internal::WireFormatLite::WriteUInt32ToArray(3, this->line(), target); + } + + // optional uint32 column = 4; + if (has_column()) { + target = ::google::protobuf::internal::WireFormatLite::WriteUInt32ToArray(4, this->column(), target); + } + + // optional bytes source = 5; + if (has_source()) { + target = + ::google::protobuf::internal::WireFormatLite::WriteBytesToArray( + 5, this->source(), target); + } + + // optional uint64 sourceRef = 6; + if (has_sourceref()) { + target = ::google::protobuf::internal::WireFormatLite::WriteUInt64ToArray(6, this->sourceref(), target); + } + + // optional bytes functionDisplayName = 7; + if (has_functiondisplayname()) { + target = + ::google::protobuf::internal::WireFormatLite::WriteBytesToArray( + 7, this->functiondisplayname(), target); + } + + // optional uint64 functionDisplayNameRef = 8; + if (has_functiondisplaynameref()) { + target = ::google::protobuf::internal::WireFormatLite::WriteUInt64ToArray(8, this->functiondisplaynameref(), target); + } + + // optional bool isSystem = 9; + if (has_issystem()) { + target = ::google::protobuf::internal::WireFormatLite::WriteBoolToArray(9, this->issystem(), target); + } + + // optional bool isSelfHosted = 10; + if (has_isselfhosted()) { + target = ::google::protobuf::internal::WireFormatLite::WriteBoolToArray(10, this->isselfhosted(), target); + } + + if (!unknown_fields().empty()) { + target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray( + unknown_fields(), target); + } + // @@protoc_insertion_point(serialize_to_array_end:mozilla.devtools.protobuf.StackFrame.Data) + return target; +} + +int StackFrame_Data::ByteSize() const { + int total_size = 0; + + if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) { + // optional uint64 id = 1; + if (has_id()) { + total_size += 1 + + ::google::protobuf::internal::WireFormatLite::UInt64Size( + this->id()); + } + + // optional .mozilla.devtools.protobuf.StackFrame parent = 2; + if (has_parent()) { + total_size += 1 + + ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual( + this->parent()); + } + + // optional uint32 line = 3; + if (has_line()) { + total_size += 1 + + ::google::protobuf::internal::WireFormatLite::UInt32Size( + this->line()); + } + + // optional uint32 column = 4; + if (has_column()) { + total_size += 1 + + ::google::protobuf::internal::WireFormatLite::UInt32Size( + this->column()); + } + + } + if (_has_bits_[8 / 32] & (0xffu << (8 % 32))) { + // optional bool isSystem = 9; + if (has_issystem()) { + total_size += 1 + 1; + } + + // optional bool isSelfHosted = 10; + if (has_isselfhosted()) { + total_size += 1 + 1; + } + + } + switch (SourceOrRef_case()) { + // optional bytes source = 5; + case kSource: { + total_size += 1 + + ::google::protobuf::internal::WireFormatLite::BytesSize( + this->source()); + break; + } + // optional uint64 sourceRef = 6; + case kSourceRef: { + total_size += 1 + + ::google::protobuf::internal::WireFormatLite::UInt64Size( + this->sourceref()); + break; + } + case SOURCEORREF_NOT_SET: { + break; + } + } + switch (FunctionDisplayNameOrRef_case()) { + // optional bytes functionDisplayName = 7; + case kFunctionDisplayName: { + total_size += 1 + + ::google::protobuf::internal::WireFormatLite::BytesSize( + this->functiondisplayname()); + break; + } + // optional uint64 functionDisplayNameRef = 8; + case kFunctionDisplayNameRef: { + total_size += 1 + + ::google::protobuf::internal::WireFormatLite::UInt64Size( + this->functiondisplaynameref()); + break; + } + case FUNCTIONDISPLAYNAMEORREF_NOT_SET: { + break; + } + } + if (!unknown_fields().empty()) { + total_size += + ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize( + unknown_fields()); + } + GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN(); + _cached_size_ = total_size; + GOOGLE_SAFE_CONCURRENT_WRITES_END(); + return total_size; +} + +void StackFrame_Data::MergeFrom(const ::google::protobuf::Message& from) { + GOOGLE_CHECK_NE(&from, this); + const StackFrame_Data* source = + ::google::protobuf::internal::dynamic_cast_if_available<const StackFrame_Data*>( + &from); + if (source == NULL) { + ::google::protobuf::internal::ReflectionOps::Merge(from, this); + } else { + MergeFrom(*source); + } +} + +void StackFrame_Data::MergeFrom(const StackFrame_Data& from) { + GOOGLE_CHECK_NE(&from, this); + switch (from.SourceOrRef_case()) { + case kSource: { + set_source(from.source()); + break; + } + case kSourceRef: { + set_sourceref(from.sourceref()); + break; + } + case SOURCEORREF_NOT_SET: { + break; + } + } + switch (from.FunctionDisplayNameOrRef_case()) { + case kFunctionDisplayName: { + set_functiondisplayname(from.functiondisplayname()); + break; + } + case kFunctionDisplayNameRef: { + set_functiondisplaynameref(from.functiondisplaynameref()); + break; + } + case FUNCTIONDISPLAYNAMEORREF_NOT_SET: { + break; + } + } + if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) { + if (from.has_id()) { + set_id(from.id()); + } + if (from.has_parent()) { + mutable_parent()->::mozilla::devtools::protobuf::StackFrame::MergeFrom(from.parent()); + } + if (from.has_line()) { + set_line(from.line()); + } + if (from.has_column()) { + set_column(from.column()); + } + } + if (from._has_bits_[8 / 32] & (0xffu << (8 % 32))) { + if (from.has_issystem()) { + set_issystem(from.issystem()); + } + if (from.has_isselfhosted()) { + set_isselfhosted(from.isselfhosted()); + } + } + mutable_unknown_fields()->MergeFrom(from.unknown_fields()); +} + +void StackFrame_Data::CopyFrom(const ::google::protobuf::Message& from) { + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +void StackFrame_Data::CopyFrom(const StackFrame_Data& from) { + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool StackFrame_Data::IsInitialized() const { + + return true; +} + +void StackFrame_Data::Swap(StackFrame_Data* other) { + if (other != this) { + std::swap(id_, other->id_); + std::swap(parent_, other->parent_); + std::swap(line_, other->line_); + std::swap(column_, other->column_); + std::swap(issystem_, other->issystem_); + std::swap(isselfhosted_, other->isselfhosted_); + std::swap(SourceOrRef_, other->SourceOrRef_); + std::swap(_oneof_case_[0], other->_oneof_case_[0]); + std::swap(FunctionDisplayNameOrRef_, other->FunctionDisplayNameOrRef_); + std::swap(_oneof_case_[1], other->_oneof_case_[1]); + std::swap(_has_bits_[0], other->_has_bits_[0]); + _unknown_fields_.Swap(&other->_unknown_fields_); + std::swap(_cached_size_, other->_cached_size_); + } +} + +::google::protobuf::Metadata StackFrame_Data::GetMetadata() const { + protobuf_AssignDescriptorsOnce(); + ::google::protobuf::Metadata metadata; + metadata.descriptor = StackFrame_Data_descriptor_; + metadata.reflection = StackFrame_Data_reflection_; + return metadata; +} + + +// ------------------------------------------------------------------- + +#ifndef _MSC_VER +const int StackFrame::kDataFieldNumber; +const int StackFrame::kRefFieldNumber; +#endif // !_MSC_VER + +StackFrame::StackFrame() + : ::google::protobuf::Message() { + SharedCtor(); + // @@protoc_insertion_point(constructor:mozilla.devtools.protobuf.StackFrame) +} + +void StackFrame::InitAsDefaultInstance() { + StackFrame_default_oneof_instance_->data_ = const_cast< ::mozilla::devtools::protobuf::StackFrame_Data*>(&::mozilla::devtools::protobuf::StackFrame_Data::default_instance()); + StackFrame_default_oneof_instance_->ref_ = GOOGLE_ULONGLONG(0); +} + +StackFrame::StackFrame(const StackFrame& from) + : ::google::protobuf::Message() { + SharedCtor(); + MergeFrom(from); + // @@protoc_insertion_point(copy_constructor:mozilla.devtools.protobuf.StackFrame) +} + +void StackFrame::SharedCtor() { + _cached_size_ = 0; + ::memset(_has_bits_, 0, sizeof(_has_bits_)); + clear_has_StackFrameType(); +} + +StackFrame::~StackFrame() { + // @@protoc_insertion_point(destructor:mozilla.devtools.protobuf.StackFrame) + SharedDtor(); +} + +void StackFrame::SharedDtor() { + if (has_StackFrameType()) { + clear_StackFrameType(); + } + if (this != default_instance_) { + } +} + +void StackFrame::SetCachedSize(int size) const { + GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN(); + _cached_size_ = size; + GOOGLE_SAFE_CONCURRENT_WRITES_END(); +} +const ::google::protobuf::Descriptor* StackFrame::descriptor() { + protobuf_AssignDescriptorsOnce(); + return StackFrame_descriptor_; +} + +const StackFrame& StackFrame::default_instance() { + if (default_instance_ == NULL) protobuf_AddDesc_CoreDump_2eproto(); + return *default_instance_; +} + +StackFrame* StackFrame::default_instance_ = NULL; + +StackFrame* StackFrame::New() const { + return new StackFrame; +} + +void StackFrame::clear_StackFrameType() { + switch(StackFrameType_case()) { + case kData: { + delete StackFrameType_.data_; + break; + } + case kRef: { + // No need to clear + break; + } + case STACKFRAMETYPE_NOT_SET: { + break; + } + } + _oneof_case_[0] = STACKFRAMETYPE_NOT_SET; +} + + +void StackFrame::Clear() { + clear_StackFrameType(); + ::memset(_has_bits_, 0, sizeof(_has_bits_)); + mutable_unknown_fields()->Clear(); +} + +bool StackFrame::MergePartialFromCodedStream( + ::google::protobuf::io::CodedInputStream* input) { +#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure + ::google::protobuf::uint32 tag; + // @@protoc_insertion_point(parse_start:mozilla.devtools.protobuf.StackFrame) + for (;;) { + ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127); + tag = p.first; + if (!p.second) goto handle_unusual; + switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) { + // optional .mozilla.devtools.protobuf.StackFrame.Data data = 1; + case 1: { + if (tag == 10) { + DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual( + input, mutable_data())); + } else { + goto handle_unusual; + } + if (input->ExpectTag(16)) goto parse_ref; + break; + } + + // optional uint64 ref = 2; + case 2: { + if (tag == 16) { + parse_ref: + clear_StackFrameType(); + DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive< + ::google::protobuf::uint64, ::google::protobuf::internal::WireFormatLite::TYPE_UINT64>( + input, &StackFrameType_.ref_))); + set_has_ref(); + } else { + goto handle_unusual; + } + if (input->ExpectAtEnd()) goto success; + break; + } + + default: { + handle_unusual: + if (tag == 0 || + ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) == + ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) { + goto success; + } + DO_(::google::protobuf::internal::WireFormat::SkipField( + input, tag, mutable_unknown_fields())); + break; + } + } + } +success: + // @@protoc_insertion_point(parse_success:mozilla.devtools.protobuf.StackFrame) + return true; +failure: + // @@protoc_insertion_point(parse_failure:mozilla.devtools.protobuf.StackFrame) + return false; +#undef DO_ +} + +void StackFrame::SerializeWithCachedSizes( + ::google::protobuf::io::CodedOutputStream* output) const { + // @@protoc_insertion_point(serialize_start:mozilla.devtools.protobuf.StackFrame) + // optional .mozilla.devtools.protobuf.StackFrame.Data data = 1; + if (has_data()) { + ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray( + 1, this->data(), output); + } + + // optional uint64 ref = 2; + if (has_ref()) { + ::google::protobuf::internal::WireFormatLite::WriteUInt64(2, this->ref(), output); + } + + if (!unknown_fields().empty()) { + ::google::protobuf::internal::WireFormat::SerializeUnknownFields( + unknown_fields(), output); + } + // @@protoc_insertion_point(serialize_end:mozilla.devtools.protobuf.StackFrame) +} + +::google::protobuf::uint8* StackFrame::SerializeWithCachedSizesToArray( + ::google::protobuf::uint8* target) const { + // @@protoc_insertion_point(serialize_to_array_start:mozilla.devtools.protobuf.StackFrame) + // optional .mozilla.devtools.protobuf.StackFrame.Data data = 1; + if (has_data()) { + target = ::google::protobuf::internal::WireFormatLite:: + WriteMessageNoVirtualToArray( + 1, this->data(), target); + } + + // optional uint64 ref = 2; + if (has_ref()) { + target = ::google::protobuf::internal::WireFormatLite::WriteUInt64ToArray(2, this->ref(), target); + } + + if (!unknown_fields().empty()) { + target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray( + unknown_fields(), target); + } + // @@protoc_insertion_point(serialize_to_array_end:mozilla.devtools.protobuf.StackFrame) + return target; +} + +int StackFrame::ByteSize() const { + int total_size = 0; + + switch (StackFrameType_case()) { + // optional .mozilla.devtools.protobuf.StackFrame.Data data = 1; + case kData: { + total_size += 1 + + ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual( + this->data()); + break; + } + // optional uint64 ref = 2; + case kRef: { + total_size += 1 + + ::google::protobuf::internal::WireFormatLite::UInt64Size( + this->ref()); + break; + } + case STACKFRAMETYPE_NOT_SET: { + break; + } + } + if (!unknown_fields().empty()) { + total_size += + ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize( + unknown_fields()); + } + GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN(); + _cached_size_ = total_size; + GOOGLE_SAFE_CONCURRENT_WRITES_END(); + return total_size; +} + +void StackFrame::MergeFrom(const ::google::protobuf::Message& from) { + GOOGLE_CHECK_NE(&from, this); + const StackFrame* source = + ::google::protobuf::internal::dynamic_cast_if_available<const StackFrame*>( + &from); + if (source == NULL) { + ::google::protobuf::internal::ReflectionOps::Merge(from, this); + } else { + MergeFrom(*source); + } +} + +void StackFrame::MergeFrom(const StackFrame& from) { + GOOGLE_CHECK_NE(&from, this); + switch (from.StackFrameType_case()) { + case kData: { + mutable_data()->::mozilla::devtools::protobuf::StackFrame_Data::MergeFrom(from.data()); + break; + } + case kRef: { + set_ref(from.ref()); + break; + } + case STACKFRAMETYPE_NOT_SET: { + break; + } + } + mutable_unknown_fields()->MergeFrom(from.unknown_fields()); +} + +void StackFrame::CopyFrom(const ::google::protobuf::Message& from) { + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +void StackFrame::CopyFrom(const StackFrame& from) { + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool StackFrame::IsInitialized() const { + + return true; +} + +void StackFrame::Swap(StackFrame* other) { + if (other != this) { + std::swap(StackFrameType_, other->StackFrameType_); + std::swap(_oneof_case_[0], other->_oneof_case_[0]); + std::swap(_has_bits_[0], other->_has_bits_[0]); + _unknown_fields_.Swap(&other->_unknown_fields_); + std::swap(_cached_size_, other->_cached_size_); + } +} + +::google::protobuf::Metadata StackFrame::GetMetadata() const { + protobuf_AssignDescriptorsOnce(); + ::google::protobuf::Metadata metadata; + metadata.descriptor = StackFrame_descriptor_; + metadata.reflection = StackFrame_reflection_; + return metadata; +} + + +// =================================================================== + +#ifndef _MSC_VER +const int Node::kIdFieldNumber; +const int Node::kTypeNameFieldNumber; +const int Node::kTypeNameRefFieldNumber; +const int Node::kSizeFieldNumber; +const int Node::kEdgesFieldNumber; +const int Node::kAllocationStackFieldNumber; +const int Node::kJsObjectClassNameFieldNumber; +const int Node::kJsObjectClassNameRefFieldNumber; +const int Node::kCoarseTypeFieldNumber; +const int Node::kScriptFilenameFieldNumber; +const int Node::kScriptFilenameRefFieldNumber; +#endif // !_MSC_VER + +Node::Node() + : ::google::protobuf::Message() { + SharedCtor(); + // @@protoc_insertion_point(constructor:mozilla.devtools.protobuf.Node) +} + +void Node::InitAsDefaultInstance() { + Node_default_oneof_instance_->typename__ = &::google::protobuf::internal::GetEmptyStringAlreadyInited(); + Node_default_oneof_instance_->typenameref_ = GOOGLE_ULONGLONG(0); + allocationstack_ = const_cast< ::mozilla::devtools::protobuf::StackFrame*>(&::mozilla::devtools::protobuf::StackFrame::default_instance()); + Node_default_oneof_instance_->jsobjectclassname_ = &::google::protobuf::internal::GetEmptyStringAlreadyInited(); + Node_default_oneof_instance_->jsobjectclassnameref_ = GOOGLE_ULONGLONG(0); + Node_default_oneof_instance_->scriptfilename_ = &::google::protobuf::internal::GetEmptyStringAlreadyInited(); + Node_default_oneof_instance_->scriptfilenameref_ = GOOGLE_ULONGLONG(0); +} + +Node::Node(const Node& from) + : ::google::protobuf::Message() { + SharedCtor(); + MergeFrom(from); + // @@protoc_insertion_point(copy_constructor:mozilla.devtools.protobuf.Node) +} + +void Node::SharedCtor() { + ::google::protobuf::internal::GetEmptyString(); + _cached_size_ = 0; + id_ = GOOGLE_ULONGLONG(0); + size_ = GOOGLE_ULONGLONG(0); + allocationstack_ = NULL; + coarsetype_ = 0u; + ::memset(_has_bits_, 0, sizeof(_has_bits_)); + clear_has_TypeNameOrRef(); + clear_has_JSObjectClassNameOrRef(); + clear_has_ScriptFilenameOrRef(); +} + +Node::~Node() { + // @@protoc_insertion_point(destructor:mozilla.devtools.protobuf.Node) + SharedDtor(); +} + +void Node::SharedDtor() { + if (has_TypeNameOrRef()) { + clear_TypeNameOrRef(); + } + if (has_JSObjectClassNameOrRef()) { + clear_JSObjectClassNameOrRef(); + } + if (has_ScriptFilenameOrRef()) { + clear_ScriptFilenameOrRef(); + } + if (this != default_instance_) { + delete allocationstack_; + } +} + +void Node::SetCachedSize(int size) const { + GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN(); + _cached_size_ = size; + GOOGLE_SAFE_CONCURRENT_WRITES_END(); +} +const ::google::protobuf::Descriptor* Node::descriptor() { + protobuf_AssignDescriptorsOnce(); + return Node_descriptor_; +} + +const Node& Node::default_instance() { + if (default_instance_ == NULL) protobuf_AddDesc_CoreDump_2eproto(); + return *default_instance_; +} + +Node* Node::default_instance_ = NULL; + +Node* Node::New() const { + return new Node; +} + +void Node::clear_TypeNameOrRef() { + switch(TypeNameOrRef_case()) { + case kTypeName: { + delete TypeNameOrRef_.typename__; + break; + } + case kTypeNameRef: { + // No need to clear + break; + } + case TYPENAMEORREF_NOT_SET: { + break; + } + } + _oneof_case_[0] = TYPENAMEORREF_NOT_SET; +} + +void Node::clear_JSObjectClassNameOrRef() { + switch(JSObjectClassNameOrRef_case()) { + case kJsObjectClassName: { + delete JSObjectClassNameOrRef_.jsobjectclassname_; + break; + } + case kJsObjectClassNameRef: { + // No need to clear + break; + } + case JSOBJECTCLASSNAMEORREF_NOT_SET: { + break; + } + } + _oneof_case_[1] = JSOBJECTCLASSNAMEORREF_NOT_SET; +} + +void Node::clear_ScriptFilenameOrRef() { + switch(ScriptFilenameOrRef_case()) { + case kScriptFilename: { + delete ScriptFilenameOrRef_.scriptfilename_; + break; + } + case kScriptFilenameRef: { + // No need to clear + break; + } + case SCRIPTFILENAMEORREF_NOT_SET: { + break; + } + } + _oneof_case_[2] = SCRIPTFILENAMEORREF_NOT_SET; +} + + +void Node::Clear() { +#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>( \ + &reinterpret_cast<Node*>(16)->f) - \ + reinterpret_cast<char*>(16)) + +#define ZR_(first, last) do { \ + size_t f = OFFSET_OF_FIELD_(first); \ + size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last); \ + ::memset(&first, 0, n); \ + } while (0) + + if (_has_bits_[0 / 32] & 41) { + ZR_(id_, size_); + if (has_allocationstack()) { + if (allocationstack_ != NULL) allocationstack_->::mozilla::devtools::protobuf::StackFrame::Clear(); + } + } + coarsetype_ = 0u; + +#undef OFFSET_OF_FIELD_ +#undef ZR_ + + edges_.Clear(); + clear_TypeNameOrRef(); + clear_JSObjectClassNameOrRef(); + clear_ScriptFilenameOrRef(); + ::memset(_has_bits_, 0, sizeof(_has_bits_)); + mutable_unknown_fields()->Clear(); +} + +bool Node::MergePartialFromCodedStream( + ::google::protobuf::io::CodedInputStream* input) { +#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure + ::google::protobuf::uint32 tag; + // @@protoc_insertion_point(parse_start:mozilla.devtools.protobuf.Node) + for (;;) { + ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127); + tag = p.first; + if (!p.second) goto handle_unusual; + switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) { + // optional uint64 id = 1; + case 1: { + if (tag == 8) { + DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive< + ::google::protobuf::uint64, ::google::protobuf::internal::WireFormatLite::TYPE_UINT64>( + input, &id_))); + set_has_id(); + } else { + goto handle_unusual; + } + if (input->ExpectTag(18)) goto parse_typeName; + break; + } + + // optional bytes typeName = 2; + case 2: { + if (tag == 18) { + parse_typeName: + DO_(::google::protobuf::internal::WireFormatLite::ReadBytes( + input, this->mutable_typename_())); + } else { + goto handle_unusual; + } + if (input->ExpectTag(24)) goto parse_typeNameRef; + break; + } + + // optional uint64 typeNameRef = 3; + case 3: { + if (tag == 24) { + parse_typeNameRef: + clear_TypeNameOrRef(); + DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive< + ::google::protobuf::uint64, ::google::protobuf::internal::WireFormatLite::TYPE_UINT64>( + input, &TypeNameOrRef_.typenameref_))); + set_has_typenameref(); + } else { + goto handle_unusual; + } + if (input->ExpectTag(32)) goto parse_size; + break; + } + + // optional uint64 size = 4; + case 4: { + if (tag == 32) { + parse_size: + DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive< + ::google::protobuf::uint64, ::google::protobuf::internal::WireFormatLite::TYPE_UINT64>( + input, &size_))); + set_has_size(); + } else { + goto handle_unusual; + } + if (input->ExpectTag(42)) goto parse_edges; + break; + } + + // repeated .mozilla.devtools.protobuf.Edge edges = 5; + case 5: { + if (tag == 42) { + parse_edges: + DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual( + input, add_edges())); + } else { + goto handle_unusual; + } + if (input->ExpectTag(42)) goto parse_edges; + if (input->ExpectTag(50)) goto parse_allocationStack; + break; + } + + // optional .mozilla.devtools.protobuf.StackFrame allocationStack = 6; + case 6: { + if (tag == 50) { + parse_allocationStack: + DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual( + input, mutable_allocationstack())); + } else { + goto handle_unusual; + } + if (input->ExpectTag(58)) goto parse_jsObjectClassName; + break; + } + + // optional bytes jsObjectClassName = 7; + case 7: { + if (tag == 58) { + parse_jsObjectClassName: + DO_(::google::protobuf::internal::WireFormatLite::ReadBytes( + input, this->mutable_jsobjectclassname())); + } else { + goto handle_unusual; + } + if (input->ExpectTag(64)) goto parse_jsObjectClassNameRef; + break; + } + + // optional uint64 jsObjectClassNameRef = 8; + case 8: { + if (tag == 64) { + parse_jsObjectClassNameRef: + clear_JSObjectClassNameOrRef(); + DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive< + ::google::protobuf::uint64, ::google::protobuf::internal::WireFormatLite::TYPE_UINT64>( + input, &JSObjectClassNameOrRef_.jsobjectclassnameref_))); + set_has_jsobjectclassnameref(); + } else { + goto handle_unusual; + } + if (input->ExpectTag(72)) goto parse_coarseType; + break; + } + + // optional uint32 coarseType = 9 [default = 0]; + case 9: { + if (tag == 72) { + parse_coarseType: + DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive< + ::google::protobuf::uint32, ::google::protobuf::internal::WireFormatLite::TYPE_UINT32>( + input, &coarsetype_))); + set_has_coarsetype(); + } else { + goto handle_unusual; + } + if (input->ExpectTag(82)) goto parse_scriptFilename; + break; + } + + // optional bytes scriptFilename = 10; + case 10: { + if (tag == 82) { + parse_scriptFilename: + DO_(::google::protobuf::internal::WireFormatLite::ReadBytes( + input, this->mutable_scriptfilename())); + } else { + goto handle_unusual; + } + if (input->ExpectTag(88)) goto parse_scriptFilenameRef; + break; + } + + // optional uint64 scriptFilenameRef = 11; + case 11: { + if (tag == 88) { + parse_scriptFilenameRef: + clear_ScriptFilenameOrRef(); + DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive< + ::google::protobuf::uint64, ::google::protobuf::internal::WireFormatLite::TYPE_UINT64>( + input, &ScriptFilenameOrRef_.scriptfilenameref_))); + set_has_scriptfilenameref(); + } else { + goto handle_unusual; + } + if (input->ExpectAtEnd()) goto success; + break; + } + + default: { + handle_unusual: + if (tag == 0 || + ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) == + ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) { + goto success; + } + DO_(::google::protobuf::internal::WireFormat::SkipField( + input, tag, mutable_unknown_fields())); + break; + } + } + } +success: + // @@protoc_insertion_point(parse_success:mozilla.devtools.protobuf.Node) + return true; +failure: + // @@protoc_insertion_point(parse_failure:mozilla.devtools.protobuf.Node) + return false; +#undef DO_ +} + +void Node::SerializeWithCachedSizes( + ::google::protobuf::io::CodedOutputStream* output) const { + // @@protoc_insertion_point(serialize_start:mozilla.devtools.protobuf.Node) + // optional uint64 id = 1; + if (has_id()) { + ::google::protobuf::internal::WireFormatLite::WriteUInt64(1, this->id(), output); + } + + // optional bytes typeName = 2; + if (has_typename_()) { + ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased( + 2, this->typename_(), output); + } + + // optional uint64 typeNameRef = 3; + if (has_typenameref()) { + ::google::protobuf::internal::WireFormatLite::WriteUInt64(3, this->typenameref(), output); + } + + // optional uint64 size = 4; + if (has_size()) { + ::google::protobuf::internal::WireFormatLite::WriteUInt64(4, this->size(), output); + } + + // repeated .mozilla.devtools.protobuf.Edge edges = 5; + for (int i = 0; i < this->edges_size(); i++) { + ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray( + 5, this->edges(i), output); + } + + // optional .mozilla.devtools.protobuf.StackFrame allocationStack = 6; + if (has_allocationstack()) { + ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray( + 6, this->allocationstack(), output); + } + + // optional bytes jsObjectClassName = 7; + if (has_jsobjectclassname()) { + ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased( + 7, this->jsobjectclassname(), output); + } + + // optional uint64 jsObjectClassNameRef = 8; + if (has_jsobjectclassnameref()) { + ::google::protobuf::internal::WireFormatLite::WriteUInt64(8, this->jsobjectclassnameref(), output); + } + + // optional uint32 coarseType = 9 [default = 0]; + if (has_coarsetype()) { + ::google::protobuf::internal::WireFormatLite::WriteUInt32(9, this->coarsetype(), output); + } + + // optional bytes scriptFilename = 10; + if (has_scriptfilename()) { + ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased( + 10, this->scriptfilename(), output); + } + + // optional uint64 scriptFilenameRef = 11; + if (has_scriptfilenameref()) { + ::google::protobuf::internal::WireFormatLite::WriteUInt64(11, this->scriptfilenameref(), output); + } + + if (!unknown_fields().empty()) { + ::google::protobuf::internal::WireFormat::SerializeUnknownFields( + unknown_fields(), output); + } + // @@protoc_insertion_point(serialize_end:mozilla.devtools.protobuf.Node) +} + +::google::protobuf::uint8* Node::SerializeWithCachedSizesToArray( + ::google::protobuf::uint8* target) const { + // @@protoc_insertion_point(serialize_to_array_start:mozilla.devtools.protobuf.Node) + // optional uint64 id = 1; + if (has_id()) { + target = ::google::protobuf::internal::WireFormatLite::WriteUInt64ToArray(1, this->id(), target); + } + + // optional bytes typeName = 2; + if (has_typename_()) { + target = + ::google::protobuf::internal::WireFormatLite::WriteBytesToArray( + 2, this->typename_(), target); + } + + // optional uint64 typeNameRef = 3; + if (has_typenameref()) { + target = ::google::protobuf::internal::WireFormatLite::WriteUInt64ToArray(3, this->typenameref(), target); + } + + // optional uint64 size = 4; + if (has_size()) { + target = ::google::protobuf::internal::WireFormatLite::WriteUInt64ToArray(4, this->size(), target); + } + + // repeated .mozilla.devtools.protobuf.Edge edges = 5; + for (int i = 0; i < this->edges_size(); i++) { + target = ::google::protobuf::internal::WireFormatLite:: + WriteMessageNoVirtualToArray( + 5, this->edges(i), target); + } + + // optional .mozilla.devtools.protobuf.StackFrame allocationStack = 6; + if (has_allocationstack()) { + target = ::google::protobuf::internal::WireFormatLite:: + WriteMessageNoVirtualToArray( + 6, this->allocationstack(), target); + } + + // optional bytes jsObjectClassName = 7; + if (has_jsobjectclassname()) { + target = + ::google::protobuf::internal::WireFormatLite::WriteBytesToArray( + 7, this->jsobjectclassname(), target); + } + + // optional uint64 jsObjectClassNameRef = 8; + if (has_jsobjectclassnameref()) { + target = ::google::protobuf::internal::WireFormatLite::WriteUInt64ToArray(8, this->jsobjectclassnameref(), target); + } + + // optional uint32 coarseType = 9 [default = 0]; + if (has_coarsetype()) { + target = ::google::protobuf::internal::WireFormatLite::WriteUInt32ToArray(9, this->coarsetype(), target); + } + + // optional bytes scriptFilename = 10; + if (has_scriptfilename()) { + target = + ::google::protobuf::internal::WireFormatLite::WriteBytesToArray( + 10, this->scriptfilename(), target); + } + + // optional uint64 scriptFilenameRef = 11; + if (has_scriptfilenameref()) { + target = ::google::protobuf::internal::WireFormatLite::WriteUInt64ToArray(11, this->scriptfilenameref(), target); + } + + if (!unknown_fields().empty()) { + target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray( + unknown_fields(), target); + } + // @@protoc_insertion_point(serialize_to_array_end:mozilla.devtools.protobuf.Node) + return target; +} + +int Node::ByteSize() const { + int total_size = 0; + + if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) { + // optional uint64 id = 1; + if (has_id()) { + total_size += 1 + + ::google::protobuf::internal::WireFormatLite::UInt64Size( + this->id()); + } + + // optional uint64 size = 4; + if (has_size()) { + total_size += 1 + + ::google::protobuf::internal::WireFormatLite::UInt64Size( + this->size()); + } + + // optional .mozilla.devtools.protobuf.StackFrame allocationStack = 6; + if (has_allocationstack()) { + total_size += 1 + + ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual( + this->allocationstack()); + } + + } + if (_has_bits_[8 / 32] & (0xffu << (8 % 32))) { + // optional uint32 coarseType = 9 [default = 0]; + if (has_coarsetype()) { + total_size += 1 + + ::google::protobuf::internal::WireFormatLite::UInt32Size( + this->coarsetype()); + } + + } + // repeated .mozilla.devtools.protobuf.Edge edges = 5; + total_size += 1 * this->edges_size(); + for (int i = 0; i < this->edges_size(); i++) { + total_size += + ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual( + this->edges(i)); + } + + switch (TypeNameOrRef_case()) { + // optional bytes typeName = 2; + case kTypeName: { + total_size += 1 + + ::google::protobuf::internal::WireFormatLite::BytesSize( + this->typename_()); + break; + } + // optional uint64 typeNameRef = 3; + case kTypeNameRef: { + total_size += 1 + + ::google::protobuf::internal::WireFormatLite::UInt64Size( + this->typenameref()); + break; + } + case TYPENAMEORREF_NOT_SET: { + break; + } + } + switch (JSObjectClassNameOrRef_case()) { + // optional bytes jsObjectClassName = 7; + case kJsObjectClassName: { + total_size += 1 + + ::google::protobuf::internal::WireFormatLite::BytesSize( + this->jsobjectclassname()); + break; + } + // optional uint64 jsObjectClassNameRef = 8; + case kJsObjectClassNameRef: { + total_size += 1 + + ::google::protobuf::internal::WireFormatLite::UInt64Size( + this->jsobjectclassnameref()); + break; + } + case JSOBJECTCLASSNAMEORREF_NOT_SET: { + break; + } + } + switch (ScriptFilenameOrRef_case()) { + // optional bytes scriptFilename = 10; + case kScriptFilename: { + total_size += 1 + + ::google::protobuf::internal::WireFormatLite::BytesSize( + this->scriptfilename()); + break; + } + // optional uint64 scriptFilenameRef = 11; + case kScriptFilenameRef: { + total_size += 1 + + ::google::protobuf::internal::WireFormatLite::UInt64Size( + this->scriptfilenameref()); + break; + } + case SCRIPTFILENAMEORREF_NOT_SET: { + break; + } + } + if (!unknown_fields().empty()) { + total_size += + ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize( + unknown_fields()); + } + GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN(); + _cached_size_ = total_size; + GOOGLE_SAFE_CONCURRENT_WRITES_END(); + return total_size; +} + +void Node::MergeFrom(const ::google::protobuf::Message& from) { + GOOGLE_CHECK_NE(&from, this); + const Node* source = + ::google::protobuf::internal::dynamic_cast_if_available<const Node*>( + &from); + if (source == NULL) { + ::google::protobuf::internal::ReflectionOps::Merge(from, this); + } else { + MergeFrom(*source); + } +} + +void Node::MergeFrom(const Node& from) { + GOOGLE_CHECK_NE(&from, this); + edges_.MergeFrom(from.edges_); + switch (from.TypeNameOrRef_case()) { + case kTypeName: { + set_typename_(from.typename_()); + break; + } + case kTypeNameRef: { + set_typenameref(from.typenameref()); + break; + } + case TYPENAMEORREF_NOT_SET: { + break; + } + } + switch (from.JSObjectClassNameOrRef_case()) { + case kJsObjectClassName: { + set_jsobjectclassname(from.jsobjectclassname()); + break; + } + case kJsObjectClassNameRef: { + set_jsobjectclassnameref(from.jsobjectclassnameref()); + break; + } + case JSOBJECTCLASSNAMEORREF_NOT_SET: { + break; + } + } + switch (from.ScriptFilenameOrRef_case()) { + case kScriptFilename: { + set_scriptfilename(from.scriptfilename()); + break; + } + case kScriptFilenameRef: { + set_scriptfilenameref(from.scriptfilenameref()); + break; + } + case SCRIPTFILENAMEORREF_NOT_SET: { + break; + } + } + if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) { + if (from.has_id()) { + set_id(from.id()); + } + if (from.has_size()) { + set_size(from.size()); + } + if (from.has_allocationstack()) { + mutable_allocationstack()->::mozilla::devtools::protobuf::StackFrame::MergeFrom(from.allocationstack()); + } + } + if (from._has_bits_[8 / 32] & (0xffu << (8 % 32))) { + if (from.has_coarsetype()) { + set_coarsetype(from.coarsetype()); + } + } + mutable_unknown_fields()->MergeFrom(from.unknown_fields()); +} + +void Node::CopyFrom(const ::google::protobuf::Message& from) { + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +void Node::CopyFrom(const Node& from) { + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool Node::IsInitialized() const { + + return true; +} + +void Node::Swap(Node* other) { + if (other != this) { + std::swap(id_, other->id_); + std::swap(size_, other->size_); + edges_.Swap(&other->edges_); + std::swap(allocationstack_, other->allocationstack_); + std::swap(coarsetype_, other->coarsetype_); + std::swap(TypeNameOrRef_, other->TypeNameOrRef_); + std::swap(_oneof_case_[0], other->_oneof_case_[0]); + std::swap(JSObjectClassNameOrRef_, other->JSObjectClassNameOrRef_); + std::swap(_oneof_case_[1], other->_oneof_case_[1]); + std::swap(ScriptFilenameOrRef_, other->ScriptFilenameOrRef_); + std::swap(_oneof_case_[2], other->_oneof_case_[2]); + std::swap(_has_bits_[0], other->_has_bits_[0]); + _unknown_fields_.Swap(&other->_unknown_fields_); + std::swap(_cached_size_, other->_cached_size_); + } +} + +::google::protobuf::Metadata Node::GetMetadata() const { + protobuf_AssignDescriptorsOnce(); + ::google::protobuf::Metadata metadata; + metadata.descriptor = Node_descriptor_; + metadata.reflection = Node_reflection_; + return metadata; +} + + +// =================================================================== + +#ifndef _MSC_VER +const int Edge::kReferentFieldNumber; +const int Edge::kNameFieldNumber; +const int Edge::kNameRefFieldNumber; +#endif // !_MSC_VER + +Edge::Edge() + : ::google::protobuf::Message() { + SharedCtor(); + // @@protoc_insertion_point(constructor:mozilla.devtools.protobuf.Edge) +} + +void Edge::InitAsDefaultInstance() { + Edge_default_oneof_instance_->name_ = &::google::protobuf::internal::GetEmptyStringAlreadyInited(); + Edge_default_oneof_instance_->nameref_ = GOOGLE_ULONGLONG(0); +} + +Edge::Edge(const Edge& from) + : ::google::protobuf::Message() { + SharedCtor(); + MergeFrom(from); + // @@protoc_insertion_point(copy_constructor:mozilla.devtools.protobuf.Edge) +} + +void Edge::SharedCtor() { + ::google::protobuf::internal::GetEmptyString(); + _cached_size_ = 0; + referent_ = GOOGLE_ULONGLONG(0); + ::memset(_has_bits_, 0, sizeof(_has_bits_)); + clear_has_EdgeNameOrRef(); +} + +Edge::~Edge() { + // @@protoc_insertion_point(destructor:mozilla.devtools.protobuf.Edge) + SharedDtor(); +} + +void Edge::SharedDtor() { + if (has_EdgeNameOrRef()) { + clear_EdgeNameOrRef(); + } + if (this != default_instance_) { + } +} + +void Edge::SetCachedSize(int size) const { + GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN(); + _cached_size_ = size; + GOOGLE_SAFE_CONCURRENT_WRITES_END(); +} +const ::google::protobuf::Descriptor* Edge::descriptor() { + protobuf_AssignDescriptorsOnce(); + return Edge_descriptor_; +} + +const Edge& Edge::default_instance() { + if (default_instance_ == NULL) protobuf_AddDesc_CoreDump_2eproto(); + return *default_instance_; +} + +Edge* Edge::default_instance_ = NULL; + +Edge* Edge::New() const { + return new Edge; +} + +void Edge::clear_EdgeNameOrRef() { + switch(EdgeNameOrRef_case()) { + case kName: { + delete EdgeNameOrRef_.name_; + break; + } + case kNameRef: { + // No need to clear + break; + } + case EDGENAMEORREF_NOT_SET: { + break; + } + } + _oneof_case_[0] = EDGENAMEORREF_NOT_SET; +} + + +void Edge::Clear() { + referent_ = GOOGLE_ULONGLONG(0); + clear_EdgeNameOrRef(); + ::memset(_has_bits_, 0, sizeof(_has_bits_)); + mutable_unknown_fields()->Clear(); +} + +bool Edge::MergePartialFromCodedStream( + ::google::protobuf::io::CodedInputStream* input) { +#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure + ::google::protobuf::uint32 tag; + // @@protoc_insertion_point(parse_start:mozilla.devtools.protobuf.Edge) + for (;;) { + ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127); + tag = p.first; + if (!p.second) goto handle_unusual; + switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) { + // optional uint64 referent = 1; + case 1: { + if (tag == 8) { + DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive< + ::google::protobuf::uint64, ::google::protobuf::internal::WireFormatLite::TYPE_UINT64>( + input, &referent_))); + set_has_referent(); + } else { + goto handle_unusual; + } + if (input->ExpectTag(18)) goto parse_name; + break; + } + + // optional bytes name = 2; + case 2: { + if (tag == 18) { + parse_name: + DO_(::google::protobuf::internal::WireFormatLite::ReadBytes( + input, this->mutable_name())); + } else { + goto handle_unusual; + } + if (input->ExpectTag(24)) goto parse_nameRef; + break; + } + + // optional uint64 nameRef = 3; + case 3: { + if (tag == 24) { + parse_nameRef: + clear_EdgeNameOrRef(); + DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive< + ::google::protobuf::uint64, ::google::protobuf::internal::WireFormatLite::TYPE_UINT64>( + input, &EdgeNameOrRef_.nameref_))); + set_has_nameref(); + } else { + goto handle_unusual; + } + if (input->ExpectAtEnd()) goto success; + break; + } + + default: { + handle_unusual: + if (tag == 0 || + ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) == + ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) { + goto success; + } + DO_(::google::protobuf::internal::WireFormat::SkipField( + input, tag, mutable_unknown_fields())); + break; + } + } + } +success: + // @@protoc_insertion_point(parse_success:mozilla.devtools.protobuf.Edge) + return true; +failure: + // @@protoc_insertion_point(parse_failure:mozilla.devtools.protobuf.Edge) + return false; +#undef DO_ +} + +void Edge::SerializeWithCachedSizes( + ::google::protobuf::io::CodedOutputStream* output) const { + // @@protoc_insertion_point(serialize_start:mozilla.devtools.protobuf.Edge) + // optional uint64 referent = 1; + if (has_referent()) { + ::google::protobuf::internal::WireFormatLite::WriteUInt64(1, this->referent(), output); + } + + // optional bytes name = 2; + if (has_name()) { + ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased( + 2, this->name(), output); + } + + // optional uint64 nameRef = 3; + if (has_nameref()) { + ::google::protobuf::internal::WireFormatLite::WriteUInt64(3, this->nameref(), output); + } + + if (!unknown_fields().empty()) { + ::google::protobuf::internal::WireFormat::SerializeUnknownFields( + unknown_fields(), output); + } + // @@protoc_insertion_point(serialize_end:mozilla.devtools.protobuf.Edge) +} + +::google::protobuf::uint8* Edge::SerializeWithCachedSizesToArray( + ::google::protobuf::uint8* target) const { + // @@protoc_insertion_point(serialize_to_array_start:mozilla.devtools.protobuf.Edge) + // optional uint64 referent = 1; + if (has_referent()) { + target = ::google::protobuf::internal::WireFormatLite::WriteUInt64ToArray(1, this->referent(), target); + } + + // optional bytes name = 2; + if (has_name()) { + target = + ::google::protobuf::internal::WireFormatLite::WriteBytesToArray( + 2, this->name(), target); + } + + // optional uint64 nameRef = 3; + if (has_nameref()) { + target = ::google::protobuf::internal::WireFormatLite::WriteUInt64ToArray(3, this->nameref(), target); + } + + if (!unknown_fields().empty()) { + target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray( + unknown_fields(), target); + } + // @@protoc_insertion_point(serialize_to_array_end:mozilla.devtools.protobuf.Edge) + return target; +} + +int Edge::ByteSize() const { + int total_size = 0; + + if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) { + // optional uint64 referent = 1; + if (has_referent()) { + total_size += 1 + + ::google::protobuf::internal::WireFormatLite::UInt64Size( + this->referent()); + } + + } + switch (EdgeNameOrRef_case()) { + // optional bytes name = 2; + case kName: { + total_size += 1 + + ::google::protobuf::internal::WireFormatLite::BytesSize( + this->name()); + break; + } + // optional uint64 nameRef = 3; + case kNameRef: { + total_size += 1 + + ::google::protobuf::internal::WireFormatLite::UInt64Size( + this->nameref()); + break; + } + case EDGENAMEORREF_NOT_SET: { + break; + } + } + if (!unknown_fields().empty()) { + total_size += + ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize( + unknown_fields()); + } + GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN(); + _cached_size_ = total_size; + GOOGLE_SAFE_CONCURRENT_WRITES_END(); + return total_size; +} + +void Edge::MergeFrom(const ::google::protobuf::Message& from) { + GOOGLE_CHECK_NE(&from, this); + const Edge* source = + ::google::protobuf::internal::dynamic_cast_if_available<const Edge*>( + &from); + if (source == NULL) { + ::google::protobuf::internal::ReflectionOps::Merge(from, this); + } else { + MergeFrom(*source); + } +} + +void Edge::MergeFrom(const Edge& from) { + GOOGLE_CHECK_NE(&from, this); + switch (from.EdgeNameOrRef_case()) { + case kName: { + set_name(from.name()); + break; + } + case kNameRef: { + set_nameref(from.nameref()); + break; + } + case EDGENAMEORREF_NOT_SET: { + break; + } + } + if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) { + if (from.has_referent()) { + set_referent(from.referent()); + } + } + mutable_unknown_fields()->MergeFrom(from.unknown_fields()); +} + +void Edge::CopyFrom(const ::google::protobuf::Message& from) { + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +void Edge::CopyFrom(const Edge& from) { + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool Edge::IsInitialized() const { + + return true; +} + +void Edge::Swap(Edge* other) { + if (other != this) { + std::swap(referent_, other->referent_); + std::swap(EdgeNameOrRef_, other->EdgeNameOrRef_); + std::swap(_oneof_case_[0], other->_oneof_case_[0]); + std::swap(_has_bits_[0], other->_has_bits_[0]); + _unknown_fields_.Swap(&other->_unknown_fields_); + std::swap(_cached_size_, other->_cached_size_); + } +} + +::google::protobuf::Metadata Edge::GetMetadata() const { + protobuf_AssignDescriptorsOnce(); + ::google::protobuf::Metadata metadata; + metadata.descriptor = Edge_descriptor_; + metadata.reflection = Edge_reflection_; + return metadata; +} + + +// @@protoc_insertion_point(namespace_scope) + +} // namespace protobuf +} // namespace devtools +} // namespace mozilla + +// @@protoc_insertion_point(global_scope) diff --git a/dom/heapsnapshot/CoreDump.pb.h b/dom/heapsnapshot/CoreDump.pb.h new file mode 100644 index 000000000..584c2e379 --- /dev/null +++ b/dom/heapsnapshot/CoreDump.pb.h @@ -0,0 +1,1893 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: CoreDump.proto + +#ifndef PROTOBUF_CoreDump_2eproto__INCLUDED +#define PROTOBUF_CoreDump_2eproto__INCLUDED + +#include <string> + +#include <google/protobuf/stubs/common.h> + +#if GOOGLE_PROTOBUF_VERSION < 2006000 +#error This file was generated by a newer version of protoc which is +#error incompatible with your Protocol Buffer headers. Please update +#error your headers. +#endif +#if 2006001 < GOOGLE_PROTOBUF_MIN_PROTOC_VERSION +#error This file was generated by an older version of protoc which is +#error incompatible with your Protocol Buffer headers. Please +#error regenerate this file with a newer version of protoc. +#endif + +#include <google/protobuf/generated_message_util.h> +#include <google/protobuf/message.h> +#include <google/protobuf/repeated_field.h> +#include <google/protobuf/extension_set.h> +#include <google/protobuf/unknown_field_set.h> +// @@protoc_insertion_point(includes) + +namespace mozilla { +namespace devtools { +namespace protobuf { + +// Internal implementation detail -- do not call these. +void protobuf_AddDesc_CoreDump_2eproto(); +void protobuf_AssignDesc_CoreDump_2eproto(); +void protobuf_ShutdownFile_CoreDump_2eproto(); + +class Metadata; +class StackFrame; +class StackFrame_Data; +class Node; +class Edge; + +// =================================================================== + +class Metadata : public ::google::protobuf::Message { + public: + Metadata(); + virtual ~Metadata(); + + Metadata(const Metadata& from); + + inline Metadata& operator=(const Metadata& from) { + CopyFrom(from); + return *this; + } + + inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const { + return _unknown_fields_; + } + + inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() { + return &_unknown_fields_; + } + + static const ::google::protobuf::Descriptor* descriptor(); + static const Metadata& default_instance(); + + void Swap(Metadata* other); + + // implements Message ---------------------------------------------- + + Metadata* New() const; + void CopyFrom(const ::google::protobuf::Message& from); + void MergeFrom(const ::google::protobuf::Message& from); + void CopyFrom(const Metadata& from); + void MergeFrom(const Metadata& from); + void Clear(); + bool IsInitialized() const; + + int ByteSize() const; + bool MergePartialFromCodedStream( + ::google::protobuf::io::CodedInputStream* input); + void SerializeWithCachedSizes( + ::google::protobuf::io::CodedOutputStream* output) const; + ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const; + int GetCachedSize() const { return _cached_size_; } + private: + void SharedCtor(); + void SharedDtor(); + void SetCachedSize(int size) const; + public: + ::google::protobuf::Metadata GetMetadata() const; + + // nested types ---------------------------------------------------- + + // accessors ------------------------------------------------------- + + // optional uint64 timeStamp = 1; + inline bool has_timestamp() const; + inline void clear_timestamp(); + static const int kTimeStampFieldNumber = 1; + inline ::google::protobuf::uint64 timestamp() const; + inline void set_timestamp(::google::protobuf::uint64 value); + + // @@protoc_insertion_point(class_scope:mozilla.devtools.protobuf.Metadata) + private: + inline void set_has_timestamp(); + inline void clear_has_timestamp(); + + ::google::protobuf::UnknownFieldSet _unknown_fields_; + + ::google::protobuf::uint32 _has_bits_[1]; + mutable int _cached_size_; + ::google::protobuf::uint64 timestamp_; + friend void protobuf_AddDesc_CoreDump_2eproto(); + friend void protobuf_AssignDesc_CoreDump_2eproto(); + friend void protobuf_ShutdownFile_CoreDump_2eproto(); + + void InitAsDefaultInstance(); + static Metadata* default_instance_; +}; +// ------------------------------------------------------------------- + +class StackFrame_Data : public ::google::protobuf::Message { + public: + StackFrame_Data(); + virtual ~StackFrame_Data(); + + StackFrame_Data(const StackFrame_Data& from); + + inline StackFrame_Data& operator=(const StackFrame_Data& from) { + CopyFrom(from); + return *this; + } + + inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const { + return _unknown_fields_; + } + + inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() { + return &_unknown_fields_; + } + + static const ::google::protobuf::Descriptor* descriptor(); + static const StackFrame_Data& default_instance(); + + enum SourceOrRefCase { + kSource = 5, + kSourceRef = 6, + SOURCEORREF_NOT_SET = 0, + }; + + enum FunctionDisplayNameOrRefCase { + kFunctionDisplayName = 7, + kFunctionDisplayNameRef = 8, + FUNCTIONDISPLAYNAMEORREF_NOT_SET = 0, + }; + + void Swap(StackFrame_Data* other); + + // implements Message ---------------------------------------------- + + StackFrame_Data* New() const; + void CopyFrom(const ::google::protobuf::Message& from); + void MergeFrom(const ::google::protobuf::Message& from); + void CopyFrom(const StackFrame_Data& from); + void MergeFrom(const StackFrame_Data& from); + void Clear(); + bool IsInitialized() const; + + int ByteSize() const; + bool MergePartialFromCodedStream( + ::google::protobuf::io::CodedInputStream* input); + void SerializeWithCachedSizes( + ::google::protobuf::io::CodedOutputStream* output) const; + ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const; + int GetCachedSize() const { return _cached_size_; } + private: + void SharedCtor(); + void SharedDtor(); + void SetCachedSize(int size) const; + public: + ::google::protobuf::Metadata GetMetadata() const; + + // nested types ---------------------------------------------------- + + // accessors ------------------------------------------------------- + + // optional uint64 id = 1; + inline bool has_id() const; + inline void clear_id(); + static const int kIdFieldNumber = 1; + inline ::google::protobuf::uint64 id() const; + inline void set_id(::google::protobuf::uint64 value); + + // optional .mozilla.devtools.protobuf.StackFrame parent = 2; + inline bool has_parent() const; + inline void clear_parent(); + static const int kParentFieldNumber = 2; + inline const ::mozilla::devtools::protobuf::StackFrame& parent() const; + inline ::mozilla::devtools::protobuf::StackFrame* mutable_parent(); + inline ::mozilla::devtools::protobuf::StackFrame* release_parent(); + inline void set_allocated_parent(::mozilla::devtools::protobuf::StackFrame* parent); + + // optional uint32 line = 3; + inline bool has_line() const; + inline void clear_line(); + static const int kLineFieldNumber = 3; + inline ::google::protobuf::uint32 line() const; + inline void set_line(::google::protobuf::uint32 value); + + // optional uint32 column = 4; + inline bool has_column() const; + inline void clear_column(); + static const int kColumnFieldNumber = 4; + inline ::google::protobuf::uint32 column() const; + inline void set_column(::google::protobuf::uint32 value); + + // optional bytes source = 5; + inline bool has_source() const; + inline void clear_source(); + static const int kSourceFieldNumber = 5; + inline const ::std::string& source() const; + inline void set_source(const ::std::string& value); + inline void set_source(const char* value); + inline void set_source(const void* value, size_t size); + inline ::std::string* mutable_source(); + inline ::std::string* release_source(); + inline void set_allocated_source(::std::string* source); + + // optional uint64 sourceRef = 6; + inline bool has_sourceref() const; + inline void clear_sourceref(); + static const int kSourceRefFieldNumber = 6; + inline ::google::protobuf::uint64 sourceref() const; + inline void set_sourceref(::google::protobuf::uint64 value); + + // optional bytes functionDisplayName = 7; + inline bool has_functiondisplayname() const; + inline void clear_functiondisplayname(); + static const int kFunctionDisplayNameFieldNumber = 7; + inline const ::std::string& functiondisplayname() const; + inline void set_functiondisplayname(const ::std::string& value); + inline void set_functiondisplayname(const char* value); + inline void set_functiondisplayname(const void* value, size_t size); + inline ::std::string* mutable_functiondisplayname(); + inline ::std::string* release_functiondisplayname(); + inline void set_allocated_functiondisplayname(::std::string* functiondisplayname); + + // optional uint64 functionDisplayNameRef = 8; + inline bool has_functiondisplaynameref() const; + inline void clear_functiondisplaynameref(); + static const int kFunctionDisplayNameRefFieldNumber = 8; + inline ::google::protobuf::uint64 functiondisplaynameref() const; + inline void set_functiondisplaynameref(::google::protobuf::uint64 value); + + // optional bool isSystem = 9; + inline bool has_issystem() const; + inline void clear_issystem(); + static const int kIsSystemFieldNumber = 9; + inline bool issystem() const; + inline void set_issystem(bool value); + + // optional bool isSelfHosted = 10; + inline bool has_isselfhosted() const; + inline void clear_isselfhosted(); + static const int kIsSelfHostedFieldNumber = 10; + inline bool isselfhosted() const; + inline void set_isselfhosted(bool value); + + inline SourceOrRefCase SourceOrRef_case() const; + inline FunctionDisplayNameOrRefCase FunctionDisplayNameOrRef_case() const; + // @@protoc_insertion_point(class_scope:mozilla.devtools.protobuf.StackFrame.Data) + private: + inline void set_has_id(); + inline void clear_has_id(); + inline void set_has_parent(); + inline void clear_has_parent(); + inline void set_has_line(); + inline void clear_has_line(); + inline void set_has_column(); + inline void clear_has_column(); + inline void set_has_source(); + inline void set_has_sourceref(); + inline void set_has_functiondisplayname(); + inline void set_has_functiondisplaynameref(); + inline void set_has_issystem(); + inline void clear_has_issystem(); + inline void set_has_isselfhosted(); + inline void clear_has_isselfhosted(); + + inline bool has_SourceOrRef(); + void clear_SourceOrRef(); + inline void clear_has_SourceOrRef(); + + inline bool has_FunctionDisplayNameOrRef(); + void clear_FunctionDisplayNameOrRef(); + inline void clear_has_FunctionDisplayNameOrRef(); + + ::google::protobuf::UnknownFieldSet _unknown_fields_; + + ::google::protobuf::uint32 _has_bits_[1]; + mutable int _cached_size_; + ::google::protobuf::uint64 id_; + ::mozilla::devtools::protobuf::StackFrame* parent_; + ::google::protobuf::uint32 line_; + ::google::protobuf::uint32 column_; + bool issystem_; + bool isselfhosted_; + union SourceOrRefUnion { + ::std::string* source_; + ::google::protobuf::uint64 sourceref_; + } SourceOrRef_; + union FunctionDisplayNameOrRefUnion { + ::std::string* functiondisplayname_; + ::google::protobuf::uint64 functiondisplaynameref_; + } FunctionDisplayNameOrRef_; + ::google::protobuf::uint32 _oneof_case_[2]; + + friend void protobuf_AddDesc_CoreDump_2eproto(); + friend void protobuf_AssignDesc_CoreDump_2eproto(); + friend void protobuf_ShutdownFile_CoreDump_2eproto(); + + void InitAsDefaultInstance(); + static StackFrame_Data* default_instance_; +}; +// ------------------------------------------------------------------- + +class StackFrame : public ::google::protobuf::Message { + public: + StackFrame(); + virtual ~StackFrame(); + + StackFrame(const StackFrame& from); + + inline StackFrame& operator=(const StackFrame& from) { + CopyFrom(from); + return *this; + } + + inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const { + return _unknown_fields_; + } + + inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() { + return &_unknown_fields_; + } + + static const ::google::protobuf::Descriptor* descriptor(); + static const StackFrame& default_instance(); + + enum StackFrameTypeCase { + kData = 1, + kRef = 2, + STACKFRAMETYPE_NOT_SET = 0, + }; + + void Swap(StackFrame* other); + + // implements Message ---------------------------------------------- + + StackFrame* New() const; + void CopyFrom(const ::google::protobuf::Message& from); + void MergeFrom(const ::google::protobuf::Message& from); + void CopyFrom(const StackFrame& from); + void MergeFrom(const StackFrame& from); + void Clear(); + bool IsInitialized() const; + + int ByteSize() const; + bool MergePartialFromCodedStream( + ::google::protobuf::io::CodedInputStream* input); + void SerializeWithCachedSizes( + ::google::protobuf::io::CodedOutputStream* output) const; + ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const; + int GetCachedSize() const { return _cached_size_; } + private: + void SharedCtor(); + void SharedDtor(); + void SetCachedSize(int size) const; + public: + ::google::protobuf::Metadata GetMetadata() const; + + // nested types ---------------------------------------------------- + + typedef StackFrame_Data Data; + + // accessors ------------------------------------------------------- + + // optional .mozilla.devtools.protobuf.StackFrame.Data data = 1; + inline bool has_data() const; + inline void clear_data(); + static const int kDataFieldNumber = 1; + inline const ::mozilla::devtools::protobuf::StackFrame_Data& data() const; + inline ::mozilla::devtools::protobuf::StackFrame_Data* mutable_data(); + inline ::mozilla::devtools::protobuf::StackFrame_Data* release_data(); + inline void set_allocated_data(::mozilla::devtools::protobuf::StackFrame_Data* data); + + // optional uint64 ref = 2; + inline bool has_ref() const; + inline void clear_ref(); + static const int kRefFieldNumber = 2; + inline ::google::protobuf::uint64 ref() const; + inline void set_ref(::google::protobuf::uint64 value); + + inline StackFrameTypeCase StackFrameType_case() const; + // @@protoc_insertion_point(class_scope:mozilla.devtools.protobuf.StackFrame) + private: + inline void set_has_data(); + inline void set_has_ref(); + + inline bool has_StackFrameType(); + void clear_StackFrameType(); + inline void clear_has_StackFrameType(); + + ::google::protobuf::UnknownFieldSet _unknown_fields_; + + ::google::protobuf::uint32 _has_bits_[1]; + mutable int _cached_size_; + union StackFrameTypeUnion { + ::mozilla::devtools::protobuf::StackFrame_Data* data_; + ::google::protobuf::uint64 ref_; + } StackFrameType_; + ::google::protobuf::uint32 _oneof_case_[1]; + + friend void protobuf_AddDesc_CoreDump_2eproto(); + friend void protobuf_AssignDesc_CoreDump_2eproto(); + friend void protobuf_ShutdownFile_CoreDump_2eproto(); + + void InitAsDefaultInstance(); + static StackFrame* default_instance_; +}; +// ------------------------------------------------------------------- + +class Node : public ::google::protobuf::Message { + public: + Node(); + virtual ~Node(); + + Node(const Node& from); + + inline Node& operator=(const Node& from) { + CopyFrom(from); + return *this; + } + + inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const { + return _unknown_fields_; + } + + inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() { + return &_unknown_fields_; + } + + static const ::google::protobuf::Descriptor* descriptor(); + static const Node& default_instance(); + + enum TypeNameOrRefCase { + kTypeName = 2, + kTypeNameRef = 3, + TYPENAMEORREF_NOT_SET = 0, + }; + + enum JSObjectClassNameOrRefCase { + kJsObjectClassName = 7, + kJsObjectClassNameRef = 8, + JSOBJECTCLASSNAMEORREF_NOT_SET = 0, + }; + + enum ScriptFilenameOrRefCase { + kScriptFilename = 10, + kScriptFilenameRef = 11, + SCRIPTFILENAMEORREF_NOT_SET = 0, + }; + + void Swap(Node* other); + + // implements Message ---------------------------------------------- + + Node* New() const; + void CopyFrom(const ::google::protobuf::Message& from); + void MergeFrom(const ::google::protobuf::Message& from); + void CopyFrom(const Node& from); + void MergeFrom(const Node& from); + void Clear(); + bool IsInitialized() const; + + int ByteSize() const; + bool MergePartialFromCodedStream( + ::google::protobuf::io::CodedInputStream* input); + void SerializeWithCachedSizes( + ::google::protobuf::io::CodedOutputStream* output) const; + ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const; + int GetCachedSize() const { return _cached_size_; } + private: + void SharedCtor(); + void SharedDtor(); + void SetCachedSize(int size) const; + public: + ::google::protobuf::Metadata GetMetadata() const; + + // nested types ---------------------------------------------------- + + // accessors ------------------------------------------------------- + + // optional uint64 id = 1; + inline bool has_id() const; + inline void clear_id(); + static const int kIdFieldNumber = 1; + inline ::google::protobuf::uint64 id() const; + inline void set_id(::google::protobuf::uint64 value); + + // optional bytes typeName = 2; + inline bool has_typename_() const; + inline void clear_typename_(); + static const int kTypeNameFieldNumber = 2; + inline const ::std::string& typename_() const; + inline void set_typename_(const ::std::string& value); + inline void set_typename_(const char* value); + inline void set_typename_(const void* value, size_t size); + inline ::std::string* mutable_typename_(); + inline ::std::string* release_typename_(); + inline void set_allocated_typename_(::std::string* typename_); + + // optional uint64 typeNameRef = 3; + inline bool has_typenameref() const; + inline void clear_typenameref(); + static const int kTypeNameRefFieldNumber = 3; + inline ::google::protobuf::uint64 typenameref() const; + inline void set_typenameref(::google::protobuf::uint64 value); + + // optional uint64 size = 4; + inline bool has_size() const; + inline void clear_size(); + static const int kSizeFieldNumber = 4; + inline ::google::protobuf::uint64 size() const; + inline void set_size(::google::protobuf::uint64 value); + + // repeated .mozilla.devtools.protobuf.Edge edges = 5; + inline int edges_size() const; + inline void clear_edges(); + static const int kEdgesFieldNumber = 5; + inline const ::mozilla::devtools::protobuf::Edge& edges(int index) const; + inline ::mozilla::devtools::protobuf::Edge* mutable_edges(int index); + inline ::mozilla::devtools::protobuf::Edge* add_edges(); + inline const ::google::protobuf::RepeatedPtrField< ::mozilla::devtools::protobuf::Edge >& + edges() const; + inline ::google::protobuf::RepeatedPtrField< ::mozilla::devtools::protobuf::Edge >* + mutable_edges(); + + // optional .mozilla.devtools.protobuf.StackFrame allocationStack = 6; + inline bool has_allocationstack() const; + inline void clear_allocationstack(); + static const int kAllocationStackFieldNumber = 6; + inline const ::mozilla::devtools::protobuf::StackFrame& allocationstack() const; + inline ::mozilla::devtools::protobuf::StackFrame* mutable_allocationstack(); + inline ::mozilla::devtools::protobuf::StackFrame* release_allocationstack(); + inline void set_allocated_allocationstack(::mozilla::devtools::protobuf::StackFrame* allocationstack); + + // optional bytes jsObjectClassName = 7; + inline bool has_jsobjectclassname() const; + inline void clear_jsobjectclassname(); + static const int kJsObjectClassNameFieldNumber = 7; + inline const ::std::string& jsobjectclassname() const; + inline void set_jsobjectclassname(const ::std::string& value); + inline void set_jsobjectclassname(const char* value); + inline void set_jsobjectclassname(const void* value, size_t size); + inline ::std::string* mutable_jsobjectclassname(); + inline ::std::string* release_jsobjectclassname(); + inline void set_allocated_jsobjectclassname(::std::string* jsobjectclassname); + + // optional uint64 jsObjectClassNameRef = 8; + inline bool has_jsobjectclassnameref() const; + inline void clear_jsobjectclassnameref(); + static const int kJsObjectClassNameRefFieldNumber = 8; + inline ::google::protobuf::uint64 jsobjectclassnameref() const; + inline void set_jsobjectclassnameref(::google::protobuf::uint64 value); + + // optional uint32 coarseType = 9 [default = 0]; + inline bool has_coarsetype() const; + inline void clear_coarsetype(); + static const int kCoarseTypeFieldNumber = 9; + inline ::google::protobuf::uint32 coarsetype() const; + inline void set_coarsetype(::google::protobuf::uint32 value); + + // optional bytes scriptFilename = 10; + inline bool has_scriptfilename() const; + inline void clear_scriptfilename(); + static const int kScriptFilenameFieldNumber = 10; + inline const ::std::string& scriptfilename() const; + inline void set_scriptfilename(const ::std::string& value); + inline void set_scriptfilename(const char* value); + inline void set_scriptfilename(const void* value, size_t size); + inline ::std::string* mutable_scriptfilename(); + inline ::std::string* release_scriptfilename(); + inline void set_allocated_scriptfilename(::std::string* scriptfilename); + + // optional uint64 scriptFilenameRef = 11; + inline bool has_scriptfilenameref() const; + inline void clear_scriptfilenameref(); + static const int kScriptFilenameRefFieldNumber = 11; + inline ::google::protobuf::uint64 scriptfilenameref() const; + inline void set_scriptfilenameref(::google::protobuf::uint64 value); + + inline TypeNameOrRefCase TypeNameOrRef_case() const; + inline JSObjectClassNameOrRefCase JSObjectClassNameOrRef_case() const; + inline ScriptFilenameOrRefCase ScriptFilenameOrRef_case() const; + // @@protoc_insertion_point(class_scope:mozilla.devtools.protobuf.Node) + private: + inline void set_has_id(); + inline void clear_has_id(); + inline void set_has_typename_(); + inline void set_has_typenameref(); + inline void set_has_size(); + inline void clear_has_size(); + inline void set_has_allocationstack(); + inline void clear_has_allocationstack(); + inline void set_has_jsobjectclassname(); + inline void set_has_jsobjectclassnameref(); + inline void set_has_coarsetype(); + inline void clear_has_coarsetype(); + inline void set_has_scriptfilename(); + inline void set_has_scriptfilenameref(); + + inline bool has_TypeNameOrRef(); + void clear_TypeNameOrRef(); + inline void clear_has_TypeNameOrRef(); + + inline bool has_JSObjectClassNameOrRef(); + void clear_JSObjectClassNameOrRef(); + inline void clear_has_JSObjectClassNameOrRef(); + + inline bool has_ScriptFilenameOrRef(); + void clear_ScriptFilenameOrRef(); + inline void clear_has_ScriptFilenameOrRef(); + + ::google::protobuf::UnknownFieldSet _unknown_fields_; + + ::google::protobuf::uint32 _has_bits_[1]; + mutable int _cached_size_; + ::google::protobuf::uint64 id_; + ::google::protobuf::uint64 size_; + ::google::protobuf::RepeatedPtrField< ::mozilla::devtools::protobuf::Edge > edges_; + ::mozilla::devtools::protobuf::StackFrame* allocationstack_; + ::google::protobuf::uint32 coarsetype_; + union TypeNameOrRefUnion { + ::std::string* typename__; + ::google::protobuf::uint64 typenameref_; + } TypeNameOrRef_; + union JSObjectClassNameOrRefUnion { + ::std::string* jsobjectclassname_; + ::google::protobuf::uint64 jsobjectclassnameref_; + } JSObjectClassNameOrRef_; + union ScriptFilenameOrRefUnion { + ::std::string* scriptfilename_; + ::google::protobuf::uint64 scriptfilenameref_; + } ScriptFilenameOrRef_; + ::google::protobuf::uint32 _oneof_case_[3]; + + friend void protobuf_AddDesc_CoreDump_2eproto(); + friend void protobuf_AssignDesc_CoreDump_2eproto(); + friend void protobuf_ShutdownFile_CoreDump_2eproto(); + + void InitAsDefaultInstance(); + static Node* default_instance_; +}; +// ------------------------------------------------------------------- + +class Edge : public ::google::protobuf::Message { + public: + Edge(); + virtual ~Edge(); + + Edge(const Edge& from); + + inline Edge& operator=(const Edge& from) { + CopyFrom(from); + return *this; + } + + inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const { + return _unknown_fields_; + } + + inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() { + return &_unknown_fields_; + } + + static const ::google::protobuf::Descriptor* descriptor(); + static const Edge& default_instance(); + + enum EdgeNameOrRefCase { + kName = 2, + kNameRef = 3, + EDGENAMEORREF_NOT_SET = 0, + }; + + void Swap(Edge* other); + + // implements Message ---------------------------------------------- + + Edge* New() const; + void CopyFrom(const ::google::protobuf::Message& from); + void MergeFrom(const ::google::protobuf::Message& from); + void CopyFrom(const Edge& from); + void MergeFrom(const Edge& from); + void Clear(); + bool IsInitialized() const; + + int ByteSize() const; + bool MergePartialFromCodedStream( + ::google::protobuf::io::CodedInputStream* input); + void SerializeWithCachedSizes( + ::google::protobuf::io::CodedOutputStream* output) const; + ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const; + int GetCachedSize() const { return _cached_size_; } + private: + void SharedCtor(); + void SharedDtor(); + void SetCachedSize(int size) const; + public: + ::google::protobuf::Metadata GetMetadata() const; + + // nested types ---------------------------------------------------- + + // accessors ------------------------------------------------------- + + // optional uint64 referent = 1; + inline bool has_referent() const; + inline void clear_referent(); + static const int kReferentFieldNumber = 1; + inline ::google::protobuf::uint64 referent() const; + inline void set_referent(::google::protobuf::uint64 value); + + // optional bytes name = 2; + inline bool has_name() const; + inline void clear_name(); + static const int kNameFieldNumber = 2; + inline const ::std::string& name() const; + inline void set_name(const ::std::string& value); + inline void set_name(const char* value); + inline void set_name(const void* value, size_t size); + inline ::std::string* mutable_name(); + inline ::std::string* release_name(); + inline void set_allocated_name(::std::string* name); + + // optional uint64 nameRef = 3; + inline bool has_nameref() const; + inline void clear_nameref(); + static const int kNameRefFieldNumber = 3; + inline ::google::protobuf::uint64 nameref() const; + inline void set_nameref(::google::protobuf::uint64 value); + + inline EdgeNameOrRefCase EdgeNameOrRef_case() const; + // @@protoc_insertion_point(class_scope:mozilla.devtools.protobuf.Edge) + private: + inline void set_has_referent(); + inline void clear_has_referent(); + inline void set_has_name(); + inline void set_has_nameref(); + + inline bool has_EdgeNameOrRef(); + void clear_EdgeNameOrRef(); + inline void clear_has_EdgeNameOrRef(); + + ::google::protobuf::UnknownFieldSet _unknown_fields_; + + ::google::protobuf::uint32 _has_bits_[1]; + mutable int _cached_size_; + ::google::protobuf::uint64 referent_; + union EdgeNameOrRefUnion { + ::std::string* name_; + ::google::protobuf::uint64 nameref_; + } EdgeNameOrRef_; + ::google::protobuf::uint32 _oneof_case_[1]; + + friend void protobuf_AddDesc_CoreDump_2eproto(); + friend void protobuf_AssignDesc_CoreDump_2eproto(); + friend void protobuf_ShutdownFile_CoreDump_2eproto(); + + void InitAsDefaultInstance(); + static Edge* default_instance_; +}; +// =================================================================== + + +// =================================================================== + +// Metadata + +// optional uint64 timeStamp = 1; +inline bool Metadata::has_timestamp() const { + return (_has_bits_[0] & 0x00000001u) != 0; +} +inline void Metadata::set_has_timestamp() { + _has_bits_[0] |= 0x00000001u; +} +inline void Metadata::clear_has_timestamp() { + _has_bits_[0] &= ~0x00000001u; +} +inline void Metadata::clear_timestamp() { + timestamp_ = GOOGLE_ULONGLONG(0); + clear_has_timestamp(); +} +inline ::google::protobuf::uint64 Metadata::timestamp() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Metadata.timeStamp) + return timestamp_; +} +inline void Metadata::set_timestamp(::google::protobuf::uint64 value) { + set_has_timestamp(); + timestamp_ = value; + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Metadata.timeStamp) +} + +// ------------------------------------------------------------------- + +// StackFrame_Data + +// optional uint64 id = 1; +inline bool StackFrame_Data::has_id() const { + return (_has_bits_[0] & 0x00000001u) != 0; +} +inline void StackFrame_Data::set_has_id() { + _has_bits_[0] |= 0x00000001u; +} +inline void StackFrame_Data::clear_has_id() { + _has_bits_[0] &= ~0x00000001u; +} +inline void StackFrame_Data::clear_id() { + id_ = GOOGLE_ULONGLONG(0); + clear_has_id(); +} +inline ::google::protobuf::uint64 StackFrame_Data::id() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.id) + return id_; +} +inline void StackFrame_Data::set_id(::google::protobuf::uint64 value) { + set_has_id(); + id_ = value; + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.Data.id) +} + +// optional .mozilla.devtools.protobuf.StackFrame parent = 2; +inline bool StackFrame_Data::has_parent() const { + return (_has_bits_[0] & 0x00000002u) != 0; +} +inline void StackFrame_Data::set_has_parent() { + _has_bits_[0] |= 0x00000002u; +} +inline void StackFrame_Data::clear_has_parent() { + _has_bits_[0] &= ~0x00000002u; +} +inline void StackFrame_Data::clear_parent() { + if (parent_ != NULL) parent_->::mozilla::devtools::protobuf::StackFrame::Clear(); + clear_has_parent(); +} +inline const ::mozilla::devtools::protobuf::StackFrame& StackFrame_Data::parent() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.parent) + return parent_ != NULL ? *parent_ : *default_instance_->parent_; +} +inline ::mozilla::devtools::protobuf::StackFrame* StackFrame_Data::mutable_parent() { + set_has_parent(); + if (parent_ == NULL) parent_ = new ::mozilla::devtools::protobuf::StackFrame; + // @@protoc_insertion_point(field_mutable:mozilla.devtools.protobuf.StackFrame.Data.parent) + return parent_; +} +inline ::mozilla::devtools::protobuf::StackFrame* StackFrame_Data::release_parent() { + clear_has_parent(); + ::mozilla::devtools::protobuf::StackFrame* temp = parent_; + parent_ = NULL; + return temp; +} +inline void StackFrame_Data::set_allocated_parent(::mozilla::devtools::protobuf::StackFrame* parent) { + delete parent_; + parent_ = parent; + if (parent) { + set_has_parent(); + } else { + clear_has_parent(); + } + // @@protoc_insertion_point(field_set_allocated:mozilla.devtools.protobuf.StackFrame.Data.parent) +} + +// optional uint32 line = 3; +inline bool StackFrame_Data::has_line() const { + return (_has_bits_[0] & 0x00000004u) != 0; +} +inline void StackFrame_Data::set_has_line() { + _has_bits_[0] |= 0x00000004u; +} +inline void StackFrame_Data::clear_has_line() { + _has_bits_[0] &= ~0x00000004u; +} +inline void StackFrame_Data::clear_line() { + line_ = 0u; + clear_has_line(); +} +inline ::google::protobuf::uint32 StackFrame_Data::line() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.line) + return line_; +} +inline void StackFrame_Data::set_line(::google::protobuf::uint32 value) { + set_has_line(); + line_ = value; + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.Data.line) +} + +// optional uint32 column = 4; +inline bool StackFrame_Data::has_column() const { + return (_has_bits_[0] & 0x00000008u) != 0; +} +inline void StackFrame_Data::set_has_column() { + _has_bits_[0] |= 0x00000008u; +} +inline void StackFrame_Data::clear_has_column() { + _has_bits_[0] &= ~0x00000008u; +} +inline void StackFrame_Data::clear_column() { + column_ = 0u; + clear_has_column(); +} +inline ::google::protobuf::uint32 StackFrame_Data::column() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.column) + return column_; +} +inline void StackFrame_Data::set_column(::google::protobuf::uint32 value) { + set_has_column(); + column_ = value; + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.Data.column) +} + +// optional bytes source = 5; +inline bool StackFrame_Data::has_source() const { + return SourceOrRef_case() == kSource; +} +inline void StackFrame_Data::set_has_source() { + _oneof_case_[0] = kSource; +} +inline void StackFrame_Data::clear_source() { + if (has_source()) { + delete SourceOrRef_.source_; + clear_has_SourceOrRef(); + } +} +inline const ::std::string& StackFrame_Data::source() const { + if (has_source()) { + return *SourceOrRef_.source_; + } + return ::google::protobuf::internal::GetEmptyStringAlreadyInited(); +} +inline void StackFrame_Data::set_source(const ::std::string& value) { + if (!has_source()) { + clear_SourceOrRef(); + set_has_source(); + SourceOrRef_.source_ = new ::std::string; + } + SourceOrRef_.source_->assign(value); +} +inline void StackFrame_Data::set_source(const char* value) { + if (!has_source()) { + clear_SourceOrRef(); + set_has_source(); + SourceOrRef_.source_ = new ::std::string; + } + SourceOrRef_.source_->assign(value); +} +inline void StackFrame_Data::set_source(const void* value, size_t size) { + if (!has_source()) { + clear_SourceOrRef(); + set_has_source(); + SourceOrRef_.source_ = new ::std::string; + } + SourceOrRef_.source_->assign( + reinterpret_cast<const char*>(value), size); +} +inline ::std::string* StackFrame_Data::mutable_source() { + if (!has_source()) { + clear_SourceOrRef(); + set_has_source(); + SourceOrRef_.source_ = new ::std::string; + } + return SourceOrRef_.source_; +} +inline ::std::string* StackFrame_Data::release_source() { + if (has_source()) { + clear_has_SourceOrRef(); + ::std::string* temp = SourceOrRef_.source_; + SourceOrRef_.source_ = NULL; + return temp; + } else { + return NULL; + } +} +inline void StackFrame_Data::set_allocated_source(::std::string* source) { + clear_SourceOrRef(); + if (source) { + set_has_source(); + SourceOrRef_.source_ = source; + } +} + +// optional uint64 sourceRef = 6; +inline bool StackFrame_Data::has_sourceref() const { + return SourceOrRef_case() == kSourceRef; +} +inline void StackFrame_Data::set_has_sourceref() { + _oneof_case_[0] = kSourceRef; +} +inline void StackFrame_Data::clear_sourceref() { + if (has_sourceref()) { + SourceOrRef_.sourceref_ = GOOGLE_ULONGLONG(0); + clear_has_SourceOrRef(); + } +} +inline ::google::protobuf::uint64 StackFrame_Data::sourceref() const { + if (has_sourceref()) { + return SourceOrRef_.sourceref_; + } + return GOOGLE_ULONGLONG(0); +} +inline void StackFrame_Data::set_sourceref(::google::protobuf::uint64 value) { + if (!has_sourceref()) { + clear_SourceOrRef(); + set_has_sourceref(); + } + SourceOrRef_.sourceref_ = value; +} + +// optional bytes functionDisplayName = 7; +inline bool StackFrame_Data::has_functiondisplayname() const { + return FunctionDisplayNameOrRef_case() == kFunctionDisplayName; +} +inline void StackFrame_Data::set_has_functiondisplayname() { + _oneof_case_[1] = kFunctionDisplayName; +} +inline void StackFrame_Data::clear_functiondisplayname() { + if (has_functiondisplayname()) { + delete FunctionDisplayNameOrRef_.functiondisplayname_; + clear_has_FunctionDisplayNameOrRef(); + } +} +inline const ::std::string& StackFrame_Data::functiondisplayname() const { + if (has_functiondisplayname()) { + return *FunctionDisplayNameOrRef_.functiondisplayname_; + } + return ::google::protobuf::internal::GetEmptyStringAlreadyInited(); +} +inline void StackFrame_Data::set_functiondisplayname(const ::std::string& value) { + if (!has_functiondisplayname()) { + clear_FunctionDisplayNameOrRef(); + set_has_functiondisplayname(); + FunctionDisplayNameOrRef_.functiondisplayname_ = new ::std::string; + } + FunctionDisplayNameOrRef_.functiondisplayname_->assign(value); +} +inline void StackFrame_Data::set_functiondisplayname(const char* value) { + if (!has_functiondisplayname()) { + clear_FunctionDisplayNameOrRef(); + set_has_functiondisplayname(); + FunctionDisplayNameOrRef_.functiondisplayname_ = new ::std::string; + } + FunctionDisplayNameOrRef_.functiondisplayname_->assign(value); +} +inline void StackFrame_Data::set_functiondisplayname(const void* value, size_t size) { + if (!has_functiondisplayname()) { + clear_FunctionDisplayNameOrRef(); + set_has_functiondisplayname(); + FunctionDisplayNameOrRef_.functiondisplayname_ = new ::std::string; + } + FunctionDisplayNameOrRef_.functiondisplayname_->assign( + reinterpret_cast<const char*>(value), size); +} +inline ::std::string* StackFrame_Data::mutable_functiondisplayname() { + if (!has_functiondisplayname()) { + clear_FunctionDisplayNameOrRef(); + set_has_functiondisplayname(); + FunctionDisplayNameOrRef_.functiondisplayname_ = new ::std::string; + } + return FunctionDisplayNameOrRef_.functiondisplayname_; +} +inline ::std::string* StackFrame_Data::release_functiondisplayname() { + if (has_functiondisplayname()) { + clear_has_FunctionDisplayNameOrRef(); + ::std::string* temp = FunctionDisplayNameOrRef_.functiondisplayname_; + FunctionDisplayNameOrRef_.functiondisplayname_ = NULL; + return temp; + } else { + return NULL; + } +} +inline void StackFrame_Data::set_allocated_functiondisplayname(::std::string* functiondisplayname) { + clear_FunctionDisplayNameOrRef(); + if (functiondisplayname) { + set_has_functiondisplayname(); + FunctionDisplayNameOrRef_.functiondisplayname_ = functiondisplayname; + } +} + +// optional uint64 functionDisplayNameRef = 8; +inline bool StackFrame_Data::has_functiondisplaynameref() const { + return FunctionDisplayNameOrRef_case() == kFunctionDisplayNameRef; +} +inline void StackFrame_Data::set_has_functiondisplaynameref() { + _oneof_case_[1] = kFunctionDisplayNameRef; +} +inline void StackFrame_Data::clear_functiondisplaynameref() { + if (has_functiondisplaynameref()) { + FunctionDisplayNameOrRef_.functiondisplaynameref_ = GOOGLE_ULONGLONG(0); + clear_has_FunctionDisplayNameOrRef(); + } +} +inline ::google::protobuf::uint64 StackFrame_Data::functiondisplaynameref() const { + if (has_functiondisplaynameref()) { + return FunctionDisplayNameOrRef_.functiondisplaynameref_; + } + return GOOGLE_ULONGLONG(0); +} +inline void StackFrame_Data::set_functiondisplaynameref(::google::protobuf::uint64 value) { + if (!has_functiondisplaynameref()) { + clear_FunctionDisplayNameOrRef(); + set_has_functiondisplaynameref(); + } + FunctionDisplayNameOrRef_.functiondisplaynameref_ = value; +} + +// optional bool isSystem = 9; +inline bool StackFrame_Data::has_issystem() const { + return (_has_bits_[0] & 0x00000100u) != 0; +} +inline void StackFrame_Data::set_has_issystem() { + _has_bits_[0] |= 0x00000100u; +} +inline void StackFrame_Data::clear_has_issystem() { + _has_bits_[0] &= ~0x00000100u; +} +inline void StackFrame_Data::clear_issystem() { + issystem_ = false; + clear_has_issystem(); +} +inline bool StackFrame_Data::issystem() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.isSystem) + return issystem_; +} +inline void StackFrame_Data::set_issystem(bool value) { + set_has_issystem(); + issystem_ = value; + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.Data.isSystem) +} + +// optional bool isSelfHosted = 10; +inline bool StackFrame_Data::has_isselfhosted() const { + return (_has_bits_[0] & 0x00000200u) != 0; +} +inline void StackFrame_Data::set_has_isselfhosted() { + _has_bits_[0] |= 0x00000200u; +} +inline void StackFrame_Data::clear_has_isselfhosted() { + _has_bits_[0] &= ~0x00000200u; +} +inline void StackFrame_Data::clear_isselfhosted() { + isselfhosted_ = false; + clear_has_isselfhosted(); +} +inline bool StackFrame_Data::isselfhosted() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.isSelfHosted) + return isselfhosted_; +} +inline void StackFrame_Data::set_isselfhosted(bool value) { + set_has_isselfhosted(); + isselfhosted_ = value; + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.Data.isSelfHosted) +} + +inline bool StackFrame_Data::has_SourceOrRef() { + return SourceOrRef_case() != SOURCEORREF_NOT_SET; +} +inline void StackFrame_Data::clear_has_SourceOrRef() { + _oneof_case_[0] = SOURCEORREF_NOT_SET; +} +inline bool StackFrame_Data::has_FunctionDisplayNameOrRef() { + return FunctionDisplayNameOrRef_case() != FUNCTIONDISPLAYNAMEORREF_NOT_SET; +} +inline void StackFrame_Data::clear_has_FunctionDisplayNameOrRef() { + _oneof_case_[1] = FUNCTIONDISPLAYNAMEORREF_NOT_SET; +} +inline StackFrame_Data::SourceOrRefCase StackFrame_Data::SourceOrRef_case() const { + return StackFrame_Data::SourceOrRefCase(_oneof_case_[0]); +} +inline StackFrame_Data::FunctionDisplayNameOrRefCase StackFrame_Data::FunctionDisplayNameOrRef_case() const { + return StackFrame_Data::FunctionDisplayNameOrRefCase(_oneof_case_[1]); +} +// ------------------------------------------------------------------- + +// StackFrame + +// optional .mozilla.devtools.protobuf.StackFrame.Data data = 1; +inline bool StackFrame::has_data() const { + return StackFrameType_case() == kData; +} +inline void StackFrame::set_has_data() { + _oneof_case_[0] = kData; +} +inline void StackFrame::clear_data() { + if (has_data()) { + delete StackFrameType_.data_; + clear_has_StackFrameType(); + } +} +inline const ::mozilla::devtools::protobuf::StackFrame_Data& StackFrame::data() const { + return has_data() ? *StackFrameType_.data_ + : ::mozilla::devtools::protobuf::StackFrame_Data::default_instance(); +} +inline ::mozilla::devtools::protobuf::StackFrame_Data* StackFrame::mutable_data() { + if (!has_data()) { + clear_StackFrameType(); + set_has_data(); + StackFrameType_.data_ = new ::mozilla::devtools::protobuf::StackFrame_Data; + } + return StackFrameType_.data_; +} +inline ::mozilla::devtools::protobuf::StackFrame_Data* StackFrame::release_data() { + if (has_data()) { + clear_has_StackFrameType(); + ::mozilla::devtools::protobuf::StackFrame_Data* temp = StackFrameType_.data_; + StackFrameType_.data_ = NULL; + return temp; + } else { + return NULL; + } +} +inline void StackFrame::set_allocated_data(::mozilla::devtools::protobuf::StackFrame_Data* data) { + clear_StackFrameType(); + if (data) { + set_has_data(); + StackFrameType_.data_ = data; + } +} + +// optional uint64 ref = 2; +inline bool StackFrame::has_ref() const { + return StackFrameType_case() == kRef; +} +inline void StackFrame::set_has_ref() { + _oneof_case_[0] = kRef; +} +inline void StackFrame::clear_ref() { + if (has_ref()) { + StackFrameType_.ref_ = GOOGLE_ULONGLONG(0); + clear_has_StackFrameType(); + } +} +inline ::google::protobuf::uint64 StackFrame::ref() const { + if (has_ref()) { + return StackFrameType_.ref_; + } + return GOOGLE_ULONGLONG(0); +} +inline void StackFrame::set_ref(::google::protobuf::uint64 value) { + if (!has_ref()) { + clear_StackFrameType(); + set_has_ref(); + } + StackFrameType_.ref_ = value; +} + +inline bool StackFrame::has_StackFrameType() { + return StackFrameType_case() != STACKFRAMETYPE_NOT_SET; +} +inline void StackFrame::clear_has_StackFrameType() { + _oneof_case_[0] = STACKFRAMETYPE_NOT_SET; +} +inline StackFrame::StackFrameTypeCase StackFrame::StackFrameType_case() const { + return StackFrame::StackFrameTypeCase(_oneof_case_[0]); +} +// ------------------------------------------------------------------- + +// Node + +// optional uint64 id = 1; +inline bool Node::has_id() const { + return (_has_bits_[0] & 0x00000001u) != 0; +} +inline void Node::set_has_id() { + _has_bits_[0] |= 0x00000001u; +} +inline void Node::clear_has_id() { + _has_bits_[0] &= ~0x00000001u; +} +inline void Node::clear_id() { + id_ = GOOGLE_ULONGLONG(0); + clear_has_id(); +} +inline ::google::protobuf::uint64 Node::id() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.id) + return id_; +} +inline void Node::set_id(::google::protobuf::uint64 value) { + set_has_id(); + id_ = value; + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Node.id) +} + +// optional bytes typeName = 2; +inline bool Node::has_typename_() const { + return TypeNameOrRef_case() == kTypeName; +} +inline void Node::set_has_typename_() { + _oneof_case_[0] = kTypeName; +} +inline void Node::clear_typename_() { + if (has_typename_()) { + delete TypeNameOrRef_.typename__; + clear_has_TypeNameOrRef(); + } +} +inline const ::std::string& Node::typename_() const { + if (has_typename_()) { + return *TypeNameOrRef_.typename__; + } + return ::google::protobuf::internal::GetEmptyStringAlreadyInited(); +} +inline void Node::set_typename_(const ::std::string& value) { + if (!has_typename_()) { + clear_TypeNameOrRef(); + set_has_typename_(); + TypeNameOrRef_.typename__ = new ::std::string; + } + TypeNameOrRef_.typename__->assign(value); +} +inline void Node::set_typename_(const char* value) { + if (!has_typename_()) { + clear_TypeNameOrRef(); + set_has_typename_(); + TypeNameOrRef_.typename__ = new ::std::string; + } + TypeNameOrRef_.typename__->assign(value); +} +inline void Node::set_typename_(const void* value, size_t size) { + if (!has_typename_()) { + clear_TypeNameOrRef(); + set_has_typename_(); + TypeNameOrRef_.typename__ = new ::std::string; + } + TypeNameOrRef_.typename__->assign( + reinterpret_cast<const char*>(value), size); +} +inline ::std::string* Node::mutable_typename_() { + if (!has_typename_()) { + clear_TypeNameOrRef(); + set_has_typename_(); + TypeNameOrRef_.typename__ = new ::std::string; + } + return TypeNameOrRef_.typename__; +} +inline ::std::string* Node::release_typename_() { + if (has_typename_()) { + clear_has_TypeNameOrRef(); + ::std::string* temp = TypeNameOrRef_.typename__; + TypeNameOrRef_.typename__ = NULL; + return temp; + } else { + return NULL; + } +} +inline void Node::set_allocated_typename_(::std::string* typename_) { + clear_TypeNameOrRef(); + if (typename_) { + set_has_typename_(); + TypeNameOrRef_.typename__ = typename_; + } +} + +// optional uint64 typeNameRef = 3; +inline bool Node::has_typenameref() const { + return TypeNameOrRef_case() == kTypeNameRef; +} +inline void Node::set_has_typenameref() { + _oneof_case_[0] = kTypeNameRef; +} +inline void Node::clear_typenameref() { + if (has_typenameref()) { + TypeNameOrRef_.typenameref_ = GOOGLE_ULONGLONG(0); + clear_has_TypeNameOrRef(); + } +} +inline ::google::protobuf::uint64 Node::typenameref() const { + if (has_typenameref()) { + return TypeNameOrRef_.typenameref_; + } + return GOOGLE_ULONGLONG(0); +} +inline void Node::set_typenameref(::google::protobuf::uint64 value) { + if (!has_typenameref()) { + clear_TypeNameOrRef(); + set_has_typenameref(); + } + TypeNameOrRef_.typenameref_ = value; +} + +// optional uint64 size = 4; +inline bool Node::has_size() const { + return (_has_bits_[0] & 0x00000008u) != 0; +} +inline void Node::set_has_size() { + _has_bits_[0] |= 0x00000008u; +} +inline void Node::clear_has_size() { + _has_bits_[0] &= ~0x00000008u; +} +inline void Node::clear_size() { + size_ = GOOGLE_ULONGLONG(0); + clear_has_size(); +} +inline ::google::protobuf::uint64 Node::size() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.size) + return size_; +} +inline void Node::set_size(::google::protobuf::uint64 value) { + set_has_size(); + size_ = value; + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Node.size) +} + +// repeated .mozilla.devtools.protobuf.Edge edges = 5; +inline int Node::edges_size() const { + return edges_.size(); +} +inline void Node::clear_edges() { + edges_.Clear(); +} +inline const ::mozilla::devtools::protobuf::Edge& Node::edges(int index) const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.edges) + return edges_.Get(index); +} +inline ::mozilla::devtools::protobuf::Edge* Node::mutable_edges(int index) { + // @@protoc_insertion_point(field_mutable:mozilla.devtools.protobuf.Node.edges) + return edges_.Mutable(index); +} +inline ::mozilla::devtools::protobuf::Edge* Node::add_edges() { + // @@protoc_insertion_point(field_add:mozilla.devtools.protobuf.Node.edges) + return edges_.Add(); +} +inline const ::google::protobuf::RepeatedPtrField< ::mozilla::devtools::protobuf::Edge >& +Node::edges() const { + // @@protoc_insertion_point(field_list:mozilla.devtools.protobuf.Node.edges) + return edges_; +} +inline ::google::protobuf::RepeatedPtrField< ::mozilla::devtools::protobuf::Edge >* +Node::mutable_edges() { + // @@protoc_insertion_point(field_mutable_list:mozilla.devtools.protobuf.Node.edges) + return &edges_; +} + +// optional .mozilla.devtools.protobuf.StackFrame allocationStack = 6; +inline bool Node::has_allocationstack() const { + return (_has_bits_[0] & 0x00000020u) != 0; +} +inline void Node::set_has_allocationstack() { + _has_bits_[0] |= 0x00000020u; +} +inline void Node::clear_has_allocationstack() { + _has_bits_[0] &= ~0x00000020u; +} +inline void Node::clear_allocationstack() { + if (allocationstack_ != NULL) allocationstack_->::mozilla::devtools::protobuf::StackFrame::Clear(); + clear_has_allocationstack(); +} +inline const ::mozilla::devtools::protobuf::StackFrame& Node::allocationstack() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.allocationStack) + return allocationstack_ != NULL ? *allocationstack_ : *default_instance_->allocationstack_; +} +inline ::mozilla::devtools::protobuf::StackFrame* Node::mutable_allocationstack() { + set_has_allocationstack(); + if (allocationstack_ == NULL) allocationstack_ = new ::mozilla::devtools::protobuf::StackFrame; + // @@protoc_insertion_point(field_mutable:mozilla.devtools.protobuf.Node.allocationStack) + return allocationstack_; +} +inline ::mozilla::devtools::protobuf::StackFrame* Node::release_allocationstack() { + clear_has_allocationstack(); + ::mozilla::devtools::protobuf::StackFrame* temp = allocationstack_; + allocationstack_ = NULL; + return temp; +} +inline void Node::set_allocated_allocationstack(::mozilla::devtools::protobuf::StackFrame* allocationstack) { + delete allocationstack_; + allocationstack_ = allocationstack; + if (allocationstack) { + set_has_allocationstack(); + } else { + clear_has_allocationstack(); + } + // @@protoc_insertion_point(field_set_allocated:mozilla.devtools.protobuf.Node.allocationStack) +} + +// optional bytes jsObjectClassName = 7; +inline bool Node::has_jsobjectclassname() const { + return JSObjectClassNameOrRef_case() == kJsObjectClassName; +} +inline void Node::set_has_jsobjectclassname() { + _oneof_case_[1] = kJsObjectClassName; +} +inline void Node::clear_jsobjectclassname() { + if (has_jsobjectclassname()) { + delete JSObjectClassNameOrRef_.jsobjectclassname_; + clear_has_JSObjectClassNameOrRef(); + } +} +inline const ::std::string& Node::jsobjectclassname() const { + if (has_jsobjectclassname()) { + return *JSObjectClassNameOrRef_.jsobjectclassname_; + } + return ::google::protobuf::internal::GetEmptyStringAlreadyInited(); +} +inline void Node::set_jsobjectclassname(const ::std::string& value) { + if (!has_jsobjectclassname()) { + clear_JSObjectClassNameOrRef(); + set_has_jsobjectclassname(); + JSObjectClassNameOrRef_.jsobjectclassname_ = new ::std::string; + } + JSObjectClassNameOrRef_.jsobjectclassname_->assign(value); +} +inline void Node::set_jsobjectclassname(const char* value) { + if (!has_jsobjectclassname()) { + clear_JSObjectClassNameOrRef(); + set_has_jsobjectclassname(); + JSObjectClassNameOrRef_.jsobjectclassname_ = new ::std::string; + } + JSObjectClassNameOrRef_.jsobjectclassname_->assign(value); +} +inline void Node::set_jsobjectclassname(const void* value, size_t size) { + if (!has_jsobjectclassname()) { + clear_JSObjectClassNameOrRef(); + set_has_jsobjectclassname(); + JSObjectClassNameOrRef_.jsobjectclassname_ = new ::std::string; + } + JSObjectClassNameOrRef_.jsobjectclassname_->assign( + reinterpret_cast<const char*>(value), size); +} +inline ::std::string* Node::mutable_jsobjectclassname() { + if (!has_jsobjectclassname()) { + clear_JSObjectClassNameOrRef(); + set_has_jsobjectclassname(); + JSObjectClassNameOrRef_.jsobjectclassname_ = new ::std::string; + } + return JSObjectClassNameOrRef_.jsobjectclassname_; +} +inline ::std::string* Node::release_jsobjectclassname() { + if (has_jsobjectclassname()) { + clear_has_JSObjectClassNameOrRef(); + ::std::string* temp = JSObjectClassNameOrRef_.jsobjectclassname_; + JSObjectClassNameOrRef_.jsobjectclassname_ = NULL; + return temp; + } else { + return NULL; + } +} +inline void Node::set_allocated_jsobjectclassname(::std::string* jsobjectclassname) { + clear_JSObjectClassNameOrRef(); + if (jsobjectclassname) { + set_has_jsobjectclassname(); + JSObjectClassNameOrRef_.jsobjectclassname_ = jsobjectclassname; + } +} + +// optional uint64 jsObjectClassNameRef = 8; +inline bool Node::has_jsobjectclassnameref() const { + return JSObjectClassNameOrRef_case() == kJsObjectClassNameRef; +} +inline void Node::set_has_jsobjectclassnameref() { + _oneof_case_[1] = kJsObjectClassNameRef; +} +inline void Node::clear_jsobjectclassnameref() { + if (has_jsobjectclassnameref()) { + JSObjectClassNameOrRef_.jsobjectclassnameref_ = GOOGLE_ULONGLONG(0); + clear_has_JSObjectClassNameOrRef(); + } +} +inline ::google::protobuf::uint64 Node::jsobjectclassnameref() const { + if (has_jsobjectclassnameref()) { + return JSObjectClassNameOrRef_.jsobjectclassnameref_; + } + return GOOGLE_ULONGLONG(0); +} +inline void Node::set_jsobjectclassnameref(::google::protobuf::uint64 value) { + if (!has_jsobjectclassnameref()) { + clear_JSObjectClassNameOrRef(); + set_has_jsobjectclassnameref(); + } + JSObjectClassNameOrRef_.jsobjectclassnameref_ = value; +} + +// optional uint32 coarseType = 9 [default = 0]; +inline bool Node::has_coarsetype() const { + return (_has_bits_[0] & 0x00000100u) != 0; +} +inline void Node::set_has_coarsetype() { + _has_bits_[0] |= 0x00000100u; +} +inline void Node::clear_has_coarsetype() { + _has_bits_[0] &= ~0x00000100u; +} +inline void Node::clear_coarsetype() { + coarsetype_ = 0u; + clear_has_coarsetype(); +} +inline ::google::protobuf::uint32 Node::coarsetype() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.coarseType) + return coarsetype_; +} +inline void Node::set_coarsetype(::google::protobuf::uint32 value) { + set_has_coarsetype(); + coarsetype_ = value; + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Node.coarseType) +} + +// optional bytes scriptFilename = 10; +inline bool Node::has_scriptfilename() const { + return ScriptFilenameOrRef_case() == kScriptFilename; +} +inline void Node::set_has_scriptfilename() { + _oneof_case_[2] = kScriptFilename; +} +inline void Node::clear_scriptfilename() { + if (has_scriptfilename()) { + delete ScriptFilenameOrRef_.scriptfilename_; + clear_has_ScriptFilenameOrRef(); + } +} +inline const ::std::string& Node::scriptfilename() const { + if (has_scriptfilename()) { + return *ScriptFilenameOrRef_.scriptfilename_; + } + return ::google::protobuf::internal::GetEmptyStringAlreadyInited(); +} +inline void Node::set_scriptfilename(const ::std::string& value) { + if (!has_scriptfilename()) { + clear_ScriptFilenameOrRef(); + set_has_scriptfilename(); + ScriptFilenameOrRef_.scriptfilename_ = new ::std::string; + } + ScriptFilenameOrRef_.scriptfilename_->assign(value); +} +inline void Node::set_scriptfilename(const char* value) { + if (!has_scriptfilename()) { + clear_ScriptFilenameOrRef(); + set_has_scriptfilename(); + ScriptFilenameOrRef_.scriptfilename_ = new ::std::string; + } + ScriptFilenameOrRef_.scriptfilename_->assign(value); +} +inline void Node::set_scriptfilename(const void* value, size_t size) { + if (!has_scriptfilename()) { + clear_ScriptFilenameOrRef(); + set_has_scriptfilename(); + ScriptFilenameOrRef_.scriptfilename_ = new ::std::string; + } + ScriptFilenameOrRef_.scriptfilename_->assign( + reinterpret_cast<const char*>(value), size); +} +inline ::std::string* Node::mutable_scriptfilename() { + if (!has_scriptfilename()) { + clear_ScriptFilenameOrRef(); + set_has_scriptfilename(); + ScriptFilenameOrRef_.scriptfilename_ = new ::std::string; + } + return ScriptFilenameOrRef_.scriptfilename_; +} +inline ::std::string* Node::release_scriptfilename() { + if (has_scriptfilename()) { + clear_has_ScriptFilenameOrRef(); + ::std::string* temp = ScriptFilenameOrRef_.scriptfilename_; + ScriptFilenameOrRef_.scriptfilename_ = NULL; + return temp; + } else { + return NULL; + } +} +inline void Node::set_allocated_scriptfilename(::std::string* scriptfilename) { + clear_ScriptFilenameOrRef(); + if (scriptfilename) { + set_has_scriptfilename(); + ScriptFilenameOrRef_.scriptfilename_ = scriptfilename; + } +} + +// optional uint64 scriptFilenameRef = 11; +inline bool Node::has_scriptfilenameref() const { + return ScriptFilenameOrRef_case() == kScriptFilenameRef; +} +inline void Node::set_has_scriptfilenameref() { + _oneof_case_[2] = kScriptFilenameRef; +} +inline void Node::clear_scriptfilenameref() { + if (has_scriptfilenameref()) { + ScriptFilenameOrRef_.scriptfilenameref_ = GOOGLE_ULONGLONG(0); + clear_has_ScriptFilenameOrRef(); + } +} +inline ::google::protobuf::uint64 Node::scriptfilenameref() const { + if (has_scriptfilenameref()) { + return ScriptFilenameOrRef_.scriptfilenameref_; + } + return GOOGLE_ULONGLONG(0); +} +inline void Node::set_scriptfilenameref(::google::protobuf::uint64 value) { + if (!has_scriptfilenameref()) { + clear_ScriptFilenameOrRef(); + set_has_scriptfilenameref(); + } + ScriptFilenameOrRef_.scriptfilenameref_ = value; +} + +inline bool Node::has_TypeNameOrRef() { + return TypeNameOrRef_case() != TYPENAMEORREF_NOT_SET; +} +inline void Node::clear_has_TypeNameOrRef() { + _oneof_case_[0] = TYPENAMEORREF_NOT_SET; +} +inline bool Node::has_JSObjectClassNameOrRef() { + return JSObjectClassNameOrRef_case() != JSOBJECTCLASSNAMEORREF_NOT_SET; +} +inline void Node::clear_has_JSObjectClassNameOrRef() { + _oneof_case_[1] = JSOBJECTCLASSNAMEORREF_NOT_SET; +} +inline bool Node::has_ScriptFilenameOrRef() { + return ScriptFilenameOrRef_case() != SCRIPTFILENAMEORREF_NOT_SET; +} +inline void Node::clear_has_ScriptFilenameOrRef() { + _oneof_case_[2] = SCRIPTFILENAMEORREF_NOT_SET; +} +inline Node::TypeNameOrRefCase Node::TypeNameOrRef_case() const { + return Node::TypeNameOrRefCase(_oneof_case_[0]); +} +inline Node::JSObjectClassNameOrRefCase Node::JSObjectClassNameOrRef_case() const { + return Node::JSObjectClassNameOrRefCase(_oneof_case_[1]); +} +inline Node::ScriptFilenameOrRefCase Node::ScriptFilenameOrRef_case() const { + return Node::ScriptFilenameOrRefCase(_oneof_case_[2]); +} +// ------------------------------------------------------------------- + +// Edge + +// optional uint64 referent = 1; +inline bool Edge::has_referent() const { + return (_has_bits_[0] & 0x00000001u) != 0; +} +inline void Edge::set_has_referent() { + _has_bits_[0] |= 0x00000001u; +} +inline void Edge::clear_has_referent() { + _has_bits_[0] &= ~0x00000001u; +} +inline void Edge::clear_referent() { + referent_ = GOOGLE_ULONGLONG(0); + clear_has_referent(); +} +inline ::google::protobuf::uint64 Edge::referent() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Edge.referent) + return referent_; +} +inline void Edge::set_referent(::google::protobuf::uint64 value) { + set_has_referent(); + referent_ = value; + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Edge.referent) +} + +// optional bytes name = 2; +inline bool Edge::has_name() const { + return EdgeNameOrRef_case() == kName; +} +inline void Edge::set_has_name() { + _oneof_case_[0] = kName; +} +inline void Edge::clear_name() { + if (has_name()) { + delete EdgeNameOrRef_.name_; + clear_has_EdgeNameOrRef(); + } +} +inline const ::std::string& Edge::name() const { + if (has_name()) { + return *EdgeNameOrRef_.name_; + } + return ::google::protobuf::internal::GetEmptyStringAlreadyInited(); +} +inline void Edge::set_name(const ::std::string& value) { + if (!has_name()) { + clear_EdgeNameOrRef(); + set_has_name(); + EdgeNameOrRef_.name_ = new ::std::string; + } + EdgeNameOrRef_.name_->assign(value); +} +inline void Edge::set_name(const char* value) { + if (!has_name()) { + clear_EdgeNameOrRef(); + set_has_name(); + EdgeNameOrRef_.name_ = new ::std::string; + } + EdgeNameOrRef_.name_->assign(value); +} +inline void Edge::set_name(const void* value, size_t size) { + if (!has_name()) { + clear_EdgeNameOrRef(); + set_has_name(); + EdgeNameOrRef_.name_ = new ::std::string; + } + EdgeNameOrRef_.name_->assign( + reinterpret_cast<const char*>(value), size); +} +inline ::std::string* Edge::mutable_name() { + if (!has_name()) { + clear_EdgeNameOrRef(); + set_has_name(); + EdgeNameOrRef_.name_ = new ::std::string; + } + return EdgeNameOrRef_.name_; +} +inline ::std::string* Edge::release_name() { + if (has_name()) { + clear_has_EdgeNameOrRef(); + ::std::string* temp = EdgeNameOrRef_.name_; + EdgeNameOrRef_.name_ = NULL; + return temp; + } else { + return NULL; + } +} +inline void Edge::set_allocated_name(::std::string* name) { + clear_EdgeNameOrRef(); + if (name) { + set_has_name(); + EdgeNameOrRef_.name_ = name; + } +} + +// optional uint64 nameRef = 3; +inline bool Edge::has_nameref() const { + return EdgeNameOrRef_case() == kNameRef; +} +inline void Edge::set_has_nameref() { + _oneof_case_[0] = kNameRef; +} +inline void Edge::clear_nameref() { + if (has_nameref()) { + EdgeNameOrRef_.nameref_ = GOOGLE_ULONGLONG(0); + clear_has_EdgeNameOrRef(); + } +} +inline ::google::protobuf::uint64 Edge::nameref() const { + if (has_nameref()) { + return EdgeNameOrRef_.nameref_; + } + return GOOGLE_ULONGLONG(0); +} +inline void Edge::set_nameref(::google::protobuf::uint64 value) { + if (!has_nameref()) { + clear_EdgeNameOrRef(); + set_has_nameref(); + } + EdgeNameOrRef_.nameref_ = value; +} + +inline bool Edge::has_EdgeNameOrRef() { + return EdgeNameOrRef_case() != EDGENAMEORREF_NOT_SET; +} +inline void Edge::clear_has_EdgeNameOrRef() { + _oneof_case_[0] = EDGENAMEORREF_NOT_SET; +} +inline Edge::EdgeNameOrRefCase Edge::EdgeNameOrRef_case() const { + return Edge::EdgeNameOrRefCase(_oneof_case_[0]); +} + +// @@protoc_insertion_point(namespace_scope) + +} // namespace protobuf +} // namespace devtools +} // namespace mozilla + +#ifndef SWIG +namespace google { +namespace protobuf { + + +} // namespace google +} // namespace protobuf +#endif // SWIG + +// @@protoc_insertion_point(global_scope) + +#endif // PROTOBUF_CoreDump_2eproto__INCLUDED diff --git a/dom/heapsnapshot/CoreDump.proto b/dom/heapsnapshot/CoreDump.proto new file mode 100644 index 000000000..24a223e11 --- /dev/null +++ b/dom/heapsnapshot/CoreDump.proto @@ -0,0 +1,143 @@ +/* -*- Mode: protobuf; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// # Core Dumps +// +// A core dump is a serialized snapshot of the heap graph. We serialize the heap +// as a series of protobuf messages with each message prefixed by its Varint32 +// byte size so we can delimit individual protobuf messages (protobuf parsers +// cannot determine where a message ends on their own). +// +// The first protobuf message is an instance of the `Metadata` message. All +// subsequent messages will be instances of the `Node` message. The first of +// these `Node` messages is the root node of the serialized heap graph. Here is +// a diagram of our core dump format: +// +// +-----------------------------------------------------------------------+ +// | Varint32: The size of following `Metadata` message. | +// +-----------------------------------------------------------------------+ +// | message: The core dump `Metadata` message. | +// +-----------------------------------------------------------------------+ +// | Varint32: The size of the following `Node` message. | +// +-----------------------------------------------------------------------+ +// | message: The first `Node` message. This is the root node. | +// +-----------------------------------------------------------------------+ +// | Varint32: The size of the following `Node` message. | +// +-----------------------------------------------------------------------+ +// | message: A `Node` message. | +// +-----------------------------------------------------------------------+ +// | Varint32: The size of the following `Node` message. | +// +-----------------------------------------------------------------------+ +// | message: A `Node` message. | +// +-----------------------------------------------------------------------+ +// | . | +// | . | +// | . | +// +-----------------------------------------------------------------------+ +// +// Core dumps should always be written with a +// `google::protobuf::io::GzipOutputStream` and read from a +// `google::protobuf::io::GzipInputStream`. +// +// Note that all strings are de-duplicated. The first time the N^th unique +// string is encountered, the full string is serialized. Subsequent times that +// same string is encountered, it is referenced by N. This de-duplication +// happens across string properties, not on a per-property basis. For example, +// if the same K^th unique string is first used as an Edge::EdgeNameOrRef and +// then as a StackFrame::Data::FunctionDisplayNameOrRef, the first will be the +// actual string as the functionDisplayName oneof property, and the second will +// be a reference to the first as the edgeNameRef oneof property whose value is +// K. +// +// We would ordinarily abstract these de-duplicated strings with messages of +// their own, but unfortunately, the protobuf compiler does not have a way to +// inline a messsage within another message and the child message must be +// referenced by pointer. This leads to extra mallocs that we wish to avoid. + + +package mozilla.devtools.protobuf; + +// A collection of metadata about this core dump. +message Metadata { + // Number of microseconds since midnight (00:00:00) 1 January 1970 UTC. + optional uint64 timeStamp = 1; +} + +// A serialized version of `JS::ubi::StackFrame`. Older parent frame tails are +// de-duplicated to cut down on [de]serialization and size costs. +message StackFrame { + oneof StackFrameType { + // This is the first time this stack frame has been serialized, and so + // here is all of its data. + Data data = 1; + // A reference to a stack frame that has already been serialized and has + // the given number as its id. + uint64 ref = 2; + } + + message Data { + optional uint64 id = 1; + optional StackFrame parent = 2; + optional uint32 line = 3; + optional uint32 column = 4; + + // De-duplicated two-byte string. + oneof SourceOrRef { + bytes source = 5; + uint64 sourceRef = 6; + } + + // De-duplicated two-byte string. + oneof FunctionDisplayNameOrRef { + bytes functionDisplayName = 7; + uint64 functionDisplayNameRef = 8; + } + + optional bool isSystem = 9; + optional bool isSelfHosted = 10; + } +} + +// A serialized version of `JS::ubi::Node` and its outgoing edges. +message Node { + optional uint64 id = 1; + + // De-duplicated two-byte string. + oneof TypeNameOrRef { + bytes typeName = 2; + uint64 typeNameRef = 3; + } + + optional uint64 size = 4; + repeated Edge edges = 5; + optional StackFrame allocationStack = 6; + + // De-duplicated one-byte string. + oneof JSObjectClassNameOrRef { + bytes jsObjectClassName = 7; + uint64 jsObjectClassNameRef = 8; + } + + // JS::ubi::CoarseType. Defaults to Other. + optional uint32 coarseType = 9 [default = 0]; + + // De-duplicated one-byte string. + oneof ScriptFilenameOrRef { + bytes scriptFilename = 10; + uint64 scriptFilenameRef = 11; + } +} + +// A serialized edge from the heap graph. +message Edge { + optional uint64 referent = 1; + + // De-duplicated two-byte string. + oneof EdgeNameOrRef { + bytes name = 2; + uint64 nameRef = 3; + } +} diff --git a/dom/heapsnapshot/DeserializedNode.cpp b/dom/heapsnapshot/DeserializedNode.cpp new file mode 100644 index 000000000..fac4cccb9 --- /dev/null +++ b/dom/heapsnapshot/DeserializedNode.cpp @@ -0,0 +1,150 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/devtools/DeserializedNode.h" +#include "mozilla/devtools/HeapSnapshot.h" +#include "nsCRTGlue.h" + +namespace mozilla { +namespace devtools { + +DeserializedEdge::DeserializedEdge(DeserializedEdge&& rhs) +{ + referent = rhs.referent; + name = rhs.name; +} + +DeserializedEdge& DeserializedEdge::operator=(DeserializedEdge&& rhs) +{ + MOZ_ASSERT(&rhs != this); + this->~DeserializedEdge(); + new(this) DeserializedEdge(Move(rhs)); + return *this; +} + +JS::ubi::Node +DeserializedNode::getEdgeReferent(const DeserializedEdge& edge) +{ + auto ptr = owner->nodes.lookup(edge.referent); + MOZ_ASSERT(ptr); + + // `HashSets` only provide const access to their values, because mutating a + // value might change its hash, rendering it unfindable in the set. + // Unfortunately, the `ubi::Node` constructor requires a non-const pointer to + // its referent. However, the only aspect of a `DeserializedNode` we hash on + // is its id, which can't be changed via `ubi::Node`, so this cast can't cause + // the trouble `HashSet` is concerned a non-const reference would cause. + return JS::ubi::Node(const_cast<DeserializedNode*>(&*ptr)); +} + +JS::ubi::StackFrame +DeserializedStackFrame::getParentStackFrame() const +{ + MOZ_ASSERT(parent.isSome()); + auto ptr = owner->frames.lookup(parent.ref()); + MOZ_ASSERT(ptr); + // See above comment in DeserializedNode::getEdgeReferent about why this + // const_cast is needed and safe. + return JS::ubi::StackFrame(const_cast<DeserializedStackFrame*>(&*ptr)); +} + +} // namespace devtools +} // namespace mozilla + +namespace JS { +namespace ubi { + +using mozilla::devtools::DeserializedEdge; + +const char16_t Concrete<DeserializedNode>::concreteTypeName[] = + u"mozilla::devtools::DeserializedNode"; + +const char16_t* +Concrete<DeserializedNode>::typeName() const +{ + return get().typeName; +} + +Node::Size +Concrete<DeserializedNode>::size(mozilla::MallocSizeOf mallocSizeof) const +{ + return get().size; +} + +class DeserializedEdgeRange : public EdgeRange +{ + DeserializedNode* node; + Edge currentEdge; + size_t i; + + void settle() { + if (i >= node->edges.length()) { + front_ = nullptr; + return; + } + + auto& edge = node->edges[i]; + auto referent = node->getEdgeReferent(edge); + currentEdge = mozilla::Move(Edge(edge.name ? NS_strdup(edge.name) : nullptr, + referent)); + front_ = ¤tEdge; + } + +public: + explicit DeserializedEdgeRange(DeserializedNode& node) + : node(&node) + , i(0) + { + settle(); + } + + void popFront() override + { + i++; + settle(); + } +}; + +StackFrame +Concrete<DeserializedNode>::allocationStack() const +{ + MOZ_ASSERT(hasAllocationStack()); + auto id = get().allocationStack.ref(); + auto ptr = get().owner->frames.lookup(id); + MOZ_ASSERT(ptr); + // See above comment in DeserializedNode::getEdgeReferent about why this + // const_cast is needed and safe. + return JS::ubi::StackFrame(const_cast<DeserializedStackFrame*>(&*ptr)); +} + + +js::UniquePtr<EdgeRange> +Concrete<DeserializedNode>::edges(JSContext* cx, bool) const +{ + js::UniquePtr<DeserializedEdgeRange> range(js_new<DeserializedEdgeRange>(get())); + + if (!range) + return nullptr; + + return js::UniquePtr<EdgeRange>(range.release()); +} + +StackFrame +ConcreteStackFrame<DeserializedStackFrame>::parent() const +{ + return get().parent.isNothing() ? StackFrame() : get().getParentStackFrame(); +} + +bool +ConcreteStackFrame<DeserializedStackFrame>::constructSavedFrameStack( + JSContext* cx, + MutableHandleObject outSavedFrameStack) const +{ + StackFrame f(&get()); + return ConstructSavedFrameStackSlow(cx, f, outSavedFrameStack); +} + +} // namespace ubi +} // namespace JS diff --git a/dom/heapsnapshot/DeserializedNode.h b/dom/heapsnapshot/DeserializedNode.h new file mode 100644 index 000000000..60d1fb408 --- /dev/null +++ b/dom/heapsnapshot/DeserializedNode.h @@ -0,0 +1,317 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_devtools_DeserializedNode__ +#define mozilla_devtools_DeserializedNode__ + +#include "js/UbiNode.h" +#include "js/UniquePtr.h" +#include "mozilla/devtools/CoreDump.pb.h" +#include "mozilla/Maybe.h" +#include "mozilla/Move.h" +#include "mozilla/Vector.h" + +// `Deserialized{Node,Edge}` translate protobuf messages from our core dump +// format into structures we can rely upon for implementing `JS::ubi::Node` +// specializations on top of. All of the properties of the protobuf messages are +// optional for future compatibility, and this is the layer where we validate +// that the properties that do actually exist in any given message fulfill our +// semantic requirements. +// +// Both `DeserializedNode` and `DeserializedEdge` are always owned by a +// `HeapSnapshot` instance, and their lifetimes must not extend after that of +// their owning `HeapSnapshot`. + +namespace mozilla { +namespace devtools { + +class HeapSnapshot; + +using NodeId = uint64_t; +using StackFrameId = uint64_t; + +// A `DeserializedEdge` represents an edge in the heap graph pointing to the +// node with id equal to `DeserializedEdge::referent` that we deserialized from +// a core dump. +struct DeserializedEdge { + NodeId referent; + // A borrowed reference to a string owned by this node's owning HeapSnapshot. + const char16_t* name; + + explicit DeserializedEdge(NodeId referent, const char16_t* edgeName = nullptr) + : referent(referent) + , name(edgeName) + { } + DeserializedEdge(DeserializedEdge&& rhs); + DeserializedEdge& operator=(DeserializedEdge&& rhs); + +private: + DeserializedEdge(const DeserializedEdge&) = delete; + DeserializedEdge& operator=(const DeserializedEdge&) = delete; +}; + +// A `DeserializedNode` is a node in the heap graph that we deserialized from a +// core dump. +struct DeserializedNode { + using EdgeVector = Vector<DeserializedEdge>; + using UniqueStringPtr = UniquePtr<char16_t[]>; + + NodeId id; + JS::ubi::CoarseType coarseType; + // A borrowed reference to a string owned by this node's owning HeapSnapshot. + const char16_t* typeName; + uint64_t size; + EdgeVector edges; + Maybe<StackFrameId> allocationStack; + // A borrowed reference to a string owned by this node's owning HeapSnapshot. + const char* jsObjectClassName; + // A borrowed reference to a string owned by this node's owning HeapSnapshot. + const char* scriptFilename; + // A weak pointer to this node's owning `HeapSnapshot`. Safe without + // AddRef'ing because this node's lifetime is equal to that of its owner. + HeapSnapshot* owner; + + DeserializedNode(NodeId id, + JS::ubi::CoarseType coarseType, + const char16_t* typeName, + uint64_t size, + EdgeVector&& edges, + Maybe<StackFrameId> allocationStack, + const char* className, + const char* filename, + HeapSnapshot& owner) + : id(id) + , coarseType(coarseType) + , typeName(typeName) + , size(size) + , edges(Move(edges)) + , allocationStack(allocationStack) + , jsObjectClassName(className) + , scriptFilename(filename) + , owner(&owner) + { } + virtual ~DeserializedNode() { } + + DeserializedNode(DeserializedNode&& rhs) + : id(rhs.id) + , coarseType(rhs.coarseType) + , typeName(rhs.typeName) + , size(rhs.size) + , edges(Move(rhs.edges)) + , allocationStack(rhs.allocationStack) + , jsObjectClassName(rhs.jsObjectClassName) + , scriptFilename(rhs.scriptFilename) + , owner(rhs.owner) + { } + + DeserializedNode& operator=(DeserializedNode&& rhs) + { + MOZ_ASSERT(&rhs != this); + this->~DeserializedNode(); + new(this) DeserializedNode(Move(rhs)); + return *this; + } + + // Get a borrowed reference to the given edge's referent. This method is + // virtual to provide a hook for gmock and gtest. + virtual JS::ubi::Node getEdgeReferent(const DeserializedEdge& edge); + + struct HashPolicy; + +protected: + // This is only for use with `MockDeserializedNode` in testing. + DeserializedNode(NodeId id, const char16_t* typeName, uint64_t size) + : id(id) + , coarseType(JS::ubi::CoarseType::Other) + , typeName(typeName) + , size(size) + , edges() + , allocationStack(Nothing()) + , jsObjectClassName(nullptr) + , scriptFilename(nullptr) + , owner(nullptr) + { } + +private: + DeserializedNode(const DeserializedNode&) = delete; + DeserializedNode& operator=(const DeserializedNode&) = delete; +}; + +static inline js::HashNumber +hashIdDerivedFromPtr(uint64_t id) +{ + // NodeIds and StackFrameIds are always 64 bits, but they are derived from + // the original referents' addresses, which could have been either 32 or 64 + // bits long. As such, NodeId and StackFrameId have little entropy in their + // bottom three bits, and may or may not have entropy in their upper 32 + // bits. This hash should manage both cases well. + id >>= 3; + return js::HashNumber((id >> 32) ^ id); +} + +struct DeserializedNode::HashPolicy +{ + using Lookup = NodeId; + + static js::HashNumber hash(const Lookup& lookup) { + return hashIdDerivedFromPtr(lookup); + } + + static bool match(const DeserializedNode& existing, const Lookup& lookup) { + return existing.id == lookup; + } +}; + +// A `DeserializedStackFrame` is a stack frame referred to by a thing in the +// heap graph that we deserialized from a core dump. +struct DeserializedStackFrame { + StackFrameId id; + Maybe<StackFrameId> parent; + uint32_t line; + uint32_t column; + // Borrowed references to strings owned by this DeserializedStackFrame's + // owning HeapSnapshot. + const char16_t* source; + const char16_t* functionDisplayName; + bool isSystem; + bool isSelfHosted; + // A weak pointer to this frame's owning `HeapSnapshot`. Safe without + // AddRef'ing because this frame's lifetime is equal to that of its owner. + HeapSnapshot* owner; + + explicit DeserializedStackFrame(StackFrameId id, + const Maybe<StackFrameId>& parent, + uint32_t line, + uint32_t column, + const char16_t* source, + const char16_t* functionDisplayName, + bool isSystem, + bool isSelfHosted, + HeapSnapshot& owner) + : id(id) + , parent(parent) + , line(line) + , column(column) + , source(source) + , functionDisplayName(functionDisplayName) + , isSystem(isSystem) + , isSelfHosted(isSelfHosted) + , owner(&owner) + { + MOZ_ASSERT(source); + } + + JS::ubi::StackFrame getParentStackFrame() const; + + struct HashPolicy; + +protected: + // This is exposed only for MockDeserializedStackFrame in the gtests. + explicit DeserializedStackFrame() + : id(0) + , parent(Nothing()) + , line(0) + , column(0) + , source(nullptr) + , functionDisplayName(nullptr) + , isSystem(false) + , isSelfHosted(false) + , owner(nullptr) + { }; +}; + +struct DeserializedStackFrame::HashPolicy { + using Lookup = StackFrameId; + + static js::HashNumber hash(const Lookup& lookup) { + return hashIdDerivedFromPtr(lookup); + } + + static bool match(const DeserializedStackFrame& existing, const Lookup& lookup) { + return existing.id == lookup; + } +}; + +} // namespace devtools +} // namespace mozilla + +namespace JS { +namespace ubi { + +using mozilla::devtools::DeserializedNode; +using mozilla::devtools::DeserializedStackFrame; + +template<> +class Concrete<DeserializedNode> : public Base +{ +protected: + explicit Concrete(DeserializedNode* ptr) : Base(ptr) { } + DeserializedNode& get() const { + return *static_cast<DeserializedNode*>(ptr); + } + +public: + static void construct(void* storage, DeserializedNode* ptr) { + new (storage) Concrete(ptr); + } + + CoarseType coarseType() const final { return get().coarseType; } + Id identifier() const override { return get().id; } + bool isLive() const override { return false; } + const char16_t* typeName() const override; + Node::Size size(mozilla::MallocSizeOf mallocSizeof) const override; + const char* jsObjectClassName() const override { return get().jsObjectClassName; } + const char* scriptFilename() const final { return get().scriptFilename; } + + bool hasAllocationStack() const override { return get().allocationStack.isSome(); } + StackFrame allocationStack() const override; + + // We ignore the `bool wantNames` parameter because we can't control whether + // the core dump was serialized with edge names or not. + js::UniquePtr<EdgeRange> edges(JSContext* cx, bool) const override; + + static const char16_t concreteTypeName[]; +}; + +template<> +class ConcreteStackFrame<DeserializedStackFrame> : public BaseStackFrame +{ +protected: + explicit ConcreteStackFrame(DeserializedStackFrame* ptr) + : BaseStackFrame(ptr) + { } + + DeserializedStackFrame& get() const { + return *static_cast<DeserializedStackFrame*>(ptr); + } + +public: + static void construct(void* storage, DeserializedStackFrame* ptr) { + new (storage) ConcreteStackFrame(ptr); + } + + uint64_t identifier() const override { return get().id; } + uint32_t line() const override { return get().line; } + uint32_t column() const override { return get().column; } + bool isSystem() const override { return get().isSystem; } + bool isSelfHosted(JSContext* cx) const override { return get().isSelfHosted; } + void trace(JSTracer* trc) override { } + AtomOrTwoByteChars source() const override { + return AtomOrTwoByteChars(get().source); + } + AtomOrTwoByteChars functionDisplayName() const override { + return AtomOrTwoByteChars(get().functionDisplayName); + } + + StackFrame parent() const override; + bool constructSavedFrameStack(JSContext* cx, + MutableHandleObject outSavedFrameStack) + const override; +}; + +} // namespace ubi +} // namespace JS + +#endif // mozilla_devtools_DeserializedNode__ diff --git a/dom/heapsnapshot/DominatorTree.cpp b/dom/heapsnapshot/DominatorTree.cpp new file mode 100644 index 000000000..e53c196cf --- /dev/null +++ b/dom/heapsnapshot/DominatorTree.cpp @@ -0,0 +1,140 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/devtools/DominatorTree.h" +#include "mozilla/dom/DominatorTreeBinding.h" + +namespace mozilla { +namespace devtools { + +dom::Nullable<uint64_t> +DominatorTree::GetRetainedSize(uint64_t aNodeId, ErrorResult& aRv) +{ + JS::ubi::Node::Id id(aNodeId); + auto node = mHeapSnapshot->getNodeById(id); + if (node.isNothing()) + return dom::Nullable<uint64_t>(); + + auto mallocSizeOf = GetCurrentThreadDebuggerMallocSizeOf(); + JS::ubi::Node::Size size = 0; + if (!mDominatorTree.getRetainedSize(*node, mallocSizeOf, size)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return dom::Nullable<uint64_t>(); + } + + MOZ_ASSERT(size != 0, + "The node should not have been unknown since we got it from the heap snapshot."); + return dom::Nullable<uint64_t>(size); +} + +struct NodeAndRetainedSize +{ + JS::ubi::Node mNode; + JS::ubi::Node::Size mSize; + + NodeAndRetainedSize(const JS::ubi::Node& aNode, JS::ubi::Node::Size aSize) + : mNode(aNode) + , mSize(aSize) + { } + + struct Comparator + { + static bool + Equals(const NodeAndRetainedSize& aLhs, const NodeAndRetainedSize& aRhs) + { + return aLhs.mSize == aRhs.mSize; + } + + static bool + LessThan(const NodeAndRetainedSize& aLhs, const NodeAndRetainedSize& aRhs) + { + // Use > because we want to sort from greatest to least retained size. + return aLhs.mSize > aRhs.mSize; + } + }; +}; + +void +DominatorTree::GetImmediatelyDominated(uint64_t aNodeId, + dom::Nullable<nsTArray<uint64_t>>& aOutResult, + ErrorResult& aRv) +{ + MOZ_ASSERT(aOutResult.IsNull()); + + JS::ubi::Node::Id id(aNodeId); + Maybe<JS::ubi::Node> node = mHeapSnapshot->getNodeById(id); + if (node.isNothing()) + return; + + // Get all immediately dominated nodes and their retained sizes. + MallocSizeOf mallocSizeOf = GetCurrentThreadDebuggerMallocSizeOf(); + Maybe<JS::ubi::DominatorTree::DominatedSetRange> range = mDominatorTree.getDominatedSet(*node); + MOZ_ASSERT(range.isSome(), "The node should be known, since we got it from the heap snapshot."); + size_t length = range->length(); + nsTArray<NodeAndRetainedSize> dominatedNodes(length); + for (const JS::ubi::Node& dominatedNode : *range) { + JS::ubi::Node::Size retainedSize = 0; + if (NS_WARN_IF(!mDominatorTree.getRetainedSize(dominatedNode, mallocSizeOf, retainedSize))) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + MOZ_ASSERT(retainedSize != 0, + "retainedSize should not be zero since we know the node is in the dominator tree."); + + dominatedNodes.AppendElement(NodeAndRetainedSize(dominatedNode, retainedSize)); + } + + // Sort them by retained size. + NodeAndRetainedSize::Comparator comparator; + dominatedNodes.Sort(comparator); + + // Fill the result with the nodes' ids. + JS::ubi::Node root = mDominatorTree.root(); + aOutResult.SetValue(nsTArray<uint64_t>(length)); + for (const NodeAndRetainedSize& entry : dominatedNodes) { + // The root dominates itself, but we don't want to expose that to JS. + if (entry.mNode == root) + continue; + + aOutResult.Value().AppendElement(entry.mNode.identifier()); + } +} + +dom::Nullable<uint64_t> +DominatorTree::GetImmediateDominator(uint64_t aNodeId) const +{ + JS::ubi::Node::Id id(aNodeId); + Maybe<JS::ubi::Node> node = mHeapSnapshot->getNodeById(id); + if (node.isNothing()) + return dom::Nullable<uint64_t>(); + + JS::ubi::Node dominator = mDominatorTree.getImmediateDominator(*node); + if (!dominator || dominator == *node) + return dom::Nullable<uint64_t>(); + + return dom::Nullable<uint64_t>(dominator.identifier()); +} + + +/*** Cycle Collection Boilerplate *****************************************************************/ + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DominatorTree, mParent, mHeapSnapshot) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(DominatorTree) +NS_IMPL_CYCLE_COLLECTING_RELEASE(DominatorTree) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DominatorTree) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +/* virtual */ JSObject* +DominatorTree::WrapObject(JSContext* aCx, JS::HandleObject aGivenProto) +{ + return dom::DominatorTreeBinding::Wrap(aCx, this, aGivenProto); +} + +} // namespace devtools +} // namespace mozilla diff --git a/dom/heapsnapshot/DominatorTree.h b/dom/heapsnapshot/DominatorTree.h new file mode 100644 index 000000000..f785d4916 --- /dev/null +++ b/dom/heapsnapshot/DominatorTree.h @@ -0,0 +1,67 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_devtools_DominatorTree__ +#define mozilla_devtools_DominatorTree__ + +#include "mozilla/devtools/HeapSnapshot.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/RefCounted.h" +#include "js/UbiNodeDominatorTree.h" +#include "nsWrapperCache.h" + +namespace mozilla { +namespace devtools { + +class DominatorTree final : public nsISupports + , public nsWrapperCache +{ +protected: + nsCOMPtr<nsISupports> mParent; + + virtual ~DominatorTree() { } + +private: + JS::ubi::DominatorTree mDominatorTree; + RefPtr<HeapSnapshot> mHeapSnapshot; + +public: + explicit DominatorTree(JS::ubi::DominatorTree&& aDominatorTree, HeapSnapshot* aHeapSnapshot, + nsISupports* aParent) + : mParent(aParent) + , mDominatorTree(Move(aDominatorTree)) + , mHeapSnapshot(aHeapSnapshot) + { + MOZ_ASSERT(aParent); + MOZ_ASSERT(aHeapSnapshot); + }; + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS; + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(DominatorTree); + + nsISupports* GetParentObject() const { return mParent; } + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // readonly attribute NodeId root + uint64_t Root() const { return mDominatorTree.root().identifier(); } + + // [Throws] NodeSize getRetainedSize(NodeId node) + dom::Nullable<uint64_t> GetRetainedSize(uint64_t aNodeId, ErrorResult& aRv); + + // [Throws] sequence<NodeId>? getImmediatelyDominated(NodeId node); + void GetImmediatelyDominated(uint64_t aNodeId, dom::Nullable<nsTArray<uint64_t>>& aOutDominated, + ErrorResult& aRv); + + // NodeId? getImmediateDominator(NodeId node); + dom::Nullable<uint64_t> GetImmediateDominator(uint64_t aNodeId) const; +}; + +} // namespace devtools +} // namespace mozilla + +#endif // mozilla_devtools_DominatorTree__ diff --git a/dom/heapsnapshot/DominatorTreeNode.js b/dom/heapsnapshot/DominatorTreeNode.js new file mode 100644 index 000000000..13a847fd0 --- /dev/null +++ b/dom/heapsnapshot/DominatorTreeNode.js @@ -0,0 +1,336 @@ +/* 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; + } +}; diff --git a/dom/heapsnapshot/FileDescriptorOutputStream.cpp b/dom/heapsnapshot/FileDescriptorOutputStream.cpp new file mode 100644 index 000000000..72a289558 --- /dev/null +++ b/dom/heapsnapshot/FileDescriptorOutputStream.cpp @@ -0,0 +1,86 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/devtools/FileDescriptorOutputStream.h" +#include "private/pprio.h" + +namespace mozilla { +namespace devtools { + +/* static */ already_AddRefed<FileDescriptorOutputStream> +FileDescriptorOutputStream::Create(const ipc::FileDescriptor& fileDescriptor) +{ + if (NS_WARN_IF(!fileDescriptor.IsValid())) + return nullptr; + + auto rawFD = fileDescriptor.ClonePlatformHandle(); + PRFileDesc* prfd = PR_ImportFile(PROsfd(rawFD.release())); + if (NS_WARN_IF(!prfd)) + return nullptr; + + RefPtr<FileDescriptorOutputStream> stream = new FileDescriptorOutputStream(prfd); + return stream.forget(); +} + +NS_IMPL_ISUPPORTS(FileDescriptorOutputStream, nsIOutputStream); + +NS_IMETHODIMP +FileDescriptorOutputStream::Close() +{ + // Repeatedly closing is idempotent. + if (!fd) + return NS_OK; + + if (PR_Close(fd) != PR_SUCCESS) + return NS_ERROR_FAILURE; + fd = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +FileDescriptorOutputStream::Write(const char* buf, uint32_t count, uint32_t* retval) +{ + if (NS_WARN_IF(!fd)) + return NS_ERROR_FAILURE; + + auto written = PR_Write(fd, buf, count); + if (written < 0) + return NS_ERROR_FAILURE; + *retval = written; + return NS_OK; +} + +NS_IMETHODIMP +FileDescriptorOutputStream::Flush() +{ + if (NS_WARN_IF(!fd)) + return NS_ERROR_FAILURE; + + return PR_Sync(fd) == PR_SUCCESS ? NS_OK : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +FileDescriptorOutputStream::WriteFrom(nsIInputStream* fromStream, uint32_t count, + uint32_t* retval) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +FileDescriptorOutputStream::WriteSegments(nsReadSegmentFun reader, void* closure, + uint32_t count, uint32_t* retval) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +FileDescriptorOutputStream::IsNonBlocking(bool* retval) +{ + *retval = false; + return NS_OK; +} + +} // namespace devtools +} // namespace mozilla diff --git a/dom/heapsnapshot/FileDescriptorOutputStream.h b/dom/heapsnapshot/FileDescriptorOutputStream.h new file mode 100644 index 000000000..6990f1fc3 --- /dev/null +++ b/dom/heapsnapshot/FileDescriptorOutputStream.h @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_devtools_FileDescriptorOutputStream_h +#define mozilla_devtools_FileDescriptorOutputStream_h + +#include <prio.h> + +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/ipc/FileDescriptor.h" +#include "nsIOutputStream.h" + +namespace mozilla { +namespace devtools { + +class FileDescriptorOutputStream final : public nsIOutputStream +{ +private: + PRFileDesc* fd; + +public: + static already_AddRefed<FileDescriptorOutputStream> + Create(const ipc::FileDescriptor& fileDescriptor); + +private: + explicit FileDescriptorOutputStream(PRFileDesc* prfd) + : fd(prfd) + { } + + virtual ~FileDescriptorOutputStream() { } + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOUTPUTSTREAM +}; + +} // namespace devtools +} // namespace mozilla + +#endif // mozilla_devtools_FileDescriptorOutputStream_h diff --git a/dom/heapsnapshot/HeapAnalysesClient.js b/dom/heapsnapshot/HeapAnalysesClient.js new file mode 100644 index 000000000..98601a2b1 --- /dev/null +++ b/dom/heapsnapshot/HeapAnalysesClient.js @@ -0,0 +1,277 @@ +/* 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); +}; diff --git a/dom/heapsnapshot/HeapAnalysesWorker.js b/dom/heapsnapshot/HeapAnalysesWorker.js new file mode 100644 index 000000000..d07d67f80 --- /dev/null +++ b/dom/heapsnapshot/HeapAnalysesWorker.js @@ -0,0 +1,303 @@ +/* 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 }; +}); diff --git a/dom/heapsnapshot/HeapSnapshot.cpp b/dom/heapsnapshot/HeapSnapshot.cpp new file mode 100644 index 000000000..299a96a9c --- /dev/null +++ b/dom/heapsnapshot/HeapSnapshot.cpp @@ -0,0 +1,1619 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "HeapSnapshot.h" + +#include <google/protobuf/io/coded_stream.h> +#include <google/protobuf/io/gzip_stream.h> +#include <google/protobuf/io/zero_copy_stream_impl_lite.h> + +#include "js/Debug.h" +#include "js/TypeDecls.h" +#include "js/UbiNodeBreadthFirst.h" +#include "js/UbiNodeCensus.h" +#include "js/UbiNodeDominatorTree.h" +#include "js/UbiNodeShortestPaths.h" +#include "mozilla/Attributes.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/devtools/AutoMemMap.h" +#include "mozilla/devtools/CoreDump.pb.h" +#include "mozilla/devtools/DeserializedNode.h" +#include "mozilla/devtools/DominatorTree.h" +#include "mozilla/devtools/FileDescriptorOutputStream.h" +#include "mozilla/devtools/HeapSnapshotTempFileHelperChild.h" +#include "mozilla/devtools/ZeroCopyNSIOutputStream.h" +#include "mozilla/dom/ChromeUtils.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/HeapSnapshotBinding.h" +#include "mozilla/RangedPtr.h" +#include "mozilla/Unused.h" + +#include "jsapi.h" +#include "jsfriendapi.h" +#include "nsCycleCollectionParticipant.h" +#include "nsCRTGlue.h" +#include "nsDirectoryServiceDefs.h" +#include "nsIFile.h" +#include "nsIOutputStream.h" +#include "nsISupportsImpl.h" +#include "nsNetUtil.h" +#include "nsPrintfCString.h" +#include "prerror.h" +#include "prio.h" +#include "prtypes.h" + +namespace mozilla { +namespace devtools { + +using namespace JS; +using namespace dom; + +using ::google::protobuf::io::ArrayInputStream; +using ::google::protobuf::io::CodedInputStream; +using ::google::protobuf::io::GzipInputStream; +using ::google::protobuf::io::ZeroCopyInputStream; + +using JS::ubi::AtomOrTwoByteChars; +using JS::ubi::ShortestPaths; + +MallocSizeOf +GetCurrentThreadDebuggerMallocSizeOf() +{ + auto ccjscx = CycleCollectedJSContext::Get(); + MOZ_ASSERT(ccjscx); + auto cx = ccjscx->Context(); + MOZ_ASSERT(cx); + auto mallocSizeOf = JS::dbg::GetDebuggerMallocSizeOf(cx); + MOZ_ASSERT(mallocSizeOf); + return mallocSizeOf; +} + +/*** Cycle Collection Boilerplate *****************************************************************/ + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(HeapSnapshot, mParent) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(HeapSnapshot) +NS_IMPL_CYCLE_COLLECTING_RELEASE(HeapSnapshot) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(HeapSnapshot) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +/* virtual */ JSObject* +HeapSnapshot::WrapObject(JSContext* aCx, HandleObject aGivenProto) +{ + return HeapSnapshotBinding::Wrap(aCx, this, aGivenProto); +} + +/*** Reading Heap Snapshots ***********************************************************************/ + +/* static */ already_AddRefed<HeapSnapshot> +HeapSnapshot::Create(JSContext* cx, + GlobalObject& global, + const uint8_t* buffer, + uint32_t size, + ErrorResult& rv) +{ + RefPtr<HeapSnapshot> snapshot = new HeapSnapshot(cx, global.GetAsSupports()); + if (!snapshot->init(cx, buffer, size)) { + rv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + return snapshot.forget(); +} + +template<typename MessageType> +static bool +parseMessage(ZeroCopyInputStream& stream, uint32_t sizeOfMessage, MessageType& message) +{ + // We need to create a new `CodedInputStream` for each message so that the + // 64MB limit is applied per-message rather than to the whole stream. + CodedInputStream codedStream(&stream); + + // The protobuf message nesting that core dumps exhibit is dominated by + // allocation stacks' frames. In the most deeply nested case, each frame has + // two messages: a StackFrame message and a StackFrame::Data message. These + // frames are on top of a small constant of other messages. There are a + // MAX_STACK_DEPTH number of frames, so we multiply this by 3 to make room for + // the two messages per frame plus some head room for the constant number of + // non-dominating messages. + codedStream.SetRecursionLimit(HeapSnapshot::MAX_STACK_DEPTH * 3); + + auto limit = codedStream.PushLimit(sizeOfMessage); + if (NS_WARN_IF(!message.ParseFromCodedStream(&codedStream)) || + NS_WARN_IF(!codedStream.ConsumedEntireMessage()) || + NS_WARN_IF(codedStream.BytesUntilLimit() != 0)) + { + return false; + } + + codedStream.PopLimit(limit); + return true; +} + +template<typename CharT, typename InternedStringSet> +struct GetOrInternStringMatcher +{ + InternedStringSet& internedStrings; + + explicit GetOrInternStringMatcher(InternedStringSet& strings) : internedStrings(strings) { } + + const CharT* match(const std::string* str) { + MOZ_ASSERT(str); + size_t length = str->length() / sizeof(CharT); + auto tempString = reinterpret_cast<const CharT*>(str->data()); + + UniquePtr<CharT[], NSFreePolicy> owned(NS_strndup(tempString, length)); + if (!owned || !internedStrings.append(Move(owned))) + return nullptr; + + return internedStrings.back().get(); + } + + const CharT* match(uint64_t ref) { + if (MOZ_LIKELY(ref < internedStrings.length())) { + auto& string = internedStrings[ref]; + MOZ_ASSERT(string); + return string.get(); + } + + return nullptr; + } +}; + +template< + // Either char or char16_t. + typename CharT, + // A reference to either `internedOneByteStrings` or `internedTwoByteStrings` + // if CharT is char or char16_t respectively. + typename InternedStringSet> +const CharT* +HeapSnapshot::getOrInternString(InternedStringSet& internedStrings, + Maybe<StringOrRef>& maybeStrOrRef) +{ + // Incomplete message: has neither a string nor a reference to an already + // interned string. + if (MOZ_UNLIKELY(maybeStrOrRef.isNothing())) + return nullptr; + + GetOrInternStringMatcher<CharT, InternedStringSet> m(internedStrings); + return maybeStrOrRef->match(m); +} + +// Get a de-duplicated string as a Maybe<StringOrRef> from the given `msg`. +#define GET_STRING_OR_REF_WITH_PROP_NAMES(msg, strPropertyName, refPropertyName) \ + (msg.has_##refPropertyName() \ + ? Some(StringOrRef(msg.refPropertyName())) \ + : msg.has_##strPropertyName() \ + ? Some(StringOrRef(&msg.strPropertyName())) \ + : Nothing()) + +#define GET_STRING_OR_REF(msg, property) \ + (msg.has_##property##ref() \ + ? Some(StringOrRef(msg.property##ref())) \ + : msg.has_##property() \ + ? Some(StringOrRef(&msg.property())) \ + : Nothing()) + +bool +HeapSnapshot::saveNode(const protobuf::Node& node, NodeIdSet& edgeReferents) +{ + // NB: de-duplicated string properties must be read back and interned in the + // same order here as they are written and serialized in + // `CoreDumpWriter::writeNode` or else indices in references to already + // serialized strings will be off. + + if (NS_WARN_IF(!node.has_id())) + return false; + NodeId id = node.id(); + + // NodeIds are derived from pointers (at most 48 bits) and we rely on them + // fitting into JS numbers (IEEE 754 doubles, can precisely store 53 bit + // integers) despite storing them on disk as 64 bit integers. + if (NS_WARN_IF(!JS::Value::isNumberRepresentable(id))) + return false; + + // Should only deserialize each node once. + if (NS_WARN_IF(nodes.has(id))) + return false; + + if (NS_WARN_IF(!JS::ubi::Uint32IsValidCoarseType(node.coarsetype()))) + return false; + auto coarseType = JS::ubi::Uint32ToCoarseType(node.coarsetype()); + + Maybe<StringOrRef> typeNameOrRef = GET_STRING_OR_REF_WITH_PROP_NAMES(node, typename_, typenameref); + auto typeName = getOrInternString<char16_t>(internedTwoByteStrings, typeNameOrRef); + if (NS_WARN_IF(!typeName)) + return false; + + if (NS_WARN_IF(!node.has_size())) + return false; + uint64_t size = node.size(); + + auto edgesLength = node.edges_size(); + DeserializedNode::EdgeVector edges; + if (NS_WARN_IF(!edges.reserve(edgesLength))) + return false; + for (decltype(edgesLength) i = 0; i < edgesLength; i++) { + auto& protoEdge = node.edges(i); + + if (NS_WARN_IF(!protoEdge.has_referent())) + return false; + NodeId referent = protoEdge.referent(); + + if (NS_WARN_IF(!edgeReferents.put(referent))) + return false; + + const char16_t* edgeName = nullptr; + if (protoEdge.EdgeNameOrRef_case() != protobuf::Edge::EDGENAMEORREF_NOT_SET) { + Maybe<StringOrRef> edgeNameOrRef = GET_STRING_OR_REF(protoEdge, name); + edgeName = getOrInternString<char16_t>(internedTwoByteStrings, edgeNameOrRef); + if (NS_WARN_IF(!edgeName)) + return false; + } + + edges.infallibleAppend(DeserializedEdge(referent, edgeName)); + } + + Maybe<StackFrameId> allocationStack; + if (node.has_allocationstack()) { + StackFrameId id = 0; + if (NS_WARN_IF(!saveStackFrame(node.allocationstack(), id))) + return false; + allocationStack.emplace(id); + } + MOZ_ASSERT(allocationStack.isSome() == node.has_allocationstack()); + + const char* jsObjectClassName = nullptr; + if (node.JSObjectClassNameOrRef_case() != protobuf::Node::JSOBJECTCLASSNAMEORREF_NOT_SET) { + Maybe<StringOrRef> clsNameOrRef = GET_STRING_OR_REF(node, jsobjectclassname); + jsObjectClassName = getOrInternString<char>(internedOneByteStrings, clsNameOrRef); + if (NS_WARN_IF(!jsObjectClassName)) + return false; + } + + const char* scriptFilename = nullptr; + if (node.ScriptFilenameOrRef_case() != protobuf::Node::SCRIPTFILENAMEORREF_NOT_SET) { + Maybe<StringOrRef> scriptFilenameOrRef = GET_STRING_OR_REF(node, scriptfilename); + scriptFilename = getOrInternString<char>(internedOneByteStrings, scriptFilenameOrRef); + if (NS_WARN_IF(!scriptFilename)) + return false; + } + + if (NS_WARN_IF(!nodes.putNew(id, DeserializedNode(id, coarseType, typeName, + size, Move(edges), + allocationStack, + jsObjectClassName, + scriptFilename, *this)))) + { + return false; + }; + + return true; +} + +bool +HeapSnapshot::saveStackFrame(const protobuf::StackFrame& frame, + StackFrameId& outFrameId) +{ + // NB: de-duplicated string properties must be read in the same order here as + // they are written in `CoreDumpWriter::getProtobufStackFrame` or else indices + // in references to already serialized strings will be off. + + if (frame.has_ref()) { + // We should only get a reference to the previous frame if we have already + // seen the previous frame. + if (!frames.has(frame.ref())) + return false; + + outFrameId = frame.ref(); + return true; + } + + // Incomplete message. + if (!frame.has_data()) + return false; + + auto data = frame.data(); + + if (!data.has_id()) + return false; + StackFrameId id = data.id(); + + // This should be the first and only time we see this frame. + if (frames.has(id)) + return false; + + if (!data.has_line()) + return false; + uint32_t line = data.line(); + + if (!data.has_column()) + return false; + uint32_t column = data.column(); + + if (!data.has_issystem()) + return false; + bool isSystem = data.issystem(); + + if (!data.has_isselfhosted()) + return false; + bool isSelfHosted = data.isselfhosted(); + + Maybe<StringOrRef> sourceOrRef = GET_STRING_OR_REF(data, source); + auto source = getOrInternString<char16_t>(internedTwoByteStrings, sourceOrRef); + if (!source) + return false; + + const char16_t* functionDisplayName = nullptr; + if (data.FunctionDisplayNameOrRef_case() != + protobuf::StackFrame_Data::FUNCTIONDISPLAYNAMEORREF_NOT_SET) + { + Maybe<StringOrRef> nameOrRef = GET_STRING_OR_REF(data, functiondisplayname); + functionDisplayName = getOrInternString<char16_t>(internedTwoByteStrings, nameOrRef); + if (!functionDisplayName) + return false; + } + + Maybe<StackFrameId> parent; + if (data.has_parent()) { + StackFrameId parentId = 0; + if (!saveStackFrame(data.parent(), parentId)) + return false; + parent = Some(parentId); + } + + if (!frames.putNew(id, DeserializedStackFrame(id, parent, line, column, + source, functionDisplayName, + isSystem, isSelfHosted, *this))) + { + return false; + } + + outFrameId = id; + return true; +} + +#undef GET_STRING_OR_REF_WITH_PROP_NAMES +#undef GET_STRING_OR_REF + +// Because protobuf messages aren't self-delimiting, we serialize each message +// preceded by its size in bytes. When deserializing, we read this size and then +// limit reading from the stream to the given byte size. If we didn't, then the +// first message would consume the entire stream. +static bool +readSizeOfNextMessage(ZeroCopyInputStream& stream, uint32_t* sizep) +{ + MOZ_ASSERT(sizep); + CodedInputStream codedStream(&stream); + return codedStream.ReadVarint32(sizep) && *sizep > 0; +} + +bool +HeapSnapshot::init(JSContext* cx, const uint8_t* buffer, uint32_t size) +{ + if (!nodes.init() || !frames.init()) + return false; + + ArrayInputStream stream(buffer, size); + GzipInputStream gzipStream(&stream); + uint32_t sizeOfMessage = 0; + + // First is the metadata. + + protobuf::Metadata metadata; + if (NS_WARN_IF(!readSizeOfNextMessage(gzipStream, &sizeOfMessage))) + return false; + if (!parseMessage(gzipStream, sizeOfMessage, metadata)) + return false; + if (metadata.has_timestamp()) + timestamp.emplace(metadata.timestamp()); + + // Next is the root node. + + protobuf::Node root; + if (NS_WARN_IF(!readSizeOfNextMessage(gzipStream, &sizeOfMessage))) + return false; + if (!parseMessage(gzipStream, sizeOfMessage, root)) + return false; + + // Although the id is optional in the protobuf format for future proofing, we + // can't currently do anything without it. + if (NS_WARN_IF(!root.has_id())) + return false; + rootId = root.id(); + + // The set of all node ids we've found edges pointing to. + NodeIdSet edgeReferents(cx); + if (NS_WARN_IF(!edgeReferents.init())) + return false; + + if (NS_WARN_IF(!saveNode(root, edgeReferents))) + return false; + + // Finally, the rest of the nodes in the core dump. + + // Test for the end of the stream. The protobuf library gives no way to tell + // the difference between an underlying read error and the stream being + // done. All we can do is attempt to read the size of the next message and + // extrapolate guestimations from the result of that operation. + while (readSizeOfNextMessage(gzipStream, &sizeOfMessage)) { + protobuf::Node node; + if (!parseMessage(gzipStream, sizeOfMessage, node)) + return false; + if (NS_WARN_IF(!saveNode(node, edgeReferents))) + return false; + } + + // Check the set of node ids referred to by edges we found and ensure that we + // have the node corresponding to each id. If we don't have all of them, it is + // unsafe to perform analyses of this heap snapshot. + for (auto range = edgeReferents.all(); !range.empty(); range.popFront()) { + if (NS_WARN_IF(!nodes.has(range.front()))) + return false; + } + + return true; +} + + +/*** Heap Snapshot Analyses ***********************************************************************/ + +void +HeapSnapshot::TakeCensus(JSContext* cx, JS::HandleObject options, + JS::MutableHandleValue rval, ErrorResult& rv) +{ + JS::ubi::Census census(cx); + if (NS_WARN_IF(!census.init())) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + JS::ubi::CountTypePtr rootType; + if (NS_WARN_IF(!JS::ubi::ParseCensusOptions(cx, census, options, rootType))) { + rv.Throw(NS_ERROR_UNEXPECTED); + return; + } + + JS::ubi::RootedCount rootCount(cx, rootType->makeCount()); + if (NS_WARN_IF(!rootCount)) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + JS::ubi::CensusHandler handler(census, rootCount, GetCurrentThreadDebuggerMallocSizeOf()); + + { + JS::AutoCheckCannotGC nogc; + + JS::ubi::CensusTraversal traversal(cx, handler, nogc); + if (NS_WARN_IF(!traversal.init())) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + if (NS_WARN_IF(!traversal.addStart(getRoot()))) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + if (NS_WARN_IF(!traversal.traverse())) { + rv.Throw(NS_ERROR_UNEXPECTED); + return; + } + } + + if (NS_WARN_IF(!handler.report(cx, rval))) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } +} + +void +HeapSnapshot::DescribeNode(JSContext* cx, JS::HandleObject breakdown, uint64_t nodeId, + JS::MutableHandleValue rval, ErrorResult& rv) { + MOZ_ASSERT(breakdown); + JS::RootedValue breakdownVal(cx, JS::ObjectValue(*breakdown)); + JS::ubi::CountTypePtr rootType = JS::ubi::ParseBreakdown(cx, breakdownVal); + if (NS_WARN_IF(!rootType)) { + rv.Throw(NS_ERROR_UNEXPECTED); + return; + } + + JS::ubi::RootedCount rootCount(cx, rootType->makeCount()); + if (NS_WARN_IF(!rootCount)) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + JS::ubi::Node::Id id(nodeId); + Maybe<JS::ubi::Node> node = getNodeById(id); + if (NS_WARN_IF(node.isNothing())) { + rv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + MallocSizeOf mallocSizeOf = GetCurrentThreadDebuggerMallocSizeOf(); + if (NS_WARN_IF(!rootCount->count(mallocSizeOf, *node))) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + if (NS_WARN_IF(!rootCount->report(cx, rval))) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } +} + + +already_AddRefed<DominatorTree> +HeapSnapshot::ComputeDominatorTree(ErrorResult& rv) +{ + Maybe<JS::ubi::DominatorTree> maybeTree; + { + auto ccjscx = CycleCollectedJSContext::Get(); + MOZ_ASSERT(ccjscx); + auto cx = ccjscx->Context(); + MOZ_ASSERT(cx); + JS::AutoCheckCannotGC nogc(cx); + maybeTree = JS::ubi::DominatorTree::Create(cx, nogc, getRoot()); + } + + if (NS_WARN_IF(maybeTree.isNothing())) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return nullptr; + } + + return MakeAndAddRef<DominatorTree>(Move(*maybeTree), this, mParent); +} + +void +HeapSnapshot::ComputeShortestPaths(JSContext*cx, uint64_t start, + const Sequence<uint64_t>& targets, + uint64_t maxNumPaths, + JS::MutableHandleObject results, + ErrorResult& rv) +{ + // First ensure that our inputs are valid. + + if (NS_WARN_IF(maxNumPaths == 0)) { + rv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + Maybe<JS::ubi::Node> startNode = getNodeById(start); + if (NS_WARN_IF(startNode.isNothing())) { + rv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + if (NS_WARN_IF(targets.Length() == 0)) { + rv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + // Aggregate the targets into a set and make sure that they exist in the heap + // snapshot. + + JS::ubi::NodeSet targetsSet; + if (NS_WARN_IF(!targetsSet.init())) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + for (const auto& target : targets) { + Maybe<JS::ubi::Node> targetNode = getNodeById(target); + if (NS_WARN_IF(targetNode.isNothing())) { + rv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + if (NS_WARN_IF(!targetsSet.put(*targetNode))) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + } + + // Walk the heap graph and find the shortest paths. + + Maybe<ShortestPaths> maybeShortestPaths; + { + JS::AutoCheckCannotGC nogc(cx); + maybeShortestPaths = ShortestPaths::Create(cx, nogc, maxNumPaths, *startNode, + Move(targetsSet)); + } + + if (NS_WARN_IF(maybeShortestPaths.isNothing())) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + auto& shortestPaths = *maybeShortestPaths; + + // Convert the results into a Map object mapping target node IDs to arrays of + // paths found. + + RootedObject resultsMap(cx, JS::NewMapObject(cx)); + if (NS_WARN_IF(!resultsMap)) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + for (auto range = shortestPaths.eachTarget(); !range.empty(); range.popFront()) { + JS::RootedValue key(cx, JS::NumberValue(range.front().identifier())); + JS::AutoValueVector paths(cx); + + bool ok = shortestPaths.forEachPath(range.front(), [&](JS::ubi::Path& path) { + JS::AutoValueVector pathValues(cx); + + for (JS::ubi::BackEdge* edge : path) { + JS::RootedObject pathPart(cx, JS_NewPlainObject(cx)); + if (!pathPart) { + return false; + } + + JS::RootedValue predecessor(cx, NumberValue(edge->predecessor().identifier())); + if (!JS_DefineProperty(cx, pathPart, "predecessor", predecessor, JSPROP_ENUMERATE)) { + return false; + } + + RootedValue edgeNameVal(cx, NullValue()); + if (edge->name()) { + RootedString edgeName(cx, JS_AtomizeUCString(cx, edge->name().get())); + if (!edgeName) { + return false; + } + edgeNameVal = StringValue(edgeName); + } + + if (!JS_DefineProperty(cx, pathPart, "edge", edgeNameVal, JSPROP_ENUMERATE)) { + return false; + } + + if (!pathValues.append(ObjectValue(*pathPart))) { + return false; + } + } + + RootedObject pathObj(cx, JS_NewArrayObject(cx, pathValues)); + return pathObj && paths.append(ObjectValue(*pathObj)); + }); + + if (NS_WARN_IF(!ok)) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + JS::RootedObject pathsArray(cx, JS_NewArrayObject(cx, paths)); + if (NS_WARN_IF(!pathsArray)) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + JS::RootedValue pathsVal(cx, ObjectValue(*pathsArray)); + if (NS_WARN_IF(!JS::MapSet(cx, resultsMap, key, pathsVal))) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + } + + results.set(resultsMap); +} + +/*** Saving Heap Snapshots ************************************************************************/ + +// If we are only taking a snapshot of the heap affected by the given set of +// globals, find the set of compartments the globals are allocated +// within. Returns false on OOM failure. +static bool +PopulateCompartmentsWithGlobals(CompartmentSet& compartments, AutoObjectVector& globals) +{ + if (!compartments.init()) + return false; + + unsigned length = globals.length(); + for (unsigned i = 0; i < length; i++) { + if (!compartments.put(GetObjectCompartment(globals[i]))) + return false; + } + + return true; +} + +// Add the given set of globals as explicit roots in the given roots +// list. Returns false on OOM failure. +static bool +AddGlobalsAsRoots(AutoObjectVector& globals, ubi::RootList& roots) +{ + unsigned length = globals.length(); + for (unsigned i = 0; i < length; i++) { + if (!roots.addRoot(ubi::Node(globals[i].get()), + u"heap snapshot global")) + { + return false; + } + } + return true; +} + +// Choose roots and limits for a traversal, given `boundaries`. Set `roots` to +// the set of nodes within the boundaries that are referred to by nodes +// outside. If `boundaries` does not include all JS compartments, initialize +// `compartments` to the set of included compartments; otherwise, leave +// `compartments` uninitialized. (You can use compartments.initialized() to +// check.) +// +// If `boundaries` is incoherent, or we encounter an error while trying to +// handle it, or we run out of memory, set `rv` appropriately and return +// `false`. +static bool +EstablishBoundaries(JSContext* cx, + ErrorResult& rv, + const HeapSnapshotBoundaries& boundaries, + ubi::RootList& roots, + CompartmentSet& compartments) +{ + MOZ_ASSERT(!roots.initialized()); + MOZ_ASSERT(!compartments.initialized()); + + bool foundBoundaryProperty = false; + + if (boundaries.mRuntime.WasPassed()) { + foundBoundaryProperty = true; + + if (!boundaries.mRuntime.Value()) { + rv.Throw(NS_ERROR_INVALID_ARG); + return false; + } + + if (!roots.init()) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return false; + } + } + + if (boundaries.mDebugger.WasPassed()) { + if (foundBoundaryProperty) { + rv.Throw(NS_ERROR_INVALID_ARG); + return false; + } + foundBoundaryProperty = true; + + JSObject* dbgObj = boundaries.mDebugger.Value(); + if (!dbgObj || !dbg::IsDebugger(*dbgObj)) { + rv.Throw(NS_ERROR_INVALID_ARG); + return false; + } + + AutoObjectVector globals(cx); + if (!dbg::GetDebuggeeGlobals(cx, *dbgObj, globals) || + !PopulateCompartmentsWithGlobals(compartments, globals) || + !roots.init(compartments) || + !AddGlobalsAsRoots(globals, roots)) + { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return false; + } + } + + if (boundaries.mGlobals.WasPassed()) { + if (foundBoundaryProperty) { + rv.Throw(NS_ERROR_INVALID_ARG); + return false; + } + foundBoundaryProperty = true; + + uint32_t length = boundaries.mGlobals.Value().Length(); + if (length == 0) { + rv.Throw(NS_ERROR_INVALID_ARG); + return false; + } + + AutoObjectVector globals(cx); + for (uint32_t i = 0; i < length; i++) { + JSObject* global = boundaries.mGlobals.Value().ElementAt(i); + if (!JS_IsGlobalObject(global)) { + rv.Throw(NS_ERROR_INVALID_ARG); + return false; + } + if (!globals.append(global)) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return false; + } + } + + if (!PopulateCompartmentsWithGlobals(compartments, globals) || + !roots.init(compartments) || + !AddGlobalsAsRoots(globals, roots)) + { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return false; + } + } + + if (!foundBoundaryProperty) { + rv.Throw(NS_ERROR_INVALID_ARG); + return false; + } + + MOZ_ASSERT(roots.initialized()); + MOZ_ASSERT_IF(boundaries.mDebugger.WasPassed(), compartments.initialized()); + MOZ_ASSERT_IF(boundaries.mGlobals.WasPassed(), compartments.initialized()); + return true; +} + + +// A variant covering all the various two-byte strings that we can get from the +// ubi::Node API. +class TwoByteString : public Variant<JSAtom*, const char16_t*, JS::ubi::EdgeName> +{ + using Base = Variant<JSAtom*, const char16_t*, JS::ubi::EdgeName>; + + struct AsTwoByteStringMatcher + { + TwoByteString match(JSAtom* atom) { + return TwoByteString(atom); + } + + TwoByteString match(const char16_t* chars) { + return TwoByteString(chars); + } + }; + + struct IsNonNullMatcher + { + template<typename T> + bool match(const T& t) { return t != nullptr; } + }; + + struct LengthMatcher + { + size_t match(JSAtom* atom) { + MOZ_ASSERT(atom); + JS::ubi::AtomOrTwoByteChars s(atom); + return s.length(); + } + + size_t match(const char16_t* chars) { + MOZ_ASSERT(chars); + return NS_strlen(chars); + } + + size_t match(const JS::ubi::EdgeName& ptr) { + MOZ_ASSERT(ptr); + return NS_strlen(ptr.get()); + } + }; + + struct CopyToBufferMatcher + { + RangedPtr<char16_t> destination; + size_t maxLength; + + CopyToBufferMatcher(RangedPtr<char16_t> destination, size_t maxLength) + : destination(destination) + , maxLength(maxLength) + { } + + size_t match(JS::ubi::EdgeName& ptr) { + return ptr ? match(ptr.get()) : 0; + } + + size_t match(JSAtom* atom) { + MOZ_ASSERT(atom); + JS::ubi::AtomOrTwoByteChars s(atom); + return s.copyToBuffer(destination, maxLength); + } + + size_t match(const char16_t* chars) { + MOZ_ASSERT(chars); + JS::ubi::AtomOrTwoByteChars s(chars); + return s.copyToBuffer(destination, maxLength); + } + }; + +public: + template<typename T> + MOZ_IMPLICIT TwoByteString(T&& rhs) : Base(Forward<T>(rhs)) { } + + template<typename T> + TwoByteString& operator=(T&& rhs) { + MOZ_ASSERT(this != &rhs, "self-move disallowed"); + this->~TwoByteString(); + new (this) TwoByteString(Forward<T>(rhs)); + return *this; + } + + TwoByteString(const TwoByteString&) = delete; + TwoByteString& operator=(const TwoByteString&) = delete; + + // Rewrap the inner value of a JS::ubi::AtomOrTwoByteChars as a TwoByteString. + static TwoByteString from(JS::ubi::AtomOrTwoByteChars&& s) { + AsTwoByteStringMatcher m; + return s.match(m); + } + + // Returns true if the given TwoByteString is non-null, false otherwise. + bool isNonNull() const { + IsNonNullMatcher m; + return match(m); + } + + // Return the length of the string, 0 if it is null. + size_t length() const { + LengthMatcher m; + return match(m); + } + + // Copy the contents of a TwoByteString into the provided buffer. The buffer + // is NOT null terminated. The number of characters written is returned. + size_t copyToBuffer(RangedPtr<char16_t> destination, size_t maxLength) { + CopyToBufferMatcher m(destination, maxLength); + return match(m); + } + + struct HashPolicy; +}; + +// A hashing policy for TwoByteString. +// +// Atoms are pointer hashed and use pointer equality, which means that we +// tolerate some duplication across atoms and the other two types of two-byte +// strings. In practice, we expect the amount of this duplication to be very low +// because each type is generally a different semantic thing in addition to +// having a slightly different representation. For example, the set of edge +// names and the set stack frames' source names naturally tend not to overlap +// very much if at all. +struct TwoByteString::HashPolicy { + using Lookup = TwoByteString; + + struct HashingMatcher { + js::HashNumber match(const JSAtom* atom) { + return js::DefaultHasher<const JSAtom*>::hash(atom); + } + + js::HashNumber match(const char16_t* chars) { + MOZ_ASSERT(chars); + auto length = NS_strlen(chars); + return HashString(chars, length); + } + + js::HashNumber match(const JS::ubi::EdgeName& ptr) { + MOZ_ASSERT(ptr); + return match(ptr.get()); + } + }; + + static js::HashNumber hash(const Lookup& l) { + HashingMatcher hasher; + return l.match(hasher); + } + + struct EqualityMatcher { + const TwoByteString& rhs; + explicit EqualityMatcher(const TwoByteString& rhs) : rhs(rhs) { } + + bool match(const JSAtom* atom) { + return rhs.is<JSAtom*>() && rhs.as<JSAtom*>() == atom; + } + + bool match(const char16_t* chars) { + MOZ_ASSERT(chars); + + const char16_t* rhsChars = nullptr; + if (rhs.is<const char16_t*>()) + rhsChars = rhs.as<const char16_t*>(); + else if (rhs.is<JS::ubi::EdgeName>()) + rhsChars = rhs.as<JS::ubi::EdgeName>().get(); + else + return false; + MOZ_ASSERT(rhsChars); + + auto length = NS_strlen(chars); + if (NS_strlen(rhsChars) != length) + return false; + + return memcmp(chars, rhsChars, length * sizeof(char16_t)) == 0; + } + + bool match(const JS::ubi::EdgeName& ptr) { + MOZ_ASSERT(ptr); + return match(ptr.get()); + } + }; + + static bool match(const TwoByteString& k, const Lookup& l) { + EqualityMatcher eq(l); + return k.match(eq); + } + + static void rekey(TwoByteString& k, TwoByteString&& newKey) { + k = Move(newKey); + } +}; + +// Returns whether `edge` should be included in a heap snapshot of +// `compartments`. The optional `policy` out-param is set to INCLUDE_EDGES +// if we want to include the referent's edges, or EXCLUDE_EDGES if we don't +// want to include them. +static bool +ShouldIncludeEdge(JS::CompartmentSet* compartments, + const ubi::Node& origin, const ubi::Edge& edge, + CoreDumpWriter::EdgePolicy* policy = nullptr) +{ + if (policy) { + *policy = CoreDumpWriter::INCLUDE_EDGES; + } + + if (!compartments) { + // We aren't targeting a particular set of compartments, so serialize all the + // things! + return true; + } + + // We are targeting a particular set of compartments. If this node is in our target + // set, serialize it and all of its edges. If this node is _not_ in our + // target set, we also serialize under the assumption that it is a shared + // resource being used by something in our target compartments since we reached it + // by traversing the heap graph. However, we do not serialize its outgoing + // edges and we abandon further traversal from this node. + // + // If the node does not belong to any compartment, we also serialize its outgoing + // edges. This case is relevant for Shapes: they don't belong to a specific + // compartment and contain edges to parent/kids Shapes we want to include. Note + // that these Shapes may contain pointers into our target compartment (the + // Shape's getter/setter JSObjects). However, we do not serialize nodes in other + // compartments that are reachable from these non-compartment nodes. + + JSCompartment* compartment = edge.referent.compartment(); + + if (!compartment || compartments->has(compartment)) { + return true; + } + + if (policy) { + *policy = CoreDumpWriter::EXCLUDE_EDGES; + } + + return !!origin.compartment(); +} + +// A `CoreDumpWriter` that serializes nodes to protobufs and writes them to the +// given `ZeroCopyOutputStream`. +class MOZ_STACK_CLASS StreamWriter : public CoreDumpWriter +{ + using FrameSet = js::HashSet<uint64_t>; + using TwoByteStringMap = js::HashMap<TwoByteString, uint64_t, TwoByteString::HashPolicy>; + using OneByteStringMap = js::HashMap<const char*, uint64_t>; + + JSContext* cx; + bool wantNames; + // The set of |JS::ubi::StackFrame::identifier()|s that have already been + // serialized and written to the core dump. + FrameSet framesAlreadySerialized; + // The set of two-byte strings that have already been serialized and written + // to the core dump. + TwoByteStringMap twoByteStringsAlreadySerialized; + // The set of one-byte strings that have already been serialized and written + // to the core dump. + OneByteStringMap oneByteStringsAlreadySerialized; + + ::google::protobuf::io::ZeroCopyOutputStream& stream; + + JS::CompartmentSet* compartments; + + bool writeMessage(const ::google::protobuf::MessageLite& message) { + // We have to create a new CodedOutputStream when writing each message so + // that the 64MB size limit used by Coded{Output,Input}Stream to prevent + // integer overflow is enforced per message rather than on the whole stream. + ::google::protobuf::io::CodedOutputStream codedStream(&stream); + codedStream.WriteVarint32(message.ByteSize()); + message.SerializeWithCachedSizes(&codedStream); + return !codedStream.HadError(); + } + + // Attach the full two-byte string or a reference to a two-byte string that + // has already been serialized to a protobuf message. + template <typename SetStringFunction, + typename SetRefFunction> + bool attachTwoByteString(TwoByteString& string, SetStringFunction setString, + SetRefFunction setRef) { + auto ptr = twoByteStringsAlreadySerialized.lookupForAdd(string); + if (ptr) { + setRef(ptr->value()); + return true; + } + + auto length = string.length(); + auto stringData = MakeUnique<std::string>(length * sizeof(char16_t), '\0'); + if (!stringData) + return false; + + auto buf = const_cast<char16_t*>(reinterpret_cast<const char16_t*>(stringData->data())); + string.copyToBuffer(RangedPtr<char16_t>(buf, length), length); + + uint64_t ref = twoByteStringsAlreadySerialized.count(); + if (!twoByteStringsAlreadySerialized.add(ptr, Move(string), ref)) + return false; + + setString(stringData.release()); + return true; + } + + // Attach the full one-byte string or a reference to a one-byte string that + // has already been serialized to a protobuf message. + template <typename SetStringFunction, + typename SetRefFunction> + bool attachOneByteString(const char* string, SetStringFunction setString, + SetRefFunction setRef) { + auto ptr = oneByteStringsAlreadySerialized.lookupForAdd(string); + if (ptr) { + setRef(ptr->value()); + return true; + } + + auto length = strlen(string); + auto stringData = MakeUnique<std::string>(string, length); + if (!stringData) + return false; + + uint64_t ref = oneByteStringsAlreadySerialized.count(); + if (!oneByteStringsAlreadySerialized.add(ptr, string, ref)) + return false; + + setString(stringData.release()); + return true; + } + + protobuf::StackFrame* getProtobufStackFrame(JS::ubi::StackFrame& frame, + size_t depth = 1) { + // NB: de-duplicated string properties must be written in the same order + // here as they are read in `HeapSnapshot::saveStackFrame` or else indices + // in references to already serialized strings will be off. + + MOZ_ASSERT(frame, + "null frames should be represented as the lack of a serialized " + "stack frame"); + + auto id = frame.identifier(); + auto protobufStackFrame = MakeUnique<protobuf::StackFrame>(); + if (!protobufStackFrame) + return nullptr; + + if (framesAlreadySerialized.has(id)) { + protobufStackFrame->set_ref(id); + return protobufStackFrame.release(); + } + + auto data = MakeUnique<protobuf::StackFrame_Data>(); + if (!data) + return nullptr; + + data->set_id(id); + data->set_line(frame.line()); + data->set_column(frame.column()); + data->set_issystem(frame.isSystem()); + data->set_isselfhosted(frame.isSelfHosted(cx)); + + auto dupeSource = TwoByteString::from(frame.source()); + if (!attachTwoByteString(dupeSource, + [&] (std::string* source) { data->set_allocated_source(source); }, + [&] (uint64_t ref) { data->set_sourceref(ref); })) + { + return nullptr; + } + + auto dupeName = TwoByteString::from(frame.functionDisplayName()); + if (dupeName.isNonNull()) { + if (!attachTwoByteString(dupeName, + [&] (std::string* name) { data->set_allocated_functiondisplayname(name); }, + [&] (uint64_t ref) { data->set_functiondisplaynameref(ref); })) + { + return nullptr; + } + } + + auto parent = frame.parent(); + if (parent && depth < HeapSnapshot::MAX_STACK_DEPTH) { + auto protobufParent = getProtobufStackFrame(parent, depth + 1); + if (!protobufParent) + return nullptr; + data->set_allocated_parent(protobufParent); + } + + protobufStackFrame->set_allocated_data(data.release()); + + if (!framesAlreadySerialized.put(id)) + return nullptr; + + return protobufStackFrame.release(); + } + +public: + StreamWriter(JSContext* cx, + ::google::protobuf::io::ZeroCopyOutputStream& stream, + bool wantNames, + JS::CompartmentSet* compartments) + : cx(cx) + , wantNames(wantNames) + , framesAlreadySerialized(cx) + , twoByteStringsAlreadySerialized(cx) + , oneByteStringsAlreadySerialized(cx) + , stream(stream) + , compartments(compartments) + { } + + bool init() { + return framesAlreadySerialized.init() && + twoByteStringsAlreadySerialized.init() && + oneByteStringsAlreadySerialized.init(); + } + + ~StreamWriter() override { } + + virtual bool writeMetadata(uint64_t timestamp) final { + protobuf::Metadata metadata; + metadata.set_timestamp(timestamp); + return writeMessage(metadata); + } + + virtual bool writeNode(const JS::ubi::Node& ubiNode, + EdgePolicy includeEdges) override final { + // NB: de-duplicated string properties must be written in the same order + // here as they are read in `HeapSnapshot::saveNode` or else indices in + // references to already serialized strings will be off. + + protobuf::Node protobufNode; + protobufNode.set_id(ubiNode.identifier()); + + protobufNode.set_coarsetype(JS::ubi::CoarseTypeToUint32(ubiNode.coarseType())); + + auto typeName = TwoByteString(ubiNode.typeName()); + if (NS_WARN_IF(!attachTwoByteString(typeName, + [&] (std::string* name) { protobufNode.set_allocated_typename_(name); }, + [&] (uint64_t ref) { protobufNode.set_typenameref(ref); }))) + { + return false; + } + + mozilla::MallocSizeOf mallocSizeOf = dbg::GetDebuggerMallocSizeOf(cx); + MOZ_ASSERT(mallocSizeOf); + protobufNode.set_size(ubiNode.size(mallocSizeOf)); + + if (includeEdges) { + auto edges = ubiNode.edges(cx, wantNames); + if (NS_WARN_IF(!edges)) + return false; + + for ( ; !edges->empty(); edges->popFront()) { + ubi::Edge& ubiEdge = edges->front(); + if (!ShouldIncludeEdge(compartments, ubiNode, ubiEdge)) { + continue; + } + + protobuf::Edge* protobufEdge = protobufNode.add_edges(); + if (NS_WARN_IF(!protobufEdge)) { + return false; + } + + protobufEdge->set_referent(ubiEdge.referent.identifier()); + + if (wantNames && ubiEdge.name) { + TwoByteString edgeName(Move(ubiEdge.name)); + if (NS_WARN_IF(!attachTwoByteString(edgeName, + [&] (std::string* name) { protobufEdge->set_allocated_name(name); }, + [&] (uint64_t ref) { protobufEdge->set_nameref(ref); }))) + { + return false; + } + } + } + } + + if (ubiNode.hasAllocationStack()) { + auto ubiStackFrame = ubiNode.allocationStack(); + auto protoStackFrame = getProtobufStackFrame(ubiStackFrame); + if (NS_WARN_IF(!protoStackFrame)) + return false; + protobufNode.set_allocated_allocationstack(protoStackFrame); + } + + if (auto className = ubiNode.jsObjectClassName()) { + if (NS_WARN_IF(!attachOneByteString(className, + [&] (std::string* name) { protobufNode.set_allocated_jsobjectclassname(name); }, + [&] (uint64_t ref) { protobufNode.set_jsobjectclassnameref(ref); }))) + { + return false; + } + } + + if (auto scriptFilename = ubiNode.scriptFilename()) { + if (NS_WARN_IF(!attachOneByteString(scriptFilename, + [&] (std::string* name) { protobufNode.set_allocated_scriptfilename(name); }, + [&] (uint64_t ref) { protobufNode.set_scriptfilenameref(ref); }))) + { + return false; + } + } + + return writeMessage(protobufNode); + } +}; + +// A JS::ubi::BreadthFirst handler that serializes a snapshot of the heap into a +// core dump. +class MOZ_STACK_CLASS HeapSnapshotHandler +{ + CoreDumpWriter& writer; + JS::CompartmentSet* compartments; + +public: + HeapSnapshotHandler(CoreDumpWriter& writer, + JS::CompartmentSet* compartments) + : writer(writer), + compartments(compartments) + { } + + // JS::ubi::BreadthFirst handler interface. + + class NodeData { }; + typedef JS::ubi::BreadthFirst<HeapSnapshotHandler> Traversal; + bool operator() (Traversal& traversal, + JS::ubi::Node origin, + const JS::ubi::Edge& edge, + NodeData*, + bool first) + { + // We're only interested in the first time we reach edge.referent, not in + // every edge arriving at that node. "But, don't we want to serialize every + // edge in the heap graph?" you ask. Don't worry! This edge is still + // serialized into the core dump. Serializing a node also serializes each of + // its edges, and if we are traversing a given edge, we must have already + // visited and serialized the origin node and its edges. + if (!first) + return true; + + CoreDumpWriter::EdgePolicy policy; + if (!ShouldIncludeEdge(compartments, origin, edge, &policy)) + return true; + + if (policy == CoreDumpWriter::EXCLUDE_EDGES) + traversal.abandonReferent(); + + return writer.writeNode(edge.referent, policy); + } +}; + + +bool +WriteHeapGraph(JSContext* cx, + const JS::ubi::Node& node, + CoreDumpWriter& writer, + bool wantNames, + JS::CompartmentSet* compartments, + JS::AutoCheckCannotGC& noGC) +{ + // Serialize the starting node to the core dump. + + if (NS_WARN_IF(!writer.writeNode(node, CoreDumpWriter::INCLUDE_EDGES))) { + return false; + } + + // Walk the heap graph starting from the given node and serialize it into the + // core dump. + + HeapSnapshotHandler handler(writer, compartments); + HeapSnapshotHandler::Traversal traversal(cx, handler, noGC); + if (!traversal.init()) + return false; + traversal.wantNames = wantNames; + + bool ok = traversal.addStartVisited(node) && + traversal.traverse(); + + return ok; +} + +static unsigned long +msSinceProcessCreation(const TimeStamp& now) +{ + bool ignored; + auto duration = now - TimeStamp::ProcessCreation(ignored); + return (unsigned long) duration.ToMilliseconds(); +} + +/* static */ already_AddRefed<nsIFile> +HeapSnapshot::CreateUniqueCoreDumpFile(ErrorResult& rv, + const TimeStamp& now, + nsAString& outFilePath) +{ + nsCOMPtr<nsIFile> file; + rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(file)); + if (NS_WARN_IF(rv.Failed())) + return nullptr; + + auto ms = msSinceProcessCreation(now); + rv = file->AppendNative(nsPrintfCString("%lu.fxsnapshot", ms)); + if (NS_WARN_IF(rv.Failed())) + return nullptr; + + rv = file->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0666); + if (NS_WARN_IF(rv.Failed())) + return nullptr; + + rv = file->GetPath(outFilePath); + if (NS_WARN_IF(rv.Failed())) + return nullptr; + + return file.forget(); +} + +// Deletion policy for cleaning up PHeapSnapshotTempFileHelperChild pointers. +class DeleteHeapSnapshotTempFileHelperChild +{ +public: + constexpr DeleteHeapSnapshotTempFileHelperChild() { } + + void operator()(PHeapSnapshotTempFileHelperChild* ptr) const { + Unused << NS_WARN_IF(!HeapSnapshotTempFileHelperChild::Send__delete__(ptr)); + } +}; + +// A UniquePtr alias to automatically manage PHeapSnapshotTempFileHelperChild +// pointers. +using UniqueHeapSnapshotTempFileHelperChild = UniquePtr<PHeapSnapshotTempFileHelperChild, + DeleteHeapSnapshotTempFileHelperChild>; + +// Get an nsIOutputStream that we can write the heap snapshot to. In non-e10s +// and in the e10s parent process, open a file directly and create an output +// stream for it. In e10s child processes, we are sandboxed without access to +// the filesystem. Use IPDL to request a file descriptor from the parent +// process. +static already_AddRefed<nsIOutputStream> +getCoreDumpOutputStream(ErrorResult& rv, TimeStamp& start, nsAString& outFilePath) +{ + if (XRE_IsParentProcess()) { + // Create the file and open the output stream directly. + + nsCOMPtr<nsIFile> file = HeapSnapshot::CreateUniqueCoreDumpFile(rv, + start, + outFilePath); + if (NS_WARN_IF(rv.Failed())) + return nullptr; + + nsCOMPtr<nsIOutputStream> outputStream; + rv = NS_NewLocalFileOutputStream(getter_AddRefs(outputStream), file, + PR_WRONLY, -1, 0); + if (NS_WARN_IF(rv.Failed())) + return nullptr; + + return outputStream.forget(); + } else { + // Request a file descriptor from the parent process over IPDL. + + auto cc = ContentChild::GetSingleton(); + if (!cc) { + rv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + UniqueHeapSnapshotTempFileHelperChild helper( + cc->SendPHeapSnapshotTempFileHelperConstructor()); + if (NS_WARN_IF(!helper)) { + rv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + OpenHeapSnapshotTempFileResponse response; + if (!helper->SendOpenHeapSnapshotTempFile(&response)) { + rv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + if (response.type() == OpenHeapSnapshotTempFileResponse::Tnsresult) { + rv.Throw(response.get_nsresult()); + return nullptr; + } + + auto opened = response.get_OpenedFile(); + outFilePath = opened.path(); + nsCOMPtr<nsIOutputStream> outputStream = + FileDescriptorOutputStream::Create(opened.descriptor()); + if (NS_WARN_IF(!outputStream)) { + rv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + return outputStream.forget(); + } +} + +} // namespace devtools + +namespace dom { + +using namespace JS; +using namespace devtools; + +/* static */ void +ThreadSafeChromeUtils::SaveHeapSnapshot(GlobalObject& global, + const HeapSnapshotBoundaries& boundaries, + nsAString& outFilePath, + ErrorResult& rv) +{ + auto start = TimeStamp::Now(); + + bool wantNames = true; + CompartmentSet compartments; + + nsCOMPtr<nsIOutputStream> outputStream = getCoreDumpOutputStream(rv, start, outFilePath); + if (NS_WARN_IF(rv.Failed())) + return; + + ZeroCopyNSIOutputStream zeroCopyStream(outputStream); + ::google::protobuf::io::GzipOutputStream gzipStream(&zeroCopyStream); + + JSContext* cx = global.Context(); + + { + Maybe<AutoCheckCannotGC> maybeNoGC; + ubi::RootList rootList(cx, maybeNoGC, wantNames); + if (!EstablishBoundaries(cx, rv, boundaries, rootList, compartments)) + return; + + StreamWriter writer(cx, gzipStream, wantNames, + compartments.initialized() ? &compartments : nullptr); + if (NS_WARN_IF(!writer.init())) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + MOZ_ASSERT(maybeNoGC.isSome()); + ubi::Node roots(&rootList); + + // Serialize the initial heap snapshot metadata to the core dump. + if (!writer.writeMetadata(PR_Now()) || + // Serialize the heap graph to the core dump, starting from our list of + // roots. + !WriteHeapGraph(cx, + roots, + writer, + wantNames, + compartments.initialized() ? &compartments : nullptr, + maybeNoGC.ref())) + { + rv.Throw(zeroCopyStream.failed() + ? zeroCopyStream.result() + : NS_ERROR_UNEXPECTED); + return; + } + } +} + +/* static */ already_AddRefed<HeapSnapshot> +ThreadSafeChromeUtils::ReadHeapSnapshot(GlobalObject& global, + const nsAString& filePath, + ErrorResult& rv) +{ + UniquePtr<char[]> path(ToNewCString(filePath)); + if (!path) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return nullptr; + } + + AutoMemMap mm; + rv = mm.init(path.get()); + if (rv.Failed()) + return nullptr; + + RefPtr<HeapSnapshot> snapshot = HeapSnapshot::Create( + global.Context(), global, reinterpret_cast<const uint8_t*>(mm.address()), + mm.size(), rv); + + return snapshot.forget(); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/heapsnapshot/HeapSnapshot.h b/dom/heapsnapshot/HeapSnapshot.h new file mode 100644 index 000000000..12dfa4c2b --- /dev/null +++ b/dom/heapsnapshot/HeapSnapshot.h @@ -0,0 +1,224 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_devtools_HeapSnapshot__ +#define mozilla_devtools_HeapSnapshot__ + +#include "js/HashTable.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/devtools/DeserializedNode.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/Nullable.h" +#include "mozilla/HashFunctions.h" +#include "mozilla/Maybe.h" +#include "mozilla/RefCounted.h" +#include "mozilla/RefPtr.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/UniquePtr.h" + +#include "CoreDump.pb.h" +#include "nsCOMPtr.h" +#include "nsCRTGlue.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsWrapperCache.h" +#include "nsXPCOM.h" + +namespace mozilla { +namespace devtools { + +class DominatorTree; + +struct NSFreePolicy { + void operator()(void* ptr) { + NS_Free(ptr); + } +}; + +using UniqueTwoByteString = UniquePtr<char16_t[], NSFreePolicy>; +using UniqueOneByteString = UniquePtr<char[], NSFreePolicy>; + +class HeapSnapshot final : public nsISupports + , public nsWrapperCache +{ + friend struct DeserializedNode; + friend struct DeserializedEdge; + friend struct DeserializedStackFrame; + friend class JS::ubi::Concrete<JS::ubi::DeserializedNode>; + + explicit HeapSnapshot(JSContext* cx, nsISupports* aParent) + : timestamp(Nothing()) + , rootId(0) + , nodes(cx) + , frames(cx) + , mParent(aParent) + { + MOZ_ASSERT(aParent); + }; + + // Initialize this HeapSnapshot from the given buffer that contains a + // serialized core dump. Do NOT take ownership of the buffer, only borrow it + // for the duration of the call. Return false on failure. + bool init(JSContext* cx, const uint8_t* buffer, uint32_t size); + + using NodeIdSet = js::HashSet<NodeId>; + + // Save the given `protobuf::Node` message in this `HeapSnapshot` as a + // `DeserializedNode`. + bool saveNode(const protobuf::Node& node, NodeIdSet& edgeReferents); + + // Save the given `protobuf::StackFrame` message in this `HeapSnapshot` as a + // `DeserializedStackFrame`. The saved stack frame's id is returned via the + // out parameter. + bool saveStackFrame(const protobuf::StackFrame& frame, + StackFrameId& outFrameId); + +public: + // The maximum number of stack frames that we will serialize into a core + // dump. This helps prevent over-recursion in the protobuf library when + // deserializing stacks. + static const size_t MAX_STACK_DEPTH = 60; + +private: + // If present, a timestamp in the same units that `PR_Now` gives. + Maybe<uint64_t> timestamp; + + // The id of the root node for this deserialized heap graph. + NodeId rootId; + + // The set of nodes in this deserialized heap graph, keyed by id. + using NodeSet = js::HashSet<DeserializedNode, DeserializedNode::HashPolicy>; + NodeSet nodes; + + // The set of stack frames in this deserialized heap graph, keyed by id. + using FrameSet = js::HashSet<DeserializedStackFrame, + DeserializedStackFrame::HashPolicy>; + FrameSet frames; + + Vector<UniqueTwoByteString> internedTwoByteStrings; + Vector<UniqueOneByteString> internedOneByteStrings; + + using StringOrRef = Variant<const std::string*, uint64_t>; + + template<typename CharT, + typename InternedStringSet> + const CharT* getOrInternString(InternedStringSet& internedStrings, + Maybe<StringOrRef>& maybeStrOrRef); + +protected: + nsCOMPtr<nsISupports> mParent; + + virtual ~HeapSnapshot() { } + +public: + // Create a `HeapSnapshot` from the given buffer that contains a serialized + // core dump. Do NOT take ownership of the buffer, only borrow it for the + // duration of the call. + static already_AddRefed<HeapSnapshot> Create(JSContext* cx, + dom::GlobalObject& global, + const uint8_t* buffer, + uint32_t size, + ErrorResult& rv); + + // Creates the `$TEMP_DIR/XXXXXX-XXX.fxsnapshot` core dump file that heap + // snapshots are serialized into. + static already_AddRefed<nsIFile> CreateUniqueCoreDumpFile(ErrorResult& rv, + const TimeStamp& now, + nsAString& outFilePath); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(HeapSnapshot) + MOZ_DECLARE_REFCOUNTED_TYPENAME(HeapSnapshot) + + nsISupports* GetParentObject() const { return mParent; } + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + const char16_t* borrowUniqueString(const char16_t* duplicateString, + size_t length); + + // Get the root node of this heap snapshot's graph. + JS::ubi::Node getRoot() { + MOZ_ASSERT(nodes.initialized()); + auto p = nodes.lookup(rootId); + MOZ_ASSERT(p); + const DeserializedNode& node = *p; + return JS::ubi::Node(const_cast<DeserializedNode*>(&node)); + } + + Maybe<JS::ubi::Node> getNodeById(JS::ubi::Node::Id nodeId) { + auto p = nodes.lookup(nodeId); + if (!p) + return Nothing(); + return Some(JS::ubi::Node(const_cast<DeserializedNode*>(&*p))); + } + + void TakeCensus(JSContext* cx, JS::HandleObject options, + JS::MutableHandleValue rval, ErrorResult& rv); + + void DescribeNode(JSContext* cx, JS::HandleObject breakdown, uint64_t nodeId, + JS::MutableHandleValue rval, ErrorResult& rv); + + already_AddRefed<DominatorTree> ComputeDominatorTree(ErrorResult& rv); + + void ComputeShortestPaths(JSContext*cx, uint64_t start, + const dom::Sequence<uint64_t>& targets, + uint64_t maxNumPaths, + JS::MutableHandleObject results, + ErrorResult& rv); + + dom::Nullable<uint64_t> GetCreationTime() { + static const uint64_t maxTime = uint64_t(1) << 53; + if (timestamp.isSome() && timestamp.ref() <= maxTime) { + return dom::Nullable<uint64_t>(timestamp.ref()); + } + + return dom::Nullable<uint64_t>(); + } +}; + +// A `CoreDumpWriter` is given the data we wish to save in a core dump and +// serializes it to disk, or memory, or a socket, etc. +class CoreDumpWriter +{ +public: + virtual ~CoreDumpWriter() { }; + + // Write the given bits of metadata we would like to associate with this core + // dump. + virtual bool writeMetadata(uint64_t timestamp) = 0; + + enum EdgePolicy : bool { + INCLUDE_EDGES = true, + EXCLUDE_EDGES = false + }; + + // Write the given `JS::ubi::Node` to the core dump. The given `EdgePolicy` + // dictates whether its outgoing edges should also be written to the core + // dump, or excluded. + virtual bool writeNode(const JS::ubi::Node& node, + EdgePolicy includeEdges) = 0; +}; + +// Serialize the heap graph as seen from `node` with the given `CoreDumpWriter`. +// If `wantNames` is true, capture edge names. If `zones` is non-null, only +// capture the sub-graph within the zone set, otherwise capture the whole heap +// graph. Returns false on failure. +bool +WriteHeapGraph(JSContext* cx, + const JS::ubi::Node& node, + CoreDumpWriter& writer, + bool wantNames, + JS::CompartmentSet* compartments, + JS::AutoCheckCannotGC& noGC); + +// Get the mozilla::MallocSizeOf for the current thread's JSRuntime. +MallocSizeOf GetCurrentThreadDebuggerMallocSizeOf(); + +} // namespace devtools +} // namespace mozilla + +#endif // mozilla_devtools_HeapSnapshot__ diff --git a/dom/heapsnapshot/HeapSnapshotFileUtils.js b/dom/heapsnapshot/HeapSnapshotFileUtils.js new file mode 100644 index 000000000..abd44fc30 --- /dev/null +++ b/dom/heapsnapshot/HeapSnapshotFileUtils.js @@ -0,0 +1,95 @@ +/* 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); +}; diff --git a/dom/heapsnapshot/HeapSnapshotTempFileHelperChild.h b/dom/heapsnapshot/HeapSnapshotTempFileHelperChild.h new file mode 100644 index 000000000..a1d433a5e --- /dev/null +++ b/dom/heapsnapshot/HeapSnapshotTempFileHelperChild.h @@ -0,0 +1,32 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim: set sw=4 ts=8 et tw=80 : */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_devtools_HeapSnapshotTempFileHelperChild_h +#define mozilla_devtools_HeapSnapshotTempFileHelperChild_h + +#include "mozilla/devtools/PHeapSnapshotTempFileHelperChild.h" + +namespace mozilla { +namespace devtools { + +class HeapSnapshotTempFileHelperChild : public PHeapSnapshotTempFileHelperChild +{ + explicit HeapSnapshotTempFileHelperChild() { } + +public: + static inline PHeapSnapshotTempFileHelperChild* Create(); +}; + +/* static */ inline PHeapSnapshotTempFileHelperChild* +HeapSnapshotTempFileHelperChild::Create() +{ + return new HeapSnapshotTempFileHelperChild(); +} + +} // namespace devtools +} // namespace mozilla + +#endif // mozilla_devtools_HeapSnapshotTempFileHelperChild_h diff --git a/dom/heapsnapshot/HeapSnapshotTempFileHelperParent.cpp b/dom/heapsnapshot/HeapSnapshotTempFileHelperParent.cpp new file mode 100644 index 000000000..7246a9daa --- /dev/null +++ b/dom/heapsnapshot/HeapSnapshotTempFileHelperParent.cpp @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim: set sw=4 ts=8 et tw=80 : */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/devtools/HeapSnapshot.h" +#include "mozilla/devtools/HeapSnapshotTempFileHelperParent.h" +#include "mozilla/ErrorResult.h" +#include "private/pprio.h" + +#include "nsIFile.h" + +namespace mozilla { +namespace devtools { + +using ipc::FileDescriptor; + +static bool +openFileFailure(ErrorResult& rv, + OpenHeapSnapshotTempFileResponse* outResponse) +{ + *outResponse = rv.StealNSResult(); + return true; +} + +bool +HeapSnapshotTempFileHelperParent::RecvOpenHeapSnapshotTempFile( + OpenHeapSnapshotTempFileResponse* outResponse) +{ + auto start = TimeStamp::Now(); + ErrorResult rv; + nsAutoString filePath; + nsCOMPtr<nsIFile> file = HeapSnapshot::CreateUniqueCoreDumpFile(rv, + start, + filePath); + if (NS_WARN_IF(rv.Failed())) + return openFileFailure(rv, outResponse); + + PRFileDesc* prfd; + rv = file->OpenNSPRFileDesc(PR_WRONLY, 0, &prfd); + if (NS_WARN_IF(rv.Failed())) + return openFileFailure(rv, outResponse); + + FileDescriptor::PlatformHandleType handle = + FileDescriptor::PlatformHandleType(PR_FileDesc2NativeHandle(prfd)); + FileDescriptor fd(handle); + *outResponse = OpenedFile(filePath, fd); + return true; +} + +} // namespace devtools +} // namespace mozilla diff --git a/dom/heapsnapshot/HeapSnapshotTempFileHelperParent.h b/dom/heapsnapshot/HeapSnapshotTempFileHelperParent.h new file mode 100644 index 000000000..1582279da --- /dev/null +++ b/dom/heapsnapshot/HeapSnapshotTempFileHelperParent.h @@ -0,0 +1,35 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim: set sw=4 ts=8 et tw=80 : */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_devtools_HeapSnapshotTempFileHelperParent_h +#define mozilla_devtools_HeapSnapshotTempFileHelperParent_h + +#include "mozilla/devtools/PHeapSnapshotTempFileHelperParent.h" + +namespace mozilla { +namespace devtools { + +class HeapSnapshotTempFileHelperParent : public PHeapSnapshotTempFileHelperParent +{ + explicit HeapSnapshotTempFileHelperParent() { } + void ActorDestroy(ActorDestroyReason why) override { } + bool RecvOpenHeapSnapshotTempFile(OpenHeapSnapshotTempFileResponse* outResponse) + override; + + public: + static inline PHeapSnapshotTempFileHelperParent* Create(); +}; + +/* static */ inline PHeapSnapshotTempFileHelperParent* +HeapSnapshotTempFileHelperParent::Create() +{ + return new HeapSnapshotTempFileHelperParent(); +} + +} // namespace devtools +} // namespace mozilla + +#endif // mozilla_devtools_HeapSnapshotTempFileHelperParent_h diff --git a/dom/heapsnapshot/PHeapSnapshotTempFileHelper.ipdl b/dom/heapsnapshot/PHeapSnapshotTempFileHelper.ipdl new file mode 100644 index 000000000..2576470e2 --- /dev/null +++ b/dom/heapsnapshot/PHeapSnapshotTempFileHelper.ipdl @@ -0,0 +1,35 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include protocol PContent; + +namespace mozilla { +namespace devtools { + +struct OpenedFile +{ + nsString path; + FileDescriptor descriptor; +}; + +union OpenHeapSnapshotTempFileResponse +{ + nsresult; + OpenedFile; +}; + +sync protocol PHeapSnapshotTempFileHelper +{ + manager PContent; + +parent: + sync OpenHeapSnapshotTempFile() returns (OpenHeapSnapshotTempFileResponse response); + + async __delete__(); +}; + +} // namespace devtools +} // namespace mozilla diff --git a/dom/heapsnapshot/ZeroCopyNSIOutputStream.cpp b/dom/heapsnapshot/ZeroCopyNSIOutputStream.cpp new file mode 100644 index 000000000..0c29db7f9 --- /dev/null +++ b/dom/heapsnapshot/ZeroCopyNSIOutputStream.cpp @@ -0,0 +1,100 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/devtools/ZeroCopyNSIOutputStream.h" + +#include "mozilla/DebugOnly.h" +#include "mozilla/Unused.h" + +namespace mozilla { +namespace devtools { + +ZeroCopyNSIOutputStream::ZeroCopyNSIOutputStream(nsCOMPtr<nsIOutputStream>& out) + : out(out) + , result_(NS_OK) + , amountUsed(0) + , writtenCount(0) +{ + DebugOnly<bool> nonBlocking = false; + MOZ_ASSERT(out->IsNonBlocking(&nonBlocking) == NS_OK); + MOZ_ASSERT(!nonBlocking); +} + +ZeroCopyNSIOutputStream::~ZeroCopyNSIOutputStream() +{ + if (!failed()) + Unused << NS_WARN_IF(NS_FAILED(writeBuffer())); +} + +nsresult +ZeroCopyNSIOutputStream::writeBuffer() +{ + if (failed()) + return result_; + + if (amountUsed == 0) + return NS_OK; + + int32_t amountWritten = 0; + while (amountWritten < amountUsed) { + uint32_t justWritten = 0; + + result_ = out->Write(buffer + amountWritten, + amountUsed - amountWritten, + &justWritten); + if (NS_WARN_IF(NS_FAILED(result_))) + return result_; + + amountWritten += justWritten; + } + + writtenCount += amountUsed; + amountUsed = 0; + return NS_OK; +} + +// ZeroCopyOutputStream Interface + +bool +ZeroCopyNSIOutputStream::Next(void** data, int* size) +{ + MOZ_ASSERT(data != nullptr); + MOZ_ASSERT(size != nullptr); + + if (failed()) + return false; + + if (amountUsed == BUFFER_SIZE) { + if (NS_FAILED(writeBuffer())) + return false; + } + + *data = buffer + amountUsed; + *size = BUFFER_SIZE - amountUsed; + amountUsed = BUFFER_SIZE; + return true; +} + +void +ZeroCopyNSIOutputStream::BackUp(int count) +{ + MOZ_ASSERT(count >= 0, + "Cannot back up a negative amount of bytes."); + MOZ_ASSERT(amountUsed == BUFFER_SIZE, + "Can only call BackUp directly after calling Next."); + MOZ_ASSERT(count <= amountUsed, + "Can't back up further than we've given out."); + + amountUsed -= count; +} + +::google::protobuf::int64 +ZeroCopyNSIOutputStream::ByteCount() const +{ + return writtenCount + amountUsed; +} + +} // namespace devtools +} // namespace mozilla diff --git a/dom/heapsnapshot/ZeroCopyNSIOutputStream.h b/dom/heapsnapshot/ZeroCopyNSIOutputStream.h new file mode 100644 index 000000000..117fc0f87 --- /dev/null +++ b/dom/heapsnapshot/ZeroCopyNSIOutputStream.h @@ -0,0 +1,70 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_devtools_ZeroCopyNSIOutputStream__ +#define mozilla_devtools_ZeroCopyNSIOutputStream__ + +#include <google/protobuf/io/zero_copy_stream.h> +#include <google/protobuf/stubs/common.h> + +#include "nsCOMPtr.h" +#include "nsIOutputStream.h" + +namespace mozilla { +namespace devtools { + +// A `google::protobuf::io::ZeroCopyOutputStream` implementation that uses an +// `nsIOutputStream` under the covers. +// +// This class will automatically write and flush its data to the +// `nsIOutputStream` in its destructor, but if you care whether that call +// succeeds or fails, then you should call the `flush` method yourself. Errors +// will be logged, however. +class MOZ_STACK_CLASS ZeroCopyNSIOutputStream + : public ::google::protobuf::io::ZeroCopyOutputStream +{ + static const int BUFFER_SIZE = 8192; + + // The nsIOutputStream we are streaming to. + nsCOMPtr<nsIOutputStream>& out; + + // The buffer we write data to before passing it to the output stream. + char buffer[BUFFER_SIZE]; + + // The status of writing to the underlying output stream. + nsresult result_; + + // The number of bytes in the buffer that have been used thus far. + int amountUsed; + + // Excluding the amount of the buffer currently used (which hasn't been + // written and flushed yet), this is the number of bytes written to the output + // stream. + int64_t writtenCount; + + // Write the internal buffer to the output stream and flush it. + nsresult writeBuffer(); + +public: + explicit ZeroCopyNSIOutputStream(nsCOMPtr<nsIOutputStream>& out); + + nsresult flush() { return writeBuffer(); } + + // Return true if writing to the underlying output stream ever failed. + bool failed() const { return NS_FAILED(result_); } + + nsresult result() const { return result_; } + + // ZeroCopyOutputStream Interface + virtual ~ZeroCopyNSIOutputStream() override; + virtual bool Next(void** data, int* size) override; + virtual void BackUp(int count) override; + virtual ::google::protobuf::int64 ByteCount() const override; +}; + +} // namespace devtools +} // namespace mozilla + +#endif // mozilla_devtools_ZeroCopyNSIOutputStream__ diff --git a/dom/heapsnapshot/census-tree-node.js b/dom/heapsnapshot/census-tree-node.js new file mode 100644 index 000000000..b041e77f9 --- /dev/null +++ b/dom/heapsnapshot/census-tree-node.js @@ -0,0 +1,748 @@ +/* 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; +}; diff --git a/dom/heapsnapshot/generate-core-dump-sources.sh b/dom/heapsnapshot/generate-core-dump-sources.sh new file mode 100644 index 000000000..97e492ff0 --- /dev/null +++ b/dom/heapsnapshot/generate-core-dump-sources.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +# A script to generate devtools/server/CoreDump.pb.{h,cc} from +# devtools/server/CoreDump.proto. This script assumes you have +# downloaded and installed the protocol buffer compiler, and that it is either +# on your $PATH or located at $PROTOC_PATH. +# +# These files were last compiled with libprotoc 2.4.1. + +set -e + +cd $(dirname $0) + +if [ -n $PROTOC_PATH ]; then + PROTOC_PATH=`which protoc` +fi + +if [ ! -e $PROTOC_PATH ]; then + echo You must install the protocol compiler from + echo https://code.google.com/p/protobuf/downloads/list + exit 1 +fi + +echo Using $PROTOC_PATH as the protocol compiler + +$PROTOC_PATH --cpp_out="." CoreDump.proto diff --git a/dom/heapsnapshot/moz.build b/dom/heapsnapshot/moz.build new file mode 100644 index 000000000..fa9ef3915 --- /dev/null +++ b/dom/heapsnapshot/moz.build @@ -0,0 +1,63 @@ +# -*- 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'] diff --git a/dom/heapsnapshot/shortest-paths.js b/dom/heapsnapshot/shortest-paths.js new file mode 100644 index 000000000..2d97b7de9 --- /dev/null +++ b/dom/heapsnapshot/shortest-paths.js @@ -0,0 +1,91 @@ +/* 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 }; +}; diff --git a/dom/heapsnapshot/tests/gtest/DeserializedNodeUbiNodes.cpp b/dom/heapsnapshot/tests/gtest/DeserializedNodeUbiNodes.cpp new file mode 100644 index 000000000..e236a0acf --- /dev/null +++ b/dom/heapsnapshot/tests/gtest/DeserializedNodeUbiNodes.cpp @@ -0,0 +1,100 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Test that the `JS::ubi::Node`s we create from +// `mozilla::devtools::DeserializedNode` instances look and behave as we would +// like. + +#include "DevTools.h" +#include "js/TypeDecls.h" +#include "mozilla/devtools/DeserializedNode.h" + +using testing::Field; +using testing::ReturnRef; + +// A mock DeserializedNode for testing. +struct MockDeserializedNode : public DeserializedNode +{ + MockDeserializedNode(NodeId id, const char16_t* typeName, uint64_t size) + : DeserializedNode(id, typeName, size) + { } + + bool addEdge(DeserializedEdge&& edge) + { + return edges.append(Move(edge)); + } + + MOCK_METHOD1(getEdgeReferent, JS::ubi::Node(const DeserializedEdge&)); +}; + +size_t fakeMallocSizeOf(const void*) { + EXPECT_TRUE(false); + MOZ_ASSERT_UNREACHABLE("fakeMallocSizeOf should never be called because " + "DeserializedNodes report the deserialized size."); + return 0; +} + +DEF_TEST(DeserializedNodeUbiNodes, { + const char16_t* typeName = u"TestTypeName"; + const char* className = "MyObjectClassName"; + const char* filename = "my-cool-filename.js"; + + NodeId id = uint64_t(1) << 33; + uint64_t size = uint64_t(1) << 60; + MockDeserializedNode mocked(id, typeName, size); + mocked.coarseType = JS::ubi::CoarseType::Script; + mocked.jsObjectClassName = className; + mocked.scriptFilename = filename; + + DeserializedNode& deserialized = mocked; + JS::ubi::Node ubi(&deserialized); + + // Test the ubi::Node accessors. + + EXPECT_EQ(size, ubi.size(fakeMallocSizeOf)); + EXPECT_EQ(typeName, ubi.typeName()); + EXPECT_EQ(JS::ubi::CoarseType::Script, ubi.coarseType()); + EXPECT_EQ(id, ubi.identifier()); + EXPECT_FALSE(ubi.isLive()); + EXPECT_EQ(ubi.jsObjectClassName(), className); + EXPECT_EQ(ubi.scriptFilename(), filename); + + // Test the ubi::Node's edges. + + UniquePtr<DeserializedNode> referent1(new MockDeserializedNode(1, + nullptr, + 10)); + DeserializedEdge edge1(referent1->id); + mocked.addEdge(Move(edge1)); + EXPECT_CALL(mocked, getEdgeReferent(EdgeTo(referent1->id))) + .Times(1) + .WillOnce(Return(JS::ubi::Node(referent1.get()))); + + UniquePtr<DeserializedNode> referent2(new MockDeserializedNode(2, + nullptr, + 20)); + DeserializedEdge edge2(referent2->id); + mocked.addEdge(Move(edge2)); + EXPECT_CALL(mocked, getEdgeReferent(EdgeTo(referent2->id))) + .Times(1) + .WillOnce(Return(JS::ubi::Node(referent2.get()))); + + UniquePtr<DeserializedNode> referent3(new MockDeserializedNode(3, + nullptr, + 30)); + DeserializedEdge edge3(referent3->id); + mocked.addEdge(Move(edge3)); + EXPECT_CALL(mocked, getEdgeReferent(EdgeTo(referent3->id))) + .Times(1) + .WillOnce(Return(JS::ubi::Node(referent3.get()))); + + auto range = ubi.edges(cx); + ASSERT_TRUE(!!range); + + for ( ; !range->empty(); range->popFront()) { + // Nothing to do here. This loop ensures that we get each edge referent + // that we expect above. + } + }); diff --git a/dom/heapsnapshot/tests/gtest/DeserializedStackFrameUbiStackFrames.cpp b/dom/heapsnapshot/tests/gtest/DeserializedStackFrameUbiStackFrames.cpp new file mode 100644 index 000000000..72e363934 --- /dev/null +++ b/dom/heapsnapshot/tests/gtest/DeserializedStackFrameUbiStackFrames.cpp @@ -0,0 +1,91 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Test that the `JS::ubi::StackFrame`s we create from +// `mozilla::devtools::DeserializedStackFrame` instances look and behave as we would +// like. + +#include "DevTools.h" +#include "js/TypeDecls.h" +#include "mozilla/devtools/DeserializedNode.h" + +using testing::Field; +using testing::ReturnRef; + +// A mock DeserializedStackFrame for testing. +struct MockDeserializedStackFrame : public DeserializedStackFrame +{ + MockDeserializedStackFrame() : DeserializedStackFrame() { } +}; + +DEF_TEST(DeserializedStackFrameUbiStackFrames, { + StackFrameId id = uint64_t(1) << 42; + uint32_t line = 1337; + uint32_t column = 9; // 3 space tabs!? + const char16_t* source = u"my-javascript-file.js"; + const char16_t* functionDisplayName = u"myFunctionName"; + + MockDeserializedStackFrame mocked; + mocked.id = id; + mocked.line = line; + mocked.column = column; + mocked.source = source; + mocked.functionDisplayName = functionDisplayName; + + DeserializedStackFrame& deserialized = mocked; + JS::ubi::StackFrame ubiFrame(&deserialized); + + // Test the JS::ubi::StackFrame accessors. + + EXPECT_EQ(id, ubiFrame.identifier()); + EXPECT_EQ(JS::ubi::StackFrame(), ubiFrame.parent()); + EXPECT_EQ(line, ubiFrame.line()); + EXPECT_EQ(column, ubiFrame.column()); + EXPECT_EQ(JS::ubi::AtomOrTwoByteChars(source), ubiFrame.source()); + EXPECT_EQ(JS::ubi::AtomOrTwoByteChars(functionDisplayName), + ubiFrame.functionDisplayName()); + EXPECT_FALSE(ubiFrame.isSelfHosted(cx)); + EXPECT_FALSE(ubiFrame.isSystem()); + + JS::RootedObject savedFrame(cx); + EXPECT_TRUE(ubiFrame.constructSavedFrameStack(cx, &savedFrame)); + + uint32_t frameLine; + ASSERT_EQ(JS::SavedFrameResult::Ok, JS::GetSavedFrameLine(cx, savedFrame, &frameLine)); + EXPECT_EQ(line, frameLine); + + uint32_t frameColumn; + ASSERT_EQ(JS::SavedFrameResult::Ok, JS::GetSavedFrameColumn(cx, savedFrame, &frameColumn)); + EXPECT_EQ(column, frameColumn); + + JS::RootedObject parent(cx); + ASSERT_EQ(JS::SavedFrameResult::Ok, JS::GetSavedFrameParent(cx, savedFrame, &parent)); + EXPECT_EQ(nullptr, parent); + + ASSERT_EQ(NS_strlen(source), 21U); + char16_t sourceBuf[21] = {}; + + // Test when the length is shorter than the string length. + auto written = ubiFrame.source(RangedPtr<char16_t>(sourceBuf), 3); + EXPECT_EQ(written, 3U); + for (size_t i = 0; i < 3; i++) { + EXPECT_EQ(sourceBuf[i], source[i]); + } + + written = ubiFrame.source(RangedPtr<char16_t>(sourceBuf), 21); + EXPECT_EQ(written, 21U); + for (size_t i = 0; i < 21; i++) { + EXPECT_EQ(sourceBuf[i], source[i]); + } + + ASSERT_EQ(NS_strlen(functionDisplayName), 14U); + char16_t nameBuf[14] = {}; + + written = ubiFrame.functionDisplayName(RangedPtr<char16_t>(nameBuf), 14); + EXPECT_EQ(written, 14U); + for (size_t i = 0; i < 14; i++) { + EXPECT_EQ(nameBuf[i], functionDisplayName[i]); + } +}); diff --git a/dom/heapsnapshot/tests/gtest/DevTools.h b/dom/heapsnapshot/tests/gtest/DevTools.h new file mode 100644 index 000000000..6eb5cfe21 --- /dev/null +++ b/dom/heapsnapshot/tests/gtest/DevTools.h @@ -0,0 +1,276 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_devtools_gtest_DevTools__ +#define mozilla_devtools_gtest_DevTools__ + +#include "CoreDump.pb.h" +#include "jsapi.h" +#include "jspubtd.h" +#include "nsCRTGlue.h" + +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "mozilla/devtools/HeapSnapshot.h" +#include "mozilla/dom/ChromeUtils.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/Move.h" +#include "js/Principals.h" +#include "js/UbiNode.h" +#include "js/UniquePtr.h" + +using namespace mozilla; +using namespace mozilla::devtools; +using namespace mozilla::dom; +using namespace testing; + +// GTest fixture class that all of our tests derive from. +struct DevTools : public ::testing::Test { + bool _initialized; + JSContext* cx; + JSCompartment* compartment; + JS::Zone* zone; + JS::PersistentRootedObject global; + + DevTools() + : _initialized(false), + cx(nullptr) + { } + + virtual void SetUp() { + MOZ_ASSERT(!_initialized); + + cx = getContext(); + if (!cx) + return; + + JS_BeginRequest(cx); + + global.init(cx, createGlobal()); + if (!global) + return; + JS_EnterCompartment(cx, global); + + compartment = js::GetContextCompartment(cx); + zone = js::GetContextZone(cx); + + _initialized = true; + } + + JSContext* getContext() { + return CycleCollectedJSContext::Get()->Context(); + } + + static void reportError(JSContext* cx, const char* message, JSErrorReport* report) { + fprintf(stderr, "%s:%u:%s\n", + report->filename ? report->filename : "<no filename>", + (unsigned int) report->lineno, + message); + } + + static const JSClass* getGlobalClass() { + static const JSClassOps globalClassOps = { + nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, + JS_GlobalObjectTraceHook + }; + static const JSClass globalClass = { + "global", JSCLASS_GLOBAL_FLAGS, + &globalClassOps + }; + return &globalClass; + } + + JSObject* createGlobal() + { + /* Create the global object. */ + JS::RootedObject newGlobal(cx); + JS::CompartmentOptions options; + options.behaviors().setVersion(JSVERSION_LATEST); + newGlobal = JS_NewGlobalObject(cx, getGlobalClass(), nullptr, + JS::FireOnNewGlobalHook, options); + if (!newGlobal) + return nullptr; + + JSAutoCompartment ac(cx, newGlobal); + + /* Populate the global object with the standard globals, like Object and + Array. */ + if (!JS_InitStandardClasses(cx, newGlobal)) + return nullptr; + + return newGlobal; + } + + virtual void TearDown() { + _initialized = false; + + if (global) { + JS_LeaveCompartment(cx, nullptr); + global = nullptr; + } + if (cx) + JS_EndRequest(cx); + } +}; + + +// Helper to define a test and ensure that the fixture is initialized properly. +#define DEF_TEST(name, body) \ + TEST_F(DevTools, name) { \ + ASSERT_TRUE(_initialized); \ + body \ + } + + +// Fake JS::ubi::Node implementation +class MOZ_STACK_CLASS FakeNode +{ +public: + JS::ubi::EdgeVector edges; + JSCompartment* compartment; + JS::Zone* zone; + size_t size; + + explicit FakeNode() + : edges(), + compartment(nullptr), + zone(nullptr), + size(1) + { } +}; + +namespace JS { +namespace ubi { + +template<> +class Concrete<FakeNode> : public Base +{ + const char16_t* typeName() const override { + return concreteTypeName; + } + + js::UniquePtr<EdgeRange> edges(JSContext*, bool) const override { + return js::UniquePtr<EdgeRange>(js_new<PreComputedEdgeRange>(get().edges)); + } + + Size size(mozilla::MallocSizeOf) const override { + return get().size; + } + + JS::Zone* zone() const override { + return get().zone; + } + + JSCompartment* compartment() const override { + return get().compartment; + } + +protected: + explicit Concrete(FakeNode* ptr) : Base(ptr) { } + FakeNode& get() const { return *static_cast<FakeNode*>(ptr); } + +public: + static const char16_t concreteTypeName[]; + static void construct(void* storage, FakeNode* ptr) { + new (storage) Concrete(ptr); + } +}; + +const char16_t Concrete<FakeNode>::concreteTypeName[] = u"FakeNode"; + +} // namespace ubi +} // namespace JS + +void AddEdge(FakeNode& node, FakeNode& referent, const char16_t* edgeName = nullptr) { + char16_t* ownedEdgeName = nullptr; + if (edgeName) { + ownedEdgeName = NS_strdup(edgeName); + ASSERT_NE(ownedEdgeName, nullptr); + } + + JS::ubi::Edge edge(ownedEdgeName, &referent); + ASSERT_TRUE(node.edges.append(mozilla::Move(edge))); +} + + +// Custom GMock Matchers + +// Use the testing namespace to avoid static analysis failures in the gmock +// matcher classes that get generated from MATCHER_P macros. +namespace testing { + +// Ensure that given node has the expected number of edges. +MATCHER_P2(EdgesLength, cx, expectedLength, "") { + auto edges = arg.edges(cx); + if (!edges) + return false; + + int actualLength = 0; + for ( ; !edges->empty(); edges->popFront()) + actualLength++; + + return Matcher<int>(Eq(expectedLength)) + .MatchAndExplain(actualLength, result_listener); +} + +// Get the nth edge and match it with the given matcher. +MATCHER_P3(Edge, cx, n, matcher, "") { + auto edges = arg.edges(cx); + if (!edges) + return false; + + int i = 0; + for ( ; !edges->empty(); edges->popFront()) { + if (i == n) { + return Matcher<const JS::ubi::Edge&>(matcher) + .MatchAndExplain(edges->front(), result_listener); + } + + i++; + } + + return false; +} + +// Ensures that two char16_t* strings are equal. +MATCHER_P(UTF16StrEq, str, "") { + return NS_strcmp(arg, str) == 0; +} + +MATCHER_P(UniqueUTF16StrEq, str, "") { + return NS_strcmp(arg.get(), str) == 0; +} + +MATCHER(UniqueIsNull, "") { + return arg.get() == nullptr; +} + +// Matches an edge whose referent is the node with the given id. +MATCHER_P(EdgeTo, id, "") { + return Matcher<const DeserializedEdge&>(Field(&DeserializedEdge::referent, id)) + .MatchAndExplain(arg, result_listener); +} + +} // namespace testing + + +// A mock `Writer` class to be used with testing `WriteHeapGraph`. +class MockWriter : public CoreDumpWriter +{ +public: + virtual ~MockWriter() override { } + MOCK_METHOD2(writeNode, bool(const JS::ubi::Node&, CoreDumpWriter::EdgePolicy)); + MOCK_METHOD1(writeMetadata, bool(uint64_t)); +}; + +void ExpectWriteNode(MockWriter& writer, FakeNode& node) { + EXPECT_CALL(writer, writeNode(Eq(JS::ubi::Node(&node)), _)) + .Times(1) + .WillOnce(Return(true)); +} + +#endif // mozilla_devtools_gtest_DevTools__ diff --git a/dom/heapsnapshot/tests/gtest/DoesCrossCompartmentBoundaries.cpp b/dom/heapsnapshot/tests/gtest/DoesCrossCompartmentBoundaries.cpp new file mode 100644 index 000000000..bc517d6d9 --- /dev/null +++ b/dom/heapsnapshot/tests/gtest/DoesCrossCompartmentBoundaries.cpp @@ -0,0 +1,73 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Test that heap snapshots cross compartment boundaries when expected. + +#include "DevTools.h" + +DEF_TEST(DoesCrossCompartmentBoundaries, { + // Create a new global to get a new compartment. + JS::CompartmentOptions options; + JS::RootedObject newGlobal(cx, JS_NewGlobalObject(cx, + getGlobalClass(), + nullptr, + JS::FireOnNewGlobalHook, + options)); + ASSERT_TRUE(newGlobal); + JSCompartment* newCompartment = nullptr; + { + JSAutoCompartment ac(cx, newGlobal); + ASSERT_TRUE(JS_InitStandardClasses(cx, newGlobal)); + newCompartment = js::GetContextCompartment(cx); + } + ASSERT_TRUE(newCompartment); + ASSERT_NE(newCompartment, compartment); + + // Our set of target compartments is both the old and new compartments. + JS::CompartmentSet targetCompartments; + ASSERT_TRUE(targetCompartments.init()); + ASSERT_TRUE(targetCompartments.put(compartment)); + ASSERT_TRUE(targetCompartments.put(newCompartment)); + + FakeNode nodeA; + FakeNode nodeB; + FakeNode nodeC; + FakeNode nodeD; + + nodeA.compartment = compartment; + nodeB.compartment = nullptr; + nodeC.compartment = newCompartment; + nodeD.compartment = nullptr; + + AddEdge(nodeA, nodeB); + AddEdge(nodeA, nodeC); + AddEdge(nodeB, nodeD); + + ::testing::NiceMock<MockWriter> writer; + + // Should serialize nodeA, because it is in one of our target compartments. + ExpectWriteNode(writer, nodeA); + + // Should serialize nodeB, because it doesn't belong to a compartment and is + // therefore assumed to be shared. + ExpectWriteNode(writer, nodeB); + + // Should also serialize nodeC, which is in our target compartments, but a + // different compartment than A. + ExpectWriteNode(writer, nodeC); + + // Should serialize nodeD because it's reachable via B and both nodes B and D + // don't belong to a specific compartment. + ExpectWriteNode(writer, nodeD); + + JS::AutoCheckCannotGC noGC(cx); + + ASSERT_TRUE(WriteHeapGraph(cx, + JS::ubi::Node(&nodeA), + writer, + /* wantNames = */ false, + &targetCompartments, + noGC)); + }); diff --git a/dom/heapsnapshot/tests/gtest/DoesntCrossCompartmentBoundaries.cpp b/dom/heapsnapshot/tests/gtest/DoesntCrossCompartmentBoundaries.cpp new file mode 100644 index 000000000..2fe5e6ace --- /dev/null +++ b/dom/heapsnapshot/tests/gtest/DoesntCrossCompartmentBoundaries.cpp @@ -0,0 +1,64 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Test that heap snapshots walk the compartment boundaries correctly. + +#include "DevTools.h" + +DEF_TEST(DoesntCrossCompartmentBoundaries, { + // Create a new global to get a new compartment. + JS::CompartmentOptions options; + JS::RootedObject newGlobal(cx, JS_NewGlobalObject(cx, + getGlobalClass(), + nullptr, + JS::FireOnNewGlobalHook, + options)); + ASSERT_TRUE(newGlobal); + JSCompartment* newCompartment = nullptr; + { + JSAutoCompartment ac(cx, newGlobal); + ASSERT_TRUE(JS_InitStandardClasses(cx, newGlobal)); + newCompartment = js::GetContextCompartment(cx); + } + ASSERT_TRUE(newCompartment); + ASSERT_NE(newCompartment, compartment); + + // Our set of target compartments is only the pre-existing compartment and + // does not include the new compartment. + JS::CompartmentSet targetCompartments; + ASSERT_TRUE(targetCompartments.init()); + ASSERT_TRUE(targetCompartments.put(compartment)); + + FakeNode nodeA; + FakeNode nodeB; + FakeNode nodeC; + + nodeA.compartment = compartment; + nodeB.compartment = nullptr; + nodeC.compartment = newCompartment; + + AddEdge(nodeA, nodeB); + AddEdge(nodeB, nodeC); + + ::testing::NiceMock<MockWriter> writer; + + // Should serialize nodeA, because it is in our target compartments. + ExpectWriteNode(writer, nodeA); + + // Should serialize nodeB, because it doesn't belong to a compartment and is + // therefore assumed to be shared. + ExpectWriteNode(writer, nodeB); + + // But we shouldn't ever serialize nodeC. + + JS::AutoCheckCannotGC noGC(cx); + + ASSERT_TRUE(WriteHeapGraph(cx, + JS::ubi::Node(&nodeA), + writer, + /* wantNames = */ false, + &targetCompartments, + noGC)); + }); diff --git a/dom/heapsnapshot/tests/gtest/SerializesEdgeNames.cpp b/dom/heapsnapshot/tests/gtest/SerializesEdgeNames.cpp new file mode 100644 index 000000000..be135dbb4 --- /dev/null +++ b/dom/heapsnapshot/tests/gtest/SerializesEdgeNames.cpp @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Test that edge names get serialized correctly. + +#include "DevTools.h" + +using testing::Field; +using testing::IsNull; +using testing::Property; +using testing::Return; + +DEF_TEST(SerializesEdgeNames, { + FakeNode node; + FakeNode referent; + + const char16_t edgeName[] = u"edge name"; + const char16_t emptyStr[] = u""; + + AddEdge(node, referent, edgeName); + AddEdge(node, referent, emptyStr); + AddEdge(node, referent, nullptr); + + ::testing::NiceMock<MockWriter> writer; + + // Should get the node with edges once. + EXPECT_CALL( + writer, + writeNode(AllOf(EdgesLength(cx, 3), + Edge(cx, 0, Field(&JS::ubi::Edge::name, + UniqueUTF16StrEq(edgeName))), + Edge(cx, 1, Field(&JS::ubi::Edge::name, + UniqueUTF16StrEq(emptyStr))), + Edge(cx, 2, Field(&JS::ubi::Edge::name, + UniqueIsNull()))), + _) + ) + .Times(1) + .WillOnce(Return(true)); + + // Should get the referent node that doesn't have any edges once. + ExpectWriteNode(writer, referent); + + JS::AutoCheckCannotGC noGC(cx); + ASSERT_TRUE(WriteHeapGraph(cx, + JS::ubi::Node(&node), + writer, + /* wantNames = */ true, + /* zones = */ nullptr, + noGC)); + }); diff --git a/dom/heapsnapshot/tests/gtest/SerializesEverythingInHeapGraphOnce.cpp b/dom/heapsnapshot/tests/gtest/SerializesEverythingInHeapGraphOnce.cpp new file mode 100644 index 000000000..475442df8 --- /dev/null +++ b/dom/heapsnapshot/tests/gtest/SerializesEverythingInHeapGraphOnce.cpp @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Test that everything in the heap graph gets serialized once, and only once. + +#include "DevTools.h" + +DEF_TEST(SerializesEverythingInHeapGraphOnce, { + FakeNode nodeA; + FakeNode nodeB; + FakeNode nodeC; + FakeNode nodeD; + + AddEdge(nodeA, nodeB); + AddEdge(nodeB, nodeC); + AddEdge(nodeC, nodeD); + AddEdge(nodeD, nodeA); + + ::testing::NiceMock<MockWriter> writer; + + // Should serialize each node once. + ExpectWriteNode(writer, nodeA); + ExpectWriteNode(writer, nodeB); + ExpectWriteNode(writer, nodeC); + ExpectWriteNode(writer, nodeD); + + JS::AutoCheckCannotGC noGC(cx); + + ASSERT_TRUE(WriteHeapGraph(cx, + JS::ubi::Node(&nodeA), + writer, + /* wantNames = */ false, + /* zones = */ nullptr, + noGC)); + }); diff --git a/dom/heapsnapshot/tests/gtest/SerializesTypeNames.cpp b/dom/heapsnapshot/tests/gtest/SerializesTypeNames.cpp new file mode 100644 index 000000000..a259c297b --- /dev/null +++ b/dom/heapsnapshot/tests/gtest/SerializesTypeNames.cpp @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Test that a ubi::Node's typeName gets properly serialized into a core dump. + +#include "DevTools.h" + +using testing::Property; +using testing::Return; + +DEF_TEST(SerializesTypeNames, { + FakeNode node; + + ::testing::NiceMock<MockWriter> writer; + EXPECT_CALL(writer, writeNode(Property(&JS::ubi::Node::typeName, + UTF16StrEq(u"FakeNode")), + _)) + .Times(1) + .WillOnce(Return(true)); + + JS::AutoCheckCannotGC noGC(cx); + ASSERT_TRUE(WriteHeapGraph(cx, + JS::ubi::Node(&node), + writer, + /* wantNames = */ true, + /* zones = */ nullptr, + noGC)); + }); diff --git a/dom/heapsnapshot/tests/gtest/moz.build b/dom/heapsnapshot/tests/gtest/moz.build new file mode 100644 index 000000000..08c31e47c --- /dev/null +++ b/dom/heapsnapshot/tests/gtest/moz.build @@ -0,0 +1,31 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, you can obtain one at http://mozilla.org/MPL/2.0/. + +Library('devtoolstests') + +LOCAL_INCLUDES += [ + '../..', +] + +UNIFIED_SOURCES = [ + 'DeserializedNodeUbiNodes.cpp', + 'DeserializedStackFrameUbiStackFrames.cpp', + 'DoesCrossCompartmentBoundaries.cpp', + 'DoesntCrossCompartmentBoundaries.cpp', + 'SerializesEdgeNames.cpp', + 'SerializesEverythingInHeapGraphOnce.cpp', + 'SerializesTypeNames.cpp', +] + +if CONFIG['GNU_CXX']: + CXXFLAGS += ['-Wno-error=shadow'] + +# THE MOCK_METHOD2 macro from gtest triggers this clang warning and it's hard +# to work around, so we just ignore it. +if CONFIG['CLANG_CXX']: + CXXFLAGS += ['-Wno-inconsistent-missing-override'] + +FINAL_LIBRARY = 'xul-gtest' diff --git a/dom/heapsnapshot/tests/mochitest/chrome.ini b/dom/heapsnapshot/tests/mochitest/chrome.ini new file mode 100644 index 000000000..497b6fe37 --- /dev/null +++ b/dom/heapsnapshot/tests/mochitest/chrome.ini @@ -0,0 +1,8 @@ +[DEFAULT] +tags = devtools devtools-memory +skip-if = os == 'android' +support-files = + +[test_DominatorTree_01.html] +[test_SaveHeapSnapshot.html] + diff --git a/dom/heapsnapshot/tests/mochitest/mochitest.ini b/dom/heapsnapshot/tests/mochitest/mochitest.ini new file mode 100644 index 000000000..5e7aa8d10 --- /dev/null +++ b/dom/heapsnapshot/tests/mochitest/mochitest.ini @@ -0,0 +1,6 @@ +[DEFAULT] +tags = devtools devtools-memory +support-files = + +[test_saveHeapSnapshot_e10s_01.html] + diff --git a/dom/heapsnapshot/tests/mochitest/test_DominatorTree_01.html b/dom/heapsnapshot/tests/mochitest/test_DominatorTree_01.html new file mode 100644 index 000000000..1f9d8c080 --- /dev/null +++ b/dom/heapsnapshot/tests/mochitest/test_DominatorTree_01.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<!-- +Sanity test that we can compute dominator trees from a heap snapshot in a web window. +--> +<head> + <meta charset="utf-8"> + <title>ChromeUtils.saveHeapSnapshot test</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> +<script> +SimpleTest.waitForExplicitFinish(); +window.onload = function() { + const path = ChromeUtils.saveHeapSnapshot({ runtime: true }); + const snapshot = ChromeUtils.readHeapSnapshot(path); + + const dominatorTree = snapshot.computeDominatorTree(); + ok(dominatorTree); + ok(dominatorTree instanceof DominatorTree); + + let threw = false; + try { + new DominatorTree(); + } catch (e) { + threw = true; + } + ok(threw, "Constructor shouldn't be usable"); + + SimpleTest.finish(); +}; +</script> +</pre> +</body> +</html> diff --git a/dom/heapsnapshot/tests/mochitest/test_SaveHeapSnapshot.html b/dom/heapsnapshot/tests/mochitest/test_SaveHeapSnapshot.html new file mode 100644 index 000000000..f150a99c7 --- /dev/null +++ b/dom/heapsnapshot/tests/mochitest/test_SaveHeapSnapshot.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<!-- +Bug 1024774 - Sanity test that we can take a heap snapshot in a web window. +--> +<head> + <meta charset="utf-8"> + <title>ChromeUtils.saveHeapSnapshot test</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> +<script> +SimpleTest.waitForExplicitFinish(); +window.onload = function() { + ok(ChromeUtils, "The ChromeUtils interface should be exposed in chrome windows."); + ChromeUtils.saveHeapSnapshot({ runtime: true }); + ok(true, "Should save a heap snapshot and shouldn't throw."); + SimpleTest.finish(); +}; +</script> +</pre> +</body> +</html> diff --git a/dom/heapsnapshot/tests/mochitest/test_saveHeapSnapshot_e10s_01.html b/dom/heapsnapshot/tests/mochitest/test_saveHeapSnapshot_e10s_01.html new file mode 100644 index 000000000..15f88f8e0 --- /dev/null +++ b/dom/heapsnapshot/tests/mochitest/test_saveHeapSnapshot_e10s_01.html @@ -0,0 +1,82 @@ +<!DOCTYPE HTML> +<!-- +Bug 1201597 - Sanity test that we can take a heap snapshot in an e10s child process. +--> +<html> +<head> + <title>saveHeapSnapshot in e10s child processes</title> + <script type="application/javascript" + src="/tests/SimpleTest/SimpleTest.js"> + </script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <script type="application/javascript"> + window.onerror = function (msg, url, line, col, err) { + ok(false, "@" + url + ":" + line + ":" + col + ": " + msg + "\n" + err.stack); + }; + + SimpleTest.waitForExplicitFinish(); + + var childFrameURL = "data:text/html,<!DOCTYPE HTML><html><body></body></html>"; + + // This function is stringified and loaded in the child process as a frame + // script. + function childFrameScript() { + try { + ChromeUtils.saveHeapSnapshot({ runtime: true }); + } catch (err) { + sendAsyncMessage("testSaveHeapSnapshot:error", + { error: err.toString() }); + return; + } + + sendAsyncMessage("testSaveHeapSnapshot:done", {}); + } + + // Kick everything off on load. + window.onload = function () { + info("window.onload fired"); + SpecialPowers.addPermission("browser", true, document); + SpecialPowers.pushPrefEnv({ + "set": [ + ["dom.ipc.browser_frames.oop_by_default", true], + ["dom.mozBrowserFramesEnabled", true], + ["browser.pagethumbnails.capturing_disabled", true] + ] + }, function () { + var iframe = document.createElement("iframe"); + SpecialPowers.wrap(iframe).mozbrowser = true; + iframe.id = "iframe"; + iframe.src = childFrameURL; + + + iframe.addEventListener("mozbrowserloadend", function onLoadEnd() { + iframe.removeEventListener("mozbrowserloadend", onLoadEnd); + info("iframe done loading"); + + var mm = SpecialPowers.getBrowserFrameMessageManager(iframe); + + function onError(e) { + ok(false, e.data.error); + } + mm.addMessageListener("testSaveHeapSnapshot:error", onError); + + mm.addMessageListener("testSaveHeapSnapshot:done", function onMsg() { + mm.removeMessageListener("testSaveHeapSnapshot:done", onMsg); + mm.removeMessageListener("testSaveHeapSnapshot:error", onError); + ok(true, "Saved heap snapshot in child process"); + SimpleTest.finish(); + }); + + info("Loading frame script to save heap snapshot"); + mm.loadFrameScript("data:,(" + encodeURI(childFrameScript.toString()) + ")();", + false); + }); + + info("Loading iframe"); + document.body.appendChild(iframe); + }); + }; + </script> +</window> diff --git a/dom/heapsnapshot/tests/unit/.eslintrc.js b/dom/heapsnapshot/tests/unit/.eslintrc.js new file mode 100644 index 000000000..59adf410a --- /dev/null +++ b/dom/heapsnapshot/tests/unit/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the common devtools xpcshell eslintrc config. + "extends": "../../../../.eslintrc.xpcshell.js" +}; diff --git a/dom/heapsnapshot/tests/unit/Census.jsm b/dom/heapsnapshot/tests/unit/Census.jsm new file mode 100644 index 000000000..f8fb1ce44 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/Census.jsm @@ -0,0 +1,165 @@ +// Functions for checking results returned by +// Debugger.Memory.prototype.takeCensus and +// HeapSnapshot.prototype.takeCensus. Adapted from js/src/jit-test/lib/census.js. + +this.EXPORTED_SYMBOLS = ["Census"]; + +this.Census = (function () { + const Census = {}; + + function dumpn(msg) { + dump("DBG-TEST: Census.jsm: " + msg + "\n"); + } + + // Census.walkCensus(subject, name, walker) + // + // Use |walker| to check |subject|, a census object of the sort returned by + // Debugger.Memory.prototype.takeCensus: a tree of objects with integers at the + // leaves. Use |name| as the name for |subject| in diagnostic messages. Return + // the number of leaves of |subject| we visited. + // + // A walker is an object with three methods: + // + // - enter(prop): Return the walker we should use to check the property of the + // subject census named |prop|. This is for recursing into the subobjects of + // the subject. + // + // - done(): Called after we have called 'enter' on every property of the + // subject. + // + // - check(value): Check |value|, a leaf in the subject. + // + // Walker methods are expected to simply throw if a node we visit doesn't look + // right. + Census.walkCensus = (subject, name, walker) => walk(subject, name, walker, 0); + function walk(subject, name, walker, count) { + if (typeof subject === "object") { + dumpn(name); + for (let prop in subject) { + count = walk(subject[prop], + name + "[" + uneval(prop) + "]", + walker.enter(prop), + count); + } + walker.done(); + } else { + dumpn(name + " = " + uneval(subject)); + walker.check(subject); + count++; + } + + return count; + } + + // A walker that doesn't check anything. + Census.walkAnything = { + enter: () => Census.walkAnything, + done: () => undefined, + check: () => undefined + }; + + // A walker that requires all leaves to be zeros. + Census.assertAllZeros = { + enter: () => Census.assertAllZeros, + done: () => undefined, + check: elt => { if (elt !== 0) throw new Error("Census mismatch: expected zero, found " + elt); } + }; + + function expectedObject() { + throw new Error("Census mismatch: subject has leaf where basis has nested object"); + } + + function expectedLeaf() { + throw new Error("Census mismatch: subject has nested object where basis has leaf"); + } + + // Return a function that, given a 'basis' census, returns a census walker that + // compares the subject census against the basis. The returned walker calls the + // given |compare|, |missing|, and |extra| functions as follows: + // + // - compare(subjectLeaf, basisLeaf): Check a leaf of the subject against the + // corresponding leaf of the basis. + // + // - missing(prop, value): Called when the subject is missing a property named + // |prop| which is present in the basis with value |value|. + // + // - extra(prop): Called when the subject has a property named |prop|, but the + // basis has no such property. This should return a walker that can check + // the subject's value. + function makeBasisChecker({compare, missing, extra}) { + return function makeWalker(basis) { + if (typeof basis === "object") { + var unvisited = new Set(Object.getOwnPropertyNames(basis)); + return { + enter: prop => { + unvisited.delete(prop); + if (prop in basis) { + return makeWalker(basis[prop]); + } else { + return extra(prop); + } + }, + + done: () => unvisited.forEach(prop => missing(prop, basis[prop])), + check: expectedObject + }; + } else { + return { + enter: expectedLeaf, + done: expectedLeaf, + check: elt => compare(elt, basis) + }; + } + }; + } + + function missingProp(prop) { + throw new Error("Census mismatch: subject lacks property present in basis: " + prop); + } + + function extraProp(prop) { + throw new Error("Census mismatch: subject has property not present in basis: " + prop); + } + + // Return a walker that checks that the subject census has counts all equal to + // |basis|. + Census.assertAllEqual = makeBasisChecker({ + compare: (a, b) => { if (a !== b) throw new Error("Census mismatch: expected " + a + " got " + b);}, + missing: missingProp, + extra: extraProp + }); + + function ok(val) { + if (!val) { + throw new Error("Census mismatch: expected truthy, got " + val); + } + } + + // Return a walker that checks that the subject census has at least as many + // items of each category as |basis|. + Census.assertAllNotLessThan = makeBasisChecker({ + compare: (subject, basis) => ok(subject >= basis), + missing: missingProp, + extra: () => Census.walkAnything + }); + + // Return a walker that checks that the subject census has at most as many + // items of each category as |basis|. + Census.assertAllNotMoreThan = makeBasisChecker({ + compare: (subject, basis) => ok(subject <= basis), + missing: missingProp, + extra: () => Census.walkAnything + }); + + // Return a walker that checks that the subject census has within |fudge| + // items of each category of the count in |basis|. + Census.assertAllWithin = function (fudge, basis) { + return makeBasisChecker({ + compare: (subject, basis) => ok(Math.abs(subject - basis) <= fudge), + missing: missingProp, + extra: () => Census.walkAnything + })(basis); + }; + + return Census; +}()); diff --git a/dom/heapsnapshot/tests/unit/Match.jsm b/dom/heapsnapshot/tests/unit/Match.jsm new file mode 100644 index 000000000..c29e6484e --- /dev/null +++ b/dom/heapsnapshot/tests/unit/Match.jsm @@ -0,0 +1,190 @@ +// A little pattern-matching library. +// +// Ported from js/src/tests/js1_8_5/reflect-parse/Match.js for use with devtools +// server xpcshell tests. + +this.EXPORTED_SYMBOLS = ["Match"]; + +this.Match = (function() { + + function Pattern(template) { + // act like a constructor even as a function + if (!(this instanceof Pattern)) + return new Pattern(template); + + this.template = template; + } + + Pattern.prototype = { + match: function(act) { + return match(act, this.template); + }, + + matches: function(act) { + try { + return this.match(act); + } + catch (e if e instanceof MatchError) { + return false; + } + }, + + assert: function(act, message) { + try { + return this.match(act); + } + catch (e if e instanceof MatchError) { + throw new Error((message || "failed match") + ": " + e.message); + } + }, + + toString: () => "[object Pattern]" + }; + + Pattern.ANY = new Pattern; + Pattern.ANY.template = Pattern.ANY; + + Pattern.NUMBER = new Pattern; + Pattern.NUMBER.match = function (act) { + if (typeof act !== 'number') { + throw new MatchError("Expected number, got: " + quote(act)); + } + } + + Pattern.NATURAL = new Pattern + Pattern.NATURAL.match = function (act) { + if (typeof act !== 'number' || act !== Math.floor(act) || act < 0) { + throw new MatchError("Expected natural number, got: " + quote(act)); + } + } + + var quote = uneval; + + function MatchError(msg) { + this.message = msg; + } + + MatchError.prototype = { + toString: function() { + return "match error: " + this.message; + } + }; + + function isAtom(x) { + return (typeof x === "number") || + (typeof x === "string") || + (typeof x === "boolean") || + (x === null) || + (typeof x === "object" && x instanceof RegExp); + } + + function isObject(x) { + return (x !== null) && (typeof x === "object"); + } + + function isFunction(x) { + return typeof x === "function"; + } + + function isArrayLike(x) { + return isObject(x) && ("length" in x); + } + + function matchAtom(act, exp) { + if ((typeof exp) === "number" && isNaN(exp)) { + if ((typeof act) !== "number" || !isNaN(act)) + throw new MatchError("expected NaN, got: " + quote(act)); + return true; + } + + if (exp === null) { + if (act !== null) + throw new MatchError("expected null, got: " + quote(act)); + return true; + } + + if (exp instanceof RegExp) { + if (!(act instanceof RegExp) || exp.source !== act.source) + throw new MatchError("expected " + quote(exp) + ", got: " + quote(act)); + return true; + } + + switch (typeof exp) { + case "string": + if (act !== exp) + throw new MatchError("expected " + quote(exp) + ", got " + quote(act)); + return true; + case "boolean": + case "number": + if (exp !== act) + throw new MatchError("expected " + exp + ", got " + quote(act)); + return true; + } + + throw new Error("bad pattern: " + exp.toSource()); + } + + function matchObject(act, exp) { + if (!isObject(act)) + throw new MatchError("expected object, got " + quote(act)); + + for (var key in exp) { + if (!(key in act)) + throw new MatchError("expected property " + quote(key) + " not found in " + quote(act)); + match(act[key], exp[key]); + } + + return true; + } + + function matchFunction(act, exp) { + if (!isFunction(act)) + throw new MatchError("expected function, got " + quote(act)); + + if (act !== exp) + throw new MatchError("expected function: " + exp + + "\nbut got different function: " + act); + } + + function matchArray(act, exp) { + if (!isObject(act) || !("length" in act)) + throw new MatchError("expected array-like object, got " + quote(act)); + + var length = exp.length; + if (act.length !== exp.length) + throw new MatchError("expected array-like object of length " + length + ", got " + quote(act)); + + for (var i = 0; i < length; i++) { + if (i in exp) { + if (!(i in act)) + throw new MatchError("expected array property " + i + " not found in " + quote(act)); + match(act[i], exp[i]); + } + } + + return true; + } + + function match(act, exp) { + if (exp === Pattern.ANY) + return true; + + if (exp instanceof Pattern) + return exp.match(act); + + if (isAtom(exp)) + return matchAtom(act, exp); + + if (isArrayLike(exp)) + return matchArray(act, exp); + + if (isFunction(exp)) + return matchFunction(act, exp); + + return matchObject(act, exp); + } + + return { Pattern: Pattern, + MatchError: MatchError }; + +})(); diff --git a/dom/heapsnapshot/tests/unit/dominator-tree-worker.js b/dom/heapsnapshot/tests/unit/dominator-tree-worker.js new file mode 100644 index 000000000..1f49ca841 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/dominator-tree-worker.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +console.log("Initializing worker."); + +self.onmessage = e => { + console.log("Starting test."); + try { + const path = ThreadSafeChromeUtils.saveHeapSnapshot({ runtime: true }); + const snapshot = ThreadSafeChromeUtils.readHeapSnapshot(path); + + const dominatorTree = snapshot.computeDominatorTree(); + ok(dominatorTree); + ok(dominatorTree instanceof DominatorTree); + + let threw = false; + try { + new DominatorTree(); + } catch (e) { + threw = true; + } + ok(threw, "Constructor shouldn't be usable"); + } catch (e) { + ok(false, "Unexpected error inside worker:\n" + e.toString() + "\n" + e.stack); + } finally { + done(); + } +}; + +// Proxy assertions to the main thread. +function ok(val, msg) { + console.log("ok(" + !!val + ", \"" + msg + "\")"); + self.postMessage({ + type: "assertion", + passed: !!val, + msg, + stack: Error().stack + }); +} + +// Tell the main thread we are done with the tests. +function done() { + console.log("done()"); + self.postMessage({ + type: "done" + }); +} diff --git a/dom/heapsnapshot/tests/unit/head_heapsnapshot.js b/dom/heapsnapshot/tests/unit/head_heapsnapshot.js new file mode 100644 index 000000000..3171c8a6f --- /dev/null +++ b/dom/heapsnapshot/tests/unit/head_heapsnapshot.js @@ -0,0 +1,448 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; +var Cr = Components.results; +var CC = Components.Constructor; + +const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const { Match } = Cu.import("resource://test/Match.jsm", {}); +const { Census } = Cu.import("resource://test/Census.jsm", {}); +const { addDebuggerToGlobal } = + Cu.import("resource://gre/modules/jsdebugger.jsm", {}); +const { Task } = require("devtools/shared/task"); + +const DevToolsUtils = require("devtools/shared/DevToolsUtils"); +const flags = require("devtools/shared/flags"); +const HeapAnalysesClient = + require("devtools/shared/heapsnapshot/HeapAnalysesClient"); +const Services = require("Services"); +const { censusReportToCensusTreeNode } = require("devtools/shared/heapsnapshot/census-tree-node"); +const CensusUtils = require("devtools/shared/heapsnapshot/CensusUtils"); +const DominatorTreeNode = require("devtools/shared/heapsnapshot/DominatorTreeNode"); +const { deduplicatePaths } = require("devtools/shared/heapsnapshot/shortest-paths"); +const { LabelAndShallowSizeVisitor } = DominatorTreeNode; + + +// Always log packets when running tests. runxpcshelltests.py will throw +// the output away anyway, unless you give it the --verbose flag. +if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) { + Services.prefs.setBoolPref("devtools.debugger.log", true); +} +flags.wantLogging = true; + +const SYSTEM_PRINCIPAL = Cc["@mozilla.org/systemprincipal;1"] + .createInstance(Ci.nsIPrincipal); + +function dumpn(msg) { + dump("HEAPSNAPSHOT-TEST: " + msg + "\n"); +} + +function addTestingFunctionsToGlobal(global) { + global.eval( + ` + const testingFunctions = Components.utils.getJSTestingFunctions(); + for (let k in testingFunctions) { + this[k] = testingFunctions[k]; + } + ` + ); + if (!global.print) { + global.print = do_print; + } + if (!global.newGlobal) { + global.newGlobal = newGlobal; + } + if (!global.Debugger) { + addDebuggerToGlobal(global); + } +} + +addTestingFunctionsToGlobal(this); + +/** + * Create a new global, with all the JS shell testing functions. Similar to the + * newGlobal function exposed to JS shells, and useful for porting JS shell + * tests to xpcshell tests. + */ +function newGlobal() { + const global = new Cu.Sandbox(SYSTEM_PRINCIPAL, { freshZone: true }); + addTestingFunctionsToGlobal(global); + return global; +} + +function assertThrowsValue(f, val, msg) { + var fullmsg; + try { + f(); + } catch (exc) { + if ((exc === val) === (val === val) && (val !== 0 || 1 / exc === 1 / val)) + return; + fullmsg = "Assertion failed: expected exception " + val + ", got " + exc; + } + if (fullmsg === undefined) + fullmsg = "Assertion failed: expected exception " + val + ", no exception thrown"; + if (msg !== undefined) + fullmsg += " - " + msg; + throw new Error(fullmsg); +} + +/** + * Returns the full path of the file with the specified name in a + * platform-independent and URL-like form. + */ +function getFilePath(aName, aAllowMissing = false, aUsePlatformPathSeparator = false) +{ + let file = do_get_file(aName, aAllowMissing); + let path = Services.io.newFileURI(file).spec; + let filePrePath = "file://"; + if ("nsILocalFileWin" in Ci && + file instanceof Ci.nsILocalFileWin) { + filePrePath += "/"; + } + + path = path.slice(filePrePath.length); + + if (aUsePlatformPathSeparator && path.match(/^\w:/)) { + path = path.replace(/\//g, "\\"); + } + + return path; +} + +function saveNewHeapSnapshot(opts = { runtime: true }) { + const filePath = ChromeUtils.saveHeapSnapshot(opts); + ok(filePath, "Should get a file path to save the core dump to."); + ok(true, "Saved a heap snapshot to " + filePath); + return filePath; +} + +function readHeapSnapshot(filePath) { + const snapshot = ChromeUtils.readHeapSnapshot(filePath); + ok(snapshot, "Should have read a heap snapshot back from " + filePath); + ok(snapshot instanceof HeapSnapshot, "snapshot should be an instance of HeapSnapshot"); + return snapshot; +} + +/** + * Save a heap snapshot to the file with the given name in the current + * directory, read it back as a HeapSnapshot instance, and then take a census of + * the heap snapshot's serialized heap graph with the provided census options. + * + * @param {Object|undefined} censusOptions + * Options that should be passed through to the takeCensus method. See + * js/src/doc/Debugger/Debugger.Memory.md for details. + * + * @param {Debugger|null} dbg + * If a Debugger object is given, only serialize the subgraph covered by + * the Debugger's debuggees. If null, serialize the whole heap graph. + * + * @param {String} fileName + * The file name to save the heap snapshot's core dump file to, within + * the current directory. + * + * @returns Census + */ +function saveHeapSnapshotAndTakeCensus(dbg = null, censusOptions = undefined) { + const snapshotOptions = dbg ? { debugger: dbg } : { runtime: true }; + const filePath = saveNewHeapSnapshot(snapshotOptions); + const snapshot = readHeapSnapshot(filePath); + + equal(typeof snapshot.takeCensus, "function", "snapshot should have a takeCensus method"); + + return snapshot.takeCensus(censusOptions); +} + +/** + * Save a heap snapshot to disk, read it back as a HeapSnapshot instance, and + * then compute its dominator tree. + * + * @param {Debugger|null} dbg + * If a Debugger object is given, only serialize the subgraph covered by + * the Debugger's debuggees. If null, serialize the whole heap graph. + * + * @returns {DominatorTree} + */ +function saveHeapSnapshotAndComputeDominatorTree(dbg = null) { + const snapshotOptions = dbg ? { debugger: dbg } : { runtime: true }; + const filePath = saveNewHeapSnapshot(snapshotOptions); + const snapshot = readHeapSnapshot(filePath); + + equal(typeof snapshot.computeDominatorTree, "function", + "snapshot should have a `computeDominatorTree` method"); + + const dominatorTree = snapshot.computeDominatorTree(); + + ok(dominatorTree, "Should be able to compute a dominator tree"); + ok(dominatorTree instanceof DominatorTree, "Should be an instance of DominatorTree"); + + return dominatorTree; +} + +function isSavedFrame(obj) { + return Object.prototype.toString.call(obj) === "[object SavedFrame]"; +} + +function savedFrameReplacer(key, val) { + if (isSavedFrame(val)) { + return `<SavedFrame '${val.toString().split(/\n/g).shift()}'>`; + } else { + return val; + } +} + +/** + * Assert that creating a CensusTreeNode from the given `report` with the + * specified `breakdown` creates the given `expected` CensusTreeNode. + * + * @param {Object} breakdown + * The census breakdown. + * + * @param {Object} report + * The census report. + * + * @param {Object} expected + * The expected CensusTreeNode result. + * + * @param {Object} options + * The options to pass through to `censusReportToCensusTreeNode`. + */ +function compareCensusViewData(breakdown, report, expected, options) { + dumpn("Generating CensusTreeNode from report:"); + dumpn("breakdown: " + JSON.stringify(breakdown, null, 4)); + dumpn("report: " + JSON.stringify(report, null, 4)); + dumpn("expected: " + JSON.stringify(expected, savedFrameReplacer, 4)); + + const actual = censusReportToCensusTreeNode(breakdown, report, options); + dumpn("actual: " + JSON.stringify(actual, savedFrameReplacer, 4)); + + assertStructurallyEquivalent(actual, expected); +} + +// Deep structural equivalence that can handle Map objects in addition to plain +// objects. +function assertStructurallyEquivalent(actual, expected, path = "root") { + if (actual === expected) { + equal(actual, expected, "actual and expected are the same"); + return; + } + + equal(typeof actual, typeof expected, `${path}: typeof should be the same`); + + if (actual && typeof actual === "object") { + const actualProtoString = Object.prototype.toString.call(actual); + const expectedProtoString = Object.prototype.toString.call(expected); + equal(actualProtoString, expectedProtoString, + `${path}: Object.prototype.toString.call() should be the same`); + + if (actualProtoString === "[object Map]") { + const expectedKeys = new Set([...expected.keys()]); + + for (let key of actual.keys()) { + ok(expectedKeys.has(key), + `${path}: every key in actual should exist in expected: ${String(key).slice(0, 10)}`); + expectedKeys.delete(key); + + assertStructurallyEquivalent(actual.get(key), expected.get(key), + path + ".get(" + String(key).slice(0, 20) + ")"); + } + + equal(expectedKeys.size, 0, + `${path}: every key in expected should also exist in actual, did not see ${[...expectedKeys]}`); + } else if (actualProtoString === "[object Set]") { + const expectedItems = new Set([...expected]); + + for (let item of actual) { + ok(expectedItems.has(item), + `${path}: every set item in actual should exist in expected: ${item}`); + expectedItems.delete(item); + } + + equal(expectedItems.size, 0, + `${path}: every set item in expected should also exist in actual, did not see ${[...expectedItems]}`); + } else { + const expectedKeys = new Set(Object.keys(expected)); + + for (let key of Object.keys(actual)) { + ok(expectedKeys.has(key), + `${path}: every key in actual should exist in expected: ${key}`); + expectedKeys.delete(key); + + assertStructurallyEquivalent(actual[key], expected[key], path + "." + key); + } + + equal(expectedKeys.size, 0, + `${path}: every key in expected should also exist in actual, did not see ${[...expectedKeys]}`); + } + } else { + equal(actual, expected, `${path}: primitives should be equal`); + } +} + +/** + * Assert that creating a diff of the `first` and `second` census reports + * creates the `expected` delta-report. + * + * @param {Object} breakdown + * The census breakdown. + * + * @param {Object} first + * The first census report. + * + * @param {Object} second + * The second census report. + * + * @param {Object} expected + * The expected delta-report. + */ +function assertDiff(breakdown, first, second, expected) { + dumpn("Diffing census reports:"); + dumpn("Breakdown: " + JSON.stringify(breakdown, null, 4)); + dumpn("First census report: " + JSON.stringify(first, null, 4)); + dumpn("Second census report: " + JSON.stringify(second, null, 4)); + dumpn("Expected delta-report: " + JSON.stringify(expected, null, 4)); + + const actual = CensusUtils.diff(breakdown, first, second); + dumpn("Actual delta-report: " + JSON.stringify(actual, null, 4)); + + assertStructurallyEquivalent(actual, expected); +} + +/** + * Assert that creating a label and getting a shallow size from the given node + * description with the specified breakdown is as expected. + * + * @param {Object} breakdown + * @param {Object} givenDescription + * @param {Number} expectedShallowSize + * @param {Object} expectedLabel + */ +function assertLabelAndShallowSize(breakdown, givenDescription, expectedShallowSize, expectedLabel) { + dumpn("Computing label and shallow size from node description:"); + dumpn("Breakdown: " + JSON.stringify(breakdown, null, 4)); + dumpn("Given description: " + JSON.stringify(givenDescription, null, 4)); + + const visitor = new LabelAndShallowSizeVisitor(); + CensusUtils.walk(breakdown, description, visitor); + + dumpn("Expected shallow size: " + expectedShallowSize); + dumpn("Actual shallow size: " + visitor.shallowSize()); + equal(visitor.shallowSize(), expectedShallowSize, "Shallow size should be correct"); + + dumpn("Expected label: " + JSON.stringify(expectedLabel, null, 4)); + dumpn("Actual label: " + JSON.stringify(visitor.label(), null, 4)); + assertStructurallyEquivalent(visitor.label(), expectedLabel); +} + +// Counter for mock DominatorTreeNode ids. +let TEST_NODE_ID_COUNTER = 0; + +/** + * Create a mock DominatorTreeNode for testing, with sane defaults. Override any + * property by providing it on `opts`. Optionally pass child nodes as well. + * + * @param {Object} opts + * @param {Array<DominatorTreeNode>?} children + * + * @returns {DominatorTreeNode} + */ +function makeTestDominatorTreeNode(opts, children) { + const nodeId = TEST_NODE_ID_COUNTER++; + + const node = Object.assign({ + nodeId, + label: undefined, + shallowSize: 1, + retainedSize: (children || []).reduce((size, c) => size + c.retainedSize, 1), + parentId: undefined, + children, + moreChildrenAvailable: true, + }, opts); + + if (children && children.length) { + children.map(c => c.parentId = node.nodeId); + } + + return node; +} + +/** + * Insert `newChildren` into the given dominator `tree` as specified by the + * `path` from the root to the node the `newChildren` should be inserted + * beneath. Assert that the resulting tree matches `expected`. + */ +function assertDominatorTreeNodeInsertion(tree, path, newChildren, moreChildrenAvailable, expected) { + dumpn("Inserting new children into a dominator tree:"); + dumpn("Dominator tree: " + JSON.stringify(tree, null, 2)); + dumpn("Path: " + JSON.stringify(path, null, 2)); + dumpn("New children: " + JSON.stringify(newChildren, null, 2)); + dumpn("Expected resulting tree: " + JSON.stringify(expected, null, 2)); + + const actual = DominatorTreeNode.insert(tree, path, newChildren, moreChildrenAvailable); + dumpn("Actual resulting tree: " + JSON.stringify(actual, null, 2)); + + assertStructurallyEquivalent(actual, expected); +} + +function assertDeduplicatedPaths({ target, paths, expectedNodes, expectedEdges }) { + dumpn("Deduplicating paths:"); + dumpn("target = " + target); + dumpn("paths = " + JSON.stringify(paths, null, 2)); + dumpn("expectedNodes = " + expectedNodes); + dumpn("expectedEdges = " + JSON.stringify(expectedEdges, null, 2)); + + const { nodes, edges } = deduplicatePaths(target, paths); + + dumpn("Actual nodes = " + nodes); + dumpn("Actual edges = " + JSON.stringify(edges, null, 2)); + + equal(nodes.length, expectedNodes.length, + "actual number of nodes is equal to the expected number of nodes"); + + equal(edges.length, expectedEdges.length, + "actual number of edges is equal to the expected number of edges"); + + const expectedNodeSet = new Set(expectedNodes); + const nodeSet = new Set(nodes); + ok(nodeSet.size === nodes.length, + "each returned node should be unique"); + + for (let node of nodes) { + ok(expectedNodeSet.has(node), `the ${node} node was expected`); + } + + for (let expectedEdge of expectedEdges) { + let count = 0; + for (let edge of edges) { + if (edge.from === expectedEdge.from && + edge.to === expectedEdge.to && + edge.name === expectedEdge.name) { + count++; + } + } + equal(count, 1, + "should have exactly one matching edge for the expected edge = " + JSON.stringify(edge)); + } +} + +function assertCountToBucketBreakdown(breakdown, expected) { + dumpn("count => bucket breakdown"); + dumpn("Initial breakdown = ", JSON.stringify(breakdown, null, 2)); + dumpn("Expected results = ", JSON.stringify(expected, null, 2)); + + const actual = CensusUtils.countToBucketBreakdown(breakdown); + dumpn("Actual results = ", JSON.stringify(actual, null, 2)); + + assertStructurallyEquivalent(actual, expected); +} + +/** + * Create a mock path entry for the given predecessor and edge. + */ +function pathEntry(predecessor, edge) { + return { predecessor, edge }; +} diff --git a/dom/heapsnapshot/tests/unit/heap-snapshot-worker.js b/dom/heapsnapshot/tests/unit/heap-snapshot-worker.js new file mode 100644 index 000000000..10ee70cec --- /dev/null +++ b/dom/heapsnapshot/tests/unit/heap-snapshot-worker.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +console.log("Initializing worker."); + +self.onmessage = e => { + console.log("Starting test."); + try { + ok(typeof ChromeUtils === "undefined", + "Should not have access to ChromeUtils in a worker."); + ok(ThreadSafeChromeUtils, + "Should have access to ThreadSafeChromeUtils in a worker."); + ok(HeapSnapshot, + "Should have access to HeapSnapshot in a worker."); + + const filePath = ThreadSafeChromeUtils.saveHeapSnapshot({ globals: [this] }); + ok(true, "Should be able to save a snapshot."); + + const snapshot = ThreadSafeChromeUtils.readHeapSnapshot(filePath); + ok(snapshot, "Should be able to read a heap snapshot"); + ok(snapshot instanceof HeapSnapshot, "Should be an instanceof HeapSnapshot"); + } catch (e) { + ok(false, "Unexpected error inside worker:\n" + e.toString() + "\n" + e.stack); + } finally { + done(); + } +}; + +// Proxy assertions to the main thread. +function ok(val, msg) { + console.log("ok(" + !!val + ", \"" + msg + "\")"); + self.postMessage({ + type: "assertion", + passed: !!val, + msg, + stack: Error().stack + }); +} + +// Tell the main thread we are done with the tests. +function done() { + console.log("done()"); + self.postMessage({ + type: "done" + }); +} diff --git a/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_LabelAndShallowSize_01.js b/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_LabelAndShallowSize_01.js new file mode 100644 index 000000000..845a0d263 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_LabelAndShallowSize_01.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we can generate label structures from node description reports. + +const breakdown = { + by: "coarseType", + objects: { + by: "objectClass", + then: { by: "count", count: true, bytes: true }, + other: { by: "count", count: true, bytes: true }, + }, + strings: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + scripts: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + other: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, +}; + +const description = { + objects: { + Function: { count: 1, bytes: 32 }, + other: { count: 0, bytes: 0 } + }, + strings: {}, + scripts: {}, + other: {} +}; + +const expected = [ + "objects", + "Function" +]; + +const shallowSize = 32; + +function run_test() { + assertLabelAndShallowSize(breakdown, description, shallowSize, expected); +} diff --git a/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_LabelAndShallowSize_02.js b/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_LabelAndShallowSize_02.js new file mode 100644 index 000000000..e1f32de58 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_LabelAndShallowSize_02.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we can generate label structures from node description reports. + +const breakdown = { + by: "coarseType", + objects: { + by: "objectClass", + then: { by: "count", count: true, bytes: true }, + other: { by: "count", count: true, bytes: true }, + }, + strings: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + scripts: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + other: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, +}; + +const description = { + objects: { + other: { count: 1, bytes: 10 } + }, + strings: {}, + scripts: {}, + other: {} +}; + +const expected = [ + "objects", + "other" +]; + +const shallowSize = 10; + +function run_test() { + assertLabelAndShallowSize(breakdown, description, shallowSize, expected); +} diff --git a/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_LabelAndShallowSize_03.js b/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_LabelAndShallowSize_03.js new file mode 100644 index 000000000..ad35dcec1 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_LabelAndShallowSize_03.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we can generate label structures from node description reports. + +const breakdown = { + by: "coarseType", + objects: { + by: "objectClass", + then: { by: "count", count: true, bytes: true }, + other: { by: "count", count: true, bytes: true }, + }, + strings: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + scripts: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + other: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, +}; + +const description = { + objects: { + other: { count: 0, bytes: 0 } + }, + strings: { + "JSString": { count: 1, bytes: 42 }, + }, + scripts: {}, + other: {} +}; + +const expected = [ + "strings", + "JSString" +]; + +const shallowSize = 42; + +function run_test() { + assertLabelAndShallowSize(breakdown, description, shallowSize, expected); +} diff --git a/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_LabelAndShallowSize_04.js b/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_LabelAndShallowSize_04.js new file mode 100644 index 000000000..566ad0dab --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_LabelAndShallowSize_04.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we can generate label structures from node description reports. + +const breakdown = { + by: "coarseType", + objects: { + by: "objectClass", + then: { + by: "allocationStack", + then: { by: "count", count: true, bytes: true }, + noStack: { by: "count", count: true, bytes: true }, + }, + other: { by: "count", count: true, bytes: true }, + }, + strings: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + scripts: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + other: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, +}; + +const stack = saveStack(); + +const description = { + objects: { + Array: new Map([[stack, { count: 1, bytes: 512 }]]), + other: { count: 0, bytes: 0 } + }, + strings: {}, + scripts: {}, + other: {} +}; + +const expected = [ + "objects", + "Array", + stack +]; + +const shallowSize = 512; + +function run_test() { + assertLabelAndShallowSize(breakdown, description, shallowSize, expected); +} diff --git a/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_attachShortestPaths_01.js b/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_attachShortestPaths_01.js new file mode 100644 index 000000000..24e8e2eb5 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_attachShortestPaths_01.js @@ -0,0 +1,132 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the DominatorTreeNode.attachShortestPaths function can correctly +// attach the deduplicated shortest retaining paths for each node it is given. + +const startNodeId = 9999; +const maxNumPaths = 2; + +// Mock data mapping node id to shortest paths to that node id. +const shortestPaths = new Map([ + [1000, [ + [pathEntry(1100, "a"), pathEntry(1200, "b")], + [pathEntry(1100, "c"), pathEntry(1300, "d")], + ]], + [2000, [ + [pathEntry(2100, "e"), pathEntry(2200, "f"), pathEntry(2300, "g")] + ]], + [3000, [ + [pathEntry(3100, "h")], + [pathEntry(3100, "i")], + [pathEntry(3100, "j")], + [pathEntry(3200, "k")], + [pathEntry(3300, "l")], + [pathEntry(3400, "m")], + ]], +]); + +const actual = [ + makeTestDominatorTreeNode({ nodeId: 1000 }), + makeTestDominatorTreeNode({ nodeId: 2000 }), + makeTestDominatorTreeNode({ nodeId: 3000 }), +]; + +const expected = [ + makeTestDominatorTreeNode({ + nodeId: 1000, + shortestPaths: { + nodes: [ + { id: 1000, label: ["SomeType-1000"] }, + { id: 1100, label: ["SomeType-1100"] }, + { id: 1200, label: ["SomeType-1200"] }, + { id: 1300, label: ["SomeType-1300"] }, + ], + edges: [ + { from: 1100, to: 1200, name: "a" }, + { from: 1100, to: 1300, name: "c" }, + { from: 1200, to: 1000, name: "b" }, + { from: 1300, to: 1000, name: "d" }, + ] + } + }), + + makeTestDominatorTreeNode({ + nodeId: 2000, + shortestPaths: { + nodes: [ + { id: 2000, label: ["SomeType-2000"] }, + { id: 2100, label: ["SomeType-2100"] }, + { id: 2200, label: ["SomeType-2200"] }, + { id: 2300, label: ["SomeType-2300"] }, + ], + edges: [ + { from: 2100, to: 2200, name: "e" }, + { from: 2200, to: 2300, name: "f" }, + { from: 2300, to: 2000, name: "g" }, + ] + } + }), + + makeTestDominatorTreeNode({ nodeId: 3000, + shortestPaths: { + nodes: [ + { id: 3000, label: ["SomeType-3000"] }, + { id: 3100, label: ["SomeType-3100"] }, + { id: 3200, label: ["SomeType-3200"] }, + { id: 3300, label: ["SomeType-3300"] }, + { id: 3400, label: ["SomeType-3400"] }, + ], + edges: [ + { from: 3100, to: 3000, name: "h" }, + { from: 3100, to: 3000, name: "i" }, + { from: 3100, to: 3000, name: "j" }, + { from: 3200, to: 3000, name: "k" }, + { from: 3300, to: 3000, name: "l" }, + { from: 3400, to: 3000, name: "m" }, + ] + } + }), +]; + +const breakdown = { + by: "internalType", + then: { by: "count", count: true, bytes: true } +}; + +const mockSnapshot = { + computeShortestPaths: (start, nodeIds, max) => { + equal(start, startNodeId); + equal(max, maxNumPaths); + + return new Map(nodeIds.map(nodeId => { + const paths = shortestPaths.get(nodeId); + ok(paths, "Expected computeShortestPaths call for node id = " + nodeId); + return [nodeId, paths]; + })); + }, + + describeNode: (bd, nodeId) => { + equal(bd, breakdown); + return { + ["SomeType-" + nodeId]: { + count: 1, + bytes: 10, + } + }; + }, +}; + +function run_test() { + DominatorTreeNode.attachShortestPaths(mockSnapshot, + breakdown, + startNodeId, + actual, + maxNumPaths); + + dumpn("Expected = " + JSON.stringify(expected, null, 2)); + dumpn("Actual = " + JSON.stringify(actual, null, 2)); + + assertStructurallyEquivalent(expected, actual); +} diff --git a/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_getNodeByIdAlongPath_01.js b/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_getNodeByIdAlongPath_01.js new file mode 100644 index 000000000..de2907809 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_getNodeByIdAlongPath_01.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we can find the node with the given id along the specified path. + +const node3000 = makeTestDominatorTreeNode({ nodeId: 3000 }); + +const node2000 = makeTestDominatorTreeNode({ nodeId: 2000 }, [ + makeTestDominatorTreeNode({}), + node3000, + makeTestDominatorTreeNode({}), +]); + +const node1000 = makeTestDominatorTreeNode({ nodeId: 1000 }, [ + makeTestDominatorTreeNode({}), + node2000, + makeTestDominatorTreeNode({}), +]); + +const tree = node1000; + +const path = [1000, 2000, 3000]; + +const tests = [ + { id: 1000, expected: node1000 }, + { id: 2000, expected: node2000 }, + { id: 3000, expected: node3000 }, +]; + +function run_test() { + for (let { id, expected } of tests) { + const actual = DominatorTreeNode.getNodeByIdAlongPath(id, tree, path); + equal(actual, expected, `We should have got the node with id = ${id}`); + } + + equal(null, + DominatorTreeNode.getNodeByIdAlongPath(999999999999, tree, path), + "null is returned for nodes that are not even in the tree"); + + const lastNodeId = tree.children[tree.children.length - 1].nodeId; + equal(null, + DominatorTreeNode.getNodeByIdAlongPath(lastNodeId, tree, path), + "null is returned for nodes that are not along the path"); +} diff --git a/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_insert_01.js b/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_insert_01.js new file mode 100644 index 000000000..979232ff4 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_insert_01.js @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we can insert new children into an existing DominatorTreeNode tree. + +const tree = makeTestDominatorTreeNode({ nodeId: 1000 }, [ + makeTestDominatorTreeNode({}), + makeTestDominatorTreeNode({ nodeId: 2000 }, [ + makeTestDominatorTreeNode({}), + makeTestDominatorTreeNode({ nodeId: 3000 }), + makeTestDominatorTreeNode({}), + ]), + makeTestDominatorTreeNode({}), +]); + +const path = [1000, 2000, 3000]; + +const newChildren = [ + makeTestDominatorTreeNode({ parentId: 3000 }), + makeTestDominatorTreeNode({ parentId: 3000 }), +]; + +const moreChildrenAvailable = false; + +const expected = { + nodeId: 1000, + parentId: undefined, + label: undefined, + shallowSize: 1, + retainedSize: 7, + children: [ + { + nodeId: 0, + label: undefined, + shallowSize: 1, + retainedSize: 1, + parentId: 1000, + moreChildrenAvailable: true, + children: undefined, + }, + { + nodeId: 2000, + label: undefined, + shallowSize: 1, + retainedSize: 4, + parentId: 1000, + children: [ + { + nodeId: 1, + label: undefined, + shallowSize: 1, + retainedSize: 1, + parentId: 2000, + moreChildrenAvailable: true, + children: undefined, + }, + { + nodeId: 3000, + label: undefined, + shallowSize: 1, + retainedSize: 1, + parentId: 2000, + children: [ + { + nodeId: 7, + parentId: 3000, + label: undefined, + shallowSize: 1, + retainedSize: 1, + moreChildrenAvailable: true, + children: undefined, + }, + { + nodeId: 8, + parentId: 3000, + label: undefined, + shallowSize: 1, + retainedSize: 1, + moreChildrenAvailable: true, + children: undefined, + }, + ], + moreChildrenAvailable: false + }, + { + nodeId: 3, + label: undefined, + shallowSize: 1, + retainedSize: 1, + parentId: 2000, + moreChildrenAvailable: true, + children: undefined, + }, + ], + moreChildrenAvailable: true + }, + { + nodeId: 5, + label: undefined, + shallowSize: 1, + retainedSize: 1, + parentId: 1000, + moreChildrenAvailable: true, + children: undefined, + } + ], + moreChildrenAvailable: true +}; + +function run_test() { + assertDominatorTreeNodeInsertion(tree, path, newChildren, moreChildrenAvailable, expected); +} diff --git a/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_insert_02.js b/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_insert_02.js new file mode 100644 index 000000000..9a8d11d0b --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_insert_02.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test attempting to insert new children into an existing DominatorTreeNode +// tree with a bad path. + +const tree = makeTestDominatorTreeNode({}, [ + makeTestDominatorTreeNode({}), + makeTestDominatorTreeNode({}, [ + makeTestDominatorTreeNode({}), + makeTestDominatorTreeNode({}), + makeTestDominatorTreeNode({}), + ]), + makeTestDominatorTreeNode({}), +]); + +const path = [111111, 222222, 333333]; + +const newChildren = [ + makeTestDominatorTreeNode({ parentId: 333333 }), + makeTestDominatorTreeNode({ parentId: 333333 }), +]; + +const moreChildrenAvailable = false; + +const expected = tree; + +function run_test() { + assertDominatorTreeNodeInsertion(tree, path, newChildren, moreChildrenAvailable, expected); +} diff --git a/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_insert_03.js b/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_insert_03.js new file mode 100644 index 000000000..f8cb5eec3 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_insert_03.js @@ -0,0 +1,117 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test inserting new children into an existing DominatorTreeNode at the root. + +const tree = makeTestDominatorTreeNode({ nodeId: 666 }, [ + makeTestDominatorTreeNode({}), + makeTestDominatorTreeNode({}, [ + makeTestDominatorTreeNode({}), + makeTestDominatorTreeNode({}), + makeTestDominatorTreeNode({}), + ]), + makeTestDominatorTreeNode({}), +]); + +const path = [666]; + +const newChildren = [ + makeTestDominatorTreeNode({ + nodeId: 777, + parentId: 666 + }), + makeTestDominatorTreeNode({ + nodeId: 888, + parentId: 666 + }), +]; + +const moreChildrenAvailable = false; + +const expected = { + nodeId: 666, + label: undefined, + parentId: undefined, + shallowSize: 1, + retainedSize: 7, + children: [ + { + nodeId: 0, + label: undefined, + shallowSize: 1, + retainedSize: 1, + parentId: 666, + moreChildrenAvailable: true, + children: undefined + }, + { + nodeId: 4, + label: undefined, + shallowSize: 1, + retainedSize: 4, + parentId: 666, + children: [ + { + nodeId: 1, + label: undefined, + shallowSize: 1, + retainedSize: 1, + parentId: 4, + moreChildrenAvailable: true, + children: undefined + }, + { + nodeId: 2, + label: undefined, + shallowSize: 1, + retainedSize: 1, + parentId: 4, + moreChildrenAvailable: true, + children: undefined + }, + { + nodeId: 3, + label: undefined, + shallowSize: 1, + retainedSize: 1, + parentId: 4, + moreChildrenAvailable: true, + children: undefined + } + ], + moreChildrenAvailable: true + }, + { + nodeId: 5, + label: undefined, + shallowSize: 1, + retainedSize: 1, + parentId: 666, + moreChildrenAvailable: true, + children: undefined + }, + { + nodeId: 777, + label: undefined, + shallowSize: 1, + retainedSize: 1, + parentId: 666, + moreChildrenAvailable: true, + children: undefined + }, + { + nodeId: 888, + label: undefined, + shallowSize: 1, + retainedSize: 1, + parentId: 666, + moreChildrenAvailable: true, + children: undefined + } + ], + moreChildrenAvailable: false +}; + +function run_test() { + assertDominatorTreeNodeInsertion(tree, path, newChildren, moreChildrenAvailable, expected); +} diff --git a/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_partialTraversal_01.js b/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_partialTraversal_01.js new file mode 100644 index 000000000..78ec47b64 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_DominatorTreeNode_partialTraversal_01.js @@ -0,0 +1,164 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we correctly set `moreChildrenAvailable` when doing a partial +// traversal of a dominator tree to create the initial incrementally loaded +// `DominatorTreeNode` tree. + +// `tree` maps parent to children: +// +// 100 +// |- 200 +// | |- 500 +// | |- 600 +// | `- 700 +// |- 300 +// | |- 800 +// | |- 900 +// `- 400 +// |- 1000 +// |- 1100 +// `- 1200 +const tree = new Map([ + [100, [200, 300, 400]], + [200, [500, 600, 700]], + [300, [800, 900]], + [400, [1000, 1100, 1200]] +]); + +const mockDominatorTree = { + root: 100, + getRetainedSize: _ => 10, + getImmediatelyDominated: id => (tree.get(id) || []).slice() +}; + +const mockSnapshot = { + describeNode: _ => ({ + objects: { count: 0, bytes: 0 }, + strings: { count: 0, bytes: 0 }, + scripts: { count: 0, bytes: 0 }, + other: { SomeType: { count: 1, bytes: 10 } } + }) +}; + +const breakdown = { + by: "coarseType", + objects: { by: "count", count: true, bytes: true }, + strings: { by: "count", count: true, bytes: true }, + scripts: { by: "count", count: true, bytes: true }, + other: { + by: "internalType", + then: { by: "count", count: true, bytes: true } + }, +}; + +const expected = { + nodeId: 100, + label: [ + "other", + "SomeType" + ], + shallowSize: 10, + retainedSize: 10, + shortestPaths: undefined, + children: [ + { + nodeId: 200, + label: [ + "other", + "SomeType" + ], + shallowSize: 10, + retainedSize: 10, + parentId: 100, + shortestPaths: undefined, + children: [ + { + nodeId: 500, + label: [ + "other", + "SomeType" + ], + shallowSize: 10, + retainedSize: 10, + parentId: 200, + moreChildrenAvailable: false, + shortestPaths: undefined, + children: undefined + }, + { + nodeId: 600, + label: [ + "other", + "SomeType" + ], + shallowSize: 10, + retainedSize: 10, + parentId: 200, + moreChildrenAvailable: false, + shortestPaths: undefined, + children: undefined + } + ], + moreChildrenAvailable: true + }, + { + nodeId: 300, + label: [ + "other", + "SomeType" + ], + shallowSize: 10, + retainedSize: 10, + parentId: 100, + shortestPaths: undefined, + children: [ + { + nodeId: 800, + label: [ + "other", + "SomeType" + ], + shallowSize: 10, + retainedSize: 10, + parentId: 300, + moreChildrenAvailable: false, + shortestPaths: undefined, + children: undefined + }, + { + nodeId: 900, + label: [ + "other", + "SomeType" + ], + shallowSize: 10, + retainedSize: 10, + parentId: 300, + moreChildrenAvailable: false, + shortestPaths: undefined, + children: undefined + } + ], + moreChildrenAvailable: false + } + ], + moreChildrenAvailable: true, + parentId: undefined, +}; + +function run_test() { + // Traverse the whole depth of the test tree, but one short of the number of + // siblings. This will exercise the moreChildrenAvailable handling for + // siblings. + const actual = DominatorTreeNode.partialTraversal(mockDominatorTree, + mockSnapshot, + breakdown, + /* maxDepth = */ 4, + /* siblings = */ 2); + + dumpn("Expected = " + JSON.stringify(expected, null, 2)); + dumpn("Actual = " + JSON.stringify(actual, null, 2)); + + assertStructurallyEquivalent(expected, actual); +} diff --git a/dom/heapsnapshot/tests/unit/test_DominatorTree_01.js b/dom/heapsnapshot/tests/unit/test_DominatorTree_01.js new file mode 100644 index 000000000..e8145f658 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_DominatorTree_01.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Sanity test that we can compute dominator trees. + +function run_test() { + const path = ChromeUtils.saveHeapSnapshot({ runtime: true }); + const snapshot = ChromeUtils.readHeapSnapshot(path); + + const dominatorTree = snapshot.computeDominatorTree(); + ok(dominatorTree); + ok(dominatorTree instanceof DominatorTree); + + let threw = false; + try { + new DominatorTree(); + } catch (e) { + threw = true; + } + ok(threw, "Constructor shouldn't be usable"); + + do_test_finished(); +} diff --git a/dom/heapsnapshot/tests/unit/test_DominatorTree_02.js b/dom/heapsnapshot/tests/unit/test_DominatorTree_02.js new file mode 100644 index 000000000..a518f8a27 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_DominatorTree_02.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we can compute dominator trees from a snapshot in a worker. + +add_task(function* () { + const worker = new ChromeWorker("resource://test/dominator-tree-worker.js"); + worker.postMessage({}); + + let assertionCount = 0; + worker.onmessage = e => { + if (e.data.type !== "assertion") { + return; + } + + ok(e.data.passed, e.data.msg + "\n" + e.data.stack); + assertionCount++; + }; + + yield waitForDone(worker); + + ok(assertionCount > 0); + worker.terminate(); +}); + +function waitForDone(w) { + return new Promise((resolve, reject) => { + w.onerror = e => { + reject(); + ok(false, "Error in worker: " + e); + }; + + w.addEventListener("message", function listener(e) { + if (e.data.type === "done") { + w.removeEventListener("message", listener, false); + resolve(); + } + }, false); + }); +} diff --git a/dom/heapsnapshot/tests/unit/test_DominatorTree_03.js b/dom/heapsnapshot/tests/unit/test_DominatorTree_03.js new file mode 100644 index 000000000..0a14ce53d --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_DominatorTree_03.js @@ -0,0 +1,13 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we can get the root of dominator trees. + +function run_test() { + const dominatorTree = saveHeapSnapshotAndComputeDominatorTree(); + equal(typeof dominatorTree.root, "number", "root should be a number"); + equal(Math.floor(dominatorTree.root), dominatorTree.root, "root should be an integer"); + ok(dominatorTree.root >= 0, "root should be positive"); + ok(dominatorTree.root <= Math.pow(2, 48), "root should be less than or equal to 2^48"); + do_test_finished(); +} diff --git a/dom/heapsnapshot/tests/unit/test_DominatorTree_04.js b/dom/heapsnapshot/tests/unit/test_DominatorTree_04.js new file mode 100644 index 000000000..e5aef3fec --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_DominatorTree_04.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we can get the retained sizes of dominator trees. + +function run_test() { + const dominatorTree = saveHeapSnapshotAndComputeDominatorTree(); + equal(typeof dominatorTree.getRetainedSize, "function", + "getRetainedSize should be a function"); + + const size = dominatorTree.getRetainedSize(dominatorTree.root); + ok(size, "should get a size for the root"); + equal(typeof size, "number", "retained sizes should be a number"); + equal(Math.floor(size), size, "size should be an integer"); + ok(size > 0, "size should be positive"); + ok(size <= Math.pow(2, 64), "size should be less than or equal to 2^64"); + + const bad = dominatorTree.getRetainedSize(1); + equal(bad, null, "null is returned for unknown node ids"); + + do_test_finished(); +} diff --git a/dom/heapsnapshot/tests/unit/test_DominatorTree_05.js b/dom/heapsnapshot/tests/unit/test_DominatorTree_05.js new file mode 100644 index 000000000..c07cee994 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_DominatorTree_05.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we can get the set of immediately dominated nodes for any given +// node and that this forms a tree. + +function run_test() { + var dominatorTree = saveHeapSnapshotAndComputeDominatorTree(); + equal(typeof dominatorTree.getImmediatelyDominated, "function", + "getImmediatelyDominated should be a function"); + + // Do a traversal of the dominator tree. + // + // Note that we don't assert directly, only if we get an unexpected + // value. There are just way too many nodes in the heap graph to assert for + // every one. This test would constantly time out and assertion messages would + // overflow the log size. + + var root = dominatorTree.root; + equal(dominatorTree.getImmediateDominator(root), null, + "The root should not have a parent"); + + var seen = new Set(); + var stack = [root]; + while (stack.length > 0) { + var top = stack.pop(); + + if (seen.has(top)) { + ok(false, + "This is a tree, not a graph: we shouldn't have multiple edges to the same node"); + } + seen.add(top); + if (seen.size % 1000 === 0) { + dumpn("Progress update: seen size = " + seen.size); + } + + var newNodes = dominatorTree.getImmediatelyDominated(top); + if (Object.prototype.toString.call(newNodes) !== "[object Array]") { + ok(false, "getImmediatelyDominated should return an array for known node ids"); + } + + var topSize = dominatorTree.getRetainedSize(top); + + var lastSize = Infinity; + for (var i = 0; i < newNodes.length; i++) { + if (typeof newNodes[i] !== "number") { + ok(false, "Every dominated id should be a number"); + } + + if (dominatorTree.getImmediateDominator(newNodes[i]) !== top) { + ok(false, "child's parent should be the expected parent"); + } + + var thisSize = dominatorTree.getRetainedSize(newNodes[i]); + + if (thisSize >= topSize) { + ok(false, "the size of children in the dominator tree should always be less than that of their parent"); + } + + if (thisSize > lastSize) { + ok(false, + "children should be sorted by greatest to least retained size, " + + "lastSize = " + lastSize + ", thisSize = " + thisSize); + } + + lastSize = thisSize; + stack.push(newNodes[i]); + } + } + + ok(true, "Successfully walked the tree"); + dumpn("Walked " + seen.size + " nodes"); + + do_test_finished(); +} diff --git a/dom/heapsnapshot/tests/unit/test_DominatorTree_06.js b/dom/heapsnapshot/tests/unit/test_DominatorTree_06.js new file mode 100644 index 000000000..680478623 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_DominatorTree_06.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the retained size of a node is the sum of its children retained +// sizes plus its shallow size. + +// Note that we don't assert directly, only if we get an unexpected +// value. There are just way too many nodes in the heap graph to assert for +// every one. This test would constantly time out and assertion messages would +// overflow the log size. +function fastAssert(cond, msg) { + if (!cond) { + ok(false, msg); + } +} + +var COUNT = { by: "count", count: false, bytes: true }; + +function run_test() { + var path = saveNewHeapSnapshot(); + var snapshot = ChromeUtils.readHeapSnapshot(path); + var dominatorTree = snapshot.computeDominatorTree(); + + // Do a traversal of the dominator tree and assert the relationship between + // retained size, shallow size, and children's retained sizes. + + var root = dominatorTree.root; + var stack = [root]; + while (stack.length > 0) { + var top = stack.pop(); + + var children = dominatorTree.getImmediatelyDominated(top); + + var topRetainedSize = dominatorTree.getRetainedSize(top); + var topShallowSize = snapshot.describeNode(COUNT, top).bytes; + fastAssert(topShallowSize <= topRetainedSize, + "The shallow size should be less than or equal to the " + + "retained size"); + + var sumOfChildrensRetainedSizes = 0; + for (var i = 0; i < children.length; i++) { + sumOfChildrensRetainedSizes += dominatorTree.getRetainedSize(children[i]); + stack.push(children[i]); + } + + fastAssert(sumOfChildrensRetainedSizes <= topRetainedSize, + "The sum of the children's retained sizes should be less than " + + "or equal to the retained size"); + fastAssert(sumOfChildrensRetainedSizes + topShallowSize === topRetainedSize, + "The sum of the children's retained sizes plus the shallow " + + "size should be equal to the retained size"); + } + + ok(true, "Successfully walked the tree"); + do_test_finished(); +} diff --git a/dom/heapsnapshot/tests/unit/test_HeapAnalyses_computeDominatorTree_01.js b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_computeDominatorTree_01.js new file mode 100644 index 000000000..0114e0b69 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_computeDominatorTree_01.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test the HeapAnalyses{Client,Worker} "computeDominatorTree" request. + +function run_test() { + run_next_test(); +} + +add_task(function* () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + yield client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + const dominatorTreeId = yield client.computeDominatorTree(snapshotFilePath); + equal(typeof dominatorTreeId, "number", + "should get a dominator tree id, and it should be a number"); + + client.destroy(); +}); diff --git a/dom/heapsnapshot/tests/unit/test_HeapAnalyses_computeDominatorTree_02.js b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_computeDominatorTree_02.js new file mode 100644 index 000000000..6e3f5b257 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_computeDominatorTree_02.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test the HeapAnalyses{Client,Worker} "computeDominatorTree" request with bad +// file paths. + +function run_test() { + run_next_test(); +} + +add_task(function* () { + const client = new HeapAnalysesClient(); + + let threw = false; + try { + yield client.computeDominatorTree("/etc/passwd"); + } catch (_) { + threw = true; + } + ok(threw, "should throw when given a bad path"); + + client.destroy(); +}); diff --git a/dom/heapsnapshot/tests/unit/test_HeapAnalyses_deleteHeapSnapshot_01.js b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_deleteHeapSnapshot_01.js new file mode 100644 index 000000000..7708de93c --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_deleteHeapSnapshot_01.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the HeapAnalyses{Client,Worker} can delete heap snapshots. + +function run_test() { + run_next_test(); +} + +const breakdown = { + by: "coarseType", + objects: { by: "count", count: true, bytes: true }, + scripts: { by: "count", count: true, bytes: true }, + strings: { by: "count", count: true, bytes: true }, + other: { by: "count", count: true, bytes: true }, +}; + +add_task(function* () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + yield client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + let dominatorTreeId = yield client.computeDominatorTree(snapshotFilePath); + ok(true, "Should have computed the dominator tree"); + + yield client.deleteHeapSnapshot(snapshotFilePath); + ok(true, "Should have deleted the snapshot"); + + let threw = false; + try { + yield client.getDominatorTree({ + dominatorTreeId: dominatorTreeId, + breakdown + }); + } catch (_) { + threw = true; + } + ok(threw, "getDominatorTree on deleted tree should throw an error"); + + threw = false; + try { + yield client.computeDominatorTree(snapshotFilePath); + } catch (_) { + threw = true; + } + ok(threw, "computeDominatorTree on deleted snapshot should throw an error"); + + threw = false; + try { + yield client.takeCensus(snapshotFilePath); + } catch (_) { + threw = true; + } + ok(threw, "takeCensus on deleted tree should throw an error"); + + client.destroy(); +}); diff --git a/dom/heapsnapshot/tests/unit/test_HeapAnalyses_deleteHeapSnapshot_02.js b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_deleteHeapSnapshot_02.js new file mode 100644 index 000000000..3e25ddac4 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_deleteHeapSnapshot_02.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test deleteHeapSnapshot is a noop if the provided path matches no snapshot + +function run_test() { + run_next_test(); +} + +add_task(function* () { + const client = new HeapAnalysesClient(); + + let threw = false; + try { + yield client.deleteHeapSnapshot("path-does-not-exist"); + } catch (_) { + threw = true; + } + ok(threw, "deleteHeapSnapshot on non-existant path should throw an error"); + + client.destroy(); +}); diff --git a/dom/heapsnapshot/tests/unit/test_HeapAnalyses_deleteHeapSnapshot_03.js b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_deleteHeapSnapshot_03.js new file mode 100644 index 000000000..e648c9407 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_deleteHeapSnapshot_03.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test other dominatorTrees can still be retrieved after deleting a snapshot + +function run_test() { + run_next_test(); +} + +const breakdown = { + by: "coarseType", + objects: { by: "count", count: true, bytes: true }, + scripts: { by: "count", count: true, bytes: true }, + strings: { by: "count", count: true, bytes: true }, + other: { by: "count", count: true, bytes: true }, +}; + +function* createSnapshotAndDominatorTree(client) { + let snapshotFilePath = saveNewHeapSnapshot(); + yield client.readHeapSnapshot(snapshotFilePath); + let dominatorTreeId = yield client.computeDominatorTree(snapshotFilePath); + return { dominatorTreeId, snapshotFilePath }; +} + +add_task(function* () { + const client = new HeapAnalysesClient(); + + let savedSnapshots = [ + yield createSnapshotAndDominatorTree(client), + yield createSnapshotAndDominatorTree(client), + yield createSnapshotAndDominatorTree(client) + ]; + ok(true, "Create 3 snapshots and dominator trees"); + + yield client.deleteHeapSnapshot(savedSnapshots[1].snapshotFilePath); + ok(true, "Snapshot deleted"); + + let tree = yield client.getDominatorTree({ + dominatorTreeId: savedSnapshots[0].dominatorTreeId, + breakdown + }); + ok(tree, "Should get a valid tree for first snapshot"); + + let threw = false; + try { + yield client.getDominatorTree({ + dominatorTreeId: savedSnapshots[1].dominatorTreeId, + breakdown + }); + } catch (_) { + threw = true; + } + ok(threw, "getDominatorTree on a deleted snapshot should throw an error"); + + tree = yield client.getDominatorTree({ + dominatorTreeId: savedSnapshots[2].dominatorTreeId, + breakdown + }); + ok(tree, "Should get a valid tree for third snapshot"); + + client.destroy(); +}); diff --git a/dom/heapsnapshot/tests/unit/test_HeapAnalyses_getCensusIndividuals_01.js b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_getCensusIndividuals_01.js new file mode 100644 index 000000000..b63ad4230 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_getCensusIndividuals_01.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the HeapAnalyses{Client,Worker} can get census individuals. + +function run_test() { + run_next_test(); +} + +const COUNT = { by: "count", count: true, bytes: true }; + +const CENSUS_BREAKDOWN = { + by: "coarseType", + objects: COUNT, + strings: COUNT, + scripts: COUNT, + other: COUNT, +}; + +const LABEL_BREAKDOWN = { + by: "internalType", + then: COUNT, +}; + +const MAX_INDIVIDUALS = 10; + +add_task(function* () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + yield client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + const dominatorTreeId = yield client.computeDominatorTree(snapshotFilePath); + ok(true, "Should have computed dominator tree"); + + const { report } = yield client.takeCensus(snapshotFilePath, + { breakdown: CENSUS_BREAKDOWN }, + { asTreeNode: true }); + ok(report, "Should get a report"); + + let nodesWithLeafIndicesFound = 0; + + yield* (function* assertCanGetIndividuals(censusNode) { + if (censusNode.reportLeafIndex !== undefined) { + nodesWithLeafIndicesFound++; + + const response = yield client.getCensusIndividuals({ + dominatorTreeId, + indices: DevToolsUtils.isSet(censusNode.reportLeafIndex) + ? censusNode.reportLeafIndex + : new Set([censusNode.reportLeafIndex]), + censusBreakdown: CENSUS_BREAKDOWN, + labelBreakdown: LABEL_BREAKDOWN, + maxRetainingPaths: 1, + maxIndividuals: MAX_INDIVIDUALS, + }); + + dumpn(`response = ${JSON.stringify(response, null, 4)}`); + + equal(response.nodes.length, Math.min(MAX_INDIVIDUALS, censusNode.count), + "response.nodes.length === Math.min(MAX_INDIVIDUALS, censusNode.count)"); + + let lastRetainedSize = Infinity; + for (let individual of response.nodes) { + equal(typeof individual.nodeId, "number", + "individual.nodeId should be a number"); + ok(individual.retainedSize <= lastRetainedSize, + "individual.retainedSize <= lastRetainedSize"); + lastRetainedSize = individual.retainedSize; + ok(individual.shallowSize, "individual.shallowSize should exist and be non-zero"); + ok(individual.shortestPaths, "individual.shortestPaths should exist"); + ok(individual.shortestPaths.nodes, "individual.shortestPaths.nodes should exist"); + ok(individual.shortestPaths.edges, "individual.shortestPaths.edges should exist"); + ok(individual.label, "individual.label should exist"); + } + } + + if (censusNode.children) { + for (let child of censusNode.children) { + yield* assertCanGetIndividuals(child); + } + } + }(report)); + + equal(nodesWithLeafIndicesFound, 4, "Should have found a leaf for each coarse type"); + + client.destroy(); +}); diff --git a/dom/heapsnapshot/tests/unit/test_HeapAnalyses_getCreationTime_01.js b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_getCreationTime_01.js new file mode 100644 index 000000000..5df79de7a --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_getCreationTime_01.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the HeapAnalyses{Client,Worker} can get a HeapSnapshot's +// creation time. + +function waitForThirtyMilliseconds() { + const start = Date.now(); + while (Date.now() - start < 30) ; +} + +function run_test() { + run_next_test(); +} + +const BREAKDOWN = { + by: "internalType", + then: { by: "count", count: true, bytes: true } +}; + +add_task(function* () { + const client = new HeapAnalysesClient(); + const start = Date.now() * 1000; + + // Because Date.now() is less precise than the snapshot's time stamp, give it + // a little bit of head room. Additionally, WinXP's timers have a granularity + // of only +/-15 ms. + waitForThirtyMilliseconds(); + const snapshotFilePath = saveNewHeapSnapshot(); + waitForThirtyMilliseconds(); + const end = Date.now() * 1000; + + yield client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + let threw = false; + try { + yield client.getCreationTime("/not/a/real/path", { + breakdown: BREAKDOWN + }); + } catch (_) { + threw = true; + } + ok(threw, "getCreationTime should throw when snapshot does not exist"); + + let time = yield client.getCreationTime(snapshotFilePath, { + breakdown: BREAKDOWN + }); + + dumpn("Start = " + start); + dumpn("End = " + end); + dumpn("Time = " + time); + + ok(time >= start, "creation time occurred after start"); + ok(time <= end, "creation time occurred before end"); + + client.destroy(); +}); diff --git a/dom/heapsnapshot/tests/unit/test_HeapAnalyses_getDominatorTree_01.js b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_getDominatorTree_01.js new file mode 100644 index 000000000..cedea5375 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_getDominatorTree_01.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test the HeapAnalyses{Client,Worker} "getDominatorTree" request. + +function run_test() { + run_next_test(); +} + +const breakdown = { + by: "coarseType", + objects: { by: "count", count: true, bytes: true }, + scripts: { by: "count", count: true, bytes: true }, + strings: { by: "count", count: true, bytes: true }, + other: { by: "count", count: true, bytes: true }, +}; + +add_task(function* () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + yield client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + const dominatorTreeId = yield client.computeDominatorTree(snapshotFilePath); + equal(typeof dominatorTreeId, "number", + "should get a dominator tree id, and it should be a number"); + + const partialTree = yield client.getDominatorTree({ + dominatorTreeId, + breakdown + }); + ok(partialTree, "Should get a partial tree"); + equal(typeof partialTree, "object", + "partialTree should be an object"); + + function checkTree(node) { + equal(typeof node.nodeId, "number", "each node should have an id"); + + if (node === partialTree) { + equal(node.parentId, undefined, "the root has no parent"); + } else { + equal(typeof node.parentId, "number", "each node should have a parent id"); + } + + equal(typeof node.retainedSize, "number", + "each node should have a retained size"); + + ok(node.children === undefined || Array.isArray(node.children), + "each node either has a list of children, or undefined meaning no children loaded"); + equal(typeof node.moreChildrenAvailable, "boolean", + "each node should indicate if there are more children available or not"); + + equal(typeof node.shortestPaths, "object", + "Should have shortest paths"); + equal(typeof node.shortestPaths.nodes, "object", + "Should have shortest paths' nodes"); + equal(typeof node.shortestPaths.edges, "object", + "Should have shortest paths' edges"); + + if (node.children) { + node.children.forEach(checkTree); + } + } + + checkTree(partialTree); + + client.destroy(); +}); diff --git a/dom/heapsnapshot/tests/unit/test_HeapAnalyses_getDominatorTree_02.js b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_getDominatorTree_02.js new file mode 100644 index 000000000..fd29beece --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_getDominatorTree_02.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test the HeapAnalyses{Client,Worker} "getDominatorTree" request with bad +// dominator tree ids. + +function run_test() { + run_next_test(); +} + +const breakdown = { + by: "coarseType", + objects: { by: "count", count: true, bytes: true }, + scripts: { by: "count", count: true, bytes: true }, + strings: { by: "count", count: true, bytes: true }, + other: { by: "count", count: true, bytes: true }, +}; + +add_task(function* () { + const client = new HeapAnalysesClient(); + + let threw = false; + try { + yield client.getDominatorTree({ dominatorTreeId: 42, breakdown }); + } catch (_) { + threw = true; + } + ok(threw, "should throw when given a bad id"); + + client.destroy(); +}); diff --git a/dom/heapsnapshot/tests/unit/test_HeapAnalyses_getImmediatelyDominated_01.js b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_getImmediatelyDominated_01.js new file mode 100644 index 000000000..caf1c2056 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_getImmediatelyDominated_01.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test the HeapAnalyses{Client,Worker} "getImmediatelyDominated" request. + +function run_test() { + run_next_test(); +} + +const breakdown = { + by: "coarseType", + objects: { by: "count", count: true, bytes: true }, + scripts: { by: "count", count: true, bytes: true }, + strings: { by: "count", count: true, bytes: true }, + other: { by: "count", count: true, bytes: true }, +}; + +add_task(function* () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + yield client.readHeapSnapshot(snapshotFilePath); + const dominatorTreeId = yield client.computeDominatorTree(snapshotFilePath); + + const partialTree = yield client.getDominatorTree({ + dominatorTreeId, + breakdown + }); + ok(partialTree.children.length > 0, + "root should immediately dominate some nodes"); + + // First, test getting a subset of children available. + const response = yield client.getImmediatelyDominated({ + dominatorTreeId, + breakdown, + nodeId: partialTree.nodeId, + startIndex: 0, + maxCount: partialTree.children.length - 1 + }); + + ok(Array.isArray(response.nodes)); + ok(response.nodes.every(node => node.parentId === partialTree.nodeId)); + ok(response.moreChildrenAvailable); + equal(response.path.length, 1); + equal(response.path[0], partialTree.nodeId); + + for (let node of response.nodes) { + equal(typeof node.shortestPaths, "object", + "Should have shortest paths"); + equal(typeof node.shortestPaths.nodes, "object", + "Should have shortest paths' nodes"); + equal(typeof node.shortestPaths.edges, "object", + "Should have shortest paths' edges"); + } + + // Next, test getting a subset of children available. + const secondResponse = yield client.getImmediatelyDominated({ + dominatorTreeId, + breakdown, + nodeId: partialTree.nodeId, + startIndex: 0, + maxCount: Infinity + }); + + ok(Array.isArray(secondResponse.nodes)); + ok(secondResponse.nodes.every(node => node.parentId === partialTree.nodeId)); + ok(!secondResponse.moreChildrenAvailable); + equal(secondResponse.path.length, 1); + equal(secondResponse.path[0], partialTree.nodeId); + + for (let node of secondResponse.nodes) { + equal(typeof node.shortestPaths, "object", + "Should have shortest paths"); + equal(typeof node.shortestPaths.nodes, "object", + "Should have shortest paths' nodes"); + equal(typeof node.shortestPaths.edges, "object", + "Should have shortest paths' edges"); + } + + client.destroy(); +}); diff --git a/dom/heapsnapshot/tests/unit/test_HeapAnalyses_readHeapSnapshot_01.js b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_readHeapSnapshot_01.js new file mode 100644 index 000000000..0d0d58bef --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_readHeapSnapshot_01.js @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the HeapAnalyses{Client,Worker} can read heap snapshots. + +function run_test() { + run_next_test(); +} + +add_task(function* () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + yield client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + client.destroy(); +}); diff --git a/dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensusDiff_01.js b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensusDiff_01.js new file mode 100644 index 000000000..6f22cbad3 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensusDiff_01.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the HeapAnalyses{Client,Worker} can take diffs between censuses. + +function run_test() { + run_next_test(); +} + +const BREAKDOWN = { + by: "objectClass", + then: { by: "count", count: true, bytes: false }, + other: { by: "count", count: true, bytes: false }, +}; + +add_task(function* () { + const client = new HeapAnalysesClient(); + + const markers = [allocationMarker()]; + + const firstSnapshotFilePath = saveNewHeapSnapshot(); + + // Allocate and hold an additional AllocationMarker object so we can see it in + // the next heap snapshot. + markers.push(allocationMarker()); + + const secondSnapshotFilePath = saveNewHeapSnapshot(); + + yield client.readHeapSnapshot(firstSnapshotFilePath); + yield client.readHeapSnapshot(secondSnapshotFilePath); + ok(true, "Should have read both heap snapshot files"); + + const { delta } = yield client.takeCensusDiff(firstSnapshotFilePath, + secondSnapshotFilePath, + { breakdown: BREAKDOWN }); + + equal(delta.AllocationMarker.count, 1, + "There exists one new AllocationMarker in the second heap snapshot"); + + const { delta: deltaTreeNode } = yield client.takeCensusDiff(firstSnapshotFilePath, + secondSnapshotFilePath, + { breakdown: BREAKDOWN }, + { asTreeNode: true }); + + // Have to manually set these because symbol properties aren't structured + // cloned. + delta[CensusUtils.basisTotalBytes] = deltaTreeNode.totalBytes; + delta[CensusUtils.basisTotalCount] = deltaTreeNode.totalCount; + + compareCensusViewData(BREAKDOWN, delta, deltaTreeNode, + "Returning delta-census as a tree node represents same data as the report"); + + client.destroy(); +}); diff --git a/dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensusDiff_02.js b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensusDiff_02.js new file mode 100644 index 000000000..f1ba9ce84 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensusDiff_02.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the HeapAnalyses{Client,Worker} can take diffs between censuses as +// inverted trees. + +function run_test() { + run_next_test(); +} + +const BREAKDOWN = { + by: "coarseType", + objects: { + by: "objectClass", + then: { by: "count", count: true, bytes: true }, + other: { by: "count", count: true, bytes: true }, + }, + scripts: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + strings: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + other: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, +}; + +add_task(function* () { + const firstSnapshotFilePath = saveNewHeapSnapshot(); + const secondSnapshotFilePath = saveNewHeapSnapshot(); + + const client = new HeapAnalysesClient(); + yield client.readHeapSnapshot(firstSnapshotFilePath); + yield client.readHeapSnapshot(secondSnapshotFilePath); + + ok(true, "Should have read both heap snapshot files"); + + const { delta } = yield client.takeCensusDiff(firstSnapshotFilePath, + secondSnapshotFilePath, + { breakdown: BREAKDOWN }); + + const { delta: deltaTreeNode } = yield client.takeCensusDiff(firstSnapshotFilePath, + secondSnapshotFilePath, + { breakdown: BREAKDOWN }, + { asInvertedTreeNode: true }); + + // Have to manually set these because symbol properties aren't structured + // cloned. + delta[CensusUtils.basisTotalBytes] = deltaTreeNode.totalBytes; + delta[CensusUtils.basisTotalCount] = deltaTreeNode.totalCount; + + compareCensusViewData(BREAKDOWN, delta, deltaTreeNode, { invert: true }); + + client.destroy(); +}); diff --git a/dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_01.js b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_01.js new file mode 100644 index 000000000..e26981db4 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_01.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the HeapAnalyses{Client,Worker} can take censuses. + +function run_test() { + run_next_test(); +} + +add_task(function* () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + yield client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + const { report } = yield client.takeCensus(snapshotFilePath); + ok(report, "Should get a report"); + equal(typeof report, "object", "report should be an object"); + + ok(report.objects); + ok(report.scripts); + ok(report.strings); + ok(report.other); + + client.destroy(); +}); diff --git a/dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_02.js b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_02.js new file mode 100644 index 000000000..34494af70 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_02.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the HeapAnalyses{Client,Worker} can take censuses with breakdown +// options. + +function run_test() { + run_next_test(); +} + +add_task(function* () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + yield client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + const { report } = yield client.takeCensus(snapshotFilePath, { + breakdown: { by: "count", count: true, bytes: true } + }); + + ok(report, "Should get a report"); + equal(typeof report, "object", "report should be an object"); + + ok(report.count); + ok(report.bytes); + + client.destroy(); +}); diff --git a/dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_03.js b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_03.js new file mode 100644 index 000000000..486e250b5 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_03.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the HeapAnalyses{Client,Worker} bubbles errors properly when things +// go wrong. + +function run_test() { + run_next_test(); +} + +add_task(function* () { + const client = new HeapAnalysesClient(); + + // Snapshot file path to a file that doesn't exist. + let failed = false; + try { + yield client.readHeapSnapshot(getFilePath("foo-bar-baz" + Math.random(), true)); + } catch (e) { + failed = true; + } + ok(failed, "should not read heap snapshots that do not exist"); + + // Snapshot file path to a file that is not a heap snapshot. + failed = false; + try { + yield client.readHeapSnapshot(getFilePath("test_HeapAnalyses_takeCensus_03.js")); + } catch (e) { + failed = true; + } + ok(failed, "should not be able to read a file that is not a heap snapshot as a heap snapshot"); + + const snapshotFilePath = saveNewHeapSnapshot(); + yield client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + // Bad census breakdown options. + failed = false; + try { + yield client.takeCensus(snapshotFilePath, { + breakdown: { by: "some classification that we do not have" } + }); + } catch (e) { + failed = true; + } + ok(failed, "should not be able to breakdown by an unknown classification"); + + client.destroy(); +}); diff --git a/dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_04.js b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_04.js new file mode 100644 index 000000000..769a2d99d --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_04.js @@ -0,0 +1,118 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the HeapAnalyses{Client,Worker} can send SavedFrame stacks from +// by-allocation-stack reports from the worker. + +function run_test() { + run_next_test(); +} + +add_task(function* test() { + const client = new HeapAnalysesClient(); + + // Track some allocation stacks. + + const g = newGlobal(); + const dbg = new Debugger(g); + g.eval(` // 1 + this.log = []; // 2 + function f() { this.log.push(allocationMarker()); } // 3 + function g() { this.log.push(allocationMarker()); } // 4 + function h() { this.log.push(allocationMarker()); } // 5 + `); // 6 + + // Create one allocationMarker with tracking turned off, + // so it will have no associated stack. + g.f(); + + dbg.memory.allocationSamplingProbability = 1; + + for (let [func, n] of [ [g.f, 20], + [g.g, 10], + [g.h, 5] ]) { + for (let i = 0; i < n; i++) { + dbg.memory.trackingAllocationSites = true; + // All allocations of allocationMarker occur with this line as the oldest + // stack frame. + func(); + dbg.memory.trackingAllocationSites = false; + } + } + + // Take a heap snapshot. + + const snapshotFilePath = saveNewHeapSnapshot({ debugger: dbg }); + yield client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + // Run a census broken down by class name -> allocation stack so we can grab + // only the AllocationMarker objects we have complete control over. + + const { report } = yield client.takeCensus(snapshotFilePath, { + breakdown: { by: "objectClass", + then: { by: "allocationStack", + then: { by: "count", + bytes: true, + count: true + }, + noStack: { by: "count", + bytes: true, + count: true + } + } + } + }); + + // Test the generated report. + + ok(report, "Should get a report"); + + const map = report.AllocationMarker; + ok(map, "Should get AllocationMarkers in the report."); + // From a module with a different global, and therefore a different Map + // constructor, so we can't use instanceof. + equal(map.__proto__.constructor.name, "Map"); + + equal(map.size, 4, "Should have 4 allocation stacks (including the lack of a stack)"); + + // Gather the stacks we are expecting to appear as keys, and + // check that there are no unexpected keys. + let stacks = {}; + + map.forEach((v, k) => { + if (k === "noStack") { + // No need to save this key. + } else if (k.functionDisplayName === "f" && + k.parent.functionDisplayName === "test") { + stacks.f = k; + } else if (k.functionDisplayName === "g" && + k.parent.functionDisplayName === "test") { + stacks.g = k; + } else if (k.functionDisplayName === "h" && + k.parent.functionDisplayName === "test") { + stacks.h = k; + } else { + dumpn("Unexpected allocation stack:"); + k.toString().split(/\n/g).forEach(s => dumpn(s)); + ok(false); + } + }); + + ok(map.get("noStack")); + equal(map.get("noStack").count, 1); + + ok(stacks.f); + ok(map.get(stacks.f)); + equal(map.get(stacks.f).count, 20); + + ok(stacks.g); + ok(map.get(stacks.g)); + equal(map.get(stacks.g).count, 10); + + ok(stacks.h); + ok(map.get(stacks.h)); + equal(map.get(stacks.h).count, 5); + + client.destroy(); +}); diff --git a/dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_05.js b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_05.js new file mode 100644 index 000000000..7e16d9f00 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_05.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the HeapAnalyses{Client,Worker} can take censuses and return +// a CensusTreeNode. + +function run_test() { + run_next_test(); +} + +const BREAKDOWN = { + by: "internalType", + then: { by: "count", count: true, bytes: true } +}; + +add_task(function* () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + yield client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + const { report } = yield client.takeCensus(snapshotFilePath, { + breakdown: BREAKDOWN + }); + + const { report: treeNode } = yield client.takeCensus(snapshotFilePath, { + breakdown: BREAKDOWN + }, { + asTreeNode: true + }); + + ok(treeNode.children.length > 0, "treeNode has children"); + ok(treeNode.children.every(type => { + return "name" in type && + "bytes" in type && + "count" in type; + }), "all of tree node's children have name, bytes, count"); + + compareCensusViewData(BREAKDOWN, report, treeNode, + "Returning census as a tree node represents same data as the report"); + + client.destroy(); +}); diff --git a/dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_06.js b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_06.js new file mode 100644 index 000000000..7795a9700 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_06.js @@ -0,0 +1,109 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the HeapAnalyses{Client,Worker} can take censuses by +// "allocationStack" and return a CensusTreeNode. + +function run_test() { + run_next_test(); +} + +const BREAKDOWN = { + by: "objectClass", + then: { + by: "allocationStack", + then: { by: "count", count: true, bytes: true }, + noStack: { by: "count", count: true, bytes: true } + }, + other: { by: "count", count: true, bytes: true } +}; + +add_task(function* () { + const g = newGlobal(); + const dbg = new Debugger(g); + + // 5 allocation markers with no stack. + g.eval(` + this.markers = []; + for (var i = 0; i < 5; i++) { + markers.push(allocationMarker()); + } + `); + + dbg.memory.allocationSamplingProbability = 1; + dbg.memory.trackingAllocationSites = true; + + // 5 allocation markers at 5 stacks. + g.eval(` + (function shouldHaveCountOfOne() { + markers.push(allocationMarker()); + markers.push(allocationMarker()); + markers.push(allocationMarker()); + markers.push(allocationMarker()); + markers.push(allocationMarker()); + }()); + `); + + // 5 allocation markers at 1 stack. + g.eval(` + (function shouldHaveCountOfFive() { + for (var i = 0; i < 5; i++) { + markers.push(allocationMarker()); + } + }()); + `); + + const snapshotFilePath = saveNewHeapSnapshot({ debugger: dbg }); + + const client = new HeapAnalysesClient(); + yield client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + const { report } = yield client.takeCensus(snapshotFilePath, { + breakdown: BREAKDOWN + }); + + const { report: treeNode } = yield client.takeCensus(snapshotFilePath, { + breakdown: BREAKDOWN + }, { + asTreeNode: true + }); + + const markers = treeNode.children.find(c => c.name === "AllocationMarker"); + ok(markers); + + const noStack = markers.children.find(c => c.name === "noStack"); + equal(noStack.count, 5); + + let numShouldHaveFiveFound = 0; + let numShouldHaveOneFound = 0; + + function walk(node) { + if (node.children) { + node.children.forEach(walk); + } + + if (!isSavedFrame(node.name)) { + return; + } + + if (node.name.functionDisplayName === "shouldHaveCountOfFive") { + equal(node.count, 5, "shouldHaveCountOfFive should have count of five"); + numShouldHaveFiveFound++; + } + + if (node.name.functionDisplayName === "shouldHaveCountOfOne") { + equal(node.count, 1, "shouldHaveCountOfOne should have count of one"); + numShouldHaveOneFound++; + } + } + markers.children.forEach(walk); + + equal(numShouldHaveFiveFound, 1); + equal(numShouldHaveOneFound, 5); + + compareCensusViewData(BREAKDOWN, report, treeNode, + "Returning census as a tree node represents same data as the report"); + + client.destroy(); +}); diff --git a/dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_07.js b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_07.js new file mode 100644 index 000000000..986b3aaa8 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_07.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the HeapAnalyses{Client,Worker} can take censuses and return +// an inverted CensusTreeNode. + +function run_test() { + run_next_test(); +} + +const BREAKDOWN = { + by: "coarseType", + objects: { + by: "objectClass", + then: { by: "count", count: true, bytes: true }, + other: { by: "count", count: true, bytes: true }, + }, + scripts: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + strings: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + other: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, +}; + +add_task(function* () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + yield client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + const { report } = yield client.takeCensus(snapshotFilePath, { + breakdown: BREAKDOWN + }); + + const { report: treeNode } = yield client.takeCensus(snapshotFilePath, { + breakdown: BREAKDOWN + }, { + asInvertedTreeNode: true + }); + + compareCensusViewData(BREAKDOWN, report, treeNode, { invert: true }); + + client.destroy(); +}); diff --git a/dom/heapsnapshot/tests/unit/test_HeapSnapshot_computeShortestPaths_01.js b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_computeShortestPaths_01.js new file mode 100644 index 000000000..2ec577bd0 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_computeShortestPaths_01.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Sanity test that we can compute shortest paths. +// +// Because the actual heap graph is too unpredictable and likely to drastically +// change as various implementation bits change, we don't test exact paths +// here. See js/src/jsapi-tests/testUbiNode.cpp for such tests, where we can +// control the specific graph shape and structure and so testing exact paths is +// reliable. + +function run_test() { + const path = ChromeUtils.saveHeapSnapshot({ runtime: true }); + const snapshot = ChromeUtils.readHeapSnapshot(path); + + const dominatorTree = snapshot.computeDominatorTree(); + const dominatedByRoot = dominatorTree.getImmediatelyDominated(dominatorTree.root) + .slice(0, 10); + ok(dominatedByRoot); + ok(dominatedByRoot.length); + + const targetSet = new Set(dominatedByRoot); + + const shortestPaths = snapshot.computeShortestPaths(dominatorTree.root, dominatedByRoot, 2); + ok(shortestPaths); + ok(shortestPaths instanceof Map); + ok(shortestPaths.size === targetSet.size); + + for (let [target, paths] of shortestPaths) { + ok(targetSet.has(target), + "We should only get paths for our targets"); + targetSet.delete(target); + + ok(paths.length > 0, + "We must have at least one path, since the target is dominated by the root"); + ok(paths.length <= 2, + "Should not have recorded more paths than the max requested"); + + dumpn("---------------------"); + dumpn("Shortest paths for 0x" + target.toString(16) + ":"); + for (let path of paths) { + dumpn(" path ="); + for (let part of path) { + dumpn(" predecessor: 0x" + part.predecessor.toString(16) + + "; edge: " + part.edge); + } + } + dumpn("---------------------"); + + for (let path of paths) { + ok(path.length > 0, "Cannot have zero length paths"); + ok(path[0].predecessor === dominatorTree.root, + "The first predecessor is always our start node"); + + for (let part of path) { + ok(part.predecessor, "Each part of a path has a predecessor"); + ok(!!snapshot.describeNode({ by: "count", count: true, bytes: true}, + part.predecessor), + "The predecessor is in the heap snapshot"); + ok("edge" in part, "Each part has an (potentially null) edge property"); + } + } + } + + ok(targetSet.size === 0, + "We found paths for all of our targets"); + + do_test_finished(); +} diff --git a/dom/heapsnapshot/tests/unit/test_HeapSnapshot_computeShortestPaths_02.js b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_computeShortestPaths_02.js new file mode 100644 index 000000000..04fe58733 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_computeShortestPaths_02.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test computing shortest paths with invalid arguments. + +function run_test() { + const path = ChromeUtils.saveHeapSnapshot({ runtime: true }); + const snapshot = ChromeUtils.readHeapSnapshot(path); + + const dominatorTree = snapshot.computeDominatorTree(); + const target = dominatorTree.getImmediatelyDominated(dominatorTree.root).pop(); + ok(target); + + let threw = false; + try { + snapshot.computeShortestPaths(0, [target], 2); + } catch (_) { + threw = true; + } + ok(threw, "invalid start node should throw"); + + threw = false; + try { + snapshot.computeShortestPaths(dominatorTree.root, [0], 2); + } catch (_) { + threw = true; + } + ok(threw, "invalid target nodes should throw"); + + threw = false; + try { + snapshot.computeShortestPaths(dominatorTree.root, [], 2); + } catch (_) { + threw = true; + } + ok(threw, "empty target nodes should throw"); + + threw = false; + try { + snapshot.computeShortestPaths(dominatorTree.root, [target], 0); + } catch (_) { + threw = true; + } + ok(threw, "0 max paths should throw"); + + do_test_finished(); +} diff --git a/dom/heapsnapshot/tests/unit/test_HeapSnapshot_creationTime_01.js b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_creationTime_01.js new file mode 100644 index 000000000..0d08fea16 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_creationTime_01.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// HeapSnapshot.prototype.creationTime returns the expected time. + +function waitForThirtyMilliseconds() { + const start = Date.now(); + while (Date.now() - start < 30) ; +} + +function run_test() { + const start = Date.now() * 1000; + do_print("start = " + start); + + // Because Date.now() is less precise than the snapshot's time stamp, give it + // a little bit of head room. Additionally, WinXP's timer only has granularity + // of +/- 15ms. + waitForThirtyMilliseconds(); + const path = ChromeUtils.saveHeapSnapshot({ runtime: true }); + waitForThirtyMilliseconds(); + + const end = Date.now() * 1000; + do_print("end = " + end); + + const snapshot = ChromeUtils.readHeapSnapshot(path); + do_print("snapshot.creationTime = " + snapshot.creationTime); + + ok(snapshot.creationTime >= start); + ok(snapshot.creationTime <= end); +} diff --git a/dom/heapsnapshot/tests/unit/test_HeapSnapshot_deepStack_01.js b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_deepStack_01.js new file mode 100644 index 000000000..9eb11d9af --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_deepStack_01.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we can save a core dump with very deep allocation stacks and read +// it back into a HeapSnapshot. + +function stackDepth(stack) { + return stack ? 1 + stackDepth(stack.parent) : 0; +} + +function run_test() { + // Create a Debugger observing a debuggee's allocations. + const debuggee = new Cu.Sandbox(null); + const dbg = new Debugger(debuggee); + dbg.memory.trackingAllocationSites = true; + + // Allocate some objects in the debuggee that will have their allocation + // stacks recorded by the Debugger. + + debuggee.eval("this.objects = []"); + debuggee.eval( + (function recursiveAllocate(n) { + if (n <= 0) + return; + + // Make sure to recurse before pushing the object so that when TCO is + // implemented sometime in the future, it doesn't invalidate this test. + recursiveAllocate(n - 1); + this.objects.push({}); + }).toString() + ); + debuggee.eval("recursiveAllocate = recursiveAllocate.bind(this);"); + debuggee.eval("recursiveAllocate(200);"); + + // Now save a snapshot that will include the allocation stacks and read it + // back again. + + const filePath = ChromeUtils.saveHeapSnapshot({ runtime: true }); + ok(true, "Should be able to save a snapshot."); + + const snapshot = ChromeUtils.readHeapSnapshot(filePath); + ok(snapshot, "Should be able to read a heap snapshot"); + ok(snapshot instanceof HeapSnapshot, "Should be an instanceof HeapSnapshot"); + + const report = snapshot.takeCensus({ + breakdown: { by: "allocationStack", + then: { by: "count", bytes: true, count: true }, + noStack: { by: "count", bytes: true, count: true } + } + }); + + // Keep this synchronized with `HeapSnapshot::MAX_STACK_DEPTH`! + const MAX_STACK_DEPTH = 60; + + let foundStacks = false; + report.forEach((v, k) => { + if (k === "noStack") { + return; + } + + foundStacks = true; + const depth = stackDepth(k); + dumpn("Stack depth is " + depth); + ok(depth <= MAX_STACK_DEPTH, + "Every stack should have depth less than or equal to the maximum stack depth"); + }); + ok(foundStacks); + + do_test_finished(); +} diff --git a/dom/heapsnapshot/tests/unit/test_HeapSnapshot_describeNode_01.js b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_describeNode_01.js new file mode 100644 index 000000000..d79cb5a7b --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_describeNode_01.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we can describe nodes with a breakdown. + +function run_test() { + const path = saveNewHeapSnapshot(); + const snapshot = ChromeUtils.readHeapSnapshot(path); + ok(snapshot.describeNode); + equal(typeof snapshot.describeNode, "function"); + + const dt = snapshot.computeDominatorTree(); + + let threw = false; + try { + snapshot.describeNode(undefined, dt.root); + } catch (_) { + threw = true; + } + ok(threw, "Should require a breakdown"); + + const breakdown = { + by: "coarseType", + objects: { by: "objectClass" }, + scripts: { by: "internalType" }, + strings: { by: "internalType" }, + other: { by: "internalType" } + }; + + threw = false; + try { + snapshot.describeNode(breakdown, 0); + } catch (_) { + threw = true; + } + ok(threw, "Should throw when given an invalid node id"); + + const description = snapshot.describeNode(breakdown, dt.root); + ok(description); + ok(description.other); + ok(description.other["JS::ubi::RootList"]); +} diff --git a/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_01.js b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_01.js new file mode 100644 index 000000000..f3b3090b0 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_01.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// HeapSnapshot.prototype.takeCensus returns a value of an appropriate +// shape. Ported from js/src/jit-tests/debug/Memory-takeCensus-01.js + +function run_test() { + var dbg = new Debugger; + + function checkProperties(census) { + equal(typeof census, "object"); + for (prop of Object.getOwnPropertyNames(census)) { + var desc = Object.getOwnPropertyDescriptor(census, prop); + equal(desc.enumerable, true); + equal(desc.configurable, true); + equal(desc.writable, true); + if (typeof desc.value === "object") + checkProperties(desc.value); + else + equal(typeof desc.value, "number"); + } + } + + checkProperties(saveHeapSnapshotAndTakeCensus(dbg)); + + var g = newGlobal(); + dbg.addDebuggee(g); + checkProperties(saveHeapSnapshotAndTakeCensus(dbg)); + + do_test_finished(); +} diff --git a/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_02.js b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_02.js new file mode 100644 index 000000000..680ac9b58 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_02.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// HeapSnapshot.prototype.takeCensus behaves plausibly as we allocate objects. +// +// Exact object counts vary in ways we can't predict. For example, +// BaselineScripts can hold onto "template objects", which exist only to hold +// the shape and type for newly created objects. When BaselineScripts are +// discarded, these template objects go with them. +// +// So instead of expecting precise counts, we expect counts that are at least as +// many as we would expect given the object graph we've built. +// +// Ported from js/src/jit-tests/debug/Memory-takeCensus-02.js + +function run_test() { + // A Debugger with no debuggees had better not find anything. + var dbg = new Debugger; + var census0 = saveHeapSnapshotAndTakeCensus(dbg); + Census.walkCensus(census0, "census0", Census.assertAllZeros); + + function newGlobalWithDefs() { + var g = newGlobal(); + g.eval(` + function times(n, fn) { + var a=[]; + for (var i = 0; i<n; i++) + a.push(fn()); + return a; + } + `); + return g; + } + + // Allocate a large number of various types of objects, and check that census + // finds them. + var g = newGlobalWithDefs(); + dbg.addDebuggee(g); + + g.eval("var objs = times(100, () => ({}));"); + g.eval("var rxs = times(200, () => /foo/);"); + g.eval("var ars = times(400, () => []);"); + g.eval("var fns = times(800, () => () => {});"); + + var census1 = dbg.memory.takeCensus(dbg); + Census.walkCensus(census1, "census1", + Census.assertAllNotLessThan( + { "objects": + { "Object": { count: 100 }, + "RegExp": { count: 200 }, + "Array": { count: 400 }, + "Function": { count: 800 } + } + })); + + do_test_finished(); +} diff --git a/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_03.js b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_03.js new file mode 100644 index 000000000..25f2c3791 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_03.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// HeapSnapshot.prototype.takeCensus behaves plausibly as we add and remove +// debuggees. +// +// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-03.js + +function run_test() { + var dbg = new Debugger; + + var census0 = saveHeapSnapshotAndTakeCensus(dbg); + Census.walkCensus(census0, "census0", Census.assertAllZeros); + + var g1 = newGlobal(); + dbg.addDebuggee(g1); + var census1 = saveHeapSnapshotAndTakeCensus(dbg); + Census.walkCensus(census1, "census1", Census.assertAllNotLessThan(census0)); + + var g2 = newGlobal(); + dbg.addDebuggee(g2); + var census2 = saveHeapSnapshotAndTakeCensus(dbg); + Census.walkCensus(census2, "census2", Census.assertAllNotLessThan(census1)); + + dbg.removeDebuggee(g2); + var census3 = saveHeapSnapshotAndTakeCensus(dbg); + Census.walkCensus(census3, "census3", Census.assertAllEqual(census1)); + + dbg.removeDebuggee(g1); + var census4 = saveHeapSnapshotAndTakeCensus(dbg); + Census.walkCensus(census4, "census4", Census.assertAllEqual(census0)); + + do_test_finished(); +} diff --git a/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_04.js b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_04.js new file mode 100644 index 000000000..799844cde --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_04.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that HeapSnapshot.prototype.takeCensus finds GC roots that are on the +// stack. +// +// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-04.js + +function run_test() { + var g = newGlobal(); + var dbg = new Debugger(g); + + g.eval(` +function withAllocationMarkerOnStack(f) { + (function () { + var onStack = allocationMarker(); + f(); + }()); +} +`); + + equal("AllocationMarker" in saveHeapSnapshotAndTakeCensus(dbg).objects, false, + "There shouldn't exist any allocation markers in the census."); + + var allocationMarkerCount; + g.withAllocationMarkerOnStack(() => { + const census = saveHeapSnapshotAndTakeCensus(dbg); + allocationMarkerCount = census.objects.AllocationMarker.count; + }); + + equal(allocationMarkerCount, 1, + "Should have one allocation marker in the census, because there " + + "was one on the stack."); + + do_test_finished(); +} diff --git a/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_05.js b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_05.js new file mode 100644 index 000000000..da6067624 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_05.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that HeapSnapshot.prototype.takeCensus finds cross compartment +// wrapper GC roots. +// +// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-05.js + +function run_test() { + var g = newGlobal(); + var dbg = new Debugger(g); + + equal("AllocationMarker" in saveHeapSnapshotAndTakeCensus(dbg).objects, false, + "No allocation markers should exist in the census."); + + this.ccw = g.allocationMarker(); + + const census = saveHeapSnapshotAndTakeCensus(dbg); + equal(census.objects.AllocationMarker.count, 1, + "Should have one allocation marker in the census, because there " + + "is one cross-compartment wrapper referring to it."); + + do_test_finished(); +} diff --git a/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_06.js b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_06.js new file mode 100644 index 000000000..0412410c0 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_06.js @@ -0,0 +1,125 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check HeapSnapshot.prototype.takeCensus handling of 'breakdown' argument. +// +// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-06.js + +function run_test() { + var Pattern = Match.Pattern; + + var g = newGlobal(); + var dbg = new Debugger(g); + + Pattern({ count: Pattern.NATURAL, + bytes: Pattern.NATURAL }) + .assert(saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "count" } })); + + let census = saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "count", count: false, bytes: false } }); + equal("count" in census, false); + equal("bytes" in census, false); + + census = saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "count", count: true, bytes: false } }); + equal("count" in census, true); + equal("bytes" in census, false); + + census = saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "count", count: false, bytes: true } }); + equal("count" in census, false); + equal("bytes" in census, true); + + census = saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "count", count: true, bytes: true } }); + equal("count" in census, true); + equal("bytes" in census, true); + + + // Pattern doesn't mind objects with extra properties, so we'll restrict this + // list to the object classes we're pretty sure are going to stick around for + // the forseeable future. + Pattern({ + Function: { count: Pattern.NATURAL }, + Object: { count: Pattern.NATURAL }, + Debugger: { count: Pattern.NATURAL }, + Sandbox: { count: Pattern.NATURAL }, + + // The below are all Debugger prototype objects. + Source: { count: Pattern.NATURAL }, + Environment: { count: Pattern.NATURAL }, + Script: { count: Pattern.NATURAL }, + Memory: { count: Pattern.NATURAL }, + Frame: { count: Pattern.NATURAL } + }) + .assert(saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "objectClass" } })); + + Pattern({ + objects: { count: Pattern.NATURAL }, + scripts: { count: Pattern.NATURAL }, + strings: { count: Pattern.NATURAL }, + other: { count: Pattern.NATURAL } + }) + .assert(saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "coarseType" } })); + + // As for { by: 'objectClass' }, restrict our pattern to the types + // we predict will stick around for a long time. + Pattern({ + JSString: { count: Pattern.NATURAL }, + "js::Shape": { count: Pattern.NATURAL }, + JSObject: { count: Pattern.NATURAL }, + JSScript: { count: Pattern.NATURAL } + }) + .assert(saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "internalType" } })); + + + // Nested breakdowns. + + let coarseTypePattern = { + objects: { count: Pattern.NATURAL }, + scripts: { count: Pattern.NATURAL }, + strings: { count: Pattern.NATURAL }, + other: { count: Pattern.NATURAL } + }; + + Pattern({ + JSString: coarseTypePattern, + "js::Shape": coarseTypePattern, + JSObject: coarseTypePattern, + JSScript: coarseTypePattern, + }) + .assert(saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { by: "internalType", + then: { by: "coarseType" } + } + })); + + Pattern({ + Function: { count: Pattern.NATURAL }, + Object: { count: Pattern.NATURAL }, + Debugger: { count: Pattern.NATURAL }, + Sandbox: { count: Pattern.NATURAL }, + other: coarseTypePattern + }) + .assert(saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { + by: "objectClass", + then: { by: "count" }, + other: { by: "coarseType" } + } + })); + + Pattern({ + objects: { count: Pattern.NATURAL, label: "object" }, + scripts: { count: Pattern.NATURAL, label: "scripts" }, + strings: { count: Pattern.NATURAL, label: "strings" }, + other: { count: Pattern.NATURAL, label: "other" } + }) + .assert(saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { + by: "coarseType", + objects: { by: "count", label: "object" }, + scripts: { by: "count", label: "scripts" }, + strings: { by: "count", label: "strings" }, + other: { by: "count", label: "other" } + } + })); + + do_test_finished(); +} diff --git a/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_07.js b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_07.js new file mode 100644 index 000000000..f5c36056f --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_07.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// HeapSnapshot.prototype.takeCensus breakdown: check error handling on property +// gets. +// +// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-07.js + +function run_test() { + var g = newGlobal(); + var dbg = new Debugger(g); + + assertThrowsValue(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { get by() { throw "ಠ_ಠ"; } } + }); + }, "ಠ_ಠ"); + + + + assertThrowsValue(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { by: "count", get count() { throw "ಠ_ಠ"; } } + }); + }, "ಠ_ಠ"); + + assertThrowsValue(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { by: "count", get bytes() { throw "ಠ_ಠ"; } } + }); + }, "ಠ_ಠ"); + + + + assertThrowsValue(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { by: "objectClass", get then() { throw "ಠ_ಠ"; } } + }); + }, "ಠ_ಠ"); + + assertThrowsValue(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { by: "objectClass", get other() { throw "ಠ_ಠ"; } } + }); + }, "ಠ_ಠ"); + + + + assertThrowsValue(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { by: "coarseType", get objects() { throw "ಠ_ಠ"; } } + }); + }, "ಠ_ಠ"); + + assertThrowsValue(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { by: "coarseType", get scripts() { throw "ಠ_ಠ"; } } + }); + }, "ಠ_ಠ"); + + assertThrowsValue(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { by: "coarseType", get strings() { throw "ಠ_ಠ"; } } + }); + }, "ಠ_ಠ"); + + assertThrowsValue(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { by: "coarseType", get other() { throw "ಠ_ಠ"; } } + }); + }, "ಠ_ಠ"); + + + + assertThrowsValue(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { by: "internalType", get then() { throw "ಠ_ಠ"; } } + }); + }, "ಠ_ಠ"); + + do_test_finished(); +} diff --git a/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_08.js b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_08.js new file mode 100644 index 000000000..5934aa919 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_08.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// HeapSnapshot.prototype.takeCensus: test by: 'count' breakdown +// +// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-08.js + +function run_test() { + let g = newGlobal(); + let dbg = new Debugger(g); + + g.eval(` + var stuff = []; + function add(n, c) { + for (let i = 0; i < n; i++) + stuff.push(c()); + } + + let count = 0; + + function obj() { return { count: count++ }; } + obj.factor = 1; + + // This creates a closure (a function JSObject) that has captured + // a Call object. So each call creates two items. + function fun() { let v = count; return () => { return v; } } + fun.factor = 2; + + function str() { return 'perambulator' + count++; } + str.factor = 1; + + // Eval a fresh text each time, allocating: + // - a fresh ScriptSourceObject + // - a new JSScripts, not an eval cache hits + // - a fresh prototype object + // - a fresh Call object, since the eval makes 'ev' heavyweight + // - the new function itself + function ev() { + return eval(\`(function () { return \${ count++ } })\`); + } + ev.factor = 5; + + // A new object (1) with a new shape (2) with a new atom (3) + function shape() { return { [ 'theobroma' + count++ ]: count }; } + shape.factor = 3; + `); + + let baseline = 0; + function countIncreasedByAtLeast(n) { + let oldBaseline = baseline; + + // Since a census counts only reachable objects, one might assume that calling + // GC here would have no effect on the census results. But GC also throws away + // JIT code and any objects it might be holding (template objects, say); + // takeCensus reaches those. Shake everything loose that we can, to make the + // census approximate reachability a bit more closely, and make our results a + // bit more predictable. + gc(g, "shrinking"); + + baseline = saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "count" } }).count; + return baseline >= oldBaseline + n; + } + + countIncreasedByAtLeast(0); + + g.add(100, g.obj); + ok(countIncreasedByAtLeast(g.obj.factor * 100)); + + g.add(100, g.fun); + ok(countIncreasedByAtLeast(g.fun.factor * 100)); + + g.add(100, g.str); + ok(countIncreasedByAtLeast(g.str.factor * 100)); + + g.add(100, g.ev); + ok(countIncreasedByAtLeast(g.ev.factor * 100)); + + g.add(100, g.shape); + ok(countIncreasedByAtLeast(g.shape.factor * 100)); + + do_test_finished(); +} diff --git a/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_09.js b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_09.js new file mode 100644 index 000000000..bbacccc8d --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_09.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// HeapSnapshot.prototype.takeCensus: by: allocationStack breakdown +// +// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-09.js + +function run_test() { + var g = newGlobal(); + var dbg = new Debugger(g); + + g.eval(` // 1 + var log = []; // 2 + function f() { log.push(allocationMarker()); } // 3 + function g() { f(); } // 4 + function h() { f(); } // 5 + `); // 6 + + // Create one allocationMarker with tracking turned off, + // so it will have no associated stack. + g.f(); + + dbg.memory.allocationSamplingProbability = 1; + + for ([func, n] of [[g.f, 20], [g.g, 10], [g.h, 5]]) { + for (let i = 0; i < n; i++) { + dbg.memory.trackingAllocationSites = true; + // All allocations of allocationMarker occur with this line as the oldest + // stack frame. + func(); + dbg.memory.trackingAllocationSites = false; + } + } + + let census = saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "objectClass", + then: { by: "allocationStack", + then: { by: "count", + label: "haz stack" + }, + noStack: { by: "count", + label: "no haz stack" + } + } + } + }); + + let map = census.AllocationMarker; + ok(map instanceof Map, "Should be a Map instance"); + equal(map.size, 4, "Should have 4 allocation stacks (including the lack of a stack)"); + + // Gather the stacks we are expecting to appear as keys, and + // check that there are no unexpected keys. + let stacks = { }; + + map.forEach((v, k) => { + if (k === "noStack") { + // No need to save this key. + } else if (k.functionDisplayName === "f" && + k.parent.functionDisplayName === "run_test") { + stacks.f = k; + } else if (k.functionDisplayName === "f" && + k.parent.functionDisplayName === "g" && + k.parent.parent.functionDisplayName === "run_test") { + stacks.fg = k; + } else if (k.functionDisplayName === "f" && + k.parent.functionDisplayName === "h" && + k.parent.parent.functionDisplayName === "run_test") { + stacks.fh = k; + } else { + dumpn("Unexpected allocation stack:"); + k.toString().split(/\n/g).forEach(s => dumpn(s)); + ok(false); + } + }); + + equal(map.get("noStack").label, "no haz stack"); + equal(map.get("noStack").count, 1); + + ok(stacks.f); + equal(map.get(stacks.f).label, "haz stack"); + equal(map.get(stacks.f).count, 20); + + ok(stacks.fg); + equal(map.get(stacks.fg).label, "haz stack"); + equal(map.get(stacks.fg).count, 10); + + ok(stacks.fh); + equal(map.get(stacks.fh).label, "haz stack"); + equal(map.get(stacks.fh).count, 5); + + do_test_finished(); +} diff --git a/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_10.js b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_10.js new file mode 100644 index 000000000..a7f987f5a --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_10.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check byte counts produced by takeCensus. +// +// Ported from js/src/jit-test/tests/debug/Memory-take Census-10.js + +function run_test() { + let g = newGlobal(); + let dbg = new Debugger(g); + + let sizeOfAM = byteSize(allocationMarker()); + + // Allocate a single allocation marker, and check that we can find it. + g.eval("var hold = allocationMarker();"); + let census = saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "objectClass" } }); + equal(census.AllocationMarker.count, 1); + equal(census.AllocationMarker.bytes, sizeOfAM); + g.hold = null; + + g.eval(` // 1 + var objs = []; // 2 + function fnerd() { // 3 + objs.push(allocationMarker()); // 4 + for (let i = 0; i < 10; i++) // 5 + objs.push(allocationMarker()); // 6 + } // 7 + `); // 8 + + dbg.memory.allocationSamplingProbability = 1; + dbg.memory.trackingAllocationSites = true; + g.fnerd(); + dbg.memory.trackingAllocationSites = false; + + census = saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { by: "objectClass", + then: { by: "allocationStack" } + } + }); + + let seen = 0; + census.AllocationMarker.forEach((v, k) => { + equal(k.functionDisplayName, "fnerd"); + switch (k.line) { + case 4: + equal(v.count, 1); + equal(v.bytes, sizeOfAM); + seen++; + break; + + case 6: + equal(v.count, 10); + equal(v.bytes, 10 * sizeOfAM); + seen++; + break; + + default: + dumpn("Unexpected stack:"); + k.toString().split(/\n/g).forEach(s => dumpn(s)); + ok(false); + break; + } + }); + + equal(seen, 2); + + do_test_finished(); +} diff --git a/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_11.js b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_11.js new file mode 100644 index 000000000..3d898b2d1 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_11.js @@ -0,0 +1,116 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that Debugger.Memory.prototype.takeCensus and +// HeapSnapshot.prototype.takeCensus return the same data for the same heap +// graph. + +function doLiveAndOfflineCensus(g, dbg, opts) { + dbg.memory.allocationSamplingProbability = 1; + dbg.memory.trackingAllocationSites = true; + g.eval(` // 1 + (function unsafeAtAnySpeed() { // 2 + for (var i = 0; i < 100; i++) { // 3 + this.markers.push(allocationMarker()); // 4 + } // 5 + }()); // 6 + `); // 7 + dbg.memory.trackingAllocationSites = false; + + return { + live: dbg.memory.takeCensus(opts), + offline: saveHeapSnapshotAndTakeCensus(dbg, opts) + }; +} + +function run_test() { + var g = newGlobal(); + var dbg = new Debugger(g); + + g.eval("this.markers = []"); + const markerSize = byteSize(allocationMarker()); + + // First, test that we get the same counts and sizes as we allocate and retain + // more things. + + let prevCount = 0; + let prevBytes = 0; + + for (var i = 0; i < 10; i++) { + const { live, offline } = doLiveAndOfflineCensus(g, dbg, { + breakdown: { by: "objectClass", + then: { by: "count"} } + }); + + equal(live.AllocationMarker.count, offline.AllocationMarker.count); + equal(live.AllocationMarker.bytes, offline.AllocationMarker.bytes); + equal(live.AllocationMarker.count, prevCount + 100); + equal(live.AllocationMarker.bytes, prevBytes + 100 * markerSize); + + prevCount = live.AllocationMarker.count; + prevBytes = live.AllocationMarker.bytes; + } + + // Second, test that the reported allocation stacks and counts and sizes at + // those allocation stacks match up. + + const { live, offline } = doLiveAndOfflineCensus(g, dbg, { + breakdown: { by: "objectClass", + then: { by: "allocationStack"} } + }); + + equal(live.AllocationMarker.size, offline.AllocationMarker.size); + // One stack with the loop further above, and another stack featuring the call + // right above. + equal(live.AllocationMarker.size, 2); + + // Note that because SavedFrame stacks reconstructed from an offline heap + // snapshot don't have the same principals as SavedFrame stacks captured from + // a live stack, the live and offline allocation stacks won't be identity + // equal, but should be structurally the same. + + const liveEntries = []; + live.AllocationMarker.forEach((v, k) => { + dumpn("Allocation stack:"); + k.toString().split(/\n/g).forEach(s => dumpn(s)); + + equal(k.functionDisplayName, "unsafeAtAnySpeed"); + equal(k.line, 4); + + liveEntries.push([k.toString(), v]); + }); + + const offlineEntries = []; + offline.AllocationMarker.forEach((v, k) => { + dumpn("Allocation stack:"); + k.toString().split(/\n/g).forEach(s => dumpn(s)); + + equal(k.functionDisplayName, "unsafeAtAnySpeed"); + equal(k.line, 4); + + offlineEntries.push([k.toString(), v]); + }); + + const sortEntries = (a, b) => { + if (a[0] < b[0]) { + return -1; + } else if (a[0] > b[0]) { + return 1; + } else { + return 0; + } + }; + liveEntries.sort(sortEntries); + offlineEntries.sort(sortEntries); + + equal(liveEntries.length, live.AllocationMarker.size); + equal(liveEntries.length, offlineEntries.length); + + for (let i = 0; i < liveEntries.length; i++) { + equal(liveEntries[i][0], offlineEntries[i][0]); + equal(liveEntries[i][1].count, offlineEntries[i][1].count); + equal(liveEntries[i][1].bytes, offlineEntries[i][1].bytes); + } + + do_test_finished(); +} diff --git a/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_12.js b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_12.js new file mode 100644 index 000000000..f10dd5b03 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_12.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that when we take a census and get a bucket list of ids that matched the +// given category, that the returned ids are all in the snapshot and their +// reported category. + +function run_test() { + const g = newGlobal(); + const dbg = new Debugger(g); + + const path = saveNewHeapSnapshot({ debugger: dbg }); + const snapshot = readHeapSnapshot(path); + + const bucket = { by: "bucket" }; + const count = { by: "count", count: true, bytes: false }; + const objectClassCount = { by: "objectClass", then: count, other: count }; + + const byClassName = snapshot.takeCensus({ + breakdown: { + by: "objectClass", + then: bucket, + other: bucket, + } + }); + + const byClassNameCount = snapshot.takeCensus({ + breakdown: objectClassCount + }); + + const keys = new Set(Object.keys(byClassName)); + equal(keys.size, Object.keys(byClassNameCount).length, + "Should have the same number of keys."); + for (let k of Object.keys(byClassNameCount)) { + ok(keys.has(k), "Should not have any unexpected class names"); + } + + for (let key of Object.keys(byClassName)) { + equal(byClassNameCount[key].count, byClassName[key].length, + "Length of the bucket and count should be equal"); + + for (let id of byClassName[key]) { + const desc = snapshot.describeNode(objectClassCount, id); + equal(desc[key].count, 1, + "Describing the bucketed node confirms that it belongs to the category"); + } + } + + do_test_finished(); +} diff --git a/dom/heapsnapshot/tests/unit/test_ReadHeapSnapshot.js b/dom/heapsnapshot/tests/unit/test_ReadHeapSnapshot.js new file mode 100644 index 000000000..dde139ffd --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_ReadHeapSnapshot.js @@ -0,0 +1,20 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we can read core dumps into HeapSnapshot instances. + +if (typeof Debugger != "function") { + const { addDebuggerToGlobal } = Cu.import("resource://gre/modules/jsdebugger.jsm", {}); + addDebuggerToGlobal(this); +} + +function run_test() { + const filePath = ChromeUtils.saveHeapSnapshot({ globals: [this] }); + ok(true, "Should be able to save a snapshot."); + + const snapshot = ChromeUtils.readHeapSnapshot(filePath); + ok(snapshot, "Should be able to read a heap snapshot"); + ok(snapshot instanceof HeapSnapshot, "Should be an instanceof HeapSnapshot"); + + do_test_finished(); +} diff --git a/dom/heapsnapshot/tests/unit/test_ReadHeapSnapshot_with_allocations.js b/dom/heapsnapshot/tests/unit/test_ReadHeapSnapshot_with_allocations.js new file mode 100644 index 000000000..d91f36f56 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_ReadHeapSnapshot_with_allocations.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we can save a core dump with allocation stacks and read it back +// into a HeapSnapshot. + +if (typeof Debugger != "function") { + const { addDebuggerToGlobal } = Cu.import("resource://gre/modules/jsdebugger.jsm", {}); + addDebuggerToGlobal(this); +} + +function run_test() { + // Create a Debugger observing a debuggee's allocations. + const debuggee = new Cu.Sandbox(null); + const dbg = new Debugger(debuggee); + dbg.memory.trackingAllocationSites = true; + + // Allocate some objects in the debuggee that will have their allocation + // stacks recorded by the Debugger. + debuggee.eval("this.objects = []"); + for (let i = 0; i < 100; i++) { + debuggee.eval("this.objects.push({})"); + } + + // Now save a snapshot that will include the allocation stacks and read it + // back again. + + const filePath = ChromeUtils.saveHeapSnapshot({ runtime: true }); + ok(true, "Should be able to save a snapshot."); + + const snapshot = ChromeUtils.readHeapSnapshot(filePath); + ok(snapshot, "Should be able to read a heap snapshot"); + ok(snapshot instanceof HeapSnapshot, "Should be an instanceof HeapSnapshot"); + + do_test_finished(); +} diff --git a/dom/heapsnapshot/tests/unit/test_ReadHeapSnapshot_worker.js b/dom/heapsnapshot/tests/unit/test_ReadHeapSnapshot_worker.js new file mode 100644 index 000000000..76461b694 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_ReadHeapSnapshot_worker.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we can read core dumps into HeapSnapshot instances in a worker. + +add_task(function* () { + const worker = new ChromeWorker("resource://test/heap-snapshot-worker.js"); + worker.postMessage({}); + + let assertionCount = 0; + worker.onmessage = e => { + if (e.data.type !== "assertion") { + return; + } + + ok(e.data.passed, e.data.msg + "\n" + e.data.stack); + assertionCount++; + }; + + yield waitForDone(worker); + + ok(assertionCount > 0); + worker.terminate(); +}); + +function waitForDone(w) { + return new Promise((resolve, reject) => { + w.onerror = e => { + reject(); + ok(false, "Error in worker: " + e); + }; + + w.addEventListener("message", function listener(e) { + if (e.data.type === "done") { + w.removeEventListener("message", listener, false); + resolve(); + } + }, false); + }); +} diff --git a/dom/heapsnapshot/tests/unit/test_SaveHeapSnapshot.js b/dom/heapsnapshot/tests/unit/test_SaveHeapSnapshot.js new file mode 100644 index 000000000..affd8d1e4 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_SaveHeapSnapshot.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test the ChromeUtils interface. + +if (typeof Debugger != "function") { + const { addDebuggerToGlobal } = Cu.import("resource://gre/modules/jsdebugger.jsm", {}); + addDebuggerToGlobal(this); +} + +function run_test() { + ok(ChromeUtils, "Should be able to get the ChromeUtils interface"); + + testBadParameters(); + testGoodParameters(); + + do_test_finished(); +} + +function testBadParameters() { + throws(() => ChromeUtils.saveHeapSnapshot(), + "Should throw if arguments aren't passed in."); + + throws(() => ChromeUtils.saveHeapSnapshot(null), + "Should throw if boundaries isn't an object."); + + throws(() => ChromeUtils.saveHeapSnapshot({}), + "Should throw if the boundaries object doesn't have any properties."); + + throws(() => ChromeUtils.saveHeapSnapshot({ runtime: true, + globals: [this] }), + "Should throw if the boundaries object has more than one property."); + + throws(() => ChromeUtils.saveHeapSnapshot({ debugger: {} }), + "Should throw if the debuggees object is not a Debugger object"); + + throws(() => ChromeUtils.saveHeapSnapshot({ globals: [{}] }), + "Should throw if the globals array contains non-global objects."); + + throws(() => ChromeUtils.saveHeapSnapshot({ runtime: false }), + "Should throw if runtime is supplied and is not true."); + + throws(() => ChromeUtils.saveHeapSnapshot({ globals: null }), + "Should throw if globals is not an object."); + + throws(() => ChromeUtils.saveHeapSnapshot({ globals: {} }), + "Should throw if globals is not an array."); + + throws(() => ChromeUtils.saveHeapSnapshot({ debugger: Debugger.prototype }), + "Should throw if debugger is the Debugger.prototype object."); + + throws(() => ChromeUtils.saveHeapSnapshot({ get globals() { return [this]; } }), + "Should throw if boundaries property is a getter."); +} + +const makeNewSandbox = () => + Cu.Sandbox(CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")()); + +function testGoodParameters() { + let sandbox = makeNewSandbox(); + let dbg = new Debugger(sandbox); + + ChromeUtils.saveHeapSnapshot({ debugger: dbg }); + ok(true, "Should be able to save a snapshot for a debuggee global."); + + dbg = new Debugger; + let sandboxes = Array(10).fill(null).map(makeNewSandbox); + sandboxes.forEach(sb => dbg.addDebuggee(sb)); + + ChromeUtils.saveHeapSnapshot({ debugger: dbg }); + ok(true, "Should be able to save a snapshot for many debuggee globals."); + + dbg = new Debugger; + ChromeUtils.saveHeapSnapshot({ debugger: dbg }); + ok(true, "Should be able to save a snapshot with no debuggee globals."); + + ChromeUtils.saveHeapSnapshot({ globals: [this] }); + ok(true, "Should be able to save a snapshot for a specific global."); + + ChromeUtils.saveHeapSnapshot({ runtime: true }); + ok(true, "Should be able to save a snapshot of the full runtime."); +} diff --git a/dom/heapsnapshot/tests/unit/test_census-tree-node-01.js b/dom/heapsnapshot/tests/unit/test_census-tree-node-01.js new file mode 100644 index 000000000..16038c5c4 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_census-tree-node-01.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests CensusTreeNode with `internalType` breakdown. + */ + +const BREAKDOWN = { + by: "internalType", + then: { by: "count", count: true, bytes: true } +}; + +const REPORT = { + "JSObject": { + "bytes": 100, + "count": 10, + }, + "js::Shape": { + "bytes": 500, + "count": 50, + }, + "JSString": { + "bytes": 10, + "count": 1, + }, +}; + +const EXPECTED = { + name: null, + bytes: 0, + totalBytes: 610, + count: 0, + totalCount: 61, + children: [ + { + name: "js::Shape", + bytes: 500, + totalBytes: 500, + count: 50, + totalCount: 50, + children: undefined, + id: 3, + parent: 1, + reportLeafIndex: 2, + }, + { + name: "JSObject", + bytes: 100, + totalBytes: 100, + count: 10, + totalCount: 10, + children: undefined, + id: 2, + parent: 1, + reportLeafIndex: 1, + }, + { + name: "JSString", + bytes: 10, + totalBytes: 10, + count: 1, + totalCount: 1, + children: undefined, + id: 4, + parent: 1, + reportLeafIndex: 3, + }, + ], + id: 1, + parent: undefined, + reportLeafIndex: undefined, +}; + +function run_test() { + compareCensusViewData(BREAKDOWN, REPORT, EXPECTED); +} diff --git a/dom/heapsnapshot/tests/unit/test_census-tree-node-02.js b/dom/heapsnapshot/tests/unit/test_census-tree-node-02.js new file mode 100644 index 000000000..37d039954 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_census-tree-node-02.js @@ -0,0 +1,136 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests CensusTreeNode with `coarseType` breakdown. + */ + +const countBreakdown = { by: "count", count: true, bytes: true }; + +const BREAKDOWN = { + by: "coarseType", + objects: { by: "objectClass", then: countBreakdown }, + strings: countBreakdown, + scripts: countBreakdown, + other: { by: "internalType", then: countBreakdown }, +}; + +const REPORT = { + "objects": { + "Function": { bytes: 10, count: 1 }, + "Array": { bytes: 20, count: 2 }, + }, + "strings": { bytes: 10, count: 1 }, + "scripts": { bytes: 1, count: 1 }, + "other": { + "js::Shape": { bytes: 30, count: 3 }, + "js::Shape2": { bytes: 40, count: 4 } + }, +}; + +const EXPECTED = { + name: null, + bytes: 0, + totalBytes: 111, + count: 0, + totalCount: 12, + children: [ + { + name: "other", + count: 0, + totalCount: 7, + bytes: 0, + totalBytes: 70, + children: [ + { + name: "js::Shape2", + bytes: 40, + totalBytes: 40, + count: 4, + totalCount: 4, + children: undefined, + id: 9, + parent: 7, + reportLeafIndex: 8, + }, + { + name: "js::Shape", + bytes: 30, + totalBytes: 30, + count: 3, + totalCount: 3, + children: undefined, + id: 8, + parent: 7, + reportLeafIndex: 7, + }, + ], + id: 7, + parent: 1, + reportLeafIndex: undefined, + }, + { + name: "objects", + count: 0, + totalCount: 3, + bytes: 0, + totalBytes: 30, + children: [ + { + name: "Array", + bytes: 20, + totalBytes: 20, + count: 2, + totalCount: 2, + children: undefined, + id: 4, + parent: 2, + reportLeafIndex: 3, + }, + { + name: "Function", + bytes: 10, + totalBytes: 10, + count: 1, + totalCount: 1, + children: undefined, + id: 3, + parent: 2, + reportLeafIndex: 2, + }, + ], + id: 2, + parent: 1, + reportLeafIndex: undefined, + }, + { + name: "strings", + count: 1, + totalCount: 1, + bytes: 10, + totalBytes: 10, + children: undefined, + id: 6, + parent: 1, + reportLeafIndex: 5, + }, + { + name: "scripts", + count: 1, + totalCount: 1, + bytes: 1, + totalBytes: 1, + children: undefined, + id: 5, + parent: 1, + reportLeafIndex: 4, + }, + ], + id: 1, + parent: undefined, + reportLeafIndex: undefined, +}; + +function run_test() { + compareCensusViewData(BREAKDOWN, REPORT, EXPECTED); +} diff --git a/dom/heapsnapshot/tests/unit/test_census-tree-node-03.js b/dom/heapsnapshot/tests/unit/test_census-tree-node-03.js new file mode 100644 index 000000000..bdf932099 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_census-tree-node-03.js @@ -0,0 +1,96 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests CensusTreeNode with `objectClass` breakdown. + */ + +const countBreakdown = { by: "count", count: true, bytes: true }; + +const BREAKDOWN = { + by: "objectClass", + then: countBreakdown, + other: { by: "internalType", then: countBreakdown } +}; + +const REPORT = { + "Function": { bytes: 10, count: 10 }, + "Array": { bytes: 100, count: 1 }, + "other": { + "JIT::CODE::NOW!!!": { bytes: 20, count: 2 }, + "JIT::CODE::LATER!!!": { bytes: 40, count: 4 } + } +}; + +const EXPECTED = { + name: null, + count: 0, + totalCount: 17, + bytes: 0, + totalBytes: 170, + children: [ + { + name: "Array", + bytes: 100, + totalBytes: 100, + count: 1, + totalCount: 1, + children: undefined, + id: 3, + parent: 1, + reportLeafIndex: 2, + }, + { + name: "other", + count: 0, + totalCount: 6, + bytes: 0, + totalBytes: 60, + children: [ + { + name: "JIT::CODE::LATER!!!", + bytes: 40, + totalBytes: 40, + count: 4, + totalCount: 4, + children: undefined, + id: 6, + parent: 4, + reportLeafIndex: 5, + }, + { + name: "JIT::CODE::NOW!!!", + bytes: 20, + totalBytes: 20, + count: 2, + totalCount: 2, + children: undefined, + id: 5, + parent: 4, + reportLeafIndex: 4, + }, + ], + id: 4, + parent: 1, + reportLeafIndex: undefined, + }, + { + name: "Function", + bytes: 10, + totalBytes: 10, + count: 10, + totalCount: 10, + children: undefined, + id: 2, + parent: 1, + reportLeafIndex: 1, + }, + ], + id: 1, + parent: undefined, + reportLeafIndex: undefined, +}; + +function run_test() { + compareCensusViewData(BREAKDOWN, REPORT, EXPECTED); +} diff --git a/dom/heapsnapshot/tests/unit/test_census-tree-node-04.js b/dom/heapsnapshot/tests/unit/test_census-tree-node-04.js new file mode 100644 index 000000000..cc0c3bac0 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_census-tree-node-04.js @@ -0,0 +1,159 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests CensusTreeNode with `allocationStack` breakdown. + */ + +function run_test() { + const countBreakdown = { by: "count", count: true, bytes: true }; + + const BREAKDOWN = { + by: "allocationStack", + then: countBreakdown, + noStack: countBreakdown, + }; + + let stack1, stack2, stack3, stack4, stack5; + + (function a() { + (function b() { + (function c() { + stack1 = saveStack(3); + }()); + (function d() { + stack2 = saveStack(3); + stack3 = saveStack(3); + }()); + stack4 = saveStack(2); + }()); + }()); + + stack5 = saveStack(1); + + const REPORT = new Map([ + [stack1, { bytes: 10, count: 1 }], + [stack2, { bytes: 20, count: 2 }], + [stack3, { bytes: 30, count: 3 }], + [stack4, { bytes: 40, count: 4 }], + [stack5, { bytes: 50, count: 5 }], + ["noStack", { bytes: 60, count: 6 }], + ]); + + const EXPECTED = { + name: null, + bytes: 0, + totalBytes: 210, + count: 0, + totalCount: 21, + children: [ + { + name: stack4.parent, + bytes: 0, + totalBytes: 100, + count: 0, + totalCount: 10, + children: [ + { + name: stack3.parent, + bytes: 0, + totalBytes: 50, + count: 0, + totalCount: 5, + children: [ + { + name: stack3, + bytes: 30, + totalBytes: 30, + count: 3, + totalCount: 3, + children: undefined, + id: 7, + parent: 5, + reportLeafIndex: 3, + }, + { + name: stack2, + bytes: 20, + totalBytes: 20, + count: 2, + totalCount: 2, + children: undefined, + id: 6, + parent: 5, + reportLeafIndex: 2, + } + ], + id: 5, + parent: 2, + reportLeafIndex: undefined, + }, + { + name: stack4, + bytes: 40, + totalBytes: 40, + count: 4, + totalCount: 4, + children: undefined, + id: 8, + parent: 2, + reportLeafIndex: 4, + }, + { + name: stack1.parent, + bytes: 0, + totalBytes: 10, + count: 0, + totalCount: 1, + children: [ + { + name: stack1, + bytes: 10, + totalBytes: 10, + count: 1, + totalCount: 1, + children: undefined, + id: 4, + parent: 3, + reportLeafIndex: 1, + }, + ], + id: 3, + parent: 2, + reportLeafIndex: undefined, + }, + ], + id: 2, + parent: 1, + reportLeafIndex: undefined, + }, + { + name: "noStack", + bytes: 60, + totalBytes: 60, + count: 6, + totalCount: 6, + children: undefined, + id: 10, + parent: 1, + reportLeafIndex: 6, + }, + { + name: stack5, + bytes: 50, + totalBytes: 50, + count: 5, + totalCount: 5, + children: undefined, + id: 9, + parent: 1, + reportLeafIndex: 5 + }, + ], + id: 1, + parent: undefined, + reportLeafIndex: undefined, + }; + + compareCensusViewData(BREAKDOWN, REPORT, EXPECTED); +} diff --git a/dom/heapsnapshot/tests/unit/test_census-tree-node-05.js b/dom/heapsnapshot/tests/unit/test_census-tree-node-05.js new file mode 100644 index 000000000..20fb76bd2 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_census-tree-node-05.js @@ -0,0 +1,145 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests CensusTreeNode with `allocationStack` => `objectClass` breakdown. + */ + +function run_test() { + const countBreakdown = { by: "count", count: true, bytes: true }; + + const BREAKDOWN = { + by: "allocationStack", + then: { + by: "objectClass", + then: countBreakdown, + other: countBreakdown + }, + noStack: countBreakdown, + }; + + let stack; + + (function a() { + (function b() { + (function c() { + stack = saveStack(3); + }()); + }()); + }()); + + const REPORT = new Map([ + [stack, { Foo: { bytes: 10, count: 1 }, + Bar: { bytes: 20, count: 2 }, + Baz: { bytes: 30, count: 3 }, + other: { bytes: 40, count: 4 } + }], + ["noStack", { bytes: 50, count: 5 }], + ]); + + const EXPECTED = { + name: null, + bytes: 0, + totalBytes: 150, + count: 0, + totalCount: 15, + children: [ + { + name: stack.parent.parent, + bytes: 0, + totalBytes: 100, + count: 0, + totalCount: 10, + children: [ + { + name: stack.parent, + bytes: 0, + totalBytes: 100, + count: 0, + totalCount: 10, + children: [ + { + name: stack, + bytes: 0, + totalBytes: 100, + count: 0, + totalCount: 10, + children: [ + { + name: "other", + bytes: 40, + totalBytes: 40, + count: 4, + totalCount: 4, + children: undefined, + id: 8, + parent: 4, + reportLeafIndex: 5, + }, + { + name: "Baz", + bytes: 30, + totalBytes: 30, + count: 3, + totalCount: 3, + children: undefined, + id: 7, + parent: 4, + reportLeafIndex: 4, + }, + { + name: "Bar", + bytes: 20, + totalBytes: 20, + count: 2, + totalCount: 2, + children: undefined, + id: 6, + parent: 4, + reportLeafIndex: 3, + }, + { + name: "Foo", + bytes: 10, + totalBytes: 10, + count: 1, + totalCount: 1, + children: undefined, + id: 5, + parent: 4, + reportLeafIndex: 2, + }, + ], + id: 4, + parent: 3, + reportLeafIndex: undefined, + } + ], + id: 3, + parent: 2, + reportLeafIndex: undefined, + } + ], + id: 2, + parent: 1, + reportLeafIndex: undefined, + }, + { + name: "noStack", + bytes: 50, + totalBytes: 50, + count: 5, + totalCount: 5, + children: undefined, + id: 9, + parent: 1, + reportLeafIndex: 6, + }, + ], + id: 1, + parent: undefined, + reportLeafIndex: undefined, + }; + + compareCensusViewData(BREAKDOWN, REPORT, EXPECTED); +} diff --git a/dom/heapsnapshot/tests/unit/test_census-tree-node-06.js b/dom/heapsnapshot/tests/unit/test_census-tree-node-06.js new file mode 100644 index 000000000..eb1801207 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_census-tree-node-06.js @@ -0,0 +1,200 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test inverting CensusTreeNode with a by alloaction stack breakdown. + */ + +function run_test() { + const BREAKDOWN = { + by: "allocationStack", + then: { by: "count", count: true, bytes: true }, + noStack: { by: "count", count: true, bytes: true }, + }; + + let stack1, stack2, stack3, stack4; + + function a(n) { + return b(n); + } + function b(n) { + return c(n); + } + function c(n) { + return saveStack(n); + } + function d(n) { + return b(n); + } + function e(n) { + return c(n); + } + + const abc_Stack = a(3); + const bc_Stack = b(2); + const c_Stack = c(1); + const dbc_Stack = d(3); + const ec_Stack = e(2); + + const REPORT = new Map([ + [abc_Stack, { bytes: 10, count: 1 }], + [ bc_Stack, { bytes: 10, count: 1 }], + [ c_Stack, { bytes: 10, count: 1 }], + [dbc_Stack, { bytes: 10, count: 1 }], + [ ec_Stack, { bytes: 10, count: 1 }], + ["noStack", { bytes: 50, count: 5 }], + ]); + + const EXPECTED = { + name: null, + bytes: 0, + totalBytes: 100, + count: 0, + totalCount: 10, + children: [ + { + name: "noStack", + bytes: 50, + totalBytes: 50, + count: 5, + totalCount: 5, + children: [ + { + name: null, + bytes: 0, + totalBytes: 100, + count: 0, + totalCount: 10, + children: undefined, + id: 16, + parent: 15, + reportLeafIndex: undefined, + } + ], + id: 15, + parent: 14, + reportLeafIndex: 6, + }, + { + name: abc_Stack, + bytes: 50, + totalBytes: 10, + count: 5, + totalCount: 1, + children: [ + { + name: null, + bytes: 0, + totalBytes: 100, + count: 0, + totalCount: 10, + children: undefined, + id: 18, + parent: 17, + reportLeafIndex: undefined, + }, + { + name: abc_Stack.parent, + bytes: 0, + totalBytes: 10, + count: 0, + totalCount: 1, + children: [ + { + name: null, + bytes: 0, + totalBytes: 100, + count: 0, + totalCount: 10, + children: undefined, + id: 22, + parent: 19, + reportLeafIndex: undefined, + }, + { + name: abc_Stack.parent.parent, + bytes: 0, + totalBytes: 10, + count: 0, + totalCount: 1, + children: [ + { + name: null, + bytes: 0, + totalBytes: 100, + count: 0, + totalCount: 10, + children: undefined, + id: 21, + parent: 20, + reportLeafIndex: undefined, + } + ], + id: 20, + parent: 19, + reportLeafIndex: undefined, + }, + { + name: dbc_Stack.parent.parent, + bytes: 0, + totalBytes: 10, + count: 0, + totalCount: 1, + children: [ + { + name: null, + bytes: 0, + totalBytes: 100, + count: 0, + totalCount: 10, + children: undefined, + id: 24, + parent: 23, + reportLeafIndex: undefined, + } + ], + id: 23, + parent: 19, + reportLeafIndex: undefined, + } + ], + id: 19, + parent: 17, + reportLeafIndex: undefined, + }, + { + name: ec_Stack.parent, + bytes: 0, + totalBytes: 10, + count: 0, + totalCount: 1, + children: [ + { + name: null, + bytes: 0, + totalBytes: 100, + count: 0, + totalCount: 10, + children: undefined, + id: 26, + parent: 25, + reportLeafIndex: undefined, + }, + ], + id: 25, + parent: 17, + reportLeafIndex: undefined, + }, + ], + id: 17, + parent: 14, + reportLeafIndex: new Set([1, 2, 3, 4, 5]), + } + ], + id: 14, + parent: undefined, + reportLeafIndex: undefined, + }; + + compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { invert: true }); +} diff --git a/dom/heapsnapshot/tests/unit/test_census-tree-node-07.js b/dom/heapsnapshot/tests/unit/test_census-tree-node-07.js new file mode 100644 index 000000000..6bc085257 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_census-tree-node-07.js @@ -0,0 +1,200 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test inverting CensusTreeNode with a non-allocation stack breakdown. + */ + +function run_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 }, + }, + }; + + const REPORT = { + objects: { + Array: { bytes: 50, count: 5 }, + other: { bytes: 0, count: 0 }, + }, + scripts: { + "js::jit::JitScript": { bytes: 30, count: 3 }, + }, + strings: { + JSAtom: { bytes: 60, count: 6 }, + }, + other: { + "js::Shape": { bytes: 80, count: 8 }, + } + }; + + const EXPECTED = { + name: null, + bytes: 0, + totalBytes: 220, + count: 0, + totalCount: 22, + children: [ + { + name: "js::Shape", + bytes: 80, + totalBytes: 80, + count: 8, + totalCount: 8, + children: [ + { + name: "other", + bytes: 0, + totalBytes: 80, + count: 0, + totalCount: 8, + children: [ + { + name: null, + bytes: 0, + totalBytes: 220, + count: 0, + totalCount: 22, + children: undefined, + id: 14, + parent: 13, + reportLeafIndex: undefined, + } + ], + id: 13, + parent: 12, + reportLeafIndex: undefined, + } + ], + id: 12, + parent: 11, + reportLeafIndex: 9, + }, + { + name: "JSAtom", + bytes: 60, + totalBytes: 60, + count: 6, + totalCount: 6, + children: [ + { + name: "strings", + bytes: 0, + totalBytes: 60, + count: 0, + totalCount: 6, + children: [ + { + name: null, + bytes: 0, + totalBytes: 220, + count: 0, + totalCount: 22, + children: undefined, + id: 17, + parent: 16, + reportLeafIndex: undefined, + } + ], + id: 16, + parent: 15, + reportLeafIndex: undefined, + } + ], + id: 15, + parent: 11, + reportLeafIndex: 7, + }, + { + name: "Array", + bytes: 50, + totalBytes: 50, + count: 5, + totalCount: 5, + children: [ + { + name: "objects", + bytes: 0, + totalBytes: 50, + count: 0, + totalCount: 5, + children: [ + { + name: null, + bytes: 0, + totalBytes: 220, + count: 0, + totalCount: 22, + children: undefined, + id: 20, + parent: 19, + reportLeafIndex: undefined, + } + ], + id: 19, + parent: 18, + reportLeafIndex: undefined, + } + ], + id: 18, + parent: 11, + reportLeafIndex: 2, + }, + { + name: "js::jit::JitScript", + bytes: 30, + totalBytes: 30, + count: 3, + totalCount: 3, + children: [ + { + name: "scripts", + bytes: 0, + totalBytes: 30, + count: 0, + totalCount: 3, + children: [ + { + name: null, + bytes: 0, + totalBytes: 220, + count: 0, + totalCount: 22, + children: undefined, + id: 23, + parent: 22, + reportLeafIndex: undefined, + } + ], + id: 22, + parent: 21, + reportLeafIndex: undefined, + } + ], + id: 21, + parent: 11, + reportLeafIndex: 5, + }, + ], + id: 11, + parent: undefined, + reportLeafIndex: undefined, + }; + + compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { invert: true }); +} diff --git a/dom/heapsnapshot/tests/unit/test_census-tree-node-08.js b/dom/heapsnapshot/tests/unit/test_census-tree-node-08.js new file mode 100644 index 000000000..1c686c810 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_census-tree-node-08.js @@ -0,0 +1,142 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test inverting CensusTreeNode with a non-allocation stack breakdown. + */ + +function run_test() { + const BREAKDOWN = { + by: "filename", + then: { + by: "internalType", + then: { by: "count", count: true, bytes: true } + }, + noFilename: { + by: "internalType", + then: { by: "count", count: true, bytes: true } + }, + }; + + const REPORT = { + "http://example.com/app.js": { + JSScript: { count: 10, bytes: 100 } + }, + "http://example.com/ads.js": { + "js::LazyScript": { count: 20, bytes: 200 } + }, + "http://example.com/trackers.js": { + JSScript: { count: 30, bytes: 300 } + }, + noFilename: { + "js::jit::JitCode": { count: 40, bytes: 400 } + } + }; + + const EXPECTED = { + name: null, + bytes: 0, + totalBytes: 1000, + count: 0, + totalCount: 100, + children: [ + { + name: "noFilename", + bytes: 0, + totalBytes: 400, + count: 0, + totalCount: 40, + children: [ + { + name: "js::jit::JitCode", + bytes: 400, + totalBytes: 400, + count: 40, + totalCount: 40, + children: undefined, + id: 9, + parent: 8, + reportLeafIndex: 8, + } + ], + id: 8, + parent: 1, + reportLeafIndex: undefined, + }, + { + name: "http://example.com/trackers.js", + bytes: 0, + totalBytes: 300, + count: 0, + totalCount: 30, + children: [ + { + name: "JSScript", + bytes: 300, + totalBytes: 300, + count: 30, + totalCount: 30, + children: undefined, + id: 7, + parent: 6, + reportLeafIndex: 6, + } + ], + id: 6, + parent: 1, + reportLeafIndex: undefined, + }, + { + name: "http://example.com/ads.js", + bytes: 0, + totalBytes: 200, + count: 0, + totalCount: 20, + children: [ + { + name: "js::LazyScript", + bytes: 200, + totalBytes: 200, + count: 20, + totalCount: 20, + children: undefined, + id: 5, + parent: 4, + reportLeafIndex: 4, + } + ], + id: 4, + parent: 1, + reportLeafIndex: undefined, + }, + { + name: "http://example.com/app.js", + bytes: 0, + totalBytes: 100, + count: 0, + totalCount: 10, + children: [ + { + name: "JSScript", + bytes: 100, + totalBytes: 100, + count: 10, + totalCount: 10, + children: undefined, + id: 3, + parent: 2, + reportLeafIndex: 2, + } + ], + id: 2, + parent: 1, + reportLeafIndex: undefined, + } + ], + id: 1, + parent: undefined, + reportLeafIndex: undefined, + }; + + compareCensusViewData(BREAKDOWN, REPORT, EXPECTED); +} diff --git a/dom/heapsnapshot/tests/unit/test_census-tree-node-09.js b/dom/heapsnapshot/tests/unit/test_census-tree-node-09.js new file mode 100644 index 000000000..3efed04b0 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_census-tree-node-09.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * Test that repeatedly converting the same census report to a CensusTreeNode + * tree results in the same CensusTreeNode tree. + */ + +function run_test() { + const BREAKDOWN = { + by: "filename", + then: { + by: "internalType", + then: { by: "count", count: true, bytes: true } + }, + noFilename: { + by: "internalType", + then: { by: "count", count: true, bytes: true } + }, + }; + + const REPORT = { + "http://example.com/app.js": { + JSScript: { count: 10, bytes: 100 } + }, + "http://example.com/ads.js": { + "js::LazyScript": { count: 20, bytes: 200 } + }, + "http://example.com/trackers.js": { + JSScript: { count: 30, bytes: 300 } + }, + noFilename: { + "js::jit::JitCode": { count: 40, bytes: 400 } + } + }; + + const first = censusReportToCensusTreeNode(BREAKDOWN, REPORT); + const second = censusReportToCensusTreeNode(BREAKDOWN, REPORT); + const third = censusReportToCensusTreeNode(BREAKDOWN, REPORT); + + assertStructurallyEquivalent(first, second); + assertStructurallyEquivalent(second, third); +} diff --git a/dom/heapsnapshot/tests/unit/test_census-tree-node-10.js b/dom/heapsnapshot/tests/unit/test_census-tree-node-10.js new file mode 100644 index 000000000..b7798f23f --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_census-tree-node-10.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * Test when multiple leaves in the census report map to the same node in an + * inverted CensusReportTree. + */ + +function run_test() { + const BREAKDOWN = { + by: "coarseType", + objects: { + by: "objectClass", + then: { by: "count", count: true, bytes: true }, + }, + other: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + strings: { by: "count", count: true, bytes: true }, + scripts: { by: "count", count: true, bytes: true }, + }; + + const REPORT = { + objects: { + Array: { count: 1, bytes: 10 }, + }, + other: { + Array: { count: 1, bytes: 10 }, + }, + strings: { count: 0, bytes: 0 }, + scripts: { count: 0, bytes: 0 }, + }; + + const node = censusReportToCensusTreeNode(BREAKDOWN, REPORT, { invert: true }); + + equal(node.children[0].name, "Array"); + equal(node.children[0].reportLeafIndex.size, 2); + dumpn(`node.children[0].reportLeafIndex = ${[...node.children[0].reportLeafIndex]}`); + ok(node.children[0].reportLeafIndex.has(2)); + ok(node.children[0].reportLeafIndex.has(6)); +} diff --git a/dom/heapsnapshot/tests/unit/test_census_diff_01.js b/dom/heapsnapshot/tests/unit/test_census_diff_01.js new file mode 100644 index 000000000..75977bccb --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_census_diff_01.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test diffing census reports of breakdown by "internalType". + +const BREAKDOWN = { + by: "internalType", + then: { by: "count", count: true, bytes: true } +}; + +const REPORT1 = { + "JSObject": { + "count": 10, + "bytes": 100, + }, + "js::Shape": { + "count": 50, + "bytes": 500, + }, + "JSString": { + "count": 0, + "bytes": 0, + }, + "js::LazyScript": { + "count": 1, + "bytes": 10, + }, +}; + +const REPORT2 = { + "JSObject": { + "count": 11, + "bytes": 110, + }, + "js::Shape": { + "count": 51, + "bytes": 510, + }, + "JSString": { + "count": 1, + "bytes": 1, + }, + "js::BaseShape": { + "count": 1, + "bytes": 42, + }, +}; + +const EXPECTED = { + "JSObject": { + "count": 1, + "bytes": 10, + }, + "js::Shape": { + "count": 1, + "bytes": 10, + }, + "JSString": { + "count": 1, + "bytes": 1, + }, + "js::LazyScript": { + "count": -1, + "bytes": -10, + }, + "js::BaseShape": { + "count": 1, + "bytes": 42, + }, +}; + +function run_test() { + assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED); +} diff --git a/dom/heapsnapshot/tests/unit/test_census_diff_02.js b/dom/heapsnapshot/tests/unit/test_census_diff_02.js new file mode 100644 index 000000000..169e3f036 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_census_diff_02.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test diffing census reports of breakdown by "count". + +const BREAKDOWN = { by: "count", count: true, bytes: true }; + +const REPORT1 = { + "count": 10, + "bytes": 100, +}; + +const REPORT2 = { + "count": 11, + "bytes": 110, +}; + +const EXPECTED = { + "count": 1, + "bytes": 10, +}; + +function run_test() { + assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED); +} diff --git a/dom/heapsnapshot/tests/unit/test_census_diff_03.js b/dom/heapsnapshot/tests/unit/test_census_diff_03.js new file mode 100644 index 000000000..6dbca3e40 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_census_diff_03.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test diffing census reports of breakdown by "coarseType". + +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 }, +}; + +const REPORT1 = { + objects: { + count: 1, + bytes: 10, + }, + scripts: { + count: 1, + bytes: 10, + }, + strings: { + count: 1, + bytes: 10, + }, + other: { + count: 3, + bytes: 30, + }, +}; + +const REPORT2 = { + objects: { + count: 1, + bytes: 10, + }, + scripts: { + count: 0, + bytes: 0, + }, + strings: { + count: 2, + bytes: 20, + }, + other: { + count: 4, + bytes: 40, + }, +}; + +const EXPECTED = { + objects: { + count: 0, + bytes: 0, + }, + scripts: { + count: -1, + bytes: -10, + }, + strings: { + count: 1, + bytes: 10, + }, + other: { + count: 1, + bytes: 10, + }, +}; + +function run_test() { + assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED); +} diff --git a/dom/heapsnapshot/tests/unit/test_census_diff_04.js b/dom/heapsnapshot/tests/unit/test_census_diff_04.js new file mode 100644 index 000000000..a10097945 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_census_diff_04.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test diffing census reports of breakdown by "objectClass". + +const BREAKDOWN = { + by: "objectClass", + then: { by: "count", count: true, bytes: true }, + other: { by: "count", count: true, bytes: true }, +}; + +const REPORT1 = { + "Array": { + count: 1, + bytes: 100, + }, + "Function": { + count: 10, + bytes: 10, + }, + "other": { + count: 10, + bytes: 100, + } +}; + +const REPORT2 = { + "Object": { + count: 1, + bytes: 100, + }, + "Function": { + count: 20, + bytes: 20, + }, + "other": { + count: 10, + bytes: 100, + } +}; + +const EXPECTED = { + "Array": { + count: -1, + bytes: -100, + }, + "Function": { + count: 10, + bytes: 10, + }, + "other": { + count: 0, + bytes: 0, + }, + "Object": { + count: 1, + bytes: 100, + }, +}; + +function run_test() { + assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED); +} diff --git a/dom/heapsnapshot/tests/unit/test_census_diff_05.js b/dom/heapsnapshot/tests/unit/test_census_diff_05.js new file mode 100644 index 000000000..b6d99f823 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_census_diff_05.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test diffing census reports of breakdown by "allocationStack". + +const BREAKDOWN = { + by: "allocationStack", + then: { by: "count", count: true, bytes: true }, + noStack: { by: "count", count: true, bytes: true }, +}; + +const stack1 = saveStack(); +const stack2 = saveStack(); +const stack3 = saveStack(); + +const REPORT1 = new Map([ + [stack1, { "count": 10, "bytes": 100 }], + [stack2, { "count": 1, "bytes": 10 }], +]); + +const REPORT2 = new Map([ + [stack2, { "count": 10, "bytes": 100 }], + [stack3, { "count": 1, "bytes": 10 }], +]); + +const EXPECTED = new Map([ + [stack1, { "count": -10, "bytes": -100 }], + [stack2, { "count": 9, "bytes": 90 }], + [stack3, { "count": 1, "bytes": 10 }], +]); + +function run_test() { + assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED); +} diff --git a/dom/heapsnapshot/tests/unit/test_census_diff_06.js b/dom/heapsnapshot/tests/unit/test_census_diff_06.js new file mode 100644 index 000000000..430ff8c9c --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_census_diff_06.js @@ -0,0 +1,137 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test diffing census reports of a "complex" and "realistic" breakdown. + +const BREAKDOWN = { + by: "coarseType", + objects: { + by: "allocationStack", + then: { + by: "objectClass", + then: { by: "count", count: false, bytes: true }, + other: { by: "count", count: false, bytes: true } + }, + noStack: { + by: "objectClass", + then: { by: "count", count: false, bytes: true }, + other: { by: "count", count: false, bytes: true } + } + }, + strings: { + by: "internalType", + then: { by: "count", count: false, bytes: true } + }, + scripts: { + by: "internalType", + then: { by: "count", count: false, bytes: true } + }, + other: { + by: "internalType", + then: { by: "count", count: false, bytes: true } + }, +}; + +const stack1 = saveStack(); +const stack2 = saveStack(); +const stack3 = saveStack(); + +const REPORT1 = { + objects: new Map([ + [stack1, { Function: { bytes: 1 }, + Object: { bytes: 2 }, + other: { bytes: 0 }, + }], + [stack2, { Array: { bytes: 3 }, + Date: { bytes: 4 }, + other: { bytes: 0 }, + }], + ["noStack", { Object: { bytes: 3 }}], + ]), + strings: { + JSAtom: { bytes: 10 }, + JSLinearString: { bytes: 5 }, + }, + scripts: { + JSScript: { bytes: 1 }, + "js::jit::JitCode": { bytes: 2 }, + }, + other: { + "mozilla::dom::Thing": { bytes: 1 }, + } +}; + +const REPORT2 = { + objects: new Map([ + [stack2, { Array: { bytes: 1 }, + Date: { bytes: 2 }, + other: { bytes: 3 }, + }], + [stack3, { Function: { bytes: 1 }, + Object: { bytes: 2 }, + other: { bytes: 0 }, + }], + ["noStack", { Object: { bytes: 3 }}], + ]), + strings: { + JSAtom: { bytes: 5 }, + JSLinearString: { bytes: 10 }, + }, + scripts: { + JSScript: { bytes: 2 }, + "js::LazyScript": { bytes: 42 }, + "js::jit::JitCode": { bytes: 1 }, + }, + other: { + "mozilla::dom::OtherThing": { bytes: 1 }, + } +}; + +const EXPECTED = { + "objects": new Map([ + [stack1, { Function: { bytes: -1 }, + Object: { bytes: -2 }, + other: { bytes: 0 }, + }], + [stack2, { Array: { bytes: -2 }, + Date: { bytes: -2 }, + other: { bytes: 3 }, + }], + [stack3, { Function: { bytes: 1 }, + Object: { bytes: 2 }, + other: { bytes: 0 }, + }], + ["noStack", { Object: { bytes: 0 }}], + ]), + "scripts": { + "JSScript": { + "bytes": 1 + }, + "js::jit::JitCode": { + "bytes": -1 + }, + "js::LazyScript": { + "bytes": 42 + } + }, + "strings": { + "JSAtom": { + "bytes": -5 + }, + "JSLinearString": { + "bytes": 5 + } + }, + "other": { + "mozilla::dom::Thing": { + "bytes": -1 + }, + "mozilla::dom::OtherThing": { + "bytes": 1 + } + } +}; + +function run_test() { + assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED); +} diff --git a/dom/heapsnapshot/tests/unit/test_census_filtering_01.js b/dom/heapsnapshot/tests/unit/test_census_filtering_01.js new file mode 100644 index 000000000..57724d7c1 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_census_filtering_01.js @@ -0,0 +1,105 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test filtering basic CensusTreeNode trees. + +function run_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 }, + }, + }; + + const REPORT = { + objects: { + Array: { bytes: 50, count: 5 }, + UInt8Array: { bytes: 80, count: 8 }, + Int32Array: { bytes: 320, count: 32 }, + other: { bytes: 0, count: 0 }, + }, + scripts: { + "js::jit::JitScript": { bytes: 30, count: 3 }, + }, + strings: { + JSAtom: { bytes: 60, count: 6 }, + }, + other: { + "js::Shape": { bytes: 80, count: 8 }, + } + }; + + const EXPECTED = { + name: null, + bytes: 0, + totalBytes: 620, + count: 0, + totalCount: 62, + children: [ + { + name: "objects", + bytes: 0, + totalBytes: 450, + count: 0, + totalCount: 45, + children: [ + { + name: "Int32Array", + bytes: 320, + totalBytes: 320, + count: 32, + totalCount: 32, + children: undefined, + id: 15, + parent: 14, + reportLeafIndex: 4, + }, + { + name: "UInt8Array", + bytes: 80, + totalBytes: 80, + count: 8, + totalCount: 8, + children: undefined, + id: 16, + parent: 14, + reportLeafIndex: 3, + }, + { + name: "Array", + bytes: 50, + totalBytes: 50, + count: 5, + totalCount: 5, + children: undefined, + id: 17, + parent: 14, + reportLeafIndex: 2, + } + ], + id: 14, + parent: 13, + reportLeafIndex: undefined, + } + ], + id: 13, + parent: undefined, + reportLeafIndex: undefined, + }; + + compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { filter: "Array" }); +} diff --git a/dom/heapsnapshot/tests/unit/test_census_filtering_02.js b/dom/heapsnapshot/tests/unit/test_census_filtering_02.js new file mode 100644 index 000000000..0a57ce66d --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_census_filtering_02.js @@ -0,0 +1,124 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test filtering CensusTreeNode trees with an `allocationStack` breakdown. + +function run_test() { + const countBreakdown = { by: "count", count: true, bytes: true }; + + const BREAKDOWN = { + by: "allocationStack", + then: countBreakdown, + noStack: countBreakdown, + }; + + let stack1, stack2, stack3, stack4, stack5; + + (function foo() { + (function bar() { + (function baz() { + stack1 = saveStack(3); + }()); + (function quux() { + stack2 = saveStack(3); + stack3 = saveStack(3); + }()); + }()); + stack4 = saveStack(2); + }()); + + stack5 = saveStack(1); + + const REPORT = new Map([ + [stack1, { bytes: 10, count: 1 }], + [stack2, { bytes: 20, count: 2 }], + [stack3, { bytes: 30, count: 3 }], + [stack4, { bytes: 40, count: 4 }], + [stack5, { bytes: 50, count: 5 }], + ["noStack", { bytes: 60, count: 6 }], + ]); + + const EXPECTED = { + name: null, + bytes: 0, + totalBytes: 210, + count: 0, + totalCount: 21, + children: [ + { + name: stack1.parent.parent, + bytes: 0, + totalBytes: 60, + count: 0, + totalCount: 6, + children: [ + { + name: stack2.parent, + bytes: 0, + totalBytes: 50, + count: 0, + totalCount: 5, + children: [ + { + name: stack3, + bytes: 30, + totalBytes: 30, + count: 3, + totalCount: 3, + children: undefined, + id: 15, + parent: 14, + reportLeafIndex: 3, + }, + { + name: stack2, + bytes: 20, + totalBytes: 20, + count: 2, + totalCount: 2, + children: undefined, + id: 16, + parent: 14, + reportLeafIndex: 2, + } + ], + id: 14, + parent: 13, + reportLeafIndex: undefined, + }, + { + name: stack1.parent, + bytes: 0, + totalBytes: 10, + count: 0, + totalCount: 1, + children: [ + { + name: stack1, + bytes: 10, + totalBytes: 10, + count: 1, + totalCount: 1, + children: undefined, + id: 18, + parent: 17, + reportLeafIndex: 1, + } + ], + id: 17, + parent: 13, + reportLeafIndex: undefined, + } + ], + id: 13, + parent: 12, + reportLeafIndex: undefined, + } + ], + id: 12, + parent: undefined, + reportLeafIndex: undefined, + }; + + compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { filter: "bar" }); +} diff --git a/dom/heapsnapshot/tests/unit/test_census_filtering_03.js b/dom/heapsnapshot/tests/unit/test_census_filtering_03.js new file mode 100644 index 000000000..2c69a14b8 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_census_filtering_03.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test filtering with no matches. + +function run_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 }, + }, + }; + + const REPORT = { + objects: { + Array: { bytes: 50, count: 5 }, + UInt8Array: { bytes: 80, count: 8 }, + Int32Array: { bytes: 320, count: 32 }, + other: { bytes: 0, count: 0 }, + }, + scripts: { + "js::jit::JitScript": { bytes: 30, count: 3 }, + }, + strings: { + JSAtom: { bytes: 60, count: 6 }, + }, + other: { + "js::Shape": { bytes: 80, count: 8 }, + } + }; + + const EXPECTED = { + name: null, + bytes: 0, + totalBytes: 620, + count: 0, + totalCount: 62, + children: undefined, + id: 13, + parent: undefined, + reportLeafIndex: undefined, + }; + + compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { filter: "zzzzzzzzzzzzzzzzzzzz" }); +} diff --git a/dom/heapsnapshot/tests/unit/test_census_filtering_04.js b/dom/heapsnapshot/tests/unit/test_census_filtering_04.js new file mode 100644 index 000000000..c9871436b --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_census_filtering_04.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test the filtered nodes' counts and bytes are the same as they were when +// unfiltered. + +function run_test() { + const COUNT = { by: "count", count: true, bytes: true }; + const INTERNAL_TYPE = { by: "internalType", then: COUNT }; + + const BREAKDOWN = { + by: "coarseType", + objects: { by: "objectClass", then: COUNT, other: COUNT }, + strings: COUNT, + scripts: { + by: "filename", + then: INTERNAL_TYPE, + noFilename: INTERNAL_TYPE + }, + other: INTERNAL_TYPE, + }; + + const REPORT = { + objects: { + Function: { + count: 7, + bytes: 70 + }, + Array: { + count: 6, + bytes: 60 + } + }, + scripts: { + "http://mozilla.github.io/pdf.js/build/pdf.js": { + "js::LazyScript": { + count: 4, + bytes: 40 + }, + } + }, + strings: { + count: 2, + bytes: 20 + }, + other: { + "js::Shape": { + count: 1, + bytes: 10 + } + } + }; + + const EXPECTED = { + name: null, + bytes: 0, + totalBytes: 200, + count: 0, + totalCount: 20, + parent: undefined, + children: [ + { + name: "objects", + bytes: 0, + totalBytes: 130, + count: 0, + totalCount: 13, + children: [ + { + name: "Function", + bytes: 70, + totalBytes: 70, + count: 7, + totalCount: 7, + id: 13, + parent: 12, + children: undefined, + reportLeafIndex: 2, + }, + { + name: "Array", + bytes: 60, + totalBytes: 60, + count: 6, + totalCount: 6, + id: 14, + parent: 12, + children: undefined, + reportLeafIndex: 3, + }, + ], + id: 12, + parent: 11, + reportLeafIndex: undefined, + } + ], + id: 11, + reportLeafIndex: undefined, + }; + + compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { filter: "objects" }); +} diff --git a/dom/heapsnapshot/tests/unit/test_census_filtering_05.js b/dom/heapsnapshot/tests/unit/test_census_filtering_05.js new file mode 100644 index 000000000..1d1f4fa55 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_census_filtering_05.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that filtered and inverted allocation stack census trees are sorted +// properly. + +function run_test() { + const countBreakdown = { by: "count", count: true, bytes: true }; + + const BREAKDOWN = { + by: "allocationStack", + then: countBreakdown, + noStack: countBreakdown, + }; + + const stacks = []; + + function foo(depth = 1) { + stacks.push(saveStack(depth)); + bar(depth + 1); + baz(depth + 1); + stacks.push(saveStack(depth)); + } + + function bar(depth = 1) { + stacks.push(saveStack(depth)); + stacks.push(saveStack(depth)); + } + + function baz(depth = 1) { + stacks.push(saveStack(depth)); + bang(depth + 1); + stacks.push(saveStack(depth)); + } + + function bang(depth = 1) { + stacks.push(saveStack(depth)); + stacks.push(saveStack(depth)); + stacks.push(saveStack(depth)); + } + + foo(); + bar(); + baz(); + bang(); + + const REPORT = new Map(stacks.map((s, i) => { + return [s, { + count: i + 1, + bytes: (i + 1) * 10 + }]; + })); + + const tree = censusReportToCensusTreeNode(BREAKDOWN, REPORT, { + filter: "baz", + invert: true + }); + + dumpn("tree = " + JSON.stringify(tree, savedFrameReplacer, 4)); + + (function assertSortedBySelf(node) { + if (node.children) { + let lastSelfBytes = Infinity; + for (let child of node.children) { + ok(child.bytes <= lastSelfBytes, `${child.bytes} <= ${lastSelfBytes}`); + lastSelfBytes = child.bytes; + assertSortedBySelf(child); + } + } + }(tree)); +} diff --git a/dom/heapsnapshot/tests/unit/test_countToBucketBreakdown_01.js b/dom/heapsnapshot/tests/unit/test_countToBucketBreakdown_01.js new file mode 100644 index 000000000..e89048c33 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_countToBucketBreakdown_01.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that we can turn a breakdown with { by: "count" } leaves into a +// breakdown with { by: "bucket" } leaves. + +const COUNT = { by: "count", count: true, bytes: true }; +const BUCKET = { by: "bucket" }; + +const BREAKDOWN = { + by: "coarseType", + objects: { by: "objectClass", then: COUNT, other: COUNT }, + strings: COUNT, + scripts: { + by: "filename", + then: { by: "internalType", then: COUNT }, + noFilename: { by: "internalType", then: COUNT }, + }, + other: { by: "internalType", then: COUNT }, +}; + +const EXPECTED = { + by: "coarseType", + objects: { by: "objectClass", then: BUCKET, other: BUCKET }, + strings: BUCKET, + scripts: { + by: "filename", + then: { by: "internalType", then: BUCKET }, + noFilename: { by: "internalType", then: BUCKET }, + }, + other: { by: "internalType", then: BUCKET }, +}; + +function run_test() { + assertCountToBucketBreakdown(BREAKDOWN, EXPECTED); +} diff --git a/dom/heapsnapshot/tests/unit/test_deduplicatePaths_01.js b/dom/heapsnapshot/tests/unit/test_deduplicatePaths_01.js new file mode 100644 index 000000000..418b49db3 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_deduplicatePaths_01.js @@ -0,0 +1,113 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test the behavior of the deduplicatePaths utility function. + +function edge(from, to, name) { + return { from, to, name }; +} + +function run_test() { + const a = 1; + const b = 2; + const c = 3; + const d = 4; + const e = 5; + const f = 6; + const g = 7; + + dumpn("Single long path"); + assertDeduplicatedPaths({ + target: g, + paths: [ + [ + pathEntry(a, "e1"), + pathEntry(b, "e2"), + pathEntry(c, "e3"), + pathEntry(d, "e4"), + pathEntry(e, "e5"), + pathEntry(f, "e6"), + ] + ], + expectedNodes: [a, b, c, d, e, f, g], + expectedEdges: [ + edge(a, b, "e1"), + edge(b, c, "e2"), + edge(c, d, "e3"), + edge(d, e, "e4"), + edge(e, f, "e5"), + edge(f, g, "e6"), + ] + }); + + dumpn("Multiple edges from and to the same nodes"); + assertDeduplicatedPaths({ + target: a, + paths: [ + [pathEntry(b, "x")], + [pathEntry(b, "y")], + [pathEntry(b, "z")], + ], + expectedNodes: [a, b], + expectedEdges: [ + edge(b, a, "x"), + edge(b, a, "y"), + edge(b, a, "z"), + ] + }); + + dumpn("Multiple paths sharing some nodes and edges"); + assertDeduplicatedPaths({ + target: g, + paths: [ + [ + pathEntry(a, "a->b"), + pathEntry(b, "b->c"), + pathEntry(c, "foo"), + ], + [ + pathEntry(a, "a->b"), + pathEntry(b, "b->d"), + pathEntry(d, "bar"), + ], + [ + pathEntry(a, "a->b"), + pathEntry(b, "b->e"), + pathEntry(e, "baz"), + ], + ], + expectedNodes: [a, b, c, d, e, g], + expectedEdges: [ + edge(a, b, "a->b"), + edge(b, c, "b->c"), + edge(b, d, "b->d"), + edge(b, e, "b->e"), + edge(c, g, "foo"), + edge(d, g, "bar"), + edge(e, g, "baz"), + ] + }); + + dumpn("Second shortest path contains target itself"); + assertDeduplicatedPaths({ + target: g, + paths: [ + [ + pathEntry(a, "a->b"), + pathEntry(b, "b->g"), + ], + [ + pathEntry(a, "a->b"), + pathEntry(b, "b->g"), + pathEntry(g, "g->f"), + pathEntry(f, "f->g"), + ], + ], + expectedNodes: [a, b, g], + expectedEdges: [ + edge(a, b, "a->b"), + edge(b, g, "b->g"), + ] + }); +} diff --git a/dom/heapsnapshot/tests/unit/test_getCensusIndividuals_01.js b/dom/heapsnapshot/tests/unit/test_getCensusIndividuals_01.js new file mode 100644 index 000000000..9c4f60991 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_getCensusIndividuals_01.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test basic functionality of `CensusUtils.getCensusIndividuals`. + +function run_test() { + const stack1 = saveStack(1); + const stack2 = saveStack(1); + const stack3 = saveStack(1); + + const COUNT = { by: "count", count: true, bytes: true }; + const INTERNAL_TYPE = { by: "internalType", then: COUNT }; + + const BREAKDOWN = { + by: "allocationStack", + then: INTERNAL_TYPE, + noStack: INTERNAL_TYPE, + }; + + const MOCK_SNAPSHOT = { + takeCensus: ({ breakdown }) => { + assertStructurallyEquivalent( + breakdown, + CensusUtils.countToBucketBreakdown(BREAKDOWN)); + + // DFS Index + return new Map([ // 0 + [stack1, { // 1 + JSObject: [101, 102, 103], // 2 + JSString: [111, 112, 113], // 3 + }], + [stack2, { // 4 + JSObject: [201, 202, 203], // 5 + JSString: [211, 212, 213], // 6 + }], + [stack3, { // 7 + JSObject: [301, 302, 303], // 8 + JSString: [311, 312, 313], // 9 + }], + ["noStack", { // 10 + JSObject: [401, 402, 403], // 11 + JSString: [411, 412, 413], // 12 + }], + ]); + } + }; + + const INDICES = new Set([3, 5, 9]); + + const EXPECTED = new Set([111, 112, 113, + 201, 202, 203, + 311, 312, 313]); + + const actual = new Set(CensusUtils.getCensusIndividuals(INDICES, + BREAKDOWN, + MOCK_SNAPSHOT)); + + assertStructurallyEquivalent(EXPECTED, actual); +} diff --git a/dom/heapsnapshot/tests/unit/test_getReportLeaves_01.js b/dom/heapsnapshot/tests/unit/test_getReportLeaves_01.js new file mode 100644 index 000000000..4c4298b6a --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_getReportLeaves_01.js @@ -0,0 +1,114 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test basic functionality of `CensusUtils.getReportLeaves`. + +function run_test() { + const BREAKDOWN = { + by: "coarseType", + objects: { + by: "objectClass", + then: { by: "count", count: true, bytes: true }, + other: { by: "count", count: true, bytes: true }, + }, + strings: { by: "count", count: true, bytes: true }, + scripts: { + by: "filename", + then: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + noFilename: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + }, + other: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + }; + + const REPORT = { + objects: { + Array: { count: 6, bytes: 60 }, + Function: { count: 1, bytes: 10 }, + Object: { count: 1, bytes: 10 }, + RegExp: { count: 1, bytes: 10 }, + other: { count: 0, bytes: 0 }, + }, + strings: { count: 1, bytes: 10 }, + scripts: { + "foo.js": { + JSScript: { count: 1, bytes: 10 }, + "js::jit::IonScript": { count: 1, bytes: 10 }, + }, + noFilename: { + JSScript: { count: 1, bytes: 10 }, + "js::jit::IonScript": { count: 1, bytes: 10 }, + }, + }, + other: { + "js::Shape": { count: 7, bytes: 70 }, + "js::BaseShape": { count: 1, bytes: 10 }, + }, + }; + + const root = censusReportToCensusTreeNode(BREAKDOWN, REPORT); + dumpn("CensusTreeNode tree = " + JSON.stringify(root, null, 4)); + + (function assertEveryNodeCanFindItsLeaf(node) { + if (node.reportLeafIndex) { + const [ leaf ] = CensusUtils.getReportLeaves(new Set([node.reportLeafIndex]), + BREAKDOWN, + REPORT); + ok(leaf, "Should be able to find leaf for a node with a reportLeafIndex = " + node.reportLeafIndex); + } + + if (node.children) { + for (let child of node.children) { + assertEveryNodeCanFindItsLeaf(child); + } + } + }(root)); + + // Test finding multiple leaves at a time. + + function find(name, node) { + if (node.name === name) { + return node; + } + + if (node.children) { + for (let child of node.children) { + const found = find(name, child); + if (found) { + return found; + } + } + } + } + + const arrayNode = find("Array", root); + ok(arrayNode); + equal(typeof arrayNode.reportLeafIndex, "number"); + + const shapeNode = find("js::Shape", root); + ok(shapeNode); + equal(typeof shapeNode.reportLeafIndex, "number"); + + const indices = new Set([arrayNode.reportLeafIndex, shapeNode.reportLeafIndex]); + const leaves = CensusUtils.getReportLeaves(indices, BREAKDOWN, REPORT); + equal(leaves.length, 2); + + // `getReportLeaves` does not guarantee order of the results, so handle both + // cases. + ok(leaves.some(l => l === REPORT.objects.Array)); + ok(leaves.some(l => l === REPORT.other["js::Shape"])); + + // Test that bad indices do not yield results. + + const none = CensusUtils.getReportLeaves(new Set([999999999999]), BREAKDOWN, REPORT); + equal(none.length, 0); +} diff --git a/dom/heapsnapshot/tests/unit/test_saveHeapSnapshot_e10s_01.js b/dom/heapsnapshot/tests/unit/test_saveHeapSnapshot_e10s_01.js new file mode 100644 index 000000000..067b9effb --- /dev/null +++ b/dom/heapsnapshot/tests/unit/test_saveHeapSnapshot_e10s_01.js @@ -0,0 +1,8 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test saving a heap snapshot in the sandboxed e10s child process. + +function run_test() { + run_test_in_child("../unit/test_SaveHeapSnapshot.js"); +} diff --git a/dom/heapsnapshot/tests/unit/xpcshell.ini b/dom/heapsnapshot/tests/unit/xpcshell.ini new file mode 100644 index 000000000..f84b282d1 --- /dev/null +++ b/dom/heapsnapshot/tests/unit/xpcshell.ini @@ -0,0 +1,98 @@ +[DEFAULT] +tags = devtools heapsnapshot devtools-memory +head = head_heapsnapshot.js +tail = +firefox-appdir = browser +skip-if = toolkit == 'android' + +support-files = + Census.jsm + dominator-tree-worker.js + heap-snapshot-worker.js + Match.jsm + +[test_census_diff_01.js] +[test_census_diff_02.js] +[test_census_diff_03.js] +[test_census_diff_04.js] +[test_census_diff_05.js] +[test_census_diff_06.js] +[test_census_filtering_01.js] +[test_census_filtering_02.js] +[test_census_filtering_03.js] +[test_census_filtering_04.js] +[test_census_filtering_05.js] +[test_census-tree-node-01.js] +[test_census-tree-node-02.js] +[test_census-tree-node-03.js] +[test_census-tree-node-04.js] +[test_census-tree-node-05.js] +[test_census-tree-node-06.js] +[test_census-tree-node-07.js] +[test_census-tree-node-08.js] +[test_census-tree-node-09.js] +[test_census-tree-node-10.js] +[test_countToBucketBreakdown_01.js] +[test_deduplicatePaths_01.js] +[test_DominatorTree_01.js] +[test_DominatorTree_02.js] +[test_DominatorTree_03.js] +[test_DominatorTree_04.js] +[test_DominatorTree_05.js] +[test_DominatorTree_06.js] +[test_DominatorTreeNode_attachShortestPaths_01.js] +[test_DominatorTreeNode_getNodeByIdAlongPath_01.js] +[test_DominatorTreeNode_insert_01.js] +[test_DominatorTreeNode_insert_02.js] +[test_DominatorTreeNode_insert_03.js] +[test_DominatorTreeNode_LabelAndShallowSize_01.js] +[test_DominatorTreeNode_LabelAndShallowSize_02.js] +[test_DominatorTreeNode_LabelAndShallowSize_03.js] +[test_DominatorTreeNode_LabelAndShallowSize_04.js] +[test_DominatorTreeNode_partialTraversal_01.js] +[test_getCensusIndividuals_01.js] +[test_getReportLeaves_01.js] +[test_HeapAnalyses_computeDominatorTree_01.js] +[test_HeapAnalyses_computeDominatorTree_02.js] +[test_HeapAnalyses_deleteHeapSnapshot_01.js] +[test_HeapAnalyses_deleteHeapSnapshot_02.js] +[test_HeapAnalyses_deleteHeapSnapshot_03.js] +[test_HeapAnalyses_getCensusIndividuals_01.js] +[test_HeapAnalyses_getCreationTime_01.js] +[test_HeapAnalyses_getDominatorTree_01.js] +[test_HeapAnalyses_getDominatorTree_02.js] +[test_HeapAnalyses_getImmediatelyDominated_01.js] +[test_HeapAnalyses_readHeapSnapshot_01.js] +[test_HeapAnalyses_takeCensusDiff_01.js] +[test_HeapAnalyses_takeCensusDiff_02.js] +[test_HeapAnalyses_takeCensus_01.js] +[test_HeapAnalyses_takeCensus_02.js] +[test_HeapAnalyses_takeCensus_03.js] +[test_HeapAnalyses_takeCensus_04.js] +[test_HeapAnalyses_takeCensus_05.js] +[test_HeapAnalyses_takeCensus_06.js] +[test_HeapAnalyses_takeCensus_07.js] +[test_HeapSnapshot_creationTime_01.js] +[test_HeapSnapshot_deepStack_01.js] +[test_HeapSnapshot_describeNode_01.js] +[test_HeapSnapshot_computeShortestPaths_01.js] +[test_HeapSnapshot_computeShortestPaths_02.js] +[test_HeapSnapshot_takeCensus_01.js] +[test_HeapSnapshot_takeCensus_02.js] +[test_HeapSnapshot_takeCensus_03.js] +[test_HeapSnapshot_takeCensus_04.js] +[test_HeapSnapshot_takeCensus_05.js] +[test_HeapSnapshot_takeCensus_06.js] +[test_HeapSnapshot_takeCensus_07.js] +[test_HeapSnapshot_takeCensus_08.js] +[test_HeapSnapshot_takeCensus_09.js] +[test_HeapSnapshot_takeCensus_10.js] +[test_HeapSnapshot_takeCensus_11.js] +[test_HeapSnapshot_takeCensus_12.js] +[test_ReadHeapSnapshot.js] +[test_ReadHeapSnapshot_with_allocations.js] +skip-if = os == 'linux' # Bug 1176173 +[test_ReadHeapSnapshot_worker.js] +skip-if = os == 'linux' # Bug 1176173 +[test_SaveHeapSnapshot.js] +[test_saveHeapSnapshot_e10s_01.js] diff --git a/dom/moz.build b/dom/moz.build index cfcf6f865..89c539b4b 100644 --- a/dom/moz.build +++ b/dom/moz.build @@ -55,6 +55,7 @@ DIRS += [ 'gamepad', 'geolocation', 'grid', + 'heapsnapshot', 'html', 'json', 'jsurl', |