diff options
Diffstat (limited to 'devtools/shared/heapsnapshot')
135 files changed, 19152 insertions, 0 deletions
diff --git a/devtools/shared/heapsnapshot/AutoMemMap.cpp b/devtools/shared/heapsnapshot/AutoMemMap.cpp new file mode 100644 index 0000000000..8a7c89b4bf --- /dev/null +++ b/devtools/shared/heapsnapshot/AutoMemMap.cpp @@ -0,0 +1,67 @@ +/* -*- 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(nsIFile* file, int flags, int mode, + PRFileMapProtect prot) { + MOZ_ASSERT(!fd); + MOZ_ASSERT(!fileMap); + MOZ_ASSERT(!addr); + + nsresult rv; + int64_t inputFileSize; + rv = file->GetFileSize(&inputFileSize); + if (NS_FAILED(rv)) { + return rv; + } + + // Check if the file is too big to memmap. + if (inputFileSize > int64_t(UINT32_MAX)) { + return NS_ERROR_INVALID_ARG; + } + fileSize = uint32_t(inputFileSize); + + rv = file->OpenNSPRFileDesc(flags, mode, &fd); + if (NS_FAILED(rv)) { + return rv; + } + if (!fd) return NS_ERROR_UNEXPECTED; + + fileMap = PR_CreateFileMap(fd, inputFileSize, prot); + if (!fileMap) return NS_ERROR_UNEXPECTED; + + addr = PR_MemMap(fileMap, 0, fileSize); + if (!addr) return NS_ERROR_UNEXPECTED; + + return NS_OK; +} + +} // namespace devtools +} // namespace mozilla diff --git a/devtools/shared/heapsnapshot/AutoMemMap.h b/devtools/shared/heapsnapshot/AutoMemMap.h new file mode 100644 index 0000000000..f67dacf0df --- /dev/null +++ b/devtools/shared/heapsnapshot/AutoMemMap.h @@ -0,0 +1,77 @@ +/* -*- 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 "nsIFile.h" +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "ErrorList.h" + +namespace mozilla { +namespace devtools { + +// # AutoMemMap +// +// AutoMemMap is an RAII class to manage mapping a file to memory. It is a +// wrapper around 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 { + // At the time of this writing, this class supports file imports up to + // UINT32_MAX bytes due to limitations in the underlying function PR_MemMap. + uint32_t fileSize; + PRFileDesc* fd; + PRFileMap* fileMap; + void* addr; + + AutoMemMap(const AutoMemMap& aOther) = delete; + void operator=(const AutoMemMap& aOther) = delete; + + public: + explicit AutoMemMap() + : fileSize(0), fd(nullptr), fileMap(nullptr), addr(nullptr){}; + ~AutoMemMap(); + + // Initialize this AutoMemMap. + nsresult init(nsIFile* file, 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(fd, "Should only call size() if init() succeeded."); + return fileSize; + } + + // 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/devtools/shared/heapsnapshot/CensusUtils.js b/devtools/shared/heapsnapshot/CensusUtils.js new file mode 100644 index 0000000000..3af68d3b57 --- /dev/null +++ b/devtools/shared/heapsnapshot/CensusUtils.js @@ -0,0 +1,502 @@ +/* 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.descriptiveType = 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 }, + { edge: "domNode", referent: report.domNode, breakdown: breakdown.domNode }, + ]; +}; + +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 (const { + edge: ed, + referent, + breakdown: subBreakdown, + } of getReportEdges(breakdown, report)) { + recursiveWalk(subBreakdown, ed, 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 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 (const edg of unvisited) { + this._set(results, edg, this._get(other, edg)); + } + } + + 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 = function (node, getId = n => n.id, aggregator = {}) { + 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; +}; +exports.createParentMap = createParentMap; + +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/devtools/shared/heapsnapshot/CoreDump.pb.cc b/devtools/shared/heapsnapshot/CoreDump.pb.cc new file mode 100644 index 0000000000..9283733771 --- /dev/null +++ b/devtools/shared/heapsnapshot/CoreDump.pb.cc @@ -0,0 +1,2242 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: CoreDump.proto + +#include "CoreDump.pb.h" + +#include <algorithm> + +#include <google/protobuf/io/coded_stream.h> +#include <google/protobuf/extension_set.h> +#include <google/protobuf/wire_format_lite.h> +#include <google/protobuf/io/zero_copy_stream_impl_lite.h> +// @@protoc_insertion_point(includes) +#include <google/protobuf/port_def.inc> + +PROTOBUF_PRAGMA_INIT_SEG + +namespace _pb = ::PROTOBUF_NAMESPACE_ID; +namespace _pbi = _pb::internal; + +namespace mozilla { +namespace devtools { +namespace protobuf { +PROTOBUF_CONSTEXPR Metadata::Metadata( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.timestamp_)*/uint64_t{0u}} {} +struct MetadataDefaultTypeInternal { + PROTOBUF_CONSTEXPR MetadataDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~MetadataDefaultTypeInternal() {} + union { + Metadata _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 MetadataDefaultTypeInternal _Metadata_default_instance_; +PROTOBUF_CONSTEXPR StackFrame_Data::StackFrame_Data( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.parent_)*/nullptr + , /*decltype(_impl_.id_)*/uint64_t{0u} + , /*decltype(_impl_.line_)*/0u + , /*decltype(_impl_.column_)*/0u + , /*decltype(_impl_.issystem_)*/false + , /*decltype(_impl_.isselfhosted_)*/false + , /*decltype(_impl_.SourceOrRef_)*/{} + , /*decltype(_impl_.FunctionDisplayNameOrRef_)*/{} + , /*decltype(_impl_._oneof_case_)*/{}} {} +struct StackFrame_DataDefaultTypeInternal { + PROTOBUF_CONSTEXPR StackFrame_DataDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~StackFrame_DataDefaultTypeInternal() {} + union { + StackFrame_Data _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 StackFrame_DataDefaultTypeInternal _StackFrame_Data_default_instance_; +PROTOBUF_CONSTEXPR StackFrame::StackFrame( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_.StackFrameType_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_._oneof_case_)*/{}} {} +struct StackFrameDefaultTypeInternal { + PROTOBUF_CONSTEXPR StackFrameDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~StackFrameDefaultTypeInternal() {} + union { + StackFrame _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 StackFrameDefaultTypeInternal _StackFrame_default_instance_; +PROTOBUF_CONSTEXPR Node::Node( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.edges_)*/{} + , /*decltype(_impl_.allocationstack_)*/nullptr + , /*decltype(_impl_.id_)*/uint64_t{0u} + , /*decltype(_impl_.size_)*/uint64_t{0u} + , /*decltype(_impl_.coarsetype_)*/0u + , /*decltype(_impl_.TypeNameOrRef_)*/{} + , /*decltype(_impl_.JSObjectClassNameOrRef_)*/{} + , /*decltype(_impl_.ScriptFilenameOrRef_)*/{} + , /*decltype(_impl_.descriptiveTypeNameOrRef_)*/{} + , /*decltype(_impl_._oneof_case_)*/{}} {} +struct NodeDefaultTypeInternal { + PROTOBUF_CONSTEXPR NodeDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~NodeDefaultTypeInternal() {} + union { + Node _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 NodeDefaultTypeInternal _Node_default_instance_; +PROTOBUF_CONSTEXPR Edge::Edge( + ::_pbi::ConstantInitialized): _impl_{ + /*decltype(_impl_._has_bits_)*/{} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_.referent_)*/uint64_t{0u} + , /*decltype(_impl_.EdgeNameOrRef_)*/{} + , /*decltype(_impl_._oneof_case_)*/{}} {} +struct EdgeDefaultTypeInternal { + PROTOBUF_CONSTEXPR EdgeDefaultTypeInternal() + : _instance(::_pbi::ConstantInitialized{}) {} + ~EdgeDefaultTypeInternal() {} + union { + Edge _instance; + }; +}; +PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 EdgeDefaultTypeInternal _Edge_default_instance_; +} // namespace protobuf +} // namespace devtools +} // namespace mozilla +namespace mozilla { +namespace devtools { +namespace protobuf { + +// =================================================================== + +class Metadata::_Internal { + public: + using HasBits = decltype(std::declval<Metadata>()._impl_._has_bits_); + static void set_has_timestamp(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } +}; + +Metadata::Metadata(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:mozilla.devtools.protobuf.Metadata) +} +Metadata::Metadata(const Metadata& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + Metadata* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.timestamp_){}}; + + _internal_metadata_.MergeFrom<std::string>(from._internal_metadata_); + _this->_impl_.timestamp_ = from._impl_.timestamp_; + // @@protoc_insertion_point(copy_constructor:mozilla.devtools.protobuf.Metadata) +} + +inline void Metadata::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.timestamp_){uint64_t{0u}} + }; +} + +Metadata::~Metadata() { + // @@protoc_insertion_point(destructor:mozilla.devtools.protobuf.Metadata) + if (auto *arena = _internal_metadata_.DeleteReturnArena<std::string>()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void Metadata::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); +} + +void Metadata::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void Metadata::Clear() { +// @@protoc_insertion_point(message_clear_start:mozilla.devtools.protobuf.Metadata) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + _impl_.timestamp_ = uint64_t{0u}; + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear<std::string>(); +} + +const char* Metadata::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // optional uint64 timeStamp = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 8)) { + _Internal::set_has_timestamp(&has_bits); + _impl_.timestamp_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields<std::string>(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* Metadata::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:mozilla.devtools.protobuf.Metadata) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // optional uint64 timeStamp = 1; + if (cached_has_bits & 0x00000001u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt64ToArray(1, this->_internal_timestamp(), target); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast<int>(_internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:mozilla.devtools.protobuf.Metadata) + return target; +} + +size_t Metadata::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:mozilla.devtools.protobuf.Metadata) + size_t total_size = 0; + + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + // optional uint64 timeStamp = 1; + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000001u) { + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_timestamp()); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void Metadata::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast<const Metadata*>( + &from)); +} + +void Metadata::MergeFrom(const Metadata& from) { + Metadata* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:mozilla.devtools.protobuf.Metadata) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + if (from._internal_has_timestamp()) { + _this->_internal_set_timestamp(from._internal_timestamp()); + } + _this->_internal_metadata_.MergeFrom<std::string>(from._internal_metadata_); +} + +void Metadata::CopyFrom(const Metadata& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:mozilla.devtools.protobuf.Metadata) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool Metadata::IsInitialized() const { + return true; +} + +void Metadata::InternalSwap(Metadata* other) { + using std::swap; + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + swap(_impl_.timestamp_, other->_impl_.timestamp_); +} + +std::string Metadata::GetTypeName() const { + return "mozilla.devtools.protobuf.Metadata"; +} + + +// =================================================================== + +class StackFrame_Data::_Internal { + public: + using HasBits = decltype(std::declval<StackFrame_Data>()._impl_._has_bits_); + static void set_has_id(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } + static const ::mozilla::devtools::protobuf::StackFrame& parent(const StackFrame_Data* msg); + static void set_has_parent(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static void set_has_line(HasBits* has_bits) { + (*has_bits)[0] |= 4u; + } + static void set_has_column(HasBits* has_bits) { + (*has_bits)[0] |= 8u; + } + static void set_has_issystem(HasBits* has_bits) { + (*has_bits)[0] |= 16u; + } + static void set_has_isselfhosted(HasBits* has_bits) { + (*has_bits)[0] |= 32u; + } +}; + +const ::mozilla::devtools::protobuf::StackFrame& +StackFrame_Data::_Internal::parent(const StackFrame_Data* msg) { + return *msg->_impl_.parent_; +} +StackFrame_Data::StackFrame_Data(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:mozilla.devtools.protobuf.StackFrame.Data) +} +StackFrame_Data::StackFrame_Data(const StackFrame_Data& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + StackFrame_Data* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.parent_){nullptr} + , decltype(_impl_.id_){} + , decltype(_impl_.line_){} + , decltype(_impl_.column_){} + , decltype(_impl_.issystem_){} + , decltype(_impl_.isselfhosted_){} + , decltype(_impl_.SourceOrRef_){} + , decltype(_impl_.FunctionDisplayNameOrRef_){} + , /*decltype(_impl_._oneof_case_)*/{}}; + + _internal_metadata_.MergeFrom<std::string>(from._internal_metadata_); + if (from._internal_has_parent()) { + _this->_impl_.parent_ = new ::mozilla::devtools::protobuf::StackFrame(*from._impl_.parent_); + } + ::memcpy(&_impl_.id_, &from._impl_.id_, + static_cast<size_t>(reinterpret_cast<char*>(&_impl_.isselfhosted_) - + reinterpret_cast<char*>(&_impl_.id_)) + sizeof(_impl_.isselfhosted_)); + clear_has_SourceOrRef(); + switch (from.SourceOrRef_case()) { + case kSource: { + _this->_internal_set_source(from._internal_source()); + break; + } + case kSourceRef: { + _this->_internal_set_sourceref(from._internal_sourceref()); + break; + } + case SOURCEORREF_NOT_SET: { + break; + } + } + clear_has_FunctionDisplayNameOrRef(); + switch (from.FunctionDisplayNameOrRef_case()) { + case kFunctionDisplayName: { + _this->_internal_set_functiondisplayname(from._internal_functiondisplayname()); + break; + } + case kFunctionDisplayNameRef: { + _this->_internal_set_functiondisplaynameref(from._internal_functiondisplaynameref()); + break; + } + case FUNCTIONDISPLAYNAMEORREF_NOT_SET: { + break; + } + } + // @@protoc_insertion_point(copy_constructor:mozilla.devtools.protobuf.StackFrame.Data) +} + +inline void StackFrame_Data::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.parent_){nullptr} + , decltype(_impl_.id_){uint64_t{0u}} + , decltype(_impl_.line_){0u} + , decltype(_impl_.column_){0u} + , decltype(_impl_.issystem_){false} + , decltype(_impl_.isselfhosted_){false} + , decltype(_impl_.SourceOrRef_){} + , decltype(_impl_.FunctionDisplayNameOrRef_){} + , /*decltype(_impl_._oneof_case_)*/{} + }; + clear_has_SourceOrRef(); + clear_has_FunctionDisplayNameOrRef(); +} + +StackFrame_Data::~StackFrame_Data() { + // @@protoc_insertion_point(destructor:mozilla.devtools.protobuf.StackFrame.Data) + if (auto *arena = _internal_metadata_.DeleteReturnArena<std::string>()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void StackFrame_Data::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + if (this != internal_default_instance()) delete _impl_.parent_; + if (has_SourceOrRef()) { + clear_SourceOrRef(); + } + if (has_FunctionDisplayNameOrRef()) { + clear_FunctionDisplayNameOrRef(); + } +} + +void StackFrame_Data::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void StackFrame_Data::clear_SourceOrRef() { +// @@protoc_insertion_point(one_of_clear_start:mozilla.devtools.protobuf.StackFrame.Data) + switch (SourceOrRef_case()) { + case kSource: { + _impl_.SourceOrRef_.source_.Destroy(); + break; + } + case kSourceRef: { + // No need to clear + break; + } + case SOURCEORREF_NOT_SET: { + break; + } + } + _impl_._oneof_case_[0] = SOURCEORREF_NOT_SET; +} + +void StackFrame_Data::clear_FunctionDisplayNameOrRef() { +// @@protoc_insertion_point(one_of_clear_start:mozilla.devtools.protobuf.StackFrame.Data) + switch (FunctionDisplayNameOrRef_case()) { + case kFunctionDisplayName: { + _impl_.FunctionDisplayNameOrRef_.functiondisplayname_.Destroy(); + break; + } + case kFunctionDisplayNameRef: { + // No need to clear + break; + } + case FUNCTIONDISPLAYNAMEORREF_NOT_SET: { + break; + } + } + _impl_._oneof_case_[1] = FUNCTIONDISPLAYNAMEORREF_NOT_SET; +} + + +void StackFrame_Data::Clear() { +// @@protoc_insertion_point(message_clear_start:mozilla.devtools.protobuf.StackFrame.Data) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000001u) { + GOOGLE_DCHECK(_impl_.parent_ != nullptr); + _impl_.parent_->Clear(); + } + if (cached_has_bits & 0x0000003eu) { + ::memset(&_impl_.id_, 0, static_cast<size_t>( + reinterpret_cast<char*>(&_impl_.isselfhosted_) - + reinterpret_cast<char*>(&_impl_.id_)) + sizeof(_impl_.isselfhosted_)); + } + clear_SourceOrRef(); + clear_FunctionDisplayNameOrRef(); + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear<std::string>(); +} + +const char* StackFrame_Data::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // optional uint64 id = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 8)) { + _Internal::set_has_id(&has_bits); + _impl_.id_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional .mozilla.devtools.protobuf.StackFrame parent = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 18)) { + ptr = ctx->ParseMessage(_internal_mutable_parent(), ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional uint32 line = 3; + case 3: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 24)) { + _Internal::set_has_line(&has_bits); + _impl_.line_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint32(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional uint32 column = 4; + case 4: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 32)) { + _Internal::set_has_column(&has_bits); + _impl_.column_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint32(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // bytes source = 5; + case 5: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 42)) { + auto str = _internal_mutable_source(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // uint64 sourceRef = 6; + case 6: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 48)) { + _internal_set_sourceref(::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr)); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // bytes functionDisplayName = 7; + case 7: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 58)) { + auto str = _internal_mutable_functiondisplayname(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // uint64 functionDisplayNameRef = 8; + case 8: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 64)) { + _internal_set_functiondisplaynameref(::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr)); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional bool isSystem = 9; + case 9: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 72)) { + _Internal::set_has_issystem(&has_bits); + _impl_.issystem_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional bool isSelfHosted = 10; + case 10: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 80)) { + _Internal::set_has_isselfhosted(&has_bits); + _impl_.isselfhosted_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields<std::string>(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* StackFrame_Data::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:mozilla.devtools.protobuf.StackFrame.Data) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // optional uint64 id = 1; + if (cached_has_bits & 0x00000002u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt64ToArray(1, this->_internal_id(), target); + } + + // optional .mozilla.devtools.protobuf.StackFrame parent = 2; + if (cached_has_bits & 0x00000001u) { + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(2, _Internal::parent(this), + _Internal::parent(this).GetCachedSize(), target, stream); + } + + // optional uint32 line = 3; + if (cached_has_bits & 0x00000004u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt32ToArray(3, this->_internal_line(), target); + } + + // optional uint32 column = 4; + if (cached_has_bits & 0x00000008u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt32ToArray(4, this->_internal_column(), target); + } + + switch (SourceOrRef_case()) { + case kSource: { + target = stream->WriteBytesMaybeAliased( + 5, this->_internal_source(), target); + break; + } + case kSourceRef: { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt64ToArray(6, this->_internal_sourceref(), target); + break; + } + default: ; + } + switch (FunctionDisplayNameOrRef_case()) { + case kFunctionDisplayName: { + target = stream->WriteBytesMaybeAliased( + 7, this->_internal_functiondisplayname(), target); + break; + } + case kFunctionDisplayNameRef: { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt64ToArray(8, this->_internal_functiondisplaynameref(), target); + break; + } + default: ; + } + // optional bool isSystem = 9; + if (cached_has_bits & 0x00000010u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteBoolToArray(9, this->_internal_issystem(), target); + } + + // optional bool isSelfHosted = 10; + if (cached_has_bits & 0x00000020u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteBoolToArray(10, this->_internal_isselfhosted(), target); + } + + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast<int>(_internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:mozilla.devtools.protobuf.StackFrame.Data) + return target; +} + +size_t StackFrame_Data::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:mozilla.devtools.protobuf.StackFrame.Data) + size_t total_size = 0; + + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x0000003fu) { + // optional .mozilla.devtools.protobuf.StackFrame parent = 2; + if (cached_has_bits & 0x00000001u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize( + *_impl_.parent_); + } + + // optional uint64 id = 1; + if (cached_has_bits & 0x00000002u) { + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_id()); + } + + // optional uint32 line = 3; + if (cached_has_bits & 0x00000004u) { + total_size += ::_pbi::WireFormatLite::UInt32SizePlusOne(this->_internal_line()); + } + + // optional uint32 column = 4; + if (cached_has_bits & 0x00000008u) { + total_size += ::_pbi::WireFormatLite::UInt32SizePlusOne(this->_internal_column()); + } + + // optional bool isSystem = 9; + if (cached_has_bits & 0x00000010u) { + total_size += 1 + 1; + } + + // optional bool isSelfHosted = 10; + if (cached_has_bits & 0x00000020u) { + total_size += 1 + 1; + } + + } + switch (SourceOrRef_case()) { + // bytes source = 5; + case kSource: { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_source()); + break; + } + // uint64 sourceRef = 6; + case kSourceRef: { + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_sourceref()); + break; + } + case SOURCEORREF_NOT_SET: { + break; + } + } + switch (FunctionDisplayNameOrRef_case()) { + // bytes functionDisplayName = 7; + case kFunctionDisplayName: { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_functiondisplayname()); + break; + } + // uint64 functionDisplayNameRef = 8; + case kFunctionDisplayNameRef: { + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_functiondisplaynameref()); + break; + } + case FUNCTIONDISPLAYNAMEORREF_NOT_SET: { + break; + } + } + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void StackFrame_Data::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast<const StackFrame_Data*>( + &from)); +} + +void StackFrame_Data::MergeFrom(const StackFrame_Data& from) { + StackFrame_Data* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:mozilla.devtools.protobuf.StackFrame.Data) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x0000003fu) { + if (cached_has_bits & 0x00000001u) { + _this->_internal_mutable_parent()->::mozilla::devtools::protobuf::StackFrame::MergeFrom( + from._internal_parent()); + } + if (cached_has_bits & 0x00000002u) { + _this->_impl_.id_ = from._impl_.id_; + } + if (cached_has_bits & 0x00000004u) { + _this->_impl_.line_ = from._impl_.line_; + } + if (cached_has_bits & 0x00000008u) { + _this->_impl_.column_ = from._impl_.column_; + } + if (cached_has_bits & 0x00000010u) { + _this->_impl_.issystem_ = from._impl_.issystem_; + } + if (cached_has_bits & 0x00000020u) { + _this->_impl_.isselfhosted_ = from._impl_.isselfhosted_; + } + _this->_impl_._has_bits_[0] |= cached_has_bits; + } + switch (from.SourceOrRef_case()) { + case kSource: { + _this->_internal_set_source(from._internal_source()); + break; + } + case kSourceRef: { + _this->_internal_set_sourceref(from._internal_sourceref()); + break; + } + case SOURCEORREF_NOT_SET: { + break; + } + } + switch (from.FunctionDisplayNameOrRef_case()) { + case kFunctionDisplayName: { + _this->_internal_set_functiondisplayname(from._internal_functiondisplayname()); + break; + } + case kFunctionDisplayNameRef: { + _this->_internal_set_functiondisplaynameref(from._internal_functiondisplaynameref()); + break; + } + case FUNCTIONDISPLAYNAMEORREF_NOT_SET: { + break; + } + } + _this->_internal_metadata_.MergeFrom<std::string>(from._internal_metadata_); +} + +void StackFrame_Data::CopyFrom(const StackFrame_Data& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:mozilla.devtools.protobuf.StackFrame.Data) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool StackFrame_Data::IsInitialized() const { + return true; +} + +void StackFrame_Data::InternalSwap(StackFrame_Data* other) { + using std::swap; + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + ::PROTOBUF_NAMESPACE_ID::internal::memswap< + PROTOBUF_FIELD_OFFSET(StackFrame_Data, _impl_.isselfhosted_) + + sizeof(StackFrame_Data::_impl_.isselfhosted_) + - PROTOBUF_FIELD_OFFSET(StackFrame_Data, _impl_.parent_)>( + reinterpret_cast<char*>(&_impl_.parent_), + reinterpret_cast<char*>(&other->_impl_.parent_)); + swap(_impl_.SourceOrRef_, other->_impl_.SourceOrRef_); + swap(_impl_.FunctionDisplayNameOrRef_, other->_impl_.FunctionDisplayNameOrRef_); + swap(_impl_._oneof_case_[0], other->_impl_._oneof_case_[0]); + swap(_impl_._oneof_case_[1], other->_impl_._oneof_case_[1]); +} + +std::string StackFrame_Data::GetTypeName() const { + return "mozilla.devtools.protobuf.StackFrame.Data"; +} + + +// =================================================================== + +class StackFrame::_Internal { + public: + static const ::mozilla::devtools::protobuf::StackFrame_Data& data(const StackFrame* msg); +}; + +const ::mozilla::devtools::protobuf::StackFrame_Data& +StackFrame::_Internal::data(const StackFrame* msg) { + return *msg->_impl_.StackFrameType_.data_; +} +void StackFrame::set_allocated_data(::mozilla::devtools::protobuf::StackFrame_Data* data) { + ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation(); + clear_StackFrameType(); + if (data) { + ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena = + ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(data); + if (message_arena != submessage_arena) { + data = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage( + message_arena, data, submessage_arena); + } + set_has_data(); + _impl_.StackFrameType_.data_ = data; + } + // @@protoc_insertion_point(field_set_allocated:mozilla.devtools.protobuf.StackFrame.data) +} +StackFrame::StackFrame(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:mozilla.devtools.protobuf.StackFrame) +} +StackFrame::StackFrame(const StackFrame& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + StackFrame* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_.StackFrameType_){} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_._oneof_case_)*/{}}; + + _internal_metadata_.MergeFrom<std::string>(from._internal_metadata_); + clear_has_StackFrameType(); + switch (from.StackFrameType_case()) { + case kData: { + _this->_internal_mutable_data()->::mozilla::devtools::protobuf::StackFrame_Data::MergeFrom( + from._internal_data()); + break; + } + case kRef: { + _this->_internal_set_ref(from._internal_ref()); + break; + } + case STACKFRAMETYPE_NOT_SET: { + break; + } + } + // @@protoc_insertion_point(copy_constructor:mozilla.devtools.protobuf.StackFrame) +} + +inline void StackFrame::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_.StackFrameType_){} + , /*decltype(_impl_._cached_size_)*/{} + , /*decltype(_impl_._oneof_case_)*/{} + }; + clear_has_StackFrameType(); +} + +StackFrame::~StackFrame() { + // @@protoc_insertion_point(destructor:mozilla.devtools.protobuf.StackFrame) + if (auto *arena = _internal_metadata_.DeleteReturnArena<std::string>()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void StackFrame::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + if (has_StackFrameType()) { + clear_StackFrameType(); + } +} + +void StackFrame::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void StackFrame::clear_StackFrameType() { +// @@protoc_insertion_point(one_of_clear_start:mozilla.devtools.protobuf.StackFrame) + switch (StackFrameType_case()) { + case kData: { + if (GetArenaForAllocation() == nullptr) { + delete _impl_.StackFrameType_.data_; + } + break; + } + case kRef: { + // No need to clear + break; + } + case STACKFRAMETYPE_NOT_SET: { + break; + } + } + _impl_._oneof_case_[0] = STACKFRAMETYPE_NOT_SET; +} + + +void StackFrame::Clear() { +// @@protoc_insertion_point(message_clear_start:mozilla.devtools.protobuf.StackFrame) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + clear_StackFrameType(); + _internal_metadata_.Clear<std::string>(); +} + +const char* StackFrame::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // .mozilla.devtools.protobuf.StackFrame.Data data = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 10)) { + ptr = ctx->ParseMessage(_internal_mutable_data(), ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // uint64 ref = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 16)) { + _internal_set_ref(::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr)); + CHK_(ptr); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields<std::string>(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* StackFrame::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:mozilla.devtools.protobuf.StackFrame) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + switch (StackFrameType_case()) { + case kData: { + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(1, _Internal::data(this), + _Internal::data(this).GetCachedSize(), target, stream); + break; + } + case kRef: { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt64ToArray(2, this->_internal_ref(), target); + break; + } + default: ; + } + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast<int>(_internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:mozilla.devtools.protobuf.StackFrame) + return target; +} + +size_t StackFrame::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:mozilla.devtools.protobuf.StackFrame) + size_t total_size = 0; + + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + switch (StackFrameType_case()) { + // .mozilla.devtools.protobuf.StackFrame.Data data = 1; + case kData: { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize( + *_impl_.StackFrameType_.data_); + break; + } + // uint64 ref = 2; + case kRef: { + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_ref()); + break; + } + case STACKFRAMETYPE_NOT_SET: { + break; + } + } + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void StackFrame::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast<const StackFrame*>( + &from)); +} + +void StackFrame::MergeFrom(const StackFrame& from) { + StackFrame* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:mozilla.devtools.protobuf.StackFrame) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + switch (from.StackFrameType_case()) { + case kData: { + _this->_internal_mutable_data()->::mozilla::devtools::protobuf::StackFrame_Data::MergeFrom( + from._internal_data()); + break; + } + case kRef: { + _this->_internal_set_ref(from._internal_ref()); + break; + } + case STACKFRAMETYPE_NOT_SET: { + break; + } + } + _this->_internal_metadata_.MergeFrom<std::string>(from._internal_metadata_); +} + +void StackFrame::CopyFrom(const StackFrame& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:mozilla.devtools.protobuf.StackFrame) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool StackFrame::IsInitialized() const { + return true; +} + +void StackFrame::InternalSwap(StackFrame* other) { + using std::swap; + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_.StackFrameType_, other->_impl_.StackFrameType_); + swap(_impl_._oneof_case_[0], other->_impl_._oneof_case_[0]); +} + +std::string StackFrame::GetTypeName() const { + return "mozilla.devtools.protobuf.StackFrame"; +} + + +// =================================================================== + +class Node::_Internal { + public: + using HasBits = decltype(std::declval<Node>()._impl_._has_bits_); + static void set_has_id(HasBits* has_bits) { + (*has_bits)[0] |= 2u; + } + static void set_has_size(HasBits* has_bits) { + (*has_bits)[0] |= 4u; + } + static const ::mozilla::devtools::protobuf::StackFrame& allocationstack(const Node* msg); + static void set_has_allocationstack(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } + static void set_has_coarsetype(HasBits* has_bits) { + (*has_bits)[0] |= 8u; + } +}; + +const ::mozilla::devtools::protobuf::StackFrame& +Node::_Internal::allocationstack(const Node* msg) { + return *msg->_impl_.allocationstack_; +} +Node::Node(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:mozilla.devtools.protobuf.Node) +} +Node::Node(const Node& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + Node* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.edges_){from._impl_.edges_} + , decltype(_impl_.allocationstack_){nullptr} + , decltype(_impl_.id_){} + , decltype(_impl_.size_){} + , decltype(_impl_.coarsetype_){} + , decltype(_impl_.TypeNameOrRef_){} + , decltype(_impl_.JSObjectClassNameOrRef_){} + , decltype(_impl_.ScriptFilenameOrRef_){} + , decltype(_impl_.descriptiveTypeNameOrRef_){} + , /*decltype(_impl_._oneof_case_)*/{}}; + + _internal_metadata_.MergeFrom<std::string>(from._internal_metadata_); + if (from._internal_has_allocationstack()) { + _this->_impl_.allocationstack_ = new ::mozilla::devtools::protobuf::StackFrame(*from._impl_.allocationstack_); + } + ::memcpy(&_impl_.id_, &from._impl_.id_, + static_cast<size_t>(reinterpret_cast<char*>(&_impl_.coarsetype_) - + reinterpret_cast<char*>(&_impl_.id_)) + sizeof(_impl_.coarsetype_)); + clear_has_TypeNameOrRef(); + switch (from.TypeNameOrRef_case()) { + case kTypeName: { + _this->_internal_set_typename_(from._internal_typename_()); + break; + } + case kTypeNameRef: { + _this->_internal_set_typenameref(from._internal_typenameref()); + break; + } + case TYPENAMEORREF_NOT_SET: { + break; + } + } + clear_has_JSObjectClassNameOrRef(); + switch (from.JSObjectClassNameOrRef_case()) { + case kJsObjectClassName: { + _this->_internal_set_jsobjectclassname(from._internal_jsobjectclassname()); + break; + } + case kJsObjectClassNameRef: { + _this->_internal_set_jsobjectclassnameref(from._internal_jsobjectclassnameref()); + break; + } + case JSOBJECTCLASSNAMEORREF_NOT_SET: { + break; + } + } + clear_has_ScriptFilenameOrRef(); + switch (from.ScriptFilenameOrRef_case()) { + case kScriptFilename: { + _this->_internal_set_scriptfilename(from._internal_scriptfilename()); + break; + } + case kScriptFilenameRef: { + _this->_internal_set_scriptfilenameref(from._internal_scriptfilenameref()); + break; + } + case SCRIPTFILENAMEORREF_NOT_SET: { + break; + } + } + clear_has_descriptiveTypeNameOrRef(); + switch (from.descriptiveTypeNameOrRef_case()) { + case kDescriptiveTypeName: { + _this->_internal_set_descriptivetypename(from._internal_descriptivetypename()); + break; + } + case kDescriptiveTypeNameRef: { + _this->_internal_set_descriptivetypenameref(from._internal_descriptivetypenameref()); + break; + } + case DESCRIPTIVETYPENAMEORREF_NOT_SET: { + break; + } + } + // @@protoc_insertion_point(copy_constructor:mozilla.devtools.protobuf.Node) +} + +inline void Node::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.edges_){arena} + , decltype(_impl_.allocationstack_){nullptr} + , decltype(_impl_.id_){uint64_t{0u}} + , decltype(_impl_.size_){uint64_t{0u}} + , decltype(_impl_.coarsetype_){0u} + , decltype(_impl_.TypeNameOrRef_){} + , decltype(_impl_.JSObjectClassNameOrRef_){} + , decltype(_impl_.ScriptFilenameOrRef_){} + , decltype(_impl_.descriptiveTypeNameOrRef_){} + , /*decltype(_impl_._oneof_case_)*/{} + }; + clear_has_TypeNameOrRef(); + clear_has_JSObjectClassNameOrRef(); + clear_has_ScriptFilenameOrRef(); + clear_has_descriptiveTypeNameOrRef(); +} + +Node::~Node() { + // @@protoc_insertion_point(destructor:mozilla.devtools.protobuf.Node) + if (auto *arena = _internal_metadata_.DeleteReturnArena<std::string>()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void Node::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + _impl_.edges_.~RepeatedPtrField(); + if (this != internal_default_instance()) delete _impl_.allocationstack_; + if (has_TypeNameOrRef()) { + clear_TypeNameOrRef(); + } + if (has_JSObjectClassNameOrRef()) { + clear_JSObjectClassNameOrRef(); + } + if (has_ScriptFilenameOrRef()) { + clear_ScriptFilenameOrRef(); + } + if (has_descriptiveTypeNameOrRef()) { + clear_descriptiveTypeNameOrRef(); + } +} + +void Node::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void Node::clear_TypeNameOrRef() { +// @@protoc_insertion_point(one_of_clear_start:mozilla.devtools.protobuf.Node) + switch (TypeNameOrRef_case()) { + case kTypeName: { + _impl_.TypeNameOrRef_.typename__.Destroy(); + break; + } + case kTypeNameRef: { + // No need to clear + break; + } + case TYPENAMEORREF_NOT_SET: { + break; + } + } + _impl_._oneof_case_[0] = TYPENAMEORREF_NOT_SET; +} + +void Node::clear_JSObjectClassNameOrRef() { +// @@protoc_insertion_point(one_of_clear_start:mozilla.devtools.protobuf.Node) + switch (JSObjectClassNameOrRef_case()) { + case kJsObjectClassName: { + _impl_.JSObjectClassNameOrRef_.jsobjectclassname_.Destroy(); + break; + } + case kJsObjectClassNameRef: { + // No need to clear + break; + } + case JSOBJECTCLASSNAMEORREF_NOT_SET: { + break; + } + } + _impl_._oneof_case_[1] = JSOBJECTCLASSNAMEORREF_NOT_SET; +} + +void Node::clear_ScriptFilenameOrRef() { +// @@protoc_insertion_point(one_of_clear_start:mozilla.devtools.protobuf.Node) + switch (ScriptFilenameOrRef_case()) { + case kScriptFilename: { + _impl_.ScriptFilenameOrRef_.scriptfilename_.Destroy(); + break; + } + case kScriptFilenameRef: { + // No need to clear + break; + } + case SCRIPTFILENAMEORREF_NOT_SET: { + break; + } + } + _impl_._oneof_case_[2] = SCRIPTFILENAMEORREF_NOT_SET; +} + +void Node::clear_descriptiveTypeNameOrRef() { +// @@protoc_insertion_point(one_of_clear_start:mozilla.devtools.protobuf.Node) + switch (descriptiveTypeNameOrRef_case()) { + case kDescriptiveTypeName: { + _impl_.descriptiveTypeNameOrRef_.descriptivetypename_.Destroy(); + break; + } + case kDescriptiveTypeNameRef: { + // No need to clear + break; + } + case DESCRIPTIVETYPENAMEORREF_NOT_SET: { + break; + } + } + _impl_._oneof_case_[3] = DESCRIPTIVETYPENAMEORREF_NOT_SET; +} + + +void Node::Clear() { +// @@protoc_insertion_point(message_clear_start:mozilla.devtools.protobuf.Node) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + _impl_.edges_.Clear(); + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000001u) { + GOOGLE_DCHECK(_impl_.allocationstack_ != nullptr); + _impl_.allocationstack_->Clear(); + } + if (cached_has_bits & 0x0000000eu) { + ::memset(&_impl_.id_, 0, static_cast<size_t>( + reinterpret_cast<char*>(&_impl_.coarsetype_) - + reinterpret_cast<char*>(&_impl_.id_)) + sizeof(_impl_.coarsetype_)); + } + clear_TypeNameOrRef(); + clear_JSObjectClassNameOrRef(); + clear_ScriptFilenameOrRef(); + clear_descriptiveTypeNameOrRef(); + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear<std::string>(); +} + +const char* Node::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // optional uint64 id = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 8)) { + _Internal::set_has_id(&has_bits); + _impl_.id_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // bytes typeName = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 18)) { + auto str = _internal_mutable_typename_(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // uint64 typeNameRef = 3; + case 3: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 24)) { + _internal_set_typenameref(::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr)); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional uint64 size = 4; + case 4: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 32)) { + _Internal::set_has_size(&has_bits); + _impl_.size_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // repeated .mozilla.devtools.protobuf.Edge edges = 5; + case 5: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 42)) { + ptr -= 1; + do { + ptr += 1; + ptr = ctx->ParseMessage(_internal_add_edges(), ptr); + CHK_(ptr); + if (!ctx->DataAvailable(ptr)) break; + } while (::PROTOBUF_NAMESPACE_ID::internal::ExpectTag<42>(ptr)); + } else + goto handle_unusual; + continue; + // optional .mozilla.devtools.protobuf.StackFrame allocationStack = 6; + case 6: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 50)) { + ptr = ctx->ParseMessage(_internal_mutable_allocationstack(), ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // bytes jsObjectClassName = 7; + case 7: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 58)) { + auto str = _internal_mutable_jsobjectclassname(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // uint64 jsObjectClassNameRef = 8; + case 8: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 64)) { + _internal_set_jsobjectclassnameref(::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr)); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // optional uint32 coarseType = 9 [default = 0]; + case 9: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 72)) { + _Internal::set_has_coarsetype(&has_bits); + _impl_.coarsetype_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint32(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // bytes scriptFilename = 10; + case 10: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 82)) { + auto str = _internal_mutable_scriptfilename(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // uint64 scriptFilenameRef = 11; + case 11: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 88)) { + _internal_set_scriptfilenameref(::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr)); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // bytes descriptiveTypeName = 12; + case 12: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 98)) { + auto str = _internal_mutable_descriptivetypename(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // uint64 descriptiveTypeNameRef = 13; + case 13: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 104)) { + _internal_set_descriptivetypenameref(::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr)); + CHK_(ptr); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields<std::string>(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* Node::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:mozilla.devtools.protobuf.Node) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // optional uint64 id = 1; + if (cached_has_bits & 0x00000002u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt64ToArray(1, this->_internal_id(), target); + } + + switch (TypeNameOrRef_case()) { + case kTypeName: { + target = stream->WriteBytesMaybeAliased( + 2, this->_internal_typename_(), target); + break; + } + case kTypeNameRef: { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt64ToArray(3, this->_internal_typenameref(), target); + break; + } + default: ; + } + // optional uint64 size = 4; + if (cached_has_bits & 0x00000004u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt64ToArray(4, this->_internal_size(), target); + } + + // repeated .mozilla.devtools.protobuf.Edge edges = 5; + for (unsigned i = 0, + n = static_cast<unsigned>(this->_internal_edges_size()); i < n; i++) { + const auto& repfield = this->_internal_edges(i); + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(5, repfield, repfield.GetCachedSize(), target, stream); + } + + // optional .mozilla.devtools.protobuf.StackFrame allocationStack = 6; + if (cached_has_bits & 0x00000001u) { + target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite:: + InternalWriteMessage(6, _Internal::allocationstack(this), + _Internal::allocationstack(this).GetCachedSize(), target, stream); + } + + switch (JSObjectClassNameOrRef_case()) { + case kJsObjectClassName: { + target = stream->WriteBytesMaybeAliased( + 7, this->_internal_jsobjectclassname(), target); + break; + } + case kJsObjectClassNameRef: { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt64ToArray(8, this->_internal_jsobjectclassnameref(), target); + break; + } + default: ; + } + // optional uint32 coarseType = 9 [default = 0]; + if (cached_has_bits & 0x00000008u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt32ToArray(9, this->_internal_coarsetype(), target); + } + + switch (ScriptFilenameOrRef_case()) { + case kScriptFilename: { + target = stream->WriteBytesMaybeAliased( + 10, this->_internal_scriptfilename(), target); + break; + } + case kScriptFilenameRef: { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt64ToArray(11, this->_internal_scriptfilenameref(), target); + break; + } + default: ; + } + switch (descriptiveTypeNameOrRef_case()) { + case kDescriptiveTypeName: { + target = stream->WriteBytesMaybeAliased( + 12, this->_internal_descriptivetypename(), target); + break; + } + case kDescriptiveTypeNameRef: { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt64ToArray(13, this->_internal_descriptivetypenameref(), target); + break; + } + default: ; + } + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast<int>(_internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:mozilla.devtools.protobuf.Node) + return target; +} + +size_t Node::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:mozilla.devtools.protobuf.Node) + size_t total_size = 0; + + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + // repeated .mozilla.devtools.protobuf.Edge edges = 5; + total_size += 1UL * this->_internal_edges_size(); + for (const auto& msg : this->_impl_.edges_) { + total_size += + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize(msg); + } + + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x0000000fu) { + // optional .mozilla.devtools.protobuf.StackFrame allocationStack = 6; + if (cached_has_bits & 0x00000001u) { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::MessageSize( + *_impl_.allocationstack_); + } + + // optional uint64 id = 1; + if (cached_has_bits & 0x00000002u) { + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_id()); + } + + // optional uint64 size = 4; + if (cached_has_bits & 0x00000004u) { + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_size()); + } + + // optional uint32 coarseType = 9 [default = 0]; + if (cached_has_bits & 0x00000008u) { + total_size += ::_pbi::WireFormatLite::UInt32SizePlusOne(this->_internal_coarsetype()); + } + + } + switch (TypeNameOrRef_case()) { + // bytes typeName = 2; + case kTypeName: { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_typename_()); + break; + } + // uint64 typeNameRef = 3; + case kTypeNameRef: { + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_typenameref()); + break; + } + case TYPENAMEORREF_NOT_SET: { + break; + } + } + switch (JSObjectClassNameOrRef_case()) { + // bytes jsObjectClassName = 7; + case kJsObjectClassName: { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_jsobjectclassname()); + break; + } + // uint64 jsObjectClassNameRef = 8; + case kJsObjectClassNameRef: { + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_jsobjectclassnameref()); + break; + } + case JSOBJECTCLASSNAMEORREF_NOT_SET: { + break; + } + } + switch (ScriptFilenameOrRef_case()) { + // bytes scriptFilename = 10; + case kScriptFilename: { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_scriptfilename()); + break; + } + // uint64 scriptFilenameRef = 11; + case kScriptFilenameRef: { + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_scriptfilenameref()); + break; + } + case SCRIPTFILENAMEORREF_NOT_SET: { + break; + } + } + switch (descriptiveTypeNameOrRef_case()) { + // bytes descriptiveTypeName = 12; + case kDescriptiveTypeName: { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_descriptivetypename()); + break; + } + // uint64 descriptiveTypeNameRef = 13; + case kDescriptiveTypeNameRef: { + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_descriptivetypenameref()); + break; + } + case DESCRIPTIVETYPENAMEORREF_NOT_SET: { + break; + } + } + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void Node::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast<const Node*>( + &from)); +} + +void Node::MergeFrom(const Node& from) { + Node* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:mozilla.devtools.protobuf.Node) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + _this->_impl_.edges_.MergeFrom(from._impl_.edges_); + cached_has_bits = from._impl_._has_bits_[0]; + if (cached_has_bits & 0x0000000fu) { + if (cached_has_bits & 0x00000001u) { + _this->_internal_mutable_allocationstack()->::mozilla::devtools::protobuf::StackFrame::MergeFrom( + from._internal_allocationstack()); + } + if (cached_has_bits & 0x00000002u) { + _this->_impl_.id_ = from._impl_.id_; + } + if (cached_has_bits & 0x00000004u) { + _this->_impl_.size_ = from._impl_.size_; + } + if (cached_has_bits & 0x00000008u) { + _this->_impl_.coarsetype_ = from._impl_.coarsetype_; + } + _this->_impl_._has_bits_[0] |= cached_has_bits; + } + switch (from.TypeNameOrRef_case()) { + case kTypeName: { + _this->_internal_set_typename_(from._internal_typename_()); + break; + } + case kTypeNameRef: { + _this->_internal_set_typenameref(from._internal_typenameref()); + break; + } + case TYPENAMEORREF_NOT_SET: { + break; + } + } + switch (from.JSObjectClassNameOrRef_case()) { + case kJsObjectClassName: { + _this->_internal_set_jsobjectclassname(from._internal_jsobjectclassname()); + break; + } + case kJsObjectClassNameRef: { + _this->_internal_set_jsobjectclassnameref(from._internal_jsobjectclassnameref()); + break; + } + case JSOBJECTCLASSNAMEORREF_NOT_SET: { + break; + } + } + switch (from.ScriptFilenameOrRef_case()) { + case kScriptFilename: { + _this->_internal_set_scriptfilename(from._internal_scriptfilename()); + break; + } + case kScriptFilenameRef: { + _this->_internal_set_scriptfilenameref(from._internal_scriptfilenameref()); + break; + } + case SCRIPTFILENAMEORREF_NOT_SET: { + break; + } + } + switch (from.descriptiveTypeNameOrRef_case()) { + case kDescriptiveTypeName: { + _this->_internal_set_descriptivetypename(from._internal_descriptivetypename()); + break; + } + case kDescriptiveTypeNameRef: { + _this->_internal_set_descriptivetypenameref(from._internal_descriptivetypenameref()); + break; + } + case DESCRIPTIVETYPENAMEORREF_NOT_SET: { + break; + } + } + _this->_internal_metadata_.MergeFrom<std::string>(from._internal_metadata_); +} + +void Node::CopyFrom(const Node& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:mozilla.devtools.protobuf.Node) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool Node::IsInitialized() const { + return true; +} + +void Node::InternalSwap(Node* other) { + using std::swap; + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + _impl_.edges_.InternalSwap(&other->_impl_.edges_); + ::PROTOBUF_NAMESPACE_ID::internal::memswap< + PROTOBUF_FIELD_OFFSET(Node, _impl_.coarsetype_) + + sizeof(Node::_impl_.coarsetype_) + - PROTOBUF_FIELD_OFFSET(Node, _impl_.allocationstack_)>( + reinterpret_cast<char*>(&_impl_.allocationstack_), + reinterpret_cast<char*>(&other->_impl_.allocationstack_)); + swap(_impl_.TypeNameOrRef_, other->_impl_.TypeNameOrRef_); + swap(_impl_.JSObjectClassNameOrRef_, other->_impl_.JSObjectClassNameOrRef_); + swap(_impl_.ScriptFilenameOrRef_, other->_impl_.ScriptFilenameOrRef_); + swap(_impl_.descriptiveTypeNameOrRef_, other->_impl_.descriptiveTypeNameOrRef_); + swap(_impl_._oneof_case_[0], other->_impl_._oneof_case_[0]); + swap(_impl_._oneof_case_[1], other->_impl_._oneof_case_[1]); + swap(_impl_._oneof_case_[2], other->_impl_._oneof_case_[2]); + swap(_impl_._oneof_case_[3], other->_impl_._oneof_case_[3]); +} + +std::string Node::GetTypeName() const { + return "mozilla.devtools.protobuf.Node"; +} + + +// =================================================================== + +class Edge::_Internal { + public: + using HasBits = decltype(std::declval<Edge>()._impl_._has_bits_); + static void set_has_referent(HasBits* has_bits) { + (*has_bits)[0] |= 1u; + } +}; + +Edge::Edge(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned) + : ::PROTOBUF_NAMESPACE_ID::MessageLite(arena, is_message_owned) { + SharedCtor(arena, is_message_owned); + // @@protoc_insertion_point(arena_constructor:mozilla.devtools.protobuf.Edge) +} +Edge::Edge(const Edge& from) + : ::PROTOBUF_NAMESPACE_ID::MessageLite() { + Edge* const _this = this; (void)_this; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){from._impl_._has_bits_} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.referent_){} + , decltype(_impl_.EdgeNameOrRef_){} + , /*decltype(_impl_._oneof_case_)*/{}}; + + _internal_metadata_.MergeFrom<std::string>(from._internal_metadata_); + _this->_impl_.referent_ = from._impl_.referent_; + clear_has_EdgeNameOrRef(); + switch (from.EdgeNameOrRef_case()) { + case kName: { + _this->_internal_set_name(from._internal_name()); + break; + } + case kNameRef: { + _this->_internal_set_nameref(from._internal_nameref()); + break; + } + case EDGENAMEORREF_NOT_SET: { + break; + } + } + // @@protoc_insertion_point(copy_constructor:mozilla.devtools.protobuf.Edge) +} + +inline void Edge::SharedCtor( + ::_pb::Arena* arena, bool is_message_owned) { + (void)arena; + (void)is_message_owned; + new (&_impl_) Impl_{ + decltype(_impl_._has_bits_){} + , /*decltype(_impl_._cached_size_)*/{} + , decltype(_impl_.referent_){uint64_t{0u}} + , decltype(_impl_.EdgeNameOrRef_){} + , /*decltype(_impl_._oneof_case_)*/{} + }; + clear_has_EdgeNameOrRef(); +} + +Edge::~Edge() { + // @@protoc_insertion_point(destructor:mozilla.devtools.protobuf.Edge) + if (auto *arena = _internal_metadata_.DeleteReturnArena<std::string>()) { + (void)arena; + return; + } + SharedDtor(); +} + +inline void Edge::SharedDtor() { + GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); + if (has_EdgeNameOrRef()) { + clear_EdgeNameOrRef(); + } +} + +void Edge::SetCachedSize(int size) const { + _impl_._cached_size_.Set(size); +} + +void Edge::clear_EdgeNameOrRef() { +// @@protoc_insertion_point(one_of_clear_start:mozilla.devtools.protobuf.Edge) + switch (EdgeNameOrRef_case()) { + case kName: { + _impl_.EdgeNameOrRef_.name_.Destroy(); + break; + } + case kNameRef: { + // No need to clear + break; + } + case EDGENAMEORREF_NOT_SET: { + break; + } + } + _impl_._oneof_case_[0] = EDGENAMEORREF_NOT_SET; +} + + +void Edge::Clear() { +// @@protoc_insertion_point(message_clear_start:mozilla.devtools.protobuf.Edge) + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + _impl_.referent_ = uint64_t{0u}; + clear_EdgeNameOrRef(); + _impl_._has_bits_.Clear(); + _internal_metadata_.Clear<std::string>(); +} + +const char* Edge::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) { +#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure + _Internal::HasBits has_bits{}; + while (!ctx->Done(&ptr)) { + uint32_t tag; + ptr = ::_pbi::ReadTag(ptr, &tag); + switch (tag >> 3) { + // optional uint64 referent = 1; + case 1: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 8)) { + _Internal::set_has_referent(&has_bits); + _impl_.referent_ = ::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // bytes name = 2; + case 2: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 18)) { + auto str = _internal_mutable_name(); + ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); + CHK_(ptr); + } else + goto handle_unusual; + continue; + // uint64 nameRef = 3; + case 3: + if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 24)) { + _internal_set_nameref(::PROTOBUF_NAMESPACE_ID::internal::ReadVarint64(&ptr)); + CHK_(ptr); + } else + goto handle_unusual; + continue; + default: + goto handle_unusual; + } // switch + handle_unusual: + if ((tag == 0) || ((tag & 7) == 4)) { + CHK_(ptr); + ctx->SetLastTag(tag); + goto message_done; + } + ptr = UnknownFieldParse( + tag, + _internal_metadata_.mutable_unknown_fields<std::string>(), + ptr, ctx); + CHK_(ptr != nullptr); + } // while +message_done: + _impl_._has_bits_.Or(has_bits); + return ptr; +failure: + ptr = nullptr; + goto message_done; +#undef CHK_ +} + +uint8_t* Edge::_InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const { + // @@protoc_insertion_point(serialize_to_array_start:mozilla.devtools.protobuf.Edge) + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + cached_has_bits = _impl_._has_bits_[0]; + // optional uint64 referent = 1; + if (cached_has_bits & 0x00000001u) { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt64ToArray(1, this->_internal_referent(), target); + } + + switch (EdgeNameOrRef_case()) { + case kName: { + target = stream->WriteBytesMaybeAliased( + 2, this->_internal_name(), target); + break; + } + case kNameRef: { + target = stream->EnsureSpace(target); + target = ::_pbi::WireFormatLite::WriteUInt64ToArray(3, this->_internal_nameref(), target); + break; + } + default: ; + } + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + target = stream->WriteRaw(_internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).data(), + static_cast<int>(_internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size()), target); + } + // @@protoc_insertion_point(serialize_to_array_end:mozilla.devtools.protobuf.Edge) + return target; +} + +size_t Edge::ByteSizeLong() const { +// @@protoc_insertion_point(message_byte_size_start:mozilla.devtools.protobuf.Edge) + size_t total_size = 0; + + uint32_t cached_has_bits = 0; + // Prevent compiler warnings about cached_has_bits being unused + (void) cached_has_bits; + + // optional uint64 referent = 1; + cached_has_bits = _impl_._has_bits_[0]; + if (cached_has_bits & 0x00000001u) { + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_referent()); + } + + switch (EdgeNameOrRef_case()) { + // bytes name = 2; + case kName: { + total_size += 1 + + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( + this->_internal_name()); + break; + } + // uint64 nameRef = 3; + case kNameRef: { + total_size += ::_pbi::WireFormatLite::UInt64SizePlusOne(this->_internal_nameref()); + break; + } + case EDGENAMEORREF_NOT_SET: { + break; + } + } + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { + total_size += _internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString).size(); + } + int cached_size = ::_pbi::ToCachedSize(total_size); + SetCachedSize(cached_size); + return total_size; +} + +void Edge::CheckTypeAndMergeFrom( + const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) { + MergeFrom(*::_pbi::DownCast<const Edge*>( + &from)); +} + +void Edge::MergeFrom(const Edge& from) { + Edge* const _this = this; + // @@protoc_insertion_point(class_specific_merge_from_start:mozilla.devtools.protobuf.Edge) + GOOGLE_DCHECK_NE(&from, _this); + uint32_t cached_has_bits = 0; + (void) cached_has_bits; + + if (from._internal_has_referent()) { + _this->_internal_set_referent(from._internal_referent()); + } + switch (from.EdgeNameOrRef_case()) { + case kName: { + _this->_internal_set_name(from._internal_name()); + break; + } + case kNameRef: { + _this->_internal_set_nameref(from._internal_nameref()); + break; + } + case EDGENAMEORREF_NOT_SET: { + break; + } + } + _this->_internal_metadata_.MergeFrom<std::string>(from._internal_metadata_); +} + +void Edge::CopyFrom(const Edge& from) { +// @@protoc_insertion_point(class_specific_copy_from_start:mozilla.devtools.protobuf.Edge) + if (&from == this) return; + Clear(); + MergeFrom(from); +} + +bool Edge::IsInitialized() const { + return true; +} + +void Edge::InternalSwap(Edge* other) { + using std::swap; + _internal_metadata_.InternalSwap(&other->_internal_metadata_); + swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); + swap(_impl_.referent_, other->_impl_.referent_); + swap(_impl_.EdgeNameOrRef_, other->_impl_.EdgeNameOrRef_); + swap(_impl_._oneof_case_[0], other->_impl_._oneof_case_[0]); +} + +std::string Edge::GetTypeName() const { + return "mozilla.devtools.protobuf.Edge"; +} + + +// @@protoc_insertion_point(namespace_scope) +} // namespace protobuf +} // namespace devtools +} // namespace mozilla +PROTOBUF_NAMESPACE_OPEN +template<> PROTOBUF_NOINLINE ::mozilla::devtools::protobuf::Metadata* +Arena::CreateMaybeMessage< ::mozilla::devtools::protobuf::Metadata >(Arena* arena) { + return Arena::CreateMessageInternal< ::mozilla::devtools::protobuf::Metadata >(arena); +} +template<> PROTOBUF_NOINLINE ::mozilla::devtools::protobuf::StackFrame_Data* +Arena::CreateMaybeMessage< ::mozilla::devtools::protobuf::StackFrame_Data >(Arena* arena) { + return Arena::CreateMessageInternal< ::mozilla::devtools::protobuf::StackFrame_Data >(arena); +} +template<> PROTOBUF_NOINLINE ::mozilla::devtools::protobuf::StackFrame* +Arena::CreateMaybeMessage< ::mozilla::devtools::protobuf::StackFrame >(Arena* arena) { + return Arena::CreateMessageInternal< ::mozilla::devtools::protobuf::StackFrame >(arena); +} +template<> PROTOBUF_NOINLINE ::mozilla::devtools::protobuf::Node* +Arena::CreateMaybeMessage< ::mozilla::devtools::protobuf::Node >(Arena* arena) { + return Arena::CreateMessageInternal< ::mozilla::devtools::protobuf::Node >(arena); +} +template<> PROTOBUF_NOINLINE ::mozilla::devtools::protobuf::Edge* +Arena::CreateMaybeMessage< ::mozilla::devtools::protobuf::Edge >(Arena* arena) { + return Arena::CreateMessageInternal< ::mozilla::devtools::protobuf::Edge >(arena); +} +PROTOBUF_NAMESPACE_CLOSE + +// @@protoc_insertion_point(global_scope) +#include <google/protobuf/port_undef.inc> diff --git a/devtools/shared/heapsnapshot/CoreDump.pb.h b/devtools/shared/heapsnapshot/CoreDump.pb.h new file mode 100644 index 0000000000..734c38c017 --- /dev/null +++ b/devtools/shared/heapsnapshot/CoreDump.pb.h @@ -0,0 +1,2883 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: CoreDump.proto + +#ifndef GOOGLE_PROTOBUF_INCLUDED_CoreDump_2eproto +#define GOOGLE_PROTOBUF_INCLUDED_CoreDump_2eproto + +#include <limits> +#include <string> + +#include <google/protobuf/port_def.inc> +#if PROTOBUF_VERSION < 3021000 +#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 3021006 < 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/port_undef.inc> +#include <google/protobuf/io/coded_stream.h> +#include <google/protobuf/arena.h> +#include <google/protobuf/arenastring.h> +#include <google/protobuf/generated_message_util.h> +#include <google/protobuf/metadata_lite.h> +#include <google/protobuf/message_lite.h> +#include <google/protobuf/repeated_field.h> // IWYU pragma: export +#include <google/protobuf/extension_set.h> // IWYU pragma: export +// @@protoc_insertion_point(includes) +#include <google/protobuf/port_def.inc> +#define PROTOBUF_INTERNAL_EXPORT_CoreDump_2eproto +PROTOBUF_NAMESPACE_OPEN +namespace internal { +class AnyMetadata; +} // namespace internal +PROTOBUF_NAMESPACE_CLOSE + +// Internal implementation detail -- do not use these members. +struct TableStruct_CoreDump_2eproto { + static const uint32_t offsets[]; +}; +namespace mozilla { +namespace devtools { +namespace protobuf { +class Edge; +struct EdgeDefaultTypeInternal; +extern EdgeDefaultTypeInternal _Edge_default_instance_; +class Metadata; +struct MetadataDefaultTypeInternal; +extern MetadataDefaultTypeInternal _Metadata_default_instance_; +class Node; +struct NodeDefaultTypeInternal; +extern NodeDefaultTypeInternal _Node_default_instance_; +class StackFrame; +struct StackFrameDefaultTypeInternal; +extern StackFrameDefaultTypeInternal _StackFrame_default_instance_; +class StackFrame_Data; +struct StackFrame_DataDefaultTypeInternal; +extern StackFrame_DataDefaultTypeInternal _StackFrame_Data_default_instance_; +} // namespace protobuf +} // namespace devtools +} // namespace mozilla +PROTOBUF_NAMESPACE_OPEN +template<> ::mozilla::devtools::protobuf::Edge* Arena::CreateMaybeMessage<::mozilla::devtools::protobuf::Edge>(Arena*); +template<> ::mozilla::devtools::protobuf::Metadata* Arena::CreateMaybeMessage<::mozilla::devtools::protobuf::Metadata>(Arena*); +template<> ::mozilla::devtools::protobuf::Node* Arena::CreateMaybeMessage<::mozilla::devtools::protobuf::Node>(Arena*); +template<> ::mozilla::devtools::protobuf::StackFrame* Arena::CreateMaybeMessage<::mozilla::devtools::protobuf::StackFrame>(Arena*); +template<> ::mozilla::devtools::protobuf::StackFrame_Data* Arena::CreateMaybeMessage<::mozilla::devtools::protobuf::StackFrame_Data>(Arena*); +PROTOBUF_NAMESPACE_CLOSE +namespace mozilla { +namespace devtools { +namespace protobuf { + +// =================================================================== + +class Metadata final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:mozilla.devtools.protobuf.Metadata) */ { + public: + inline Metadata() : Metadata(nullptr) {} + ~Metadata() override; + explicit PROTOBUF_CONSTEXPR Metadata(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + Metadata(const Metadata& from); + Metadata(Metadata&& from) noexcept + : Metadata() { + *this = ::std::move(from); + } + + inline Metadata& operator=(const Metadata& from) { + CopyFrom(from); + return *this; + } + inline Metadata& operator=(Metadata&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields<std::string>(); + } + + static const Metadata& default_instance() { + return *internal_default_instance(); + } + static inline const Metadata* internal_default_instance() { + return reinterpret_cast<const Metadata*>( + &_Metadata_default_instance_); + } + static constexpr int kIndexInFileMessages = + 0; + + friend void swap(Metadata& a, Metadata& b) { + a.Swap(&b); + } + inline void Swap(Metadata* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(Metadata* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + Metadata* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage<Metadata>(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const Metadata& from); + void MergeFrom(const Metadata& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(Metadata* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "mozilla.devtools.protobuf.Metadata"; + } + protected: + explicit Metadata(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + // accessors ------------------------------------------------------- + + enum : int { + kTimeStampFieldNumber = 1, + }; + // optional uint64 timeStamp = 1; + bool has_timestamp() const; + private: + bool _internal_has_timestamp() const; + public: + void clear_timestamp(); + uint64_t timestamp() const; + void set_timestamp(uint64_t value); + private: + uint64_t _internal_timestamp() const; + void _internal_set_timestamp(uint64_t value); + public: + + // @@protoc_insertion_point(class_scope:mozilla.devtools.protobuf.Metadata) + private: + class _Internal; + + template <typename T> friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + uint64_t timestamp_; + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_CoreDump_2eproto; +}; +// ------------------------------------------------------------------- + +class StackFrame_Data final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:mozilla.devtools.protobuf.StackFrame.Data) */ { + public: + inline StackFrame_Data() : StackFrame_Data(nullptr) {} + ~StackFrame_Data() override; + explicit PROTOBUF_CONSTEXPR StackFrame_Data(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + StackFrame_Data(const StackFrame_Data& from); + StackFrame_Data(StackFrame_Data&& from) noexcept + : StackFrame_Data() { + *this = ::std::move(from); + } + + inline StackFrame_Data& operator=(const StackFrame_Data& from) { + CopyFrom(from); + return *this; + } + inline StackFrame_Data& operator=(StackFrame_Data&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields<std::string>(); + } + + static const StackFrame_Data& default_instance() { + return *internal_default_instance(); + } + enum SourceOrRefCase { + kSource = 5, + kSourceRef = 6, + SOURCEORREF_NOT_SET = 0, + }; + + enum FunctionDisplayNameOrRefCase { + kFunctionDisplayName = 7, + kFunctionDisplayNameRef = 8, + FUNCTIONDISPLAYNAMEORREF_NOT_SET = 0, + }; + + static inline const StackFrame_Data* internal_default_instance() { + return reinterpret_cast<const StackFrame_Data*>( + &_StackFrame_Data_default_instance_); + } + static constexpr int kIndexInFileMessages = + 1; + + friend void swap(StackFrame_Data& a, StackFrame_Data& b) { + a.Swap(&b); + } + inline void Swap(StackFrame_Data* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(StackFrame_Data* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + StackFrame_Data* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage<StackFrame_Data>(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const StackFrame_Data& from); + void MergeFrom(const StackFrame_Data& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(StackFrame_Data* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "mozilla.devtools.protobuf.StackFrame.Data"; + } + protected: + explicit StackFrame_Data(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + // accessors ------------------------------------------------------- + + enum : int { + kParentFieldNumber = 2, + kIdFieldNumber = 1, + kLineFieldNumber = 3, + kColumnFieldNumber = 4, + kIsSystemFieldNumber = 9, + kIsSelfHostedFieldNumber = 10, + kSourceFieldNumber = 5, + kSourceRefFieldNumber = 6, + kFunctionDisplayNameFieldNumber = 7, + kFunctionDisplayNameRefFieldNumber = 8, + }; + // optional .mozilla.devtools.protobuf.StackFrame parent = 2; + bool has_parent() const; + private: + bool _internal_has_parent() const; + public: + void clear_parent(); + const ::mozilla::devtools::protobuf::StackFrame& parent() const; + PROTOBUF_NODISCARD ::mozilla::devtools::protobuf::StackFrame* release_parent(); + ::mozilla::devtools::protobuf::StackFrame* mutable_parent(); + void set_allocated_parent(::mozilla::devtools::protobuf::StackFrame* parent); + private: + const ::mozilla::devtools::protobuf::StackFrame& _internal_parent() const; + ::mozilla::devtools::protobuf::StackFrame* _internal_mutable_parent(); + public: + void unsafe_arena_set_allocated_parent( + ::mozilla::devtools::protobuf::StackFrame* parent); + ::mozilla::devtools::protobuf::StackFrame* unsafe_arena_release_parent(); + + // optional uint64 id = 1; + bool has_id() const; + private: + bool _internal_has_id() const; + public: + void clear_id(); + uint64_t id() const; + void set_id(uint64_t value); + private: + uint64_t _internal_id() const; + void _internal_set_id(uint64_t value); + public: + + // optional uint32 line = 3; + bool has_line() const; + private: + bool _internal_has_line() const; + public: + void clear_line(); + uint32_t line() const; + void set_line(uint32_t value); + private: + uint32_t _internal_line() const; + void _internal_set_line(uint32_t value); + public: + + // optional uint32 column = 4; + bool has_column() const; + private: + bool _internal_has_column() const; + public: + void clear_column(); + uint32_t column() const; + void set_column(uint32_t value); + private: + uint32_t _internal_column() const; + void _internal_set_column(uint32_t value); + public: + + // optional bool isSystem = 9; + bool has_issystem() const; + private: + bool _internal_has_issystem() const; + public: + void clear_issystem(); + bool issystem() const; + void set_issystem(bool value); + private: + bool _internal_issystem() const; + void _internal_set_issystem(bool value); + public: + + // optional bool isSelfHosted = 10; + bool has_isselfhosted() const; + private: + bool _internal_has_isselfhosted() const; + public: + void clear_isselfhosted(); + bool isselfhosted() const; + void set_isselfhosted(bool value); + private: + bool _internal_isselfhosted() const; + void _internal_set_isselfhosted(bool value); + public: + + // bytes source = 5; + bool has_source() const; + private: + bool _internal_has_source() const; + public: + void clear_source(); + const std::string& source() const; + template <typename ArgT0 = const std::string&, typename... ArgT> + void set_source(ArgT0&& arg0, ArgT... args); + std::string* mutable_source(); + PROTOBUF_NODISCARD std::string* release_source(); + void set_allocated_source(std::string* source); + private: + const std::string& _internal_source() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_source(const std::string& value); + std::string* _internal_mutable_source(); + public: + + // uint64 sourceRef = 6; + bool has_sourceref() const; + private: + bool _internal_has_sourceref() const; + public: + void clear_sourceref(); + uint64_t sourceref() const; + void set_sourceref(uint64_t value); + private: + uint64_t _internal_sourceref() const; + void _internal_set_sourceref(uint64_t value); + public: + + // bytes functionDisplayName = 7; + bool has_functiondisplayname() const; + private: + bool _internal_has_functiondisplayname() const; + public: + void clear_functiondisplayname(); + const std::string& functiondisplayname() const; + template <typename ArgT0 = const std::string&, typename... ArgT> + void set_functiondisplayname(ArgT0&& arg0, ArgT... args); + std::string* mutable_functiondisplayname(); + PROTOBUF_NODISCARD std::string* release_functiondisplayname(); + void set_allocated_functiondisplayname(std::string* functiondisplayname); + private: + const std::string& _internal_functiondisplayname() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_functiondisplayname(const std::string& value); + std::string* _internal_mutable_functiondisplayname(); + public: + + // uint64 functionDisplayNameRef = 8; + bool has_functiondisplaynameref() const; + private: + bool _internal_has_functiondisplaynameref() const; + public: + void clear_functiondisplaynameref(); + uint64_t functiondisplaynameref() const; + void set_functiondisplaynameref(uint64_t value); + private: + uint64_t _internal_functiondisplaynameref() const; + void _internal_set_functiondisplaynameref(uint64_t value); + public: + + void clear_SourceOrRef(); + SourceOrRefCase SourceOrRef_case() const; + void clear_FunctionDisplayNameOrRef(); + FunctionDisplayNameOrRefCase FunctionDisplayNameOrRef_case() const; + // @@protoc_insertion_point(class_scope:mozilla.devtools.protobuf.StackFrame.Data) + private: + class _Internal; + void set_has_source(); + void set_has_sourceref(); + void set_has_functiondisplayname(); + void set_has_functiondisplaynameref(); + + inline bool has_SourceOrRef() const; + inline void clear_has_SourceOrRef(); + + inline bool has_FunctionDisplayNameOrRef() const; + inline void clear_has_FunctionDisplayNameOrRef(); + + template <typename T> friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + ::mozilla::devtools::protobuf::StackFrame* parent_; + uint64_t id_; + uint32_t line_; + uint32_t column_; + bool issystem_; + bool isselfhosted_; + union SourceOrRefUnion { + constexpr SourceOrRefUnion() : _constinit_{} {} + ::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized _constinit_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr source_; + uint64_t sourceref_; + } SourceOrRef_; + union FunctionDisplayNameOrRefUnion { + constexpr FunctionDisplayNameOrRefUnion() : _constinit_{} {} + ::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized _constinit_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr functiondisplayname_; + uint64_t functiondisplaynameref_; + } FunctionDisplayNameOrRef_; + uint32_t _oneof_case_[2]; + + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_CoreDump_2eproto; +}; +// ------------------------------------------------------------------- + +class StackFrame final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:mozilla.devtools.protobuf.StackFrame) */ { + public: + inline StackFrame() : StackFrame(nullptr) {} + ~StackFrame() override; + explicit PROTOBUF_CONSTEXPR StackFrame(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + StackFrame(const StackFrame& from); + StackFrame(StackFrame&& from) noexcept + : StackFrame() { + *this = ::std::move(from); + } + + inline StackFrame& operator=(const StackFrame& from) { + CopyFrom(from); + return *this; + } + inline StackFrame& operator=(StackFrame&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields<std::string>(); + } + + static const StackFrame& default_instance() { + return *internal_default_instance(); + } + enum StackFrameTypeCase { + kData = 1, + kRef = 2, + STACKFRAMETYPE_NOT_SET = 0, + }; + + static inline const StackFrame* internal_default_instance() { + return reinterpret_cast<const StackFrame*>( + &_StackFrame_default_instance_); + } + static constexpr int kIndexInFileMessages = + 2; + + friend void swap(StackFrame& a, StackFrame& b) { + a.Swap(&b); + } + inline void Swap(StackFrame* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(StackFrame* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + StackFrame* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage<StackFrame>(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const StackFrame& from); + void MergeFrom(const StackFrame& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(StackFrame* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "mozilla.devtools.protobuf.StackFrame"; + } + protected: + explicit StackFrame(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + typedef StackFrame_Data Data; + + // accessors ------------------------------------------------------- + + enum : int { + kDataFieldNumber = 1, + kRefFieldNumber = 2, + }; + // .mozilla.devtools.protobuf.StackFrame.Data data = 1; + bool has_data() const; + private: + bool _internal_has_data() const; + public: + void clear_data(); + const ::mozilla::devtools::protobuf::StackFrame_Data& data() const; + PROTOBUF_NODISCARD ::mozilla::devtools::protobuf::StackFrame_Data* release_data(); + ::mozilla::devtools::protobuf::StackFrame_Data* mutable_data(); + void set_allocated_data(::mozilla::devtools::protobuf::StackFrame_Data* data); + private: + const ::mozilla::devtools::protobuf::StackFrame_Data& _internal_data() const; + ::mozilla::devtools::protobuf::StackFrame_Data* _internal_mutable_data(); + public: + void unsafe_arena_set_allocated_data( + ::mozilla::devtools::protobuf::StackFrame_Data* data); + ::mozilla::devtools::protobuf::StackFrame_Data* unsafe_arena_release_data(); + + // uint64 ref = 2; + bool has_ref() const; + private: + bool _internal_has_ref() const; + public: + void clear_ref(); + uint64_t ref() const; + void set_ref(uint64_t value); + private: + uint64_t _internal_ref() const; + void _internal_set_ref(uint64_t value); + public: + + void clear_StackFrameType(); + StackFrameTypeCase StackFrameType_case() const; + // @@protoc_insertion_point(class_scope:mozilla.devtools.protobuf.StackFrame) + private: + class _Internal; + void set_has_data(); + void set_has_ref(); + + inline bool has_StackFrameType() const; + inline void clear_has_StackFrameType(); + + template <typename T> friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + union StackFrameTypeUnion { + constexpr StackFrameTypeUnion() : _constinit_{} {} + ::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized _constinit_; + ::mozilla::devtools::protobuf::StackFrame_Data* data_; + uint64_t ref_; + } StackFrameType_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + uint32_t _oneof_case_[1]; + + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_CoreDump_2eproto; +}; +// ------------------------------------------------------------------- + +class Node final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:mozilla.devtools.protobuf.Node) */ { + public: + inline Node() : Node(nullptr) {} + ~Node() override; + explicit PROTOBUF_CONSTEXPR Node(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + Node(const Node& from); + Node(Node&& from) noexcept + : Node() { + *this = ::std::move(from); + } + + inline Node& operator=(const Node& from) { + CopyFrom(from); + return *this; + } + inline Node& operator=(Node&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields<std::string>(); + } + + static const Node& default_instance() { + return *internal_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, + }; + + enum DescriptiveTypeNameOrRefCase { + kDescriptiveTypeName = 12, + kDescriptiveTypeNameRef = 13, + DESCRIPTIVETYPENAMEORREF_NOT_SET = 0, + }; + + static inline const Node* internal_default_instance() { + return reinterpret_cast<const Node*>( + &_Node_default_instance_); + } + static constexpr int kIndexInFileMessages = + 3; + + friend void swap(Node& a, Node& b) { + a.Swap(&b); + } + inline void Swap(Node* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(Node* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + Node* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage<Node>(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const Node& from); + void MergeFrom(const Node& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(Node* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "mozilla.devtools.protobuf.Node"; + } + protected: + explicit Node(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + // accessors ------------------------------------------------------- + + enum : int { + kEdgesFieldNumber = 5, + kAllocationStackFieldNumber = 6, + kIdFieldNumber = 1, + kSizeFieldNumber = 4, + kCoarseTypeFieldNumber = 9, + kTypeNameFieldNumber = 2, + kTypeNameRefFieldNumber = 3, + kJsObjectClassNameFieldNumber = 7, + kJsObjectClassNameRefFieldNumber = 8, + kScriptFilenameFieldNumber = 10, + kScriptFilenameRefFieldNumber = 11, + kDescriptiveTypeNameFieldNumber = 12, + kDescriptiveTypeNameRefFieldNumber = 13, + }; + // repeated .mozilla.devtools.protobuf.Edge edges = 5; + int edges_size() const; + private: + int _internal_edges_size() const; + public: + void clear_edges(); + ::mozilla::devtools::protobuf::Edge* mutable_edges(int index); + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::mozilla::devtools::protobuf::Edge >* + mutable_edges(); + private: + const ::mozilla::devtools::protobuf::Edge& _internal_edges(int index) const; + ::mozilla::devtools::protobuf::Edge* _internal_add_edges(); + public: + const ::mozilla::devtools::protobuf::Edge& edges(int index) const; + ::mozilla::devtools::protobuf::Edge* add_edges(); + const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::mozilla::devtools::protobuf::Edge >& + edges() const; + + // optional .mozilla.devtools.protobuf.StackFrame allocationStack = 6; + bool has_allocationstack() const; + private: + bool _internal_has_allocationstack() const; + public: + void clear_allocationstack(); + const ::mozilla::devtools::protobuf::StackFrame& allocationstack() const; + PROTOBUF_NODISCARD ::mozilla::devtools::protobuf::StackFrame* release_allocationstack(); + ::mozilla::devtools::protobuf::StackFrame* mutable_allocationstack(); + void set_allocated_allocationstack(::mozilla::devtools::protobuf::StackFrame* allocationstack); + private: + const ::mozilla::devtools::protobuf::StackFrame& _internal_allocationstack() const; + ::mozilla::devtools::protobuf::StackFrame* _internal_mutable_allocationstack(); + public: + void unsafe_arena_set_allocated_allocationstack( + ::mozilla::devtools::protobuf::StackFrame* allocationstack); + ::mozilla::devtools::protobuf::StackFrame* unsafe_arena_release_allocationstack(); + + // optional uint64 id = 1; + bool has_id() const; + private: + bool _internal_has_id() const; + public: + void clear_id(); + uint64_t id() const; + void set_id(uint64_t value); + private: + uint64_t _internal_id() const; + void _internal_set_id(uint64_t value); + public: + + // optional uint64 size = 4; + bool has_size() const; + private: + bool _internal_has_size() const; + public: + void clear_size(); + uint64_t size() const; + void set_size(uint64_t value); + private: + uint64_t _internal_size() const; + void _internal_set_size(uint64_t value); + public: + + // optional uint32 coarseType = 9 [default = 0]; + bool has_coarsetype() const; + private: + bool _internal_has_coarsetype() const; + public: + void clear_coarsetype(); + uint32_t coarsetype() const; + void set_coarsetype(uint32_t value); + private: + uint32_t _internal_coarsetype() const; + void _internal_set_coarsetype(uint32_t value); + public: + + // bytes typeName = 2; + bool has_typename_() const; + private: + bool _internal_has_typename_() const; + public: + void clear_typename_(); + const std::string& typename_() const; + template <typename ArgT0 = const std::string&, typename... ArgT> + void set_typename_(ArgT0&& arg0, ArgT... args); + std::string* mutable_typename_(); + PROTOBUF_NODISCARD std::string* release_typename_(); + void set_allocated_typename_(std::string* typename_); + private: + const std::string& _internal_typename_() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_typename_(const std::string& value); + std::string* _internal_mutable_typename_(); + public: + + // uint64 typeNameRef = 3; + bool has_typenameref() const; + private: + bool _internal_has_typenameref() const; + public: + void clear_typenameref(); + uint64_t typenameref() const; + void set_typenameref(uint64_t value); + private: + uint64_t _internal_typenameref() const; + void _internal_set_typenameref(uint64_t value); + public: + + // bytes jsObjectClassName = 7; + bool has_jsobjectclassname() const; + private: + bool _internal_has_jsobjectclassname() const; + public: + void clear_jsobjectclassname(); + const std::string& jsobjectclassname() const; + template <typename ArgT0 = const std::string&, typename... ArgT> + void set_jsobjectclassname(ArgT0&& arg0, ArgT... args); + std::string* mutable_jsobjectclassname(); + PROTOBUF_NODISCARD std::string* release_jsobjectclassname(); + void set_allocated_jsobjectclassname(std::string* jsobjectclassname); + private: + const std::string& _internal_jsobjectclassname() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_jsobjectclassname(const std::string& value); + std::string* _internal_mutable_jsobjectclassname(); + public: + + // uint64 jsObjectClassNameRef = 8; + bool has_jsobjectclassnameref() const; + private: + bool _internal_has_jsobjectclassnameref() const; + public: + void clear_jsobjectclassnameref(); + uint64_t jsobjectclassnameref() const; + void set_jsobjectclassnameref(uint64_t value); + private: + uint64_t _internal_jsobjectclassnameref() const; + void _internal_set_jsobjectclassnameref(uint64_t value); + public: + + // bytes scriptFilename = 10; + bool has_scriptfilename() const; + private: + bool _internal_has_scriptfilename() const; + public: + void clear_scriptfilename(); + const std::string& scriptfilename() const; + template <typename ArgT0 = const std::string&, typename... ArgT> + void set_scriptfilename(ArgT0&& arg0, ArgT... args); + std::string* mutable_scriptfilename(); + PROTOBUF_NODISCARD std::string* release_scriptfilename(); + void set_allocated_scriptfilename(std::string* scriptfilename); + private: + const std::string& _internal_scriptfilename() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_scriptfilename(const std::string& value); + std::string* _internal_mutable_scriptfilename(); + public: + + // uint64 scriptFilenameRef = 11; + bool has_scriptfilenameref() const; + private: + bool _internal_has_scriptfilenameref() const; + public: + void clear_scriptfilenameref(); + uint64_t scriptfilenameref() const; + void set_scriptfilenameref(uint64_t value); + private: + uint64_t _internal_scriptfilenameref() const; + void _internal_set_scriptfilenameref(uint64_t value); + public: + + // bytes descriptiveTypeName = 12; + bool has_descriptivetypename() const; + private: + bool _internal_has_descriptivetypename() const; + public: + void clear_descriptivetypename(); + const std::string& descriptivetypename() const; + template <typename ArgT0 = const std::string&, typename... ArgT> + void set_descriptivetypename(ArgT0&& arg0, ArgT... args); + std::string* mutable_descriptivetypename(); + PROTOBUF_NODISCARD std::string* release_descriptivetypename(); + void set_allocated_descriptivetypename(std::string* descriptivetypename); + private: + const std::string& _internal_descriptivetypename() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_descriptivetypename(const std::string& value); + std::string* _internal_mutable_descriptivetypename(); + public: + + // uint64 descriptiveTypeNameRef = 13; + bool has_descriptivetypenameref() const; + private: + bool _internal_has_descriptivetypenameref() const; + public: + void clear_descriptivetypenameref(); + uint64_t descriptivetypenameref() const; + void set_descriptivetypenameref(uint64_t value); + private: + uint64_t _internal_descriptivetypenameref() const; + void _internal_set_descriptivetypenameref(uint64_t value); + public: + + void clear_TypeNameOrRef(); + TypeNameOrRefCase TypeNameOrRef_case() const; + void clear_JSObjectClassNameOrRef(); + JSObjectClassNameOrRefCase JSObjectClassNameOrRef_case() const; + void clear_ScriptFilenameOrRef(); + ScriptFilenameOrRefCase ScriptFilenameOrRef_case() const; + void clear_descriptiveTypeNameOrRef(); + DescriptiveTypeNameOrRefCase descriptiveTypeNameOrRef_case() const; + // @@protoc_insertion_point(class_scope:mozilla.devtools.protobuf.Node) + private: + class _Internal; + void set_has_typename_(); + void set_has_typenameref(); + void set_has_jsobjectclassname(); + void set_has_jsobjectclassnameref(); + void set_has_scriptfilename(); + void set_has_scriptfilenameref(); + void set_has_descriptivetypename(); + void set_has_descriptivetypenameref(); + + inline bool has_TypeNameOrRef() const; + inline void clear_has_TypeNameOrRef(); + + inline bool has_JSObjectClassNameOrRef() const; + inline void clear_has_JSObjectClassNameOrRef(); + + inline bool has_ScriptFilenameOrRef() const; + inline void clear_has_ScriptFilenameOrRef(); + + inline bool has_descriptiveTypeNameOrRef() const; + inline void clear_has_descriptiveTypeNameOrRef(); + + template <typename T> friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::mozilla::devtools::protobuf::Edge > edges_; + ::mozilla::devtools::protobuf::StackFrame* allocationstack_; + uint64_t id_; + uint64_t size_; + uint32_t coarsetype_; + union TypeNameOrRefUnion { + constexpr TypeNameOrRefUnion() : _constinit_{} {} + ::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized _constinit_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr typename__; + uint64_t typenameref_; + } TypeNameOrRef_; + union JSObjectClassNameOrRefUnion { + constexpr JSObjectClassNameOrRefUnion() : _constinit_{} {} + ::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized _constinit_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr jsobjectclassname_; + uint64_t jsobjectclassnameref_; + } JSObjectClassNameOrRef_; + union ScriptFilenameOrRefUnion { + constexpr ScriptFilenameOrRefUnion() : _constinit_{} {} + ::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized _constinit_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr scriptfilename_; + uint64_t scriptfilenameref_; + } ScriptFilenameOrRef_; + union DescriptiveTypeNameOrRefUnion { + constexpr DescriptiveTypeNameOrRefUnion() : _constinit_{} {} + ::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized _constinit_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr descriptivetypename_; + uint64_t descriptivetypenameref_; + } descriptiveTypeNameOrRef_; + uint32_t _oneof_case_[4]; + + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_CoreDump_2eproto; +}; +// ------------------------------------------------------------------- + +class Edge final : + public ::PROTOBUF_NAMESPACE_ID::MessageLite /* @@protoc_insertion_point(class_definition:mozilla.devtools.protobuf.Edge) */ { + public: + inline Edge() : Edge(nullptr) {} + ~Edge() override; + explicit PROTOBUF_CONSTEXPR Edge(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized); + + Edge(const Edge& from); + Edge(Edge&& from) noexcept + : Edge() { + *this = ::std::move(from); + } + + inline Edge& operator=(const Edge& from) { + CopyFrom(from); + return *this; + } + inline Edge& operator=(Edge&& from) noexcept { + if (this == &from) return *this; + if (GetOwningArena() == from.GetOwningArena() + #ifdef PROTOBUF_FORCE_COPY_IN_MOVE + && GetOwningArena() != nullptr + #endif // !PROTOBUF_FORCE_COPY_IN_MOVE + ) { + InternalSwap(&from); + } else { + CopyFrom(from); + } + return *this; + } + + inline const std::string& unknown_fields() const { + return _internal_metadata_.unknown_fields<std::string>(::PROTOBUF_NAMESPACE_ID::internal::GetEmptyString); + } + inline std::string* mutable_unknown_fields() { + return _internal_metadata_.mutable_unknown_fields<std::string>(); + } + + static const Edge& default_instance() { + return *internal_default_instance(); + } + enum EdgeNameOrRefCase { + kName = 2, + kNameRef = 3, + EDGENAMEORREF_NOT_SET = 0, + }; + + static inline const Edge* internal_default_instance() { + return reinterpret_cast<const Edge*>( + &_Edge_default_instance_); + } + static constexpr int kIndexInFileMessages = + 4; + + friend void swap(Edge& a, Edge& b) { + a.Swap(&b); + } + inline void Swap(Edge* other) { + if (other == this) return; + #ifdef PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() != nullptr && + GetOwningArena() == other->GetOwningArena()) { + #else // PROTOBUF_FORCE_COPY_IN_SWAP + if (GetOwningArena() == other->GetOwningArena()) { + #endif // !PROTOBUF_FORCE_COPY_IN_SWAP + InternalSwap(other); + } else { + ::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other); + } + } + void UnsafeArenaSwap(Edge* other) { + if (other == this) return; + GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena()); + InternalSwap(other); + } + + // implements Message ---------------------------------------------- + + Edge* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final { + return CreateMaybeMessage<Edge>(arena); + } + void CheckTypeAndMergeFrom(const ::PROTOBUF_NAMESPACE_ID::MessageLite& from) final; + void CopyFrom(const Edge& from); + void MergeFrom(const Edge& from); + PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final; + bool IsInitialized() const final; + + size_t ByteSizeLong() const final; + const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final; + uint8_t* _InternalSerialize( + uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final; + int GetCachedSize() const final { return _impl_._cached_size_.Get(); } + + private: + void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned); + void SharedDtor(); + void SetCachedSize(int size) const; + void InternalSwap(Edge* other); + + private: + friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata; + static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { + return "mozilla.devtools.protobuf.Edge"; + } + protected: + explicit Edge(::PROTOBUF_NAMESPACE_ID::Arena* arena, + bool is_message_owned = false); + public: + + std::string GetTypeName() const final; + + // nested types ---------------------------------------------------- + + // accessors ------------------------------------------------------- + + enum : int { + kReferentFieldNumber = 1, + kNameFieldNumber = 2, + kNameRefFieldNumber = 3, + }; + // optional uint64 referent = 1; + bool has_referent() const; + private: + bool _internal_has_referent() const; + public: + void clear_referent(); + uint64_t referent() const; + void set_referent(uint64_t value); + private: + uint64_t _internal_referent() const; + void _internal_set_referent(uint64_t value); + public: + + // bytes name = 2; + bool has_name() const; + private: + bool _internal_has_name() const; + public: + void clear_name(); + const std::string& name() const; + template <typename ArgT0 = const std::string&, typename... ArgT> + void set_name(ArgT0&& arg0, ArgT... args); + std::string* mutable_name(); + PROTOBUF_NODISCARD std::string* release_name(); + void set_allocated_name(std::string* name); + private: + const std::string& _internal_name() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_name(const std::string& value); + std::string* _internal_mutable_name(); + public: + + // uint64 nameRef = 3; + bool has_nameref() const; + private: + bool _internal_has_nameref() const; + public: + void clear_nameref(); + uint64_t nameref() const; + void set_nameref(uint64_t value); + private: + uint64_t _internal_nameref() const; + void _internal_set_nameref(uint64_t value); + public: + + void clear_EdgeNameOrRef(); + EdgeNameOrRefCase EdgeNameOrRef_case() const; + // @@protoc_insertion_point(class_scope:mozilla.devtools.protobuf.Edge) + private: + class _Internal; + void set_has_name(); + void set_has_nameref(); + + inline bool has_EdgeNameOrRef() const; + inline void clear_has_EdgeNameOrRef(); + + template <typename T> friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper; + typedef void InternalArenaConstructable_; + typedef void DestructorSkippable_; + struct Impl_ { + ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; + mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; + uint64_t referent_; + union EdgeNameOrRefUnion { + constexpr EdgeNameOrRefUnion() : _constinit_{} {} + ::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized _constinit_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr name_; + uint64_t nameref_; + } EdgeNameOrRef_; + uint32_t _oneof_case_[1]; + + }; + union { Impl_ _impl_; }; + friend struct ::TableStruct_CoreDump_2eproto; +}; +// =================================================================== + + +// =================================================================== + +#ifdef __GNUC__ + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wstrict-aliasing" +#endif // __GNUC__ +// Metadata + +// optional uint64 timeStamp = 1; +inline bool Metadata::_internal_has_timestamp() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + return value; +} +inline bool Metadata::has_timestamp() const { + return _internal_has_timestamp(); +} +inline void Metadata::clear_timestamp() { + _impl_.timestamp_ = uint64_t{0u}; + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline uint64_t Metadata::_internal_timestamp() const { + return _impl_.timestamp_; +} +inline uint64_t Metadata::timestamp() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Metadata.timeStamp) + return _internal_timestamp(); +} +inline void Metadata::_internal_set_timestamp(uint64_t value) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.timestamp_ = value; +} +inline void Metadata::set_timestamp(uint64_t value) { + _internal_set_timestamp(value); + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Metadata.timeStamp) +} + +// ------------------------------------------------------------------- + +// StackFrame_Data + +// optional uint64 id = 1; +inline bool StackFrame_Data::_internal_has_id() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + return value; +} +inline bool StackFrame_Data::has_id() const { + return _internal_has_id(); +} +inline void StackFrame_Data::clear_id() { + _impl_.id_ = uint64_t{0u}; + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline uint64_t StackFrame_Data::_internal_id() const { + return _impl_.id_; +} +inline uint64_t StackFrame_Data::id() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.id) + return _internal_id(); +} +inline void StackFrame_Data::_internal_set_id(uint64_t value) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.id_ = value; +} +inline void StackFrame_Data::set_id(uint64_t value) { + _internal_set_id(value); + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.Data.id) +} + +// optional .mozilla.devtools.protobuf.StackFrame parent = 2; +inline bool StackFrame_Data::_internal_has_parent() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + PROTOBUF_ASSUME(!value || _impl_.parent_ != nullptr); + return value; +} +inline bool StackFrame_Data::has_parent() const { + return _internal_has_parent(); +} +inline void StackFrame_Data::clear_parent() { + if (_impl_.parent_ != nullptr) _impl_.parent_->Clear(); + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline const ::mozilla::devtools::protobuf::StackFrame& StackFrame_Data::_internal_parent() const { + const ::mozilla::devtools::protobuf::StackFrame* p = _impl_.parent_; + return p != nullptr ? *p : reinterpret_cast<const ::mozilla::devtools::protobuf::StackFrame&>( + ::mozilla::devtools::protobuf::_StackFrame_default_instance_); +} +inline const ::mozilla::devtools::protobuf::StackFrame& StackFrame_Data::parent() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.parent) + return _internal_parent(); +} +inline void StackFrame_Data::unsafe_arena_set_allocated_parent( + ::mozilla::devtools::protobuf::StackFrame* parent) { + if (GetArenaForAllocation() == nullptr) { + delete reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.parent_); + } + _impl_.parent_ = parent; + if (parent) { + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + // @@protoc_insertion_point(field_unsafe_arena_set_allocated:mozilla.devtools.protobuf.StackFrame.Data.parent) +} +inline ::mozilla::devtools::protobuf::StackFrame* StackFrame_Data::release_parent() { + _impl_._has_bits_[0] &= ~0x00000001u; + ::mozilla::devtools::protobuf::StackFrame* temp = _impl_.parent_; + _impl_.parent_ = nullptr; +#ifdef PROTOBUF_FORCE_COPY_IN_RELEASE + auto* old = reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(temp); + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + if (GetArenaForAllocation() == nullptr) { delete old; } +#else // PROTOBUF_FORCE_COPY_IN_RELEASE + if (GetArenaForAllocation() != nullptr) { + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + } +#endif // !PROTOBUF_FORCE_COPY_IN_RELEASE + return temp; +} +inline ::mozilla::devtools::protobuf::StackFrame* StackFrame_Data::unsafe_arena_release_parent() { + // @@protoc_insertion_point(field_release:mozilla.devtools.protobuf.StackFrame.Data.parent) + _impl_._has_bits_[0] &= ~0x00000001u; + ::mozilla::devtools::protobuf::StackFrame* temp = _impl_.parent_; + _impl_.parent_ = nullptr; + return temp; +} +inline ::mozilla::devtools::protobuf::StackFrame* StackFrame_Data::_internal_mutable_parent() { + _impl_._has_bits_[0] |= 0x00000001u; + if (_impl_.parent_ == nullptr) { + auto* p = CreateMaybeMessage<::mozilla::devtools::protobuf::StackFrame>(GetArenaForAllocation()); + _impl_.parent_ = p; + } + return _impl_.parent_; +} +inline ::mozilla::devtools::protobuf::StackFrame* StackFrame_Data::mutable_parent() { + ::mozilla::devtools::protobuf::StackFrame* _msg = _internal_mutable_parent(); + // @@protoc_insertion_point(field_mutable:mozilla.devtools.protobuf.StackFrame.Data.parent) + return _msg; +} +inline void StackFrame_Data::set_allocated_parent(::mozilla::devtools::protobuf::StackFrame* parent) { + ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation(); + if (message_arena == nullptr) { + delete _impl_.parent_; + } + if (parent) { + ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena = + ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(parent); + if (message_arena != submessage_arena) { + parent = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage( + message_arena, parent, submessage_arena); + } + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + _impl_.parent_ = parent; + // @@protoc_insertion_point(field_set_allocated:mozilla.devtools.protobuf.StackFrame.Data.parent) +} + +// optional uint32 line = 3; +inline bool StackFrame_Data::_internal_has_line() const { + bool value = (_impl_._has_bits_[0] & 0x00000004u) != 0; + return value; +} +inline bool StackFrame_Data::has_line() const { + return _internal_has_line(); +} +inline void StackFrame_Data::clear_line() { + _impl_.line_ = 0u; + _impl_._has_bits_[0] &= ~0x00000004u; +} +inline uint32_t StackFrame_Data::_internal_line() const { + return _impl_.line_; +} +inline uint32_t StackFrame_Data::line() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.line) + return _internal_line(); +} +inline void StackFrame_Data::_internal_set_line(uint32_t value) { + _impl_._has_bits_[0] |= 0x00000004u; + _impl_.line_ = value; +} +inline void StackFrame_Data::set_line(uint32_t value) { + _internal_set_line(value); + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.Data.line) +} + +// optional uint32 column = 4; +inline bool StackFrame_Data::_internal_has_column() const { + bool value = (_impl_._has_bits_[0] & 0x00000008u) != 0; + return value; +} +inline bool StackFrame_Data::has_column() const { + return _internal_has_column(); +} +inline void StackFrame_Data::clear_column() { + _impl_.column_ = 0u; + _impl_._has_bits_[0] &= ~0x00000008u; +} +inline uint32_t StackFrame_Data::_internal_column() const { + return _impl_.column_; +} +inline uint32_t StackFrame_Data::column() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.column) + return _internal_column(); +} +inline void StackFrame_Data::_internal_set_column(uint32_t value) { + _impl_._has_bits_[0] |= 0x00000008u; + _impl_.column_ = value; +} +inline void StackFrame_Data::set_column(uint32_t value) { + _internal_set_column(value); + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.Data.column) +} + +// bytes source = 5; +inline bool StackFrame_Data::_internal_has_source() const { + return SourceOrRef_case() == kSource; +} +inline bool StackFrame_Data::has_source() const { + return _internal_has_source(); +} +inline void StackFrame_Data::set_has_source() { + _impl_._oneof_case_[0] = kSource; +} +inline void StackFrame_Data::clear_source() { + if (_internal_has_source()) { + _impl_.SourceOrRef_.source_.Destroy(); + clear_has_SourceOrRef(); + } +} +inline const std::string& StackFrame_Data::source() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.source) + return _internal_source(); +} +template <typename ArgT0, typename... ArgT> +inline void StackFrame_Data::set_source(ArgT0&& arg0, ArgT... args) { + if (!_internal_has_source()) { + clear_SourceOrRef(); + set_has_source(); + _impl_.SourceOrRef_.source_.InitDefault(); + } + _impl_.SourceOrRef_.source_.SetBytes( static_cast<ArgT0 &&>(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.Data.source) +} +inline std::string* StackFrame_Data::mutable_source() { + std::string* _s = _internal_mutable_source(); + // @@protoc_insertion_point(field_mutable:mozilla.devtools.protobuf.StackFrame.Data.source) + return _s; +} +inline const std::string& StackFrame_Data::_internal_source() const { + if (_internal_has_source()) { + return _impl_.SourceOrRef_.source_.Get(); + } + return ::PROTOBUF_NAMESPACE_ID::internal::GetEmptyStringAlreadyInited(); +} +inline void StackFrame_Data::_internal_set_source(const std::string& value) { + if (!_internal_has_source()) { + clear_SourceOrRef(); + set_has_source(); + _impl_.SourceOrRef_.source_.InitDefault(); + } + _impl_.SourceOrRef_.source_.Set(value, GetArenaForAllocation()); +} +inline std::string* StackFrame_Data::_internal_mutable_source() { + if (!_internal_has_source()) { + clear_SourceOrRef(); + set_has_source(); + _impl_.SourceOrRef_.source_.InitDefault(); + } + return _impl_.SourceOrRef_.source_.Mutable( GetArenaForAllocation()); +} +inline std::string* StackFrame_Data::release_source() { + // @@protoc_insertion_point(field_release:mozilla.devtools.protobuf.StackFrame.Data.source) + if (_internal_has_source()) { + clear_has_SourceOrRef(); + return _impl_.SourceOrRef_.source_.Release(); + } else { + return nullptr; + } +} +inline void StackFrame_Data::set_allocated_source(std::string* source) { + if (has_SourceOrRef()) { + clear_SourceOrRef(); + } + if (source != nullptr) { + set_has_source(); + _impl_.SourceOrRef_.source_.InitAllocated(source, GetArenaForAllocation()); + } + // @@protoc_insertion_point(field_set_allocated:mozilla.devtools.protobuf.StackFrame.Data.source) +} + +// uint64 sourceRef = 6; +inline bool StackFrame_Data::_internal_has_sourceref() const { + return SourceOrRef_case() == kSourceRef; +} +inline bool StackFrame_Data::has_sourceref() const { + return _internal_has_sourceref(); +} +inline void StackFrame_Data::set_has_sourceref() { + _impl_._oneof_case_[0] = kSourceRef; +} +inline void StackFrame_Data::clear_sourceref() { + if (_internal_has_sourceref()) { + _impl_.SourceOrRef_.sourceref_ = uint64_t{0u}; + clear_has_SourceOrRef(); + } +} +inline uint64_t StackFrame_Data::_internal_sourceref() const { + if (_internal_has_sourceref()) { + return _impl_.SourceOrRef_.sourceref_; + } + return uint64_t{0u}; +} +inline void StackFrame_Data::_internal_set_sourceref(uint64_t value) { + if (!_internal_has_sourceref()) { + clear_SourceOrRef(); + set_has_sourceref(); + } + _impl_.SourceOrRef_.sourceref_ = value; +} +inline uint64_t StackFrame_Data::sourceref() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.sourceRef) + return _internal_sourceref(); +} +inline void StackFrame_Data::set_sourceref(uint64_t value) { + _internal_set_sourceref(value); + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.Data.sourceRef) +} + +// bytes functionDisplayName = 7; +inline bool StackFrame_Data::_internal_has_functiondisplayname() const { + return FunctionDisplayNameOrRef_case() == kFunctionDisplayName; +} +inline bool StackFrame_Data::has_functiondisplayname() const { + return _internal_has_functiondisplayname(); +} +inline void StackFrame_Data::set_has_functiondisplayname() { + _impl_._oneof_case_[1] = kFunctionDisplayName; +} +inline void StackFrame_Data::clear_functiondisplayname() { + if (_internal_has_functiondisplayname()) { + _impl_.FunctionDisplayNameOrRef_.functiondisplayname_.Destroy(); + clear_has_FunctionDisplayNameOrRef(); + } +} +inline const std::string& StackFrame_Data::functiondisplayname() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.functionDisplayName) + return _internal_functiondisplayname(); +} +template <typename ArgT0, typename... ArgT> +inline void StackFrame_Data::set_functiondisplayname(ArgT0&& arg0, ArgT... args) { + if (!_internal_has_functiondisplayname()) { + clear_FunctionDisplayNameOrRef(); + set_has_functiondisplayname(); + _impl_.FunctionDisplayNameOrRef_.functiondisplayname_.InitDefault(); + } + _impl_.FunctionDisplayNameOrRef_.functiondisplayname_.SetBytes( static_cast<ArgT0 &&>(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.Data.functionDisplayName) +} +inline std::string* StackFrame_Data::mutable_functiondisplayname() { + std::string* _s = _internal_mutable_functiondisplayname(); + // @@protoc_insertion_point(field_mutable:mozilla.devtools.protobuf.StackFrame.Data.functionDisplayName) + return _s; +} +inline const std::string& StackFrame_Data::_internal_functiondisplayname() const { + if (_internal_has_functiondisplayname()) { + return _impl_.FunctionDisplayNameOrRef_.functiondisplayname_.Get(); + } + return ::PROTOBUF_NAMESPACE_ID::internal::GetEmptyStringAlreadyInited(); +} +inline void StackFrame_Data::_internal_set_functiondisplayname(const std::string& value) { + if (!_internal_has_functiondisplayname()) { + clear_FunctionDisplayNameOrRef(); + set_has_functiondisplayname(); + _impl_.FunctionDisplayNameOrRef_.functiondisplayname_.InitDefault(); + } + _impl_.FunctionDisplayNameOrRef_.functiondisplayname_.Set(value, GetArenaForAllocation()); +} +inline std::string* StackFrame_Data::_internal_mutable_functiondisplayname() { + if (!_internal_has_functiondisplayname()) { + clear_FunctionDisplayNameOrRef(); + set_has_functiondisplayname(); + _impl_.FunctionDisplayNameOrRef_.functiondisplayname_.InitDefault(); + } + return _impl_.FunctionDisplayNameOrRef_.functiondisplayname_.Mutable( GetArenaForAllocation()); +} +inline std::string* StackFrame_Data::release_functiondisplayname() { + // @@protoc_insertion_point(field_release:mozilla.devtools.protobuf.StackFrame.Data.functionDisplayName) + if (_internal_has_functiondisplayname()) { + clear_has_FunctionDisplayNameOrRef(); + return _impl_.FunctionDisplayNameOrRef_.functiondisplayname_.Release(); + } else { + return nullptr; + } +} +inline void StackFrame_Data::set_allocated_functiondisplayname(std::string* functiondisplayname) { + if (has_FunctionDisplayNameOrRef()) { + clear_FunctionDisplayNameOrRef(); + } + if (functiondisplayname != nullptr) { + set_has_functiondisplayname(); + _impl_.FunctionDisplayNameOrRef_.functiondisplayname_.InitAllocated(functiondisplayname, GetArenaForAllocation()); + } + // @@protoc_insertion_point(field_set_allocated:mozilla.devtools.protobuf.StackFrame.Data.functionDisplayName) +} + +// uint64 functionDisplayNameRef = 8; +inline bool StackFrame_Data::_internal_has_functiondisplaynameref() const { + return FunctionDisplayNameOrRef_case() == kFunctionDisplayNameRef; +} +inline bool StackFrame_Data::has_functiondisplaynameref() const { + return _internal_has_functiondisplaynameref(); +} +inline void StackFrame_Data::set_has_functiondisplaynameref() { + _impl_._oneof_case_[1] = kFunctionDisplayNameRef; +} +inline void StackFrame_Data::clear_functiondisplaynameref() { + if (_internal_has_functiondisplaynameref()) { + _impl_.FunctionDisplayNameOrRef_.functiondisplaynameref_ = uint64_t{0u}; + clear_has_FunctionDisplayNameOrRef(); + } +} +inline uint64_t StackFrame_Data::_internal_functiondisplaynameref() const { + if (_internal_has_functiondisplaynameref()) { + return _impl_.FunctionDisplayNameOrRef_.functiondisplaynameref_; + } + return uint64_t{0u}; +} +inline void StackFrame_Data::_internal_set_functiondisplaynameref(uint64_t value) { + if (!_internal_has_functiondisplaynameref()) { + clear_FunctionDisplayNameOrRef(); + set_has_functiondisplaynameref(); + } + _impl_.FunctionDisplayNameOrRef_.functiondisplaynameref_ = value; +} +inline uint64_t StackFrame_Data::functiondisplaynameref() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.functionDisplayNameRef) + return _internal_functiondisplaynameref(); +} +inline void StackFrame_Data::set_functiondisplaynameref(uint64_t value) { + _internal_set_functiondisplaynameref(value); + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.Data.functionDisplayNameRef) +} + +// optional bool isSystem = 9; +inline bool StackFrame_Data::_internal_has_issystem() const { + bool value = (_impl_._has_bits_[0] & 0x00000010u) != 0; + return value; +} +inline bool StackFrame_Data::has_issystem() const { + return _internal_has_issystem(); +} +inline void StackFrame_Data::clear_issystem() { + _impl_.issystem_ = false; + _impl_._has_bits_[0] &= ~0x00000010u; +} +inline bool StackFrame_Data::_internal_issystem() const { + return _impl_.issystem_; +} +inline bool StackFrame_Data::issystem() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.isSystem) + return _internal_issystem(); +} +inline void StackFrame_Data::_internal_set_issystem(bool value) { + _impl_._has_bits_[0] |= 0x00000010u; + _impl_.issystem_ = value; +} +inline void StackFrame_Data::set_issystem(bool value) { + _internal_set_issystem(value); + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.Data.isSystem) +} + +// optional bool isSelfHosted = 10; +inline bool StackFrame_Data::_internal_has_isselfhosted() const { + bool value = (_impl_._has_bits_[0] & 0x00000020u) != 0; + return value; +} +inline bool StackFrame_Data::has_isselfhosted() const { + return _internal_has_isselfhosted(); +} +inline void StackFrame_Data::clear_isselfhosted() { + _impl_.isselfhosted_ = false; + _impl_._has_bits_[0] &= ~0x00000020u; +} +inline bool StackFrame_Data::_internal_isselfhosted() const { + return _impl_.isselfhosted_; +} +inline bool StackFrame_Data::isselfhosted() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.isSelfHosted) + return _internal_isselfhosted(); +} +inline void StackFrame_Data::_internal_set_isselfhosted(bool value) { + _impl_._has_bits_[0] |= 0x00000020u; + _impl_.isselfhosted_ = value; +} +inline void StackFrame_Data::set_isselfhosted(bool value) { + _internal_set_isselfhosted(value); + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.Data.isSelfHosted) +} + +inline bool StackFrame_Data::has_SourceOrRef() const { + return SourceOrRef_case() != SOURCEORREF_NOT_SET; +} +inline void StackFrame_Data::clear_has_SourceOrRef() { + _impl_._oneof_case_[0] = SOURCEORREF_NOT_SET; +} +inline bool StackFrame_Data::has_FunctionDisplayNameOrRef() const { + return FunctionDisplayNameOrRef_case() != FUNCTIONDISPLAYNAMEORREF_NOT_SET; +} +inline void StackFrame_Data::clear_has_FunctionDisplayNameOrRef() { + _impl_._oneof_case_[1] = FUNCTIONDISPLAYNAMEORREF_NOT_SET; +} +inline StackFrame_Data::SourceOrRefCase StackFrame_Data::SourceOrRef_case() const { + return StackFrame_Data::SourceOrRefCase(_impl_._oneof_case_[0]); +} +inline StackFrame_Data::FunctionDisplayNameOrRefCase StackFrame_Data::FunctionDisplayNameOrRef_case() const { + return StackFrame_Data::FunctionDisplayNameOrRefCase(_impl_._oneof_case_[1]); +} +// ------------------------------------------------------------------- + +// StackFrame + +// .mozilla.devtools.protobuf.StackFrame.Data data = 1; +inline bool StackFrame::_internal_has_data() const { + return StackFrameType_case() == kData; +} +inline bool StackFrame::has_data() const { + return _internal_has_data(); +} +inline void StackFrame::set_has_data() { + _impl_._oneof_case_[0] = kData; +} +inline void StackFrame::clear_data() { + if (_internal_has_data()) { + if (GetArenaForAllocation() == nullptr) { + delete _impl_.StackFrameType_.data_; + } + clear_has_StackFrameType(); + } +} +inline ::mozilla::devtools::protobuf::StackFrame_Data* StackFrame::release_data() { + // @@protoc_insertion_point(field_release:mozilla.devtools.protobuf.StackFrame.data) + if (_internal_has_data()) { + clear_has_StackFrameType(); + ::mozilla::devtools::protobuf::StackFrame_Data* temp = _impl_.StackFrameType_.data_; + if (GetArenaForAllocation() != nullptr) { + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + } + _impl_.StackFrameType_.data_ = nullptr; + return temp; + } else { + return nullptr; + } +} +inline const ::mozilla::devtools::protobuf::StackFrame_Data& StackFrame::_internal_data() const { + return _internal_has_data() + ? *_impl_.StackFrameType_.data_ + : reinterpret_cast< ::mozilla::devtools::protobuf::StackFrame_Data&>(::mozilla::devtools::protobuf::_StackFrame_Data_default_instance_); +} +inline const ::mozilla::devtools::protobuf::StackFrame_Data& StackFrame::data() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.data) + return _internal_data(); +} +inline ::mozilla::devtools::protobuf::StackFrame_Data* StackFrame::unsafe_arena_release_data() { + // @@protoc_insertion_point(field_unsafe_arena_release:mozilla.devtools.protobuf.StackFrame.data) + if (_internal_has_data()) { + clear_has_StackFrameType(); + ::mozilla::devtools::protobuf::StackFrame_Data* temp = _impl_.StackFrameType_.data_; + _impl_.StackFrameType_.data_ = nullptr; + return temp; + } else { + return nullptr; + } +} +inline void StackFrame::unsafe_arena_set_allocated_data(::mozilla::devtools::protobuf::StackFrame_Data* data) { + clear_StackFrameType(); + if (data) { + set_has_data(); + _impl_.StackFrameType_.data_ = data; + } + // @@protoc_insertion_point(field_unsafe_arena_set_allocated:mozilla.devtools.protobuf.StackFrame.data) +} +inline ::mozilla::devtools::protobuf::StackFrame_Data* StackFrame::_internal_mutable_data() { + if (!_internal_has_data()) { + clear_StackFrameType(); + set_has_data(); + _impl_.StackFrameType_.data_ = CreateMaybeMessage< ::mozilla::devtools::protobuf::StackFrame_Data >(GetArenaForAllocation()); + } + return _impl_.StackFrameType_.data_; +} +inline ::mozilla::devtools::protobuf::StackFrame_Data* StackFrame::mutable_data() { + ::mozilla::devtools::protobuf::StackFrame_Data* _msg = _internal_mutable_data(); + // @@protoc_insertion_point(field_mutable:mozilla.devtools.protobuf.StackFrame.data) + return _msg; +} + +// uint64 ref = 2; +inline bool StackFrame::_internal_has_ref() const { + return StackFrameType_case() == kRef; +} +inline bool StackFrame::has_ref() const { + return _internal_has_ref(); +} +inline void StackFrame::set_has_ref() { + _impl_._oneof_case_[0] = kRef; +} +inline void StackFrame::clear_ref() { + if (_internal_has_ref()) { + _impl_.StackFrameType_.ref_ = uint64_t{0u}; + clear_has_StackFrameType(); + } +} +inline uint64_t StackFrame::_internal_ref() const { + if (_internal_has_ref()) { + return _impl_.StackFrameType_.ref_; + } + return uint64_t{0u}; +} +inline void StackFrame::_internal_set_ref(uint64_t value) { + if (!_internal_has_ref()) { + clear_StackFrameType(); + set_has_ref(); + } + _impl_.StackFrameType_.ref_ = value; +} +inline uint64_t StackFrame::ref() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.ref) + return _internal_ref(); +} +inline void StackFrame::set_ref(uint64_t value) { + _internal_set_ref(value); + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.ref) +} + +inline bool StackFrame::has_StackFrameType() const { + return StackFrameType_case() != STACKFRAMETYPE_NOT_SET; +} +inline void StackFrame::clear_has_StackFrameType() { + _impl_._oneof_case_[0] = STACKFRAMETYPE_NOT_SET; +} +inline StackFrame::StackFrameTypeCase StackFrame::StackFrameType_case() const { + return StackFrame::StackFrameTypeCase(_impl_._oneof_case_[0]); +} +// ------------------------------------------------------------------- + +// Node + +// optional uint64 id = 1; +inline bool Node::_internal_has_id() const { + bool value = (_impl_._has_bits_[0] & 0x00000002u) != 0; + return value; +} +inline bool Node::has_id() const { + return _internal_has_id(); +} +inline void Node::clear_id() { + _impl_.id_ = uint64_t{0u}; + _impl_._has_bits_[0] &= ~0x00000002u; +} +inline uint64_t Node::_internal_id() const { + return _impl_.id_; +} +inline uint64_t Node::id() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.id) + return _internal_id(); +} +inline void Node::_internal_set_id(uint64_t value) { + _impl_._has_bits_[0] |= 0x00000002u; + _impl_.id_ = value; +} +inline void Node::set_id(uint64_t value) { + _internal_set_id(value); + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Node.id) +} + +// bytes typeName = 2; +inline bool Node::_internal_has_typename_() const { + return TypeNameOrRef_case() == kTypeName; +} +inline bool Node::has_typename_() const { + return _internal_has_typename_(); +} +inline void Node::set_has_typename_() { + _impl_._oneof_case_[0] = kTypeName; +} +inline void Node::clear_typename_() { + if (_internal_has_typename_()) { + _impl_.TypeNameOrRef_.typename__.Destroy(); + clear_has_TypeNameOrRef(); + } +} +inline const std::string& Node::typename_() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.typeName) + return _internal_typename_(); +} +template <typename ArgT0, typename... ArgT> +inline void Node::set_typename_(ArgT0&& arg0, ArgT... args) { + if (!_internal_has_typename_()) { + clear_TypeNameOrRef(); + set_has_typename_(); + _impl_.TypeNameOrRef_.typename__.InitDefault(); + } + _impl_.TypeNameOrRef_.typename__.SetBytes( static_cast<ArgT0 &&>(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Node.typeName) +} +inline std::string* Node::mutable_typename_() { + std::string* _s = _internal_mutable_typename_(); + // @@protoc_insertion_point(field_mutable:mozilla.devtools.protobuf.Node.typeName) + return _s; +} +inline const std::string& Node::_internal_typename_() const { + if (_internal_has_typename_()) { + return _impl_.TypeNameOrRef_.typename__.Get(); + } + return ::PROTOBUF_NAMESPACE_ID::internal::GetEmptyStringAlreadyInited(); +} +inline void Node::_internal_set_typename_(const std::string& value) { + if (!_internal_has_typename_()) { + clear_TypeNameOrRef(); + set_has_typename_(); + _impl_.TypeNameOrRef_.typename__.InitDefault(); + } + _impl_.TypeNameOrRef_.typename__.Set(value, GetArenaForAllocation()); +} +inline std::string* Node::_internal_mutable_typename_() { + if (!_internal_has_typename_()) { + clear_TypeNameOrRef(); + set_has_typename_(); + _impl_.TypeNameOrRef_.typename__.InitDefault(); + } + return _impl_.TypeNameOrRef_.typename__.Mutable( GetArenaForAllocation()); +} +inline std::string* Node::release_typename_() { + // @@protoc_insertion_point(field_release:mozilla.devtools.protobuf.Node.typeName) + if (_internal_has_typename_()) { + clear_has_TypeNameOrRef(); + return _impl_.TypeNameOrRef_.typename__.Release(); + } else { + return nullptr; + } +} +inline void Node::set_allocated_typename_(std::string* typename_) { + if (has_TypeNameOrRef()) { + clear_TypeNameOrRef(); + } + if (typename_ != nullptr) { + set_has_typename_(); + _impl_.TypeNameOrRef_.typename__.InitAllocated(typename_, GetArenaForAllocation()); + } + // @@protoc_insertion_point(field_set_allocated:mozilla.devtools.protobuf.Node.typeName) +} + +// uint64 typeNameRef = 3; +inline bool Node::_internal_has_typenameref() const { + return TypeNameOrRef_case() == kTypeNameRef; +} +inline bool Node::has_typenameref() const { + return _internal_has_typenameref(); +} +inline void Node::set_has_typenameref() { + _impl_._oneof_case_[0] = kTypeNameRef; +} +inline void Node::clear_typenameref() { + if (_internal_has_typenameref()) { + _impl_.TypeNameOrRef_.typenameref_ = uint64_t{0u}; + clear_has_TypeNameOrRef(); + } +} +inline uint64_t Node::_internal_typenameref() const { + if (_internal_has_typenameref()) { + return _impl_.TypeNameOrRef_.typenameref_; + } + return uint64_t{0u}; +} +inline void Node::_internal_set_typenameref(uint64_t value) { + if (!_internal_has_typenameref()) { + clear_TypeNameOrRef(); + set_has_typenameref(); + } + _impl_.TypeNameOrRef_.typenameref_ = value; +} +inline uint64_t Node::typenameref() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.typeNameRef) + return _internal_typenameref(); +} +inline void Node::set_typenameref(uint64_t value) { + _internal_set_typenameref(value); + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Node.typeNameRef) +} + +// optional uint64 size = 4; +inline bool Node::_internal_has_size() const { + bool value = (_impl_._has_bits_[0] & 0x00000004u) != 0; + return value; +} +inline bool Node::has_size() const { + return _internal_has_size(); +} +inline void Node::clear_size() { + _impl_.size_ = uint64_t{0u}; + _impl_._has_bits_[0] &= ~0x00000004u; +} +inline uint64_t Node::_internal_size() const { + return _impl_.size_; +} +inline uint64_t Node::size() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.size) + return _internal_size(); +} +inline void Node::_internal_set_size(uint64_t value) { + _impl_._has_bits_[0] |= 0x00000004u; + _impl_.size_ = value; +} +inline void Node::set_size(uint64_t value) { + _internal_set_size(value); + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Node.size) +} + +// repeated .mozilla.devtools.protobuf.Edge edges = 5; +inline int Node::_internal_edges_size() const { + return _impl_.edges_.size(); +} +inline int Node::edges_size() const { + return _internal_edges_size(); +} +inline void Node::clear_edges() { + _impl_.edges_.Clear(); +} +inline ::mozilla::devtools::protobuf::Edge* Node::mutable_edges(int index) { + // @@protoc_insertion_point(field_mutable:mozilla.devtools.protobuf.Node.edges) + return _impl_.edges_.Mutable(index); +} +inline ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::mozilla::devtools::protobuf::Edge >* +Node::mutable_edges() { + // @@protoc_insertion_point(field_mutable_list:mozilla.devtools.protobuf.Node.edges) + return &_impl_.edges_; +} +inline const ::mozilla::devtools::protobuf::Edge& Node::_internal_edges(int index) const { + return _impl_.edges_.Get(index); +} +inline const ::mozilla::devtools::protobuf::Edge& Node::edges(int index) const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.edges) + return _internal_edges(index); +} +inline ::mozilla::devtools::protobuf::Edge* Node::_internal_add_edges() { + return _impl_.edges_.Add(); +} +inline ::mozilla::devtools::protobuf::Edge* Node::add_edges() { + ::mozilla::devtools::protobuf::Edge* _add = _internal_add_edges(); + // @@protoc_insertion_point(field_add:mozilla.devtools.protobuf.Node.edges) + return _add; +} +inline const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::mozilla::devtools::protobuf::Edge >& +Node::edges() const { + // @@protoc_insertion_point(field_list:mozilla.devtools.protobuf.Node.edges) + return _impl_.edges_; +} + +// optional .mozilla.devtools.protobuf.StackFrame allocationStack = 6; +inline bool Node::_internal_has_allocationstack() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + PROTOBUF_ASSUME(!value || _impl_.allocationstack_ != nullptr); + return value; +} +inline bool Node::has_allocationstack() const { + return _internal_has_allocationstack(); +} +inline void Node::clear_allocationstack() { + if (_impl_.allocationstack_ != nullptr) _impl_.allocationstack_->Clear(); + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline const ::mozilla::devtools::protobuf::StackFrame& Node::_internal_allocationstack() const { + const ::mozilla::devtools::protobuf::StackFrame* p = _impl_.allocationstack_; + return p != nullptr ? *p : reinterpret_cast<const ::mozilla::devtools::protobuf::StackFrame&>( + ::mozilla::devtools::protobuf::_StackFrame_default_instance_); +} +inline const ::mozilla::devtools::protobuf::StackFrame& Node::allocationstack() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.allocationStack) + return _internal_allocationstack(); +} +inline void Node::unsafe_arena_set_allocated_allocationstack( + ::mozilla::devtools::protobuf::StackFrame* allocationstack) { + if (GetArenaForAllocation() == nullptr) { + delete reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(_impl_.allocationstack_); + } + _impl_.allocationstack_ = allocationstack; + if (allocationstack) { + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + // @@protoc_insertion_point(field_unsafe_arena_set_allocated:mozilla.devtools.protobuf.Node.allocationStack) +} +inline ::mozilla::devtools::protobuf::StackFrame* Node::release_allocationstack() { + _impl_._has_bits_[0] &= ~0x00000001u; + ::mozilla::devtools::protobuf::StackFrame* temp = _impl_.allocationstack_; + _impl_.allocationstack_ = nullptr; +#ifdef PROTOBUF_FORCE_COPY_IN_RELEASE + auto* old = reinterpret_cast<::PROTOBUF_NAMESPACE_ID::MessageLite*>(temp); + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + if (GetArenaForAllocation() == nullptr) { delete old; } +#else // PROTOBUF_FORCE_COPY_IN_RELEASE + if (GetArenaForAllocation() != nullptr) { + temp = ::PROTOBUF_NAMESPACE_ID::internal::DuplicateIfNonNull(temp); + } +#endif // !PROTOBUF_FORCE_COPY_IN_RELEASE + return temp; +} +inline ::mozilla::devtools::protobuf::StackFrame* Node::unsafe_arena_release_allocationstack() { + // @@protoc_insertion_point(field_release:mozilla.devtools.protobuf.Node.allocationStack) + _impl_._has_bits_[0] &= ~0x00000001u; + ::mozilla::devtools::protobuf::StackFrame* temp = _impl_.allocationstack_; + _impl_.allocationstack_ = nullptr; + return temp; +} +inline ::mozilla::devtools::protobuf::StackFrame* Node::_internal_mutable_allocationstack() { + _impl_._has_bits_[0] |= 0x00000001u; + if (_impl_.allocationstack_ == nullptr) { + auto* p = CreateMaybeMessage<::mozilla::devtools::protobuf::StackFrame>(GetArenaForAllocation()); + _impl_.allocationstack_ = p; + } + return _impl_.allocationstack_; +} +inline ::mozilla::devtools::protobuf::StackFrame* Node::mutable_allocationstack() { + ::mozilla::devtools::protobuf::StackFrame* _msg = _internal_mutable_allocationstack(); + // @@protoc_insertion_point(field_mutable:mozilla.devtools.protobuf.Node.allocationStack) + return _msg; +} +inline void Node::set_allocated_allocationstack(::mozilla::devtools::protobuf::StackFrame* allocationstack) { + ::PROTOBUF_NAMESPACE_ID::Arena* message_arena = GetArenaForAllocation(); + if (message_arena == nullptr) { + delete _impl_.allocationstack_; + } + if (allocationstack) { + ::PROTOBUF_NAMESPACE_ID::Arena* submessage_arena = + ::PROTOBUF_NAMESPACE_ID::Arena::InternalGetOwningArena(allocationstack); + if (message_arena != submessage_arena) { + allocationstack = ::PROTOBUF_NAMESPACE_ID::internal::GetOwnedMessage( + message_arena, allocationstack, submessage_arena); + } + _impl_._has_bits_[0] |= 0x00000001u; + } else { + _impl_._has_bits_[0] &= ~0x00000001u; + } + _impl_.allocationstack_ = allocationstack; + // @@protoc_insertion_point(field_set_allocated:mozilla.devtools.protobuf.Node.allocationStack) +} + +// bytes jsObjectClassName = 7; +inline bool Node::_internal_has_jsobjectclassname() const { + return JSObjectClassNameOrRef_case() == kJsObjectClassName; +} +inline bool Node::has_jsobjectclassname() const { + return _internal_has_jsobjectclassname(); +} +inline void Node::set_has_jsobjectclassname() { + _impl_._oneof_case_[1] = kJsObjectClassName; +} +inline void Node::clear_jsobjectclassname() { + if (_internal_has_jsobjectclassname()) { + _impl_.JSObjectClassNameOrRef_.jsobjectclassname_.Destroy(); + clear_has_JSObjectClassNameOrRef(); + } +} +inline const std::string& Node::jsobjectclassname() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.jsObjectClassName) + return _internal_jsobjectclassname(); +} +template <typename ArgT0, typename... ArgT> +inline void Node::set_jsobjectclassname(ArgT0&& arg0, ArgT... args) { + if (!_internal_has_jsobjectclassname()) { + clear_JSObjectClassNameOrRef(); + set_has_jsobjectclassname(); + _impl_.JSObjectClassNameOrRef_.jsobjectclassname_.InitDefault(); + } + _impl_.JSObjectClassNameOrRef_.jsobjectclassname_.SetBytes( static_cast<ArgT0 &&>(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Node.jsObjectClassName) +} +inline std::string* Node::mutable_jsobjectclassname() { + std::string* _s = _internal_mutable_jsobjectclassname(); + // @@protoc_insertion_point(field_mutable:mozilla.devtools.protobuf.Node.jsObjectClassName) + return _s; +} +inline const std::string& Node::_internal_jsobjectclassname() const { + if (_internal_has_jsobjectclassname()) { + return _impl_.JSObjectClassNameOrRef_.jsobjectclassname_.Get(); + } + return ::PROTOBUF_NAMESPACE_ID::internal::GetEmptyStringAlreadyInited(); +} +inline void Node::_internal_set_jsobjectclassname(const std::string& value) { + if (!_internal_has_jsobjectclassname()) { + clear_JSObjectClassNameOrRef(); + set_has_jsobjectclassname(); + _impl_.JSObjectClassNameOrRef_.jsobjectclassname_.InitDefault(); + } + _impl_.JSObjectClassNameOrRef_.jsobjectclassname_.Set(value, GetArenaForAllocation()); +} +inline std::string* Node::_internal_mutable_jsobjectclassname() { + if (!_internal_has_jsobjectclassname()) { + clear_JSObjectClassNameOrRef(); + set_has_jsobjectclassname(); + _impl_.JSObjectClassNameOrRef_.jsobjectclassname_.InitDefault(); + } + return _impl_.JSObjectClassNameOrRef_.jsobjectclassname_.Mutable( GetArenaForAllocation()); +} +inline std::string* Node::release_jsobjectclassname() { + // @@protoc_insertion_point(field_release:mozilla.devtools.protobuf.Node.jsObjectClassName) + if (_internal_has_jsobjectclassname()) { + clear_has_JSObjectClassNameOrRef(); + return _impl_.JSObjectClassNameOrRef_.jsobjectclassname_.Release(); + } else { + return nullptr; + } +} +inline void Node::set_allocated_jsobjectclassname(std::string* jsobjectclassname) { + if (has_JSObjectClassNameOrRef()) { + clear_JSObjectClassNameOrRef(); + } + if (jsobjectclassname != nullptr) { + set_has_jsobjectclassname(); + _impl_.JSObjectClassNameOrRef_.jsobjectclassname_.InitAllocated(jsobjectclassname, GetArenaForAllocation()); + } + // @@protoc_insertion_point(field_set_allocated:mozilla.devtools.protobuf.Node.jsObjectClassName) +} + +// uint64 jsObjectClassNameRef = 8; +inline bool Node::_internal_has_jsobjectclassnameref() const { + return JSObjectClassNameOrRef_case() == kJsObjectClassNameRef; +} +inline bool Node::has_jsobjectclassnameref() const { + return _internal_has_jsobjectclassnameref(); +} +inline void Node::set_has_jsobjectclassnameref() { + _impl_._oneof_case_[1] = kJsObjectClassNameRef; +} +inline void Node::clear_jsobjectclassnameref() { + if (_internal_has_jsobjectclassnameref()) { + _impl_.JSObjectClassNameOrRef_.jsobjectclassnameref_ = uint64_t{0u}; + clear_has_JSObjectClassNameOrRef(); + } +} +inline uint64_t Node::_internal_jsobjectclassnameref() const { + if (_internal_has_jsobjectclassnameref()) { + return _impl_.JSObjectClassNameOrRef_.jsobjectclassnameref_; + } + return uint64_t{0u}; +} +inline void Node::_internal_set_jsobjectclassnameref(uint64_t value) { + if (!_internal_has_jsobjectclassnameref()) { + clear_JSObjectClassNameOrRef(); + set_has_jsobjectclassnameref(); + } + _impl_.JSObjectClassNameOrRef_.jsobjectclassnameref_ = value; +} +inline uint64_t Node::jsobjectclassnameref() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.jsObjectClassNameRef) + return _internal_jsobjectclassnameref(); +} +inline void Node::set_jsobjectclassnameref(uint64_t value) { + _internal_set_jsobjectclassnameref(value); + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Node.jsObjectClassNameRef) +} + +// optional uint32 coarseType = 9 [default = 0]; +inline bool Node::_internal_has_coarsetype() const { + bool value = (_impl_._has_bits_[0] & 0x00000008u) != 0; + return value; +} +inline bool Node::has_coarsetype() const { + return _internal_has_coarsetype(); +} +inline void Node::clear_coarsetype() { + _impl_.coarsetype_ = 0u; + _impl_._has_bits_[0] &= ~0x00000008u; +} +inline uint32_t Node::_internal_coarsetype() const { + return _impl_.coarsetype_; +} +inline uint32_t Node::coarsetype() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.coarseType) + return _internal_coarsetype(); +} +inline void Node::_internal_set_coarsetype(uint32_t value) { + _impl_._has_bits_[0] |= 0x00000008u; + _impl_.coarsetype_ = value; +} +inline void Node::set_coarsetype(uint32_t value) { + _internal_set_coarsetype(value); + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Node.coarseType) +} + +// bytes scriptFilename = 10; +inline bool Node::_internal_has_scriptfilename() const { + return ScriptFilenameOrRef_case() == kScriptFilename; +} +inline bool Node::has_scriptfilename() const { + return _internal_has_scriptfilename(); +} +inline void Node::set_has_scriptfilename() { + _impl_._oneof_case_[2] = kScriptFilename; +} +inline void Node::clear_scriptfilename() { + if (_internal_has_scriptfilename()) { + _impl_.ScriptFilenameOrRef_.scriptfilename_.Destroy(); + clear_has_ScriptFilenameOrRef(); + } +} +inline const std::string& Node::scriptfilename() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.scriptFilename) + return _internal_scriptfilename(); +} +template <typename ArgT0, typename... ArgT> +inline void Node::set_scriptfilename(ArgT0&& arg0, ArgT... args) { + if (!_internal_has_scriptfilename()) { + clear_ScriptFilenameOrRef(); + set_has_scriptfilename(); + _impl_.ScriptFilenameOrRef_.scriptfilename_.InitDefault(); + } + _impl_.ScriptFilenameOrRef_.scriptfilename_.SetBytes( static_cast<ArgT0 &&>(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Node.scriptFilename) +} +inline std::string* Node::mutable_scriptfilename() { + std::string* _s = _internal_mutable_scriptfilename(); + // @@protoc_insertion_point(field_mutable:mozilla.devtools.protobuf.Node.scriptFilename) + return _s; +} +inline const std::string& Node::_internal_scriptfilename() const { + if (_internal_has_scriptfilename()) { + return _impl_.ScriptFilenameOrRef_.scriptfilename_.Get(); + } + return ::PROTOBUF_NAMESPACE_ID::internal::GetEmptyStringAlreadyInited(); +} +inline void Node::_internal_set_scriptfilename(const std::string& value) { + if (!_internal_has_scriptfilename()) { + clear_ScriptFilenameOrRef(); + set_has_scriptfilename(); + _impl_.ScriptFilenameOrRef_.scriptfilename_.InitDefault(); + } + _impl_.ScriptFilenameOrRef_.scriptfilename_.Set(value, GetArenaForAllocation()); +} +inline std::string* Node::_internal_mutable_scriptfilename() { + if (!_internal_has_scriptfilename()) { + clear_ScriptFilenameOrRef(); + set_has_scriptfilename(); + _impl_.ScriptFilenameOrRef_.scriptfilename_.InitDefault(); + } + return _impl_.ScriptFilenameOrRef_.scriptfilename_.Mutable( GetArenaForAllocation()); +} +inline std::string* Node::release_scriptfilename() { + // @@protoc_insertion_point(field_release:mozilla.devtools.protobuf.Node.scriptFilename) + if (_internal_has_scriptfilename()) { + clear_has_ScriptFilenameOrRef(); + return _impl_.ScriptFilenameOrRef_.scriptfilename_.Release(); + } else { + return nullptr; + } +} +inline void Node::set_allocated_scriptfilename(std::string* scriptfilename) { + if (has_ScriptFilenameOrRef()) { + clear_ScriptFilenameOrRef(); + } + if (scriptfilename != nullptr) { + set_has_scriptfilename(); + _impl_.ScriptFilenameOrRef_.scriptfilename_.InitAllocated(scriptfilename, GetArenaForAllocation()); + } + // @@protoc_insertion_point(field_set_allocated:mozilla.devtools.protobuf.Node.scriptFilename) +} + +// uint64 scriptFilenameRef = 11; +inline bool Node::_internal_has_scriptfilenameref() const { + return ScriptFilenameOrRef_case() == kScriptFilenameRef; +} +inline bool Node::has_scriptfilenameref() const { + return _internal_has_scriptfilenameref(); +} +inline void Node::set_has_scriptfilenameref() { + _impl_._oneof_case_[2] = kScriptFilenameRef; +} +inline void Node::clear_scriptfilenameref() { + if (_internal_has_scriptfilenameref()) { + _impl_.ScriptFilenameOrRef_.scriptfilenameref_ = uint64_t{0u}; + clear_has_ScriptFilenameOrRef(); + } +} +inline uint64_t Node::_internal_scriptfilenameref() const { + if (_internal_has_scriptfilenameref()) { + return _impl_.ScriptFilenameOrRef_.scriptfilenameref_; + } + return uint64_t{0u}; +} +inline void Node::_internal_set_scriptfilenameref(uint64_t value) { + if (!_internal_has_scriptfilenameref()) { + clear_ScriptFilenameOrRef(); + set_has_scriptfilenameref(); + } + _impl_.ScriptFilenameOrRef_.scriptfilenameref_ = value; +} +inline uint64_t Node::scriptfilenameref() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.scriptFilenameRef) + return _internal_scriptfilenameref(); +} +inline void Node::set_scriptfilenameref(uint64_t value) { + _internal_set_scriptfilenameref(value); + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Node.scriptFilenameRef) +} + +// bytes descriptiveTypeName = 12; +inline bool Node::_internal_has_descriptivetypename() const { + return descriptiveTypeNameOrRef_case() == kDescriptiveTypeName; +} +inline bool Node::has_descriptivetypename() const { + return _internal_has_descriptivetypename(); +} +inline void Node::set_has_descriptivetypename() { + _impl_._oneof_case_[3] = kDescriptiveTypeName; +} +inline void Node::clear_descriptivetypename() { + if (_internal_has_descriptivetypename()) { + _impl_.descriptiveTypeNameOrRef_.descriptivetypename_.Destroy(); + clear_has_descriptiveTypeNameOrRef(); + } +} +inline const std::string& Node::descriptivetypename() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.descriptiveTypeName) + return _internal_descriptivetypename(); +} +template <typename ArgT0, typename... ArgT> +inline void Node::set_descriptivetypename(ArgT0&& arg0, ArgT... args) { + if (!_internal_has_descriptivetypename()) { + clear_descriptiveTypeNameOrRef(); + set_has_descriptivetypename(); + _impl_.descriptiveTypeNameOrRef_.descriptivetypename_.InitDefault(); + } + _impl_.descriptiveTypeNameOrRef_.descriptivetypename_.SetBytes( static_cast<ArgT0 &&>(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Node.descriptiveTypeName) +} +inline std::string* Node::mutable_descriptivetypename() { + std::string* _s = _internal_mutable_descriptivetypename(); + // @@protoc_insertion_point(field_mutable:mozilla.devtools.protobuf.Node.descriptiveTypeName) + return _s; +} +inline const std::string& Node::_internal_descriptivetypename() const { + if (_internal_has_descriptivetypename()) { + return _impl_.descriptiveTypeNameOrRef_.descriptivetypename_.Get(); + } + return ::PROTOBUF_NAMESPACE_ID::internal::GetEmptyStringAlreadyInited(); +} +inline void Node::_internal_set_descriptivetypename(const std::string& value) { + if (!_internal_has_descriptivetypename()) { + clear_descriptiveTypeNameOrRef(); + set_has_descriptivetypename(); + _impl_.descriptiveTypeNameOrRef_.descriptivetypename_.InitDefault(); + } + _impl_.descriptiveTypeNameOrRef_.descriptivetypename_.Set(value, GetArenaForAllocation()); +} +inline std::string* Node::_internal_mutable_descriptivetypename() { + if (!_internal_has_descriptivetypename()) { + clear_descriptiveTypeNameOrRef(); + set_has_descriptivetypename(); + _impl_.descriptiveTypeNameOrRef_.descriptivetypename_.InitDefault(); + } + return _impl_.descriptiveTypeNameOrRef_.descriptivetypename_.Mutable( GetArenaForAllocation()); +} +inline std::string* Node::release_descriptivetypename() { + // @@protoc_insertion_point(field_release:mozilla.devtools.protobuf.Node.descriptiveTypeName) + if (_internal_has_descriptivetypename()) { + clear_has_descriptiveTypeNameOrRef(); + return _impl_.descriptiveTypeNameOrRef_.descriptivetypename_.Release(); + } else { + return nullptr; + } +} +inline void Node::set_allocated_descriptivetypename(std::string* descriptivetypename) { + if (has_descriptiveTypeNameOrRef()) { + clear_descriptiveTypeNameOrRef(); + } + if (descriptivetypename != nullptr) { + set_has_descriptivetypename(); + _impl_.descriptiveTypeNameOrRef_.descriptivetypename_.InitAllocated(descriptivetypename, GetArenaForAllocation()); + } + // @@protoc_insertion_point(field_set_allocated:mozilla.devtools.protobuf.Node.descriptiveTypeName) +} + +// uint64 descriptiveTypeNameRef = 13; +inline bool Node::_internal_has_descriptivetypenameref() const { + return descriptiveTypeNameOrRef_case() == kDescriptiveTypeNameRef; +} +inline bool Node::has_descriptivetypenameref() const { + return _internal_has_descriptivetypenameref(); +} +inline void Node::set_has_descriptivetypenameref() { + _impl_._oneof_case_[3] = kDescriptiveTypeNameRef; +} +inline void Node::clear_descriptivetypenameref() { + if (_internal_has_descriptivetypenameref()) { + _impl_.descriptiveTypeNameOrRef_.descriptivetypenameref_ = uint64_t{0u}; + clear_has_descriptiveTypeNameOrRef(); + } +} +inline uint64_t Node::_internal_descriptivetypenameref() const { + if (_internal_has_descriptivetypenameref()) { + return _impl_.descriptiveTypeNameOrRef_.descriptivetypenameref_; + } + return uint64_t{0u}; +} +inline void Node::_internal_set_descriptivetypenameref(uint64_t value) { + if (!_internal_has_descriptivetypenameref()) { + clear_descriptiveTypeNameOrRef(); + set_has_descriptivetypenameref(); + } + _impl_.descriptiveTypeNameOrRef_.descriptivetypenameref_ = value; +} +inline uint64_t Node::descriptivetypenameref() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.descriptiveTypeNameRef) + return _internal_descriptivetypenameref(); +} +inline void Node::set_descriptivetypenameref(uint64_t value) { + _internal_set_descriptivetypenameref(value); + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Node.descriptiveTypeNameRef) +} + +inline bool Node::has_TypeNameOrRef() const { + return TypeNameOrRef_case() != TYPENAMEORREF_NOT_SET; +} +inline void Node::clear_has_TypeNameOrRef() { + _impl_._oneof_case_[0] = TYPENAMEORREF_NOT_SET; +} +inline bool Node::has_JSObjectClassNameOrRef() const { + return JSObjectClassNameOrRef_case() != JSOBJECTCLASSNAMEORREF_NOT_SET; +} +inline void Node::clear_has_JSObjectClassNameOrRef() { + _impl_._oneof_case_[1] = JSOBJECTCLASSNAMEORREF_NOT_SET; +} +inline bool Node::has_ScriptFilenameOrRef() const { + return ScriptFilenameOrRef_case() != SCRIPTFILENAMEORREF_NOT_SET; +} +inline void Node::clear_has_ScriptFilenameOrRef() { + _impl_._oneof_case_[2] = SCRIPTFILENAMEORREF_NOT_SET; +} +inline bool Node::has_descriptiveTypeNameOrRef() const { + return descriptiveTypeNameOrRef_case() != DESCRIPTIVETYPENAMEORREF_NOT_SET; +} +inline void Node::clear_has_descriptiveTypeNameOrRef() { + _impl_._oneof_case_[3] = DESCRIPTIVETYPENAMEORREF_NOT_SET; +} +inline Node::TypeNameOrRefCase Node::TypeNameOrRef_case() const { + return Node::TypeNameOrRefCase(_impl_._oneof_case_[0]); +} +inline Node::JSObjectClassNameOrRefCase Node::JSObjectClassNameOrRef_case() const { + return Node::JSObjectClassNameOrRefCase(_impl_._oneof_case_[1]); +} +inline Node::ScriptFilenameOrRefCase Node::ScriptFilenameOrRef_case() const { + return Node::ScriptFilenameOrRefCase(_impl_._oneof_case_[2]); +} +inline Node::DescriptiveTypeNameOrRefCase Node::descriptiveTypeNameOrRef_case() const { + return Node::DescriptiveTypeNameOrRefCase(_impl_._oneof_case_[3]); +} +// ------------------------------------------------------------------- + +// Edge + +// optional uint64 referent = 1; +inline bool Edge::_internal_has_referent() const { + bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; + return value; +} +inline bool Edge::has_referent() const { + return _internal_has_referent(); +} +inline void Edge::clear_referent() { + _impl_.referent_ = uint64_t{0u}; + _impl_._has_bits_[0] &= ~0x00000001u; +} +inline uint64_t Edge::_internal_referent() const { + return _impl_.referent_; +} +inline uint64_t Edge::referent() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Edge.referent) + return _internal_referent(); +} +inline void Edge::_internal_set_referent(uint64_t value) { + _impl_._has_bits_[0] |= 0x00000001u; + _impl_.referent_ = value; +} +inline void Edge::set_referent(uint64_t value) { + _internal_set_referent(value); + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Edge.referent) +} + +// bytes name = 2; +inline bool Edge::_internal_has_name() const { + return EdgeNameOrRef_case() == kName; +} +inline bool Edge::has_name() const { + return _internal_has_name(); +} +inline void Edge::set_has_name() { + _impl_._oneof_case_[0] = kName; +} +inline void Edge::clear_name() { + if (_internal_has_name()) { + _impl_.EdgeNameOrRef_.name_.Destroy(); + clear_has_EdgeNameOrRef(); + } +} +inline const std::string& Edge::name() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Edge.name) + return _internal_name(); +} +template <typename ArgT0, typename... ArgT> +inline void Edge::set_name(ArgT0&& arg0, ArgT... args) { + if (!_internal_has_name()) { + clear_EdgeNameOrRef(); + set_has_name(); + _impl_.EdgeNameOrRef_.name_.InitDefault(); + } + _impl_.EdgeNameOrRef_.name_.SetBytes( static_cast<ArgT0 &&>(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Edge.name) +} +inline std::string* Edge::mutable_name() { + std::string* _s = _internal_mutable_name(); + // @@protoc_insertion_point(field_mutable:mozilla.devtools.protobuf.Edge.name) + return _s; +} +inline const std::string& Edge::_internal_name() const { + if (_internal_has_name()) { + return _impl_.EdgeNameOrRef_.name_.Get(); + } + return ::PROTOBUF_NAMESPACE_ID::internal::GetEmptyStringAlreadyInited(); +} +inline void Edge::_internal_set_name(const std::string& value) { + if (!_internal_has_name()) { + clear_EdgeNameOrRef(); + set_has_name(); + _impl_.EdgeNameOrRef_.name_.InitDefault(); + } + _impl_.EdgeNameOrRef_.name_.Set(value, GetArenaForAllocation()); +} +inline std::string* Edge::_internal_mutable_name() { + if (!_internal_has_name()) { + clear_EdgeNameOrRef(); + set_has_name(); + _impl_.EdgeNameOrRef_.name_.InitDefault(); + } + return _impl_.EdgeNameOrRef_.name_.Mutable( GetArenaForAllocation()); +} +inline std::string* Edge::release_name() { + // @@protoc_insertion_point(field_release:mozilla.devtools.protobuf.Edge.name) + if (_internal_has_name()) { + clear_has_EdgeNameOrRef(); + return _impl_.EdgeNameOrRef_.name_.Release(); + } else { + return nullptr; + } +} +inline void Edge::set_allocated_name(std::string* name) { + if (has_EdgeNameOrRef()) { + clear_EdgeNameOrRef(); + } + if (name != nullptr) { + set_has_name(); + _impl_.EdgeNameOrRef_.name_.InitAllocated(name, GetArenaForAllocation()); + } + // @@protoc_insertion_point(field_set_allocated:mozilla.devtools.protobuf.Edge.name) +} + +// uint64 nameRef = 3; +inline bool Edge::_internal_has_nameref() const { + return EdgeNameOrRef_case() == kNameRef; +} +inline bool Edge::has_nameref() const { + return _internal_has_nameref(); +} +inline void Edge::set_has_nameref() { + _impl_._oneof_case_[0] = kNameRef; +} +inline void Edge::clear_nameref() { + if (_internal_has_nameref()) { + _impl_.EdgeNameOrRef_.nameref_ = uint64_t{0u}; + clear_has_EdgeNameOrRef(); + } +} +inline uint64_t Edge::_internal_nameref() const { + if (_internal_has_nameref()) { + return _impl_.EdgeNameOrRef_.nameref_; + } + return uint64_t{0u}; +} +inline void Edge::_internal_set_nameref(uint64_t value) { + if (!_internal_has_nameref()) { + clear_EdgeNameOrRef(); + set_has_nameref(); + } + _impl_.EdgeNameOrRef_.nameref_ = value; +} +inline uint64_t Edge::nameref() const { + // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Edge.nameRef) + return _internal_nameref(); +} +inline void Edge::set_nameref(uint64_t value) { + _internal_set_nameref(value); + // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Edge.nameRef) +} + +inline bool Edge::has_EdgeNameOrRef() const { + return EdgeNameOrRef_case() != EDGENAMEORREF_NOT_SET; +} +inline void Edge::clear_has_EdgeNameOrRef() { + _impl_._oneof_case_[0] = EDGENAMEORREF_NOT_SET; +} +inline Edge::EdgeNameOrRefCase Edge::EdgeNameOrRef_case() const { + return Edge::EdgeNameOrRefCase(_impl_._oneof_case_[0]); +} +#ifdef __GNUC__ + #pragma GCC diagnostic pop +#endif // __GNUC__ +// ------------------------------------------------------------------- + +// ------------------------------------------------------------------- + +// ------------------------------------------------------------------- + +// ------------------------------------------------------------------- + + +// @@protoc_insertion_point(namespace_scope) + +} // namespace protobuf +} // namespace devtools +} // namespace mozilla + +// @@protoc_insertion_point(global_scope) + +#include <google/protobuf/port_undef.inc> +#endif // GOOGLE_PROTOBUF_INCLUDED_GOOGLE_PROTOBUF_INCLUDED_CoreDump_2eproto diff --git a/devtools/shared/heapsnapshot/CoreDump.proto b/devtools/shared/heapsnapshot/CoreDump.proto new file mode 100644 index 0000000000..4d0bf27f01 --- /dev/null +++ b/devtools/shared/heapsnapshot/CoreDump.proto @@ -0,0 +1,152 @@ +/* -*- 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. + +syntax = "proto2"; + +option optimize_for = LITE_RUNTIME; + +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; + } + + // De-duplicated one-byte string. + oneof descriptiveTypeNameOrRef { + bytes descriptiveTypeName = 12; + uint64 descriptiveTypeNameRef = 13; + } +} + +// 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/devtools/shared/heapsnapshot/DeserializedNode.cpp b/devtools/shared/heapsnapshot/DeserializedNode.cpp new file mode 100644 index 0000000000..7de7d17faa --- /dev/null +++ b/devtools/shared/heapsnapshot/DeserializedNode.cpp @@ -0,0 +1,124 @@ +/* -*- 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(std::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 { + +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 = Edge(edge.name ? NS_xstrdup(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, JS::MutableHandle<JSObject*> outSavedFrameStack) const { + StackFrame f(&get()); + return ConstructSavedFrameStackSlow(cx, f, outSavedFrameStack); +} + +} // namespace ubi +} // namespace JS diff --git a/devtools/shared/heapsnapshot/DeserializedNode.h b/devtools/shared/heapsnapshot/DeserializedNode.h new file mode 100644 index 0000000000..cc831b4dc5 --- /dev/null +++ b/devtools/shared/heapsnapshot/DeserializedNode.h @@ -0,0 +1,308 @@ +/* -*- 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 <utility> + +#include "js/ColumnNumber.h" // JS::TaggedColumnNumberOneOrigin +#include "js/UbiNode.h" +#include "js/UniquePtr.h" +#include "mozilla/HashFunctions.h" +#include "mozilla/Maybe.h" +#include "mozilla/Vector.h" +#include "mozilla/devtools/CoreDump.pb.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 borrowed reference to a string owned by this node's owning HeapSnapshot. + const char16_t* descriptiveTypeName; + // 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, + const Maybe<StackFrameId>& allocationStack, + const char* className, const char* filename, + const char16_t* descriptiveName, HeapSnapshot& owner) + : id(id), + coarseType(coarseType), + typeName(typeName), + size(size), + edges(std::move(edges)), + allocationStack(allocationStack), + jsObjectClassName(className), + scriptFilename(filename), + descriptiveTypeName(descriptiveName), + owner(&owner) {} + virtual ~DeserializedNode() {} + + DeserializedNode(DeserializedNode&& rhs) + : id(rhs.id), + coarseType(rhs.coarseType), + typeName(rhs.typeName), + size(rhs.size), + edges(std::move(rhs.edges)), + allocationStack(rhs.allocationStack), + jsObjectClassName(rhs.jsObjectClassName), + scriptFilename(rhs.scriptFilename), + descriptiveTypeName(rhs.descriptiveTypeName), + owner(rhs.owner) {} + + DeserializedNode& operator=(DeserializedNode&& rhs) { + MOZ_ASSERT(&rhs != this); + this->~DeserializedNode(); + new (this) DeserializedNode(std::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), + descriptiveTypeName(nullptr), + owner(nullptr) {} + + private: + DeserializedNode(const DeserializedNode&) = delete; + DeserializedNode& operator=(const DeserializedNode&) = delete; +}; + +static inline js::HashNumber hashIdDerivedFromPtr(uint64_t id) { + return mozilla::HashGeneric(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; + JS::TaggedColumnNumberOneOrigin 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, + JS::TaggedColumnNumberOneOrigin 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), + 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; } + const char16_t* descriptiveTypeName() const override { + return get().descriptiveTypeName; + } + + 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; } + JS::TaggedColumnNumberOneOrigin 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); + } + uint32_t sourceId() const override { + // Source IDs are local to their host process and are not serialized. + return 0; + } + AtomOrTwoByteChars functionDisplayName() const override { + return AtomOrTwoByteChars(get().functionDisplayName); + } + + StackFrame parent() const override; + bool constructSavedFrameStack( + JSContext* cx, + JS::MutableHandle<JSObject*> outSavedFrameStack) const override; +}; + +} // namespace ubi +} // namespace JS + +#endif // mozilla_devtools_DeserializedNode__ diff --git a/devtools/shared/heapsnapshot/DominatorTree.cpp b/devtools/shared/heapsnapshot/DominatorTree.cpp new file mode 100644 index 0000000000..065db13576 --- /dev/null +++ b/devtools/shared/heapsnapshot/DominatorTree.cpp @@ -0,0 +1,133 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/devtools/DominatorTree.h" +#include "mozilla/dom/DominatorTreeBinding.h" +#include "mozilla/ErrorResult.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::Handle<JSObject*> aGivenProto) { + return dom::DominatorTree_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace devtools +} // namespace mozilla diff --git a/devtools/shared/heapsnapshot/DominatorTree.h b/devtools/shared/heapsnapshot/DominatorTree.h new file mode 100644 index 0000000000..fb3275cf23 --- /dev/null +++ b/devtools/shared/heapsnapshot/DominatorTree.h @@ -0,0 +1,66 @@ +/* -*- 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/RefCounted.h" +#include "js/UbiNodeDominatorTree.h" +#include "nsWrapperCache.h" + +namespace mozilla { +class ErrorResult; + +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(std::move(aDominatorTree)), + mHeapSnapshot(aHeapSnapshot) { + MOZ_ASSERT(aParent); + MOZ_ASSERT(aHeapSnapshot); + }; + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS; + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_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/devtools/shared/heapsnapshot/DominatorTreeNode.js b/devtools/shared/heapsnapshot/DominatorTreeNode.js new file mode 100644 index 0000000000..9fe5cf1032 --- /dev/null +++ b/devtools/shared/heapsnapshot/DominatorTreeNode.js @@ -0,0 +1,378 @@ +/* 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.js"); + +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; + } + + 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 ( + nodeTree, + 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(nodeTree, 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 (const node of treeNodes) { + const id = node.nodeId; + idToTreeNode.set(id, node); + targets.push(id); + } + + const shortestPaths = snapshot.computeShortestPaths( + start, + targets, + maxNumPaths + ); + + for (const [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/devtools/shared/heapsnapshot/FileDescriptorOutputStream.cpp b/devtools/shared/heapsnapshot/FileDescriptorOutputStream.cpp new file mode 100644 index 0000000000..651d7b9496 --- /dev/null +++ b/devtools/shared/heapsnapshot/FileDescriptorOutputStream.cpp @@ -0,0 +1,81 @@ +/* -*- 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::StreamStatus() { + return fd ? NS_OK : NS_ERROR_FAILURE; +} + +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/devtools/shared/heapsnapshot/FileDescriptorOutputStream.h b/devtools/shared/heapsnapshot/FileDescriptorOutputStream.h new file mode 100644 index 0000000000..5d8f31424c --- /dev/null +++ b/devtools/shared/heapsnapshot/FileDescriptorOutputStream.h @@ -0,0 +1,38 @@ +/* -*- 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/devtools/shared/heapsnapshot/HeapAnalyses.worker.js b/devtools/shared/heapsnapshot/HeapAnalyses.worker.js new file mode 100644 index 0000000000..0f61d99a94 --- /dev/null +++ b/devtools/shared/heapsnapshot/HeapAnalyses.worker.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/. */ + +// 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"; + +/* import-globals-from /toolkit/components/workerloader/require.js */ +importScripts("resource://gre/modules/workers/require.js"); +/* import-globals-from ../worker/helper.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] = ChromeUtils.readHeapSnapshot(snapshotFilePath); + return true; +}); + +/** + * @see HeapAnalysesClient.prototype.deleteHeapSnapshot + */ +workerHelper.createTask(self, "deleteHeapSnapshot", ({ snapshotFilePath }) => { + const snapshot = snapshots[snapshotFilePath]; + if (!snapshot) { + throw new Error(`No known heap snapshot for '${snapshotFilePath}'`); + } + + snapshots[snapshotFilePath] = undefined; + + const 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 (!(dominatorTreeId >= 0 && 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 (!(dominatorTreeId >= 0 && 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; + 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/devtools/shared/heapsnapshot/HeapAnalysesClient.js b/devtools/shared/heapsnapshot/HeapAnalysesClient.js new file mode 100644 index 0000000000..a77d9397a5 --- /dev/null +++ b/devtools/shared/heapsnapshot/HeapAnalysesClient.js @@ -0,0 +1,285 @@ +/* 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("resource://devtools/shared/DevToolsUtils.js"); +const { + DevToolsWorker, +} = require("resource://devtools/shared/worker/worker.js"); + +const WORKER_URL = + "resource://devtools/shared/heapsnapshot/HeapAnalyses.worker.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/devtools/shared/heapsnapshot/HeapSnapshot.cpp b/devtools/shared/heapsnapshot/HeapSnapshot.cpp new file mode 100644 index 0000000000..ce0eec2d5c --- /dev/null +++ b/devtools/shared/heapsnapshot/HeapSnapshot.cpp @@ -0,0 +1,1581 @@ +/* -*- 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/Array.h" // JS::NewArrayObject +#include "js/ColumnNumber.h" // JS::LimitedColumnNumberOneOrigin, JS::TaggedColumnNumberOneOrigin +#include "js/Debug.h" +#include "js/PropertyAndElement.h" // JS_DefineProperty +#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/Telemetry.h" +#include "mozilla/Unused.h" + +#include "jsapi.h" +#include "jsfriendapi.h" +#include "js/MapAndSet.h" +#include "js/Object.h" // JS::GetCompartment +#include "nsComponentManagerUtils.h" // do_CreateInstance +#include "nsCycleCollectionParticipant.h" +#include "nsCRTGlue.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" +#include "SpecialSystemDirectory.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, + JS::Handle<JSObject*> aGivenProto) { + return HeapSnapshot_Binding::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* operator()(const std::string* str) { + MOZ_ASSERT(str); + size_t length = str->length() / sizeof(CharT); + auto tempString = reinterpret_cast<const CharT*>(str->data()); + + UniqueFreePtr<CharT[]> owned(NS_xstrndup(tempString, length)); + if (!internedStrings.append(std::move(owned))) return nullptr; + + return internedStrings.back().get(); + } + + const CharT* operator()(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; + } + + const char16_t* descriptiveTypeName = nullptr; + if (node.descriptiveTypeNameOrRef_case() != + protobuf::Node::DESCRIPTIVETYPENAMEORREF_NOT_SET) { + Maybe<StringOrRef> descriptiveTypeNameOrRef = + GET_STRING_OR_REF(node, descriptivetypename); + descriptiveTypeName = getOrInternString<char16_t>(internedTwoByteStrings, + descriptiveTypeNameOrRef); + if (NS_WARN_IF(!descriptiveTypeName)) return false; + } + + if (NS_WARN_IF(!nodes.putNew( + id, DeserializedNode(id, coarseType, typeName, size, std::move(edges), + allocationStack, jsObjectClassName, + scriptFilename, descriptiveTypeName, *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; + JS::TaggedColumnNumberOneOrigin column( + JS::LimitedColumnNumberOneOrigin(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) { + 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(!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 iter = edgeReferents.iter(); !iter.done(); iter.next()) { + if (NS_WARN_IF(!nodes.has(iter.get()))) return false; + } + + return true; +} + +/*** Heap Snapshot Analyses ***************************************************/ + +void HeapSnapshot::TakeCensus(JSContext* cx, JS::Handle<JSObject*> options, + JS::MutableHandle<JS::Value> rval, + ErrorResult& rv) { + JS::ubi::Census census(cx); + + 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.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::Handle<JSObject*> breakdown, + uint64_t nodeId, + JS::MutableHandle<JS::Value> rval, + ErrorResult& rv) { + MOZ_ASSERT(breakdown); + JS::Rooted<JS::Value> 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>(std::move(*maybeTree), this, mParent); +} + +void HeapSnapshot::ComputeShortestPaths(JSContext* cx, uint64_t start, + const Sequence<uint64_t>& targets, + uint64_t maxNumPaths, + JS::MutableHandle<JSObject*> 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; + + 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, std::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. + + JS::Rooted<JSObject*> resultsMap(cx, JS::NewMapObject(cx)); + if (NS_WARN_IF(!resultsMap)) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + for (auto iter = shortestPaths.targetIter(); !iter.done(); iter.next()) { + JS::Rooted<JS::Value> key(cx, JS::NumberValue(iter.get().identifier())); + JS::RootedVector<JS::Value> paths(cx); + + bool ok = shortestPaths.forEachPath(iter.get(), [&](JS::ubi::Path& path) { + JS::RootedVector<JS::Value> pathValues(cx); + + for (JS::ubi::BackEdge* edge : path) { + JS::Rooted<JSObject*> pathPart(cx, JS_NewPlainObject(cx)); + if (!pathPart) { + return false; + } + + JS::Rooted<JS::Value> predecessor( + cx, NumberValue(edge->predecessor().identifier())); + if (!JS_DefineProperty(cx, pathPart, "predecessor", predecessor, + JSPROP_ENUMERATE)) { + return false; + } + + JS::Rooted<JS::Value> edgeNameVal(cx, NullValue()); + if (edge->name()) { + JS::Rooted<JSString*> 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; + } + } + + JS::Rooted<JSObject*> 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::Rooted<JSObject*> pathsArray(cx, JS::NewArrayObject(cx, paths)); + if (NS_WARN_IF(!pathsArray)) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + JS::Rooted<JS::Value> 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, JS::HandleVector<JSObject*> globals) { + unsigned length = globals.length(); + for (unsigned i = 0; i < length; i++) { + if (!compartments.put(JS::GetCompartment(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(JS::HandleVector<JSObject*> 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`. +// +// Return value is a pair of the status and an AutoCheckCannotGC token, +// forwarded from ubi::RootList::init(), to ensure that the caller does +// not GC while the RootList is live and initialized. +static std::pair<bool, AutoCheckCannotGC> EstablishBoundaries( + JSContext* cx, ErrorResult& rv, const HeapSnapshotBoundaries& boundaries, + ubi::RootList& roots, CompartmentSet& compartments) { + MOZ_ASSERT(!roots.initialized()); + MOZ_ASSERT(compartments.empty()); + + bool foundBoundaryProperty = false; + + if (boundaries.mRuntime.WasPassed()) { + foundBoundaryProperty = true; + + if (!boundaries.mRuntime.Value()) { + rv.Throw(NS_ERROR_INVALID_ARG); + return {false, AutoCheckCannotGC(cx)}; + } + + auto [ok, nogc] = roots.init(); + if (!ok) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return {false, nogc}; + } + } + + if (boundaries.mDebugger.WasPassed()) { + if (foundBoundaryProperty) { + rv.Throw(NS_ERROR_INVALID_ARG); + return {false, AutoCheckCannotGC(cx)}; + } + foundBoundaryProperty = true; + + JSObject* dbgObj = boundaries.mDebugger.Value(); + if (!dbgObj || !dbg::IsDebugger(*dbgObj)) { + rv.Throw(NS_ERROR_INVALID_ARG); + return {false, AutoCheckCannotGC(cx)}; + } + + JS::RootedVector<JSObject*> globals(cx); + if (!dbg::GetDebuggeeGlobals(cx, *dbgObj, &globals) || + !PopulateCompartmentsWithGlobals(compartments, globals) || + !roots.init(compartments).first || !AddGlobalsAsRoots(globals, roots)) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return {false, AutoCheckCannotGC(cx)}; + } + } + + if (boundaries.mGlobals.WasPassed()) { + if (foundBoundaryProperty) { + rv.Throw(NS_ERROR_INVALID_ARG); + return {false, AutoCheckCannotGC(cx)}; + } + foundBoundaryProperty = true; + + uint32_t length = boundaries.mGlobals.Value().Length(); + if (length == 0) { + rv.Throw(NS_ERROR_INVALID_ARG); + return {false, AutoCheckCannotGC(cx)}; + } + + JS::RootedVector<JSObject*> 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, AutoCheckCannotGC(cx)}; + } + if (!globals.append(global)) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return {false, AutoCheckCannotGC(cx)}; + } + } + + if (!PopulateCompartmentsWithGlobals(compartments, globals) || + !roots.init(compartments).first || !AddGlobalsAsRoots(globals, roots)) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return {false, AutoCheckCannotGC(cx)}; + } + } + AutoCheckCannotGC nogc(cx); + + if (!foundBoundaryProperty) { + rv.Throw(NS_ERROR_INVALID_ARG); + return {false, nogc}; + } + + MOZ_ASSERT(roots.initialized()); + return {true, nogc}; +} + +// 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 CopyToBufferMatcher { + RangedPtr<char16_t> destination; + size_t maxLength; + + CopyToBufferMatcher(RangedPtr<char16_t> destination, size_t maxLength) + : destination(destination), maxLength(maxLength) {} + + size_t operator()(JS::ubi::EdgeName& ptr) { + return ptr ? operator()(ptr.get()) : 0; + } + + size_t operator()(JSAtom* atom) { + MOZ_ASSERT(atom); + JS::ubi::AtomOrTwoByteChars s(atom); + return s.copyToBuffer(destination, maxLength); + } + + size_t operator()(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(std::forward<T>(rhs)) {} + + template <typename T> + TwoByteString& operator=(T&& rhs) { + MOZ_ASSERT(this != &rhs, "self-move disallowed"); + this->~TwoByteString(); + new (this) TwoByteString(std::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) { + return s.match([](auto* a) { return TwoByteString(a); }); + } + + // Returns true if the given TwoByteString is non-null, false otherwise. + bool isNonNull() const { + return match([](auto& t) { return t != nullptr; }); + } + + // Return the length of the string, 0 if it is null. + size_t length() const { + return match( + [](JSAtom* atom) -> size_t { + MOZ_ASSERT(atom); + JS::ubi::AtomOrTwoByteChars s(atom); + return s.length(); + }, + [](const char16_t* chars) -> size_t { + MOZ_ASSERT(chars); + return NS_strlen(chars); + }, + [](const JS::ubi::EdgeName& ptr) -> size_t { + MOZ_ASSERT(ptr); + return NS_strlen(ptr.get()); + }); + } + + // 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; + + static js::HashNumber hash(const Lookup& l) { + return l.match( + [](const JSAtom* atom) { + return js::DefaultHasher<const JSAtom*>::hash(atom); + }, + [](const char16_t* chars) { + MOZ_ASSERT(chars); + auto length = NS_strlen(chars); + return HashString(chars, length); + }, + [](const JS::ubi::EdgeName& ptr) { + const char16_t* chars = ptr.get(); + MOZ_ASSERT(chars); + auto length = NS_strlen(chars); + return HashString(chars, length); + }); + } + + struct EqualityMatcher { + const TwoByteString& rhs; + explicit EqualityMatcher(const TwoByteString& rhs) : rhs(rhs) {} + + bool operator()(const JSAtom* atom) { + return rhs.is<JSAtom*>() && rhs.as<JSAtom*>() == atom; + } + + bool operator()(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 operator()(const JS::ubi::EdgeName& ptr) { + MOZ_ASSERT(ptr); + return operator()(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 = std::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. + + JS::Compartment* 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.ByteSizeLong()); + 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, std::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().oneOriginValue()); + 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) {} + + ~StreamWriter() override {} + + bool writeMetadata(uint64_t timestamp) final { + protobuf::Metadata metadata; + metadata.set_timestamp(timestamp); + return writeMessage(metadata); + } + + bool writeNode(const JS::ubi::Node& ubiNode, EdgePolicy includeEdges) 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(std::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; + } + } + + if (ubiNode.descriptiveTypeName()) { + auto descriptiveTypeName = TwoByteString(ubiNode.descriptiveTypeName()); + if (NS_WARN_IF(!attachTwoByteString( + descriptiveTypeName, + [&](std::string* name) { + protobufNode.set_allocated_descriptivetypename(name); + }, + [&](uint64_t ref) { + protobufNode.set_descriptivetypenameref(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: + // For telemetry. + uint32_t nodeCount; + uint32_t edgeCount; + + HeapSnapshotHandler(CoreDumpWriter& writer, JS::CompartmentSet* compartments) + : writer(writer), + compartments(compartments), + nodeCount(0), + edgeCount(0) {} + + // 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) { + edgeCount++; + + // 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)) { + // Because ShouldIncludeEdge considers the |origin| node as well, we don't + // want to consider this node 'visited' until we write it to the core + // dump. + traversal.doNotMarkReferentAsVisited(); + return true; + } + + nodeCount++; + + 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, uint32_t& outNodeCount, + uint32_t& outEdgeCount) { + // 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); + traversal.wantNames = wantNames; + + bool ok = traversal.addStartVisited(node) && traversal.traverse(); + + if (ok) { + outNodeCount = handler.nodeCount; + outEdgeCount = handler.edgeCount; + } + + return ok; +} + +static unsigned long msSinceProcessCreation(const TimeStamp& now) { + auto duration = now - TimeStamp::ProcessCreation(); + return (unsigned long)duration.ToMilliseconds(); +} + +/* static */ +already_AddRefed<nsIFile> HeapSnapshot::CreateUniqueCoreDumpFile( + ErrorResult& rv, const TimeStamp& now, nsAString& outFilePath, + nsAString& outSnapshotId) { + MOZ_RELEASE_ASSERT(XRE_IsParentProcess()); + nsCOMPtr<nsIFile> file; + rv = GetSpecialSystemDirectory(OS_TemporaryDirectory, getter_AddRefs(file)); + if (NS_WARN_IF(rv.Failed())) return nullptr; + + nsAutoString tempPath; + rv = file->GetPath(tempPath); + 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; + + // The snapshot ID must be computed in the process that created the + // temp file, because TmpD may not be the same in all processes. + outSnapshotId.Assign(Substring( + outFilePath, tempPath.Length() + 1, + outFilePath.Length() - tempPath.Length() - sizeof(".fxsnapshot"))); + + 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, + nsAString& outSnapshotId) { + if (XRE_IsParentProcess()) { + // Create the file and open the output stream directly. + + nsCOMPtr<nsIFile> file = HeapSnapshot::CreateUniqueCoreDumpFile( + rv, start, outFilePath, outSnapshotId); + 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(); + } + // 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(); + outSnapshotId = opened.snapshotId(); + 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 ChromeUtils::SaveHeapSnapshotShared( + GlobalObject& global, const HeapSnapshotBoundaries& boundaries, + nsAString& outFilePath, nsAString& outSnapshotId, ErrorResult& rv) { + auto start = TimeStamp::Now(); + + bool wantNames = true; + CompartmentSet compartments; + uint32_t nodeCount = 0; + uint32_t edgeCount = 0; + + nsCOMPtr<nsIOutputStream> outputStream = + getCoreDumpOutputStream(rv, start, outFilePath, outSnapshotId); + if (NS_WARN_IF(rv.Failed())) return; + + ZeroCopyNSIOutputStream zeroCopyStream(outputStream); + ::google::protobuf::io::GzipOutputStream gzipStream(&zeroCopyStream); + + JSContext* cx = global.Context(); + + { + ubi::RootList rootList(cx, wantNames); + auto [ok, nogc] = + EstablishBoundaries(cx, rv, boundaries, rootList, compartments); + if (!ok) { + return; + } + + StreamWriter writer(cx, gzipStream, wantNames, + !compartments.empty() ? &compartments : nullptr); + + 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.empty() ? &compartments : nullptr, nogc, + nodeCount, edgeCount)) { + rv.Throw(zeroCopyStream.failed() ? zeroCopyStream.result() + : NS_ERROR_UNEXPECTED); + return; + } + } + + Telemetry::AccumulateTimeDelta(Telemetry::DEVTOOLS_SAVE_HEAP_SNAPSHOT_MS, + start); + Telemetry::Accumulate(Telemetry::DEVTOOLS_HEAP_SNAPSHOT_NODE_COUNT, + nodeCount); + Telemetry::Accumulate(Telemetry::DEVTOOLS_HEAP_SNAPSHOT_EDGE_COUNT, + edgeCount); +} + +/* static */ +uint64_t ChromeUtils::GetObjectNodeId(GlobalObject& global, + JS::Handle<JSObject*> val) { + JS::Rooted<JSObject*> obj(global.Context(), val); + + JS::ubi::Node node(obj); + return node.identifier(); +} + +/* static */ +void ChromeUtils::SaveHeapSnapshot(GlobalObject& global, + const HeapSnapshotBoundaries& boundaries, + nsAString& outFilePath, ErrorResult& rv) { + nsAutoString snapshotId; + SaveHeapSnapshotShared(global, boundaries, outFilePath, snapshotId, rv); +} + +/* static */ +void ChromeUtils::SaveHeapSnapshotGetId( + GlobalObject& global, const HeapSnapshotBoundaries& boundaries, + nsAString& outSnapshotId, ErrorResult& rv) { + nsAutoString filePath; + SaveHeapSnapshotShared(global, boundaries, filePath, outSnapshotId, rv); +} + +/* static */ +already_AddRefed<HeapSnapshot> ChromeUtils::ReadHeapSnapshot( + GlobalObject& global, const nsAString& filePath, ErrorResult& rv) { + auto start = TimeStamp::Now(); + + nsresult nsrv; + nsCOMPtr<nsIFile> snapshotFile = + do_CreateInstance("@mozilla.org/file/local;1", &nsrv); + + if (NS_FAILED(nsrv)) { + rv = nsrv; + return nullptr; + } + + rv = snapshotFile->InitWithPath(filePath); + if (rv.Failed()) { + return nullptr; + } + + AutoMemMap mm; + rv = mm.init(snapshotFile); + if (rv.Failed()) return nullptr; + + RefPtr<HeapSnapshot> snapshot = HeapSnapshot::Create( + global.Context(), global, reinterpret_cast<const uint8_t*>(mm.address()), + mm.size(), rv); + + if (!rv.Failed()) + Telemetry::AccumulateTimeDelta(Telemetry::DEVTOOLS_READ_HEAP_SNAPSHOT_MS, + start); + + return snapshot.forget(); +} + +} // namespace dom +} // namespace mozilla diff --git a/devtools/shared/heapsnapshot/HeapSnapshot.h b/devtools/shared/heapsnapshot/HeapSnapshot.h new file mode 100644 index 0000000000..a23fdc2c86 --- /dev/null +++ b/devtools/shared/heapsnapshot/HeapSnapshot.h @@ -0,0 +1,216 @@ +/* -*- 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/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/UniquePtrExtensions.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 { +class ErrorResult; + +namespace devtools { + +class DominatorTree; + +using UniqueTwoByteString = UniqueFreePtr<char16_t[]>; +using UniqueOneByteString = UniqueFreePtr<char[]>; + +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, + nsAString& outSnapshotId); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_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() { + 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::Handle<JSObject*> options, + JS::MutableHandle<JS::Value> rval, ErrorResult& rv); + + void DescribeNode(JSContext* cx, JS::Handle<JSObject*> breakdown, + uint64_t nodeId, JS::MutableHandle<JS::Value> 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::MutableHandle<JSObject*> 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, uint32_t& outNodeCount, + uint32_t& outEdgeCount); +inline bool WriteHeapGraph(JSContext* cx, const JS::ubi::Node& node, + CoreDumpWriter& writer, bool wantNames, + JS::CompartmentSet* compartments, + JS::AutoCheckCannotGC& noGC) { + uint32_t ignoreNodeCount; + uint32_t ignoreEdgeCount; + return WriteHeapGraph(cx, node, writer, wantNames, compartments, noGC, + ignoreNodeCount, ignoreEdgeCount); +} + +// Get the mozilla::MallocSizeOf for the current thread's JSRuntime. +MallocSizeOf GetCurrentThreadDebuggerMallocSizeOf(); + +} // namespace devtools +} // namespace mozilla + +#endif // mozilla_devtools_HeapSnapshot__ diff --git a/devtools/shared/heapsnapshot/HeapSnapshotFileUtils.js b/devtools/shared/heapsnapshot/HeapSnapshotFileUtils.js new file mode 100644 index 0000000000..ab804eded1 --- /dev/null +++ b/devtools/shared/heapsnapshot/HeapSnapshotFileUtils.js @@ -0,0 +1,85 @@ +/* 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 lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", +}); + +function getHeapSnapshotFileTemplate() { + return PathUtils.join(PathUtils.tempDir, `${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 () { + const file = new lazy.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 PathUtils.join(PathUtils.tempDir, 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 IOUtils.stat(path).then( + () => true, + () => false + ); +}; diff --git a/devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperChild.h b/devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperChild.h new file mode 100644 index 0000000000..becf00eb85 --- /dev/null +++ b/devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperChild.h @@ -0,0 +1,31 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 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/devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperParent.cpp b/devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperParent.cpp new file mode 100644 index 0000000000..ea1cf8378a --- /dev/null +++ b/devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperParent.cpp @@ -0,0 +1,56 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 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 { + +static bool openFileFailure(ErrorResult& rv, + OpenHeapSnapshotTempFileResponse* outResponse) { + *outResponse = rv.StealNSResult(); + return true; +} + +mozilla::ipc::IPCResult +HeapSnapshotTempFileHelperParent::RecvOpenHeapSnapshotTempFile( + OpenHeapSnapshotTempFileResponse* outResponse) { + auto start = TimeStamp::Now(); + ErrorResult rv; + nsAutoString filePath; + nsAutoString snapshotId; + nsCOMPtr<nsIFile> file = + HeapSnapshot::CreateUniqueCoreDumpFile(rv, start, filePath, snapshotId); + if (NS_WARN_IF(rv.Failed())) { + if (!openFileFailure(rv, outResponse)) { + return IPC_FAIL_NO_REASON(this); + } + return IPC_OK(); + } + + PRFileDesc* prfd; + rv = file->OpenNSPRFileDesc(PR_WRONLY, 0, &prfd); + if (NS_WARN_IF(rv.Failed())) { + if (!openFileFailure(rv, outResponse)) { + return IPC_FAIL_NO_REASON(this); + } + return IPC_OK(); + } + + FileDescriptor::PlatformHandleType handle = + FileDescriptor::PlatformHandleType(PR_FileDesc2NativeHandle(prfd)); + FileDescriptor fd(handle); + *outResponse = OpenedFile(filePath, snapshotId, fd); + return IPC_OK(); +} + +} // namespace devtools +} // namespace mozilla diff --git a/devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperParent.h b/devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperParent.h new file mode 100644 index 0000000000..058f0c6eea --- /dev/null +++ b/devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperParent.h @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 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 { + friend class PHeapSnapshotTempFileHelperParent; + + explicit HeapSnapshotTempFileHelperParent() {} + void ActorDestroy(ActorDestroyReason why) override {} + mozilla::ipc::IPCResult RecvOpenHeapSnapshotTempFile( + OpenHeapSnapshotTempFileResponse* outResponse); + + 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/devtools/shared/heapsnapshot/PHeapSnapshotTempFileHelper.ipdl b/devtools/shared/heapsnapshot/PHeapSnapshotTempFileHelper.ipdl new file mode 100644 index 0000000000..7257e43088 --- /dev/null +++ b/devtools/shared/heapsnapshot/PHeapSnapshotTempFileHelper.ipdl @@ -0,0 +1,37 @@ +/* -*- 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; + nsString snapshotId; + FileDescriptor descriptor; +}; + +union OpenHeapSnapshotTempFileResponse +{ + nsresult; + OpenedFile; +}; + +[ManualDealloc] +sync protocol PHeapSnapshotTempFileHelper +{ + manager PContent; + +parent: + sync OpenHeapSnapshotTempFile() returns (OpenHeapSnapshotTempFileResponse response); + + async __delete__(); +}; + +} // namespace devtools +} // namespace mozilla diff --git a/devtools/shared/heapsnapshot/ZeroCopyNSIOutputStream.cpp b/devtools/shared/heapsnapshot/ZeroCopyNSIOutputStream.cpp new file mode 100644 index 0000000000..8edde20635 --- /dev/null +++ b/devtools/shared/heapsnapshot/ZeroCopyNSIOutputStream.cpp @@ -0,0 +1,79 @@ +/* -*- 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/devtools/shared/heapsnapshot/ZeroCopyNSIOutputStream.h b/devtools/shared/heapsnapshot/ZeroCopyNSIOutputStream.h new file mode 100644 index 0000000000..96158e389e --- /dev/null +++ b/devtools/shared/heapsnapshot/ZeroCopyNSIOutputStream.h @@ -0,0 +1,69 @@ +/* -*- 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/devtools/shared/heapsnapshot/census-tree-node.js b/devtools/shared/heapsnapshot/census-tree-node.js new file mode 100644 index 0000000000..82f5929370 --- /dev/null +++ b/devtools/shared/heapsnapshot/census-tree-node.js @@ -0,0 +1,764 @@ +/* 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) { + // eslint-disable-next-line max-len + 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); + + const 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/devtools/shared/heapsnapshot/generate-core-dump-sources.sh b/devtools/shared/heapsnapshot/generate-core-dump-sources.sh new file mode 100755 index 0000000000..97e492ff05 --- /dev/null +++ b/devtools/shared/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/devtools/shared/heapsnapshot/moz.build b/devtools/shared/heapsnapshot/moz.build new file mode 100644 index 0000000000..41bc0fb4aa --- /dev/null +++ b/devtools/shared/heapsnapshot/moz.build @@ -0,0 +1,59 @@ +# -*- 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 = ("DevTools", "Memory") + +if CONFIG["ENABLE_TESTS"]: + DIRS += ["tests/gtest"] + +XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.toml"] +MOCHITEST_CHROME_MANIFESTS += ["tests/chrome/chrome.toml"] +BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"] + +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" + +DevToolsModules( + "census-tree-node.js", + "CensusUtils.js", + "DominatorTreeNode.js", + "HeapAnalyses.worker.js", + "HeapAnalysesClient.js", + "HeapSnapshotFileUtils.js", + "shortest-paths.js", +) diff --git a/devtools/shared/heapsnapshot/shortest-paths.js b/devtools/shared/heapsnapshot/shortest-paths.js new file mode 100644 index 0000000000..eca1ab9735 --- /dev/null +++ b/devtools/shared/heapsnapshot/shortest-paths.js @@ -0,0 +1,93 @@ +/* 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); + } + + // eslint-disable-next-line no-labels + outer: for (const 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)) { + // eslint-disable-next-line no-labels + 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 (const [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 (const [to, edgeNameSet] of toMap) { + for (const name of edgeNameSet) { + edges.push({ from, to, name }); + } + } + } + + return { nodes, edges }; +}; diff --git a/devtools/shared/heapsnapshot/tests/browser/browser.toml b/devtools/shared/heapsnapshot/tests/browser/browser.toml new file mode 100644 index 0000000000..dc0042b957 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/browser/browser.toml @@ -0,0 +1,9 @@ +[DEFAULT] +tags = "devtools" +subsuite = "devtools" +support-files = [ + "!/devtools/client/shared/test/shared-head.js", + "!/devtools/client/shared/test/telemetry-test-helpers.js", +] + +["browser_saveHeapSnapshot_e10s_01.js"] diff --git a/devtools/shared/heapsnapshot/tests/browser/browser_saveHeapSnapshot_e10s_01.js b/devtools/shared/heapsnapshot/tests/browser/browser_saveHeapSnapshot_e10s_01.js new file mode 100644 index 0000000000..1fc25341b8 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/browser/browser_saveHeapSnapshot_e10s_01.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Bug 1201597 - Test to verify that we can take a heap snapshot in an e10s child process. + */ + +"use strict"; + +add_task(async function () { + // Create a minimal browser + const browser = document.createXULElement("browser"); + browser.setAttribute("type", "content"); + document.body.appendChild(browser); + await BrowserTestUtils.browserLoaded(browser); + + info("Save heap snapshot"); + const result = await SpecialPowers.spawn(browser, [], () => { + try { + ChromeUtils.saveHeapSnapshot({ runtime: true }); + } catch (err) { + return err.toString(); + } + + return ""; + }); + is(result, "", "result of saveHeapSnapshot"); + + browser.remove(); +}); diff --git a/devtools/shared/heapsnapshot/tests/chrome/chrome.toml b/devtools/shared/heapsnapshot/tests/chrome/chrome.toml new file mode 100644 index 0000000000..afe21d17fa --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/chrome/chrome.toml @@ -0,0 +1,7 @@ +[DEFAULT] +tags = "devtools devtools-memory" +skip-if = ["os == 'android'"] + +["test_DominatorTree_01.html"] + +["test_SaveHeapSnapshot.html"] diff --git a/devtools/shared/heapsnapshot/tests/chrome/test_DominatorTree_01.html b/devtools/shared/heapsnapshot/tests/chrome/test_DominatorTree_01.html new file mode 100644 index 0000000000..61e60ae209 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/chrome/test_DominatorTree_01.html @@ -0,0 +1,40 @@ +<!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 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> +"use strict"; +/* global window, ChromeUtils, DominatorTree */ + +SimpleTest.waitForExplicitFinish(); +window.onload = function() { + const path = ChromeUtils.saveHeapSnapshot({ runtime: true }); + const snapshot = ChromeUtils.readHeapSnapshot(path); + + const dominatorTree = snapshot.computeDominatorTree(); + ok(dominatorTree); + ok(DominatorTree.isInstance(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/devtools/shared/heapsnapshot/tests/chrome/test_SaveHeapSnapshot.html b/devtools/shared/heapsnapshot/tests/chrome/test_SaveHeapSnapshot.html new file mode 100644 index 0000000000..53831bfaa2 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/chrome/test_SaveHeapSnapshot.html @@ -0,0 +1,27 @@ +<!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 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> +"use strict"; + +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/devtools/shared/heapsnapshot/tests/gtest/DeserializedNodeUbiNodes.cpp b/devtools/shared/heapsnapshot/tests/gtest/DeserializedNodeUbiNodes.cpp new file mode 100644 index 0000000000..dc24d13e98 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/gtest/DeserializedNodeUbiNodes.cpp @@ -0,0 +1,95 @@ +/* -*- 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(std::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(std::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(std::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(std::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/devtools/shared/heapsnapshot/tests/gtest/DeserializedStackFrameUbiStackFrames.cpp b/devtools/shared/heapsnapshot/tests/gtest/DeserializedStackFrameUbiStackFrames.cpp new file mode 100644 index 0000000000..4ce2c8968f --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/gtest/DeserializedStackFrameUbiStackFrames.cpp @@ -0,0 +1,98 @@ +/* -*- 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/ColumnNumber.h" // JS::LimitedColumnNumberOneOrigin, JS::TaggedColumnNumberOneOrigin +#include "js/SavedFrameAPI.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() = default; +}; + +DEF_TEST(DeserializedStackFrameUbiStackFrames, { + StackFrameId id = uint64_t(1) << 42; + uint32_t line = 1337; + JS::TaggedColumnNumberOneOrigin column( + JS::LimitedColumnNumberOneOrigin(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::Rooted<JSObject*> savedFrame(cx); + EXPECT_TRUE(ubiFrame.constructSavedFrameStack(cx, &savedFrame)); + + JSPrincipals* principals = JS::GetRealmPrincipals(js::GetContextRealm(cx)); + + uint32_t frameLine; + ASSERT_EQ(JS::SavedFrameResult::Ok, + JS::GetSavedFrameLine(cx, principals, savedFrame, &frameLine)); + EXPECT_EQ(line, frameLine); + + JS::TaggedColumnNumberOneOrigin frameColumn; + ASSERT_EQ(JS::SavedFrameResult::Ok, + JS::GetSavedFrameColumn(cx, principals, savedFrame, &frameColumn)); + EXPECT_EQ(column, frameColumn); + + JS::Rooted<JSObject*> parent(cx); + ASSERT_EQ(JS::SavedFrameResult::Ok, + JS::GetSavedFrameParent(cx, principals, 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/devtools/shared/heapsnapshot/tests/gtest/DevTools.cpp b/devtools/shared/heapsnapshot/tests/gtest/DevTools.cpp new file mode 100644 index 0000000000..8e89d5ecd7 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/gtest/DevTools.cpp @@ -0,0 +1,7 @@ +/* -*- 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 "DevTools.h" +const char16_t JS::ubi::Concrete<FakeNode>::concreteTypeName[] = u"FakeNode"; diff --git a/devtools/shared/heapsnapshot/tests/gtest/DevTools.h b/devtools/shared/heapsnapshot/tests/gtest/DevTools.h new file mode 100644 index 0000000000..a3c29e87fc --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/gtest/DevTools.h @@ -0,0 +1,217 @@ +/* -*- 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 <utility> + +#include "CoreDump.pb.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "js/Principals.h" +#include "js/UbiNode.h" +#include "js/UniquePtr.h" +#include "jsapi.h" +#include "jspubtd.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/devtools/HeapSnapshot.h" +#include "mozilla/dom/ChromeUtils.h" +#include "nsCRTGlue.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; + JS::Compartment* compartment; + JS::Zone* zone; + JS::PersistentRooted<JSObject*> global; + + DevTools() : _initialized(false), cx(nullptr) {} + + virtual void SetUp() { + MOZ_ASSERT(!_initialized); + + cx = getContext(); + if (!cx) return; + + global.init(cx, createGlobal()); + if (!global) return; + JS::EnterRealm(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.c_str() : "<no filename>", + (unsigned int)report->lineno, message); + } + + static const JSClass* getGlobalClass() { + static const JSClass globalClass = {"global", JSCLASS_GLOBAL_FLAGS, + &JS::DefaultGlobalClassOps}; + return &globalClass; + } + + JSObject* createGlobal() { + /* Create the global object. */ + JS::RealmOptions options; + // dummy + options.behaviors().setReduceTimerPrecisionCallerType( + JS::RTPCallerTypeToken{0}); + return JS_NewGlobalObject(cx, getGlobalClass(), nullptr, + JS::FireOnNewGlobalHook, options); + } + + virtual void TearDown() { + _initialized = false; + + if (global) { + JS::LeaveRealm(cx, nullptr); + global = nullptr; + } + } +}; + +// 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; + JS::Compartment* compartment; + JS::Zone* zone; + size_t size; + + explicit FakeNode() : 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; } + + JS::Compartment* 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); + } +}; + +} // namespace ubi +} // namespace JS + +inline void AddEdge(FakeNode& node, FakeNode& referent, + const char16_t* edgeName = nullptr) { + char16_t* ownedEdgeName = nullptr; + if (edgeName) { + ownedEdgeName = NS_xstrdup(edgeName); + } + + JS::ubi::Edge edge(ownedEdgeName, &referent); + ASSERT_TRUE(node.edges.append(std::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 = default; + MOCK_METHOD2(writeNode, + bool(const JS::ubi::Node&, CoreDumpWriter::EdgePolicy)); + MOCK_METHOD1(writeMetadata, bool(uint64_t)); +}; + +inline 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/devtools/shared/heapsnapshot/tests/gtest/DoesCrossCompartmentBoundaries.cpp b/devtools/shared/heapsnapshot/tests/gtest/DoesCrossCompartmentBoundaries.cpp new file mode 100644 index 0000000000..d264c73738 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/gtest/DoesCrossCompartmentBoundaries.cpp @@ -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/. */ + +// 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::RealmOptions options; + // dummy + options.behaviors().setReduceTimerPrecisionCallerType( + JS::RTPCallerTypeToken{0}); + JS::Rooted<JSObject*> newGlobal( + cx, JS_NewGlobalObject(cx, getGlobalClass(), nullptr, + JS::FireOnNewGlobalHook, options)); + ASSERT_TRUE(newGlobal); + JS::Compartment* newCompartment = nullptr; + { + JSAutoRealm ar(cx, newGlobal); + ASSERT_TRUE(JS::InitRealmStandardClasses(cx)); + 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.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/devtools/shared/heapsnapshot/tests/gtest/DoesntCrossCompartmentBoundaries.cpp b/devtools/shared/heapsnapshot/tests/gtest/DoesntCrossCompartmentBoundaries.cpp new file mode 100644 index 0000000000..6b506aabff --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/gtest/DoesntCrossCompartmentBoundaries.cpp @@ -0,0 +1,61 @@ +/* -*- 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::RealmOptions options; + // dummy + options.behaviors().setReduceTimerPrecisionCallerType( + JS::RTPCallerTypeToken{0}); + JS::Rooted<JSObject*> newGlobal( + cx, JS_NewGlobalObject(cx, getGlobalClass(), nullptr, + JS::FireOnNewGlobalHook, options)); + ASSERT_TRUE(newGlobal); + JS::Compartment* newCompartment = nullptr; + { + JSAutoRealm ar(cx, newGlobal); + ASSERT_TRUE(JS::InitRealmStandardClasses(cx)); + 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.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/devtools/shared/heapsnapshot/tests/gtest/SerializesEdgeNames.cpp b/devtools/shared/heapsnapshot/tests/gtest/SerializesEdgeNames.cpp new file mode 100644 index 0000000000..ab47941e39 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/gtest/SerializesEdgeNames.cpp @@ -0,0 +1,49 @@ +/* -*- 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/devtools/shared/heapsnapshot/tests/gtest/SerializesEverythingInHeapGraphOnce.cpp b/devtools/shared/heapsnapshot/tests/gtest/SerializesEverythingInHeapGraphOnce.cpp new file mode 100644 index 0000000000..d71c86703c --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/gtest/SerializesEverythingInHeapGraphOnce.cpp @@ -0,0 +1,34 @@ +/* -*- 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/devtools/shared/heapsnapshot/tests/gtest/SerializesTypeNames.cpp b/devtools/shared/heapsnapshot/tests/gtest/SerializesTypeNames.cpp new file mode 100644 index 0000000000..4c29b28832 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/gtest/SerializesTypeNames.cpp @@ -0,0 +1,27 @@ +/* -*- 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/devtools/shared/heapsnapshot/tests/gtest/moz.build b/devtools/shared/heapsnapshot/tests/gtest/moz.build new file mode 100644 index 0000000000..880d7b334e --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/gtest/moz.build @@ -0,0 +1,32 @@ +# -*- 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 += [ + "../..", +] + +DEFINES["GOOGLE_PROTOBUF_NO_RTTI"] = True +DEFINES["GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER"] = True + +UNIFIED_SOURCES = [ + "DeserializedNodeUbiNodes.cpp", + "DeserializedStackFrameUbiStackFrames.cpp", + "DevTools.cpp", + "DoesCrossCompartmentBoundaries.cpp", + "DoesntCrossCompartmentBoundaries.cpp", + "SerializesEdgeNames.cpp", + "SerializesEverythingInHeapGraphOnce.cpp", + "SerializesTypeNames.cpp", +] + +# THE MOCK_METHOD2 macro from gtest triggers this clang warning and it's hard +# to work around, so we just ignore it. +if CONFIG["CC_TYPE"] == "clang": + CXXFLAGS += ["-Wno-inconsistent-missing-override"] + +FINAL_LIBRARY = "xul-gtest" diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/.eslintrc.js b/devtools/shared/heapsnapshot/tests/xpcshell/.eslintrc.js new file mode 100644 index 0000000000..8611c174f5 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/.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/devtools/shared/heapsnapshot/tests/xpcshell/Census.sys.mjs b/devtools/shared/heapsnapshot/tests/xpcshell/Census.sys.mjs new file mode 100644 index 0000000000..0e86f8b055 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/Census.sys.mjs @@ -0,0 +1,176 @@ +// Functions for checking results returned by +// Debugger.Memory.prototype.takeCensus and +// HeapSnapshot.prototype.takeCensus. Adapted from js/src/jit-test/lib/census.js. + +export 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 (const 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") { + const unvisited = new Set(Object.getOwnPropertyNames(basis)); + return { + enter: prop => { + unvisited.delete(prop); + if (prop in basis) { + return makeWalker(basis[prop]); + } + + return extra(prop); + }, + + done: () => unvisited.forEach(prop => missing(prop, basis[prop])), + check: expectedObject, + }; + } + + 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, base) => ok(Math.abs(subject - base) <= fudge), + missing: missingProp, + extra: () => Census.walkAnything, + })(basis); +}; diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/Match.sys.mjs b/devtools/shared/heapsnapshot/tests/xpcshell/Match.sys.mjs new file mode 100644 index 0000000000..76312db86a --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/Match.sys.mjs @@ -0,0 +1,218 @@ +// A little pattern-matching library. +// +// Ported from js/src/tests/js1_8_5/reflect-parse/Match.js for use with devtools +// server xpcshell tests. + +export const 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(act) { + return match(act, this.template); + }, + + matches(act) { + try { + return this.match(act); + } catch (e) { + if (e instanceof MatchError) { + return false; + } + } + return false; + }, + + assert(act, message) { + try { + return this.match(act); + } catch (e) { + if (e instanceof MatchError) { + throw new Error((message || "failed match") + ": " + e.message); + } + } + return false; + }, + + 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)); + } + }; + + const quote = uneval; + + function MatchError(msg) { + this.message = msg; + } + + MatchError.prototype = { + toString() { + 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 (const 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)); + } + + const length = exp.length; + if (act.length !== exp.length) { + throw new MatchError( + "expected array-like object of length " + length + ", got " + quote(act) + ); + } + + for (let 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, MatchError }; +})(); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/dominator-tree-worker.js b/devtools/shared/heapsnapshot/tests/xpcshell/dominator-tree-worker.js new file mode 100644 index 0000000000..c636226101 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/dominator-tree-worker.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env worker */ + +"use strict"; + +console.log("Initializing worker."); + +self.onmessage = e => { + console.log("Starting test."); + try { + const path = ChromeUtils.saveHeapSnapshot({ runtime: true }); + const snapshot = ChromeUtils.readHeapSnapshot(path); + + const dominatorTree = snapshot.computeDominatorTree(); + ok(dominatorTree); + ok(DominatorTree.isInstance(dominatorTree)); + + let threw = false; + try { + new DominatorTree(); + } catch (excp) { + threw = true; + } + ok(threw, "Constructor shouldn't be usable"); + } catch (ex) { + ok( + false, + "Unexpected error inside worker:\n" + ex.toString() + "\n" + ex.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/devtools/shared/heapsnapshot/tests/xpcshell/head_heapsnapshot.js b/devtools/shared/heapsnapshot/tests/xpcshell/head_heapsnapshot.js new file mode 100644 index 0000000000..d51decb104 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/head_heapsnapshot.js @@ -0,0 +1,554 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; +/* exported Cr, CC, Match, Census, Task, DevToolsUtils, HeapAnalysesClient, + assertThrows, getFilePath, saveHeapSnapshotAndTakeCensus, + saveHeapSnapshotAndComputeDominatorTree, compareCensusViewData, assertDiff, + assertLabelAndShallowSize, makeTestDominatorTreeNode, + assertDominatorTreeNodeInsertion, assertDeduplicatedPaths, + assertCountToBucketBreakdown, pathEntry */ + +var CC = Components.Constructor; + +const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); +const { Match } = ChromeUtils.importESModule("resource://test/Match.sys.mjs"); +const { Census } = ChromeUtils.importESModule("resource://test/Census.sys.mjs"); +const { addDebuggerToGlobal } = ChromeUtils.importESModule( + "resource://gre/modules/jsdebugger.sys.mjs" +); + +const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); +const HeapAnalysesClient = require("resource://devtools/shared/heapsnapshot/HeapAnalysesClient.js"); +const { + censusReportToCensusTreeNode, +} = require("resource://devtools/shared/heapsnapshot/census-tree-node.js"); +const CensusUtils = require("resource://devtools/shared/heapsnapshot/CensusUtils.js"); +const DominatorTreeNode = require("resource://devtools/shared/heapsnapshot/DominatorTreeNode.js"); +const { + deduplicatePaths, +} = require("resource://devtools/shared/heapsnapshot/shortest-paths.js"); +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); + Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.debugger.log"); + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); + }); +} + +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 = info; + } + 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 assertThrows(f, val, msg) { + let fullmsg; + try { + f(); + } catch (exc) { + if (exc === val && (val !== 0 || 1 / exc === 1 / val)) { + return; + } else if (exc instanceof Error && exc.message === 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( + name, + allowMissing = false, + usePlatformPathSeparator = false +) { + const file = do_get_file(name, allowMissing); + 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 (usePlatformPathSeparator && 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( + HeapSnapshot.isInstance(snapshot), + "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.isInstance(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()}'>`; + } + 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 (const key of actual.keys()) { + ok( + expectedKeys.has(key), + `${path}: every key in actual is 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 (const 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 (const 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, givenDescription, 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); + Assert.strictEqual( + nodeSet.size, + nodes.length, + "each returned node should be unique" + ); + + for (const node of nodes) { + ok(expectedNodeSet.has(node), `the ${node} node was expected`); + } + + for (const expectedEdge of expectedEdges) { + let count = 0; + for (const 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(expectedEdge) + ); + } +} + +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/devtools/shared/heapsnapshot/tests/xpcshell/heap-snapshot-worker.js b/devtools/shared/heapsnapshot/tests/xpcshell/heap-snapshot-worker.js new file mode 100644 index 0000000000..a79f442193 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/heap-snapshot-worker.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env worker */ + +"use strict"; + +console.log("Initializing worker."); + +self.onmessage = ex => { + console.log("Starting test."); + try { + ok(ChromeUtils, "Should have access to ChromeUtils in a worker."); + ok(HeapSnapshot, "Should have access to HeapSnapshot in a worker."); + + 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( + HeapSnapshot.isInstance(snapshot), + "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/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_01.js new file mode 100644 index 0000000000..7d4560e6dc --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_01.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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 }, + }, + domNode: { + by: "descriptiveType", + then: { by: "count", count: true, bytes: true }, + }, +}; + +const description = { + objects: { + Function: { count: 1, bytes: 32 }, + other: { count: 0, bytes: 0 }, + }, + strings: {}, + scripts: {}, + other: {}, + domNode: {}, +}; + +const expected = ["objects", "Function"]; + +const shallowSize = 32; + +function run_test() { + assertLabelAndShallowSize(breakdown, description, shallowSize, expected); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_02.js new file mode 100644 index 0000000000..cd424afdd0 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_02.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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 }, + }, + domNode: { + by: "descriptiveType", + then: { by: "count", count: true, bytes: true }, + }, +}; + +const description = { + objects: { + other: { count: 1, bytes: 10 }, + }, + strings: {}, + scripts: {}, + other: {}, + domNode: {}, +}; + +const expected = ["objects", "other"]; + +const shallowSize = 10; + +function run_test() { + assertLabelAndShallowSize(breakdown, description, shallowSize, expected); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_03.js new file mode 100644 index 0000000000..098e3efc4f --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_03.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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 }, + }, + domNode: { + by: "descriptiveType", + then: { by: "count", count: true, bytes: true }, + }, +}; + +const description = { + objects: { + other: { count: 0, bytes: 0 }, + }, + strings: { + JSString: { count: 1, bytes: 42 }, + }, + scripts: {}, + other: {}, + domNode: {}, +}; + +const expected = ["strings", "JSString"]; + +const shallowSize = 42; + +function run_test() { + assertLabelAndShallowSize(breakdown, description, shallowSize, expected); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_04.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_04.js new file mode 100644 index 0000000000..a087c39a2a --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_04.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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 }, + }, + domNode: { + by: "descriptiveType", + 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: {}, + domNode: {}, +}; + +const expected = ["objects", "Array", stack]; + +const shallowSize = 512; + +function run_test() { + assertLabelAndShallowSize(breakdown, description, shallowSize, expected); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_attachShortestPaths_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_attachShortestPaths_01.js new file mode 100644 index 0000000000..07894c67b1 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_attachShortestPaths_01.js @@ -0,0 +1,141 @@ +/* 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/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_getNodeByIdAlongPath_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_getNodeByIdAlongPath_01.js new file mode 100644 index 0000000000..be1f210c3e --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_getNodeByIdAlongPath_01.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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 (const { 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/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_01.js new file mode 100644 index 0000000000..7567c473e0 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_01.js @@ -0,0 +1,119 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_02.js new file mode 100644 index 0000000000..b0b80c3c95 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_02.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_03.js new file mode 100644 index 0000000000..552ed72735 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_03.js @@ -0,0 +1,124 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_partialTraversal_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_partialTraversal_01.js new file mode 100644 index 0000000000..6da0587f57 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_partialTraversal_01.js @@ -0,0 +1,150 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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 } }, + domNode: { count: 0, bytes: 0 }, + }), +}; + +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 }, + }, + domNode: { 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/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_01.js new file mode 100644 index 0000000000..f98094fd32 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_01.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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.isInstance(dominatorTree)); + + let threw = false; + try { + new DominatorTree(); + } catch (e) { + threw = true; + } + ok(threw, "Constructor shouldn't be usable"); + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_02.js new file mode 100644 index 0000000000..525e031ccf --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_02.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that we can compute dominator trees from a snapshot in a worker. + +add_task(async 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++; + }; + + await waitForDone(worker); + + Assert.greater(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); + resolve(); + } + }); + }); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_03.js new file mode 100644 index 0000000000..d304845328 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_03.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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" + ); + Assert.greaterOrEqual(dominatorTree.root, 0, "root should be positive"); + Assert.lessOrEqual( + dominatorTree.root, + Math.pow(2, 48), + "root should be less than or equal to 2^48" + ); + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_04.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_04.js new file mode 100644 index 0000000000..2bfe46977f --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_04.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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"); + Assert.greater(size, 0, "size should be positive"); + Assert.lessOrEqual( + 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/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_05.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_05.js new file mode 100644 index 0000000000..23abf1bd74 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_05.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that we can get the set of immediately dominated nodes for any given +// node and that this forms a tree. + +function run_test() { + const 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. + + const root = dominatorTree.root; + equal( + dominatorTree.getImmediateDominator(root), + null, + "The root should not have a parent" + ); + + const seen = new Set(); + const stack = [root]; + while (stack.length) { + const 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); + } + + const newNodes = dominatorTree.getImmediatelyDominated(top); + if (Object.prototype.toString.call(newNodes) !== "[object Array]") { + ok( + false, + "getImmediatelyDominated should return an array for known node ids" + ); + } + + const topSize = dominatorTree.getRetainedSize(top); + + let lastSize = Infinity; + for (let 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"); + } + + const 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/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_06.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_06.js new file mode 100644 index 0000000000..fdd4191c5f --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_06.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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); + } +} + +const COUNT = { by: "count", count: false, bytes: true }; + +function run_test() { + const path = saveNewHeapSnapshot(); + const snapshot = ChromeUtils.readHeapSnapshot(path); + const dominatorTree = snapshot.computeDominatorTree(); + + // Do a traversal of the dominator tree and assert the relationship between + // retained size, shallow size, and children's retained sizes. + + const root = dominatorTree.root; + const stack = [root]; + while (stack.length) { + const top = stack.pop(); + + const children = dominatorTree.getImmediatelyDominated(top); + + const topRetainedSize = dominatorTree.getRetainedSize(top); + const topShallowSize = snapshot.describeNode(COUNT, top).bytes; + fastAssert( + topShallowSize <= topRetainedSize, + "The shallow size should be less than or equal to the " + "retained size" + ); + + let sumOfChildrensRetainedSizes = 0; + for (let 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/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_computeDominatorTree_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_computeDominatorTree_01.js new file mode 100644 index 0000000000..ba622f55df --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_computeDominatorTree_01.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test the HeapAnalyses{Client,Worker} "computeDominatorTree" request. + +add_task(async function () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + await client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + const dominatorTreeId = await client.computeDominatorTree(snapshotFilePath); + equal( + typeof dominatorTreeId, + "number", + "should get a dominator tree id, and it should be a number" + ); + + client.destroy(); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_computeDominatorTree_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_computeDominatorTree_02.js new file mode 100644 index 0000000000..c3ee76be13 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_computeDominatorTree_02.js @@ -0,0 +1,20 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test the HeapAnalyses{Client,Worker} "computeDominatorTree" request with bad +// file paths. + +add_task(async function () { + const client = new HeapAnalysesClient(); + + let threw = false; + try { + await client.computeDominatorTree("/etc/passwd"); + } catch (_) { + threw = true; + } + ok(threw, "should throw when given a bad path"); + + client.destroy(); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_01.js new file mode 100644 index 0000000000..b85e4b19fc --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_01.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the HeapAnalyses{Client,Worker} can delete heap snapshots. + +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(async function () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + await client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + const dominatorTreeId = await client.computeDominatorTree(snapshotFilePath); + ok(true, "Should have computed the dominator tree"); + + await client.deleteHeapSnapshot(snapshotFilePath); + ok(true, "Should have deleted the snapshot"); + + let threw = false; + try { + await client.getDominatorTree({ + dominatorTreeId, + breakdown, + }); + } catch (_) { + threw = true; + } + ok(threw, "getDominatorTree on deleted tree should throw an error"); + + threw = false; + try { + await client.computeDominatorTree(snapshotFilePath); + } catch (_) { + threw = true; + } + ok(threw, "computeDominatorTree on deleted snapshot should throw an error"); + + threw = false; + try { + await client.takeCensus(snapshotFilePath); + } catch (_) { + threw = true; + } + ok(threw, "takeCensus on deleted tree should throw an error"); + + client.destroy(); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_02.js new file mode 100644 index 0000000000..6960081afd --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_02.js @@ -0,0 +1,19 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test deleteHeapSnapshot is a noop if the provided path matches no snapshot + +add_task(async function () { + const client = new HeapAnalysesClient(); + + let threw = false; + try { + await 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/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_03.js new file mode 100644 index 0000000000..c84622f633 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_03.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test other dominatorTrees can still be retrieved after deleting a snapshot + +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 }, + domNode: { by: "count", count: true, bytes: true }, +}; + +async function createSnapshotAndDominatorTree(client) { + const snapshotFilePath = saveNewHeapSnapshot(); + await client.readHeapSnapshot(snapshotFilePath); + const dominatorTreeId = await client.computeDominatorTree(snapshotFilePath); + return { dominatorTreeId, snapshotFilePath }; +} + +add_task(async function () { + const client = new HeapAnalysesClient(); + + const savedSnapshots = [ + await createSnapshotAndDominatorTree(client), + await createSnapshotAndDominatorTree(client), + await createSnapshotAndDominatorTree(client), + ]; + ok(true, "Create 3 snapshots and dominator trees"); + + await client.deleteHeapSnapshot(savedSnapshots[1].snapshotFilePath); + ok(true, "Snapshot deleted"); + + let tree = await client.getDominatorTree({ + dominatorTreeId: savedSnapshots[0].dominatorTreeId, + breakdown, + }); + ok(tree, "Should get a valid tree for first snapshot"); + + let threw = false; + try { + await client.getDominatorTree({ + dominatorTreeId: savedSnapshots[1].dominatorTreeId, + breakdown, + }); + } catch (_) { + threw = true; + } + ok(threw, "getDominatorTree on a deleted snapshot should throw an error"); + + tree = await client.getDominatorTree({ + dominatorTreeId: savedSnapshots[2].dominatorTreeId, + breakdown, + }); + ok(tree, "Should get a valid tree for third snapshot"); + + client.destroy(); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getCensusIndividuals_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getCensusIndividuals_01.js new file mode 100644 index 0000000000..942547835b --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getCensusIndividuals_01.js @@ -0,0 +1,111 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the HeapAnalyses{Client,Worker} can get census individuals. + +const COUNT = { by: "count", count: true, bytes: true }; + +const CENSUS_BREAKDOWN = { + by: "coarseType", + objects: COUNT, + strings: COUNT, + scripts: COUNT, + other: COUNT, + domNode: COUNT, +}; + +const LABEL_BREAKDOWN = { + by: "internalType", + then: COUNT, +}; + +const MAX_INDIVIDUALS = 10; + +add_task(async function () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + await client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + const dominatorTreeId = await client.computeDominatorTree(snapshotFilePath); + ok(true, "Should have computed dominator tree"); + + const { report } = await client.takeCensus( + snapshotFilePath, + { breakdown: CENSUS_BREAKDOWN }, + { asTreeNode: true } + ); + ok(report, "Should get a report"); + + let nodesWithLeafIndicesFound = 0; + + await (async function assertCanGetIndividuals(censusNode) { + if (censusNode.reportLeafIndex !== undefined) { + nodesWithLeafIndicesFound++; + + const response = await 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 (const individual of response.nodes) { + equal( + typeof individual.nodeId, + "number", + "individual.nodeId should be a number" + ); + Assert.lessOrEqual( + 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 (const child of censusNode.children) { + await assertCanGetIndividuals(child); + } + } + })(report); + + equal( + nodesWithLeafIndicesFound, + 4, + "Should have found a leaf for each coarse type" + ); + + client.destroy(); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getCreationTime_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getCreationTime_01.js new file mode 100644 index 0000000000..b52280d970 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getCreationTime_01.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the HeapAnalyses{Client,Worker} can get a HeapSnapshot's +// creation time. + +function waitForThirtyMilliseconds() { + const start = Date.now(); + while (Date.now() - start < 30) { + // do nothing + } +} + +const BREAKDOWN = { + by: "internalType", + then: { by: "count", count: true, bytes: true }, +}; + +add_task(async 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; + + await client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + let threw = false; + try { + await client.getCreationTime("/not/a/real/path", { + breakdown: BREAKDOWN, + }); + } catch (_) { + threw = true; + } + ok(threw, "getCreationTime should throw when snapshot does not exist"); + + const time = await client.getCreationTime(snapshotFilePath, { + breakdown: BREAKDOWN, + }); + + dumpn("Start = " + start); + dumpn("End = " + end); + dumpn("Time = " + time); + + Assert.greaterOrEqual(time, start, "creation time occurred after start"); + Assert.lessOrEqual(time, end, "creation time occurred before end"); + + client.destroy(); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getDominatorTree_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getDominatorTree_01.js new file mode 100644 index 0000000000..9035624ca2 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getDominatorTree_01.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test the HeapAnalyses{Client,Worker} "getDominatorTree" request. + +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 }, + domNode: { by: "count", count: true, bytes: true }, +}; + +add_task(async function () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + await client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + const dominatorTreeId = await client.computeDominatorTree(snapshotFilePath); + equal( + typeof dominatorTreeId, + "number", + "should get a dominator tree id, and it should be a number" + ); + + const partialTree = await 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/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getDominatorTree_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getDominatorTree_02.js new file mode 100644 index 0000000000..83a7cbbd3d --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getDominatorTree_02.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test the HeapAnalyses{Client,Worker} "getDominatorTree" request with bad +// dominator tree ids. + +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(async function () { + const client = new HeapAnalysesClient(); + + let threw = false; + try { + await client.getDominatorTree({ dominatorTreeId: 42, breakdown }); + } catch (_) { + threw = true; + } + ok(threw, "should throw when given a bad id"); + + client.destroy(); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getImmediatelyDominated_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getImmediatelyDominated_01.js new file mode 100644 index 0000000000..47860870cb --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getImmediatelyDominated_01.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test the HeapAnalyses{Client,Worker} "getImmediatelyDominated" request. + +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 }, + domNode: { by: "count", count: true, bytes: true }, +}; + +add_task(async function () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + await client.readHeapSnapshot(snapshotFilePath); + const dominatorTreeId = await client.computeDominatorTree(snapshotFilePath); + + const partialTree = await client.getDominatorTree({ + dominatorTreeId, + breakdown, + }); + ok( + !!partialTree.children.length, + "root should immediately dominate some nodes" + ); + + // First, test getting a subset of children available. + const response = await 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 (const 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 = await 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 (const 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/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_readHeapSnapshot_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_readHeapSnapshot_01.js new file mode 100644 index 0000000000..dea8269a92 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_readHeapSnapshot_01.js @@ -0,0 +1,15 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the HeapAnalyses{Client,Worker} can read heap snapshots. + +add_task(async function () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + await client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + client.destroy(); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensusDiff_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensusDiff_01.js new file mode 100644 index 0000000000..9703229438 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensusDiff_01.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the HeapAnalyses{Client,Worker} can take diffs between censuses. + +const BREAKDOWN = { + by: "objectClass", + then: { by: "count", count: true, bytes: false }, + other: { by: "count", count: true, bytes: false }, +}; + +add_task(async 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(); + + await client.readHeapSnapshot(firstSnapshotFilePath); + await client.readHeapSnapshot(secondSnapshotFilePath); + ok(true, "Should have read both heap snapshot files"); + + const { delta } = await client.takeCensusDiff( + firstSnapshotFilePath, + secondSnapshotFilePath, + { breakdown: BREAKDOWN } + ); + + equal( + delta.AllocationMarker.count, + 1, + "There exists one new AllocationMarker in the second heap snapshot" + ); + + const { delta: deltaTreeNode } = await 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/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensusDiff_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensusDiff_02.js new file mode 100644 index 0000000000..d5d988f78f --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensusDiff_02.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the HeapAnalyses{Client,Worker} can take diffs between censuses as +// inverted trees. + +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 }, + }, + domNode: { + by: "descriptiveType", + then: { by: "count", count: true, bytes: true }, + }, +}; + +add_task(async function () { + const firstSnapshotFilePath = saveNewHeapSnapshot(); + const secondSnapshotFilePath = saveNewHeapSnapshot(); + + const client = new HeapAnalysesClient(); + await client.readHeapSnapshot(firstSnapshotFilePath); + await client.readHeapSnapshot(secondSnapshotFilePath); + + ok(true, "Should have read both heap snapshot files"); + + const { delta } = await client.takeCensusDiff( + firstSnapshotFilePath, + secondSnapshotFilePath, + { breakdown: BREAKDOWN } + ); + + const { delta: deltaTreeNode } = await 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/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_01.js new file mode 100644 index 0000000000..55da2bf4b8 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_01.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the HeapAnalyses{Client,Worker} can take censuses. + +add_task(async function () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + await client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + const { report } = await 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/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_02.js new file mode 100644 index 0000000000..5b1dcefe03 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_02.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the HeapAnalyses{Client,Worker} can take censuses with breakdown +// options. + +add_task(async function () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + await client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + const { report } = await 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/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_03.js new file mode 100644 index 0000000000..0dfda73f1f --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_03.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the HeapAnalyses{Client,Worker} bubbles errors properly when things +// go wrong. + +add_task(async function () { + const client = new HeapAnalysesClient(); + + // Snapshot file path to a file that doesn't exist. + let failed = false; + try { + await 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 { + await 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(); + await client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + // Bad census breakdown options. + failed = false; + try { + await 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/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_04.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_04.js new file mode 100644 index 0000000000..26f78ad4f7 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_04.js @@ -0,0 +1,133 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the HeapAnalyses{Client,Worker} can send SavedFrame stacks from +// by-allocation-stack reports from the worker. + +add_task(async 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 + `); + + // Create one allocationMarker with tracking turned off, + // so it will have no associated stack. + g.f(); + + dbg.memory.allocationSamplingProbability = 1; + + for (const [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 }); + await 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 } = await 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(Object.getPrototypeOf(map).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. + const 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/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_05.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_05.js new file mode 100644 index 0000000000..951a3b3133 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_05.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the HeapAnalyses{Client,Worker} can take censuses and return +// a CensusTreeNode. + +const BREAKDOWN = { + by: "internalType", + then: { by: "count", count: true, bytes: true }, +}; + +add_task(async function () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + await client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + const { report } = await client.takeCensus(snapshotFilePath, { + breakdown: BREAKDOWN, + }); + + const { report: treeNode } = await client.takeCensus( + snapshotFilePath, + { + breakdown: BREAKDOWN, + }, + { + asTreeNode: true, + } + ); + + ok(!!treeNode.children.length, "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/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_06.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_06.js new file mode 100644 index 0000000000..d9fdaa3708 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_06.js @@ -0,0 +1,114 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the HeapAnalyses{Client,Worker} can take censuses by +// "allocationStack" and return a CensusTreeNode. + +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(async 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(); + await client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + const { report } = await client.takeCensus(snapshotFilePath, { + breakdown: BREAKDOWN, + }); + + const { report: treeNode } = await 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/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_07.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_07.js new file mode 100644 index 0000000000..3f603122d3 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_07.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the HeapAnalyses{Client,Worker} can take censuses and return +// an inverted CensusTreeNode. + +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 }, + }, + domNode: { + by: "descriptiveType", + then: { by: "count", count: true, bytes: true }, + }, +}; + +add_task(async function () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + await client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + const { report } = await client.takeCensus(snapshotFilePath, { + breakdown: BREAKDOWN, + }); + + const { report: treeNode } = await client.takeCensus( + snapshotFilePath, + { + breakdown: BREAKDOWN, + }, + { + asInvertedTreeNode: true, + } + ); + + compareCensusViewData(BREAKDOWN, report, treeNode, { invert: true }); + + client.destroy(); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_computeShortestPaths_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_computeShortestPaths_01.js new file mode 100644 index 0000000000..354aa129a0 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_computeShortestPaths_01.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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); + Assert.strictEqual(shortestPaths.size, targetSet.size); + + for (const [target, paths] of shortestPaths) { + ok(targetSet.has(target), "We should only get paths for our targets"); + targetSet.delete(target); + + ok( + !!paths.length, + "We must have at least one path, since the target is dominated by the root" + ); + Assert.lessOrEqual( + paths.length, + 2, + "Should not have recorded more paths than the max requested" + ); + + dumpn("---------------------"); + dumpn("Shortest paths for 0x" + target.toString(16) + ":"); + for (const pth of paths) { + dumpn(" path ="); + for (const part of pth) { + dumpn( + " predecessor: 0x" + + part.predecessor.toString(16) + + "; edge: " + + part.edge + ); + } + } + dumpn("---------------------"); + + for (const path2 of paths) { + ok(!!path2.length, "Cannot have zero length paths"); + Assert.strictEqual( + path2[0].predecessor, + dominatorTree.root, + "The first predecessor is always our start node" + ); + + for (const part of path2) { + 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"); + } + } + } + + Assert.strictEqual( + targetSet.size, + 0, + "We found paths for all of our targets" + ); + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_computeShortestPaths_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_computeShortestPaths_02.js new file mode 100644 index 0000000000..714986c601 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_computeShortestPaths_02.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_creationTime_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_creationTime_01.js new file mode 100644 index 0000000000..e18c79752f --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_creationTime_01.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// HeapSnapshot.prototype.creationTime returns the expected time. + +function waitForThirtyMilliseconds() { + const start = Date.now(); + while (Date.now() - start < 30) { + // do nothing + } +} + +function run_test() { + const start = Date.now() * 1000; + info("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; + info("end = " + end); + + const snapshot = ChromeUtils.readHeapSnapshot(path); + info("snapshot.creationTime = " + snapshot.creationTime); + + Assert.greaterOrEqual(snapshot.creationTime, start); + Assert.lessOrEqual(snapshot.creationTime, end); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_deepStack_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_deepStack_01.js new file mode 100644 index 0000000000..7f88d0a142 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_deepStack_01.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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() { + Services.prefs.setBoolPref( + "security.allow_parent_unrestricted_js_loads", + true + ); + Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); + Services.prefs.setBoolPref("security.allow_eval_in_parent_process", true); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_parent_unrestricted_js_loads"); + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); + Services.prefs.clearUserPref("security.allow_eval_in_parent_process"); + }); + + // 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(HeapSnapshot.isInstance(snapshot), "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); + Assert.lessOrEqual( + 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/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_describeNode_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_describeNode_01.js new file mode 100644 index 0000000000..8597fb4edf --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_describeNode_01.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_getObjectNodeId_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_getObjectNodeId_01.js new file mode 100644 index 0000000000..26997dd904 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_getObjectNodeId_01.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test ChromeUtils.getObjectNodeId() + +function run_test() { + // Create a test object, which we want to analyse + const testObject = { + foo: { + bar: {}, + }, + }; + + const path = ChromeUtils.saveHeapSnapshot({ runtime: true }); + const snapshot = ChromeUtils.readHeapSnapshot(path); + + // Get the NodeId for our test object + const objectNodeIdRoot = ChromeUtils.getObjectNodeId(testObject); + const objectNodeIdFoo = ChromeUtils.getObjectNodeId(testObject.foo); + const objectNodeIdBar = ChromeUtils.getObjectNodeId(testObject.foo.bar); + + // Also try to ensure that this is the right object via its retained path + const shortestPaths = snapshot.computeShortestPaths( + objectNodeIdRoot, + [objectNodeIdBar], + 50 + ); + ok(shortestPaths); + ok(shortestPaths instanceof Map); + Assert.equal( + shortestPaths.size, + 1, + "We get only one path between the root object and bar object" + ); + + const paths = shortestPaths.get(objectNodeIdBar); + Assert.equal(paths.length, 1, "There is only one path between root and bar"); + Assert.equal( + paths[0].length, + 2, + "The shortest path is made of two edges: foo and bar" + ); + + const [path1, path2] = paths[0]; + Assert.equal( + path1.predecessor, + objectNodeIdRoot, + "The first edge goes from the root object" + ); + Assert.equal(path1.edge, "foo", "The first edge is the foo attribute"); + + Assert.equal( + path2.predecessor, + objectNodeIdFoo, + "The second edge goes from the foo object" + ); + Assert.equal(path2.edge, "bar", "The first edge is the bar attribute"); + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_01.js new file mode 100644 index 0000000000..f4a7836be3 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_01.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// HeapSnapshot.prototype.takeCensus returns a value of an appropriate +// shape. Ported from js/src/jit-tests/debug/Memory-takeCensus-01.js + +function run_test() { + const dbg = new Debugger(); + + function checkProperties(census) { + equal(typeof census, "object"); + for (const prop of Object.getOwnPropertyNames(census)) { + const 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)); + + const g = newGlobal(); + dbg.addDebuggee(g); + checkProperties(saveHeapSnapshotAndTakeCensus(dbg)); + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_02.js new file mode 100644 index 0000000000..a26e9a96d8 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_02.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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. + const dbg = new Debugger(); + const census0 = saveHeapSnapshotAndTakeCensus(dbg); + Census.walkCensus(census0, "census0", Census.assertAllZeros); + + function newGlobalWithDefs() { + const 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. + const 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, () => () => {});"); + + const 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/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_03.js new file mode 100644 index 0000000000..06c574d4e0 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_03.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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() { + const dbg = new Debugger(); + + const census0 = saveHeapSnapshotAndTakeCensus(dbg); + Census.walkCensus(census0, "census0", Census.assertAllZeros); + + const g1 = newGlobal(); + dbg.addDebuggee(g1); + const census1 = saveHeapSnapshotAndTakeCensus(dbg); + Census.walkCensus(census1, "census1", Census.assertAllNotLessThan(census0)); + + const g2 = newGlobal(); + dbg.addDebuggee(g2); + const census2 = saveHeapSnapshotAndTakeCensus(dbg); + Census.walkCensus(census2, "census2", Census.assertAllNotLessThan(census1)); + + dbg.removeDebuggee(g2); + const census3 = saveHeapSnapshotAndTakeCensus(dbg); + Census.walkCensus(census3, "census3", Census.assertAllEqual(census1)); + + dbg.removeDebuggee(g1); + const census4 = saveHeapSnapshotAndTakeCensus(dbg); + Census.walkCensus(census4, "census4", Census.assertAllEqual(census0)); + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_04.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_04.js new file mode 100644 index 0000000000..ac484c8815 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_04.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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() { + const g = newGlobal(); + const 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." + ); + + let 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/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_05.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_05.js new file mode 100644 index 0000000000..22c5324c68 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_05.js @@ -0,0 +1,31 @@ +/* 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 + +/* eslint-disable strict */ +function run_test() { + const g = newGlobal(); + const 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/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_06.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_06.js new file mode 100644 index 0000000000..2b639411f9 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_06.js @@ -0,0 +1,127 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Check HeapSnapshot.prototype.takeCensus handling of 'breakdown' argument. +// +// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-06.js + +function run_test() { + const Pattern = Match.Pattern; + + const g = newGlobal(); + const 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 }, + DebuggerPrototype: { count: Pattern.NATURAL }, + Sandbox: { 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 }, + }).assert( + saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "internalType" } }) + ); + + // Nested breakdowns. + + const 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, + }).assert( + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { by: "internalType", then: { by: "coarseType" } }, + }) + ); + + Pattern({ + Function: { count: Pattern.NATURAL }, + Object: { count: Pattern.NATURAL }, + DebuggerPrototype: { 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/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_07.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_07.js new file mode 100644 index 0000000000..56bec51074 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_07.js @@ -0,0 +1,124 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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() { + const g = newGlobal(); + const dbg = new Debugger(g); + + assertThrows(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { + get by() { + throw Error("ಠ_ಠ"); + }, + }, + }); + }, "ಠ_ಠ"); + + assertThrows(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { + by: "count", + get count() { + throw Error("ಠ_ಠ"); + }, + }, + }); + }, "ಠ_ಠ"); + + assertThrows(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { + by: "count", + get bytes() { + throw Error("ಠ_ಠ"); + }, + }, + }); + }, "ಠ_ಠ"); + + assertThrows(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { + by: "objectClass", + get then() { + throw Error("ಠ_ಠ"); + }, + }, + }); + }, "ಠ_ಠ"); + + assertThrows(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { + by: "objectClass", + get other() { + throw Error("ಠ_ಠ"); + }, + }, + }); + }, "ಠ_ಠ"); + + assertThrows(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { + by: "coarseType", + get objects() { + throw Error("ಠ_ಠ"); + }, + }, + }); + }, "ಠ_ಠ"); + + assertThrows(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { + by: "coarseType", + get scripts() { + throw Error("ಠ_ಠ"); + }, + }, + }); + }, "ಠ_ಠ"); + + assertThrows(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { + by: "coarseType", + get strings() { + throw Error("ಠ_ಠ"); + }, + }, + }); + }, "ಠ_ಠ"); + + assertThrows(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { + by: "coarseType", + get other() { + throw Error("ಠ_ಠ"); + }, + }, + }); + }, "ಠ_ಠ"); + + assertThrows(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { + by: "internalType", + get then() { + throw Error("ಠ_ಠ"); + }, + }, + }); + }, "ಠ_ಠ"); + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_08.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_08.js new file mode 100644 index 0000000000..f40d367cd6 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_08.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// HeapSnapshot.prototype.takeCensus: test by: 'count' breakdown +// +// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-08.js + +function run_test() { + const g = newGlobal(); + const 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) { + const 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/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_09.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_09.js new file mode 100644 index 0000000000..99830d75cb --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_09.js @@ -0,0 +1,111 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// HeapSnapshot.prototype.takeCensus: by: allocationStack breakdown +// +// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-09.js + +function run_test() { + const g = newGlobal(); + const dbg = new Debugger(g); + + g.eval(` // 1 + var log = []; // 2 + function f() { log.push(allocationMarker()); } // 3 + function g() { f(); } // 4 + function h() { f(); } // 5 + `); + + // Create one allocationMarker with tracking turned off, + // so it will have no associated stack. + g.f(); + + dbg.memory.allocationSamplingProbability = 1; + + for (const [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; + } + } + + const census = saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { + by: "objectClass", + then: { + by: "allocationStack", + then: { by: "count", label: "haz stack" }, + noStack: { + by: "count", + label: "no haz stack", + }, + }, + }, + }); + + const 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. + const 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/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_10.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_10.js new file mode 100644 index 0000000000..9b0e0f8c74 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_10.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Check byte counts produced by takeCensus. +// +// Note that tracking allocation sites adds unique IDs to objects which +// increases their size, making it hard to test reported sizes exactly. +// +// Ported from js/src/jit-test/tests/debug/Memory-take Census-10.js + +function run_test() { + const g = newGlobal(); + const dbg = new Debugger(g); + + const 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, true); + 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 + `); + + 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, true); + seen++; + break; + + case 6: + equal(v.count, 10); + equal(v.bytes >= 10 * sizeOfAM, true); + 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/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_11.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_11.js new file mode 100644 index 0000000000..151437bb41 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_11.js @@ -0,0 +1,131 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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 + `); + dbg.memory.trackingAllocationSites = false; + + return { + live: dbg.memory.takeCensus(opts), + offline: saveHeapSnapshotAndTakeCensus(dbg, opts), + }; +} + +function getMarkerSize(g, dbg) { + dbg.memory.allocationSamplingProbability = 1; + dbg.memory.trackingAllocationSites = true; + g.eval("var hold = allocationMarker();"); + dbg.memory.trackingAllocationSites = false; + const live = dbg.memory.takeCensus({ + breakdown: { by: "objectClass", then: { by: "count" } }, + }); + g.hold = null; + equal(live.AllocationMarker.count, 1); + return live.AllocationMarker.bytes; +} + +function run_test() { + const g = newGlobal(); + const dbg = new Debugger(g); + + g.eval("this.markers = []"); + const markerSize = getMarkerSize(g, dbg); + + // First, test that we get the same counts and sizes as we allocate and retain + // more things. + + let prevCount = 0; + let prevBytes = 0; + + for (let 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; + } + 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/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_12.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_12.js new file mode 100644 index 0000000000..f3a72102b0 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_12.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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 (const k of Object.keys(byClassNameCount)) { + ok(keys.has(k), "Should not have any unexpected class names"); + } + + for (const key of Object.keys(byClassName)) { + equal( + byClassNameCount[key].count, + byClassName[key].length, + "Length of the bucket and count should be equal" + ); + + for (const 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/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot.js new file mode 100644 index 0000000000..b7a3c9c2f8 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot.js @@ -0,0 +1,22 @@ +/* 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. +/* eslint-disable strict */ +if (typeof Debugger != "function") { + const { addDebuggerToGlobal } = ChromeUtils.importESModule( + "resource://gre/modules/jsdebugger.sys.mjs" + ); + addDebuggerToGlobal(globalThis); +} + +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(HeapSnapshot.isInstance(snapshot), "Should be an instanceof HeapSnapshot"); + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_with_allocations.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_with_allocations.js new file mode 100644 index 0000000000..273736abcd --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_with_allocations.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that we can save a core dump with allocation stacks and read it back +// into a HeapSnapshot. + +if (typeof Debugger != "function") { + const { addDebuggerToGlobal } = ChromeUtils.importESModule( + "resource://gre/modules/jsdebugger.sys.mjs" + ); + addDebuggerToGlobal(globalThis); +} + +function run_test() { + Services.prefs.setBoolPref( + "security.allow_parent_unrestricted_js_loads", + true + ); + Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); + Services.prefs.setBoolPref("security.allow_eval_in_parent_process", true); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_parent_unrestricted_js_loads"); + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); + Services.prefs.clearUserPref("security.allow_eval_in_parent_process"); + }); + + // 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(HeapSnapshot.isInstance(snapshot), "Should be an instanceof HeapSnapshot"); + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_with_utf8_paths.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_with_utf8_paths.js new file mode 100644 index 0000000000..aecbd9cdda --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_with_utf8_paths.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we can read core dumps with a UTF8 path into HeapSnapshot instances. +/* eslint-disable strict */ +add_task(async function () { + const fileNameWithRussianCharacters = + "Снимок памяти Click.ru 08.06.2020 (Firefox dump).fxsnapshot"; + const filePathWithRussianCharacters = PathUtils.join( + PathUtils.tempDir, + fileNameWithRussianCharacters + ); + + const filePath = ChromeUtils.saveHeapSnapshot({ globals: [this] }); + ok(true, "Should be able to save a snapshot."); + + await IOUtils.copy(filePath, filePathWithRussianCharacters); + + ok( + await IOUtils.exists(filePathWithRussianCharacters), + `We could copy the file to the expected path ${filePathWithRussianCharacters}` + ); + + const snapshot = ChromeUtils.readHeapSnapshot(filePathWithRussianCharacters); + ok(snapshot, "Should be able to read a heap snapshot from an utf8 path"); + ok(HeapSnapshot.isInstance(snapshot), "Should be an instanceof HeapSnapshot"); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_worker.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_worker.js new file mode 100644 index 0000000000..d7427a3367 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_worker.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that we can read core dumps into HeapSnapshot instances in a worker. +add_task(async 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++; + }; + + await waitForDone(worker); + + Assert.greater(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); + resolve(); + } + }); + }); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_SaveHeapSnapshot.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_SaveHeapSnapshot.js new file mode 100644 index 0000000000..33367af7f7 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_SaveHeapSnapshot.js @@ -0,0 +1,121 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test the ChromeUtils interface. +// eslint-disable-next-line +if (typeof Debugger != "function") { + const { addDebuggerToGlobal } = ChromeUtils.importESModule( + "resource://gre/modules/jsdebugger.sys.mjs" + ); + addDebuggerToGlobal(globalThis); +} + +function run_test() { + ok(ChromeUtils, "Should be able to get the ChromeUtils interface"); + + testBadParameters(); + testGoodParameters(); + + do_test_finished(); +} + +function testBadParameters() { + Assert.throws( + () => ChromeUtils.saveHeapSnapshot(), + /NS_ERROR_ILLEGAL_VALUE/, + "Should throw if arguments aren't passed in." + ); + + Assert.throws( + () => ChromeUtils.saveHeapSnapshot(null), + /NS_ERROR_ILLEGAL_VALUE/, + "Should throw if boundaries isn't an object." + ); + + Assert.throws( + () => ChromeUtils.saveHeapSnapshot({}), + /NS_ERROR_ILLEGAL_VALUE/, + "Should throw if the boundaries object doesn't have any properties." + ); + + Assert.throws( + () => ChromeUtils.saveHeapSnapshot({ runtime: true, globals: [this] }), + /NS_ERROR_ILLEGAL_VALUE/, + "Should throw if the boundaries object has more than one property." + ); + + Assert.throws( + () => ChromeUtils.saveHeapSnapshot({ debugger: {} }), + /NS_ERROR_ILLEGAL_VALUE/, + "Should throw if the debuggees object is not a Debugger object" + ); + + Assert.throws( + () => ChromeUtils.saveHeapSnapshot({ globals: [{}] }), + /NS_ERROR_ILLEGAL_VALUE/, + "Should throw if the globals array contains non-global objects." + ); + + Assert.throws( + () => ChromeUtils.saveHeapSnapshot({ runtime: false }), + /NS_ERROR_ILLEGAL_VALUE/, + "Should throw if runtime is supplied and is not true." + ); + + Assert.throws( + () => ChromeUtils.saveHeapSnapshot({ globals: null }), + /TypeError:.*can't be converted to a sequence/, + "Should throw if globals is not an object." + ); + + Assert.throws( + () => ChromeUtils.saveHeapSnapshot({ globals: {} }), + /TypeError:.*can't be converted to a sequence/, + "Should throw if globals is not an array." + ); + + Assert.throws( + () => ChromeUtils.saveHeapSnapshot({ debugger: Debugger.prototype }), + /NS_ERROR_ILLEGAL_VALUE/, + "Should throw if debugger is the Debugger.prototype object." + ); + + Assert.throws( + () => + ChromeUtils.saveHeapSnapshot({ + get globals() { + return [this]; + }, + }), + /NS_ERROR_ILLEGAL_VALUE/, + "Should throw if boundaries property is a getter." + ); +} + +const makeNewSandbox = () => + Cu.Sandbox(CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")()); + +function testGoodParameters() { + const 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(); + const 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/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-01.js new file mode 100644 index 0000000000..625d359675 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-01.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * 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/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-02.js new file mode 100644 index 0000000000..75ed87c25b --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-02.js @@ -0,0 +1,139 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * 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 }, + domNode: 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 }, + }, + domNode: { bytes: 0, count: 0 }, +}; + +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/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-03.js new file mode 100644 index 0000000000..e0ca7dc15d --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-03.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * 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/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-04.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-04.js new file mode 100644 index 0000000000..e1cc32d697 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-04.js @@ -0,0 +1,160 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * 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; + + (function a() { + (function b() { + (function c() { + stack1 = saveStack(3); + })(); + (function d() { + stack2 = saveStack(3); + stack3 = saveStack(3); + })(); + stack4 = saveStack(2); + })(); + })(); + + const 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/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-05.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-05.js new file mode 100644 index 0000000000..fbd9944507 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-05.js @@ -0,0 +1,150 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * 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/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-06.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-06.js new file mode 100644 index 0000000000..70cc4a2696 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-06.js @@ -0,0 +1,199 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * 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 }, + }; + + 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/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-07.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-07.js new file mode 100644 index 0000000000..009848b9e6 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-07.js @@ -0,0 +1,206 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * 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 }, + }, + domNode: { + by: "descriptiveType", + 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 }, + }, + domNode: {}, + }; + + 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: 15, + parent: 14, + reportLeafIndex: undefined, + }, + ], + id: 14, + parent: 13, + reportLeafIndex: undefined, + }, + ], + id: 13, + parent: 12, + 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: 18, + parent: 17, + reportLeafIndex: undefined, + }, + ], + id: 17, + parent: 16, + reportLeafIndex: undefined, + }, + ], + id: 16, + parent: 12, + 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: 21, + parent: 20, + reportLeafIndex: undefined, + }, + ], + id: 20, + parent: 19, + reportLeafIndex: undefined, + }, + ], + id: 19, + parent: 12, + 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: 24, + parent: 23, + reportLeafIndex: undefined, + }, + ], + id: 23, + parent: 22, + reportLeafIndex: undefined, + }, + ], + id: 22, + parent: 12, + reportLeafIndex: 5, + }, + ], + id: 12, + parent: undefined, + reportLeafIndex: undefined, + }; + + compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { invert: true }); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-08.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-08.js new file mode 100644 index 0000000000..0e8e2ebcc1 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-08.js @@ -0,0 +1,143 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * 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/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-09.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-09.js new file mode 100644 index 0000000000..5f91e5e67b --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/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/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-10.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-10.js new file mode 100644 index 0000000000..ba783f8e47 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-10.js @@ -0,0 +1,51 @@ +/* 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 }, + domNode: { 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 }, + domNode: { 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/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_01.js new file mode 100644 index 0000000000..c90b34af70 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_01.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_02.js new file mode 100644 index 0000000000..cefe76abde --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_02.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_03.js new file mode 100644 index 0000000000..e625f4e0be --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_03.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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 }, + domNode: { 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, + }, + domNode: { + count: 0, + bytes: 0, + }, +}; + +const REPORT2 = { + objects: { + count: 1, + bytes: 10, + }, + scripts: { + count: 0, + bytes: 0, + }, + strings: { + count: 2, + bytes: 20, + }, + other: { + count: 4, + bytes: 40, + }, + domNode: { + count: 0, + bytes: 0, + }, +}; + +const EXPECTED = { + objects: { + count: 0, + bytes: 0, + }, + scripts: { + count: -1, + bytes: -10, + }, + strings: { + count: 1, + bytes: 10, + }, + other: { + count: 1, + bytes: 10, + }, + domNode: { + count: 0, + bytes: 0, + }, +}; + +function run_test() { + assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_04.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_04.js new file mode 100644 index 0000000000..036a33804f --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_04.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_05.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_05.js new file mode 100644 index 0000000000..1ce0cfeb5b --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_05.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_06.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_06.js new file mode 100644 index 0000000000..b805de48dd --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_06.js @@ -0,0 +1,139 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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 }, + }, + domNode: { + 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 }, + }, + domNode: {}, +}; + +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 }, + }, + domNode: {}, +}; + +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, + }, + }, + domNode: {}, +}; + +function run_test() { + assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_01.js new file mode 100644 index 0000000000..961023b2c2 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_01.js @@ -0,0 +1,111 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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 }, + }, + domNode: { + by: "descriptiveType", + 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 }, + }, + domNode: {}, + }; + + 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: 16, + parent: 15, + reportLeafIndex: 4, + }, + { + name: "UInt8Array", + bytes: 80, + totalBytes: 80, + count: 8, + totalCount: 8, + children: undefined, + id: 17, + parent: 15, + reportLeafIndex: 3, + }, + { + name: "Array", + bytes: 50, + totalBytes: 50, + count: 5, + totalCount: 5, + children: undefined, + id: 18, + parent: 15, + reportLeafIndex: 2, + }, + ], + id: 15, + parent: 14, + reportLeafIndex: undefined, + }, + ], + id: 14, + parent: undefined, + reportLeafIndex: undefined, + }; + + compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { filter: "Array" }); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_02.js new file mode 100644 index 0000000000..9915acb678 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_02.js @@ -0,0 +1,125 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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; + + (function foo() { + (function bar() { + (function baz() { + stack1 = saveStack(3); + })(); + (function quux() { + stack2 = saveStack(3); + stack3 = saveStack(3); + })(); + })(); + stack4 = saveStack(2); + })(); + + const 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/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_03.js new file mode 100644 index 0000000000..dc144bac2e --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_03.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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 }, + }, + domNode: { + by: "descriptiveType", + 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 }, + }, + domNode: {}, + }; + + const EXPECTED = { + name: null, + bytes: 0, + totalBytes: 620, + count: 0, + totalCount: 62, + children: undefined, + id: 14, + parent: undefined, + reportLeafIndex: undefined, + }; + + compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { + filter: "zzzzzzzzzzzzzzzzzzzz", + }); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_04.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_04.js new file mode 100644 index 0000000000..7e37b2c5da --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_04.js @@ -0,0 +1,105 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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, + domNode: { by: "descriptiveType", then: COUNT, other: COUNT }, + }; + + 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, + }, + }, + domNode: {}, + }; + + 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: 14, + parent: 13, + children: undefined, + reportLeafIndex: 2, + }, + { + name: "Array", + bytes: 60, + totalBytes: 60, + count: 6, + totalCount: 6, + id: 15, + parent: 13, + children: undefined, + reportLeafIndex: 3, + }, + ], + id: 13, + parent: 12, + reportLeafIndex: undefined, + }, + ], + id: 12, + reportLeafIndex: undefined, + }; + + compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { filter: "objects" }); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_05.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_05.js new file mode 100644 index 0000000000..667717688c --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_05.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// 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 (const child of node.children) { + Assert.lessOrEqual( + child.bytes, + lastSelfBytes, + `${child.bytes} <= ${lastSelfBytes}` + ); + lastSelfBytes = child.bytes; + assertSortedBySelf(child); + } + } + })(tree); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_countToBucketBreakdown_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_countToBucketBreakdown_01.js new file mode 100644 index 0000000000..e89048c333 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/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/devtools/shared/heapsnapshot/tests/xpcshell/test_deduplicatePaths_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_deduplicatePaths_01.js new file mode 100644 index 0000000000..fc2864f5f4 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_deduplicatePaths_01.js @@ -0,0 +1,87 @@ +/* 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/devtools/shared/heapsnapshot/tests/xpcshell/test_getCensusIndividuals_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_getCensusIndividuals_01.js new file mode 100644 index 0000000000..6963f3f33a --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/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 + // prettier-ignore + 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/devtools/shared/heapsnapshot/tests/xpcshell/test_getReportLeaves_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_getReportLeaves_01.js new file mode 100644 index 0000000000..04aae90e99 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_getReportLeaves_01.js @@ -0,0 +1,135 @@ +/* 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 }, + }, + domNode: { + by: "descriptiveType", + 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 }, + }, + domNode: {}, + }; + + 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 (const 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 (const child of node.children) { + const found = find(name, child); + if (found) { + return found; + } + } + } + + return undefined; + } + + 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/devtools/shared/heapsnapshot/tests/xpcshell/test_saveHeapSnapshot_e10s_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_saveHeapSnapshot_e10s_01.js new file mode 100644 index 0000000000..0a46618003 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_saveHeapSnapshot_e10s_01.js @@ -0,0 +1,9 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test saving a heap snapshot in the sandboxed e10s child process. + +function run_test() { + run_test_in_child("test_SaveHeapSnapshot.js"); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/xpcshell.toml b/devtools/shared/heapsnapshot/tests/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..155aeef009 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/xpcshell.toml @@ -0,0 +1,184 @@ +[DEFAULT] +tags = "devtools heapsnapshot devtools-memory" +head = "head_heapsnapshot.js" +firefox-appdir = "browser" +skip-if = ["os == 'android'"] +support-files = [ + "Census.sys.mjs", + "dominator-tree-worker.js", + "heap-snapshot-worker.js", + "Match.sys.mjs", +] + +["test_DominatorTreeNode_LabelAndShallowSize_01.js"] + +["test_DominatorTreeNode_LabelAndShallowSize_02.js"] + +["test_DominatorTreeNode_LabelAndShallowSize_03.js"] + +["test_DominatorTreeNode_LabelAndShallowSize_04.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_partialTraversal_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_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"] +skip-if = ["tsan"] # Unreasonably slow, bug 1612707 + +["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_computeShortestPaths_01.js"] + +["test_HeapSnapshot_computeShortestPaths_02.js"] + +["test_HeapSnapshot_creationTime_01.js"] + +["test_HeapSnapshot_deepStack_01.js"] + +["test_HeapSnapshot_describeNode_01.js"] + +["test_HeapSnapshot_getObjectNodeId_01.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_with_utf8_paths.js"] + +["test_ReadHeapSnapshot_worker.js"] +skip-if = ["os == 'linux'"] # Bug 1176173 + +["test_SaveHeapSnapshot.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_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_countToBucketBreakdown_01.js"] + +["test_deduplicatePaths_01.js"] + +["test_getCensusIndividuals_01.js"] + +["test_getReportLeaves_01.js"] + +["test_saveHeapSnapshot_e10s_01.js"] |