summaryrefslogtreecommitdiffstats
path: root/devtools/shared/heapsnapshot
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/heapsnapshot')
-rw-r--r--devtools/shared/heapsnapshot/.gitattributes1
-rw-r--r--devtools/shared/heapsnapshot/AutoMemMap.cpp64
-rw-r--r--devtools/shared/heapsnapshot/AutoMemMap.h75
-rw-r--r--devtools/shared/heapsnapshot/CensusUtils.js489
-rw-r--r--devtools/shared/heapsnapshot/CoreDump.pb.cc2542
-rw-r--r--devtools/shared/heapsnapshot/CoreDump.pb.h1893
-rw-r--r--devtools/shared/heapsnapshot/CoreDump.proto143
-rw-r--r--devtools/shared/heapsnapshot/DeserializedNode.cpp150
-rw-r--r--devtools/shared/heapsnapshot/DeserializedNode.h317
-rw-r--r--devtools/shared/heapsnapshot/DominatorTree.cpp140
-rw-r--r--devtools/shared/heapsnapshot/DominatorTree.h67
-rw-r--r--devtools/shared/heapsnapshot/DominatorTreeNode.js336
-rw-r--r--devtools/shared/heapsnapshot/FileDescriptorOutputStream.cpp86
-rw-r--r--devtools/shared/heapsnapshot/FileDescriptorOutputStream.h41
-rw-r--r--devtools/shared/heapsnapshot/HeapAnalysesClient.js277
-rw-r--r--devtools/shared/heapsnapshot/HeapAnalysesWorker.js303
-rw-r--r--devtools/shared/heapsnapshot/HeapSnapshot.cpp1652
-rw-r--r--devtools/shared/heapsnapshot/HeapSnapshot.h239
-rw-r--r--devtools/shared/heapsnapshot/HeapSnapshotFileUtils.js95
-rw-r--r--devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperChild.h32
-rw-r--r--devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperParent.cpp53
-rw-r--r--devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperParent.h35
-rw-r--r--devtools/shared/heapsnapshot/PHeapSnapshotTempFileHelper.ipdl35
-rw-r--r--devtools/shared/heapsnapshot/ZeroCopyNSIOutputStream.cpp100
-rw-r--r--devtools/shared/heapsnapshot/ZeroCopyNSIOutputStream.h70
-rw-r--r--devtools/shared/heapsnapshot/census-tree-node.js748
-rwxr-xr-xdevtools/shared/heapsnapshot/generate-core-dump-sources.sh26
-rw-r--r--devtools/shared/heapsnapshot/moz.build62
-rw-r--r--devtools/shared/heapsnapshot/shortest-paths.js91
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/DeserializedNodeUbiNodes.cpp100
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/DeserializedStackFrameUbiStackFrames.cpp91
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/DevTools.h276
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/DoesCrossCompartmentBoundaries.cpp73
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/DoesntCrossCompartmentBoundaries.cpp64
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/SerializesEdgeNames.cpp53
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/SerializesEverythingInHeapGraphOnce.cpp37
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/SerializesTypeNames.cpp30
-rw-r--r--devtools/shared/heapsnapshot/tests/gtest/moz.build31
-rw-r--r--devtools/shared/heapsnapshot/tests/mochitest/chrome.ini8
-rw-r--r--devtools/shared/heapsnapshot/tests/mochitest/mochitest.ini6
-rw-r--r--devtools/shared/heapsnapshot/tests/mochitest/test_DominatorTree_01.html37
-rw-r--r--devtools/shared/heapsnapshot/tests/mochitest/test_SaveHeapSnapshot.html25
-rw-r--r--devtools/shared/heapsnapshot/tests/mochitest/test_saveHeapSnapshot_e10s_01.html82
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/.eslintrc.js6
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/Census.jsm165
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/Match.jsm190
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/dominator-tree-worker.js47
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/head_heapsnapshot.js448
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/heap-snapshot-worker.js46
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_LabelAndShallowSize_01.js46
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_LabelAndShallowSize_02.js45
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_LabelAndShallowSize_03.js47
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_LabelAndShallowSize_04.js53
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_attachShortestPaths_01.js132
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_getNodeByIdAlongPath_01.js44
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_insert_01.js112
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_insert_02.js30
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_insert_03.js117
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_partialTraversal_01.js164
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_DominatorTree_01.js23
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_DominatorTree_02.js40
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_DominatorTree_03.js13
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_DominatorTree_04.js22
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_DominatorTree_05.js75
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_DominatorTree_06.js56
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_computeDominatorTree_01.js22
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_computeDominatorTree_02.js23
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_deleteHeapSnapshot_01.js59
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_deleteHeapSnapshot_02.js22
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_deleteHeapSnapshot_03.js62
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_getCensusIndividuals_01.js89
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_getCreationTime_01.js58
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_getDominatorTree_01.js69
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_getDominatorTree_02.js31
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_getImmediatelyDominated_01.js81
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_readHeapSnapshot_01.js18
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensusDiff_01.js54
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensusDiff_02.js59
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_01.js27
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_02.js29
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_03.js48
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_04.js118
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_05.js44
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_06.js109
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_07.js52
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_computeShortestPaths_01.js69
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_computeShortestPaths_02.js47
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_creationTime_01.js30
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_deepStack_01.js70
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_describeNode_01.js42
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_01.js31
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_02.js57
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_03.js34
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_04.js36
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_05.js24
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_06.js125
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_07.js82
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_08.js82
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_09.js92
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_10.js68
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_11.js116
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_12.js50
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_ReadHeapSnapshot.js20
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_ReadHeapSnapshot_with_allocations.js36
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_ReadHeapSnapshot_worker.js40
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_SaveHeapSnapshot.js82
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-01.js76
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-02.js136
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-03.js96
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-04.js159
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-05.js145
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-06.js200
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-07.js200
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-08.js142
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-09.js44
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-10.js43
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_census_diff_01.js74
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_census_diff_02.js25
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_census_diff_03.js73
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_census_diff_04.js63
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_census_diff_05.js34
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_census_diff_06.js137
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_census_filtering_01.js105
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_census_filtering_02.js124
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_census_filtering_03.js59
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_census_filtering_04.js102
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_census_filtering_05.js71
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_countToBucketBreakdown_01.js37
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_deduplicatePaths_01.js113
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_getCensusIndividuals_01.js60
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_getReportLeaves_01.js114
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/test_saveHeapSnapshot_e10s_01.js8
-rw-r--r--devtools/shared/heapsnapshot/tests/unit/xpcshell.ini98
133 files changed, 17841 insertions, 0 deletions
diff --git a/devtools/shared/heapsnapshot/.gitattributes b/devtools/shared/heapsnapshot/.gitattributes
new file mode 100644
index 000000000..44e248a8d
--- /dev/null
+++ b/devtools/shared/heapsnapshot/.gitattributes
@@ -0,0 +1 @@
+CoreDump.pb.* binary
diff --git a/devtools/shared/heapsnapshot/AutoMemMap.cpp b/devtools/shared/heapsnapshot/AutoMemMap.cpp
new file mode 100644
index 000000000..e725a99c6
--- /dev/null
+++ b/devtools/shared/heapsnapshot/AutoMemMap.cpp
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/devtools/AutoMemMap.h"
+
+#include "mozilla/Unused.h"
+#include "nsDebug.h"
+
+namespace mozilla {
+namespace devtools {
+
+AutoMemMap::~AutoMemMap()
+{
+ if (addr) {
+ Unused << NS_WARN_IF(PR_MemUnmap(addr, size()) != PR_SUCCESS);
+ addr = nullptr;
+ }
+
+ if (fileMap) {
+ Unused << NS_WARN_IF(PR_CloseFileMap(fileMap) != PR_SUCCESS);
+ fileMap = nullptr;
+ }
+
+ if (fd) {
+ Unused << NS_WARN_IF(PR_Close(fd) != PR_SUCCESS);
+ fd = nullptr;
+ }
+}
+
+nsresult
+AutoMemMap::init(const char* filePath, int flags, int mode, PRFileMapProtect prot)
+{
+ MOZ_ASSERT(!fd);
+ MOZ_ASSERT(!fileMap);
+ MOZ_ASSERT(!addr);
+
+ if (PR_GetFileInfo64(filePath, &fileInfo) != PR_SUCCESS)
+ return NS_ERROR_FILE_NOT_FOUND;
+
+ // Check if the file is too big to memmap.
+ if (fileInfo.size > int64_t(UINT32_MAX))
+ return NS_ERROR_INVALID_ARG;
+ auto length = uint32_t(fileInfo.size);
+
+ fd = PR_Open(filePath, flags, flags);
+ if (!fd)
+ return NS_ERROR_UNEXPECTED;
+
+ fileMap = PR_CreateFileMap(fd, fileInfo.size, prot);
+ if (!fileMap)
+ return NS_ERROR_UNEXPECTED;
+
+ addr = PR_MemMap(fileMap, 0, length);
+ if (!addr)
+ return NS_ERROR_UNEXPECTED;
+
+ return NS_OK;
+}
+
+} // namespace devtools
+} // namespace mozilla
diff --git a/devtools/shared/heapsnapshot/AutoMemMap.h b/devtools/shared/heapsnapshot/AutoMemMap.h
new file mode 100644
index 000000000..537d68004
--- /dev/null
+++ b/devtools/shared/heapsnapshot/AutoMemMap.h
@@ -0,0 +1,75 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_devtools_AutoMemMap_h
+#define mozilla_devtools_AutoMemMap_h
+
+#include <prio.h>
+#include "mozilla/GuardObjects.h"
+
+namespace mozilla {
+namespace devtools {
+
+// # AutoMemMap
+//
+// AutoMemMap is an RAII class to manage mapping a file to memory. It is a
+// wrapper aorund managing opening and closing a file and calling PR_MemMap and
+// PR_MemUnmap.
+//
+// Example usage:
+//
+// {
+// AutoMemMap mm;
+// if (NS_FAILED(mm.init("/path/to/desired/file"))) {
+// // Handle the error however you see fit.
+// return false;
+// }
+//
+// doStuffWithMappedMemory(mm.address());
+// }
+// // The memory is automatically unmapped when the AutoMemMap leaves scope.
+class MOZ_RAII AutoMemMap
+{
+ MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER;
+
+ PRFileInfo64 fileInfo;
+ PRFileDesc* fd;
+ PRFileMap* fileMap;
+ void* addr;
+
+ AutoMemMap(const AutoMemMap& aOther) = delete;
+ void operator=(const AutoMemMap& aOther) = delete;
+
+public:
+ explicit AutoMemMap(MOZ_GUARD_OBJECT_NOTIFIER_ONLY_PARAM)
+ : fd(nullptr)
+ , fileMap(nullptr)
+ , addr(nullptr)
+ {
+ MOZ_GUARD_OBJECT_NOTIFIER_INIT;
+ };
+ ~AutoMemMap();
+
+ // Initialize this AutoMemMap.
+ nsresult init(const char* filePath, int flags = PR_RDONLY, int mode = 0,
+ PRFileMapProtect prot = PR_PROT_READONLY);
+
+ // Get the size of the memory mapped file.
+ uint32_t size() const {
+ MOZ_ASSERT(fileInfo.size <= UINT32_MAX,
+ "Should only call size() if init() succeeded.");
+ return uint32_t(fileInfo.size);
+ }
+
+ // Get the mapped memory.
+ void* address() { MOZ_ASSERT(addr); return addr; }
+ const void* address() const { MOZ_ASSERT(addr); return addr; }
+};
+
+} // namespace devtools
+} // namespace mozilla
+
+#endif // mozilla_devtools_AutoMemMap_h
diff --git a/devtools/shared/heapsnapshot/CensusUtils.js b/devtools/shared/heapsnapshot/CensusUtils.js
new file mode 100644
index 000000000..36bdd2d82
--- /dev/null
+++ b/devtools/shared/heapsnapshot/CensusUtils.js
@@ -0,0 +1,489 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { flatten } = require("resource://devtools/shared/ThreadSafeDevToolsUtils.js");
+
+/** * Visitor ****************************************************************/
+
+/**
+ * A Visitor visits each node and edge of a census report tree as the census
+ * report is being traversed by `walk`.
+ */
+function Visitor() { }
+exports.Visitor = Visitor;
+
+/**
+ * The `enter` method is called when a new sub-report is entered in traversal.
+ *
+ * @param {Object} breakdown
+ * The breakdown for the sub-report that is being entered by traversal.
+ *
+ * @param {Object} report
+ * The report generated by the given breakdown.
+ *
+ * @param {any} edge
+ * The edge leading to this sub-report. The edge is null if (but not iff!
+ * eg, null allocation stack edges) we are entering the root report.
+ */
+Visitor.prototype.enter = function (breakdown, report, edge) { };
+
+/**
+ * The `exit` method is called when traversal of a sub-report has finished.
+ *
+ * @param {Object} breakdown
+ * The breakdown for the sub-report whose traversal has finished.
+ *
+ * @param {Object} report
+ * The report generated by the given breakdown.
+ *
+ * @param {any} edge
+ * The edge leading to this sub-report. The edge is null if (but not iff!
+ * eg, null allocation stack edges) we are entering the root report.
+ */
+Visitor.prototype.exit = function (breakdown, report, edge) { };
+
+/**
+ * The `count` method is called when leaf nodes (reports whose breakdown is
+ * by: "count") in the report tree are encountered.
+ *
+ * @param {Object} breakdown
+ * The count breakdown for this report.
+ *
+ * @param {Object} report
+ * The report generated by a breakdown by "count".
+ *
+ * @param {any|null} edge
+ * The edge leading to this count report. The edge is null if we are
+ * entering the root report.
+ */
+Visitor.prototype.count = function (breakdown, report, edge) { };
+
+/** * getReportEdges *********************************************************/
+
+const EDGES = Object.create(null);
+
+EDGES.count = function (breakdown, report) {
+ return [];
+};
+
+EDGES.bucket = function (breakdown, report) {
+ return [];
+};
+
+EDGES.internalType = function (breakdown, report) {
+ return Object.keys(report).map(key => ({
+ edge: key,
+ referent: report[key],
+ breakdown: breakdown.then
+ }));
+};
+
+EDGES.objectClass = function (breakdown, report) {
+ return Object.keys(report).map(key => ({
+ edge: key,
+ referent: report[key],
+ breakdown: key === "other" ? breakdown.other : breakdown.then
+ }));
+};
+
+EDGES.coarseType = function (breakdown, report) {
+ return [
+ { edge: "objects", referent: report.objects, breakdown: breakdown.objects },
+ { edge: "scripts", referent: report.scripts, breakdown: breakdown.scripts },
+ { edge: "strings", referent: report.strings, breakdown: breakdown.strings },
+ { edge: "other", referent: report.other, breakdown: breakdown.other },
+ ];
+};
+
+EDGES.allocationStack = function (breakdown, report) {
+ const edges = [];
+ report.forEach((value, key) => {
+ edges.push({
+ edge: key,
+ referent: value,
+ breakdown: key === "noStack" ? breakdown.noStack : breakdown.then
+ });
+ });
+ return edges;
+};
+
+EDGES.filename = function (breakdown, report) {
+ return Object.keys(report).map(key => ({
+ edge: key,
+ referent: report[key],
+ breakdown: key === "noFilename" ? breakdown.noFilename : breakdown.then
+ }));
+};
+
+/**
+ * Get the set of outgoing edges from `report` as specified by the given
+ * breakdown.
+ *
+ * @param {Object} breakdown
+ * The census breakdown.
+ *
+ * @param {Object} report
+ * The census report.
+ */
+function getReportEdges(breakdown, report) {
+ return EDGES[breakdown.by](breakdown, report);
+}
+exports.getReportEdges = getReportEdges;
+
+/** * walk *******************************************************************/
+
+function recursiveWalk(breakdown, edge, report, visitor) {
+ if (breakdown.by === "count") {
+ visitor.enter(breakdown, report, edge);
+ visitor.count(breakdown, report, edge);
+ visitor.exit(breakdown, report, edge);
+ } else {
+ visitor.enter(breakdown, report, edge);
+ for (let { edge, referent, breakdown: subBreakdown } of getReportEdges(breakdown, report)) {
+ recursiveWalk(subBreakdown, edge, referent, visitor);
+ }
+ visitor.exit(breakdown, report, edge);
+ }
+}
+
+/**
+ * Walk the given `report` that was generated by taking a census with the
+ * specified `breakdown`.
+ *
+ * @param {Object} breakdown
+ * The census breakdown.
+ *
+ * @param {Object} report
+ * The census report.
+ *
+ * @param {Visitor} visitor
+ * The Visitor instance to call into while traversing.
+ */
+function walk(breakdown, report, visitor) {
+ recursiveWalk(breakdown, null, report, visitor);
+}
+exports.walk = walk;
+
+/** * diff *******************************************************************/
+
+/**
+ * Return true if the object is a Map, false otherwise. Works with Map objects
+ * from other globals, unlike `instanceof`.
+ *
+ * @returns {Boolean}
+ */
+function isMap(obj) {
+ return Object.prototype.toString.call(obj) === "[object Map]";
+}
+
+/**
+ * A Visitor for computing the difference between the census report being
+ * traversed and the given other census.
+ *
+ * @param {Object} otherCensus
+ * The other census report.
+ */
+function DiffVisitor(otherCensus) {
+ // The other census we are comparing against.
+ this._otherCensus = otherCensus;
+
+ // The total bytes and count of the basis census we are traversing.
+ this._totalBytes = 0;
+ this._totalCount = 0;
+
+ // Stack maintaining the current corresponding sub-report for the other
+ // census we are comparing against.
+ this._otherCensusStack = [];
+
+ // Stack maintaining the set of edges visited at each sub-report.
+ this._edgesVisited = [new Set()];
+
+ // The final delta census. Valid only after traversal.
+ this._results = null;
+
+ // Stack maintaining the results corresponding to each sub-report we are
+ // currently traversing.
+ this._resultsStack = [];
+}
+
+DiffVisitor.prototype = Object.create(Visitor.prototype);
+
+/**
+ * Given a report and an outgoing edge, get the edge's referent.
+ */
+DiffVisitor.prototype._get = function (report, edge) {
+ if (!report) {
+ return undefined;
+ }
+ return isMap(report) ? report.get(edge) : report[edge];
+};
+
+/**
+ * Given a report, an outgoing edge, and a value, set the edge's referent to
+ * the given value.
+ */
+DiffVisitor.prototype._set = function (report, edge, val) {
+ if (isMap(report)) {
+ report.set(edge, val);
+ } else {
+ report[edge] = val;
+ }
+};
+
+/**
+ * @overrides Visitor.prototype.enter
+ */
+DiffVisitor.prototype.enter = function (breakdown, report, edge) {
+ const isFirstTimeEntering = this._results === null;
+
+ const newResults = breakdown.by === "allocationStack" ? new Map() : {};
+ let newOther;
+
+ if (!this._results) {
+ // This is the first time we have entered a sub-report.
+ this._results = newResults;
+ newOther = this._otherCensus;
+ } else {
+ const topResults = this._resultsStack[this._resultsStack.length - 1];
+ this._set(topResults, edge, newResults);
+
+ const topOther = this._otherCensusStack[this._otherCensusStack.length - 1];
+ newOther = this._get(topOther, edge);
+ }
+
+ this._resultsStack.push(newResults);
+ this._otherCensusStack.push(newOther);
+
+ const visited = this._edgesVisited[this._edgesVisited.length - 1];
+ visited.add(edge);
+ this._edgesVisited.push(new Set());
+};
+
+/**
+ * @overrides Visitor.prototype.exit
+ */
+DiffVisitor.prototype.exit = function (breakdown, report, edge) {
+ // Find all the edges in the other census report that were not traversed and
+ // add them to the results directly.
+ const other = this._otherCensusStack[this._otherCensusStack.length - 1];
+ if (other) {
+ const visited = this._edgesVisited[this._edgesVisited.length - 1];
+ const unvisited = getReportEdges(breakdown, other)
+ .map(e => e.edge)
+ .filter(e => !visited.has(e));
+ const results = this._resultsStack[this._resultsStack.length - 1];
+ for (let edge of unvisited) {
+ this._set(results, edge, this._get(other, edge));
+ }
+ }
+
+ this._otherCensusStack.pop();
+ this._resultsStack.pop();
+ this._edgesVisited.pop();
+};
+
+/**
+ * @overrides Visitor.prototype.count
+ */
+DiffVisitor.prototype.count = function (breakdown, report, edge) {
+ const other = this._otherCensusStack[this._otherCensusStack.length - 1];
+ const results = this._resultsStack[this._resultsStack.length - 1];
+
+ if (breakdown.count) {
+ this._totalCount += report.count;
+ }
+ if (breakdown.bytes) {
+ this._totalBytes += report.bytes;
+ }
+
+ if (other) {
+ if (breakdown.count) {
+ results.count = other.count - report.count;
+ }
+ if (breakdown.bytes) {
+ results.bytes = other.bytes - report.bytes;
+ }
+ } else {
+ if (breakdown.count) {
+ results.count = -report.count;
+ }
+ if (breakdown.bytes) {
+ results.bytes = -report.bytes;
+ }
+ }
+};
+
+const basisTotalBytes = exports.basisTotalBytes = Symbol("basisTotalBytes");
+const basisTotalCount = exports.basisTotalCount = Symbol("basisTotalCount");
+
+/**
+ * Get the resulting report of the difference between the traversed census
+ * report and the other census report.
+ *
+ * @returns {Object}
+ * The delta census report.
+ */
+DiffVisitor.prototype.results = function () {
+ if (!this._results) {
+ throw new Error("Attempt to get results before computing diff!");
+ }
+
+ if (this._resultsStack.length) {
+ throw new Error("Attempt to get results while still computing diff!");
+ }
+
+ this._results[basisTotalBytes] = this._totalBytes;
+ this._results[basisTotalCount] = this._totalCount;
+
+ return this._results;
+};
+
+/**
+ * Take the difference between two censuses. The resulting delta report
+ * contains the number/size of things that are in the `endCensus` that are not
+ * in the `startCensus`.
+ *
+ * @param {Object} breakdown
+ * The breakdown used to generate both census reports.
+ *
+ * @param {Object} startCensus
+ * The first census report.
+ *
+ * @param {Object} endCensus
+ * The second census report.
+ *
+ * @returns {Object}
+ * A delta report mirroring the structure of the two census reports (as
+ * specified by the given breakdown). Has two additional properties:
+ * - {Number} basisTotalBytes: the total number of bytes in the start
+ * census.
+ * - {Number} basisTotalCount: the total count in the start census.
+ */
+function diff(breakdown, startCensus, endCensus) {
+ const visitor = new DiffVisitor(endCensus);
+ walk(breakdown, startCensus, visitor);
+ return visitor.results();
+}
+exports.diff = diff;
+
+/**
+ * Creates a hash map mapping node IDs to its parent node.
+ *
+ * @param {CensusTreeNode} node
+ * @param {Object<number, TreeNode>} aggregator
+ *
+ * @return {Object<number, TreeNode>}
+ */
+const createParentMap = exports.createParentMap = function (node,
+ getId = node => node.id,
+ aggregator = Object.create(null)) {
+ if (node.children) {
+ for (let i = 0, length = node.children.length; i < length; i++) {
+ const child = node.children[i];
+ aggregator[getId(child)] = node;
+ createParentMap(child, getId, aggregator);
+ }
+ }
+
+ return aggregator;
+};
+
+const BUCKET = Object.freeze({ by: "bucket" });
+
+/**
+ * Convert a breakdown whose leaves are { by: "count" } to an identical
+ * breakdown, except with { by: "bucket" } leaves.
+ *
+ * @param {Object} breakdown
+ * @returns {Object}
+ */
+exports.countToBucketBreakdown = function (breakdown) {
+ if (typeof breakdown !== "object" || !breakdown) {
+ return breakdown;
+ }
+
+ if (breakdown.by === "count") {
+ return BUCKET;
+ }
+
+ const keys = Object.keys(breakdown);
+ const vals = keys.reduce((vs, k) => {
+ vs.push(exports.countToBucketBreakdown(breakdown[k]));
+ return vs;
+ }, []);
+
+ const result = {};
+ for (let i = 0, length = keys.length; i < length; i++) {
+ result[keys[i]] = vals[i];
+ }
+
+ return Object.freeze(result);
+};
+
+/**
+ * A Visitor for finding report leaves by their DFS index.
+ */
+function GetLeavesVisitor(targetIndices) {
+ this._index = -1;
+ this._targetIndices = targetIndices;
+ this._leaves = [];
+}
+
+GetLeavesVisitor.prototype = Object.create(Visitor.prototype);
+
+/**
+ * @overrides Visitor.prototype.enter
+ */
+GetLeavesVisitor.prototype.enter = function (breakdown, report, edge) {
+ this._index++;
+ if (this._targetIndices.has(this._index)) {
+ this._leaves.push(report);
+ }
+};
+
+/**
+ * Get the accumulated report leaves after traversal.
+ */
+GetLeavesVisitor.prototype.leaves = function () {
+ if (this._index === -1) {
+ throw new Error("Attempt to call `leaves` before traversing report!");
+ }
+ return this._leaves;
+};
+
+/**
+ * Given a set of indices of leaves in a pre-order depth-first traversal of the
+ * given census report, return the leaves.
+ *
+ * @param {Set<Number>} indices
+ * @param {Object} breakdown
+ * @param {Object} report
+ *
+ * @returns {Array<Object>}
+ */
+exports.getReportLeaves = function (indices, breakdown, report) {
+ const visitor = new GetLeavesVisitor(indices);
+ walk(breakdown, report, visitor);
+ return visitor.leaves();
+};
+
+/**
+ * Get a list of the individual node IDs that belong to the census report leaves
+ * of the given indices.
+ *
+ * @param {Set<Number>} indices
+ * @param {Object} breakdown
+ * @param {HeapSnapshot} snapshot
+ *
+ * @returns {Array<NodeId>}
+ */
+exports.getCensusIndividuals = function (indices, countBreakdown, snapshot) {
+ const bucketBreakdown = exports.countToBucketBreakdown(countBreakdown);
+ const bucketReport = snapshot.takeCensus({ breakdown: bucketBreakdown });
+ const buckets = exports.getReportLeaves(indices,
+ bucketBreakdown,
+ bucketReport);
+ return flatten(buckets);
+};
diff --git a/devtools/shared/heapsnapshot/CoreDump.pb.cc b/devtools/shared/heapsnapshot/CoreDump.pb.cc
new file mode 100644
index 000000000..6c7c0e8a4
--- /dev/null
+++ b/devtools/shared/heapsnapshot/CoreDump.pb.cc
@@ -0,0 +1,2542 @@
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: CoreDump.proto
+
+#define INTERNAL_SUPPRESS_PROTOBUF_FIELD_DEPRECATION
+#include "CoreDump.pb.h"
+
+#include <algorithm>
+
+#include <google/protobuf/stubs/common.h>
+#include <google/protobuf/stubs/once.h>
+#include <google/protobuf/io/coded_stream.h>
+#include <google/protobuf/wire_format_lite_inl.h>
+#include <google/protobuf/descriptor.h>
+#include <google/protobuf/generated_message_reflection.h>
+#include <google/protobuf/reflection_ops.h>
+#include <google/protobuf/wire_format.h>
+// @@protoc_insertion_point(includes)
+
+namespace mozilla {
+namespace devtools {
+namespace protobuf {
+
+namespace {
+
+const ::google::protobuf::Descriptor* Metadata_descriptor_ = NULL;
+const ::google::protobuf::internal::GeneratedMessageReflection*
+ Metadata_reflection_ = NULL;
+const ::google::protobuf::Descriptor* StackFrame_descriptor_ = NULL;
+const ::google::protobuf::internal::GeneratedMessageReflection*
+ StackFrame_reflection_ = NULL;
+struct StackFrameOneofInstance {
+ const ::mozilla::devtools::protobuf::StackFrame_Data* data_;
+ ::google::protobuf::uint64 ref_;
+}* StackFrame_default_oneof_instance_ = NULL;
+const ::google::protobuf::Descriptor* StackFrame_Data_descriptor_ = NULL;
+const ::google::protobuf::internal::GeneratedMessageReflection*
+ StackFrame_Data_reflection_ = NULL;
+struct StackFrame_DataOneofInstance {
+ const ::std::string* source_;
+ ::google::protobuf::uint64 sourceref_;
+ const ::std::string* functiondisplayname_;
+ ::google::protobuf::uint64 functiondisplaynameref_;
+}* StackFrame_Data_default_oneof_instance_ = NULL;
+const ::google::protobuf::Descriptor* Node_descriptor_ = NULL;
+const ::google::protobuf::internal::GeneratedMessageReflection*
+ Node_reflection_ = NULL;
+struct NodeOneofInstance {
+ const ::std::string* typename__;
+ ::google::protobuf::uint64 typenameref_;
+ const ::std::string* jsobjectclassname_;
+ ::google::protobuf::uint64 jsobjectclassnameref_;
+ const ::std::string* scriptfilename_;
+ ::google::protobuf::uint64 scriptfilenameref_;
+}* Node_default_oneof_instance_ = NULL;
+const ::google::protobuf::Descriptor* Edge_descriptor_ = NULL;
+const ::google::protobuf::internal::GeneratedMessageReflection*
+ Edge_reflection_ = NULL;
+struct EdgeOneofInstance {
+ const ::std::string* name_;
+ ::google::protobuf::uint64 nameref_;
+}* Edge_default_oneof_instance_ = NULL;
+
+} // namespace
+
+
+void protobuf_AssignDesc_CoreDump_2eproto() {
+ protobuf_AddDesc_CoreDump_2eproto();
+ const ::google::protobuf::FileDescriptor* file =
+ ::google::protobuf::DescriptorPool::generated_pool()->FindFileByName(
+ "CoreDump.proto");
+ GOOGLE_CHECK(file != NULL);
+ Metadata_descriptor_ = file->message_type(0);
+ static const int Metadata_offsets_[1] = {
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Metadata, timestamp_),
+ };
+ Metadata_reflection_ =
+ new ::google::protobuf::internal::GeneratedMessageReflection(
+ Metadata_descriptor_,
+ Metadata::default_instance_,
+ Metadata_offsets_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Metadata, _has_bits_[0]),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Metadata, _unknown_fields_),
+ -1,
+ ::google::protobuf::DescriptorPool::generated_pool(),
+ ::google::protobuf::MessageFactory::generated_factory(),
+ sizeof(Metadata));
+ StackFrame_descriptor_ = file->message_type(1);
+ static const int StackFrame_offsets_[3] = {
+ PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(StackFrame_default_oneof_instance_, data_),
+ PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(StackFrame_default_oneof_instance_, ref_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame, StackFrameType_),
+ };
+ StackFrame_reflection_ =
+ new ::google::protobuf::internal::GeneratedMessageReflection(
+ StackFrame_descriptor_,
+ StackFrame::default_instance_,
+ StackFrame_offsets_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame, _has_bits_[0]),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame, _unknown_fields_),
+ -1,
+ StackFrame_default_oneof_instance_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame, _oneof_case_[0]),
+ ::google::protobuf::DescriptorPool::generated_pool(),
+ ::google::protobuf::MessageFactory::generated_factory(),
+ sizeof(StackFrame));
+ StackFrame_Data_descriptor_ = StackFrame_descriptor_->nested_type(0);
+ static const int StackFrame_Data_offsets_[12] = {
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame_Data, id_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame_Data, parent_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame_Data, line_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame_Data, column_),
+ PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(StackFrame_Data_default_oneof_instance_, source_),
+ PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(StackFrame_Data_default_oneof_instance_, sourceref_),
+ PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(StackFrame_Data_default_oneof_instance_, functiondisplayname_),
+ PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(StackFrame_Data_default_oneof_instance_, functiondisplaynameref_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame_Data, issystem_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame_Data, isselfhosted_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame_Data, SourceOrRef_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame_Data, FunctionDisplayNameOrRef_),
+ };
+ StackFrame_Data_reflection_ =
+ new ::google::protobuf::internal::GeneratedMessageReflection(
+ StackFrame_Data_descriptor_,
+ StackFrame_Data::default_instance_,
+ StackFrame_Data_offsets_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame_Data, _has_bits_[0]),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame_Data, _unknown_fields_),
+ -1,
+ StackFrame_Data_default_oneof_instance_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(StackFrame_Data, _oneof_case_[0]),
+ ::google::protobuf::DescriptorPool::generated_pool(),
+ ::google::protobuf::MessageFactory::generated_factory(),
+ sizeof(StackFrame_Data));
+ Node_descriptor_ = file->message_type(2);
+ static const int Node_offsets_[14] = {
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Node, id_),
+ PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(Node_default_oneof_instance_, typename__),
+ PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(Node_default_oneof_instance_, typenameref_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Node, size_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Node, edges_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Node, allocationstack_),
+ PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(Node_default_oneof_instance_, jsobjectclassname_),
+ PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(Node_default_oneof_instance_, jsobjectclassnameref_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Node, coarsetype_),
+ PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(Node_default_oneof_instance_, scriptfilename_),
+ PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(Node_default_oneof_instance_, scriptfilenameref_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Node, TypeNameOrRef_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Node, JSObjectClassNameOrRef_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Node, ScriptFilenameOrRef_),
+ };
+ Node_reflection_ =
+ new ::google::protobuf::internal::GeneratedMessageReflection(
+ Node_descriptor_,
+ Node::default_instance_,
+ Node_offsets_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Node, _has_bits_[0]),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Node, _unknown_fields_),
+ -1,
+ Node_default_oneof_instance_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Node, _oneof_case_[0]),
+ ::google::protobuf::DescriptorPool::generated_pool(),
+ ::google::protobuf::MessageFactory::generated_factory(),
+ sizeof(Node));
+ Edge_descriptor_ = file->message_type(3);
+ static const int Edge_offsets_[4] = {
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Edge, referent_),
+ PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(Edge_default_oneof_instance_, name_),
+ PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(Edge_default_oneof_instance_, nameref_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Edge, EdgeNameOrRef_),
+ };
+ Edge_reflection_ =
+ new ::google::protobuf::internal::GeneratedMessageReflection(
+ Edge_descriptor_,
+ Edge::default_instance_,
+ Edge_offsets_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Edge, _has_bits_[0]),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Edge, _unknown_fields_),
+ -1,
+ Edge_default_oneof_instance_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Edge, _oneof_case_[0]),
+ ::google::protobuf::DescriptorPool::generated_pool(),
+ ::google::protobuf::MessageFactory::generated_factory(),
+ sizeof(Edge));
+}
+
+namespace {
+
+GOOGLE_PROTOBUF_DECLARE_ONCE(protobuf_AssignDescriptors_once_);
+inline void protobuf_AssignDescriptorsOnce() {
+ ::google::protobuf::GoogleOnceInit(&protobuf_AssignDescriptors_once_,
+ &protobuf_AssignDesc_CoreDump_2eproto);
+}
+
+void protobuf_RegisterTypes(const ::std::string&) {
+ protobuf_AssignDescriptorsOnce();
+ ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage(
+ Metadata_descriptor_, &Metadata::default_instance());
+ ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage(
+ StackFrame_descriptor_, &StackFrame::default_instance());
+ ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage(
+ StackFrame_Data_descriptor_, &StackFrame_Data::default_instance());
+ ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage(
+ Node_descriptor_, &Node::default_instance());
+ ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage(
+ Edge_descriptor_, &Edge::default_instance());
+}
+
+} // namespace
+
+void protobuf_ShutdownFile_CoreDump_2eproto() {
+ delete Metadata::default_instance_;
+ delete Metadata_reflection_;
+ delete StackFrame::default_instance_;
+ delete StackFrame_default_oneof_instance_;
+ delete StackFrame_reflection_;
+ delete StackFrame_Data::default_instance_;
+ delete StackFrame_Data_default_oneof_instance_;
+ delete StackFrame_Data_reflection_;
+ delete Node::default_instance_;
+ delete Node_default_oneof_instance_;
+ delete Node_reflection_;
+ delete Edge::default_instance_;
+ delete Edge_default_oneof_instance_;
+ delete Edge_reflection_;
+}
+
+void protobuf_AddDesc_CoreDump_2eproto() {
+ static bool already_here = false;
+ if (already_here) return;
+ already_here = true;
+ GOOGLE_PROTOBUF_VERIFY_VERSION;
+
+ ::google::protobuf::DescriptorPool::InternalAddGeneratedFile(
+ "\n\016CoreDump.proto\022\031mozilla.devtools.proto"
+ "buf\"\035\n\010Metadata\022\021\n\ttimeStamp\030\001 \001(\004\"\216\003\n\nS"
+ "tackFrame\022:\n\004data\030\001 \001(\0132*.mozilla.devtoo"
+ "ls.protobuf.StackFrame.DataH\000\022\r\n\003ref\030\002 \001"
+ "(\004H\000\032\242\002\n\004Data\022\n\n\002id\030\001 \001(\004\0225\n\006parent\030\002 \001("
+ "\0132%.mozilla.devtools.protobuf.StackFrame"
+ "\022\014\n\004line\030\003 \001(\r\022\016\n\006column\030\004 \001(\r\022\020\n\006source"
+ "\030\005 \001(\014H\000\022\023\n\tsourceRef\030\006 \001(\004H\000\022\035\n\023functio"
+ "nDisplayName\030\007 \001(\014H\001\022 \n\026functionDisplayN"
+ "ameRef\030\010 \001(\004H\001\022\020\n\010isSystem\030\t \001(\010\022\024\n\014isSe"
+ "lfHosted\030\n \001(\010B\r\n\013SourceOrRefB\032\n\030Functio"
+ "nDisplayNameOrRefB\020\n\016StackFrameType\"\210\003\n\004"
+ "Node\022\n\n\002id\030\001 \001(\004\022\022\n\010typeName\030\002 \001(\014H\000\022\025\n\013"
+ "typeNameRef\030\003 \001(\004H\000\022\014\n\004size\030\004 \001(\004\022.\n\005edg"
+ "es\030\005 \003(\0132\037.mozilla.devtools.protobuf.Edg"
+ "e\022>\n\017allocationStack\030\006 \001(\0132%.mozilla.dev"
+ "tools.protobuf.StackFrame\022\033\n\021jsObjectCla"
+ "ssName\030\007 \001(\014H\001\022\036\n\024jsObjectClassNameRef\030\010"
+ " \001(\004H\001\022\025\n\ncoarseType\030\t \001(\r:\0010\022\030\n\016scriptF"
+ "ilename\030\n \001(\014H\002\022\033\n\021scriptFilenameRef\030\013 \001"
+ "(\004H\002B\017\n\rTypeNameOrRefB\030\n\026JSObjectClassNa"
+ "meOrRefB\025\n\023ScriptFilenameOrRef\"L\n\004Edge\022\020"
+ "\n\010referent\030\001 \001(\004\022\016\n\004name\030\002 \001(\014H\000\022\021\n\007name"
+ "Ref\030\003 \001(\004H\000B\017\n\rEdgeNameOrRef", 948);
+ ::google::protobuf::MessageFactory::InternalRegisterGeneratedFile(
+ "CoreDump.proto", &protobuf_RegisterTypes);
+ Metadata::default_instance_ = new Metadata();
+ StackFrame::default_instance_ = new StackFrame();
+ StackFrame_default_oneof_instance_ = new StackFrameOneofInstance;
+ StackFrame_Data::default_instance_ = new StackFrame_Data();
+ StackFrame_Data_default_oneof_instance_ = new StackFrame_DataOneofInstance;
+ Node::default_instance_ = new Node();
+ Node_default_oneof_instance_ = new NodeOneofInstance;
+ Edge::default_instance_ = new Edge();
+ Edge_default_oneof_instance_ = new EdgeOneofInstance;
+ Metadata::default_instance_->InitAsDefaultInstance();
+ StackFrame::default_instance_->InitAsDefaultInstance();
+ StackFrame_Data::default_instance_->InitAsDefaultInstance();
+ Node::default_instance_->InitAsDefaultInstance();
+ Edge::default_instance_->InitAsDefaultInstance();
+ ::google::protobuf::internal::OnShutdown(&protobuf_ShutdownFile_CoreDump_2eproto);
+}
+
+// Force AddDescriptors() to be called at static initialization time.
+struct StaticDescriptorInitializer_CoreDump_2eproto {
+ StaticDescriptorInitializer_CoreDump_2eproto() {
+ protobuf_AddDesc_CoreDump_2eproto();
+ }
+} static_descriptor_initializer_CoreDump_2eproto_;
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int Metadata::kTimeStampFieldNumber;
+#endif // !_MSC_VER
+
+Metadata::Metadata()
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.devtools.protobuf.Metadata)
+}
+
+void Metadata::InitAsDefaultInstance() {
+}
+
+Metadata::Metadata(const Metadata& from)
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.devtools.protobuf.Metadata)
+}
+
+void Metadata::SharedCtor() {
+ _cached_size_ = 0;
+ timestamp_ = GOOGLE_ULONGLONG(0);
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+Metadata::~Metadata() {
+ // @@protoc_insertion_point(destructor:mozilla.devtools.protobuf.Metadata)
+ SharedDtor();
+}
+
+void Metadata::SharedDtor() {
+ if (this != default_instance_) {
+ }
+}
+
+void Metadata::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ::google::protobuf::Descriptor* Metadata::descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return Metadata_descriptor_;
+}
+
+const Metadata& Metadata::default_instance() {
+ if (default_instance_ == NULL) protobuf_AddDesc_CoreDump_2eproto();
+ return *default_instance_;
+}
+
+Metadata* Metadata::default_instance_ = NULL;
+
+Metadata* Metadata::New() const {
+ return new Metadata;
+}
+
+void Metadata::Clear() {
+ timestamp_ = GOOGLE_ULONGLONG(0);
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->Clear();
+}
+
+bool Metadata::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ // @@protoc_insertion_point(parse_start:mozilla.devtools.protobuf.Metadata)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional uint64 timeStamp = 1;
+ case 1: {
+ if (tag == 8) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::uint64, ::google::protobuf::internal::WireFormatLite::TYPE_UINT64>(
+ input, &timestamp_)));
+ set_has_timestamp();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormat::SkipField(
+ input, tag, mutable_unknown_fields()));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.devtools.protobuf.Metadata)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.devtools.protobuf.Metadata)
+ return false;
+#undef DO_
+}
+
+void Metadata::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.devtools.protobuf.Metadata)
+ // optional uint64 timeStamp = 1;
+ if (has_timestamp()) {
+ ::google::protobuf::internal::WireFormatLite::WriteUInt64(1, this->timestamp(), output);
+ }
+
+ if (!unknown_fields().empty()) {
+ ::google::protobuf::internal::WireFormat::SerializeUnknownFields(
+ unknown_fields(), output);
+ }
+ // @@protoc_insertion_point(serialize_end:mozilla.devtools.protobuf.Metadata)
+}
+
+::google::protobuf::uint8* Metadata::SerializeWithCachedSizesToArray(
+ ::google::protobuf::uint8* target) const {
+ // @@protoc_insertion_point(serialize_to_array_start:mozilla.devtools.protobuf.Metadata)
+ // optional uint64 timeStamp = 1;
+ if (has_timestamp()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteUInt64ToArray(1, this->timestamp(), target);
+ }
+
+ if (!unknown_fields().empty()) {
+ target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray(
+ unknown_fields(), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:mozilla.devtools.protobuf.Metadata)
+ return target;
+}
+
+int Metadata::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional uint64 timeStamp = 1;
+ if (has_timestamp()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::UInt64Size(
+ this->timestamp());
+ }
+
+ }
+ if (!unknown_fields().empty()) {
+ total_size +=
+ ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize(
+ unknown_fields());
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void Metadata::MergeFrom(const ::google::protobuf::Message& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ const Metadata* source =
+ ::google::protobuf::internal::dynamic_cast_if_available<const Metadata*>(
+ &from);
+ if (source == NULL) {
+ ::google::protobuf::internal::ReflectionOps::Merge(from, this);
+ } else {
+ MergeFrom(*source);
+ }
+}
+
+void Metadata::MergeFrom(const Metadata& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_timestamp()) {
+ set_timestamp(from.timestamp());
+ }
+ }
+ mutable_unknown_fields()->MergeFrom(from.unknown_fields());
+}
+
+void Metadata::CopyFrom(const ::google::protobuf::Message& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+void Metadata::CopyFrom(const Metadata& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool Metadata::IsInitialized() const {
+
+ return true;
+}
+
+void Metadata::Swap(Metadata* other) {
+ if (other != this) {
+ std::swap(timestamp_, other->timestamp_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.Swap(&other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::google::protobuf::Metadata Metadata::GetMetadata() const {
+ protobuf_AssignDescriptorsOnce();
+ ::google::protobuf::Metadata metadata;
+ metadata.descriptor = Metadata_descriptor_;
+ metadata.reflection = Metadata_reflection_;
+ return metadata;
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int StackFrame_Data::kIdFieldNumber;
+const int StackFrame_Data::kParentFieldNumber;
+const int StackFrame_Data::kLineFieldNumber;
+const int StackFrame_Data::kColumnFieldNumber;
+const int StackFrame_Data::kSourceFieldNumber;
+const int StackFrame_Data::kSourceRefFieldNumber;
+const int StackFrame_Data::kFunctionDisplayNameFieldNumber;
+const int StackFrame_Data::kFunctionDisplayNameRefFieldNumber;
+const int StackFrame_Data::kIsSystemFieldNumber;
+const int StackFrame_Data::kIsSelfHostedFieldNumber;
+#endif // !_MSC_VER
+
+StackFrame_Data::StackFrame_Data()
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.devtools.protobuf.StackFrame.Data)
+}
+
+void StackFrame_Data::InitAsDefaultInstance() {
+ parent_ = const_cast< ::mozilla::devtools::protobuf::StackFrame*>(&::mozilla::devtools::protobuf::StackFrame::default_instance());
+ StackFrame_Data_default_oneof_instance_->source_ = &::google::protobuf::internal::GetEmptyStringAlreadyInited();
+ StackFrame_Data_default_oneof_instance_->sourceref_ = GOOGLE_ULONGLONG(0);
+ StackFrame_Data_default_oneof_instance_->functiondisplayname_ = &::google::protobuf::internal::GetEmptyStringAlreadyInited();
+ StackFrame_Data_default_oneof_instance_->functiondisplaynameref_ = GOOGLE_ULONGLONG(0);
+}
+
+StackFrame_Data::StackFrame_Data(const StackFrame_Data& from)
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.devtools.protobuf.StackFrame.Data)
+}
+
+void StackFrame_Data::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ id_ = GOOGLE_ULONGLONG(0);
+ parent_ = NULL;
+ line_ = 0u;
+ column_ = 0u;
+ issystem_ = false;
+ isselfhosted_ = false;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ clear_has_SourceOrRef();
+ clear_has_FunctionDisplayNameOrRef();
+}
+
+StackFrame_Data::~StackFrame_Data() {
+ // @@protoc_insertion_point(destructor:mozilla.devtools.protobuf.StackFrame.Data)
+ SharedDtor();
+}
+
+void StackFrame_Data::SharedDtor() {
+ if (has_SourceOrRef()) {
+ clear_SourceOrRef();
+ }
+ if (has_FunctionDisplayNameOrRef()) {
+ clear_FunctionDisplayNameOrRef();
+ }
+ if (this != default_instance_) {
+ delete parent_;
+ }
+}
+
+void StackFrame_Data::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ::google::protobuf::Descriptor* StackFrame_Data::descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return StackFrame_Data_descriptor_;
+}
+
+const StackFrame_Data& StackFrame_Data::default_instance() {
+ if (default_instance_ == NULL) protobuf_AddDesc_CoreDump_2eproto();
+ return *default_instance_;
+}
+
+StackFrame_Data* StackFrame_Data::default_instance_ = NULL;
+
+StackFrame_Data* StackFrame_Data::New() const {
+ return new StackFrame_Data;
+}
+
+void StackFrame_Data::clear_SourceOrRef() {
+ switch(SourceOrRef_case()) {
+ case kSource: {
+ delete SourceOrRef_.source_;
+ break;
+ }
+ case kSourceRef: {
+ // No need to clear
+ break;
+ }
+ case SOURCEORREF_NOT_SET: {
+ break;
+ }
+ }
+ _oneof_case_[0] = SOURCEORREF_NOT_SET;
+}
+
+void StackFrame_Data::clear_FunctionDisplayNameOrRef() {
+ switch(FunctionDisplayNameOrRef_case()) {
+ case kFunctionDisplayName: {
+ delete FunctionDisplayNameOrRef_.functiondisplayname_;
+ break;
+ }
+ case kFunctionDisplayNameRef: {
+ // No need to clear
+ break;
+ }
+ case FUNCTIONDISPLAYNAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ _oneof_case_[1] = FUNCTIONDISPLAYNAMEORREF_NOT_SET;
+}
+
+
+void StackFrame_Data::Clear() {
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>( \
+ &reinterpret_cast<StackFrame_Data*>(16)->f) - \
+ reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do { \
+ size_t f = OFFSET_OF_FIELD_(first); \
+ size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last); \
+ ::memset(&first, 0, n); \
+ } while (0)
+
+ if (_has_bits_[0 / 32] & 15) {
+ ZR_(line_, column_);
+ id_ = GOOGLE_ULONGLONG(0);
+ if (has_parent()) {
+ if (parent_ != NULL) parent_->::mozilla::devtools::protobuf::StackFrame::Clear();
+ }
+ }
+ ZR_(issystem_, isselfhosted_);
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+ clear_SourceOrRef();
+ clear_FunctionDisplayNameOrRef();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->Clear();
+}
+
+bool StackFrame_Data::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ // @@protoc_insertion_point(parse_start:mozilla.devtools.protobuf.StackFrame.Data)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional uint64 id = 1;
+ case 1: {
+ if (tag == 8) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::uint64, ::google::protobuf::internal::WireFormatLite::TYPE_UINT64>(
+ input, &id_)));
+ set_has_id();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_parent;
+ break;
+ }
+
+ // optional .mozilla.devtools.protobuf.StackFrame parent = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_parent:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_parent()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(24)) goto parse_line;
+ break;
+ }
+
+ // optional uint32 line = 3;
+ case 3: {
+ if (tag == 24) {
+ parse_line:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::uint32, ::google::protobuf::internal::WireFormatLite::TYPE_UINT32>(
+ input, &line_)));
+ set_has_line();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(32)) goto parse_column;
+ break;
+ }
+
+ // optional uint32 column = 4;
+ case 4: {
+ if (tag == 32) {
+ parse_column:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::uint32, ::google::protobuf::internal::WireFormatLite::TYPE_UINT32>(
+ input, &column_)));
+ set_has_column();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(42)) goto parse_source;
+ break;
+ }
+
+ // optional bytes source = 5;
+ case 5: {
+ if (tag == 42) {
+ parse_source:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_source()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(48)) goto parse_sourceRef;
+ break;
+ }
+
+ // optional uint64 sourceRef = 6;
+ case 6: {
+ if (tag == 48) {
+ parse_sourceRef:
+ clear_SourceOrRef();
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::uint64, ::google::protobuf::internal::WireFormatLite::TYPE_UINT64>(
+ input, &SourceOrRef_.sourceref_)));
+ set_has_sourceref();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(58)) goto parse_functionDisplayName;
+ break;
+ }
+
+ // optional bytes functionDisplayName = 7;
+ case 7: {
+ if (tag == 58) {
+ parse_functionDisplayName:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_functiondisplayname()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(64)) goto parse_functionDisplayNameRef;
+ break;
+ }
+
+ // optional uint64 functionDisplayNameRef = 8;
+ case 8: {
+ if (tag == 64) {
+ parse_functionDisplayNameRef:
+ clear_FunctionDisplayNameOrRef();
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::uint64, ::google::protobuf::internal::WireFormatLite::TYPE_UINT64>(
+ input, &FunctionDisplayNameOrRef_.functiondisplaynameref_)));
+ set_has_functiondisplaynameref();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(72)) goto parse_isSystem;
+ break;
+ }
+
+ // optional bool isSystem = 9;
+ case 9: {
+ if (tag == 72) {
+ parse_isSystem:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &issystem_)));
+ set_has_issystem();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(80)) goto parse_isSelfHosted;
+ break;
+ }
+
+ // optional bool isSelfHosted = 10;
+ case 10: {
+ if (tag == 80) {
+ parse_isSelfHosted:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &isselfhosted_)));
+ set_has_isselfhosted();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormat::SkipField(
+ input, tag, mutable_unknown_fields()));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.devtools.protobuf.StackFrame.Data)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.devtools.protobuf.StackFrame.Data)
+ return false;
+#undef DO_
+}
+
+void StackFrame_Data::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.devtools.protobuf.StackFrame.Data)
+ // optional uint64 id = 1;
+ if (has_id()) {
+ ::google::protobuf::internal::WireFormatLite::WriteUInt64(1, this->id(), output);
+ }
+
+ // optional .mozilla.devtools.protobuf.StackFrame parent = 2;
+ if (has_parent()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 2, this->parent(), output);
+ }
+
+ // optional uint32 line = 3;
+ if (has_line()) {
+ ::google::protobuf::internal::WireFormatLite::WriteUInt32(3, this->line(), output);
+ }
+
+ // optional uint32 column = 4;
+ if (has_column()) {
+ ::google::protobuf::internal::WireFormatLite::WriteUInt32(4, this->column(), output);
+ }
+
+ // optional bytes source = 5;
+ if (has_source()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 5, this->source(), output);
+ }
+
+ // optional uint64 sourceRef = 6;
+ if (has_sourceref()) {
+ ::google::protobuf::internal::WireFormatLite::WriteUInt64(6, this->sourceref(), output);
+ }
+
+ // optional bytes functionDisplayName = 7;
+ if (has_functiondisplayname()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 7, this->functiondisplayname(), output);
+ }
+
+ // optional uint64 functionDisplayNameRef = 8;
+ if (has_functiondisplaynameref()) {
+ ::google::protobuf::internal::WireFormatLite::WriteUInt64(8, this->functiondisplaynameref(), output);
+ }
+
+ // optional bool isSystem = 9;
+ if (has_issystem()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(9, this->issystem(), output);
+ }
+
+ // optional bool isSelfHosted = 10;
+ if (has_isselfhosted()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(10, this->isselfhosted(), output);
+ }
+
+ if (!unknown_fields().empty()) {
+ ::google::protobuf::internal::WireFormat::SerializeUnknownFields(
+ unknown_fields(), output);
+ }
+ // @@protoc_insertion_point(serialize_end:mozilla.devtools.protobuf.StackFrame.Data)
+}
+
+::google::protobuf::uint8* StackFrame_Data::SerializeWithCachedSizesToArray(
+ ::google::protobuf::uint8* target) const {
+ // @@protoc_insertion_point(serialize_to_array_start:mozilla.devtools.protobuf.StackFrame.Data)
+ // optional uint64 id = 1;
+ if (has_id()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteUInt64ToArray(1, this->id(), target);
+ }
+
+ // optional .mozilla.devtools.protobuf.StackFrame parent = 2;
+ if (has_parent()) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 2, this->parent(), target);
+ }
+
+ // optional uint32 line = 3;
+ if (has_line()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteUInt32ToArray(3, this->line(), target);
+ }
+
+ // optional uint32 column = 4;
+ if (has_column()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteUInt32ToArray(4, this->column(), target);
+ }
+
+ // optional bytes source = 5;
+ if (has_source()) {
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteBytesToArray(
+ 5, this->source(), target);
+ }
+
+ // optional uint64 sourceRef = 6;
+ if (has_sourceref()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteUInt64ToArray(6, this->sourceref(), target);
+ }
+
+ // optional bytes functionDisplayName = 7;
+ if (has_functiondisplayname()) {
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteBytesToArray(
+ 7, this->functiondisplayname(), target);
+ }
+
+ // optional uint64 functionDisplayNameRef = 8;
+ if (has_functiondisplaynameref()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteUInt64ToArray(8, this->functiondisplaynameref(), target);
+ }
+
+ // optional bool isSystem = 9;
+ if (has_issystem()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteBoolToArray(9, this->issystem(), target);
+ }
+
+ // optional bool isSelfHosted = 10;
+ if (has_isselfhosted()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteBoolToArray(10, this->isselfhosted(), target);
+ }
+
+ if (!unknown_fields().empty()) {
+ target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray(
+ unknown_fields(), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:mozilla.devtools.protobuf.StackFrame.Data)
+ return target;
+}
+
+int StackFrame_Data::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional uint64 id = 1;
+ if (has_id()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::UInt64Size(
+ this->id());
+ }
+
+ // optional .mozilla.devtools.protobuf.StackFrame parent = 2;
+ if (has_parent()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->parent());
+ }
+
+ // optional uint32 line = 3;
+ if (has_line()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::UInt32Size(
+ this->line());
+ }
+
+ // optional uint32 column = 4;
+ if (has_column()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::UInt32Size(
+ this->column());
+ }
+
+ }
+ if (_has_bits_[8 / 32] & (0xffu << (8 % 32))) {
+ // optional bool isSystem = 9;
+ if (has_issystem()) {
+ total_size += 1 + 1;
+ }
+
+ // optional bool isSelfHosted = 10;
+ if (has_isselfhosted()) {
+ total_size += 1 + 1;
+ }
+
+ }
+ switch (SourceOrRef_case()) {
+ // optional bytes source = 5;
+ case kSource: {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->source());
+ break;
+ }
+ // optional uint64 sourceRef = 6;
+ case kSourceRef: {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::UInt64Size(
+ this->sourceref());
+ break;
+ }
+ case SOURCEORREF_NOT_SET: {
+ break;
+ }
+ }
+ switch (FunctionDisplayNameOrRef_case()) {
+ // optional bytes functionDisplayName = 7;
+ case kFunctionDisplayName: {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->functiondisplayname());
+ break;
+ }
+ // optional uint64 functionDisplayNameRef = 8;
+ case kFunctionDisplayNameRef: {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::UInt64Size(
+ this->functiondisplaynameref());
+ break;
+ }
+ case FUNCTIONDISPLAYNAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ if (!unknown_fields().empty()) {
+ total_size +=
+ ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize(
+ unknown_fields());
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void StackFrame_Data::MergeFrom(const ::google::protobuf::Message& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ const StackFrame_Data* source =
+ ::google::protobuf::internal::dynamic_cast_if_available<const StackFrame_Data*>(
+ &from);
+ if (source == NULL) {
+ ::google::protobuf::internal::ReflectionOps::Merge(from, this);
+ } else {
+ MergeFrom(*source);
+ }
+}
+
+void StackFrame_Data::MergeFrom(const StackFrame_Data& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ switch (from.SourceOrRef_case()) {
+ case kSource: {
+ set_source(from.source());
+ break;
+ }
+ case kSourceRef: {
+ set_sourceref(from.sourceref());
+ break;
+ }
+ case SOURCEORREF_NOT_SET: {
+ break;
+ }
+ }
+ switch (from.FunctionDisplayNameOrRef_case()) {
+ case kFunctionDisplayName: {
+ set_functiondisplayname(from.functiondisplayname());
+ break;
+ }
+ case kFunctionDisplayNameRef: {
+ set_functiondisplaynameref(from.functiondisplaynameref());
+ break;
+ }
+ case FUNCTIONDISPLAYNAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_id()) {
+ set_id(from.id());
+ }
+ if (from.has_parent()) {
+ mutable_parent()->::mozilla::devtools::protobuf::StackFrame::MergeFrom(from.parent());
+ }
+ if (from.has_line()) {
+ set_line(from.line());
+ }
+ if (from.has_column()) {
+ set_column(from.column());
+ }
+ }
+ if (from._has_bits_[8 / 32] & (0xffu << (8 % 32))) {
+ if (from.has_issystem()) {
+ set_issystem(from.issystem());
+ }
+ if (from.has_isselfhosted()) {
+ set_isselfhosted(from.isselfhosted());
+ }
+ }
+ mutable_unknown_fields()->MergeFrom(from.unknown_fields());
+}
+
+void StackFrame_Data::CopyFrom(const ::google::protobuf::Message& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+void StackFrame_Data::CopyFrom(const StackFrame_Data& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool StackFrame_Data::IsInitialized() const {
+
+ return true;
+}
+
+void StackFrame_Data::Swap(StackFrame_Data* other) {
+ if (other != this) {
+ std::swap(id_, other->id_);
+ std::swap(parent_, other->parent_);
+ std::swap(line_, other->line_);
+ std::swap(column_, other->column_);
+ std::swap(issystem_, other->issystem_);
+ std::swap(isselfhosted_, other->isselfhosted_);
+ std::swap(SourceOrRef_, other->SourceOrRef_);
+ std::swap(_oneof_case_[0], other->_oneof_case_[0]);
+ std::swap(FunctionDisplayNameOrRef_, other->FunctionDisplayNameOrRef_);
+ std::swap(_oneof_case_[1], other->_oneof_case_[1]);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.Swap(&other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::google::protobuf::Metadata StackFrame_Data::GetMetadata() const {
+ protobuf_AssignDescriptorsOnce();
+ ::google::protobuf::Metadata metadata;
+ metadata.descriptor = StackFrame_Data_descriptor_;
+ metadata.reflection = StackFrame_Data_reflection_;
+ return metadata;
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int StackFrame::kDataFieldNumber;
+const int StackFrame::kRefFieldNumber;
+#endif // !_MSC_VER
+
+StackFrame::StackFrame()
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.devtools.protobuf.StackFrame)
+}
+
+void StackFrame::InitAsDefaultInstance() {
+ StackFrame_default_oneof_instance_->data_ = const_cast< ::mozilla::devtools::protobuf::StackFrame_Data*>(&::mozilla::devtools::protobuf::StackFrame_Data::default_instance());
+ StackFrame_default_oneof_instance_->ref_ = GOOGLE_ULONGLONG(0);
+}
+
+StackFrame::StackFrame(const StackFrame& from)
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.devtools.protobuf.StackFrame)
+}
+
+void StackFrame::SharedCtor() {
+ _cached_size_ = 0;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ clear_has_StackFrameType();
+}
+
+StackFrame::~StackFrame() {
+ // @@protoc_insertion_point(destructor:mozilla.devtools.protobuf.StackFrame)
+ SharedDtor();
+}
+
+void StackFrame::SharedDtor() {
+ if (has_StackFrameType()) {
+ clear_StackFrameType();
+ }
+ if (this != default_instance_) {
+ }
+}
+
+void StackFrame::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ::google::protobuf::Descriptor* StackFrame::descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return StackFrame_descriptor_;
+}
+
+const StackFrame& StackFrame::default_instance() {
+ if (default_instance_ == NULL) protobuf_AddDesc_CoreDump_2eproto();
+ return *default_instance_;
+}
+
+StackFrame* StackFrame::default_instance_ = NULL;
+
+StackFrame* StackFrame::New() const {
+ return new StackFrame;
+}
+
+void StackFrame::clear_StackFrameType() {
+ switch(StackFrameType_case()) {
+ case kData: {
+ delete StackFrameType_.data_;
+ break;
+ }
+ case kRef: {
+ // No need to clear
+ break;
+ }
+ case STACKFRAMETYPE_NOT_SET: {
+ break;
+ }
+ }
+ _oneof_case_[0] = STACKFRAMETYPE_NOT_SET;
+}
+
+
+void StackFrame::Clear() {
+ clear_StackFrameType();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->Clear();
+}
+
+bool StackFrame::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ // @@protoc_insertion_point(parse_start:mozilla.devtools.protobuf.StackFrame)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional .mozilla.devtools.protobuf.StackFrame.Data data = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_data()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(16)) goto parse_ref;
+ break;
+ }
+
+ // optional uint64 ref = 2;
+ case 2: {
+ if (tag == 16) {
+ parse_ref:
+ clear_StackFrameType();
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::uint64, ::google::protobuf::internal::WireFormatLite::TYPE_UINT64>(
+ input, &StackFrameType_.ref_)));
+ set_has_ref();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormat::SkipField(
+ input, tag, mutable_unknown_fields()));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.devtools.protobuf.StackFrame)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.devtools.protobuf.StackFrame)
+ return false;
+#undef DO_
+}
+
+void StackFrame::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.devtools.protobuf.StackFrame)
+ // optional .mozilla.devtools.protobuf.StackFrame.Data data = 1;
+ if (has_data()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 1, this->data(), output);
+ }
+
+ // optional uint64 ref = 2;
+ if (has_ref()) {
+ ::google::protobuf::internal::WireFormatLite::WriteUInt64(2, this->ref(), output);
+ }
+
+ if (!unknown_fields().empty()) {
+ ::google::protobuf::internal::WireFormat::SerializeUnknownFields(
+ unknown_fields(), output);
+ }
+ // @@protoc_insertion_point(serialize_end:mozilla.devtools.protobuf.StackFrame)
+}
+
+::google::protobuf::uint8* StackFrame::SerializeWithCachedSizesToArray(
+ ::google::protobuf::uint8* target) const {
+ // @@protoc_insertion_point(serialize_to_array_start:mozilla.devtools.protobuf.StackFrame)
+ // optional .mozilla.devtools.protobuf.StackFrame.Data data = 1;
+ if (has_data()) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 1, this->data(), target);
+ }
+
+ // optional uint64 ref = 2;
+ if (has_ref()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteUInt64ToArray(2, this->ref(), target);
+ }
+
+ if (!unknown_fields().empty()) {
+ target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray(
+ unknown_fields(), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:mozilla.devtools.protobuf.StackFrame)
+ return target;
+}
+
+int StackFrame::ByteSize() const {
+ int total_size = 0;
+
+ switch (StackFrameType_case()) {
+ // optional .mozilla.devtools.protobuf.StackFrame.Data data = 1;
+ case kData: {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->data());
+ break;
+ }
+ // optional uint64 ref = 2;
+ case kRef: {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::UInt64Size(
+ this->ref());
+ break;
+ }
+ case STACKFRAMETYPE_NOT_SET: {
+ break;
+ }
+ }
+ if (!unknown_fields().empty()) {
+ total_size +=
+ ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize(
+ unknown_fields());
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void StackFrame::MergeFrom(const ::google::protobuf::Message& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ const StackFrame* source =
+ ::google::protobuf::internal::dynamic_cast_if_available<const StackFrame*>(
+ &from);
+ if (source == NULL) {
+ ::google::protobuf::internal::ReflectionOps::Merge(from, this);
+ } else {
+ MergeFrom(*source);
+ }
+}
+
+void StackFrame::MergeFrom(const StackFrame& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ switch (from.StackFrameType_case()) {
+ case kData: {
+ mutable_data()->::mozilla::devtools::protobuf::StackFrame_Data::MergeFrom(from.data());
+ break;
+ }
+ case kRef: {
+ set_ref(from.ref());
+ break;
+ }
+ case STACKFRAMETYPE_NOT_SET: {
+ break;
+ }
+ }
+ mutable_unknown_fields()->MergeFrom(from.unknown_fields());
+}
+
+void StackFrame::CopyFrom(const ::google::protobuf::Message& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+void StackFrame::CopyFrom(const StackFrame& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool StackFrame::IsInitialized() const {
+
+ return true;
+}
+
+void StackFrame::Swap(StackFrame* other) {
+ if (other != this) {
+ std::swap(StackFrameType_, other->StackFrameType_);
+ std::swap(_oneof_case_[0], other->_oneof_case_[0]);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.Swap(&other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::google::protobuf::Metadata StackFrame::GetMetadata() const {
+ protobuf_AssignDescriptorsOnce();
+ ::google::protobuf::Metadata metadata;
+ metadata.descriptor = StackFrame_descriptor_;
+ metadata.reflection = StackFrame_reflection_;
+ return metadata;
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int Node::kIdFieldNumber;
+const int Node::kTypeNameFieldNumber;
+const int Node::kTypeNameRefFieldNumber;
+const int Node::kSizeFieldNumber;
+const int Node::kEdgesFieldNumber;
+const int Node::kAllocationStackFieldNumber;
+const int Node::kJsObjectClassNameFieldNumber;
+const int Node::kJsObjectClassNameRefFieldNumber;
+const int Node::kCoarseTypeFieldNumber;
+const int Node::kScriptFilenameFieldNumber;
+const int Node::kScriptFilenameRefFieldNumber;
+#endif // !_MSC_VER
+
+Node::Node()
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.devtools.protobuf.Node)
+}
+
+void Node::InitAsDefaultInstance() {
+ Node_default_oneof_instance_->typename__ = &::google::protobuf::internal::GetEmptyStringAlreadyInited();
+ Node_default_oneof_instance_->typenameref_ = GOOGLE_ULONGLONG(0);
+ allocationstack_ = const_cast< ::mozilla::devtools::protobuf::StackFrame*>(&::mozilla::devtools::protobuf::StackFrame::default_instance());
+ Node_default_oneof_instance_->jsobjectclassname_ = &::google::protobuf::internal::GetEmptyStringAlreadyInited();
+ Node_default_oneof_instance_->jsobjectclassnameref_ = GOOGLE_ULONGLONG(0);
+ Node_default_oneof_instance_->scriptfilename_ = &::google::protobuf::internal::GetEmptyStringAlreadyInited();
+ Node_default_oneof_instance_->scriptfilenameref_ = GOOGLE_ULONGLONG(0);
+}
+
+Node::Node(const Node& from)
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.devtools.protobuf.Node)
+}
+
+void Node::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ id_ = GOOGLE_ULONGLONG(0);
+ size_ = GOOGLE_ULONGLONG(0);
+ allocationstack_ = NULL;
+ coarsetype_ = 0u;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ clear_has_TypeNameOrRef();
+ clear_has_JSObjectClassNameOrRef();
+ clear_has_ScriptFilenameOrRef();
+}
+
+Node::~Node() {
+ // @@protoc_insertion_point(destructor:mozilla.devtools.protobuf.Node)
+ SharedDtor();
+}
+
+void Node::SharedDtor() {
+ if (has_TypeNameOrRef()) {
+ clear_TypeNameOrRef();
+ }
+ if (has_JSObjectClassNameOrRef()) {
+ clear_JSObjectClassNameOrRef();
+ }
+ if (has_ScriptFilenameOrRef()) {
+ clear_ScriptFilenameOrRef();
+ }
+ if (this != default_instance_) {
+ delete allocationstack_;
+ }
+}
+
+void Node::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ::google::protobuf::Descriptor* Node::descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return Node_descriptor_;
+}
+
+const Node& Node::default_instance() {
+ if (default_instance_ == NULL) protobuf_AddDesc_CoreDump_2eproto();
+ return *default_instance_;
+}
+
+Node* Node::default_instance_ = NULL;
+
+Node* Node::New() const {
+ return new Node;
+}
+
+void Node::clear_TypeNameOrRef() {
+ switch(TypeNameOrRef_case()) {
+ case kTypeName: {
+ delete TypeNameOrRef_.typename__;
+ break;
+ }
+ case kTypeNameRef: {
+ // No need to clear
+ break;
+ }
+ case TYPENAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ _oneof_case_[0] = TYPENAMEORREF_NOT_SET;
+}
+
+void Node::clear_JSObjectClassNameOrRef() {
+ switch(JSObjectClassNameOrRef_case()) {
+ case kJsObjectClassName: {
+ delete JSObjectClassNameOrRef_.jsobjectclassname_;
+ break;
+ }
+ case kJsObjectClassNameRef: {
+ // No need to clear
+ break;
+ }
+ case JSOBJECTCLASSNAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ _oneof_case_[1] = JSOBJECTCLASSNAMEORREF_NOT_SET;
+}
+
+void Node::clear_ScriptFilenameOrRef() {
+ switch(ScriptFilenameOrRef_case()) {
+ case kScriptFilename: {
+ delete ScriptFilenameOrRef_.scriptfilename_;
+ break;
+ }
+ case kScriptFilenameRef: {
+ // No need to clear
+ break;
+ }
+ case SCRIPTFILENAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ _oneof_case_[2] = SCRIPTFILENAMEORREF_NOT_SET;
+}
+
+
+void Node::Clear() {
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>( \
+ &reinterpret_cast<Node*>(16)->f) - \
+ reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do { \
+ size_t f = OFFSET_OF_FIELD_(first); \
+ size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last); \
+ ::memset(&first, 0, n); \
+ } while (0)
+
+ if (_has_bits_[0 / 32] & 41) {
+ ZR_(id_, size_);
+ if (has_allocationstack()) {
+ if (allocationstack_ != NULL) allocationstack_->::mozilla::devtools::protobuf::StackFrame::Clear();
+ }
+ }
+ coarsetype_ = 0u;
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+ edges_.Clear();
+ clear_TypeNameOrRef();
+ clear_JSObjectClassNameOrRef();
+ clear_ScriptFilenameOrRef();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->Clear();
+}
+
+bool Node::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ // @@protoc_insertion_point(parse_start:mozilla.devtools.protobuf.Node)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional uint64 id = 1;
+ case 1: {
+ if (tag == 8) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::uint64, ::google::protobuf::internal::WireFormatLite::TYPE_UINT64>(
+ input, &id_)));
+ set_has_id();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_typeName;
+ break;
+ }
+
+ // optional bytes typeName = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_typeName:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_typename_()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(24)) goto parse_typeNameRef;
+ break;
+ }
+
+ // optional uint64 typeNameRef = 3;
+ case 3: {
+ if (tag == 24) {
+ parse_typeNameRef:
+ clear_TypeNameOrRef();
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::uint64, ::google::protobuf::internal::WireFormatLite::TYPE_UINT64>(
+ input, &TypeNameOrRef_.typenameref_)));
+ set_has_typenameref();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(32)) goto parse_size;
+ break;
+ }
+
+ // optional uint64 size = 4;
+ case 4: {
+ if (tag == 32) {
+ parse_size:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::uint64, ::google::protobuf::internal::WireFormatLite::TYPE_UINT64>(
+ input, &size_)));
+ set_has_size();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(42)) goto parse_edges;
+ break;
+ }
+
+ // repeated .mozilla.devtools.protobuf.Edge edges = 5;
+ case 5: {
+ if (tag == 42) {
+ parse_edges:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_edges()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(42)) goto parse_edges;
+ if (input->ExpectTag(50)) goto parse_allocationStack;
+ break;
+ }
+
+ // optional .mozilla.devtools.protobuf.StackFrame allocationStack = 6;
+ case 6: {
+ if (tag == 50) {
+ parse_allocationStack:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_allocationstack()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(58)) goto parse_jsObjectClassName;
+ break;
+ }
+
+ // optional bytes jsObjectClassName = 7;
+ case 7: {
+ if (tag == 58) {
+ parse_jsObjectClassName:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_jsobjectclassname()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(64)) goto parse_jsObjectClassNameRef;
+ break;
+ }
+
+ // optional uint64 jsObjectClassNameRef = 8;
+ case 8: {
+ if (tag == 64) {
+ parse_jsObjectClassNameRef:
+ clear_JSObjectClassNameOrRef();
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::uint64, ::google::protobuf::internal::WireFormatLite::TYPE_UINT64>(
+ input, &JSObjectClassNameOrRef_.jsobjectclassnameref_)));
+ set_has_jsobjectclassnameref();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(72)) goto parse_coarseType;
+ break;
+ }
+
+ // optional uint32 coarseType = 9 [default = 0];
+ case 9: {
+ if (tag == 72) {
+ parse_coarseType:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::uint32, ::google::protobuf::internal::WireFormatLite::TYPE_UINT32>(
+ input, &coarsetype_)));
+ set_has_coarsetype();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(82)) goto parse_scriptFilename;
+ break;
+ }
+
+ // optional bytes scriptFilename = 10;
+ case 10: {
+ if (tag == 82) {
+ parse_scriptFilename:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_scriptfilename()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(88)) goto parse_scriptFilenameRef;
+ break;
+ }
+
+ // optional uint64 scriptFilenameRef = 11;
+ case 11: {
+ if (tag == 88) {
+ parse_scriptFilenameRef:
+ clear_ScriptFilenameOrRef();
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::uint64, ::google::protobuf::internal::WireFormatLite::TYPE_UINT64>(
+ input, &ScriptFilenameOrRef_.scriptfilenameref_)));
+ set_has_scriptfilenameref();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormat::SkipField(
+ input, tag, mutable_unknown_fields()));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.devtools.protobuf.Node)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.devtools.protobuf.Node)
+ return false;
+#undef DO_
+}
+
+void Node::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.devtools.protobuf.Node)
+ // optional uint64 id = 1;
+ if (has_id()) {
+ ::google::protobuf::internal::WireFormatLite::WriteUInt64(1, this->id(), output);
+ }
+
+ // optional bytes typeName = 2;
+ if (has_typename_()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 2, this->typename_(), output);
+ }
+
+ // optional uint64 typeNameRef = 3;
+ if (has_typenameref()) {
+ ::google::protobuf::internal::WireFormatLite::WriteUInt64(3, this->typenameref(), output);
+ }
+
+ // optional uint64 size = 4;
+ if (has_size()) {
+ ::google::protobuf::internal::WireFormatLite::WriteUInt64(4, this->size(), output);
+ }
+
+ // repeated .mozilla.devtools.protobuf.Edge edges = 5;
+ for (int i = 0; i < this->edges_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 5, this->edges(i), output);
+ }
+
+ // optional .mozilla.devtools.protobuf.StackFrame allocationStack = 6;
+ if (has_allocationstack()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 6, this->allocationstack(), output);
+ }
+
+ // optional bytes jsObjectClassName = 7;
+ if (has_jsobjectclassname()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 7, this->jsobjectclassname(), output);
+ }
+
+ // optional uint64 jsObjectClassNameRef = 8;
+ if (has_jsobjectclassnameref()) {
+ ::google::protobuf::internal::WireFormatLite::WriteUInt64(8, this->jsobjectclassnameref(), output);
+ }
+
+ // optional uint32 coarseType = 9 [default = 0];
+ if (has_coarsetype()) {
+ ::google::protobuf::internal::WireFormatLite::WriteUInt32(9, this->coarsetype(), output);
+ }
+
+ // optional bytes scriptFilename = 10;
+ if (has_scriptfilename()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 10, this->scriptfilename(), output);
+ }
+
+ // optional uint64 scriptFilenameRef = 11;
+ if (has_scriptfilenameref()) {
+ ::google::protobuf::internal::WireFormatLite::WriteUInt64(11, this->scriptfilenameref(), output);
+ }
+
+ if (!unknown_fields().empty()) {
+ ::google::protobuf::internal::WireFormat::SerializeUnknownFields(
+ unknown_fields(), output);
+ }
+ // @@protoc_insertion_point(serialize_end:mozilla.devtools.protobuf.Node)
+}
+
+::google::protobuf::uint8* Node::SerializeWithCachedSizesToArray(
+ ::google::protobuf::uint8* target) const {
+ // @@protoc_insertion_point(serialize_to_array_start:mozilla.devtools.protobuf.Node)
+ // optional uint64 id = 1;
+ if (has_id()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteUInt64ToArray(1, this->id(), target);
+ }
+
+ // optional bytes typeName = 2;
+ if (has_typename_()) {
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteBytesToArray(
+ 2, this->typename_(), target);
+ }
+
+ // optional uint64 typeNameRef = 3;
+ if (has_typenameref()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteUInt64ToArray(3, this->typenameref(), target);
+ }
+
+ // optional uint64 size = 4;
+ if (has_size()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteUInt64ToArray(4, this->size(), target);
+ }
+
+ // repeated .mozilla.devtools.protobuf.Edge edges = 5;
+ for (int i = 0; i < this->edges_size(); i++) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 5, this->edges(i), target);
+ }
+
+ // optional .mozilla.devtools.protobuf.StackFrame allocationStack = 6;
+ if (has_allocationstack()) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 6, this->allocationstack(), target);
+ }
+
+ // optional bytes jsObjectClassName = 7;
+ if (has_jsobjectclassname()) {
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteBytesToArray(
+ 7, this->jsobjectclassname(), target);
+ }
+
+ // optional uint64 jsObjectClassNameRef = 8;
+ if (has_jsobjectclassnameref()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteUInt64ToArray(8, this->jsobjectclassnameref(), target);
+ }
+
+ // optional uint32 coarseType = 9 [default = 0];
+ if (has_coarsetype()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteUInt32ToArray(9, this->coarsetype(), target);
+ }
+
+ // optional bytes scriptFilename = 10;
+ if (has_scriptfilename()) {
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteBytesToArray(
+ 10, this->scriptfilename(), target);
+ }
+
+ // optional uint64 scriptFilenameRef = 11;
+ if (has_scriptfilenameref()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteUInt64ToArray(11, this->scriptfilenameref(), target);
+ }
+
+ if (!unknown_fields().empty()) {
+ target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray(
+ unknown_fields(), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:mozilla.devtools.protobuf.Node)
+ return target;
+}
+
+int Node::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional uint64 id = 1;
+ if (has_id()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::UInt64Size(
+ this->id());
+ }
+
+ // optional uint64 size = 4;
+ if (has_size()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::UInt64Size(
+ this->size());
+ }
+
+ // optional .mozilla.devtools.protobuf.StackFrame allocationStack = 6;
+ if (has_allocationstack()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->allocationstack());
+ }
+
+ }
+ if (_has_bits_[8 / 32] & (0xffu << (8 % 32))) {
+ // optional uint32 coarseType = 9 [default = 0];
+ if (has_coarsetype()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::UInt32Size(
+ this->coarsetype());
+ }
+
+ }
+ // repeated .mozilla.devtools.protobuf.Edge edges = 5;
+ total_size += 1 * this->edges_size();
+ for (int i = 0; i < this->edges_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->edges(i));
+ }
+
+ switch (TypeNameOrRef_case()) {
+ // optional bytes typeName = 2;
+ case kTypeName: {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->typename_());
+ break;
+ }
+ // optional uint64 typeNameRef = 3;
+ case kTypeNameRef: {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::UInt64Size(
+ this->typenameref());
+ break;
+ }
+ case TYPENAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ switch (JSObjectClassNameOrRef_case()) {
+ // optional bytes jsObjectClassName = 7;
+ case kJsObjectClassName: {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->jsobjectclassname());
+ break;
+ }
+ // optional uint64 jsObjectClassNameRef = 8;
+ case kJsObjectClassNameRef: {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::UInt64Size(
+ this->jsobjectclassnameref());
+ break;
+ }
+ case JSOBJECTCLASSNAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ switch (ScriptFilenameOrRef_case()) {
+ // optional bytes scriptFilename = 10;
+ case kScriptFilename: {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->scriptfilename());
+ break;
+ }
+ // optional uint64 scriptFilenameRef = 11;
+ case kScriptFilenameRef: {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::UInt64Size(
+ this->scriptfilenameref());
+ break;
+ }
+ case SCRIPTFILENAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ if (!unknown_fields().empty()) {
+ total_size +=
+ ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize(
+ unknown_fields());
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void Node::MergeFrom(const ::google::protobuf::Message& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ const Node* source =
+ ::google::protobuf::internal::dynamic_cast_if_available<const Node*>(
+ &from);
+ if (source == NULL) {
+ ::google::protobuf::internal::ReflectionOps::Merge(from, this);
+ } else {
+ MergeFrom(*source);
+ }
+}
+
+void Node::MergeFrom(const Node& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ edges_.MergeFrom(from.edges_);
+ switch (from.TypeNameOrRef_case()) {
+ case kTypeName: {
+ set_typename_(from.typename_());
+ break;
+ }
+ case kTypeNameRef: {
+ set_typenameref(from.typenameref());
+ break;
+ }
+ case TYPENAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ switch (from.JSObjectClassNameOrRef_case()) {
+ case kJsObjectClassName: {
+ set_jsobjectclassname(from.jsobjectclassname());
+ break;
+ }
+ case kJsObjectClassNameRef: {
+ set_jsobjectclassnameref(from.jsobjectclassnameref());
+ break;
+ }
+ case JSOBJECTCLASSNAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ switch (from.ScriptFilenameOrRef_case()) {
+ case kScriptFilename: {
+ set_scriptfilename(from.scriptfilename());
+ break;
+ }
+ case kScriptFilenameRef: {
+ set_scriptfilenameref(from.scriptfilenameref());
+ break;
+ }
+ case SCRIPTFILENAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_id()) {
+ set_id(from.id());
+ }
+ if (from.has_size()) {
+ set_size(from.size());
+ }
+ if (from.has_allocationstack()) {
+ mutable_allocationstack()->::mozilla::devtools::protobuf::StackFrame::MergeFrom(from.allocationstack());
+ }
+ }
+ if (from._has_bits_[8 / 32] & (0xffu << (8 % 32))) {
+ if (from.has_coarsetype()) {
+ set_coarsetype(from.coarsetype());
+ }
+ }
+ mutable_unknown_fields()->MergeFrom(from.unknown_fields());
+}
+
+void Node::CopyFrom(const ::google::protobuf::Message& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+void Node::CopyFrom(const Node& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool Node::IsInitialized() const {
+
+ return true;
+}
+
+void Node::Swap(Node* other) {
+ if (other != this) {
+ std::swap(id_, other->id_);
+ std::swap(size_, other->size_);
+ edges_.Swap(&other->edges_);
+ std::swap(allocationstack_, other->allocationstack_);
+ std::swap(coarsetype_, other->coarsetype_);
+ std::swap(TypeNameOrRef_, other->TypeNameOrRef_);
+ std::swap(_oneof_case_[0], other->_oneof_case_[0]);
+ std::swap(JSObjectClassNameOrRef_, other->JSObjectClassNameOrRef_);
+ std::swap(_oneof_case_[1], other->_oneof_case_[1]);
+ std::swap(ScriptFilenameOrRef_, other->ScriptFilenameOrRef_);
+ std::swap(_oneof_case_[2], other->_oneof_case_[2]);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.Swap(&other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::google::protobuf::Metadata Node::GetMetadata() const {
+ protobuf_AssignDescriptorsOnce();
+ ::google::protobuf::Metadata metadata;
+ metadata.descriptor = Node_descriptor_;
+ metadata.reflection = Node_reflection_;
+ return metadata;
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int Edge::kReferentFieldNumber;
+const int Edge::kNameFieldNumber;
+const int Edge::kNameRefFieldNumber;
+#endif // !_MSC_VER
+
+Edge::Edge()
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.devtools.protobuf.Edge)
+}
+
+void Edge::InitAsDefaultInstance() {
+ Edge_default_oneof_instance_->name_ = &::google::protobuf::internal::GetEmptyStringAlreadyInited();
+ Edge_default_oneof_instance_->nameref_ = GOOGLE_ULONGLONG(0);
+}
+
+Edge::Edge(const Edge& from)
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.devtools.protobuf.Edge)
+}
+
+void Edge::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ referent_ = GOOGLE_ULONGLONG(0);
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ clear_has_EdgeNameOrRef();
+}
+
+Edge::~Edge() {
+ // @@protoc_insertion_point(destructor:mozilla.devtools.protobuf.Edge)
+ SharedDtor();
+}
+
+void Edge::SharedDtor() {
+ if (has_EdgeNameOrRef()) {
+ clear_EdgeNameOrRef();
+ }
+ if (this != default_instance_) {
+ }
+}
+
+void Edge::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ::google::protobuf::Descriptor* Edge::descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return Edge_descriptor_;
+}
+
+const Edge& Edge::default_instance() {
+ if (default_instance_ == NULL) protobuf_AddDesc_CoreDump_2eproto();
+ return *default_instance_;
+}
+
+Edge* Edge::default_instance_ = NULL;
+
+Edge* Edge::New() const {
+ return new Edge;
+}
+
+void Edge::clear_EdgeNameOrRef() {
+ switch(EdgeNameOrRef_case()) {
+ case kName: {
+ delete EdgeNameOrRef_.name_;
+ break;
+ }
+ case kNameRef: {
+ // No need to clear
+ break;
+ }
+ case EDGENAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ _oneof_case_[0] = EDGENAMEORREF_NOT_SET;
+}
+
+
+void Edge::Clear() {
+ referent_ = GOOGLE_ULONGLONG(0);
+ clear_EdgeNameOrRef();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->Clear();
+}
+
+bool Edge::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ // @@protoc_insertion_point(parse_start:mozilla.devtools.protobuf.Edge)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional uint64 referent = 1;
+ case 1: {
+ if (tag == 8) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::uint64, ::google::protobuf::internal::WireFormatLite::TYPE_UINT64>(
+ input, &referent_)));
+ set_has_referent();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_name;
+ break;
+ }
+
+ // optional bytes name = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_name:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_name()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(24)) goto parse_nameRef;
+ break;
+ }
+
+ // optional uint64 nameRef = 3;
+ case 3: {
+ if (tag == 24) {
+ parse_nameRef:
+ clear_EdgeNameOrRef();
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::uint64, ::google::protobuf::internal::WireFormatLite::TYPE_UINT64>(
+ input, &EdgeNameOrRef_.nameref_)));
+ set_has_nameref();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormat::SkipField(
+ input, tag, mutable_unknown_fields()));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.devtools.protobuf.Edge)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.devtools.protobuf.Edge)
+ return false;
+#undef DO_
+}
+
+void Edge::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.devtools.protobuf.Edge)
+ // optional uint64 referent = 1;
+ if (has_referent()) {
+ ::google::protobuf::internal::WireFormatLite::WriteUInt64(1, this->referent(), output);
+ }
+
+ // optional bytes name = 2;
+ if (has_name()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 2, this->name(), output);
+ }
+
+ // optional uint64 nameRef = 3;
+ if (has_nameref()) {
+ ::google::protobuf::internal::WireFormatLite::WriteUInt64(3, this->nameref(), output);
+ }
+
+ if (!unknown_fields().empty()) {
+ ::google::protobuf::internal::WireFormat::SerializeUnknownFields(
+ unknown_fields(), output);
+ }
+ // @@protoc_insertion_point(serialize_end:mozilla.devtools.protobuf.Edge)
+}
+
+::google::protobuf::uint8* Edge::SerializeWithCachedSizesToArray(
+ ::google::protobuf::uint8* target) const {
+ // @@protoc_insertion_point(serialize_to_array_start:mozilla.devtools.protobuf.Edge)
+ // optional uint64 referent = 1;
+ if (has_referent()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteUInt64ToArray(1, this->referent(), target);
+ }
+
+ // optional bytes name = 2;
+ if (has_name()) {
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteBytesToArray(
+ 2, this->name(), target);
+ }
+
+ // optional uint64 nameRef = 3;
+ if (has_nameref()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteUInt64ToArray(3, this->nameref(), target);
+ }
+
+ if (!unknown_fields().empty()) {
+ target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray(
+ unknown_fields(), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:mozilla.devtools.protobuf.Edge)
+ return target;
+}
+
+int Edge::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional uint64 referent = 1;
+ if (has_referent()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::UInt64Size(
+ this->referent());
+ }
+
+ }
+ switch (EdgeNameOrRef_case()) {
+ // optional bytes name = 2;
+ case kName: {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->name());
+ break;
+ }
+ // optional uint64 nameRef = 3;
+ case kNameRef: {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::UInt64Size(
+ this->nameref());
+ break;
+ }
+ case EDGENAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ if (!unknown_fields().empty()) {
+ total_size +=
+ ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize(
+ unknown_fields());
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void Edge::MergeFrom(const ::google::protobuf::Message& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ const Edge* source =
+ ::google::protobuf::internal::dynamic_cast_if_available<const Edge*>(
+ &from);
+ if (source == NULL) {
+ ::google::protobuf::internal::ReflectionOps::Merge(from, this);
+ } else {
+ MergeFrom(*source);
+ }
+}
+
+void Edge::MergeFrom(const Edge& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ switch (from.EdgeNameOrRef_case()) {
+ case kName: {
+ set_name(from.name());
+ break;
+ }
+ case kNameRef: {
+ set_nameref(from.nameref());
+ break;
+ }
+ case EDGENAMEORREF_NOT_SET: {
+ break;
+ }
+ }
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_referent()) {
+ set_referent(from.referent());
+ }
+ }
+ mutable_unknown_fields()->MergeFrom(from.unknown_fields());
+}
+
+void Edge::CopyFrom(const ::google::protobuf::Message& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+void Edge::CopyFrom(const Edge& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool Edge::IsInitialized() const {
+
+ return true;
+}
+
+void Edge::Swap(Edge* other) {
+ if (other != this) {
+ std::swap(referent_, other->referent_);
+ std::swap(EdgeNameOrRef_, other->EdgeNameOrRef_);
+ std::swap(_oneof_case_[0], other->_oneof_case_[0]);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.Swap(&other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::google::protobuf::Metadata Edge::GetMetadata() const {
+ protobuf_AssignDescriptorsOnce();
+ ::google::protobuf::Metadata metadata;
+ metadata.descriptor = Edge_descriptor_;
+ metadata.reflection = Edge_reflection_;
+ return metadata;
+}
+
+
+// @@protoc_insertion_point(namespace_scope)
+
+} // namespace protobuf
+} // namespace devtools
+} // namespace mozilla
+
+// @@protoc_insertion_point(global_scope)
diff --git a/devtools/shared/heapsnapshot/CoreDump.pb.h b/devtools/shared/heapsnapshot/CoreDump.pb.h
new file mode 100644
index 000000000..584c2e379
--- /dev/null
+++ b/devtools/shared/heapsnapshot/CoreDump.pb.h
@@ -0,0 +1,1893 @@
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: CoreDump.proto
+
+#ifndef PROTOBUF_CoreDump_2eproto__INCLUDED
+#define PROTOBUF_CoreDump_2eproto__INCLUDED
+
+#include <string>
+
+#include <google/protobuf/stubs/common.h>
+
+#if GOOGLE_PROTOBUF_VERSION < 2006000
+#error This file was generated by a newer version of protoc which is
+#error incompatible with your Protocol Buffer headers. Please update
+#error your headers.
+#endif
+#if 2006001 < GOOGLE_PROTOBUF_MIN_PROTOC_VERSION
+#error This file was generated by an older version of protoc which is
+#error incompatible with your Protocol Buffer headers. Please
+#error regenerate this file with a newer version of protoc.
+#endif
+
+#include <google/protobuf/generated_message_util.h>
+#include <google/protobuf/message.h>
+#include <google/protobuf/repeated_field.h>
+#include <google/protobuf/extension_set.h>
+#include <google/protobuf/unknown_field_set.h>
+// @@protoc_insertion_point(includes)
+
+namespace mozilla {
+namespace devtools {
+namespace protobuf {
+
+// Internal implementation detail -- do not call these.
+void protobuf_AddDesc_CoreDump_2eproto();
+void protobuf_AssignDesc_CoreDump_2eproto();
+void protobuf_ShutdownFile_CoreDump_2eproto();
+
+class Metadata;
+class StackFrame;
+class StackFrame_Data;
+class Node;
+class Edge;
+
+// ===================================================================
+
+class Metadata : public ::google::protobuf::Message {
+ public:
+ Metadata();
+ virtual ~Metadata();
+
+ Metadata(const Metadata& from);
+
+ inline Metadata& operator=(const Metadata& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ::google::protobuf::Descriptor* descriptor();
+ static const Metadata& default_instance();
+
+ void Swap(Metadata* other);
+
+ // implements Message ----------------------------------------------
+
+ Metadata* New() const;
+ void CopyFrom(const ::google::protobuf::Message& from);
+ void MergeFrom(const ::google::protobuf::Message& from);
+ void CopyFrom(const Metadata& from);
+ void MergeFrom(const Metadata& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::google::protobuf::Metadata GetMetadata() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional uint64 timeStamp = 1;
+ inline bool has_timestamp() const;
+ inline void clear_timestamp();
+ static const int kTimeStampFieldNumber = 1;
+ inline ::google::protobuf::uint64 timestamp() const;
+ inline void set_timestamp(::google::protobuf::uint64 value);
+
+ // @@protoc_insertion_point(class_scope:mozilla.devtools.protobuf.Metadata)
+ private:
+ inline void set_has_timestamp();
+ inline void clear_has_timestamp();
+
+ ::google::protobuf::UnknownFieldSet _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::uint64 timestamp_;
+ friend void protobuf_AddDesc_CoreDump_2eproto();
+ friend void protobuf_AssignDesc_CoreDump_2eproto();
+ friend void protobuf_ShutdownFile_CoreDump_2eproto();
+
+ void InitAsDefaultInstance();
+ static Metadata* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class StackFrame_Data : public ::google::protobuf::Message {
+ public:
+ StackFrame_Data();
+ virtual ~StackFrame_Data();
+
+ StackFrame_Data(const StackFrame_Data& from);
+
+ inline StackFrame_Data& operator=(const StackFrame_Data& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ::google::protobuf::Descriptor* descriptor();
+ static const StackFrame_Data& default_instance();
+
+ enum SourceOrRefCase {
+ kSource = 5,
+ kSourceRef = 6,
+ SOURCEORREF_NOT_SET = 0,
+ };
+
+ enum FunctionDisplayNameOrRefCase {
+ kFunctionDisplayName = 7,
+ kFunctionDisplayNameRef = 8,
+ FUNCTIONDISPLAYNAMEORREF_NOT_SET = 0,
+ };
+
+ void Swap(StackFrame_Data* other);
+
+ // implements Message ----------------------------------------------
+
+ StackFrame_Data* New() const;
+ void CopyFrom(const ::google::protobuf::Message& from);
+ void MergeFrom(const ::google::protobuf::Message& from);
+ void CopyFrom(const StackFrame_Data& from);
+ void MergeFrom(const StackFrame_Data& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::google::protobuf::Metadata GetMetadata() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional uint64 id = 1;
+ inline bool has_id() const;
+ inline void clear_id();
+ static const int kIdFieldNumber = 1;
+ inline ::google::protobuf::uint64 id() const;
+ inline void set_id(::google::protobuf::uint64 value);
+
+ // optional .mozilla.devtools.protobuf.StackFrame parent = 2;
+ inline bool has_parent() const;
+ inline void clear_parent();
+ static const int kParentFieldNumber = 2;
+ inline const ::mozilla::devtools::protobuf::StackFrame& parent() const;
+ inline ::mozilla::devtools::protobuf::StackFrame* mutable_parent();
+ inline ::mozilla::devtools::protobuf::StackFrame* release_parent();
+ inline void set_allocated_parent(::mozilla::devtools::protobuf::StackFrame* parent);
+
+ // optional uint32 line = 3;
+ inline bool has_line() const;
+ inline void clear_line();
+ static const int kLineFieldNumber = 3;
+ inline ::google::protobuf::uint32 line() const;
+ inline void set_line(::google::protobuf::uint32 value);
+
+ // optional uint32 column = 4;
+ inline bool has_column() const;
+ inline void clear_column();
+ static const int kColumnFieldNumber = 4;
+ inline ::google::protobuf::uint32 column() const;
+ inline void set_column(::google::protobuf::uint32 value);
+
+ // optional bytes source = 5;
+ inline bool has_source() const;
+ inline void clear_source();
+ static const int kSourceFieldNumber = 5;
+ inline const ::std::string& source() const;
+ inline void set_source(const ::std::string& value);
+ inline void set_source(const char* value);
+ inline void set_source(const void* value, size_t size);
+ inline ::std::string* mutable_source();
+ inline ::std::string* release_source();
+ inline void set_allocated_source(::std::string* source);
+
+ // optional uint64 sourceRef = 6;
+ inline bool has_sourceref() const;
+ inline void clear_sourceref();
+ static const int kSourceRefFieldNumber = 6;
+ inline ::google::protobuf::uint64 sourceref() const;
+ inline void set_sourceref(::google::protobuf::uint64 value);
+
+ // optional bytes functionDisplayName = 7;
+ inline bool has_functiondisplayname() const;
+ inline void clear_functiondisplayname();
+ static const int kFunctionDisplayNameFieldNumber = 7;
+ inline const ::std::string& functiondisplayname() const;
+ inline void set_functiondisplayname(const ::std::string& value);
+ inline void set_functiondisplayname(const char* value);
+ inline void set_functiondisplayname(const void* value, size_t size);
+ inline ::std::string* mutable_functiondisplayname();
+ inline ::std::string* release_functiondisplayname();
+ inline void set_allocated_functiondisplayname(::std::string* functiondisplayname);
+
+ // optional uint64 functionDisplayNameRef = 8;
+ inline bool has_functiondisplaynameref() const;
+ inline void clear_functiondisplaynameref();
+ static const int kFunctionDisplayNameRefFieldNumber = 8;
+ inline ::google::protobuf::uint64 functiondisplaynameref() const;
+ inline void set_functiondisplaynameref(::google::protobuf::uint64 value);
+
+ // optional bool isSystem = 9;
+ inline bool has_issystem() const;
+ inline void clear_issystem();
+ static const int kIsSystemFieldNumber = 9;
+ inline bool issystem() const;
+ inline void set_issystem(bool value);
+
+ // optional bool isSelfHosted = 10;
+ inline bool has_isselfhosted() const;
+ inline void clear_isselfhosted();
+ static const int kIsSelfHostedFieldNumber = 10;
+ inline bool isselfhosted() const;
+ inline void set_isselfhosted(bool value);
+
+ inline SourceOrRefCase SourceOrRef_case() const;
+ inline FunctionDisplayNameOrRefCase FunctionDisplayNameOrRef_case() const;
+ // @@protoc_insertion_point(class_scope:mozilla.devtools.protobuf.StackFrame.Data)
+ private:
+ inline void set_has_id();
+ inline void clear_has_id();
+ inline void set_has_parent();
+ inline void clear_has_parent();
+ inline void set_has_line();
+ inline void clear_has_line();
+ inline void set_has_column();
+ inline void clear_has_column();
+ inline void set_has_source();
+ inline void set_has_sourceref();
+ inline void set_has_functiondisplayname();
+ inline void set_has_functiondisplaynameref();
+ inline void set_has_issystem();
+ inline void clear_has_issystem();
+ inline void set_has_isselfhosted();
+ inline void clear_has_isselfhosted();
+
+ inline bool has_SourceOrRef();
+ void clear_SourceOrRef();
+ inline void clear_has_SourceOrRef();
+
+ inline bool has_FunctionDisplayNameOrRef();
+ void clear_FunctionDisplayNameOrRef();
+ inline void clear_has_FunctionDisplayNameOrRef();
+
+ ::google::protobuf::UnknownFieldSet _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::uint64 id_;
+ ::mozilla::devtools::protobuf::StackFrame* parent_;
+ ::google::protobuf::uint32 line_;
+ ::google::protobuf::uint32 column_;
+ bool issystem_;
+ bool isselfhosted_;
+ union SourceOrRefUnion {
+ ::std::string* source_;
+ ::google::protobuf::uint64 sourceref_;
+ } SourceOrRef_;
+ union FunctionDisplayNameOrRefUnion {
+ ::std::string* functiondisplayname_;
+ ::google::protobuf::uint64 functiondisplaynameref_;
+ } FunctionDisplayNameOrRef_;
+ ::google::protobuf::uint32 _oneof_case_[2];
+
+ friend void protobuf_AddDesc_CoreDump_2eproto();
+ friend void protobuf_AssignDesc_CoreDump_2eproto();
+ friend void protobuf_ShutdownFile_CoreDump_2eproto();
+
+ void InitAsDefaultInstance();
+ static StackFrame_Data* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class StackFrame : public ::google::protobuf::Message {
+ public:
+ StackFrame();
+ virtual ~StackFrame();
+
+ StackFrame(const StackFrame& from);
+
+ inline StackFrame& operator=(const StackFrame& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ::google::protobuf::Descriptor* descriptor();
+ static const StackFrame& default_instance();
+
+ enum StackFrameTypeCase {
+ kData = 1,
+ kRef = 2,
+ STACKFRAMETYPE_NOT_SET = 0,
+ };
+
+ void Swap(StackFrame* other);
+
+ // implements Message ----------------------------------------------
+
+ StackFrame* New() const;
+ void CopyFrom(const ::google::protobuf::Message& from);
+ void MergeFrom(const ::google::protobuf::Message& from);
+ void CopyFrom(const StackFrame& from);
+ void MergeFrom(const StackFrame& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::google::protobuf::Metadata GetMetadata() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef StackFrame_Data Data;
+
+ // accessors -------------------------------------------------------
+
+ // optional .mozilla.devtools.protobuf.StackFrame.Data data = 1;
+ inline bool has_data() const;
+ inline void clear_data();
+ static const int kDataFieldNumber = 1;
+ inline const ::mozilla::devtools::protobuf::StackFrame_Data& data() const;
+ inline ::mozilla::devtools::protobuf::StackFrame_Data* mutable_data();
+ inline ::mozilla::devtools::protobuf::StackFrame_Data* release_data();
+ inline void set_allocated_data(::mozilla::devtools::protobuf::StackFrame_Data* data);
+
+ // optional uint64 ref = 2;
+ inline bool has_ref() const;
+ inline void clear_ref();
+ static const int kRefFieldNumber = 2;
+ inline ::google::protobuf::uint64 ref() const;
+ inline void set_ref(::google::protobuf::uint64 value);
+
+ inline StackFrameTypeCase StackFrameType_case() const;
+ // @@protoc_insertion_point(class_scope:mozilla.devtools.protobuf.StackFrame)
+ private:
+ inline void set_has_data();
+ inline void set_has_ref();
+
+ inline bool has_StackFrameType();
+ void clear_StackFrameType();
+ inline void clear_has_StackFrameType();
+
+ ::google::protobuf::UnknownFieldSet _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ union StackFrameTypeUnion {
+ ::mozilla::devtools::protobuf::StackFrame_Data* data_;
+ ::google::protobuf::uint64 ref_;
+ } StackFrameType_;
+ ::google::protobuf::uint32 _oneof_case_[1];
+
+ friend void protobuf_AddDesc_CoreDump_2eproto();
+ friend void protobuf_AssignDesc_CoreDump_2eproto();
+ friend void protobuf_ShutdownFile_CoreDump_2eproto();
+
+ void InitAsDefaultInstance();
+ static StackFrame* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class Node : public ::google::protobuf::Message {
+ public:
+ Node();
+ virtual ~Node();
+
+ Node(const Node& from);
+
+ inline Node& operator=(const Node& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ::google::protobuf::Descriptor* descriptor();
+ static const Node& default_instance();
+
+ enum TypeNameOrRefCase {
+ kTypeName = 2,
+ kTypeNameRef = 3,
+ TYPENAMEORREF_NOT_SET = 0,
+ };
+
+ enum JSObjectClassNameOrRefCase {
+ kJsObjectClassName = 7,
+ kJsObjectClassNameRef = 8,
+ JSOBJECTCLASSNAMEORREF_NOT_SET = 0,
+ };
+
+ enum ScriptFilenameOrRefCase {
+ kScriptFilename = 10,
+ kScriptFilenameRef = 11,
+ SCRIPTFILENAMEORREF_NOT_SET = 0,
+ };
+
+ void Swap(Node* other);
+
+ // implements Message ----------------------------------------------
+
+ Node* New() const;
+ void CopyFrom(const ::google::protobuf::Message& from);
+ void MergeFrom(const ::google::protobuf::Message& from);
+ void CopyFrom(const Node& from);
+ void MergeFrom(const Node& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::google::protobuf::Metadata GetMetadata() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional uint64 id = 1;
+ inline bool has_id() const;
+ inline void clear_id();
+ static const int kIdFieldNumber = 1;
+ inline ::google::protobuf::uint64 id() const;
+ inline void set_id(::google::protobuf::uint64 value);
+
+ // optional bytes typeName = 2;
+ inline bool has_typename_() const;
+ inline void clear_typename_();
+ static const int kTypeNameFieldNumber = 2;
+ inline const ::std::string& typename_() const;
+ inline void set_typename_(const ::std::string& value);
+ inline void set_typename_(const char* value);
+ inline void set_typename_(const void* value, size_t size);
+ inline ::std::string* mutable_typename_();
+ inline ::std::string* release_typename_();
+ inline void set_allocated_typename_(::std::string* typename_);
+
+ // optional uint64 typeNameRef = 3;
+ inline bool has_typenameref() const;
+ inline void clear_typenameref();
+ static const int kTypeNameRefFieldNumber = 3;
+ inline ::google::protobuf::uint64 typenameref() const;
+ inline void set_typenameref(::google::protobuf::uint64 value);
+
+ // optional uint64 size = 4;
+ inline bool has_size() const;
+ inline void clear_size();
+ static const int kSizeFieldNumber = 4;
+ inline ::google::protobuf::uint64 size() const;
+ inline void set_size(::google::protobuf::uint64 value);
+
+ // repeated .mozilla.devtools.protobuf.Edge edges = 5;
+ inline int edges_size() const;
+ inline void clear_edges();
+ static const int kEdgesFieldNumber = 5;
+ inline const ::mozilla::devtools::protobuf::Edge& edges(int index) const;
+ inline ::mozilla::devtools::protobuf::Edge* mutable_edges(int index);
+ inline ::mozilla::devtools::protobuf::Edge* add_edges();
+ inline const ::google::protobuf::RepeatedPtrField< ::mozilla::devtools::protobuf::Edge >&
+ edges() const;
+ inline ::google::protobuf::RepeatedPtrField< ::mozilla::devtools::protobuf::Edge >*
+ mutable_edges();
+
+ // optional .mozilla.devtools.protobuf.StackFrame allocationStack = 6;
+ inline bool has_allocationstack() const;
+ inline void clear_allocationstack();
+ static const int kAllocationStackFieldNumber = 6;
+ inline const ::mozilla::devtools::protobuf::StackFrame& allocationstack() const;
+ inline ::mozilla::devtools::protobuf::StackFrame* mutable_allocationstack();
+ inline ::mozilla::devtools::protobuf::StackFrame* release_allocationstack();
+ inline void set_allocated_allocationstack(::mozilla::devtools::protobuf::StackFrame* allocationstack);
+
+ // optional bytes jsObjectClassName = 7;
+ inline bool has_jsobjectclassname() const;
+ inline void clear_jsobjectclassname();
+ static const int kJsObjectClassNameFieldNumber = 7;
+ inline const ::std::string& jsobjectclassname() const;
+ inline void set_jsobjectclassname(const ::std::string& value);
+ inline void set_jsobjectclassname(const char* value);
+ inline void set_jsobjectclassname(const void* value, size_t size);
+ inline ::std::string* mutable_jsobjectclassname();
+ inline ::std::string* release_jsobjectclassname();
+ inline void set_allocated_jsobjectclassname(::std::string* jsobjectclassname);
+
+ // optional uint64 jsObjectClassNameRef = 8;
+ inline bool has_jsobjectclassnameref() const;
+ inline void clear_jsobjectclassnameref();
+ static const int kJsObjectClassNameRefFieldNumber = 8;
+ inline ::google::protobuf::uint64 jsobjectclassnameref() const;
+ inline void set_jsobjectclassnameref(::google::protobuf::uint64 value);
+
+ // optional uint32 coarseType = 9 [default = 0];
+ inline bool has_coarsetype() const;
+ inline void clear_coarsetype();
+ static const int kCoarseTypeFieldNumber = 9;
+ inline ::google::protobuf::uint32 coarsetype() const;
+ inline void set_coarsetype(::google::protobuf::uint32 value);
+
+ // optional bytes scriptFilename = 10;
+ inline bool has_scriptfilename() const;
+ inline void clear_scriptfilename();
+ static const int kScriptFilenameFieldNumber = 10;
+ inline const ::std::string& scriptfilename() const;
+ inline void set_scriptfilename(const ::std::string& value);
+ inline void set_scriptfilename(const char* value);
+ inline void set_scriptfilename(const void* value, size_t size);
+ inline ::std::string* mutable_scriptfilename();
+ inline ::std::string* release_scriptfilename();
+ inline void set_allocated_scriptfilename(::std::string* scriptfilename);
+
+ // optional uint64 scriptFilenameRef = 11;
+ inline bool has_scriptfilenameref() const;
+ inline void clear_scriptfilenameref();
+ static const int kScriptFilenameRefFieldNumber = 11;
+ inline ::google::protobuf::uint64 scriptfilenameref() const;
+ inline void set_scriptfilenameref(::google::protobuf::uint64 value);
+
+ inline TypeNameOrRefCase TypeNameOrRef_case() const;
+ inline JSObjectClassNameOrRefCase JSObjectClassNameOrRef_case() const;
+ inline ScriptFilenameOrRefCase ScriptFilenameOrRef_case() const;
+ // @@protoc_insertion_point(class_scope:mozilla.devtools.protobuf.Node)
+ private:
+ inline void set_has_id();
+ inline void clear_has_id();
+ inline void set_has_typename_();
+ inline void set_has_typenameref();
+ inline void set_has_size();
+ inline void clear_has_size();
+ inline void set_has_allocationstack();
+ inline void clear_has_allocationstack();
+ inline void set_has_jsobjectclassname();
+ inline void set_has_jsobjectclassnameref();
+ inline void set_has_coarsetype();
+ inline void clear_has_coarsetype();
+ inline void set_has_scriptfilename();
+ inline void set_has_scriptfilenameref();
+
+ inline bool has_TypeNameOrRef();
+ void clear_TypeNameOrRef();
+ inline void clear_has_TypeNameOrRef();
+
+ inline bool has_JSObjectClassNameOrRef();
+ void clear_JSObjectClassNameOrRef();
+ inline void clear_has_JSObjectClassNameOrRef();
+
+ inline bool has_ScriptFilenameOrRef();
+ void clear_ScriptFilenameOrRef();
+ inline void clear_has_ScriptFilenameOrRef();
+
+ ::google::protobuf::UnknownFieldSet _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::uint64 id_;
+ ::google::protobuf::uint64 size_;
+ ::google::protobuf::RepeatedPtrField< ::mozilla::devtools::protobuf::Edge > edges_;
+ ::mozilla::devtools::protobuf::StackFrame* allocationstack_;
+ ::google::protobuf::uint32 coarsetype_;
+ union TypeNameOrRefUnion {
+ ::std::string* typename__;
+ ::google::protobuf::uint64 typenameref_;
+ } TypeNameOrRef_;
+ union JSObjectClassNameOrRefUnion {
+ ::std::string* jsobjectclassname_;
+ ::google::protobuf::uint64 jsobjectclassnameref_;
+ } JSObjectClassNameOrRef_;
+ union ScriptFilenameOrRefUnion {
+ ::std::string* scriptfilename_;
+ ::google::protobuf::uint64 scriptfilenameref_;
+ } ScriptFilenameOrRef_;
+ ::google::protobuf::uint32 _oneof_case_[3];
+
+ friend void protobuf_AddDesc_CoreDump_2eproto();
+ friend void protobuf_AssignDesc_CoreDump_2eproto();
+ friend void protobuf_ShutdownFile_CoreDump_2eproto();
+
+ void InitAsDefaultInstance();
+ static Node* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class Edge : public ::google::protobuf::Message {
+ public:
+ Edge();
+ virtual ~Edge();
+
+ Edge(const Edge& from);
+
+ inline Edge& operator=(const Edge& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ::google::protobuf::Descriptor* descriptor();
+ static const Edge& default_instance();
+
+ enum EdgeNameOrRefCase {
+ kName = 2,
+ kNameRef = 3,
+ EDGENAMEORREF_NOT_SET = 0,
+ };
+
+ void Swap(Edge* other);
+
+ // implements Message ----------------------------------------------
+
+ Edge* New() const;
+ void CopyFrom(const ::google::protobuf::Message& from);
+ void MergeFrom(const ::google::protobuf::Message& from);
+ void CopyFrom(const Edge& from);
+ void MergeFrom(const Edge& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::google::protobuf::Metadata GetMetadata() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional uint64 referent = 1;
+ inline bool has_referent() const;
+ inline void clear_referent();
+ static const int kReferentFieldNumber = 1;
+ inline ::google::protobuf::uint64 referent() const;
+ inline void set_referent(::google::protobuf::uint64 value);
+
+ // optional bytes name = 2;
+ inline bool has_name() const;
+ inline void clear_name();
+ static const int kNameFieldNumber = 2;
+ inline const ::std::string& name() const;
+ inline void set_name(const ::std::string& value);
+ inline void set_name(const char* value);
+ inline void set_name(const void* value, size_t size);
+ inline ::std::string* mutable_name();
+ inline ::std::string* release_name();
+ inline void set_allocated_name(::std::string* name);
+
+ // optional uint64 nameRef = 3;
+ inline bool has_nameref() const;
+ inline void clear_nameref();
+ static const int kNameRefFieldNumber = 3;
+ inline ::google::protobuf::uint64 nameref() const;
+ inline void set_nameref(::google::protobuf::uint64 value);
+
+ inline EdgeNameOrRefCase EdgeNameOrRef_case() const;
+ // @@protoc_insertion_point(class_scope:mozilla.devtools.protobuf.Edge)
+ private:
+ inline void set_has_referent();
+ inline void clear_has_referent();
+ inline void set_has_name();
+ inline void set_has_nameref();
+
+ inline bool has_EdgeNameOrRef();
+ void clear_EdgeNameOrRef();
+ inline void clear_has_EdgeNameOrRef();
+
+ ::google::protobuf::UnknownFieldSet _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::uint64 referent_;
+ union EdgeNameOrRefUnion {
+ ::std::string* name_;
+ ::google::protobuf::uint64 nameref_;
+ } EdgeNameOrRef_;
+ ::google::protobuf::uint32 _oneof_case_[1];
+
+ friend void protobuf_AddDesc_CoreDump_2eproto();
+ friend void protobuf_AssignDesc_CoreDump_2eproto();
+ friend void protobuf_ShutdownFile_CoreDump_2eproto();
+
+ void InitAsDefaultInstance();
+ static Edge* default_instance_;
+};
+// ===================================================================
+
+
+// ===================================================================
+
+// Metadata
+
+// optional uint64 timeStamp = 1;
+inline bool Metadata::has_timestamp() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void Metadata::set_has_timestamp() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void Metadata::clear_has_timestamp() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void Metadata::clear_timestamp() {
+ timestamp_ = GOOGLE_ULONGLONG(0);
+ clear_has_timestamp();
+}
+inline ::google::protobuf::uint64 Metadata::timestamp() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Metadata.timeStamp)
+ return timestamp_;
+}
+inline void Metadata::set_timestamp(::google::protobuf::uint64 value) {
+ set_has_timestamp();
+ timestamp_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Metadata.timeStamp)
+}
+
+// -------------------------------------------------------------------
+
+// StackFrame_Data
+
+// optional uint64 id = 1;
+inline bool StackFrame_Data::has_id() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void StackFrame_Data::set_has_id() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void StackFrame_Data::clear_has_id() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void StackFrame_Data::clear_id() {
+ id_ = GOOGLE_ULONGLONG(0);
+ clear_has_id();
+}
+inline ::google::protobuf::uint64 StackFrame_Data::id() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.id)
+ return id_;
+}
+inline void StackFrame_Data::set_id(::google::protobuf::uint64 value) {
+ set_has_id();
+ id_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.Data.id)
+}
+
+// optional .mozilla.devtools.protobuf.StackFrame parent = 2;
+inline bool StackFrame_Data::has_parent() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void StackFrame_Data::set_has_parent() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void StackFrame_Data::clear_has_parent() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void StackFrame_Data::clear_parent() {
+ if (parent_ != NULL) parent_->::mozilla::devtools::protobuf::StackFrame::Clear();
+ clear_has_parent();
+}
+inline const ::mozilla::devtools::protobuf::StackFrame& StackFrame_Data::parent() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.parent)
+ return parent_ != NULL ? *parent_ : *default_instance_->parent_;
+}
+inline ::mozilla::devtools::protobuf::StackFrame* StackFrame_Data::mutable_parent() {
+ set_has_parent();
+ if (parent_ == NULL) parent_ = new ::mozilla::devtools::protobuf::StackFrame;
+ // @@protoc_insertion_point(field_mutable:mozilla.devtools.protobuf.StackFrame.Data.parent)
+ return parent_;
+}
+inline ::mozilla::devtools::protobuf::StackFrame* StackFrame_Data::release_parent() {
+ clear_has_parent();
+ ::mozilla::devtools::protobuf::StackFrame* temp = parent_;
+ parent_ = NULL;
+ return temp;
+}
+inline void StackFrame_Data::set_allocated_parent(::mozilla::devtools::protobuf::StackFrame* parent) {
+ delete parent_;
+ parent_ = parent;
+ if (parent) {
+ set_has_parent();
+ } else {
+ clear_has_parent();
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.devtools.protobuf.StackFrame.Data.parent)
+}
+
+// optional uint32 line = 3;
+inline bool StackFrame_Data::has_line() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void StackFrame_Data::set_has_line() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void StackFrame_Data::clear_has_line() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void StackFrame_Data::clear_line() {
+ line_ = 0u;
+ clear_has_line();
+}
+inline ::google::protobuf::uint32 StackFrame_Data::line() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.line)
+ return line_;
+}
+inline void StackFrame_Data::set_line(::google::protobuf::uint32 value) {
+ set_has_line();
+ line_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.Data.line)
+}
+
+// optional uint32 column = 4;
+inline bool StackFrame_Data::has_column() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void StackFrame_Data::set_has_column() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void StackFrame_Data::clear_has_column() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void StackFrame_Data::clear_column() {
+ column_ = 0u;
+ clear_has_column();
+}
+inline ::google::protobuf::uint32 StackFrame_Data::column() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.column)
+ return column_;
+}
+inline void StackFrame_Data::set_column(::google::protobuf::uint32 value) {
+ set_has_column();
+ column_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.Data.column)
+}
+
+// optional bytes source = 5;
+inline bool StackFrame_Data::has_source() const {
+ return SourceOrRef_case() == kSource;
+}
+inline void StackFrame_Data::set_has_source() {
+ _oneof_case_[0] = kSource;
+}
+inline void StackFrame_Data::clear_source() {
+ if (has_source()) {
+ delete SourceOrRef_.source_;
+ clear_has_SourceOrRef();
+ }
+}
+inline const ::std::string& StackFrame_Data::source() const {
+ if (has_source()) {
+ return *SourceOrRef_.source_;
+ }
+ return ::google::protobuf::internal::GetEmptyStringAlreadyInited();
+}
+inline void StackFrame_Data::set_source(const ::std::string& value) {
+ if (!has_source()) {
+ clear_SourceOrRef();
+ set_has_source();
+ SourceOrRef_.source_ = new ::std::string;
+ }
+ SourceOrRef_.source_->assign(value);
+}
+inline void StackFrame_Data::set_source(const char* value) {
+ if (!has_source()) {
+ clear_SourceOrRef();
+ set_has_source();
+ SourceOrRef_.source_ = new ::std::string;
+ }
+ SourceOrRef_.source_->assign(value);
+}
+inline void StackFrame_Data::set_source(const void* value, size_t size) {
+ if (!has_source()) {
+ clear_SourceOrRef();
+ set_has_source();
+ SourceOrRef_.source_ = new ::std::string;
+ }
+ SourceOrRef_.source_->assign(
+ reinterpret_cast<const char*>(value), size);
+}
+inline ::std::string* StackFrame_Data::mutable_source() {
+ if (!has_source()) {
+ clear_SourceOrRef();
+ set_has_source();
+ SourceOrRef_.source_ = new ::std::string;
+ }
+ return SourceOrRef_.source_;
+}
+inline ::std::string* StackFrame_Data::release_source() {
+ if (has_source()) {
+ clear_has_SourceOrRef();
+ ::std::string* temp = SourceOrRef_.source_;
+ SourceOrRef_.source_ = NULL;
+ return temp;
+ } else {
+ return NULL;
+ }
+}
+inline void StackFrame_Data::set_allocated_source(::std::string* source) {
+ clear_SourceOrRef();
+ if (source) {
+ set_has_source();
+ SourceOrRef_.source_ = source;
+ }
+}
+
+// optional uint64 sourceRef = 6;
+inline bool StackFrame_Data::has_sourceref() const {
+ return SourceOrRef_case() == kSourceRef;
+}
+inline void StackFrame_Data::set_has_sourceref() {
+ _oneof_case_[0] = kSourceRef;
+}
+inline void StackFrame_Data::clear_sourceref() {
+ if (has_sourceref()) {
+ SourceOrRef_.sourceref_ = GOOGLE_ULONGLONG(0);
+ clear_has_SourceOrRef();
+ }
+}
+inline ::google::protobuf::uint64 StackFrame_Data::sourceref() const {
+ if (has_sourceref()) {
+ return SourceOrRef_.sourceref_;
+ }
+ return GOOGLE_ULONGLONG(0);
+}
+inline void StackFrame_Data::set_sourceref(::google::protobuf::uint64 value) {
+ if (!has_sourceref()) {
+ clear_SourceOrRef();
+ set_has_sourceref();
+ }
+ SourceOrRef_.sourceref_ = value;
+}
+
+// optional bytes functionDisplayName = 7;
+inline bool StackFrame_Data::has_functiondisplayname() const {
+ return FunctionDisplayNameOrRef_case() == kFunctionDisplayName;
+}
+inline void StackFrame_Data::set_has_functiondisplayname() {
+ _oneof_case_[1] = kFunctionDisplayName;
+}
+inline void StackFrame_Data::clear_functiondisplayname() {
+ if (has_functiondisplayname()) {
+ delete FunctionDisplayNameOrRef_.functiondisplayname_;
+ clear_has_FunctionDisplayNameOrRef();
+ }
+}
+inline const ::std::string& StackFrame_Data::functiondisplayname() const {
+ if (has_functiondisplayname()) {
+ return *FunctionDisplayNameOrRef_.functiondisplayname_;
+ }
+ return ::google::protobuf::internal::GetEmptyStringAlreadyInited();
+}
+inline void StackFrame_Data::set_functiondisplayname(const ::std::string& value) {
+ if (!has_functiondisplayname()) {
+ clear_FunctionDisplayNameOrRef();
+ set_has_functiondisplayname();
+ FunctionDisplayNameOrRef_.functiondisplayname_ = new ::std::string;
+ }
+ FunctionDisplayNameOrRef_.functiondisplayname_->assign(value);
+}
+inline void StackFrame_Data::set_functiondisplayname(const char* value) {
+ if (!has_functiondisplayname()) {
+ clear_FunctionDisplayNameOrRef();
+ set_has_functiondisplayname();
+ FunctionDisplayNameOrRef_.functiondisplayname_ = new ::std::string;
+ }
+ FunctionDisplayNameOrRef_.functiondisplayname_->assign(value);
+}
+inline void StackFrame_Data::set_functiondisplayname(const void* value, size_t size) {
+ if (!has_functiondisplayname()) {
+ clear_FunctionDisplayNameOrRef();
+ set_has_functiondisplayname();
+ FunctionDisplayNameOrRef_.functiondisplayname_ = new ::std::string;
+ }
+ FunctionDisplayNameOrRef_.functiondisplayname_->assign(
+ reinterpret_cast<const char*>(value), size);
+}
+inline ::std::string* StackFrame_Data::mutable_functiondisplayname() {
+ if (!has_functiondisplayname()) {
+ clear_FunctionDisplayNameOrRef();
+ set_has_functiondisplayname();
+ FunctionDisplayNameOrRef_.functiondisplayname_ = new ::std::string;
+ }
+ return FunctionDisplayNameOrRef_.functiondisplayname_;
+}
+inline ::std::string* StackFrame_Data::release_functiondisplayname() {
+ if (has_functiondisplayname()) {
+ clear_has_FunctionDisplayNameOrRef();
+ ::std::string* temp = FunctionDisplayNameOrRef_.functiondisplayname_;
+ FunctionDisplayNameOrRef_.functiondisplayname_ = NULL;
+ return temp;
+ } else {
+ return NULL;
+ }
+}
+inline void StackFrame_Data::set_allocated_functiondisplayname(::std::string* functiondisplayname) {
+ clear_FunctionDisplayNameOrRef();
+ if (functiondisplayname) {
+ set_has_functiondisplayname();
+ FunctionDisplayNameOrRef_.functiondisplayname_ = functiondisplayname;
+ }
+}
+
+// optional uint64 functionDisplayNameRef = 8;
+inline bool StackFrame_Data::has_functiondisplaynameref() const {
+ return FunctionDisplayNameOrRef_case() == kFunctionDisplayNameRef;
+}
+inline void StackFrame_Data::set_has_functiondisplaynameref() {
+ _oneof_case_[1] = kFunctionDisplayNameRef;
+}
+inline void StackFrame_Data::clear_functiondisplaynameref() {
+ if (has_functiondisplaynameref()) {
+ FunctionDisplayNameOrRef_.functiondisplaynameref_ = GOOGLE_ULONGLONG(0);
+ clear_has_FunctionDisplayNameOrRef();
+ }
+}
+inline ::google::protobuf::uint64 StackFrame_Data::functiondisplaynameref() const {
+ if (has_functiondisplaynameref()) {
+ return FunctionDisplayNameOrRef_.functiondisplaynameref_;
+ }
+ return GOOGLE_ULONGLONG(0);
+}
+inline void StackFrame_Data::set_functiondisplaynameref(::google::protobuf::uint64 value) {
+ if (!has_functiondisplaynameref()) {
+ clear_FunctionDisplayNameOrRef();
+ set_has_functiondisplaynameref();
+ }
+ FunctionDisplayNameOrRef_.functiondisplaynameref_ = value;
+}
+
+// optional bool isSystem = 9;
+inline bool StackFrame_Data::has_issystem() const {
+ return (_has_bits_[0] & 0x00000100u) != 0;
+}
+inline void StackFrame_Data::set_has_issystem() {
+ _has_bits_[0] |= 0x00000100u;
+}
+inline void StackFrame_Data::clear_has_issystem() {
+ _has_bits_[0] &= ~0x00000100u;
+}
+inline void StackFrame_Data::clear_issystem() {
+ issystem_ = false;
+ clear_has_issystem();
+}
+inline bool StackFrame_Data::issystem() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.isSystem)
+ return issystem_;
+}
+inline void StackFrame_Data::set_issystem(bool value) {
+ set_has_issystem();
+ issystem_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.Data.isSystem)
+}
+
+// optional bool isSelfHosted = 10;
+inline bool StackFrame_Data::has_isselfhosted() const {
+ return (_has_bits_[0] & 0x00000200u) != 0;
+}
+inline void StackFrame_Data::set_has_isselfhosted() {
+ _has_bits_[0] |= 0x00000200u;
+}
+inline void StackFrame_Data::clear_has_isselfhosted() {
+ _has_bits_[0] &= ~0x00000200u;
+}
+inline void StackFrame_Data::clear_isselfhosted() {
+ isselfhosted_ = false;
+ clear_has_isselfhosted();
+}
+inline bool StackFrame_Data::isselfhosted() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.StackFrame.Data.isSelfHosted)
+ return isselfhosted_;
+}
+inline void StackFrame_Data::set_isselfhosted(bool value) {
+ set_has_isselfhosted();
+ isselfhosted_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.StackFrame.Data.isSelfHosted)
+}
+
+inline bool StackFrame_Data::has_SourceOrRef() {
+ return SourceOrRef_case() != SOURCEORREF_NOT_SET;
+}
+inline void StackFrame_Data::clear_has_SourceOrRef() {
+ _oneof_case_[0] = SOURCEORREF_NOT_SET;
+}
+inline bool StackFrame_Data::has_FunctionDisplayNameOrRef() {
+ return FunctionDisplayNameOrRef_case() != FUNCTIONDISPLAYNAMEORREF_NOT_SET;
+}
+inline void StackFrame_Data::clear_has_FunctionDisplayNameOrRef() {
+ _oneof_case_[1] = FUNCTIONDISPLAYNAMEORREF_NOT_SET;
+}
+inline StackFrame_Data::SourceOrRefCase StackFrame_Data::SourceOrRef_case() const {
+ return StackFrame_Data::SourceOrRefCase(_oneof_case_[0]);
+}
+inline StackFrame_Data::FunctionDisplayNameOrRefCase StackFrame_Data::FunctionDisplayNameOrRef_case() const {
+ return StackFrame_Data::FunctionDisplayNameOrRefCase(_oneof_case_[1]);
+}
+// -------------------------------------------------------------------
+
+// StackFrame
+
+// optional .mozilla.devtools.protobuf.StackFrame.Data data = 1;
+inline bool StackFrame::has_data() const {
+ return StackFrameType_case() == kData;
+}
+inline void StackFrame::set_has_data() {
+ _oneof_case_[0] = kData;
+}
+inline void StackFrame::clear_data() {
+ if (has_data()) {
+ delete StackFrameType_.data_;
+ clear_has_StackFrameType();
+ }
+}
+inline const ::mozilla::devtools::protobuf::StackFrame_Data& StackFrame::data() const {
+ return has_data() ? *StackFrameType_.data_
+ : ::mozilla::devtools::protobuf::StackFrame_Data::default_instance();
+}
+inline ::mozilla::devtools::protobuf::StackFrame_Data* StackFrame::mutable_data() {
+ if (!has_data()) {
+ clear_StackFrameType();
+ set_has_data();
+ StackFrameType_.data_ = new ::mozilla::devtools::protobuf::StackFrame_Data;
+ }
+ return StackFrameType_.data_;
+}
+inline ::mozilla::devtools::protobuf::StackFrame_Data* StackFrame::release_data() {
+ if (has_data()) {
+ clear_has_StackFrameType();
+ ::mozilla::devtools::protobuf::StackFrame_Data* temp = StackFrameType_.data_;
+ StackFrameType_.data_ = NULL;
+ return temp;
+ } else {
+ return NULL;
+ }
+}
+inline void StackFrame::set_allocated_data(::mozilla::devtools::protobuf::StackFrame_Data* data) {
+ clear_StackFrameType();
+ if (data) {
+ set_has_data();
+ StackFrameType_.data_ = data;
+ }
+}
+
+// optional uint64 ref = 2;
+inline bool StackFrame::has_ref() const {
+ return StackFrameType_case() == kRef;
+}
+inline void StackFrame::set_has_ref() {
+ _oneof_case_[0] = kRef;
+}
+inline void StackFrame::clear_ref() {
+ if (has_ref()) {
+ StackFrameType_.ref_ = GOOGLE_ULONGLONG(0);
+ clear_has_StackFrameType();
+ }
+}
+inline ::google::protobuf::uint64 StackFrame::ref() const {
+ if (has_ref()) {
+ return StackFrameType_.ref_;
+ }
+ return GOOGLE_ULONGLONG(0);
+}
+inline void StackFrame::set_ref(::google::protobuf::uint64 value) {
+ if (!has_ref()) {
+ clear_StackFrameType();
+ set_has_ref();
+ }
+ StackFrameType_.ref_ = value;
+}
+
+inline bool StackFrame::has_StackFrameType() {
+ return StackFrameType_case() != STACKFRAMETYPE_NOT_SET;
+}
+inline void StackFrame::clear_has_StackFrameType() {
+ _oneof_case_[0] = STACKFRAMETYPE_NOT_SET;
+}
+inline StackFrame::StackFrameTypeCase StackFrame::StackFrameType_case() const {
+ return StackFrame::StackFrameTypeCase(_oneof_case_[0]);
+}
+// -------------------------------------------------------------------
+
+// Node
+
+// optional uint64 id = 1;
+inline bool Node::has_id() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void Node::set_has_id() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void Node::clear_has_id() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void Node::clear_id() {
+ id_ = GOOGLE_ULONGLONG(0);
+ clear_has_id();
+}
+inline ::google::protobuf::uint64 Node::id() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.id)
+ return id_;
+}
+inline void Node::set_id(::google::protobuf::uint64 value) {
+ set_has_id();
+ id_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Node.id)
+}
+
+// optional bytes typeName = 2;
+inline bool Node::has_typename_() const {
+ return TypeNameOrRef_case() == kTypeName;
+}
+inline void Node::set_has_typename_() {
+ _oneof_case_[0] = kTypeName;
+}
+inline void Node::clear_typename_() {
+ if (has_typename_()) {
+ delete TypeNameOrRef_.typename__;
+ clear_has_TypeNameOrRef();
+ }
+}
+inline const ::std::string& Node::typename_() const {
+ if (has_typename_()) {
+ return *TypeNameOrRef_.typename__;
+ }
+ return ::google::protobuf::internal::GetEmptyStringAlreadyInited();
+}
+inline void Node::set_typename_(const ::std::string& value) {
+ if (!has_typename_()) {
+ clear_TypeNameOrRef();
+ set_has_typename_();
+ TypeNameOrRef_.typename__ = new ::std::string;
+ }
+ TypeNameOrRef_.typename__->assign(value);
+}
+inline void Node::set_typename_(const char* value) {
+ if (!has_typename_()) {
+ clear_TypeNameOrRef();
+ set_has_typename_();
+ TypeNameOrRef_.typename__ = new ::std::string;
+ }
+ TypeNameOrRef_.typename__->assign(value);
+}
+inline void Node::set_typename_(const void* value, size_t size) {
+ if (!has_typename_()) {
+ clear_TypeNameOrRef();
+ set_has_typename_();
+ TypeNameOrRef_.typename__ = new ::std::string;
+ }
+ TypeNameOrRef_.typename__->assign(
+ reinterpret_cast<const char*>(value), size);
+}
+inline ::std::string* Node::mutable_typename_() {
+ if (!has_typename_()) {
+ clear_TypeNameOrRef();
+ set_has_typename_();
+ TypeNameOrRef_.typename__ = new ::std::string;
+ }
+ return TypeNameOrRef_.typename__;
+}
+inline ::std::string* Node::release_typename_() {
+ if (has_typename_()) {
+ clear_has_TypeNameOrRef();
+ ::std::string* temp = TypeNameOrRef_.typename__;
+ TypeNameOrRef_.typename__ = NULL;
+ return temp;
+ } else {
+ return NULL;
+ }
+}
+inline void Node::set_allocated_typename_(::std::string* typename_) {
+ clear_TypeNameOrRef();
+ if (typename_) {
+ set_has_typename_();
+ TypeNameOrRef_.typename__ = typename_;
+ }
+}
+
+// optional uint64 typeNameRef = 3;
+inline bool Node::has_typenameref() const {
+ return TypeNameOrRef_case() == kTypeNameRef;
+}
+inline void Node::set_has_typenameref() {
+ _oneof_case_[0] = kTypeNameRef;
+}
+inline void Node::clear_typenameref() {
+ if (has_typenameref()) {
+ TypeNameOrRef_.typenameref_ = GOOGLE_ULONGLONG(0);
+ clear_has_TypeNameOrRef();
+ }
+}
+inline ::google::protobuf::uint64 Node::typenameref() const {
+ if (has_typenameref()) {
+ return TypeNameOrRef_.typenameref_;
+ }
+ return GOOGLE_ULONGLONG(0);
+}
+inline void Node::set_typenameref(::google::protobuf::uint64 value) {
+ if (!has_typenameref()) {
+ clear_TypeNameOrRef();
+ set_has_typenameref();
+ }
+ TypeNameOrRef_.typenameref_ = value;
+}
+
+// optional uint64 size = 4;
+inline bool Node::has_size() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void Node::set_has_size() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void Node::clear_has_size() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void Node::clear_size() {
+ size_ = GOOGLE_ULONGLONG(0);
+ clear_has_size();
+}
+inline ::google::protobuf::uint64 Node::size() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.size)
+ return size_;
+}
+inline void Node::set_size(::google::protobuf::uint64 value) {
+ set_has_size();
+ size_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Node.size)
+}
+
+// repeated .mozilla.devtools.protobuf.Edge edges = 5;
+inline int Node::edges_size() const {
+ return edges_.size();
+}
+inline void Node::clear_edges() {
+ edges_.Clear();
+}
+inline const ::mozilla::devtools::protobuf::Edge& Node::edges(int index) const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.edges)
+ return edges_.Get(index);
+}
+inline ::mozilla::devtools::protobuf::Edge* Node::mutable_edges(int index) {
+ // @@protoc_insertion_point(field_mutable:mozilla.devtools.protobuf.Node.edges)
+ return edges_.Mutable(index);
+}
+inline ::mozilla::devtools::protobuf::Edge* Node::add_edges() {
+ // @@protoc_insertion_point(field_add:mozilla.devtools.protobuf.Node.edges)
+ return edges_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::mozilla::devtools::protobuf::Edge >&
+Node::edges() const {
+ // @@protoc_insertion_point(field_list:mozilla.devtools.protobuf.Node.edges)
+ return edges_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::mozilla::devtools::protobuf::Edge >*
+Node::mutable_edges() {
+ // @@protoc_insertion_point(field_mutable_list:mozilla.devtools.protobuf.Node.edges)
+ return &edges_;
+}
+
+// optional .mozilla.devtools.protobuf.StackFrame allocationStack = 6;
+inline bool Node::has_allocationstack() const {
+ return (_has_bits_[0] & 0x00000020u) != 0;
+}
+inline void Node::set_has_allocationstack() {
+ _has_bits_[0] |= 0x00000020u;
+}
+inline void Node::clear_has_allocationstack() {
+ _has_bits_[0] &= ~0x00000020u;
+}
+inline void Node::clear_allocationstack() {
+ if (allocationstack_ != NULL) allocationstack_->::mozilla::devtools::protobuf::StackFrame::Clear();
+ clear_has_allocationstack();
+}
+inline const ::mozilla::devtools::protobuf::StackFrame& Node::allocationstack() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.allocationStack)
+ return allocationstack_ != NULL ? *allocationstack_ : *default_instance_->allocationstack_;
+}
+inline ::mozilla::devtools::protobuf::StackFrame* Node::mutable_allocationstack() {
+ set_has_allocationstack();
+ if (allocationstack_ == NULL) allocationstack_ = new ::mozilla::devtools::protobuf::StackFrame;
+ // @@protoc_insertion_point(field_mutable:mozilla.devtools.protobuf.Node.allocationStack)
+ return allocationstack_;
+}
+inline ::mozilla::devtools::protobuf::StackFrame* Node::release_allocationstack() {
+ clear_has_allocationstack();
+ ::mozilla::devtools::protobuf::StackFrame* temp = allocationstack_;
+ allocationstack_ = NULL;
+ return temp;
+}
+inline void Node::set_allocated_allocationstack(::mozilla::devtools::protobuf::StackFrame* allocationstack) {
+ delete allocationstack_;
+ allocationstack_ = allocationstack;
+ if (allocationstack) {
+ set_has_allocationstack();
+ } else {
+ clear_has_allocationstack();
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.devtools.protobuf.Node.allocationStack)
+}
+
+// optional bytes jsObjectClassName = 7;
+inline bool Node::has_jsobjectclassname() const {
+ return JSObjectClassNameOrRef_case() == kJsObjectClassName;
+}
+inline void Node::set_has_jsobjectclassname() {
+ _oneof_case_[1] = kJsObjectClassName;
+}
+inline void Node::clear_jsobjectclassname() {
+ if (has_jsobjectclassname()) {
+ delete JSObjectClassNameOrRef_.jsobjectclassname_;
+ clear_has_JSObjectClassNameOrRef();
+ }
+}
+inline const ::std::string& Node::jsobjectclassname() const {
+ if (has_jsobjectclassname()) {
+ return *JSObjectClassNameOrRef_.jsobjectclassname_;
+ }
+ return ::google::protobuf::internal::GetEmptyStringAlreadyInited();
+}
+inline void Node::set_jsobjectclassname(const ::std::string& value) {
+ if (!has_jsobjectclassname()) {
+ clear_JSObjectClassNameOrRef();
+ set_has_jsobjectclassname();
+ JSObjectClassNameOrRef_.jsobjectclassname_ = new ::std::string;
+ }
+ JSObjectClassNameOrRef_.jsobjectclassname_->assign(value);
+}
+inline void Node::set_jsobjectclassname(const char* value) {
+ if (!has_jsobjectclassname()) {
+ clear_JSObjectClassNameOrRef();
+ set_has_jsobjectclassname();
+ JSObjectClassNameOrRef_.jsobjectclassname_ = new ::std::string;
+ }
+ JSObjectClassNameOrRef_.jsobjectclassname_->assign(value);
+}
+inline void Node::set_jsobjectclassname(const void* value, size_t size) {
+ if (!has_jsobjectclassname()) {
+ clear_JSObjectClassNameOrRef();
+ set_has_jsobjectclassname();
+ JSObjectClassNameOrRef_.jsobjectclassname_ = new ::std::string;
+ }
+ JSObjectClassNameOrRef_.jsobjectclassname_->assign(
+ reinterpret_cast<const char*>(value), size);
+}
+inline ::std::string* Node::mutable_jsobjectclassname() {
+ if (!has_jsobjectclassname()) {
+ clear_JSObjectClassNameOrRef();
+ set_has_jsobjectclassname();
+ JSObjectClassNameOrRef_.jsobjectclassname_ = new ::std::string;
+ }
+ return JSObjectClassNameOrRef_.jsobjectclassname_;
+}
+inline ::std::string* Node::release_jsobjectclassname() {
+ if (has_jsobjectclassname()) {
+ clear_has_JSObjectClassNameOrRef();
+ ::std::string* temp = JSObjectClassNameOrRef_.jsobjectclassname_;
+ JSObjectClassNameOrRef_.jsobjectclassname_ = NULL;
+ return temp;
+ } else {
+ return NULL;
+ }
+}
+inline void Node::set_allocated_jsobjectclassname(::std::string* jsobjectclassname) {
+ clear_JSObjectClassNameOrRef();
+ if (jsobjectclassname) {
+ set_has_jsobjectclassname();
+ JSObjectClassNameOrRef_.jsobjectclassname_ = jsobjectclassname;
+ }
+}
+
+// optional uint64 jsObjectClassNameRef = 8;
+inline bool Node::has_jsobjectclassnameref() const {
+ return JSObjectClassNameOrRef_case() == kJsObjectClassNameRef;
+}
+inline void Node::set_has_jsobjectclassnameref() {
+ _oneof_case_[1] = kJsObjectClassNameRef;
+}
+inline void Node::clear_jsobjectclassnameref() {
+ if (has_jsobjectclassnameref()) {
+ JSObjectClassNameOrRef_.jsobjectclassnameref_ = GOOGLE_ULONGLONG(0);
+ clear_has_JSObjectClassNameOrRef();
+ }
+}
+inline ::google::protobuf::uint64 Node::jsobjectclassnameref() const {
+ if (has_jsobjectclassnameref()) {
+ return JSObjectClassNameOrRef_.jsobjectclassnameref_;
+ }
+ return GOOGLE_ULONGLONG(0);
+}
+inline void Node::set_jsobjectclassnameref(::google::protobuf::uint64 value) {
+ if (!has_jsobjectclassnameref()) {
+ clear_JSObjectClassNameOrRef();
+ set_has_jsobjectclassnameref();
+ }
+ JSObjectClassNameOrRef_.jsobjectclassnameref_ = value;
+}
+
+// optional uint32 coarseType = 9 [default = 0];
+inline bool Node::has_coarsetype() const {
+ return (_has_bits_[0] & 0x00000100u) != 0;
+}
+inline void Node::set_has_coarsetype() {
+ _has_bits_[0] |= 0x00000100u;
+}
+inline void Node::clear_has_coarsetype() {
+ _has_bits_[0] &= ~0x00000100u;
+}
+inline void Node::clear_coarsetype() {
+ coarsetype_ = 0u;
+ clear_has_coarsetype();
+}
+inline ::google::protobuf::uint32 Node::coarsetype() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Node.coarseType)
+ return coarsetype_;
+}
+inline void Node::set_coarsetype(::google::protobuf::uint32 value) {
+ set_has_coarsetype();
+ coarsetype_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Node.coarseType)
+}
+
+// optional bytes scriptFilename = 10;
+inline bool Node::has_scriptfilename() const {
+ return ScriptFilenameOrRef_case() == kScriptFilename;
+}
+inline void Node::set_has_scriptfilename() {
+ _oneof_case_[2] = kScriptFilename;
+}
+inline void Node::clear_scriptfilename() {
+ if (has_scriptfilename()) {
+ delete ScriptFilenameOrRef_.scriptfilename_;
+ clear_has_ScriptFilenameOrRef();
+ }
+}
+inline const ::std::string& Node::scriptfilename() const {
+ if (has_scriptfilename()) {
+ return *ScriptFilenameOrRef_.scriptfilename_;
+ }
+ return ::google::protobuf::internal::GetEmptyStringAlreadyInited();
+}
+inline void Node::set_scriptfilename(const ::std::string& value) {
+ if (!has_scriptfilename()) {
+ clear_ScriptFilenameOrRef();
+ set_has_scriptfilename();
+ ScriptFilenameOrRef_.scriptfilename_ = new ::std::string;
+ }
+ ScriptFilenameOrRef_.scriptfilename_->assign(value);
+}
+inline void Node::set_scriptfilename(const char* value) {
+ if (!has_scriptfilename()) {
+ clear_ScriptFilenameOrRef();
+ set_has_scriptfilename();
+ ScriptFilenameOrRef_.scriptfilename_ = new ::std::string;
+ }
+ ScriptFilenameOrRef_.scriptfilename_->assign(value);
+}
+inline void Node::set_scriptfilename(const void* value, size_t size) {
+ if (!has_scriptfilename()) {
+ clear_ScriptFilenameOrRef();
+ set_has_scriptfilename();
+ ScriptFilenameOrRef_.scriptfilename_ = new ::std::string;
+ }
+ ScriptFilenameOrRef_.scriptfilename_->assign(
+ reinterpret_cast<const char*>(value), size);
+}
+inline ::std::string* Node::mutable_scriptfilename() {
+ if (!has_scriptfilename()) {
+ clear_ScriptFilenameOrRef();
+ set_has_scriptfilename();
+ ScriptFilenameOrRef_.scriptfilename_ = new ::std::string;
+ }
+ return ScriptFilenameOrRef_.scriptfilename_;
+}
+inline ::std::string* Node::release_scriptfilename() {
+ if (has_scriptfilename()) {
+ clear_has_ScriptFilenameOrRef();
+ ::std::string* temp = ScriptFilenameOrRef_.scriptfilename_;
+ ScriptFilenameOrRef_.scriptfilename_ = NULL;
+ return temp;
+ } else {
+ return NULL;
+ }
+}
+inline void Node::set_allocated_scriptfilename(::std::string* scriptfilename) {
+ clear_ScriptFilenameOrRef();
+ if (scriptfilename) {
+ set_has_scriptfilename();
+ ScriptFilenameOrRef_.scriptfilename_ = scriptfilename;
+ }
+}
+
+// optional uint64 scriptFilenameRef = 11;
+inline bool Node::has_scriptfilenameref() const {
+ return ScriptFilenameOrRef_case() == kScriptFilenameRef;
+}
+inline void Node::set_has_scriptfilenameref() {
+ _oneof_case_[2] = kScriptFilenameRef;
+}
+inline void Node::clear_scriptfilenameref() {
+ if (has_scriptfilenameref()) {
+ ScriptFilenameOrRef_.scriptfilenameref_ = GOOGLE_ULONGLONG(0);
+ clear_has_ScriptFilenameOrRef();
+ }
+}
+inline ::google::protobuf::uint64 Node::scriptfilenameref() const {
+ if (has_scriptfilenameref()) {
+ return ScriptFilenameOrRef_.scriptfilenameref_;
+ }
+ return GOOGLE_ULONGLONG(0);
+}
+inline void Node::set_scriptfilenameref(::google::protobuf::uint64 value) {
+ if (!has_scriptfilenameref()) {
+ clear_ScriptFilenameOrRef();
+ set_has_scriptfilenameref();
+ }
+ ScriptFilenameOrRef_.scriptfilenameref_ = value;
+}
+
+inline bool Node::has_TypeNameOrRef() {
+ return TypeNameOrRef_case() != TYPENAMEORREF_NOT_SET;
+}
+inline void Node::clear_has_TypeNameOrRef() {
+ _oneof_case_[0] = TYPENAMEORREF_NOT_SET;
+}
+inline bool Node::has_JSObjectClassNameOrRef() {
+ return JSObjectClassNameOrRef_case() != JSOBJECTCLASSNAMEORREF_NOT_SET;
+}
+inline void Node::clear_has_JSObjectClassNameOrRef() {
+ _oneof_case_[1] = JSOBJECTCLASSNAMEORREF_NOT_SET;
+}
+inline bool Node::has_ScriptFilenameOrRef() {
+ return ScriptFilenameOrRef_case() != SCRIPTFILENAMEORREF_NOT_SET;
+}
+inline void Node::clear_has_ScriptFilenameOrRef() {
+ _oneof_case_[2] = SCRIPTFILENAMEORREF_NOT_SET;
+}
+inline Node::TypeNameOrRefCase Node::TypeNameOrRef_case() const {
+ return Node::TypeNameOrRefCase(_oneof_case_[0]);
+}
+inline Node::JSObjectClassNameOrRefCase Node::JSObjectClassNameOrRef_case() const {
+ return Node::JSObjectClassNameOrRefCase(_oneof_case_[1]);
+}
+inline Node::ScriptFilenameOrRefCase Node::ScriptFilenameOrRef_case() const {
+ return Node::ScriptFilenameOrRefCase(_oneof_case_[2]);
+}
+// -------------------------------------------------------------------
+
+// Edge
+
+// optional uint64 referent = 1;
+inline bool Edge::has_referent() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void Edge::set_has_referent() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void Edge::clear_has_referent() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void Edge::clear_referent() {
+ referent_ = GOOGLE_ULONGLONG(0);
+ clear_has_referent();
+}
+inline ::google::protobuf::uint64 Edge::referent() const {
+ // @@protoc_insertion_point(field_get:mozilla.devtools.protobuf.Edge.referent)
+ return referent_;
+}
+inline void Edge::set_referent(::google::protobuf::uint64 value) {
+ set_has_referent();
+ referent_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.devtools.protobuf.Edge.referent)
+}
+
+// optional bytes name = 2;
+inline bool Edge::has_name() const {
+ return EdgeNameOrRef_case() == kName;
+}
+inline void Edge::set_has_name() {
+ _oneof_case_[0] = kName;
+}
+inline void Edge::clear_name() {
+ if (has_name()) {
+ delete EdgeNameOrRef_.name_;
+ clear_has_EdgeNameOrRef();
+ }
+}
+inline const ::std::string& Edge::name() const {
+ if (has_name()) {
+ return *EdgeNameOrRef_.name_;
+ }
+ return ::google::protobuf::internal::GetEmptyStringAlreadyInited();
+}
+inline void Edge::set_name(const ::std::string& value) {
+ if (!has_name()) {
+ clear_EdgeNameOrRef();
+ set_has_name();
+ EdgeNameOrRef_.name_ = new ::std::string;
+ }
+ EdgeNameOrRef_.name_->assign(value);
+}
+inline void Edge::set_name(const char* value) {
+ if (!has_name()) {
+ clear_EdgeNameOrRef();
+ set_has_name();
+ EdgeNameOrRef_.name_ = new ::std::string;
+ }
+ EdgeNameOrRef_.name_->assign(value);
+}
+inline void Edge::set_name(const void* value, size_t size) {
+ if (!has_name()) {
+ clear_EdgeNameOrRef();
+ set_has_name();
+ EdgeNameOrRef_.name_ = new ::std::string;
+ }
+ EdgeNameOrRef_.name_->assign(
+ reinterpret_cast<const char*>(value), size);
+}
+inline ::std::string* Edge::mutable_name() {
+ if (!has_name()) {
+ clear_EdgeNameOrRef();
+ set_has_name();
+ EdgeNameOrRef_.name_ = new ::std::string;
+ }
+ return EdgeNameOrRef_.name_;
+}
+inline ::std::string* Edge::release_name() {
+ if (has_name()) {
+ clear_has_EdgeNameOrRef();
+ ::std::string* temp = EdgeNameOrRef_.name_;
+ EdgeNameOrRef_.name_ = NULL;
+ return temp;
+ } else {
+ return NULL;
+ }
+}
+inline void Edge::set_allocated_name(::std::string* name) {
+ clear_EdgeNameOrRef();
+ if (name) {
+ set_has_name();
+ EdgeNameOrRef_.name_ = name;
+ }
+}
+
+// optional uint64 nameRef = 3;
+inline bool Edge::has_nameref() const {
+ return EdgeNameOrRef_case() == kNameRef;
+}
+inline void Edge::set_has_nameref() {
+ _oneof_case_[0] = kNameRef;
+}
+inline void Edge::clear_nameref() {
+ if (has_nameref()) {
+ EdgeNameOrRef_.nameref_ = GOOGLE_ULONGLONG(0);
+ clear_has_EdgeNameOrRef();
+ }
+}
+inline ::google::protobuf::uint64 Edge::nameref() const {
+ if (has_nameref()) {
+ return EdgeNameOrRef_.nameref_;
+ }
+ return GOOGLE_ULONGLONG(0);
+}
+inline void Edge::set_nameref(::google::protobuf::uint64 value) {
+ if (!has_nameref()) {
+ clear_EdgeNameOrRef();
+ set_has_nameref();
+ }
+ EdgeNameOrRef_.nameref_ = value;
+}
+
+inline bool Edge::has_EdgeNameOrRef() {
+ return EdgeNameOrRef_case() != EDGENAMEORREF_NOT_SET;
+}
+inline void Edge::clear_has_EdgeNameOrRef() {
+ _oneof_case_[0] = EDGENAMEORREF_NOT_SET;
+}
+inline Edge::EdgeNameOrRefCase Edge::EdgeNameOrRef_case() const {
+ return Edge::EdgeNameOrRefCase(_oneof_case_[0]);
+}
+
+// @@protoc_insertion_point(namespace_scope)
+
+} // namespace protobuf
+} // namespace devtools
+} // namespace mozilla
+
+#ifndef SWIG
+namespace google {
+namespace protobuf {
+
+
+} // namespace google
+} // namespace protobuf
+#endif // SWIG
+
+// @@protoc_insertion_point(global_scope)
+
+#endif // PROTOBUF_CoreDump_2eproto__INCLUDED
diff --git a/devtools/shared/heapsnapshot/CoreDump.proto b/devtools/shared/heapsnapshot/CoreDump.proto
new file mode 100644
index 000000000..24a223e11
--- /dev/null
+++ b/devtools/shared/heapsnapshot/CoreDump.proto
@@ -0,0 +1,143 @@
+/* -*- Mode: protobuf; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: set ts=8 sts=4 et sw=4 tw=99:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// # Core Dumps
+//
+// A core dump is a serialized snapshot of the heap graph. We serialize the heap
+// as a series of protobuf messages with each message prefixed by its Varint32
+// byte size so we can delimit individual protobuf messages (protobuf parsers
+// cannot determine where a message ends on their own).
+//
+// The first protobuf message is an instance of the `Metadata` message. All
+// subsequent messages will be instances of the `Node` message. The first of
+// these `Node` messages is the root node of the serialized heap graph. Here is
+// a diagram of our core dump format:
+//
+// +-----------------------------------------------------------------------+
+// | Varint32: The size of following `Metadata` message. |
+// +-----------------------------------------------------------------------+
+// | message: The core dump `Metadata` message. |
+// +-----------------------------------------------------------------------+
+// | Varint32: The size of the following `Node` message. |
+// +-----------------------------------------------------------------------+
+// | message: The first `Node` message. This is the root node. |
+// +-----------------------------------------------------------------------+
+// | Varint32: The size of the following `Node` message. |
+// +-----------------------------------------------------------------------+
+// | message: A `Node` message. |
+// +-----------------------------------------------------------------------+
+// | Varint32: The size of the following `Node` message. |
+// +-----------------------------------------------------------------------+
+// | message: A `Node` message. |
+// +-----------------------------------------------------------------------+
+// | . |
+// | . |
+// | . |
+// +-----------------------------------------------------------------------+
+//
+// Core dumps should always be written with a
+// `google::protobuf::io::GzipOutputStream` and read from a
+// `google::protobuf::io::GzipInputStream`.
+//
+// Note that all strings are de-duplicated. The first time the N^th unique
+// string is encountered, the full string is serialized. Subsequent times that
+// same string is encountered, it is referenced by N. This de-duplication
+// happens across string properties, not on a per-property basis. For example,
+// if the same K^th unique string is first used as an Edge::EdgeNameOrRef and
+// then as a StackFrame::Data::FunctionDisplayNameOrRef, the first will be the
+// actual string as the functionDisplayName oneof property, and the second will
+// be a reference to the first as the edgeNameRef oneof property whose value is
+// K.
+//
+// We would ordinarily abstract these de-duplicated strings with messages of
+// their own, but unfortunately, the protobuf compiler does not have a way to
+// inline a messsage within another message and the child message must be
+// referenced by pointer. This leads to extra mallocs that we wish to avoid.
+
+
+package mozilla.devtools.protobuf;
+
+// A collection of metadata about this core dump.
+message Metadata {
+ // Number of microseconds since midnight (00:00:00) 1 January 1970 UTC.
+ optional uint64 timeStamp = 1;
+}
+
+// A serialized version of `JS::ubi::StackFrame`. Older parent frame tails are
+// de-duplicated to cut down on [de]serialization and size costs.
+message StackFrame {
+ oneof StackFrameType {
+ // This is the first time this stack frame has been serialized, and so
+ // here is all of its data.
+ Data data = 1;
+ // A reference to a stack frame that has already been serialized and has
+ // the given number as its id.
+ uint64 ref = 2;
+ }
+
+ message Data {
+ optional uint64 id = 1;
+ optional StackFrame parent = 2;
+ optional uint32 line = 3;
+ optional uint32 column = 4;
+
+ // De-duplicated two-byte string.
+ oneof SourceOrRef {
+ bytes source = 5;
+ uint64 sourceRef = 6;
+ }
+
+ // De-duplicated two-byte string.
+ oneof FunctionDisplayNameOrRef {
+ bytes functionDisplayName = 7;
+ uint64 functionDisplayNameRef = 8;
+ }
+
+ optional bool isSystem = 9;
+ optional bool isSelfHosted = 10;
+ }
+}
+
+// A serialized version of `JS::ubi::Node` and its outgoing edges.
+message Node {
+ optional uint64 id = 1;
+
+ // De-duplicated two-byte string.
+ oneof TypeNameOrRef {
+ bytes typeName = 2;
+ uint64 typeNameRef = 3;
+ }
+
+ optional uint64 size = 4;
+ repeated Edge edges = 5;
+ optional StackFrame allocationStack = 6;
+
+ // De-duplicated one-byte string.
+ oneof JSObjectClassNameOrRef {
+ bytes jsObjectClassName = 7;
+ uint64 jsObjectClassNameRef = 8;
+ }
+
+ // JS::ubi::CoarseType. Defaults to Other.
+ optional uint32 coarseType = 9 [default = 0];
+
+ // De-duplicated one-byte string.
+ oneof ScriptFilenameOrRef {
+ bytes scriptFilename = 10;
+ uint64 scriptFilenameRef = 11;
+ }
+}
+
+// A serialized edge from the heap graph.
+message Edge {
+ optional uint64 referent = 1;
+
+ // De-duplicated two-byte string.
+ oneof EdgeNameOrRef {
+ bytes name = 2;
+ uint64 nameRef = 3;
+ }
+}
diff --git a/devtools/shared/heapsnapshot/DeserializedNode.cpp b/devtools/shared/heapsnapshot/DeserializedNode.cpp
new file mode 100644
index 000000000..fac4cccb9
--- /dev/null
+++ b/devtools/shared/heapsnapshot/DeserializedNode.cpp
@@ -0,0 +1,150 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/devtools/DeserializedNode.h"
+#include "mozilla/devtools/HeapSnapshot.h"
+#include "nsCRTGlue.h"
+
+namespace mozilla {
+namespace devtools {
+
+DeserializedEdge::DeserializedEdge(DeserializedEdge&& rhs)
+{
+ referent = rhs.referent;
+ name = rhs.name;
+}
+
+DeserializedEdge& DeserializedEdge::operator=(DeserializedEdge&& rhs)
+{
+ MOZ_ASSERT(&rhs != this);
+ this->~DeserializedEdge();
+ new(this) DeserializedEdge(Move(rhs));
+ return *this;
+}
+
+JS::ubi::Node
+DeserializedNode::getEdgeReferent(const DeserializedEdge& edge)
+{
+ auto ptr = owner->nodes.lookup(edge.referent);
+ MOZ_ASSERT(ptr);
+
+ // `HashSets` only provide const access to their values, because mutating a
+ // value might change its hash, rendering it unfindable in the set.
+ // Unfortunately, the `ubi::Node` constructor requires a non-const pointer to
+ // its referent. However, the only aspect of a `DeserializedNode` we hash on
+ // is its id, which can't be changed via `ubi::Node`, so this cast can't cause
+ // the trouble `HashSet` is concerned a non-const reference would cause.
+ return JS::ubi::Node(const_cast<DeserializedNode*>(&*ptr));
+}
+
+JS::ubi::StackFrame
+DeserializedStackFrame::getParentStackFrame() const
+{
+ MOZ_ASSERT(parent.isSome());
+ auto ptr = owner->frames.lookup(parent.ref());
+ MOZ_ASSERT(ptr);
+ // See above comment in DeserializedNode::getEdgeReferent about why this
+ // const_cast is needed and safe.
+ return JS::ubi::StackFrame(const_cast<DeserializedStackFrame*>(&*ptr));
+}
+
+} // namespace devtools
+} // namespace mozilla
+
+namespace JS {
+namespace ubi {
+
+using mozilla::devtools::DeserializedEdge;
+
+const char16_t Concrete<DeserializedNode>::concreteTypeName[] =
+ u"mozilla::devtools::DeserializedNode";
+
+const char16_t*
+Concrete<DeserializedNode>::typeName() const
+{
+ return get().typeName;
+}
+
+Node::Size
+Concrete<DeserializedNode>::size(mozilla::MallocSizeOf mallocSizeof) const
+{
+ return get().size;
+}
+
+class DeserializedEdgeRange : public EdgeRange
+{
+ DeserializedNode* node;
+ Edge currentEdge;
+ size_t i;
+
+ void settle() {
+ if (i >= node->edges.length()) {
+ front_ = nullptr;
+ return;
+ }
+
+ auto& edge = node->edges[i];
+ auto referent = node->getEdgeReferent(edge);
+ currentEdge = mozilla::Move(Edge(edge.name ? NS_strdup(edge.name) : nullptr,
+ referent));
+ front_ = &currentEdge;
+ }
+
+public:
+ explicit DeserializedEdgeRange(DeserializedNode& node)
+ : node(&node)
+ , i(0)
+ {
+ settle();
+ }
+
+ void popFront() override
+ {
+ i++;
+ settle();
+ }
+};
+
+StackFrame
+Concrete<DeserializedNode>::allocationStack() const
+{
+ MOZ_ASSERT(hasAllocationStack());
+ auto id = get().allocationStack.ref();
+ auto ptr = get().owner->frames.lookup(id);
+ MOZ_ASSERT(ptr);
+ // See above comment in DeserializedNode::getEdgeReferent about why this
+ // const_cast is needed and safe.
+ return JS::ubi::StackFrame(const_cast<DeserializedStackFrame*>(&*ptr));
+}
+
+
+js::UniquePtr<EdgeRange>
+Concrete<DeserializedNode>::edges(JSContext* cx, bool) const
+{
+ js::UniquePtr<DeserializedEdgeRange> range(js_new<DeserializedEdgeRange>(get()));
+
+ if (!range)
+ return nullptr;
+
+ return js::UniquePtr<EdgeRange>(range.release());
+}
+
+StackFrame
+ConcreteStackFrame<DeserializedStackFrame>::parent() const
+{
+ return get().parent.isNothing() ? StackFrame() : get().getParentStackFrame();
+}
+
+bool
+ConcreteStackFrame<DeserializedStackFrame>::constructSavedFrameStack(
+ JSContext* cx,
+ MutableHandleObject outSavedFrameStack) const
+{
+ StackFrame f(&get());
+ return ConstructSavedFrameStackSlow(cx, f, outSavedFrameStack);
+}
+
+} // namespace ubi
+} // namespace JS
diff --git a/devtools/shared/heapsnapshot/DeserializedNode.h b/devtools/shared/heapsnapshot/DeserializedNode.h
new file mode 100644
index 000000000..60d1fb408
--- /dev/null
+++ b/devtools/shared/heapsnapshot/DeserializedNode.h
@@ -0,0 +1,317 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_devtools_DeserializedNode__
+#define mozilla_devtools_DeserializedNode__
+
+#include "js/UbiNode.h"
+#include "js/UniquePtr.h"
+#include "mozilla/devtools/CoreDump.pb.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Move.h"
+#include "mozilla/Vector.h"
+
+// `Deserialized{Node,Edge}` translate protobuf messages from our core dump
+// format into structures we can rely upon for implementing `JS::ubi::Node`
+// specializations on top of. All of the properties of the protobuf messages are
+// optional for future compatibility, and this is the layer where we validate
+// that the properties that do actually exist in any given message fulfill our
+// semantic requirements.
+//
+// Both `DeserializedNode` and `DeserializedEdge` are always owned by a
+// `HeapSnapshot` instance, and their lifetimes must not extend after that of
+// their owning `HeapSnapshot`.
+
+namespace mozilla {
+namespace devtools {
+
+class HeapSnapshot;
+
+using NodeId = uint64_t;
+using StackFrameId = uint64_t;
+
+// A `DeserializedEdge` represents an edge in the heap graph pointing to the
+// node with id equal to `DeserializedEdge::referent` that we deserialized from
+// a core dump.
+struct DeserializedEdge {
+ NodeId referent;
+ // A borrowed reference to a string owned by this node's owning HeapSnapshot.
+ const char16_t* name;
+
+ explicit DeserializedEdge(NodeId referent, const char16_t* edgeName = nullptr)
+ : referent(referent)
+ , name(edgeName)
+ { }
+ DeserializedEdge(DeserializedEdge&& rhs);
+ DeserializedEdge& operator=(DeserializedEdge&& rhs);
+
+private:
+ DeserializedEdge(const DeserializedEdge&) = delete;
+ DeserializedEdge& operator=(const DeserializedEdge&) = delete;
+};
+
+// A `DeserializedNode` is a node in the heap graph that we deserialized from a
+// core dump.
+struct DeserializedNode {
+ using EdgeVector = Vector<DeserializedEdge>;
+ using UniqueStringPtr = UniquePtr<char16_t[]>;
+
+ NodeId id;
+ JS::ubi::CoarseType coarseType;
+ // A borrowed reference to a string owned by this node's owning HeapSnapshot.
+ const char16_t* typeName;
+ uint64_t size;
+ EdgeVector edges;
+ Maybe<StackFrameId> allocationStack;
+ // A borrowed reference to a string owned by this node's owning HeapSnapshot.
+ const char* jsObjectClassName;
+ // A borrowed reference to a string owned by this node's owning HeapSnapshot.
+ const char* scriptFilename;
+ // A weak pointer to this node's owning `HeapSnapshot`. Safe without
+ // AddRef'ing because this node's lifetime is equal to that of its owner.
+ HeapSnapshot* owner;
+
+ DeserializedNode(NodeId id,
+ JS::ubi::CoarseType coarseType,
+ const char16_t* typeName,
+ uint64_t size,
+ EdgeVector&& edges,
+ Maybe<StackFrameId> allocationStack,
+ const char* className,
+ const char* filename,
+ HeapSnapshot& owner)
+ : id(id)
+ , coarseType(coarseType)
+ , typeName(typeName)
+ , size(size)
+ , edges(Move(edges))
+ , allocationStack(allocationStack)
+ , jsObjectClassName(className)
+ , scriptFilename(filename)
+ , owner(&owner)
+ { }
+ virtual ~DeserializedNode() { }
+
+ DeserializedNode(DeserializedNode&& rhs)
+ : id(rhs.id)
+ , coarseType(rhs.coarseType)
+ , typeName(rhs.typeName)
+ , size(rhs.size)
+ , edges(Move(rhs.edges))
+ , allocationStack(rhs.allocationStack)
+ , jsObjectClassName(rhs.jsObjectClassName)
+ , scriptFilename(rhs.scriptFilename)
+ , owner(rhs.owner)
+ { }
+
+ DeserializedNode& operator=(DeserializedNode&& rhs)
+ {
+ MOZ_ASSERT(&rhs != this);
+ this->~DeserializedNode();
+ new(this) DeserializedNode(Move(rhs));
+ return *this;
+ }
+
+ // Get a borrowed reference to the given edge's referent. This method is
+ // virtual to provide a hook for gmock and gtest.
+ virtual JS::ubi::Node getEdgeReferent(const DeserializedEdge& edge);
+
+ struct HashPolicy;
+
+protected:
+ // This is only for use with `MockDeserializedNode` in testing.
+ DeserializedNode(NodeId id, const char16_t* typeName, uint64_t size)
+ : id(id)
+ , coarseType(JS::ubi::CoarseType::Other)
+ , typeName(typeName)
+ , size(size)
+ , edges()
+ , allocationStack(Nothing())
+ , jsObjectClassName(nullptr)
+ , scriptFilename(nullptr)
+ , owner(nullptr)
+ { }
+
+private:
+ DeserializedNode(const DeserializedNode&) = delete;
+ DeserializedNode& operator=(const DeserializedNode&) = delete;
+};
+
+static inline js::HashNumber
+hashIdDerivedFromPtr(uint64_t id)
+{
+ // NodeIds and StackFrameIds are always 64 bits, but they are derived from
+ // the original referents' addresses, which could have been either 32 or 64
+ // bits long. As such, NodeId and StackFrameId have little entropy in their
+ // bottom three bits, and may or may not have entropy in their upper 32
+ // bits. This hash should manage both cases well.
+ id >>= 3;
+ return js::HashNumber((id >> 32) ^ id);
+}
+
+struct DeserializedNode::HashPolicy
+{
+ using Lookup = NodeId;
+
+ static js::HashNumber hash(const Lookup& lookup) {
+ return hashIdDerivedFromPtr(lookup);
+ }
+
+ static bool match(const DeserializedNode& existing, const Lookup& lookup) {
+ return existing.id == lookup;
+ }
+};
+
+// A `DeserializedStackFrame` is a stack frame referred to by a thing in the
+// heap graph that we deserialized from a core dump.
+struct DeserializedStackFrame {
+ StackFrameId id;
+ Maybe<StackFrameId> parent;
+ uint32_t line;
+ uint32_t column;
+ // Borrowed references to strings owned by this DeserializedStackFrame's
+ // owning HeapSnapshot.
+ const char16_t* source;
+ const char16_t* functionDisplayName;
+ bool isSystem;
+ bool isSelfHosted;
+ // A weak pointer to this frame's owning `HeapSnapshot`. Safe without
+ // AddRef'ing because this frame's lifetime is equal to that of its owner.
+ HeapSnapshot* owner;
+
+ explicit DeserializedStackFrame(StackFrameId id,
+ const Maybe<StackFrameId>& parent,
+ uint32_t line,
+ uint32_t column,
+ const char16_t* source,
+ const char16_t* functionDisplayName,
+ bool isSystem,
+ bool isSelfHosted,
+ HeapSnapshot& owner)
+ : id(id)
+ , parent(parent)
+ , line(line)
+ , column(column)
+ , source(source)
+ , functionDisplayName(functionDisplayName)
+ , isSystem(isSystem)
+ , isSelfHosted(isSelfHosted)
+ , owner(&owner)
+ {
+ MOZ_ASSERT(source);
+ }
+
+ JS::ubi::StackFrame getParentStackFrame() const;
+
+ struct HashPolicy;
+
+protected:
+ // This is exposed only for MockDeserializedStackFrame in the gtests.
+ explicit DeserializedStackFrame()
+ : id(0)
+ , parent(Nothing())
+ , line(0)
+ , column(0)
+ , source(nullptr)
+ , functionDisplayName(nullptr)
+ , isSystem(false)
+ , isSelfHosted(false)
+ , owner(nullptr)
+ { };
+};
+
+struct DeserializedStackFrame::HashPolicy {
+ using Lookup = StackFrameId;
+
+ static js::HashNumber hash(const Lookup& lookup) {
+ return hashIdDerivedFromPtr(lookup);
+ }
+
+ static bool match(const DeserializedStackFrame& existing, const Lookup& lookup) {
+ return existing.id == lookup;
+ }
+};
+
+} // namespace devtools
+} // namespace mozilla
+
+namespace JS {
+namespace ubi {
+
+using mozilla::devtools::DeserializedNode;
+using mozilla::devtools::DeserializedStackFrame;
+
+template<>
+class Concrete<DeserializedNode> : public Base
+{
+protected:
+ explicit Concrete(DeserializedNode* ptr) : Base(ptr) { }
+ DeserializedNode& get() const {
+ return *static_cast<DeserializedNode*>(ptr);
+ }
+
+public:
+ static void construct(void* storage, DeserializedNode* ptr) {
+ new (storage) Concrete(ptr);
+ }
+
+ CoarseType coarseType() const final { return get().coarseType; }
+ Id identifier() const override { return get().id; }
+ bool isLive() const override { return false; }
+ const char16_t* typeName() const override;
+ Node::Size size(mozilla::MallocSizeOf mallocSizeof) const override;
+ const char* jsObjectClassName() const override { return get().jsObjectClassName; }
+ const char* scriptFilename() const final { return get().scriptFilename; }
+
+ bool hasAllocationStack() const override { return get().allocationStack.isSome(); }
+ StackFrame allocationStack() const override;
+
+ // We ignore the `bool wantNames` parameter because we can't control whether
+ // the core dump was serialized with edge names or not.
+ js::UniquePtr<EdgeRange> edges(JSContext* cx, bool) const override;
+
+ static const char16_t concreteTypeName[];
+};
+
+template<>
+class ConcreteStackFrame<DeserializedStackFrame> : public BaseStackFrame
+{
+protected:
+ explicit ConcreteStackFrame(DeserializedStackFrame* ptr)
+ : BaseStackFrame(ptr)
+ { }
+
+ DeserializedStackFrame& get() const {
+ return *static_cast<DeserializedStackFrame*>(ptr);
+ }
+
+public:
+ static void construct(void* storage, DeserializedStackFrame* ptr) {
+ new (storage) ConcreteStackFrame(ptr);
+ }
+
+ uint64_t identifier() const override { return get().id; }
+ uint32_t line() const override { return get().line; }
+ uint32_t column() const override { return get().column; }
+ bool isSystem() const override { return get().isSystem; }
+ bool isSelfHosted(JSContext* cx) const override { return get().isSelfHosted; }
+ void trace(JSTracer* trc) override { }
+ AtomOrTwoByteChars source() const override {
+ return AtomOrTwoByteChars(get().source);
+ }
+ AtomOrTwoByteChars functionDisplayName() const override {
+ return AtomOrTwoByteChars(get().functionDisplayName);
+ }
+
+ StackFrame parent() const override;
+ bool constructSavedFrameStack(JSContext* cx,
+ MutableHandleObject outSavedFrameStack)
+ const override;
+};
+
+} // namespace ubi
+} // namespace JS
+
+#endif // mozilla_devtools_DeserializedNode__
diff --git a/devtools/shared/heapsnapshot/DominatorTree.cpp b/devtools/shared/heapsnapshot/DominatorTree.cpp
new file mode 100644
index 000000000..e53c196cf
--- /dev/null
+++ b/devtools/shared/heapsnapshot/DominatorTree.cpp
@@ -0,0 +1,140 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/devtools/DominatorTree.h"
+#include "mozilla/dom/DominatorTreeBinding.h"
+
+namespace mozilla {
+namespace devtools {
+
+dom::Nullable<uint64_t>
+DominatorTree::GetRetainedSize(uint64_t aNodeId, ErrorResult& aRv)
+{
+ JS::ubi::Node::Id id(aNodeId);
+ auto node = mHeapSnapshot->getNodeById(id);
+ if (node.isNothing())
+ return dom::Nullable<uint64_t>();
+
+ auto mallocSizeOf = GetCurrentThreadDebuggerMallocSizeOf();
+ JS::ubi::Node::Size size = 0;
+ if (!mDominatorTree.getRetainedSize(*node, mallocSizeOf, size)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return dom::Nullable<uint64_t>();
+ }
+
+ MOZ_ASSERT(size != 0,
+ "The node should not have been unknown since we got it from the heap snapshot.");
+ return dom::Nullable<uint64_t>(size);
+}
+
+struct NodeAndRetainedSize
+{
+ JS::ubi::Node mNode;
+ JS::ubi::Node::Size mSize;
+
+ NodeAndRetainedSize(const JS::ubi::Node& aNode, JS::ubi::Node::Size aSize)
+ : mNode(aNode)
+ , mSize(aSize)
+ { }
+
+ struct Comparator
+ {
+ static bool
+ Equals(const NodeAndRetainedSize& aLhs, const NodeAndRetainedSize& aRhs)
+ {
+ return aLhs.mSize == aRhs.mSize;
+ }
+
+ static bool
+ LessThan(const NodeAndRetainedSize& aLhs, const NodeAndRetainedSize& aRhs)
+ {
+ // Use > because we want to sort from greatest to least retained size.
+ return aLhs.mSize > aRhs.mSize;
+ }
+ };
+};
+
+void
+DominatorTree::GetImmediatelyDominated(uint64_t aNodeId,
+ dom::Nullable<nsTArray<uint64_t>>& aOutResult,
+ ErrorResult& aRv)
+{
+ MOZ_ASSERT(aOutResult.IsNull());
+
+ JS::ubi::Node::Id id(aNodeId);
+ Maybe<JS::ubi::Node> node = mHeapSnapshot->getNodeById(id);
+ if (node.isNothing())
+ return;
+
+ // Get all immediately dominated nodes and their retained sizes.
+ MallocSizeOf mallocSizeOf = GetCurrentThreadDebuggerMallocSizeOf();
+ Maybe<JS::ubi::DominatorTree::DominatedSetRange> range = mDominatorTree.getDominatedSet(*node);
+ MOZ_ASSERT(range.isSome(), "The node should be known, since we got it from the heap snapshot.");
+ size_t length = range->length();
+ nsTArray<NodeAndRetainedSize> dominatedNodes(length);
+ for (const JS::ubi::Node& dominatedNode : *range) {
+ JS::ubi::Node::Size retainedSize = 0;
+ if (NS_WARN_IF(!mDominatorTree.getRetainedSize(dominatedNode, mallocSizeOf, retainedSize))) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+ MOZ_ASSERT(retainedSize != 0,
+ "retainedSize should not be zero since we know the node is in the dominator tree.");
+
+ dominatedNodes.AppendElement(NodeAndRetainedSize(dominatedNode, retainedSize));
+ }
+
+ // Sort them by retained size.
+ NodeAndRetainedSize::Comparator comparator;
+ dominatedNodes.Sort(comparator);
+
+ // Fill the result with the nodes' ids.
+ JS::ubi::Node root = mDominatorTree.root();
+ aOutResult.SetValue(nsTArray<uint64_t>(length));
+ for (const NodeAndRetainedSize& entry : dominatedNodes) {
+ // The root dominates itself, but we don't want to expose that to JS.
+ if (entry.mNode == root)
+ continue;
+
+ aOutResult.Value().AppendElement(entry.mNode.identifier());
+ }
+}
+
+dom::Nullable<uint64_t>
+DominatorTree::GetImmediateDominator(uint64_t aNodeId) const
+{
+ JS::ubi::Node::Id id(aNodeId);
+ Maybe<JS::ubi::Node> node = mHeapSnapshot->getNodeById(id);
+ if (node.isNothing())
+ return dom::Nullable<uint64_t>();
+
+ JS::ubi::Node dominator = mDominatorTree.getImmediateDominator(*node);
+ if (!dominator || dominator == *node)
+ return dom::Nullable<uint64_t>();
+
+ return dom::Nullable<uint64_t>(dominator.identifier());
+}
+
+
+/*** Cycle Collection Boilerplate *****************************************************************/
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DominatorTree, mParent, mHeapSnapshot)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(DominatorTree)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(DominatorTree)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DominatorTree)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+/* virtual */ JSObject*
+DominatorTree::WrapObject(JSContext* aCx, JS::HandleObject aGivenProto)
+{
+ return dom::DominatorTreeBinding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace devtools
+} // namespace mozilla
diff --git a/devtools/shared/heapsnapshot/DominatorTree.h b/devtools/shared/heapsnapshot/DominatorTree.h
new file mode 100644
index 000000000..f785d4916
--- /dev/null
+++ b/devtools/shared/heapsnapshot/DominatorTree.h
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_devtools_DominatorTree__
+#define mozilla_devtools_DominatorTree__
+
+#include "mozilla/devtools/HeapSnapshot.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/RefCounted.h"
+#include "js/UbiNodeDominatorTree.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla {
+namespace devtools {
+
+class DominatorTree final : public nsISupports
+ , public nsWrapperCache
+{
+protected:
+ nsCOMPtr<nsISupports> mParent;
+
+ virtual ~DominatorTree() { }
+
+private:
+ JS::ubi::DominatorTree mDominatorTree;
+ RefPtr<HeapSnapshot> mHeapSnapshot;
+
+public:
+ explicit DominatorTree(JS::ubi::DominatorTree&& aDominatorTree, HeapSnapshot* aHeapSnapshot,
+ nsISupports* aParent)
+ : mParent(aParent)
+ , mDominatorTree(Move(aDominatorTree))
+ , mHeapSnapshot(aHeapSnapshot)
+ {
+ MOZ_ASSERT(aParent);
+ MOZ_ASSERT(aHeapSnapshot);
+ };
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS;
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(DominatorTree);
+
+ nsISupports* GetParentObject() const { return mParent; }
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // readonly attribute NodeId root
+ uint64_t Root() const { return mDominatorTree.root().identifier(); }
+
+ // [Throws] NodeSize getRetainedSize(NodeId node)
+ dom::Nullable<uint64_t> GetRetainedSize(uint64_t aNodeId, ErrorResult& aRv);
+
+ // [Throws] sequence<NodeId>? getImmediatelyDominated(NodeId node);
+ void GetImmediatelyDominated(uint64_t aNodeId, dom::Nullable<nsTArray<uint64_t>>& aOutDominated,
+ ErrorResult& aRv);
+
+ // NodeId? getImmediateDominator(NodeId node);
+ dom::Nullable<uint64_t> GetImmediateDominator(uint64_t aNodeId) const;
+};
+
+} // namespace devtools
+} // namespace mozilla
+
+#endif // mozilla_devtools_DominatorTree__
diff --git a/devtools/shared/heapsnapshot/DominatorTreeNode.js b/devtools/shared/heapsnapshot/DominatorTreeNode.js
new file mode 100644
index 000000000..13a847fd0
--- /dev/null
+++ b/devtools/shared/heapsnapshot/DominatorTreeNode.js
@@ -0,0 +1,336 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { immutableUpdate } = require("resource://devtools/shared/ThreadSafeDevToolsUtils.js");
+const { Visitor, walk } = require("resource://devtools/shared/heapsnapshot/CensusUtils.js");
+const { deduplicatePaths } = require("resource://devtools/shared/heapsnapshot/shortest-paths");
+
+const DEFAULT_MAX_DEPTH = 4;
+const DEFAULT_MAX_SIBLINGS = 15;
+const DEFAULT_MAX_NUM_PATHS = 5;
+
+/**
+ * A single node in a dominator tree.
+ *
+ * @param {NodeId} nodeId
+ * @param {NodeSize} retainedSize
+ */
+function DominatorTreeNode(nodeId, label, shallowSize, retainedSize) {
+ // The id of this node.
+ this.nodeId = nodeId;
+
+ // The label structure generated by describing the given node.
+ this.label = label;
+
+ // The shallow size of this node.
+ this.shallowSize = shallowSize;
+
+ // The retained size of this node.
+ this.retainedSize = retainedSize;
+
+ // The id of this node's parent or undefined if this node is the root.
+ this.parentId = undefined;
+
+ // An array of immediately dominated child `DominatorTreeNode`s, or undefined.
+ this.children = undefined;
+
+ // An object of the form returned by `deduplicatePaths`, encoding the set of
+ // the N shortest retaining paths for this node as a graph.
+ this.shortestPaths = undefined;
+
+ // True iff the `children` property does not contain every immediately
+ // dominated node.
+ //
+ // * If children is an array and this property is true: the array does not
+ // contain the complete set of immediately dominated children.
+ // * If children is an array and this property is false: the array contains
+ // the complete set of immediately dominated children.
+ // * If children is undefined and this property is true: there exist
+ // immediately dominated children for this node, but they have not been
+ // loaded yet.
+ // * If children is undefined and this property is false: this node does not
+ // dominate any others and therefore has no children.
+ this.moreChildrenAvailable = true;
+}
+
+DominatorTreeNode.prototype = null;
+
+module.exports = DominatorTreeNode;
+
+/**
+ * Add `child` to the `parent`'s set of children.
+ *
+ * @param {DominatorTreeNode} parent
+ * @param {DominatorTreeNode} child
+ */
+DominatorTreeNode.addChild = function (parent, child) {
+ if (parent.children === undefined) {
+ parent.children = [];
+ }
+
+ parent.children.push(child);
+ child.parentId = parent.nodeId;
+};
+
+/**
+ * A Visitor that is used to generate a label for a node in the heap snapshot
+ * and get its shallow size as well while we are at it.
+ */
+function LabelAndShallowSizeVisitor() {
+ // As we walk the description, we accumulate edges in this array.
+ this._labelPieces = [];
+
+ // Once we reach the non-zero count leaf node in the description, we move the
+ // labelPieces here to signify that we no longer need to accumulate edges.
+ this._label = undefined;
+
+ // Once we reach the non-zero count leaf node in the description, we grab the
+ // shallow size and place it here.
+ this._shallowSize = 0;
+}
+
+DominatorTreeNode.LabelAndShallowSizeVisitor = LabelAndShallowSizeVisitor;
+
+LabelAndShallowSizeVisitor.prototype = Object.create(Visitor);
+
+/**
+ * @overrides Visitor.prototype.enter
+ */
+LabelAndShallowSizeVisitor.prototype.enter = function (breakdown, report, edge) {
+ if (this._labelPieces && edge) {
+ this._labelPieces.push(edge);
+ }
+};
+
+/**
+ * @overrides Visitor.prototype.exit
+ */
+LabelAndShallowSizeVisitor.prototype.exit = function (breakdown, report, edge) {
+ if (this._labelPieces && edge) {
+ this._labelPieces.pop();
+ }
+};
+
+/**
+ * @overrides Visitor.prototype.count
+ */
+LabelAndShallowSizeVisitor.prototype.count = function (breakdown, report, edge) {
+ if (report.count === 0) {
+ return;
+ }
+
+ this._label = this._labelPieces;
+ this._labelPieces = undefined;
+
+ this._shallowSize = report.bytes;
+};
+
+/**
+ * Get the generated label structure accumulated by this visitor.
+ *
+ * @returns {Object}
+ */
+LabelAndShallowSizeVisitor.prototype.label = function () {
+ return this._label;
+};
+
+/**
+ * Get the shallow size of the node this visitor visited.
+ *
+ * @returns {Number}
+ */
+LabelAndShallowSizeVisitor.prototype.shallowSize = function () {
+ return this._shallowSize;
+};
+
+/**
+ * Generate a label structure for the node with the given id and grab its
+ * shallow size.
+ *
+ * What is a "label" structure? HeapSnapshot.describeNode essentially takes a
+ * census of a single node rather than the whole heap graph. The resulting
+ * report has only one count leaf that is non-zero. The label structure is the
+ * path in this report from the root to the non-zero count leaf.
+ *
+ * @param {Number} nodeId
+ * @param {HeapSnapshot} snapshot
+ * @param {Object} breakdown
+ *
+ * @returns {Object}
+ * An object with the following properties:
+ * - {Number} shallowSize
+ * - {Object} label
+ */
+DominatorTreeNode.getLabelAndShallowSize = function (nodeId,
+ snapshot,
+ breakdown) {
+ const description = snapshot.describeNode(breakdown, nodeId);
+
+ const visitor = new LabelAndShallowSizeVisitor();
+ walk(breakdown, description, visitor);
+
+ return {
+ label: visitor.label(),
+ shallowSize: visitor.shallowSize(),
+ };
+};
+
+/**
+ * Do a partial traversal of the given dominator tree and convert it into a tree
+ * of `DominatorTreeNode`s. Dominator trees have a node for every node in the
+ * snapshot's heap graph, so we must not allocate a JS object for every node. It
+ * would be way too many and the node count is effectively unbounded.
+ *
+ * Go no deeper down the tree than `maxDepth` and only consider at most
+ * `maxSiblings` within any single node's children.
+ *
+ * @param {DominatorTree} dominatorTree
+ * @param {HeapSnapshot} snapshot
+ * @param {Object} breakdown
+ * @param {Number} maxDepth
+ * @param {Number} maxSiblings
+ *
+ * @returns {DominatorTreeNode}
+ */
+DominatorTreeNode.partialTraversal = function (dominatorTree,
+ snapshot,
+ breakdown,
+ maxDepth = DEFAULT_MAX_DEPTH,
+ maxSiblings = DEFAULT_MAX_SIBLINGS) {
+ function dfs(nodeId, depth) {
+ const { label, shallowSize } =
+ DominatorTreeNode.getLabelAndShallowSize(nodeId, snapshot, breakdown);
+ const retainedSize = dominatorTree.getRetainedSize(nodeId);
+ const node = new DominatorTreeNode(nodeId, label, shallowSize, retainedSize);
+ const childNodeIds = dominatorTree.getImmediatelyDominated(nodeId);
+
+ const newDepth = depth + 1;
+ if (newDepth < maxDepth) {
+ const endIdx = Math.min(childNodeIds.length, maxSiblings);
+ for (let i = 0; i < endIdx; i++) {
+ DominatorTreeNode.addChild(node, dfs(childNodeIds[i], newDepth));
+ }
+ node.moreChildrenAvailable = endIdx < childNodeIds.length;
+ } else {
+ node.moreChildrenAvailable = childNodeIds.length > 0;
+ }
+
+ return node;
+ }
+
+ return dfs(dominatorTree.root, 0);
+};
+
+/**
+ * Insert more children into the given (partially complete) dominator tree.
+ *
+ * The tree is updated in an immutable and persistent manner: a new tree is
+ * returned, but all unmodified subtrees (which is most) are shared with the
+ * original tree. Only the modified nodes are re-allocated.
+ *
+ * @param {DominatorTreeNode} tree
+ * @param {Array<NodeId>} path
+ * @param {Array<DominatorTreeNode>} newChildren
+ * @param {Boolean} moreChildrenAvailable
+ *
+ * @returns {DominatorTreeNode}
+ */
+DominatorTreeNode.insert = function (tree, path, newChildren, moreChildrenAvailable) {
+ function insert(tree, i) {
+ if (tree.nodeId !== path[i]) {
+ return tree;
+ }
+
+ if (i == path.length - 1) {
+ return immutableUpdate(tree, {
+ children: (tree.children || []).concat(newChildren),
+ moreChildrenAvailable,
+ });
+ }
+
+ return tree.children
+ ? immutableUpdate(tree, {
+ children: tree.children.map(c => insert(c, i + 1))
+ })
+ : tree;
+ }
+
+ return insert(tree, 0);
+};
+
+/**
+ * Get the new canonical node with the given `id` in `tree` that exists along
+ * `path`. If there is no such node along `path`, return null.
+ *
+ * This is useful if we have a reference to a now-outdated DominatorTreeNode due
+ * to a recent call to DominatorTreeNode.insert and want to get the up-to-date
+ * version. We don't have to walk the whole tree: if there is an updated version
+ * of the node then it *must* be along the path.
+ *
+ * @param {NodeId} id
+ * @param {DominatorTreeNode} tree
+ * @param {Array<NodeId>} path
+ *
+ * @returns {DominatorTreeNode|null}
+ */
+DominatorTreeNode.getNodeByIdAlongPath = function (id, tree, path) {
+ function find(node, i) {
+ if (!node || node.nodeId !== path[i]) {
+ return null;
+ }
+
+ if (node.nodeId === id) {
+ return node;
+ }
+
+ if (i === path.length - 1 || !node.children) {
+ return null;
+ }
+
+ const nextId = path[i + 1];
+ return find(node.children.find(c => c.nodeId === nextId), i + 1);
+ }
+
+ return find(tree, 0);
+};
+
+/**
+ * Find the shortest retaining paths for the given set of DominatorTreeNodes,
+ * and populate each node's `shortestPaths` property with them in place.
+ *
+ * @param {HeapSnapshot} snapshot
+ * @param {Object} breakdown
+ * @param {NodeId} start
+ * @param {Array<DominatorTreeNode>} treeNodes
+ * @param {Number} maxNumPaths
+ */
+DominatorTreeNode.attachShortestPaths = function (snapshot,
+ breakdown,
+ start,
+ treeNodes,
+ maxNumPaths = DEFAULT_MAX_NUM_PATHS) {
+ const idToTreeNode = new Map();
+ const targets = [];
+ for (let node of treeNodes) {
+ const id = node.nodeId;
+ idToTreeNode.set(id, node);
+ targets.push(id);
+ }
+
+ const shortestPaths = snapshot.computeShortestPaths(start,
+ targets,
+ maxNumPaths);
+
+ for (let [target, paths] of shortestPaths) {
+ const deduped = deduplicatePaths(target, paths);
+ deduped.nodes = deduped.nodes.map(id => {
+ const { label } =
+ DominatorTreeNode.getLabelAndShallowSize(id, snapshot, breakdown);
+ return { id, label };
+ });
+
+ idToTreeNode.get(target).shortestPaths = deduped;
+ }
+};
diff --git a/devtools/shared/heapsnapshot/FileDescriptorOutputStream.cpp b/devtools/shared/heapsnapshot/FileDescriptorOutputStream.cpp
new file mode 100644
index 000000000..72a289558
--- /dev/null
+++ b/devtools/shared/heapsnapshot/FileDescriptorOutputStream.cpp
@@ -0,0 +1,86 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/devtools/FileDescriptorOutputStream.h"
+#include "private/pprio.h"
+
+namespace mozilla {
+namespace devtools {
+
+/* static */ already_AddRefed<FileDescriptorOutputStream>
+FileDescriptorOutputStream::Create(const ipc::FileDescriptor& fileDescriptor)
+{
+ if (NS_WARN_IF(!fileDescriptor.IsValid()))
+ return nullptr;
+
+ auto rawFD = fileDescriptor.ClonePlatformHandle();
+ PRFileDesc* prfd = PR_ImportFile(PROsfd(rawFD.release()));
+ if (NS_WARN_IF(!prfd))
+ return nullptr;
+
+ RefPtr<FileDescriptorOutputStream> stream = new FileDescriptorOutputStream(prfd);
+ return stream.forget();
+}
+
+NS_IMPL_ISUPPORTS(FileDescriptorOutputStream, nsIOutputStream);
+
+NS_IMETHODIMP
+FileDescriptorOutputStream::Close()
+{
+ // Repeatedly closing is idempotent.
+ if (!fd)
+ return NS_OK;
+
+ if (PR_Close(fd) != PR_SUCCESS)
+ return NS_ERROR_FAILURE;
+ fd = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+FileDescriptorOutputStream::Write(const char* buf, uint32_t count, uint32_t* retval)
+{
+ if (NS_WARN_IF(!fd))
+ return NS_ERROR_FAILURE;
+
+ auto written = PR_Write(fd, buf, count);
+ if (written < 0)
+ return NS_ERROR_FAILURE;
+ *retval = written;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+FileDescriptorOutputStream::Flush()
+{
+ if (NS_WARN_IF(!fd))
+ return NS_ERROR_FAILURE;
+
+ return PR_Sync(fd) == PR_SUCCESS ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+FileDescriptorOutputStream::WriteFrom(nsIInputStream* fromStream, uint32_t count,
+ uint32_t* retval)
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorOutputStream::WriteSegments(nsReadSegmentFun reader, void* closure,
+ uint32_t count, uint32_t* retval)
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+FileDescriptorOutputStream::IsNonBlocking(bool* retval)
+{
+ *retval = false;
+ return NS_OK;
+}
+
+} // namespace devtools
+} // namespace mozilla
diff --git a/devtools/shared/heapsnapshot/FileDescriptorOutputStream.h b/devtools/shared/heapsnapshot/FileDescriptorOutputStream.h
new file mode 100644
index 000000000..6990f1fc3
--- /dev/null
+++ b/devtools/shared/heapsnapshot/FileDescriptorOutputStream.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_devtools_FileDescriptorOutputStream_h
+#define mozilla_devtools_FileDescriptorOutputStream_h
+
+#include <prio.h>
+
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/ipc/FileDescriptor.h"
+#include "nsIOutputStream.h"
+
+namespace mozilla {
+namespace devtools {
+
+class FileDescriptorOutputStream final : public nsIOutputStream
+{
+private:
+ PRFileDesc* fd;
+
+public:
+ static already_AddRefed<FileDescriptorOutputStream>
+ Create(const ipc::FileDescriptor& fileDescriptor);
+
+private:
+ explicit FileDescriptorOutputStream(PRFileDesc* prfd)
+ : fd(prfd)
+ { }
+
+ virtual ~FileDescriptorOutputStream() { }
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIOUTPUTSTREAM
+};
+
+} // namespace devtools
+} // namespace mozilla
+
+#endif // mozilla_devtools_FileDescriptorOutputStream_h
diff --git a/devtools/shared/heapsnapshot/HeapAnalysesClient.js b/devtools/shared/heapsnapshot/HeapAnalysesClient.js
new file mode 100644
index 000000000..98601a2b1
--- /dev/null
+++ b/devtools/shared/heapsnapshot/HeapAnalysesClient.js
@@ -0,0 +1,277 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const { DevToolsWorker } = require("devtools/shared/worker/worker");
+
+const WORKER_URL =
+ "resource://devtools/shared/heapsnapshot/HeapAnalysesWorker.js";
+var workerCounter = 0;
+
+/**
+ * A HeapAnalysesClient instance provides a developer-friendly interface for
+ * interacting with a HeapAnalysesWorker. This enables users to be ignorant of
+ * the message passing protocol used to communicate with the worker. The
+ * HeapAnalysesClient owns the worker, and terminating the worker is done by
+ * terminating the client (see the `destroy` method).
+ */
+const HeapAnalysesClient = module.exports = function () {
+ this._worker = new DevToolsWorker(WORKER_URL, {
+ name: `HeapAnalyses-${workerCounter++}`,
+ verbose: DevToolsUtils.dumpv.wantVerbose
+ });
+};
+
+/**
+ * Destroy the worker, causing it to release its resources (such as heap
+ * snapshots it has deserialized and read into memory). The client is no longer
+ * usable after calling this method.
+ */
+HeapAnalysesClient.prototype.destroy = function () {
+ this._worker.destroy();
+ this._worker = null;
+};
+
+/**
+ * Tell the worker to read into memory the heap snapshot at the given file
+ * path. This is a prerequisite for asking the worker to perform various
+ * analyses on a heap snapshot.
+ *
+ * @param {String} snapshotFilePath
+ *
+ * @returns Promise
+ * The promise is fulfilled if the heap snapshot is successfully
+ * deserialized and read into memory. The promise is rejected if that
+ * does not happen, eg due to a bad file path or malformed heap
+ * snapshot file.
+ */
+HeapAnalysesClient.prototype.readHeapSnapshot = function (snapshotFilePath) {
+ return this._worker.performTask("readHeapSnapshot", { snapshotFilePath });
+};
+
+/**
+ * Tell the worker to delete all references to the snapshot and dominator trees
+ * linked to the provided snapshot file path.
+ *
+ * @param {String} snapshotFilePath
+ * @return Promise<undefined>
+ */
+HeapAnalysesClient.prototype.deleteHeapSnapshot = function (snapshotFilePath) {
+ return this._worker.performTask("deleteHeapSnapshot", { snapshotFilePath });
+};
+
+/**
+ * Request the creation time given a snapshot file path. Returns `null`
+ * if snapshot does not exist.
+ *
+ * @param {String} snapshotFilePath
+ * The path to the snapshot.
+ * @return {Number?}
+ * The unix timestamp of the creation time of the snapshot, or null if
+ * snapshot does not exist.
+ */
+HeapAnalysesClient.prototype.getCreationTime = function (snapshotFilePath) {
+ return this._worker.performTask("getCreationTime", snapshotFilePath);
+};
+
+/** * Censuses *****************************************************************/
+
+/**
+ * Ask the worker to perform a census analysis on the heap snapshot with the
+ * given path. The heap snapshot at the given path must have already been read
+ * into memory by the worker (see `readHeapSnapshot`).
+ *
+ * @param {String} snapshotFilePath
+ *
+ * @param {Object} censusOptions
+ * A structured-cloneable object specifying the requested census's
+ * breakdown. See the "takeCensus" section of
+ * `js/src/doc/Debugger/Debugger.Memory.md` for detailed documentation.
+ *
+ * @param {Object} requestOptions
+ * An object specifying options of this worker request.
+ * - {Boolean} asTreeNode
+ * Whether or not the census is returned as a CensusTreeNode,
+ * or just a breakdown report. Defaults to false.
+ * @see `devtools/shared/heapsnapshot/census-tree-node.js`
+ * - {Boolean} asInvertedTreeNode
+ * Whether or not the census is returned as an inverted
+ * CensusTreeNode. Defaults to false.
+ * - {String} filter
+ * A filter string to prune the resulting tree with. Only applies if
+ * either asTreeNode or asInvertedTreeNode is true.
+ *
+ * @returns Promise<Object>
+ * An object with the following properties:
+ * - report:
+ * The report generated by the given census breakdown, or a
+ * CensusTreeNode generated by the given census breakdown if
+ * `asTreeNode` is true.
+ * - parentMap:
+ * The result of calling CensusUtils.createParentMap on the generated
+ * report. Only exists if asTreeNode or asInvertedTreeNode are set.
+ */
+HeapAnalysesClient.prototype.takeCensus = function (snapshotFilePath,
+ censusOptions,
+ requestOptions = {}) {
+ return this._worker.performTask("takeCensus", {
+ snapshotFilePath,
+ censusOptions,
+ requestOptions,
+ });
+};
+
+/**
+ * Get the individual nodes that correspond to the given census report leaf
+ * indices.
+ *
+ * @param {Object} opts
+ * An object with the following properties:
+ * - {DominatorTreeId} dominatorTreeId: The id of the dominator tree.
+ * - {Set<Number>} indices: The indices of the census report leaves we
+ * would like to get the individuals for.
+ * - {Object} censusBreakdown: The breakdown used to generate the census.
+ * - {Object} labelBreakdown: The breakdown we would like to use when
+ * labeling the resulting nodes.
+ * - {Number} maxRetainingPaths: The maximum number of retaining paths to
+ * compute for each node.
+ * - {Number} maxIndividuals: The maximum number of individual nodes to
+ * return.
+ *
+ * @returns {Promise<Object>}
+ * A promise of an object with the following properties:
+ * - {Array<DominatorTreeNode>} nodes: An array of `DominatorTreeNode`s
+ * with their shortest paths attached, and without any dominator tree
+ * child/parent information attached. The results are sorted by
+ * retained size.
+ *
+ */
+HeapAnalysesClient.prototype.getCensusIndividuals = function (opts) {
+ return this._worker.performTask("getCensusIndividuals", opts);
+};
+
+/**
+ * Request that the worker take a census on the heap snapshots with the given
+ * paths and then return the difference between them. Both heap snapshots must
+ * have already been read into memory by the worker (see `readHeapSnapshot`).
+ *
+ * @param {String} firstSnapshotFilePath
+ * The first snapshot file path.
+ *
+ * @param {String} secondSnapshotFilePath
+ * The second snapshot file path.
+ *
+ * @param {Object} censusOptions
+ * A structured-cloneable object specifying the requested census's
+ * breakdown. See the "takeCensus" section of
+ * `js/src/doc/Debugger/Debugger.Memory.md` for detailed documentation.
+ *
+ * @param {Object} requestOptions
+ * An object specifying options for this request.
+ * - {Boolean} asTreeNode
+ * Whether the resulting delta report should be converted to a census
+ * tree node before returned. Defaults to false.
+ * - {Boolean} asInvertedTreeNode
+ * Whether or not the census is returned as an inverted
+ * CensusTreeNode. Defaults to false.
+ * - {String} filter
+ * A filter string to prune the resulting tree with. Only applies if
+ * either asTreeNode or asInvertedTreeNode is true.
+ *
+ * @returns Promise<Object>
+ * - delta:
+ * The delta report generated by diffing the two census reports, or a
+ * CensusTreeNode generated from the delta report if
+ * `requestOptions.asTreeNode` was true.
+ * - parentMap:
+ * The result of calling CensusUtils.createParentMap on the generated
+ * delta. Only exists if asTreeNode or asInvertedTreeNode are set.
+ */
+HeapAnalysesClient.prototype.takeCensusDiff = function (firstSnapshotFilePath,
+ secondSnapshotFilePath,
+ censusOptions,
+ requestOptions = {}) {
+ return this._worker.performTask("takeCensusDiff", {
+ firstSnapshotFilePath,
+ secondSnapshotFilePath,
+ censusOptions,
+ requestOptions
+ });
+};
+
+/** * Dominator Trees **********************************************************/
+
+/**
+ * Compute the dominator tree of the heap snapshot loaded from the given file
+ * path. Returns the id of the computed dominator tree.
+ *
+ * @param {String} snapshotFilePath
+ *
+ * @returns {Promise<DominatorTreeId>}
+ */
+HeapAnalysesClient.prototype.computeDominatorTree = function (snapshotFilePath) {
+ return this._worker.performTask("computeDominatorTree", snapshotFilePath);
+};
+
+/**
+ * Get the initial, partial view of the dominator tree with the given id.
+ *
+ * @param {Object} opts
+ * An object specifying options for this request.
+ * - {DominatorTreeId} dominatorTreeId
+ * The id of the dominator tree.
+ * - {Object} breakdown
+ * The breakdown used to generate node labels.
+ * - {Number} maxDepth
+ * The maximum depth to traverse down the tree to create this initial
+ * view.
+ * - {Number} maxSiblings
+ * The maximum number of siblings to visit within each traversed node's
+ * children.
+ * - {Number} maxRetainingPaths
+ * The maximum number of retaining paths to find for each node.
+ *
+ * @returns {Promise<DominatorTreeNode>}
+ */
+HeapAnalysesClient.prototype.getDominatorTree = function (opts) {
+ return this._worker.performTask("getDominatorTree", opts);
+};
+
+/**
+ * Get a subset of a nodes children in the dominator tree.
+ *
+ * @param {Object} opts
+ * An object specifying options for this request.
+ * - {DominatorTreeId} dominatorTreeId
+ * The id of the dominator tree.
+ * - {NodeId} nodeId
+ * The id of the node whose children are being found.
+ * - {Object} breakdown
+ * The breakdown used to generate node labels.
+ * - {Number} startIndex
+ * The starting index within the full set of immediately dominated
+ * children of the children being requested. Children are always sorted
+ * by greatest to least retained size.
+ * - {Number} maxCount
+ * The maximum number of children to return.
+ * - {Number} maxRetainingPaths
+ * The maximum number of retaining paths to find for each node.
+ *
+ * @returns {Promise<Object>}
+ * A promise of an object with the following properties:
+ * - {Array<DominatorTreeNode>} nodes
+ * The requested nodes that are immediately dominated by the node
+ * identified by `opts.nodeId`.
+ * - {Boolean} moreChildrenAvailable
+ * True iff there are more children available after the returned
+ * nodes.
+ * - {Array<NodeId>} path
+ * The path through the tree from the root to these node's parent, eg
+ * [root's id, child of root's id, child of child of root's id, ..., `nodeId`].
+ */
+HeapAnalysesClient.prototype.getImmediatelyDominated = function (opts) {
+ return this._worker.performTask("getImmediatelyDominated", opts);
+};
diff --git a/devtools/shared/heapsnapshot/HeapAnalysesWorker.js b/devtools/shared/heapsnapshot/HeapAnalysesWorker.js
new file mode 100644
index 000000000..d07d67f80
--- /dev/null
+++ b/devtools/shared/heapsnapshot/HeapAnalysesWorker.js
@@ -0,0 +1,303 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* global ThreadSafeChromeUtils*/
+
+// This is a worker which reads offline heap snapshots into memory and performs
+// heavyweight analyses on them without blocking the main thread. A
+// HeapAnalysesWorker is owned and communicated with by a HeapAnalysesClient
+// instance. See HeapAnalysesClient.js.
+
+"use strict";
+
+importScripts("resource://gre/modules/workers/require.js");
+importScripts("resource://devtools/shared/worker/helper.js");
+const { censusReportToCensusTreeNode } = require("resource://devtools/shared/heapsnapshot/census-tree-node.js");
+const DominatorTreeNode = require("resource://devtools/shared/heapsnapshot/DominatorTreeNode.js");
+const CensusUtils = require("resource://devtools/shared/heapsnapshot/CensusUtils.js");
+
+const DEFAULT_START_INDEX = 0;
+const DEFAULT_MAX_COUNT = 50;
+
+/**
+ * The set of HeapSnapshot instances this worker has read into memory. Keyed by
+ * snapshot file path.
+ */
+const snapshots = Object.create(null);
+
+/**
+ * The set of `DominatorTree`s that have been computed, mapped by their id (aka
+ * the index into this array).
+ *
+ * @see /dom/webidl/DominatorTree.webidl
+ */
+const dominatorTrees = [];
+
+/**
+ * The i^th HeapSnapshot in this array is the snapshot used to generate the i^th
+ * dominator tree in `dominatorTrees` above. This lets us map from a dominator
+ * tree id to the snapshot it came from.
+ */
+const dominatorTreeSnapshots = [];
+
+/**
+ * @see HeapAnalysesClient.prototype.readHeapSnapshot
+ */
+workerHelper.createTask(self, "readHeapSnapshot", ({ snapshotFilePath }) => {
+ snapshots[snapshotFilePath] =
+ ThreadSafeChromeUtils.readHeapSnapshot(snapshotFilePath);
+ return true;
+});
+
+/**
+ * @see HeapAnalysesClient.prototype.deleteHeapSnapshot
+ */
+workerHelper.createTask(self, "deleteHeapSnapshot", ({ snapshotFilePath }) => {
+ let snapshot = snapshots[snapshotFilePath];
+ if (!snapshot) {
+ throw new Error(`No known heap snapshot for '${snapshotFilePath}'`);
+ }
+
+ snapshots[snapshotFilePath] = undefined;
+
+ let dominatorTreeId = dominatorTreeSnapshots.indexOf(snapshot);
+ if (dominatorTreeId != -1) {
+ dominatorTreeSnapshots[dominatorTreeId] = undefined;
+ dominatorTrees[dominatorTreeId] = undefined;
+ }
+});
+
+/**
+ * @see HeapAnalysesClient.prototype.takeCensus
+ */
+workerHelper.createTask(self, "takeCensus", ({ snapshotFilePath, censusOptions, requestOptions }) => {
+ if (!snapshots[snapshotFilePath]) {
+ throw new Error(`No known heap snapshot for '${snapshotFilePath}'`);
+ }
+
+ let report = snapshots[snapshotFilePath].takeCensus(censusOptions);
+ let parentMap;
+
+ if (requestOptions.asTreeNode || requestOptions.asInvertedTreeNode) {
+ const opts = { filter: requestOptions.filter || null };
+ if (requestOptions.asInvertedTreeNode) {
+ opts.invert = true;
+ }
+ report = censusReportToCensusTreeNode(censusOptions.breakdown, report, opts);
+ parentMap = CensusUtils.createParentMap(report);
+ }
+
+ return { report, parentMap };
+});
+
+/**
+ * @see HeapAnalysesClient.prototype.getCensusIndividuals
+ */
+workerHelper.createTask(self, "getCensusIndividuals", request => {
+ const {
+ dominatorTreeId,
+ indices,
+ censusBreakdown,
+ labelBreakdown,
+ maxRetainingPaths,
+ maxIndividuals,
+ } = request;
+
+ const dominatorTree = dominatorTrees[dominatorTreeId];
+ if (!dominatorTree) {
+ throw new Error(
+ `There does not exist a DominatorTree with the id ${dominatorTreeId}`);
+ }
+
+ const snapshot = dominatorTreeSnapshots[dominatorTreeId];
+ const nodeIds = CensusUtils.getCensusIndividuals(indices, censusBreakdown, snapshot);
+
+ const nodes = nodeIds
+ .sort((a, b) => dominatorTree.getRetainedSize(b) - dominatorTree.getRetainedSize(a))
+ .slice(0, maxIndividuals)
+ .map(id => {
+ const { label, shallowSize } =
+ DominatorTreeNode.getLabelAndShallowSize(id, snapshot, labelBreakdown);
+ const retainedSize = dominatorTree.getRetainedSize(id);
+ const node = new DominatorTreeNode(id, label, shallowSize, retainedSize);
+ node.moreChildrenAvailable = false;
+ return node;
+ });
+
+ DominatorTreeNode.attachShortestPaths(snapshot,
+ labelBreakdown,
+ dominatorTree.root,
+ nodes,
+ maxRetainingPaths);
+
+ return { nodes };
+});
+
+/**
+ * @see HeapAnalysesClient.prototype.takeCensusDiff
+ */
+workerHelper.createTask(self, "takeCensusDiff", request => {
+ const {
+ firstSnapshotFilePath,
+ secondSnapshotFilePath,
+ censusOptions,
+ requestOptions
+ } = request;
+
+ if (!snapshots[firstSnapshotFilePath]) {
+ throw new Error(`No known heap snapshot for '${firstSnapshotFilePath}'`);
+ }
+
+ if (!snapshots[secondSnapshotFilePath]) {
+ throw new Error(`No known heap snapshot for '${secondSnapshotFilePath}'`);
+ }
+
+ const first = snapshots[firstSnapshotFilePath].takeCensus(censusOptions);
+ const second = snapshots[secondSnapshotFilePath].takeCensus(censusOptions);
+ let delta = CensusUtils.diff(censusOptions.breakdown, first, second);
+ let parentMap;
+
+ if (requestOptions.asTreeNode || requestOptions.asInvertedTreeNode) {
+ const opts = { filter: requestOptions.filter || null };
+ if (requestOptions.asInvertedTreeNode) {
+ opts.invert = true;
+ }
+ delta = censusReportToCensusTreeNode(censusOptions.breakdown, delta, opts);
+ parentMap = CensusUtils.createParentMap(delta);
+ }
+
+ return { delta, parentMap };
+});
+
+/**
+ * @see HeapAnalysesClient.prototype.getCreationTime
+ */
+workerHelper.createTask(self, "getCreationTime", snapshotFilePath => {
+ if (!snapshots[snapshotFilePath]) {
+ throw new Error(`No known heap snapshot for '${snapshotFilePath}'`);
+ }
+ return snapshots[snapshotFilePath].creationTime;
+});
+
+/**
+ * @see HeapAnalysesClient.prototype.computeDominatorTree
+ */
+workerHelper.createTask(self, "computeDominatorTree", snapshotFilePath => {
+ const snapshot = snapshots[snapshotFilePath];
+ if (!snapshot) {
+ throw new Error(`No known heap snapshot for '${snapshotFilePath}'`);
+ }
+
+ const id = dominatorTrees.length;
+ dominatorTrees.push(snapshot.computeDominatorTree());
+ dominatorTreeSnapshots.push(snapshot);
+ return id;
+});
+
+/**
+ * @see HeapAnalysesClient.prototype.getDominatorTree
+ */
+workerHelper.createTask(self, "getDominatorTree", request => {
+ const {
+ dominatorTreeId,
+ breakdown,
+ maxDepth,
+ maxSiblings,
+ maxRetainingPaths,
+ } = request;
+
+ if (!(0 <= dominatorTreeId && dominatorTreeId < dominatorTrees.length)) {
+ throw new Error(
+ `There does not exist a DominatorTree with the id ${dominatorTreeId}`);
+ }
+
+ const dominatorTree = dominatorTrees[dominatorTreeId];
+ const snapshot = dominatorTreeSnapshots[dominatorTreeId];
+
+ const tree = DominatorTreeNode.partialTraversal(dominatorTree,
+ snapshot,
+ breakdown,
+ maxDepth,
+ maxSiblings);
+
+ const nodes = [];
+ (function getNodes(node) {
+ nodes.push(node);
+ if (node.children) {
+ for (let i = 0, length = node.children.length; i < length; i++) {
+ getNodes(node.children[i]);
+ }
+ }
+ }(tree));
+
+ DominatorTreeNode.attachShortestPaths(snapshot,
+ breakdown,
+ dominatorTree.root,
+ nodes,
+ maxRetainingPaths);
+
+ return tree;
+});
+
+/**
+ * @see HeapAnalysesClient.prototype.getImmediatelyDominated
+ */
+workerHelper.createTask(self, "getImmediatelyDominated", request => {
+ const {
+ dominatorTreeId,
+ nodeId,
+ breakdown,
+ startIndex,
+ maxCount,
+ maxRetainingPaths,
+ } = request;
+
+ if (!(0 <= dominatorTreeId && dominatorTreeId < dominatorTrees.length)) {
+ throw new Error(
+ `There does not exist a DominatorTree with the id ${dominatorTreeId}`);
+ }
+
+ const dominatorTree = dominatorTrees[dominatorTreeId];
+ const snapshot = dominatorTreeSnapshots[dominatorTreeId];
+
+ const childIds = dominatorTree.getImmediatelyDominated(nodeId);
+ if (!childIds) {
+ throw new Error(`${nodeId} is not a node id in the dominator tree`);
+ }
+
+ const start = startIndex || DEFAULT_START_INDEX;
+ const count = maxCount || DEFAULT_MAX_COUNT;
+ const end = start + count;
+
+ const nodes = childIds
+ .slice(start, end)
+ .map(id => {
+ const { label, shallowSize } =
+ DominatorTreeNode.getLabelAndShallowSize(id, snapshot, breakdown);
+ const retainedSize = dominatorTree.getRetainedSize(id);
+ const node = new DominatorTreeNode(id, label, shallowSize, retainedSize);
+ node.parentId = nodeId;
+ // DominatorTree.getImmediatelyDominated will always return non-null here
+ // because we got the id directly from the dominator tree.
+ node.moreChildrenAvailable = dominatorTree.getImmediatelyDominated(id).length > 0;
+ return node;
+ });
+
+ const path = [];
+ let id = nodeId;
+ do {
+ path.push(id);
+ id = dominatorTree.getImmediateDominator(id);
+ } while (id !== null);
+ path.reverse();
+
+ const moreChildrenAvailable = childIds.length > end;
+
+ DominatorTreeNode.attachShortestPaths(snapshot,
+ breakdown,
+ dominatorTree.root,
+ nodes,
+ maxRetainingPaths);
+
+ return { nodes, moreChildrenAvailable, path };
+});
diff --git a/devtools/shared/heapsnapshot/HeapSnapshot.cpp b/devtools/shared/heapsnapshot/HeapSnapshot.cpp
new file mode 100644
index 000000000..17f43f34e
--- /dev/null
+++ b/devtools/shared/heapsnapshot/HeapSnapshot.cpp
@@ -0,0 +1,1652 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HeapSnapshot.h"
+
+#include <google/protobuf/io/coded_stream.h>
+#include <google/protobuf/io/gzip_stream.h>
+#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
+
+#include "js/Debug.h"
+#include "js/TypeDecls.h"
+#include "js/UbiNodeBreadthFirst.h"
+#include "js/UbiNodeCensus.h"
+#include "js/UbiNodeDominatorTree.h"
+#include "js/UbiNodeShortestPaths.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/CycleCollectedJSContext.h"
+#include "mozilla/devtools/AutoMemMap.h"
+#include "mozilla/devtools/CoreDump.pb.h"
+#include "mozilla/devtools/DeserializedNode.h"
+#include "mozilla/devtools/DominatorTree.h"
+#include "mozilla/devtools/FileDescriptorOutputStream.h"
+#include "mozilla/devtools/HeapSnapshotTempFileHelperChild.h"
+#include "mozilla/devtools/ZeroCopyNSIOutputStream.h"
+#include "mozilla/dom/ChromeUtils.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/HeapSnapshotBinding.h"
+#include "mozilla/RangedPtr.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/Unused.h"
+
+#include "jsapi.h"
+#include "jsfriendapi.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsCRTGlue.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIFile.h"
+#include "nsIOutputStream.h"
+#include "nsISupportsImpl.h"
+#include "nsNetUtil.h"
+#include "nsPrintfCString.h"
+#include "prerror.h"
+#include "prio.h"
+#include "prtypes.h"
+
+namespace mozilla {
+namespace devtools {
+
+using namespace JS;
+using namespace dom;
+
+using ::google::protobuf::io::ArrayInputStream;
+using ::google::protobuf::io::CodedInputStream;
+using ::google::protobuf::io::GzipInputStream;
+using ::google::protobuf::io::ZeroCopyInputStream;
+
+using JS::ubi::AtomOrTwoByteChars;
+using JS::ubi::ShortestPaths;
+
+MallocSizeOf
+GetCurrentThreadDebuggerMallocSizeOf()
+{
+ auto ccjscx = CycleCollectedJSContext::Get();
+ MOZ_ASSERT(ccjscx);
+ auto cx = ccjscx->Context();
+ MOZ_ASSERT(cx);
+ auto mallocSizeOf = JS::dbg::GetDebuggerMallocSizeOf(cx);
+ MOZ_ASSERT(mallocSizeOf);
+ return mallocSizeOf;
+}
+
+/*** Cycle Collection Boilerplate *****************************************************************/
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(HeapSnapshot, mParent)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(HeapSnapshot)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(HeapSnapshot)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(HeapSnapshot)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+/* virtual */ JSObject*
+HeapSnapshot::WrapObject(JSContext* aCx, HandleObject aGivenProto)
+{
+ return HeapSnapshotBinding::Wrap(aCx, this, aGivenProto);
+}
+
+/*** Reading Heap Snapshots ***********************************************************************/
+
+/* static */ already_AddRefed<HeapSnapshot>
+HeapSnapshot::Create(JSContext* cx,
+ GlobalObject& global,
+ const uint8_t* buffer,
+ uint32_t size,
+ ErrorResult& rv)
+{
+ RefPtr<HeapSnapshot> snapshot = new HeapSnapshot(cx, global.GetAsSupports());
+ if (!snapshot->init(cx, buffer, size)) {
+ rv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+ return snapshot.forget();
+}
+
+template<typename MessageType>
+static bool
+parseMessage(ZeroCopyInputStream& stream, uint32_t sizeOfMessage, MessageType& message)
+{
+ // We need to create a new `CodedInputStream` for each message so that the
+ // 64MB limit is applied per-message rather than to the whole stream.
+ CodedInputStream codedStream(&stream);
+
+ // The protobuf message nesting that core dumps exhibit is dominated by
+ // allocation stacks' frames. In the most deeply nested case, each frame has
+ // two messages: a StackFrame message and a StackFrame::Data message. These
+ // frames are on top of a small constant of other messages. There are a
+ // MAX_STACK_DEPTH number of frames, so we multiply this by 3 to make room for
+ // the two messages per frame plus some head room for the constant number of
+ // non-dominating messages.
+ codedStream.SetRecursionLimit(HeapSnapshot::MAX_STACK_DEPTH * 3);
+
+ auto limit = codedStream.PushLimit(sizeOfMessage);
+ if (NS_WARN_IF(!message.ParseFromCodedStream(&codedStream)) ||
+ NS_WARN_IF(!codedStream.ConsumedEntireMessage()) ||
+ NS_WARN_IF(codedStream.BytesUntilLimit() != 0))
+ {
+ return false;
+ }
+
+ codedStream.PopLimit(limit);
+ return true;
+}
+
+template<typename CharT, typename InternedStringSet>
+struct GetOrInternStringMatcher
+{
+ InternedStringSet& internedStrings;
+
+ explicit GetOrInternStringMatcher(InternedStringSet& strings) : internedStrings(strings) { }
+
+ const CharT* match(const std::string* str) {
+ MOZ_ASSERT(str);
+ size_t length = str->length() / sizeof(CharT);
+ auto tempString = reinterpret_cast<const CharT*>(str->data());
+
+ UniquePtr<CharT[], NSFreePolicy> owned(NS_strndup(tempString, length));
+ if (!owned || !internedStrings.append(Move(owned)))
+ return nullptr;
+
+ return internedStrings.back().get();
+ }
+
+ const CharT* match(uint64_t ref) {
+ if (MOZ_LIKELY(ref < internedStrings.length())) {
+ auto& string = internedStrings[ref];
+ MOZ_ASSERT(string);
+ return string.get();
+ }
+
+ return nullptr;
+ }
+};
+
+template<
+ // Either char or char16_t.
+ typename CharT,
+ // A reference to either `internedOneByteStrings` or `internedTwoByteStrings`
+ // if CharT is char or char16_t respectively.
+ typename InternedStringSet>
+const CharT*
+HeapSnapshot::getOrInternString(InternedStringSet& internedStrings,
+ Maybe<StringOrRef>& maybeStrOrRef)
+{
+ // Incomplete message: has neither a string nor a reference to an already
+ // interned string.
+ if (MOZ_UNLIKELY(maybeStrOrRef.isNothing()))
+ return nullptr;
+
+ GetOrInternStringMatcher<CharT, InternedStringSet> m(internedStrings);
+ return maybeStrOrRef->match(m);
+}
+
+// Get a de-duplicated string as a Maybe<StringOrRef> from the given `msg`.
+#define GET_STRING_OR_REF_WITH_PROP_NAMES(msg, strPropertyName, refPropertyName) \
+ (msg.has_##refPropertyName() \
+ ? Some(StringOrRef(msg.refPropertyName())) \
+ : msg.has_##strPropertyName() \
+ ? Some(StringOrRef(&msg.strPropertyName())) \
+ : Nothing())
+
+#define GET_STRING_OR_REF(msg, property) \
+ (msg.has_##property##ref() \
+ ? Some(StringOrRef(msg.property##ref())) \
+ : msg.has_##property() \
+ ? Some(StringOrRef(&msg.property())) \
+ : Nothing())
+
+bool
+HeapSnapshot::saveNode(const protobuf::Node& node, NodeIdSet& edgeReferents)
+{
+ // NB: de-duplicated string properties must be read back and interned in the
+ // same order here as they are written and serialized in
+ // `CoreDumpWriter::writeNode` or else indices in references to already
+ // serialized strings will be off.
+
+ if (NS_WARN_IF(!node.has_id()))
+ return false;
+ NodeId id = node.id();
+
+ // NodeIds are derived from pointers (at most 48 bits) and we rely on them
+ // fitting into JS numbers (IEEE 754 doubles, can precisely store 53 bit
+ // integers) despite storing them on disk as 64 bit integers.
+ if (NS_WARN_IF(!JS::Value::isNumberRepresentable(id)))
+ return false;
+
+ // Should only deserialize each node once.
+ if (NS_WARN_IF(nodes.has(id)))
+ return false;
+
+ if (NS_WARN_IF(!JS::ubi::Uint32IsValidCoarseType(node.coarsetype())))
+ return false;
+ auto coarseType = JS::ubi::Uint32ToCoarseType(node.coarsetype());
+
+ Maybe<StringOrRef> typeNameOrRef = GET_STRING_OR_REF_WITH_PROP_NAMES(node, typename_, typenameref);
+ auto typeName = getOrInternString<char16_t>(internedTwoByteStrings, typeNameOrRef);
+ if (NS_WARN_IF(!typeName))
+ return false;
+
+ if (NS_WARN_IF(!node.has_size()))
+ return false;
+ uint64_t size = node.size();
+
+ auto edgesLength = node.edges_size();
+ DeserializedNode::EdgeVector edges;
+ if (NS_WARN_IF(!edges.reserve(edgesLength)))
+ return false;
+ for (decltype(edgesLength) i = 0; i < edgesLength; i++) {
+ auto& protoEdge = node.edges(i);
+
+ if (NS_WARN_IF(!protoEdge.has_referent()))
+ return false;
+ NodeId referent = protoEdge.referent();
+
+ if (NS_WARN_IF(!edgeReferents.put(referent)))
+ return false;
+
+ const char16_t* edgeName = nullptr;
+ if (protoEdge.EdgeNameOrRef_case() != protobuf::Edge::EDGENAMEORREF_NOT_SET) {
+ Maybe<StringOrRef> edgeNameOrRef = GET_STRING_OR_REF(protoEdge, name);
+ edgeName = getOrInternString<char16_t>(internedTwoByteStrings, edgeNameOrRef);
+ if (NS_WARN_IF(!edgeName))
+ return false;
+ }
+
+ edges.infallibleAppend(DeserializedEdge(referent, edgeName));
+ }
+
+ Maybe<StackFrameId> allocationStack;
+ if (node.has_allocationstack()) {
+ StackFrameId id = 0;
+ if (NS_WARN_IF(!saveStackFrame(node.allocationstack(), id)))
+ return false;
+ allocationStack.emplace(id);
+ }
+ MOZ_ASSERT(allocationStack.isSome() == node.has_allocationstack());
+
+ const char* jsObjectClassName = nullptr;
+ if (node.JSObjectClassNameOrRef_case() != protobuf::Node::JSOBJECTCLASSNAMEORREF_NOT_SET) {
+ Maybe<StringOrRef> clsNameOrRef = GET_STRING_OR_REF(node, jsobjectclassname);
+ jsObjectClassName = getOrInternString<char>(internedOneByteStrings, clsNameOrRef);
+ if (NS_WARN_IF(!jsObjectClassName))
+ return false;
+ }
+
+ const char* scriptFilename = nullptr;
+ if (node.ScriptFilenameOrRef_case() != protobuf::Node::SCRIPTFILENAMEORREF_NOT_SET) {
+ Maybe<StringOrRef> scriptFilenameOrRef = GET_STRING_OR_REF(node, scriptfilename);
+ scriptFilename = getOrInternString<char>(internedOneByteStrings, scriptFilenameOrRef);
+ if (NS_WARN_IF(!scriptFilename))
+ return false;
+ }
+
+ if (NS_WARN_IF(!nodes.putNew(id, DeserializedNode(id, coarseType, typeName,
+ size, Move(edges),
+ allocationStack,
+ jsObjectClassName,
+ scriptFilename, *this))))
+ {
+ return false;
+ };
+
+ return true;
+}
+
+bool
+HeapSnapshot::saveStackFrame(const protobuf::StackFrame& frame,
+ StackFrameId& outFrameId)
+{
+ // NB: de-duplicated string properties must be read in the same order here as
+ // they are written in `CoreDumpWriter::getProtobufStackFrame` or else indices
+ // in references to already serialized strings will be off.
+
+ if (frame.has_ref()) {
+ // We should only get a reference to the previous frame if we have already
+ // seen the previous frame.
+ if (!frames.has(frame.ref()))
+ return false;
+
+ outFrameId = frame.ref();
+ return true;
+ }
+
+ // Incomplete message.
+ if (!frame.has_data())
+ return false;
+
+ auto data = frame.data();
+
+ if (!data.has_id())
+ return false;
+ StackFrameId id = data.id();
+
+ // This should be the first and only time we see this frame.
+ if (frames.has(id))
+ return false;
+
+ if (!data.has_line())
+ return false;
+ uint32_t line = data.line();
+
+ if (!data.has_column())
+ return false;
+ uint32_t column = data.column();
+
+ if (!data.has_issystem())
+ return false;
+ bool isSystem = data.issystem();
+
+ if (!data.has_isselfhosted())
+ return false;
+ bool isSelfHosted = data.isselfhosted();
+
+ Maybe<StringOrRef> sourceOrRef = GET_STRING_OR_REF(data, source);
+ auto source = getOrInternString<char16_t>(internedTwoByteStrings, sourceOrRef);
+ if (!source)
+ return false;
+
+ const char16_t* functionDisplayName = nullptr;
+ if (data.FunctionDisplayNameOrRef_case() !=
+ protobuf::StackFrame_Data::FUNCTIONDISPLAYNAMEORREF_NOT_SET)
+ {
+ Maybe<StringOrRef> nameOrRef = GET_STRING_OR_REF(data, functiondisplayname);
+ functionDisplayName = getOrInternString<char16_t>(internedTwoByteStrings, nameOrRef);
+ if (!functionDisplayName)
+ return false;
+ }
+
+ Maybe<StackFrameId> parent;
+ if (data.has_parent()) {
+ StackFrameId parentId = 0;
+ if (!saveStackFrame(data.parent(), parentId))
+ return false;
+ parent = Some(parentId);
+ }
+
+ if (!frames.putNew(id, DeserializedStackFrame(id, parent, line, column,
+ source, functionDisplayName,
+ isSystem, isSelfHosted, *this)))
+ {
+ return false;
+ }
+
+ outFrameId = id;
+ return true;
+}
+
+#undef GET_STRING_OR_REF_WITH_PROP_NAMES
+#undef GET_STRING_OR_REF
+
+// Because protobuf messages aren't self-delimiting, we serialize each message
+// preceded by its size in bytes. When deserializing, we read this size and then
+// limit reading from the stream to the given byte size. If we didn't, then the
+// first message would consume the entire stream.
+static bool
+readSizeOfNextMessage(ZeroCopyInputStream& stream, uint32_t* sizep)
+{
+ MOZ_ASSERT(sizep);
+ CodedInputStream codedStream(&stream);
+ return codedStream.ReadVarint32(sizep) && *sizep > 0;
+}
+
+bool
+HeapSnapshot::init(JSContext* cx, const uint8_t* buffer, uint32_t size)
+{
+ if (!nodes.init() || !frames.init())
+ return false;
+
+ ArrayInputStream stream(buffer, size);
+ GzipInputStream gzipStream(&stream);
+ uint32_t sizeOfMessage = 0;
+
+ // First is the metadata.
+
+ protobuf::Metadata metadata;
+ if (NS_WARN_IF(!readSizeOfNextMessage(gzipStream, &sizeOfMessage)))
+ return false;
+ if (!parseMessage(gzipStream, sizeOfMessage, metadata))
+ return false;
+ if (metadata.has_timestamp())
+ timestamp.emplace(metadata.timestamp());
+
+ // Next is the root node.
+
+ protobuf::Node root;
+ if (NS_WARN_IF(!readSizeOfNextMessage(gzipStream, &sizeOfMessage)))
+ return false;
+ if (!parseMessage(gzipStream, sizeOfMessage, root))
+ return false;
+
+ // Although the id is optional in the protobuf format for future proofing, we
+ // can't currently do anything without it.
+ if (NS_WARN_IF(!root.has_id()))
+ return false;
+ rootId = root.id();
+
+ // The set of all node ids we've found edges pointing to.
+ NodeIdSet edgeReferents(cx);
+ if (NS_WARN_IF(!edgeReferents.init()))
+ return false;
+
+ if (NS_WARN_IF(!saveNode(root, edgeReferents)))
+ return false;
+
+ // Finally, the rest of the nodes in the core dump.
+
+ // Test for the end of the stream. The protobuf library gives no way to tell
+ // the difference between an underlying read error and the stream being
+ // done. All we can do is attempt to read the size of the next message and
+ // extrapolate guestimations from the result of that operation.
+ while (readSizeOfNextMessage(gzipStream, &sizeOfMessage)) {
+ protobuf::Node node;
+ if (!parseMessage(gzipStream, sizeOfMessage, node))
+ return false;
+ if (NS_WARN_IF(!saveNode(node, edgeReferents)))
+ return false;
+ }
+
+ // Check the set of node ids referred to by edges we found and ensure that we
+ // have the node corresponding to each id. If we don't have all of them, it is
+ // unsafe to perform analyses of this heap snapshot.
+ for (auto range = edgeReferents.all(); !range.empty(); range.popFront()) {
+ if (NS_WARN_IF(!nodes.has(range.front())))
+ return false;
+ }
+
+ return true;
+}
+
+
+/*** Heap Snapshot Analyses ***********************************************************************/
+
+void
+HeapSnapshot::TakeCensus(JSContext* cx, JS::HandleObject options,
+ JS::MutableHandleValue rval, ErrorResult& rv)
+{
+ JS::ubi::Census census(cx);
+ if (NS_WARN_IF(!census.init())) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ JS::ubi::CountTypePtr rootType;
+ if (NS_WARN_IF(!JS::ubi::ParseCensusOptions(cx, census, options, rootType))) {
+ rv.Throw(NS_ERROR_UNEXPECTED);
+ return;
+ }
+
+ JS::ubi::RootedCount rootCount(cx, rootType->makeCount());
+ if (NS_WARN_IF(!rootCount)) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ JS::ubi::CensusHandler handler(census, rootCount, GetCurrentThreadDebuggerMallocSizeOf());
+
+ {
+ JS::AutoCheckCannotGC nogc;
+
+ JS::ubi::CensusTraversal traversal(cx, handler, nogc);
+ if (NS_WARN_IF(!traversal.init())) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ if (NS_WARN_IF(!traversal.addStart(getRoot()))) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ if (NS_WARN_IF(!traversal.traverse())) {
+ rv.Throw(NS_ERROR_UNEXPECTED);
+ return;
+ }
+ }
+
+ if (NS_WARN_IF(!handler.report(cx, rval))) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+}
+
+void
+HeapSnapshot::DescribeNode(JSContext* cx, JS::HandleObject breakdown, uint64_t nodeId,
+ JS::MutableHandleValue rval, ErrorResult& rv) {
+ MOZ_ASSERT(breakdown);
+ JS::RootedValue breakdownVal(cx, JS::ObjectValue(*breakdown));
+ JS::ubi::CountTypePtr rootType = JS::ubi::ParseBreakdown(cx, breakdownVal);
+ if (NS_WARN_IF(!rootType)) {
+ rv.Throw(NS_ERROR_UNEXPECTED);
+ return;
+ }
+
+ JS::ubi::RootedCount rootCount(cx, rootType->makeCount());
+ if (NS_WARN_IF(!rootCount)) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ JS::ubi::Node::Id id(nodeId);
+ Maybe<JS::ubi::Node> node = getNodeById(id);
+ if (NS_WARN_IF(node.isNothing())) {
+ rv.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ MallocSizeOf mallocSizeOf = GetCurrentThreadDebuggerMallocSizeOf();
+ if (NS_WARN_IF(!rootCount->count(mallocSizeOf, *node))) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ if (NS_WARN_IF(!rootCount->report(cx, rval))) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+}
+
+
+already_AddRefed<DominatorTree>
+HeapSnapshot::ComputeDominatorTree(ErrorResult& rv)
+{
+ Maybe<JS::ubi::DominatorTree> maybeTree;
+ {
+ auto ccjscx = CycleCollectedJSContext::Get();
+ MOZ_ASSERT(ccjscx);
+ auto cx = ccjscx->Context();
+ MOZ_ASSERT(cx);
+ JS::AutoCheckCannotGC nogc(cx);
+ maybeTree = JS::ubi::DominatorTree::Create(cx, nogc, getRoot());
+ }
+
+ if (NS_WARN_IF(maybeTree.isNothing())) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return nullptr;
+ }
+
+ return MakeAndAddRef<DominatorTree>(Move(*maybeTree), this, mParent);
+}
+
+void
+HeapSnapshot::ComputeShortestPaths(JSContext*cx, uint64_t start,
+ const Sequence<uint64_t>& targets,
+ uint64_t maxNumPaths,
+ JS::MutableHandleObject results,
+ ErrorResult& rv)
+{
+ // First ensure that our inputs are valid.
+
+ if (NS_WARN_IF(maxNumPaths == 0)) {
+ rv.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ Maybe<JS::ubi::Node> startNode = getNodeById(start);
+ if (NS_WARN_IF(startNode.isNothing())) {
+ rv.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ if (NS_WARN_IF(targets.Length() == 0)) {
+ rv.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ // Aggregate the targets into a set and make sure that they exist in the heap
+ // snapshot.
+
+ JS::ubi::NodeSet targetsSet;
+ if (NS_WARN_IF(!targetsSet.init())) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ for (const auto& target : targets) {
+ Maybe<JS::ubi::Node> targetNode = getNodeById(target);
+ if (NS_WARN_IF(targetNode.isNothing())) {
+ rv.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ if (NS_WARN_IF(!targetsSet.put(*targetNode))) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+ }
+
+ // Walk the heap graph and find the shortest paths.
+
+ Maybe<ShortestPaths> maybeShortestPaths;
+ {
+ JS::AutoCheckCannotGC nogc(cx);
+ maybeShortestPaths = ShortestPaths::Create(cx, nogc, maxNumPaths, *startNode,
+ Move(targetsSet));
+ }
+
+ if (NS_WARN_IF(maybeShortestPaths.isNothing())) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ auto& shortestPaths = *maybeShortestPaths;
+
+ // Convert the results into a Map object mapping target node IDs to arrays of
+ // paths found.
+
+ RootedObject resultsMap(cx, JS::NewMapObject(cx));
+ if (NS_WARN_IF(!resultsMap)) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ for (auto range = shortestPaths.eachTarget(); !range.empty(); range.popFront()) {
+ JS::RootedValue key(cx, JS::NumberValue(range.front().identifier()));
+ JS::AutoValueVector paths(cx);
+
+ bool ok = shortestPaths.forEachPath(range.front(), [&](JS::ubi::Path& path) {
+ JS::AutoValueVector pathValues(cx);
+
+ for (JS::ubi::BackEdge* edge : path) {
+ JS::RootedObject pathPart(cx, JS_NewPlainObject(cx));
+ if (!pathPart) {
+ return false;
+ }
+
+ JS::RootedValue predecessor(cx, NumberValue(edge->predecessor().identifier()));
+ if (!JS_DefineProperty(cx, pathPart, "predecessor", predecessor, JSPROP_ENUMERATE)) {
+ return false;
+ }
+
+ RootedValue edgeNameVal(cx, NullValue());
+ if (edge->name()) {
+ RootedString edgeName(cx, JS_AtomizeUCString(cx, edge->name().get()));
+ if (!edgeName) {
+ return false;
+ }
+ edgeNameVal = StringValue(edgeName);
+ }
+
+ if (!JS_DefineProperty(cx, pathPart, "edge", edgeNameVal, JSPROP_ENUMERATE)) {
+ return false;
+ }
+
+ if (!pathValues.append(ObjectValue(*pathPart))) {
+ return false;
+ }
+ }
+
+ RootedObject pathObj(cx, JS_NewArrayObject(cx, pathValues));
+ return pathObj && paths.append(ObjectValue(*pathObj));
+ });
+
+ if (NS_WARN_IF(!ok)) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ JS::RootedObject pathsArray(cx, JS_NewArrayObject(cx, paths));
+ if (NS_WARN_IF(!pathsArray)) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ JS::RootedValue pathsVal(cx, ObjectValue(*pathsArray));
+ if (NS_WARN_IF(!JS::MapSet(cx, resultsMap, key, pathsVal))) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+ }
+
+ results.set(resultsMap);
+}
+
+/*** Saving Heap Snapshots ************************************************************************/
+
+// If we are only taking a snapshot of the heap affected by the given set of
+// globals, find the set of compartments the globals are allocated
+// within. Returns false on OOM failure.
+static bool
+PopulateCompartmentsWithGlobals(CompartmentSet& compartments, AutoObjectVector& globals)
+{
+ if (!compartments.init())
+ return false;
+
+ unsigned length = globals.length();
+ for (unsigned i = 0; i < length; i++) {
+ if (!compartments.put(GetObjectCompartment(globals[i])))
+ return false;
+ }
+
+ return true;
+}
+
+// Add the given set of globals as explicit roots in the given roots
+// list. Returns false on OOM failure.
+static bool
+AddGlobalsAsRoots(AutoObjectVector& globals, ubi::RootList& roots)
+{
+ unsigned length = globals.length();
+ for (unsigned i = 0; i < length; i++) {
+ if (!roots.addRoot(ubi::Node(globals[i].get()),
+ u"heap snapshot global"))
+ {
+ return false;
+ }
+ }
+ return true;
+}
+
+// Choose roots and limits for a traversal, given `boundaries`. Set `roots` to
+// the set of nodes within the boundaries that are referred to by nodes
+// outside. If `boundaries` does not include all JS compartments, initialize
+// `compartments` to the set of included compartments; otherwise, leave
+// `compartments` uninitialized. (You can use compartments.initialized() to
+// check.)
+//
+// If `boundaries` is incoherent, or we encounter an error while trying to
+// handle it, or we run out of memory, set `rv` appropriately and return
+// `false`.
+static bool
+EstablishBoundaries(JSContext* cx,
+ ErrorResult& rv,
+ const HeapSnapshotBoundaries& boundaries,
+ ubi::RootList& roots,
+ CompartmentSet& compartments)
+{
+ MOZ_ASSERT(!roots.initialized());
+ MOZ_ASSERT(!compartments.initialized());
+
+ bool foundBoundaryProperty = false;
+
+ if (boundaries.mRuntime.WasPassed()) {
+ foundBoundaryProperty = true;
+
+ if (!boundaries.mRuntime.Value()) {
+ rv.Throw(NS_ERROR_INVALID_ARG);
+ return false;
+ }
+
+ if (!roots.init()) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return false;
+ }
+ }
+
+ if (boundaries.mDebugger.WasPassed()) {
+ if (foundBoundaryProperty) {
+ rv.Throw(NS_ERROR_INVALID_ARG);
+ return false;
+ }
+ foundBoundaryProperty = true;
+
+ JSObject* dbgObj = boundaries.mDebugger.Value();
+ if (!dbgObj || !dbg::IsDebugger(*dbgObj)) {
+ rv.Throw(NS_ERROR_INVALID_ARG);
+ return false;
+ }
+
+ AutoObjectVector globals(cx);
+ if (!dbg::GetDebuggeeGlobals(cx, *dbgObj, globals) ||
+ !PopulateCompartmentsWithGlobals(compartments, globals) ||
+ !roots.init(compartments) ||
+ !AddGlobalsAsRoots(globals, roots))
+ {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return false;
+ }
+ }
+
+ if (boundaries.mGlobals.WasPassed()) {
+ if (foundBoundaryProperty) {
+ rv.Throw(NS_ERROR_INVALID_ARG);
+ return false;
+ }
+ foundBoundaryProperty = true;
+
+ uint32_t length = boundaries.mGlobals.Value().Length();
+ if (length == 0) {
+ rv.Throw(NS_ERROR_INVALID_ARG);
+ return false;
+ }
+
+ AutoObjectVector globals(cx);
+ for (uint32_t i = 0; i < length; i++) {
+ JSObject* global = boundaries.mGlobals.Value().ElementAt(i);
+ if (!JS_IsGlobalObject(global)) {
+ rv.Throw(NS_ERROR_INVALID_ARG);
+ return false;
+ }
+ if (!globals.append(global)) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return false;
+ }
+ }
+
+ if (!PopulateCompartmentsWithGlobals(compartments, globals) ||
+ !roots.init(compartments) ||
+ !AddGlobalsAsRoots(globals, roots))
+ {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return false;
+ }
+ }
+
+ if (!foundBoundaryProperty) {
+ rv.Throw(NS_ERROR_INVALID_ARG);
+ return false;
+ }
+
+ MOZ_ASSERT(roots.initialized());
+ MOZ_ASSERT_IF(boundaries.mDebugger.WasPassed(), compartments.initialized());
+ MOZ_ASSERT_IF(boundaries.mGlobals.WasPassed(), compartments.initialized());
+ return true;
+}
+
+
+// A variant covering all the various two-byte strings that we can get from the
+// ubi::Node API.
+class TwoByteString : public Variant<JSAtom*, const char16_t*, JS::ubi::EdgeName>
+{
+ using Base = Variant<JSAtom*, const char16_t*, JS::ubi::EdgeName>;
+
+ struct AsTwoByteStringMatcher
+ {
+ TwoByteString match(JSAtom* atom) {
+ return TwoByteString(atom);
+ }
+
+ TwoByteString match(const char16_t* chars) {
+ return TwoByteString(chars);
+ }
+ };
+
+ struct IsNonNullMatcher
+ {
+ template<typename T>
+ bool match(const T& t) { return t != nullptr; }
+ };
+
+ struct LengthMatcher
+ {
+ size_t match(JSAtom* atom) {
+ MOZ_ASSERT(atom);
+ JS::ubi::AtomOrTwoByteChars s(atom);
+ return s.length();
+ }
+
+ size_t match(const char16_t* chars) {
+ MOZ_ASSERT(chars);
+ return NS_strlen(chars);
+ }
+
+ size_t match(const JS::ubi::EdgeName& ptr) {
+ MOZ_ASSERT(ptr);
+ return NS_strlen(ptr.get());
+ }
+ };
+
+ struct CopyToBufferMatcher
+ {
+ RangedPtr<char16_t> destination;
+ size_t maxLength;
+
+ CopyToBufferMatcher(RangedPtr<char16_t> destination, size_t maxLength)
+ : destination(destination)
+ , maxLength(maxLength)
+ { }
+
+ size_t match(JS::ubi::EdgeName& ptr) {
+ return ptr ? match(ptr.get()) : 0;
+ }
+
+ size_t match(JSAtom* atom) {
+ MOZ_ASSERT(atom);
+ JS::ubi::AtomOrTwoByteChars s(atom);
+ return s.copyToBuffer(destination, maxLength);
+ }
+
+ size_t match(const char16_t* chars) {
+ MOZ_ASSERT(chars);
+ JS::ubi::AtomOrTwoByteChars s(chars);
+ return s.copyToBuffer(destination, maxLength);
+ }
+ };
+
+public:
+ template<typename T>
+ MOZ_IMPLICIT TwoByteString(T&& rhs) : Base(Forward<T>(rhs)) { }
+
+ template<typename T>
+ TwoByteString& operator=(T&& rhs) {
+ MOZ_ASSERT(this != &rhs, "self-move disallowed");
+ this->~TwoByteString();
+ new (this) TwoByteString(Forward<T>(rhs));
+ return *this;
+ }
+
+ TwoByteString(const TwoByteString&) = delete;
+ TwoByteString& operator=(const TwoByteString&) = delete;
+
+ // Rewrap the inner value of a JS::ubi::AtomOrTwoByteChars as a TwoByteString.
+ static TwoByteString from(JS::ubi::AtomOrTwoByteChars&& s) {
+ AsTwoByteStringMatcher m;
+ return s.match(m);
+ }
+
+ // Returns true if the given TwoByteString is non-null, false otherwise.
+ bool isNonNull() const {
+ IsNonNullMatcher m;
+ return match(m);
+ }
+
+ // Return the length of the string, 0 if it is null.
+ size_t length() const {
+ LengthMatcher m;
+ return match(m);
+ }
+
+ // Copy the contents of a TwoByteString into the provided buffer. The buffer
+ // is NOT null terminated. The number of characters written is returned.
+ size_t copyToBuffer(RangedPtr<char16_t> destination, size_t maxLength) {
+ CopyToBufferMatcher m(destination, maxLength);
+ return match(m);
+ }
+
+ struct HashPolicy;
+};
+
+// A hashing policy for TwoByteString.
+//
+// Atoms are pointer hashed and use pointer equality, which means that we
+// tolerate some duplication across atoms and the other two types of two-byte
+// strings. In practice, we expect the amount of this duplication to be very low
+// because each type is generally a different semantic thing in addition to
+// having a slightly different representation. For example, the set of edge
+// names and the set stack frames' source names naturally tend not to overlap
+// very much if at all.
+struct TwoByteString::HashPolicy {
+ using Lookup = TwoByteString;
+
+ struct HashingMatcher {
+ js::HashNumber match(const JSAtom* atom) {
+ return js::DefaultHasher<const JSAtom*>::hash(atom);
+ }
+
+ js::HashNumber match(const char16_t* chars) {
+ MOZ_ASSERT(chars);
+ auto length = NS_strlen(chars);
+ return HashString(chars, length);
+ }
+
+ js::HashNumber match(const JS::ubi::EdgeName& ptr) {
+ MOZ_ASSERT(ptr);
+ return match(ptr.get());
+ }
+ };
+
+ static js::HashNumber hash(const Lookup& l) {
+ HashingMatcher hasher;
+ return l.match(hasher);
+ }
+
+ struct EqualityMatcher {
+ const TwoByteString& rhs;
+ explicit EqualityMatcher(const TwoByteString& rhs) : rhs(rhs) { }
+
+ bool match(const JSAtom* atom) {
+ return rhs.is<JSAtom*>() && rhs.as<JSAtom*>() == atom;
+ }
+
+ bool match(const char16_t* chars) {
+ MOZ_ASSERT(chars);
+
+ const char16_t* rhsChars = nullptr;
+ if (rhs.is<const char16_t*>())
+ rhsChars = rhs.as<const char16_t*>();
+ else if (rhs.is<JS::ubi::EdgeName>())
+ rhsChars = rhs.as<JS::ubi::EdgeName>().get();
+ else
+ return false;
+ MOZ_ASSERT(rhsChars);
+
+ auto length = NS_strlen(chars);
+ if (NS_strlen(rhsChars) != length)
+ return false;
+
+ return memcmp(chars, rhsChars, length * sizeof(char16_t)) == 0;
+ }
+
+ bool match(const JS::ubi::EdgeName& ptr) {
+ MOZ_ASSERT(ptr);
+ return match(ptr.get());
+ }
+ };
+
+ static bool match(const TwoByteString& k, const Lookup& l) {
+ EqualityMatcher eq(l);
+ return k.match(eq);
+ }
+
+ static void rekey(TwoByteString& k, TwoByteString&& newKey) {
+ k = Move(newKey);
+ }
+};
+
+// Returns whether `edge` should be included in a heap snapshot of
+// `compartments`. The optional `policy` out-param is set to INCLUDE_EDGES
+// if we want to include the referent's edges, or EXCLUDE_EDGES if we don't
+// want to include them.
+static bool
+ShouldIncludeEdge(JS::CompartmentSet* compartments,
+ const ubi::Node& origin, const ubi::Edge& edge,
+ CoreDumpWriter::EdgePolicy* policy = nullptr)
+{
+ if (policy) {
+ *policy = CoreDumpWriter::INCLUDE_EDGES;
+ }
+
+ if (!compartments) {
+ // We aren't targeting a particular set of compartments, so serialize all the
+ // things!
+ return true;
+ }
+
+ // We are targeting a particular set of compartments. If this node is in our target
+ // set, serialize it and all of its edges. If this node is _not_ in our
+ // target set, we also serialize under the assumption that it is a shared
+ // resource being used by something in our target compartments since we reached it
+ // by traversing the heap graph. However, we do not serialize its outgoing
+ // edges and we abandon further traversal from this node.
+ //
+ // If the node does not belong to any compartment, we also serialize its outgoing
+ // edges. This case is relevant for Shapes: they don't belong to a specific
+ // compartment and contain edges to parent/kids Shapes we want to include. Note
+ // that these Shapes may contain pointers into our target compartment (the
+ // Shape's getter/setter JSObjects). However, we do not serialize nodes in other
+ // compartments that are reachable from these non-compartment nodes.
+
+ JSCompartment* compartment = edge.referent.compartment();
+
+ if (!compartment || compartments->has(compartment)) {
+ return true;
+ }
+
+ if (policy) {
+ *policy = CoreDumpWriter::EXCLUDE_EDGES;
+ }
+
+ return !!origin.compartment();
+}
+
+// A `CoreDumpWriter` that serializes nodes to protobufs and writes them to the
+// given `ZeroCopyOutputStream`.
+class MOZ_STACK_CLASS StreamWriter : public CoreDumpWriter
+{
+ using FrameSet = js::HashSet<uint64_t>;
+ using TwoByteStringMap = js::HashMap<TwoByteString, uint64_t, TwoByteString::HashPolicy>;
+ using OneByteStringMap = js::HashMap<const char*, uint64_t>;
+
+ JSContext* cx;
+ bool wantNames;
+ // The set of |JS::ubi::StackFrame::identifier()|s that have already been
+ // serialized and written to the core dump.
+ FrameSet framesAlreadySerialized;
+ // The set of two-byte strings that have already been serialized and written
+ // to the core dump.
+ TwoByteStringMap twoByteStringsAlreadySerialized;
+ // The set of one-byte strings that have already been serialized and written
+ // to the core dump.
+ OneByteStringMap oneByteStringsAlreadySerialized;
+
+ ::google::protobuf::io::ZeroCopyOutputStream& stream;
+
+ JS::CompartmentSet* compartments;
+
+ bool writeMessage(const ::google::protobuf::MessageLite& message) {
+ // We have to create a new CodedOutputStream when writing each message so
+ // that the 64MB size limit used by Coded{Output,Input}Stream to prevent
+ // integer overflow is enforced per message rather than on the whole stream.
+ ::google::protobuf::io::CodedOutputStream codedStream(&stream);
+ codedStream.WriteVarint32(message.ByteSize());
+ message.SerializeWithCachedSizes(&codedStream);
+ return !codedStream.HadError();
+ }
+
+ // Attach the full two-byte string or a reference to a two-byte string that
+ // has already been serialized to a protobuf message.
+ template <typename SetStringFunction,
+ typename SetRefFunction>
+ bool attachTwoByteString(TwoByteString& string, SetStringFunction setString,
+ SetRefFunction setRef) {
+ auto ptr = twoByteStringsAlreadySerialized.lookupForAdd(string);
+ if (ptr) {
+ setRef(ptr->value());
+ return true;
+ }
+
+ auto length = string.length();
+ auto stringData = MakeUnique<std::string>(length * sizeof(char16_t), '\0');
+ if (!stringData)
+ return false;
+
+ auto buf = const_cast<char16_t*>(reinterpret_cast<const char16_t*>(stringData->data()));
+ string.copyToBuffer(RangedPtr<char16_t>(buf, length), length);
+
+ uint64_t ref = twoByteStringsAlreadySerialized.count();
+ if (!twoByteStringsAlreadySerialized.add(ptr, Move(string), ref))
+ return false;
+
+ setString(stringData.release());
+ return true;
+ }
+
+ // Attach the full one-byte string or a reference to a one-byte string that
+ // has already been serialized to a protobuf message.
+ template <typename SetStringFunction,
+ typename SetRefFunction>
+ bool attachOneByteString(const char* string, SetStringFunction setString,
+ SetRefFunction setRef) {
+ auto ptr = oneByteStringsAlreadySerialized.lookupForAdd(string);
+ if (ptr) {
+ setRef(ptr->value());
+ return true;
+ }
+
+ auto length = strlen(string);
+ auto stringData = MakeUnique<std::string>(string, length);
+ if (!stringData)
+ return false;
+
+ uint64_t ref = oneByteStringsAlreadySerialized.count();
+ if (!oneByteStringsAlreadySerialized.add(ptr, string, ref))
+ return false;
+
+ setString(stringData.release());
+ return true;
+ }
+
+ protobuf::StackFrame* getProtobufStackFrame(JS::ubi::StackFrame& frame,
+ size_t depth = 1) {
+ // NB: de-duplicated string properties must be written in the same order
+ // here as they are read in `HeapSnapshot::saveStackFrame` or else indices
+ // in references to already serialized strings will be off.
+
+ MOZ_ASSERT(frame,
+ "null frames should be represented as the lack of a serialized "
+ "stack frame");
+
+ auto id = frame.identifier();
+ auto protobufStackFrame = MakeUnique<protobuf::StackFrame>();
+ if (!protobufStackFrame)
+ return nullptr;
+
+ if (framesAlreadySerialized.has(id)) {
+ protobufStackFrame->set_ref(id);
+ return protobufStackFrame.release();
+ }
+
+ auto data = MakeUnique<protobuf::StackFrame_Data>();
+ if (!data)
+ return nullptr;
+
+ data->set_id(id);
+ data->set_line(frame.line());
+ data->set_column(frame.column());
+ data->set_issystem(frame.isSystem());
+ data->set_isselfhosted(frame.isSelfHosted(cx));
+
+ auto dupeSource = TwoByteString::from(frame.source());
+ if (!attachTwoByteString(dupeSource,
+ [&] (std::string* source) { data->set_allocated_source(source); },
+ [&] (uint64_t ref) { data->set_sourceref(ref); }))
+ {
+ return nullptr;
+ }
+
+ auto dupeName = TwoByteString::from(frame.functionDisplayName());
+ if (dupeName.isNonNull()) {
+ if (!attachTwoByteString(dupeName,
+ [&] (std::string* name) { data->set_allocated_functiondisplayname(name); },
+ [&] (uint64_t ref) { data->set_functiondisplaynameref(ref); }))
+ {
+ return nullptr;
+ }
+ }
+
+ auto parent = frame.parent();
+ if (parent && depth < HeapSnapshot::MAX_STACK_DEPTH) {
+ auto protobufParent = getProtobufStackFrame(parent, depth + 1);
+ if (!protobufParent)
+ return nullptr;
+ data->set_allocated_parent(protobufParent);
+ }
+
+ protobufStackFrame->set_allocated_data(data.release());
+
+ if (!framesAlreadySerialized.put(id))
+ return nullptr;
+
+ return protobufStackFrame.release();
+ }
+
+public:
+ StreamWriter(JSContext* cx,
+ ::google::protobuf::io::ZeroCopyOutputStream& stream,
+ bool wantNames,
+ JS::CompartmentSet* compartments)
+ : cx(cx)
+ , wantNames(wantNames)
+ , framesAlreadySerialized(cx)
+ , twoByteStringsAlreadySerialized(cx)
+ , oneByteStringsAlreadySerialized(cx)
+ , stream(stream)
+ , compartments(compartments)
+ { }
+
+ bool init() {
+ return framesAlreadySerialized.init() &&
+ twoByteStringsAlreadySerialized.init() &&
+ oneByteStringsAlreadySerialized.init();
+ }
+
+ ~StreamWriter() override { }
+
+ virtual bool writeMetadata(uint64_t timestamp) final {
+ protobuf::Metadata metadata;
+ metadata.set_timestamp(timestamp);
+ return writeMessage(metadata);
+ }
+
+ virtual bool writeNode(const JS::ubi::Node& ubiNode,
+ EdgePolicy includeEdges) override final {
+ // NB: de-duplicated string properties must be written in the same order
+ // here as they are read in `HeapSnapshot::saveNode` or else indices in
+ // references to already serialized strings will be off.
+
+ protobuf::Node protobufNode;
+ protobufNode.set_id(ubiNode.identifier());
+
+ protobufNode.set_coarsetype(JS::ubi::CoarseTypeToUint32(ubiNode.coarseType()));
+
+ auto typeName = TwoByteString(ubiNode.typeName());
+ if (NS_WARN_IF(!attachTwoByteString(typeName,
+ [&] (std::string* name) { protobufNode.set_allocated_typename_(name); },
+ [&] (uint64_t ref) { protobufNode.set_typenameref(ref); })))
+ {
+ return false;
+ }
+
+ mozilla::MallocSizeOf mallocSizeOf = dbg::GetDebuggerMallocSizeOf(cx);
+ MOZ_ASSERT(mallocSizeOf);
+ protobufNode.set_size(ubiNode.size(mallocSizeOf));
+
+ if (includeEdges) {
+ auto edges = ubiNode.edges(cx, wantNames);
+ if (NS_WARN_IF(!edges))
+ return false;
+
+ for ( ; !edges->empty(); edges->popFront()) {
+ ubi::Edge& ubiEdge = edges->front();
+ if (!ShouldIncludeEdge(compartments, ubiNode, ubiEdge)) {
+ continue;
+ }
+
+ protobuf::Edge* protobufEdge = protobufNode.add_edges();
+ if (NS_WARN_IF(!protobufEdge)) {
+ return false;
+ }
+
+ protobufEdge->set_referent(ubiEdge.referent.identifier());
+
+ if (wantNames && ubiEdge.name) {
+ TwoByteString edgeName(Move(ubiEdge.name));
+ if (NS_WARN_IF(!attachTwoByteString(edgeName,
+ [&] (std::string* name) { protobufEdge->set_allocated_name(name); },
+ [&] (uint64_t ref) { protobufEdge->set_nameref(ref); })))
+ {
+ return false;
+ }
+ }
+ }
+ }
+
+ if (ubiNode.hasAllocationStack()) {
+ auto ubiStackFrame = ubiNode.allocationStack();
+ auto protoStackFrame = getProtobufStackFrame(ubiStackFrame);
+ if (NS_WARN_IF(!protoStackFrame))
+ return false;
+ protobufNode.set_allocated_allocationstack(protoStackFrame);
+ }
+
+ if (auto className = ubiNode.jsObjectClassName()) {
+ if (NS_WARN_IF(!attachOneByteString(className,
+ [&] (std::string* name) { protobufNode.set_allocated_jsobjectclassname(name); },
+ [&] (uint64_t ref) { protobufNode.set_jsobjectclassnameref(ref); })))
+ {
+ return false;
+ }
+ }
+
+ if (auto scriptFilename = ubiNode.scriptFilename()) {
+ if (NS_WARN_IF(!attachOneByteString(scriptFilename,
+ [&] (std::string* name) { protobufNode.set_allocated_scriptfilename(name); },
+ [&] (uint64_t ref) { protobufNode.set_scriptfilenameref(ref); })))
+ {
+ return false;
+ }
+ }
+
+ return writeMessage(protobufNode);
+ }
+};
+
+// A JS::ubi::BreadthFirst handler that serializes a snapshot of the heap into a
+// core dump.
+class MOZ_STACK_CLASS HeapSnapshotHandler
+{
+ CoreDumpWriter& writer;
+ JS::CompartmentSet* compartments;
+
+public:
+ // For telemetry.
+ uint32_t nodeCount;
+ uint32_t edgeCount;
+
+ HeapSnapshotHandler(CoreDumpWriter& writer,
+ JS::CompartmentSet* compartments)
+ : writer(writer),
+ compartments(compartments)
+ { }
+
+ // JS::ubi::BreadthFirst handler interface.
+
+ class NodeData { };
+ typedef JS::ubi::BreadthFirst<HeapSnapshotHandler> Traversal;
+ bool operator() (Traversal& traversal,
+ JS::ubi::Node origin,
+ const JS::ubi::Edge& edge,
+ NodeData*,
+ bool first)
+ {
+ 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))
+ 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);
+ if (!traversal.init())
+ return false;
+ 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)
+{
+ bool ignored;
+ auto duration = now - TimeStamp::ProcessCreation(ignored);
+ return (unsigned long) duration.ToMilliseconds();
+}
+
+/* static */ already_AddRefed<nsIFile>
+HeapSnapshot::CreateUniqueCoreDumpFile(ErrorResult& rv,
+ const TimeStamp& now,
+ nsAString& outFilePath)
+{
+ nsCOMPtr<nsIFile> file;
+ rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(file));
+ if (NS_WARN_IF(rv.Failed()))
+ return nullptr;
+
+ auto ms = msSinceProcessCreation(now);
+ rv = file->AppendNative(nsPrintfCString("%lu.fxsnapshot", ms));
+ if (NS_WARN_IF(rv.Failed()))
+ return nullptr;
+
+ rv = file->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0666);
+ if (NS_WARN_IF(rv.Failed()))
+ return nullptr;
+
+ rv = file->GetPath(outFilePath);
+ if (NS_WARN_IF(rv.Failed()))
+ return nullptr;
+
+ return file.forget();
+}
+
+// Deletion policy for cleaning up PHeapSnapshotTempFileHelperChild pointers.
+class DeleteHeapSnapshotTempFileHelperChild
+{
+public:
+ constexpr DeleteHeapSnapshotTempFileHelperChild() { }
+
+ void operator()(PHeapSnapshotTempFileHelperChild* ptr) const {
+ Unused << NS_WARN_IF(!HeapSnapshotTempFileHelperChild::Send__delete__(ptr));
+ }
+};
+
+// A UniquePtr alias to automatically manage PHeapSnapshotTempFileHelperChild
+// pointers.
+using UniqueHeapSnapshotTempFileHelperChild = UniquePtr<PHeapSnapshotTempFileHelperChild,
+ DeleteHeapSnapshotTempFileHelperChild>;
+
+// Get an nsIOutputStream that we can write the heap snapshot to. In non-e10s
+// and in the e10s parent process, open a file directly and create an output
+// stream for it. In e10s child processes, we are sandboxed without access to
+// the filesystem. Use IPDL to request a file descriptor from the parent
+// process.
+static already_AddRefed<nsIOutputStream>
+getCoreDumpOutputStream(ErrorResult& rv, TimeStamp& start, nsAString& outFilePath)
+{
+ if (XRE_IsParentProcess()) {
+ // Create the file and open the output stream directly.
+
+ nsCOMPtr<nsIFile> file = HeapSnapshot::CreateUniqueCoreDumpFile(rv,
+ start,
+ outFilePath);
+ if (NS_WARN_IF(rv.Failed()))
+ return nullptr;
+
+ nsCOMPtr<nsIOutputStream> outputStream;
+ rv = NS_NewLocalFileOutputStream(getter_AddRefs(outputStream), file,
+ PR_WRONLY, -1, 0);
+ if (NS_WARN_IF(rv.Failed()))
+ return nullptr;
+
+ return outputStream.forget();
+ } else {
+ // Request a file descriptor from the parent process over IPDL.
+
+ auto cc = ContentChild::GetSingleton();
+ if (!cc) {
+ rv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ UniqueHeapSnapshotTempFileHelperChild helper(
+ cc->SendPHeapSnapshotTempFileHelperConstructor());
+ if (NS_WARN_IF(!helper)) {
+ rv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ OpenHeapSnapshotTempFileResponse response;
+ if (!helper->SendOpenHeapSnapshotTempFile(&response)) {
+ rv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+ if (response.type() == OpenHeapSnapshotTempFileResponse::Tnsresult) {
+ rv.Throw(response.get_nsresult());
+ return nullptr;
+ }
+
+ auto opened = response.get_OpenedFile();
+ outFilePath = opened.path();
+ nsCOMPtr<nsIOutputStream> outputStream =
+ FileDescriptorOutputStream::Create(opened.descriptor());
+ if (NS_WARN_IF(!outputStream)) {
+ rv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ return outputStream.forget();
+ }
+}
+
+} // namespace devtools
+
+namespace dom {
+
+using namespace JS;
+using namespace devtools;
+
+/* static */ void
+ThreadSafeChromeUtils::SaveHeapSnapshot(GlobalObject& global,
+ const HeapSnapshotBoundaries& boundaries,
+ nsAString& outFilePath,
+ ErrorResult& rv)
+{
+ auto start = TimeStamp::Now();
+
+ bool wantNames = true;
+ CompartmentSet compartments;
+ uint32_t nodeCount = 0;
+ uint32_t edgeCount = 0;
+
+ nsCOMPtr<nsIOutputStream> outputStream = getCoreDumpOutputStream(rv, start, outFilePath);
+ if (NS_WARN_IF(rv.Failed()))
+ return;
+
+ ZeroCopyNSIOutputStream zeroCopyStream(outputStream);
+ ::google::protobuf::io::GzipOutputStream gzipStream(&zeroCopyStream);
+
+ JSContext* cx = global.Context();
+
+ {
+ Maybe<AutoCheckCannotGC> maybeNoGC;
+ ubi::RootList rootList(cx, maybeNoGC, wantNames);
+ if (!EstablishBoundaries(cx, rv, boundaries, rootList, compartments))
+ return;
+
+ StreamWriter writer(cx, gzipStream, wantNames,
+ compartments.initialized() ? &compartments : nullptr);
+ if (NS_WARN_IF(!writer.init())) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ MOZ_ASSERT(maybeNoGC.isSome());
+ ubi::Node roots(&rootList);
+
+ // Serialize the initial heap snapshot metadata to the core dump.
+ if (!writer.writeMetadata(PR_Now()) ||
+ // Serialize the heap graph to the core dump, starting from our list of
+ // roots.
+ !WriteHeapGraph(cx,
+ roots,
+ writer,
+ wantNames,
+ compartments.initialized() ? &compartments : nullptr,
+ maybeNoGC.ref(),
+ 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 */ already_AddRefed<HeapSnapshot>
+ThreadSafeChromeUtils::ReadHeapSnapshot(GlobalObject& global,
+ const nsAString& filePath,
+ ErrorResult& rv)
+{
+ auto start = TimeStamp::Now();
+
+ UniquePtr<char[]> path(ToNewCString(filePath));
+ if (!path) {
+ rv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return nullptr;
+ }
+
+ AutoMemMap mm;
+ rv = mm.init(path.get());
+ if (rv.Failed())
+ return nullptr;
+
+ RefPtr<HeapSnapshot> snapshot = HeapSnapshot::Create(
+ global.Context(), global, reinterpret_cast<const uint8_t*>(mm.address()),
+ mm.size(), rv);
+
+ 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 000000000..0428033f6
--- /dev/null
+++ b/devtools/shared/heapsnapshot/HeapSnapshot.h
@@ -0,0 +1,239 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_devtools_HeapSnapshot__
+#define mozilla_devtools_HeapSnapshot__
+
+#include "js/HashTable.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/devtools/DeserializedNode.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/Nullable.h"
+#include "mozilla/HashFunctions.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/RefCounted.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/UniquePtr.h"
+
+#include "CoreDump.pb.h"
+#include "nsCOMPtr.h"
+#include "nsCRTGlue.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+#include "nsXPCOM.h"
+
+namespace mozilla {
+namespace devtools {
+
+class DominatorTree;
+
+struct NSFreePolicy {
+ void operator()(void* ptr) {
+ NS_Free(ptr);
+ }
+};
+
+using UniqueTwoByteString = UniquePtr<char16_t[], NSFreePolicy>;
+using UniqueOneByteString = UniquePtr<char[], NSFreePolicy>;
+
+class HeapSnapshot final : public nsISupports
+ , public nsWrapperCache
+{
+ friend struct DeserializedNode;
+ friend struct DeserializedEdge;
+ friend struct DeserializedStackFrame;
+ friend class JS::ubi::Concrete<JS::ubi::DeserializedNode>;
+
+ explicit HeapSnapshot(JSContext* cx, nsISupports* aParent)
+ : timestamp(Nothing())
+ , rootId(0)
+ , nodes(cx)
+ , frames(cx)
+ , mParent(aParent)
+ {
+ MOZ_ASSERT(aParent);
+ };
+
+ // Initialize this HeapSnapshot from the given buffer that contains a
+ // serialized core dump. Do NOT take ownership of the buffer, only borrow it
+ // for the duration of the call. Return false on failure.
+ bool init(JSContext* cx, const uint8_t* buffer, uint32_t size);
+
+ using NodeIdSet = js::HashSet<NodeId>;
+
+ // Save the given `protobuf::Node` message in this `HeapSnapshot` as a
+ // `DeserializedNode`.
+ bool saveNode(const protobuf::Node& node, NodeIdSet& edgeReferents);
+
+ // Save the given `protobuf::StackFrame` message in this `HeapSnapshot` as a
+ // `DeserializedStackFrame`. The saved stack frame's id is returned via the
+ // out parameter.
+ bool saveStackFrame(const protobuf::StackFrame& frame,
+ StackFrameId& outFrameId);
+
+public:
+ // The maximum number of stack frames that we will serialize into a core
+ // dump. This helps prevent over-recursion in the protobuf library when
+ // deserializing stacks.
+ static const size_t MAX_STACK_DEPTH = 60;
+
+private:
+ // If present, a timestamp in the same units that `PR_Now` gives.
+ Maybe<uint64_t> timestamp;
+
+ // The id of the root node for this deserialized heap graph.
+ NodeId rootId;
+
+ // The set of nodes in this deserialized heap graph, keyed by id.
+ using NodeSet = js::HashSet<DeserializedNode, DeserializedNode::HashPolicy>;
+ NodeSet nodes;
+
+ // The set of stack frames in this deserialized heap graph, keyed by id.
+ using FrameSet = js::HashSet<DeserializedStackFrame,
+ DeserializedStackFrame::HashPolicy>;
+ FrameSet frames;
+
+ Vector<UniqueTwoByteString> internedTwoByteStrings;
+ Vector<UniqueOneByteString> internedOneByteStrings;
+
+ using StringOrRef = Variant<const std::string*, uint64_t>;
+
+ template<typename CharT,
+ typename InternedStringSet>
+ const CharT* getOrInternString(InternedStringSet& internedStrings,
+ Maybe<StringOrRef>& maybeStrOrRef);
+
+protected:
+ nsCOMPtr<nsISupports> mParent;
+
+ virtual ~HeapSnapshot() { }
+
+public:
+ // Create a `HeapSnapshot` from the given buffer that contains a serialized
+ // core dump. Do NOT take ownership of the buffer, only borrow it for the
+ // duration of the call.
+ static already_AddRefed<HeapSnapshot> Create(JSContext* cx,
+ dom::GlobalObject& global,
+ const uint8_t* buffer,
+ uint32_t size,
+ ErrorResult& rv);
+
+ // Creates the `$TEMP_DIR/XXXXXX-XXX.fxsnapshot` core dump file that heap
+ // snapshots are serialized into.
+ static already_AddRefed<nsIFile> CreateUniqueCoreDumpFile(ErrorResult& rv,
+ const TimeStamp& now,
+ nsAString& outFilePath);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(HeapSnapshot)
+ MOZ_DECLARE_REFCOUNTED_TYPENAME(HeapSnapshot)
+
+ nsISupports* GetParentObject() const { return mParent; }
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ const char16_t* borrowUniqueString(const char16_t* duplicateString,
+ size_t length);
+
+ // Get the root node of this heap snapshot's graph.
+ JS::ubi::Node getRoot() {
+ MOZ_ASSERT(nodes.initialized());
+ auto p = nodes.lookup(rootId);
+ MOZ_ASSERT(p);
+ const DeserializedNode& node = *p;
+ return JS::ubi::Node(const_cast<DeserializedNode*>(&node));
+ }
+
+ Maybe<JS::ubi::Node> getNodeById(JS::ubi::Node::Id nodeId) {
+ auto p = nodes.lookup(nodeId);
+ if (!p)
+ return Nothing();
+ return Some(JS::ubi::Node(const_cast<DeserializedNode*>(&*p)));
+ }
+
+ void TakeCensus(JSContext* cx, JS::HandleObject options,
+ JS::MutableHandleValue rval, ErrorResult& rv);
+
+ void DescribeNode(JSContext* cx, JS::HandleObject breakdown, uint64_t nodeId,
+ JS::MutableHandleValue rval, ErrorResult& rv);
+
+ already_AddRefed<DominatorTree> ComputeDominatorTree(ErrorResult& rv);
+
+ void ComputeShortestPaths(JSContext*cx, uint64_t start,
+ const dom::Sequence<uint64_t>& targets,
+ uint64_t maxNumPaths,
+ JS::MutableHandleObject results,
+ ErrorResult& rv);
+
+ dom::Nullable<uint64_t> GetCreationTime() {
+ static const uint64_t maxTime = uint64_t(1) << 53;
+ if (timestamp.isSome() && timestamp.ref() <= maxTime) {
+ return dom::Nullable<uint64_t>(timestamp.ref());
+ }
+
+ return dom::Nullable<uint64_t>();
+ }
+};
+
+// A `CoreDumpWriter` is given the data we wish to save in a core dump and
+// serializes it to disk, or memory, or a socket, etc.
+class CoreDumpWriter
+{
+public:
+ virtual ~CoreDumpWriter() { };
+
+ // Write the given bits of metadata we would like to associate with this core
+ // dump.
+ virtual bool writeMetadata(uint64_t timestamp) = 0;
+
+ enum EdgePolicy : bool {
+ INCLUDE_EDGES = true,
+ EXCLUDE_EDGES = false
+ };
+
+ // Write the given `JS::ubi::Node` to the core dump. The given `EdgePolicy`
+ // dictates whether its outgoing edges should also be written to the core
+ // dump, or excluded.
+ virtual bool writeNode(const JS::ubi::Node& node,
+ EdgePolicy includeEdges) = 0;
+};
+
+// Serialize the heap graph as seen from `node` with the given `CoreDumpWriter`.
+// If `wantNames` is true, capture edge names. If `zones` is non-null, only
+// capture the sub-graph within the zone set, otherwise capture the whole heap
+// graph. Returns false on failure.
+bool
+WriteHeapGraph(JSContext* cx,
+ const JS::ubi::Node& node,
+ CoreDumpWriter& writer,
+ bool wantNames,
+ JS::CompartmentSet* compartments,
+ JS::AutoCheckCannotGC& noGC,
+ 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 000000000..abd44fc30
--- /dev/null
+++ b/devtools/shared/heapsnapshot/HeapSnapshotFileUtils.js
@@ -0,0 +1,95 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Heap snapshots are always saved in the temp directory, and have a regular
+// naming convention. This module provides helpers for working with heap
+// snapshot files in a safe manner. Because we attempt to avoid unnecessary
+// copies of the heap snapshot files by checking the local filesystem for a heap
+// snapshot file with the given snapshot id, we want to ensure that we are only
+// attempting to open heap snapshot files and not `~/.ssh/id_rsa`, for
+// example. Therefore, the RDP only talks about snapshot ids, or transfering the
+// bulk file data. A file path can be recovered from a snapshot id, which allows
+// one to check for the presence of the heap snapshot file on the local file
+// system, but we don't have to worry about opening arbitrary files.
+//
+// The heap snapshot file path conventions permits the following forms:
+//
+// $TEMP_DIRECTORY/XXXXXXXXXX.fxsnapshot
+// $TEMP_DIRECTORY/XXXXXXXXXX-XXXXX.fxsnapshot
+//
+// Where the strings of "X" are zero or more digits.
+
+"use strict";
+
+const { Ci } = require("chrome");
+loader.lazyRequireGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm", true);
+loader.lazyRequireGetter(this, "OS", "resource://gre/modules/osfile.jsm", true);
+
+function getHeapSnapshotFileTemplate() {
+ return OS.Path.join(OS.Constants.Path.tmpDir, `${Date.now()}.fxsnapshot`);
+}
+
+/**
+ * Get a unique temp file path for a new heap snapshot. The file is guaranteed
+ * not to exist before this call.
+ *
+ * @returns String
+ */
+exports.getNewUniqueHeapSnapshotTempFilePath = function () {
+ let file = new FileUtils.File(getHeapSnapshotFileTemplate());
+ // The call to createUnique will append "-N" after the leaf name (but before
+ // the extension) until a new file is found and create it. This guarantees we
+ // won't accidentally choose the same file twice.
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+ return file.path;
+};
+
+function isValidSnapshotFileId(snapshotId) {
+ return /^\d+(\-\d+)?$/.test(snapshotId);
+}
+
+/**
+ * Get the file path for the given snapshot id.
+ *
+ * @param {String} snapshotId
+ *
+ * @returns String | null
+ */
+exports.getHeapSnapshotTempFilePath = function (snapshotId) {
+ // Don't want anyone sneaking "../../../.." strings into the snapshot id and
+ // trying to make us open arbitrary files.
+ if (!isValidSnapshotFileId(snapshotId)) {
+ return null;
+ }
+ return OS.Path.join(OS.Constants.Path.tmpDir, snapshotId + ".fxsnapshot");
+};
+
+/**
+ * Return true if we have the heap snapshot file for the given snapshot id on
+ * the local file system. False is returned otherwise.
+ *
+ * @returns Promise<Boolean>
+ */
+exports.haveHeapSnapshotTempFile = function (snapshotId) {
+ const path = exports.getHeapSnapshotTempFilePath(snapshotId);
+ if (!path) {
+ return Promise.resolve(false);
+ }
+
+ return OS.File.stat(path).then(() => true,
+ () => false);
+};
+
+/**
+ * Given a heap snapshot's file path, extricate the snapshot id.
+ *
+ * @param {String} path
+ *
+ * @returns String
+ */
+exports.getSnapshotIdFromPath = function (path) {
+ return path.slice(OS.Constants.Path.tmpDir.length + 1,
+ path.length - ".fxsnapshot".length);
+};
diff --git a/devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperChild.h b/devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperChild.h
new file mode 100644
index 000000000..a1d433a5e
--- /dev/null
+++ b/devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperChild.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim: set sw=4 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_devtools_HeapSnapshotTempFileHelperChild_h
+#define mozilla_devtools_HeapSnapshotTempFileHelperChild_h
+
+#include "mozilla/devtools/PHeapSnapshotTempFileHelperChild.h"
+
+namespace mozilla {
+namespace devtools {
+
+class HeapSnapshotTempFileHelperChild : public PHeapSnapshotTempFileHelperChild
+{
+ explicit HeapSnapshotTempFileHelperChild() { }
+
+public:
+ static inline PHeapSnapshotTempFileHelperChild* Create();
+};
+
+/* static */ inline PHeapSnapshotTempFileHelperChild*
+HeapSnapshotTempFileHelperChild::Create()
+{
+ return new HeapSnapshotTempFileHelperChild();
+}
+
+} // namespace devtools
+} // namespace mozilla
+
+#endif // mozilla_devtools_HeapSnapshotTempFileHelperChild_h
diff --git a/devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperParent.cpp b/devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperParent.cpp
new file mode 100644
index 000000000..7246a9daa
--- /dev/null
+++ b/devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperParent.cpp
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim: set sw=4 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/devtools/HeapSnapshot.h"
+#include "mozilla/devtools/HeapSnapshotTempFileHelperParent.h"
+#include "mozilla/ErrorResult.h"
+#include "private/pprio.h"
+
+#include "nsIFile.h"
+
+namespace mozilla {
+namespace devtools {
+
+using ipc::FileDescriptor;
+
+static bool
+openFileFailure(ErrorResult& rv,
+ OpenHeapSnapshotTempFileResponse* outResponse)
+{
+ *outResponse = rv.StealNSResult();
+ return true;
+}
+
+bool
+HeapSnapshotTempFileHelperParent::RecvOpenHeapSnapshotTempFile(
+ OpenHeapSnapshotTempFileResponse* outResponse)
+{
+ auto start = TimeStamp::Now();
+ ErrorResult rv;
+ nsAutoString filePath;
+ nsCOMPtr<nsIFile> file = HeapSnapshot::CreateUniqueCoreDumpFile(rv,
+ start,
+ filePath);
+ if (NS_WARN_IF(rv.Failed()))
+ return openFileFailure(rv, outResponse);
+
+ PRFileDesc* prfd;
+ rv = file->OpenNSPRFileDesc(PR_WRONLY, 0, &prfd);
+ if (NS_WARN_IF(rv.Failed()))
+ return openFileFailure(rv, outResponse);
+
+ FileDescriptor::PlatformHandleType handle =
+ FileDescriptor::PlatformHandleType(PR_FileDesc2NativeHandle(prfd));
+ FileDescriptor fd(handle);
+ *outResponse = OpenedFile(filePath, fd);
+ return true;
+}
+
+} // namespace devtools
+} // namespace mozilla
diff --git a/devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperParent.h b/devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperParent.h
new file mode 100644
index 000000000..1582279da
--- /dev/null
+++ b/devtools/shared/heapsnapshot/HeapSnapshotTempFileHelperParent.h
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim: set sw=4 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_devtools_HeapSnapshotTempFileHelperParent_h
+#define mozilla_devtools_HeapSnapshotTempFileHelperParent_h
+
+#include "mozilla/devtools/PHeapSnapshotTempFileHelperParent.h"
+
+namespace mozilla {
+namespace devtools {
+
+class HeapSnapshotTempFileHelperParent : public PHeapSnapshotTempFileHelperParent
+{
+ explicit HeapSnapshotTempFileHelperParent() { }
+ void ActorDestroy(ActorDestroyReason why) override { }
+ bool RecvOpenHeapSnapshotTempFile(OpenHeapSnapshotTempFileResponse* outResponse)
+ override;
+
+ public:
+ static inline PHeapSnapshotTempFileHelperParent* Create();
+};
+
+/* static */ inline PHeapSnapshotTempFileHelperParent*
+HeapSnapshotTempFileHelperParent::Create()
+{
+ return new HeapSnapshotTempFileHelperParent();
+}
+
+} // namespace devtools
+} // namespace mozilla
+
+#endif // mozilla_devtools_HeapSnapshotTempFileHelperParent_h
diff --git a/devtools/shared/heapsnapshot/PHeapSnapshotTempFileHelper.ipdl b/devtools/shared/heapsnapshot/PHeapSnapshotTempFileHelper.ipdl
new file mode 100644
index 000000000..2576470e2
--- /dev/null
+++ b/devtools/shared/heapsnapshot/PHeapSnapshotTempFileHelper.ipdl
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: set ts=8 sts=4 et sw=4 tw=99:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PContent;
+
+namespace mozilla {
+namespace devtools {
+
+struct OpenedFile
+{
+ nsString path;
+ FileDescriptor descriptor;
+};
+
+union OpenHeapSnapshotTempFileResponse
+{
+ nsresult;
+ OpenedFile;
+};
+
+sync protocol PHeapSnapshotTempFileHelper
+{
+ manager PContent;
+
+parent:
+ sync OpenHeapSnapshotTempFile() returns (OpenHeapSnapshotTempFileResponse response);
+
+ async __delete__();
+};
+
+} // namespace devtools
+} // namespace mozilla
diff --git a/devtools/shared/heapsnapshot/ZeroCopyNSIOutputStream.cpp b/devtools/shared/heapsnapshot/ZeroCopyNSIOutputStream.cpp
new file mode 100644
index 000000000..0c29db7f9
--- /dev/null
+++ b/devtools/shared/heapsnapshot/ZeroCopyNSIOutputStream.cpp
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/devtools/ZeroCopyNSIOutputStream.h"
+
+#include "mozilla/DebugOnly.h"
+#include "mozilla/Unused.h"
+
+namespace mozilla {
+namespace devtools {
+
+ZeroCopyNSIOutputStream::ZeroCopyNSIOutputStream(nsCOMPtr<nsIOutputStream>& out)
+ : out(out)
+ , result_(NS_OK)
+ , amountUsed(0)
+ , writtenCount(0)
+{
+ DebugOnly<bool> nonBlocking = false;
+ MOZ_ASSERT(out->IsNonBlocking(&nonBlocking) == NS_OK);
+ MOZ_ASSERT(!nonBlocking);
+}
+
+ZeroCopyNSIOutputStream::~ZeroCopyNSIOutputStream()
+{
+ if (!failed())
+ Unused << NS_WARN_IF(NS_FAILED(writeBuffer()));
+}
+
+nsresult
+ZeroCopyNSIOutputStream::writeBuffer()
+{
+ if (failed())
+ return result_;
+
+ if (amountUsed == 0)
+ return NS_OK;
+
+ int32_t amountWritten = 0;
+ while (amountWritten < amountUsed) {
+ uint32_t justWritten = 0;
+
+ result_ = out->Write(buffer + amountWritten,
+ amountUsed - amountWritten,
+ &justWritten);
+ if (NS_WARN_IF(NS_FAILED(result_)))
+ return result_;
+
+ amountWritten += justWritten;
+ }
+
+ writtenCount += amountUsed;
+ amountUsed = 0;
+ return NS_OK;
+}
+
+// ZeroCopyOutputStream Interface
+
+bool
+ZeroCopyNSIOutputStream::Next(void** data, int* size)
+{
+ MOZ_ASSERT(data != nullptr);
+ MOZ_ASSERT(size != nullptr);
+
+ if (failed())
+ return false;
+
+ if (amountUsed == BUFFER_SIZE) {
+ if (NS_FAILED(writeBuffer()))
+ return false;
+ }
+
+ *data = buffer + amountUsed;
+ *size = BUFFER_SIZE - amountUsed;
+ amountUsed = BUFFER_SIZE;
+ return true;
+}
+
+void
+ZeroCopyNSIOutputStream::BackUp(int count)
+{
+ MOZ_ASSERT(count >= 0,
+ "Cannot back up a negative amount of bytes.");
+ MOZ_ASSERT(amountUsed == BUFFER_SIZE,
+ "Can only call BackUp directly after calling Next.");
+ MOZ_ASSERT(count <= amountUsed,
+ "Can't back up further than we've given out.");
+
+ amountUsed -= count;
+}
+
+::google::protobuf::int64
+ZeroCopyNSIOutputStream::ByteCount() const
+{
+ return writtenCount + amountUsed;
+}
+
+} // namespace devtools
+} // namespace mozilla
diff --git a/devtools/shared/heapsnapshot/ZeroCopyNSIOutputStream.h b/devtools/shared/heapsnapshot/ZeroCopyNSIOutputStream.h
new file mode 100644
index 000000000..117fc0f87
--- /dev/null
+++ b/devtools/shared/heapsnapshot/ZeroCopyNSIOutputStream.h
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_devtools_ZeroCopyNSIOutputStream__
+#define mozilla_devtools_ZeroCopyNSIOutputStream__
+
+#include <google/protobuf/io/zero_copy_stream.h>
+#include <google/protobuf/stubs/common.h>
+
+#include "nsCOMPtr.h"
+#include "nsIOutputStream.h"
+
+namespace mozilla {
+namespace devtools {
+
+// A `google::protobuf::io::ZeroCopyOutputStream` implementation that uses an
+// `nsIOutputStream` under the covers.
+//
+// This class will automatically write and flush its data to the
+// `nsIOutputStream` in its destructor, but if you care whether that call
+// succeeds or fails, then you should call the `flush` method yourself. Errors
+// will be logged, however.
+class MOZ_STACK_CLASS ZeroCopyNSIOutputStream
+ : public ::google::protobuf::io::ZeroCopyOutputStream
+{
+ static const int BUFFER_SIZE = 8192;
+
+ // The nsIOutputStream we are streaming to.
+ nsCOMPtr<nsIOutputStream>& out;
+
+ // The buffer we write data to before passing it to the output stream.
+ char buffer[BUFFER_SIZE];
+
+ // The status of writing to the underlying output stream.
+ nsresult result_;
+
+ // The number of bytes in the buffer that have been used thus far.
+ int amountUsed;
+
+ // Excluding the amount of the buffer currently used (which hasn't been
+ // written and flushed yet), this is the number of bytes written to the output
+ // stream.
+ int64_t writtenCount;
+
+ // Write the internal buffer to the output stream and flush it.
+ nsresult writeBuffer();
+
+public:
+ explicit ZeroCopyNSIOutputStream(nsCOMPtr<nsIOutputStream>& out);
+
+ nsresult flush() { return writeBuffer(); }
+
+ // Return true if writing to the underlying output stream ever failed.
+ bool failed() const { return NS_FAILED(result_); }
+
+ nsresult result() const { return result_; }
+
+ // ZeroCopyOutputStream Interface
+ virtual ~ZeroCopyNSIOutputStream() override;
+ virtual bool Next(void** data, int* size) override;
+ virtual void BackUp(int count) override;
+ virtual ::google::protobuf::int64 ByteCount() const override;
+};
+
+} // namespace devtools
+} // namespace mozilla
+
+#endif // mozilla_devtools_ZeroCopyNSIOutputStream__
diff --git a/devtools/shared/heapsnapshot/census-tree-node.js b/devtools/shared/heapsnapshot/census-tree-node.js
new file mode 100644
index 000000000..b041e77f9
--- /dev/null
+++ b/devtools/shared/heapsnapshot/census-tree-node.js
@@ -0,0 +1,748 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// CensusTreeNode is an intermediate representation of a census report that
+// exists between after a report is generated by taking a census and before the
+// report is rendered in the DOM. It must be dead simple to render, with no
+// further data processing or massaging needed before rendering DOM nodes. Our
+// goal is to do the census report to CensusTreeNode transformation in the
+// HeapAnalysesWorker, and ensure that the **only** work that the main thread
+// has to do is strictly DOM rendering work.
+
+const {
+ Visitor,
+ walk,
+ basisTotalBytes,
+ basisTotalCount,
+} = require("resource://devtools/shared/heapsnapshot/CensusUtils.js");
+
+// Monotonically increasing integer for CensusTreeNode `id`s.
+let censusTreeNodeIdCounter = 0;
+
+/**
+ * Return true if the given object is a SavedFrame stack object, false otherwise.
+ *
+ * @param {any} obj
+ * @returns {Boolean}
+ */
+function isSavedFrame(obj) {
+ return Object.prototype.toString.call(obj) === "[object SavedFrame]";
+}
+
+/**
+ * A CensusTreeNodeCache maps from SavedFrames to CensusTreeNodes. It is used when
+ * aggregating multiple SavedFrame allocation stack keys into a tree of many
+ * CensusTreeNodes. Each stack may share older frames, and we want to preserve
+ * this sharing when converting to CensusTreeNode, so before creating a new
+ * CensusTreeNode, we look for an existing one in one of our CensusTreeNodeCaches.
+ */
+function CensusTreeNodeCache() {}
+CensusTreeNodeCache.prototype = null;
+
+/**
+ * The value of a single entry stored in a CensusTreeNodeCache. It is a pair of
+ * the CensusTreeNode for this cache value, and the subsequent
+ * CensusTreeNodeCache for this node's children.
+ *
+ * @param {SavedFrame} frame
+ * The frame being cached.
+ */
+function CensusTreeNodeCacheValue() {
+ // The CensusTreeNode for this cache value.
+ this.node = undefined;
+ // The CensusTreeNodeCache for this frame's children.
+ this.children = undefined;
+}
+
+CensusTreeNodeCacheValue.prototype = null;
+
+/**
+ * Create a unique string for the given SavedFrame (ignoring the frame's parent
+ * chain) that can be used as a hash to key this frame within a CensusTreeNodeCache.
+ *
+ * NB: We manually hash rather than using an ES6 Map because we are purposely
+ * ignoring the parent chain and wish to consider frames with everything the
+ * same except their parents as the same.
+ *
+ * @param {SavedFrame} frame
+ * The SavedFrame object we would like to lookup in or insert into a
+ * CensusTreeNodeCache.
+ *
+ * @returns {String}
+ * The unique string that can be used as a key in a CensusTreeNodeCache.
+ */
+CensusTreeNodeCache.hashFrame = function (frame) {
+ return `FRAME,${frame.functionDisplayName},${frame.source},${frame.line},${frame.column},${frame.asyncCause}`;
+};
+
+/**
+ * Create a unique string for the given CensusTreeNode **with regards to
+ * siblings at the current depth of the tree, not within the whole tree.** It
+ * can be used as a hash to key this node within a CensusTreeNodeCache.
+ *
+ * @param {CensusTreeNode} node
+ * The node we would like to lookup in or insert into a cache.
+ *
+ * @returns {String}
+ * The unique string that can be used as a key in a CensusTreeNodeCache.
+ */
+CensusTreeNodeCache.hashNode = function (node) {
+ return isSavedFrame(node.name)
+ ? CensusTreeNodeCache.hashFrame(node.name)
+ : `NODE,${node.name}`;
+};
+
+/**
+ * Insert the given CensusTreeNodeCacheValue whose node.name is a SavedFrame
+ * object in the given cache.
+ *
+ * @param {CensusTreeNodeCache} cache
+ * @param {CensusTreeNodeCacheValue} value
+ */
+CensusTreeNodeCache.insertFrame = function (cache, value) {
+ cache[CensusTreeNodeCache.hashFrame(value.node.name)] = value;
+};
+
+/**
+ * Insert the given value in the cache.
+ *
+ * @param {CensusTreeNodeCache} cache
+ * @param {CensusTreeNodeCacheValue} value
+ */
+CensusTreeNodeCache.insertNode = function (cache, value) {
+ if (isSavedFrame(value.node.name)) {
+ CensusTreeNodeCache.insertFrame(cache, value);
+ } else {
+ cache[CensusTreeNodeCache.hashNode(value.node)] = value;
+ }
+};
+
+/**
+ * Lookup `frame` in `cache` and return its value if it exists.
+ *
+ * @param {CensusTreeNodeCache} cache
+ * @param {SavedFrame} frame
+ *
+ * @returns {undefined|CensusTreeNodeCacheValue}
+ */
+CensusTreeNodeCache.lookupFrame = function (cache, frame) {
+ return cache[CensusTreeNodeCache.hashFrame(frame)];
+};
+
+/**
+ * Lookup `node` in `cache` and return its value if it exists.
+ *
+ * @param {CensusTreeNodeCache} cache
+ * @param {CensusTreeNode} node
+ *
+ * @returns {undefined|CensusTreeNodeCacheValue}
+ */
+CensusTreeNodeCache.lookupNode = function (cache, node) {
+ return isSavedFrame(node.name)
+ ? CensusTreeNodeCache.lookupFrame(cache, node.name)
+ : cache[CensusTreeNodeCache.hashNode(node)];
+};
+
+/**
+ * Add `child` to `parent`'s set of children and store the parent ID
+ * on the child.
+ *
+ * @param {CensusTreeNode} parent
+ * @param {CensusTreeNode} child
+ */
+function addChild(parent, child) {
+ if (!parent.children) {
+ parent.children = [];
+ }
+ child.parent = parent.id;
+ parent.children.push(child);
+}
+
+/**
+ * Get an array of each frame in the provided stack.
+ *
+ * @param {SavedFrame} stack
+ * @returns {Array<SavedFrame>}
+ */
+function getArrayOfFrames(stack) {
+ const frames = [];
+ let frame = stack;
+ while (frame) {
+ frames.push(frame);
+ frame = frame.parent;
+ }
+ frames.reverse();
+ return frames;
+}
+
+/**
+ * Given an `edge` to a sub-`report` whose structure is described by
+ * `breakdown`, create a CensusTreeNode tree.
+ *
+ * @param {Object} breakdown
+ * The breakdown specifying the structure of the given report.
+ *
+ * @param {Object} report
+ * The census report.
+ *
+ * @param {null|String|SavedFrame} edge
+ * The edge leading to this report from the parent report.
+ *
+ * @param {CensusTreeNodeCache} cache
+ * The cache of CensusTreeNodes we have already made for the siblings of
+ * the node being created. The existing nodes are reused when possible.
+ *
+ * @param {Object} outParams
+ * The return values are attached to this object after this function
+ * returns. Because we create a CensusTreeNode for each frame in a
+ * SavedFrame stack edge, there may multiple nodes per sub-report.
+ *
+ * - top: The deepest node in the CensusTreeNode subtree created.
+ *
+ * - bottom: The shallowest node in the CensusTreeNode subtree created.
+ * This is null if the shallowest node in the subtree was
+ * found in the `cache` and reused.
+ *
+ * Note that top and bottom are not necessarily different. In the case
+ * where there is a 1:1 correspondence between an edge in the report and
+ * a CensusTreeNode, top and bottom refer to the same node.
+ */
+function makeCensusTreeNodeSubTree(breakdown, report, edge, cache, outParams) {
+ if (!isSavedFrame(edge)) {
+ const node = new CensusTreeNode(edge);
+ outParams.top = outParams.bottom = node;
+ return;
+ }
+
+ const frames = getArrayOfFrames(edge);
+ let currentCache = cache;
+ let prevNode;
+ for (let i = 0, length = frames.length; i < length; i++) {
+ const frame = frames[i];
+
+ // Get or create the CensusTreeNodeCacheValue for this frame. If we already
+ // have a CensusTreeNodeCacheValue (and hence a CensusTreeNode) for this
+ // frame, we don't need to add the node to the previous node's children as
+ // we have already done that. If we don't have a CensusTreeNodeCacheValue
+ // and CensusTreeNode for this frame, then create one and make sure to hook
+ // it up as a child of the previous node.
+ let isNewNode = false;
+ let val = CensusTreeNodeCache.lookupFrame(currentCache, frame);
+ if (!val) {
+ isNewNode = true;
+ val = new CensusTreeNodeCacheValue();
+ val.node = new CensusTreeNode(frame);
+
+ CensusTreeNodeCache.insertFrame(currentCache, val);
+ if (prevNode) {
+ addChild(prevNode, val.node);
+ }
+ }
+
+ if (i === 0) {
+ outParams.bottom = isNewNode ? val.node : null;
+ }
+ if (i === length - 1) {
+ outParams.top = val.node;
+ }
+
+ prevNode = val.node;
+
+ if (i !== length - 1 && !val.children) {
+ // This is not the last frame and therefore this node will have
+ // children, which we must cache.
+ val.children = new CensusTreeNodeCache();
+ }
+
+ currentCache = val.children;
+ }
+}
+
+/**
+ * A Visitor that walks a census report and creates the corresponding
+ * CensusTreeNode tree.
+ */
+function CensusTreeNodeVisitor() {
+ // The root of the resulting CensusTreeNode tree.
+ this._root = null;
+
+ // The stack of CensusTreeNodes that we are in the process of building while
+ // walking the census report.
+ this._nodeStack = [];
+
+ // To avoid unnecessary allocations, we reuse the same out parameter object
+ // passed to `makeCensusTreeNodeSubTree` every time we call it.
+ this._outParams = {
+ top: null,
+ bottom: null,
+ };
+
+ // The stack of `CensusTreeNodeCache`s that we use to aggregate many
+ // SavedFrame stacks into a single CensusTreeNode tree.
+ this._cacheStack = [new CensusTreeNodeCache()];
+
+ // The current index in the DFS of the census report tree.
+ this._index = -1;
+}
+
+CensusTreeNodeVisitor.prototype = Object.create(Visitor);
+
+/**
+ * Create the CensusTreeNode subtree for this sub-report and link it to the
+ * parent CensusTreeNode.
+ *
+ * @overrides Visitor.prototype.enter
+ */
+CensusTreeNodeVisitor.prototype.enter = function (breakdown, report, edge) {
+ this._index++;
+
+ const cache = this._cacheStack[this._cacheStack.length - 1];
+ makeCensusTreeNodeSubTree(breakdown, report, edge, cache, this._outParams);
+ const { top, bottom } = this._outParams;
+
+ if (!this._root) {
+ this._root = bottom;
+ } else if (bottom) {
+ addChild(this._nodeStack[this._nodeStack.length - 1], bottom);
+ }
+
+ this._cacheStack.push(new CensusTreeNodeCache());
+ this._nodeStack.push(top);
+};
+
+function values(cache) {
+ return Object.keys(cache).map(k => cache[k]);
+}
+
+function isNonEmpty(node) {
+ return (node.children !== undefined && node.children.length)
+ || node.bytes !== 0
+ || node.count !== 0;
+}
+
+/**
+ * We have finished adding children to the CensusTreeNode subtree for the
+ * current sub-report. Make sure that the children are sorted for every node in
+ * the subtree.
+ *
+ * @overrides Visitor.prototype.exit
+ */
+CensusTreeNodeVisitor.prototype.exit = function (breakdown, report, edge) {
+ // Ensure all children are sorted and have their counts/bytes aggregated. We
+ // only need to consider cache children here, because other children
+ // correspond to other sub-reports and we already fixed them up in an earlier
+ // invocation of `exit`.
+
+ function dfs(node, childrenCache) {
+ if (childrenCache) {
+ const childValues = values(childrenCache);
+ for (let i = 0, length = childValues.length; i < length; i++) {
+ dfs(childValues[i].node, childValues[i].children);
+ }
+ }
+
+ node.totalCount = node.count;
+ node.totalBytes = node.bytes;
+
+ if (node.children) {
+ // Prune empty leaves.
+ node.children = node.children.filter(isNonEmpty);
+
+ node.children.sort(compareByTotal);
+
+ for (let i = 0, length = node.children.length; i < length; i++) {
+ node.totalCount += node.children[i].totalCount;
+ node.totalBytes += node.children[i].totalBytes;
+ }
+ }
+ }
+
+ const top = this._nodeStack.pop();
+ const cache = this._cacheStack.pop();
+ dfs(top, cache);
+};
+
+/**
+ * @overrides Visitor.prototype.count
+ */
+CensusTreeNodeVisitor.prototype.count = function (breakdown, report, edge) {
+ const node = this._nodeStack[this._nodeStack.length - 1];
+ node.reportLeafIndex = this._index;
+
+ if (breakdown.count) {
+ node.count = report.count;
+ }
+
+ if (breakdown.bytes) {
+ node.bytes = report.bytes;
+ }
+};
+
+/**
+ * Get the root of the resulting CensusTreeNode tree.
+ *
+ * @returns {CensusTreeNode}
+ */
+CensusTreeNodeVisitor.prototype.root = function () {
+ if (!this._root) {
+ throw new Error("Attempt to get the root before walking the census report!");
+ }
+
+ if (this._nodeStack.length) {
+ throw new Error("Attempt to get the root while walking the census report!");
+ }
+
+ return this._root;
+};
+
+/**
+ * Create a single, uninitialized CensusTreeNode.
+ *
+ * @param {null|String|SavedFrame} name
+ */
+function CensusTreeNode(name) {
+ // Display name for this CensusTreeNode. Either null, a string, or a
+ // SavedFrame.
+ this.name = name;
+
+ // The number of bytes occupied by matching things in the heap snapshot.
+ this.bytes = 0;
+
+ // The sum of `this.bytes` and `child.totalBytes` for each child in
+ // `this.children`.
+ this.totalBytes = 0;
+
+ // The number of things in the heap snapshot that match this node in the
+ // census tree.
+ this.count = 0;
+
+ // The sum of `this.count` and `child.totalCount` for each child in
+ // `this.children`.
+ this.totalCount = 0;
+
+ // An array of this node's children, or undefined if it has no children.
+ this.children = undefined;
+
+ // The unique ID of this node.
+ this.id = ++censusTreeNodeIdCounter;
+
+ // If present, the unique ID of this node's parent. If this node does not have
+ // a parent, then undefined.
+ this.parent = undefined;
+
+ // The `reportLeafIndex` property allows mapping a CensusTreeNode node back to
+ // a leaf in the census report it was generated from. It is always one of the
+ // following variants:
+ //
+ // * A `Number` index pointing a leaf report in a pre-order DFS traversal of
+ // this CensusTreeNode's census report.
+ //
+ // * A `Set` object containing such indices, when this is part of an inverted
+ // CensusTreeNode tree and multiple leaves in the report map onto this node.
+ //
+ // * Finally, `undefined` when no leaves in the census report correspond with
+ // this node.
+ //
+ // The first and third cases are the common cases. The second case is rather
+ // uncommon, and to avoid doubling the number of allocations when creating
+ // CensusTreeNode trees, and objects that get structured cloned when sending
+ // such trees from the HeapAnalysesWorker to the main thread, we only allocate
+ // a Set object once a node actually does have multiple leaves it corresponds
+ // to.
+ this.reportLeafIndex = undefined;
+}
+
+CensusTreeNode.prototype = null;
+
+/**
+ * Compare the given nodes by their `totalBytes` properties, and breaking ties
+ * with the `totalCount`, `bytes`, and `count` properties (in that order).
+ *
+ * @param {CensusTreeNode} node1
+ * @param {CensusTreeNode} node2
+ *
+ * @returns {Number}
+ * A number suitable for using with Array.prototype.sort.
+ */
+function compareByTotal(node1, node2) {
+ return Math.abs(node2.totalBytes) - Math.abs(node1.totalBytes)
+ || Math.abs(node2.totalCount) - Math.abs(node1.totalCount)
+ || Math.abs(node2.bytes) - Math.abs(node1.bytes)
+ || Math.abs(node2.count) - Math.abs(node1.count);
+}
+
+/**
+ * Compare the given nodes by their `bytes` properties, and breaking ties with
+ * the `count`, `totalBytes`, and `totalCount` properties (in that order).
+ *
+ * @param {CensusTreeNode} node1
+ * @param {CensusTreeNode} node2
+ *
+ * @returns {Number}
+ * A number suitable for using with Array.prototype.sort.
+ */
+function compareBySelf(node1, node2) {
+ return Math.abs(node2.bytes) - Math.abs(node1.bytes)
+ || Math.abs(node2.count) - Math.abs(node1.count)
+ || Math.abs(node2.totalBytes) - Math.abs(node1.totalBytes)
+ || Math.abs(node2.totalCount) - Math.abs(node1.totalCount);
+}
+
+/**
+ * Given a parent cache value from a tree we are building and a child node from
+ * a tree we are basing the new tree off of, if we already have a corresponding
+ * node in the parent's children cache, merge this node's counts with
+ * it. Otherwise, create the corresponding node, add it to the parent's children
+ * cache, and create the parent->child edge.
+ *
+ * @param {CensusTreeNodeCacheValue} parentCachevalue
+ * @param {CensusTreeNode} node
+ *
+ * @returns {CensusTreeNodeCacheValue}
+ * The new or extant child node's corresponding cache value.
+ */
+function insertOrMergeNode(parentCacheValue, node) {
+ if (!parentCacheValue.children) {
+ parentCacheValue.children = new CensusTreeNodeCache();
+ }
+
+ let val = CensusTreeNodeCache.lookupNode(parentCacheValue.children, node);
+
+ if (val) {
+ // When inverting, it is possible that multiple leaves in the census report
+ // get merged into a single CensusTreeNode node. When this occurs, switch
+ // from a single index to a set of indices.
+ if (val.node.reportLeafIndex !== undefined &&
+ val.node.reportLeafIndex !== node.reportLeafIndex) {
+ if (typeof val.node.reportLeafIndex === "number") {
+ const oldIndex = val.node.reportLeafIndex;
+ val.node.reportLeafIndex = new Set();
+ val.node.reportLeafIndex.add(oldIndex);
+ val.node.reportLeafIndex.add(node.reportLeafIndex);
+ } else {
+ val.node.reportLeafIndex.add(node.reportLeafIndex);
+ }
+ }
+
+ val.node.count += node.count;
+ val.node.bytes += node.bytes;
+ } else {
+ val = new CensusTreeNodeCacheValue();
+
+ val.node = new CensusTreeNode(node.name);
+ val.node.reportLeafIndex = node.reportLeafIndex;
+ val.node.count = node.count;
+ val.node.totalCount = node.totalCount;
+ val.node.bytes = node.bytes;
+ val.node.totalBytes = node.totalBytes;
+
+ addChild(parentCacheValue.node, val.node);
+ CensusTreeNodeCache.insertNode(parentCacheValue.children, val);
+ }
+
+ return val;
+}
+
+/**
+ * Given an un-inverted CensusTreeNode tree, return the corresponding inverted
+ * CensusTreeNode tree. The input tree is not modified. The resulting inverted
+ * tree is sorted by self bytes rather than by total bytes.
+ *
+ * @param {CensusTreeNode} tree
+ * The un-inverted tree.
+ *
+ * @returns {CensusTreeNode}
+ * The corresponding inverted tree.
+ */
+function invert(tree) {
+ const inverted = new CensusTreeNodeCacheValue();
+ inverted.node = new CensusTreeNode(null);
+
+ // Do a depth-first search of the un-inverted tree. As we reach each leaf,
+ // take the path from the old root to the leaf, reverse that path, and add it
+ // to the new, inverted tree's root.
+
+ const path = [];
+ (function addInvertedPaths(node) {
+ path.push(node);
+
+ if (node.children) {
+ for (let i = 0, length = node.children.length; i < length; i++) {
+ addInvertedPaths(node.children[i]);
+ }
+ } else {
+ // We found a leaf node, add the reverse path to the inverted tree.
+ let currentCacheValue = inverted;
+ for (let i = path.length - 1; i >= 0; i--) {
+ currentCacheValue = insertOrMergeNode(currentCacheValue, path[i]);
+ }
+ }
+
+ path.pop();
+ }(tree));
+
+ // Ensure that the root node always has the totals.
+ inverted.node.totalBytes = tree.totalBytes;
+ inverted.node.totalCount = tree.totalCount;
+
+ return inverted.node;
+}
+
+/**
+ * Given a CensusTreeNode tree and predicate function, create the tree
+ * containing only the nodes in any path `(node_0, node_1, ..., node_n-1)` in
+ * the given tree where `predicate(node_j)` is true for `0 <= j < n`, `node_0`
+ * is the given tree's root, and `node_n-1` is a leaf in the given tree. The
+ * given tree is left unmodified.
+ *
+ * @param {CensusTreeNode} tree
+ * @param {Function} predicate
+ *
+ * @returns {CensusTreeNode}
+ */
+function filter(tree, predicate) {
+ const filtered = new CensusTreeNodeCacheValue();
+ filtered.node = new CensusTreeNode(null);
+
+ // Do a DFS over the given tree. If the predicate returns true for any node,
+ // add that node and its whole subtree to the filtered tree.
+
+ const path = [];
+ let match = false;
+
+ function addMatchingNodes(node) {
+ path.push(node);
+
+ let oldMatch = match;
+ if (!match && predicate(node)) {
+ match = true;
+ }
+
+ if (node.children) {
+ for (let i = 0, length = node.children.length; i < length; i++) {
+ addMatchingNodes(node.children[i]);
+ }
+ } else if (match) {
+ // We found a matching leaf node, add it to the filtered tree.
+ let currentCacheValue = filtered;
+ for (let i = 0, length = path.length; i < length; i++) {
+ currentCacheValue = insertOrMergeNode(currentCacheValue, path[i]);
+ }
+ }
+
+ match = oldMatch;
+ path.pop();
+ }
+
+ if (tree.children) {
+ for (let i = 0, length = tree.children.length; i < length; i++) {
+ addMatchingNodes(tree.children[i]);
+ }
+ }
+
+ filtered.node.count = tree.count;
+ filtered.node.totalCount = tree.totalCount;
+ filtered.node.bytes = tree.bytes;
+ filtered.node.totalBytes = tree.totalBytes;
+
+ return filtered.node;
+}
+
+/**
+ * Given a filter string, return a predicate function that takes a node and
+ * returns true iff the node matches the filter.
+ *
+ * @param {String} filterString
+ * @returns {Function}
+ */
+function makeFilterPredicate(filterString) {
+ return function (node) {
+ if (!node.name) {
+ return false;
+ }
+
+ if (isSavedFrame(node.name)) {
+ return node.name.source.includes(filterString)
+ || (node.name.functionDisplayName || "").includes(filterString)
+ || (node.name.asyncCause || "").includes(filterString);
+ }
+
+ return String(node.name).includes(filterString);
+ };
+}
+
+/**
+ * Takes a report from a census (`dbg.memory.takeCensus()`) and the breakdown
+ * used to generate the census and returns a structure used to render
+ * a tree to display the data.
+ *
+ * Returns a recursive "CensusTreeNode" object, looking like:
+ *
+ * CensusTreeNode = {
+ * // `children` if it exists, is sorted by `bytes`, if they are leaf nodes.
+ * children: ?[<CensusTreeNode...>],
+ * name: <?String>
+ * count: <?Number>
+ * bytes: <?Number>
+ * id: <?Number>
+ * parent: <?Number>
+ * }
+ *
+ * @param {Object} breakdown
+ * The breakdown used to generate the census report.
+ *
+ * @param {Object} report
+ * The census report generated with the specified breakdown.
+ *
+ * @param {Object} options
+ * Configuration options.
+ * - invert: Whether to invert the resulting tree or not. Defaults to
+ * false, ie uninverted.
+ *
+ * @returns {CensusTreeNode}
+ */
+exports.censusReportToCensusTreeNode = function (breakdown, report,
+ options = {
+ invert: false,
+ filter: null
+ }) {
+ // Reset the counter so that turning the same census report into a
+ // CensusTreeNode tree repeatedly is idempotent.
+ censusTreeNodeIdCounter = 0;
+
+ const visitor = new CensusTreeNodeVisitor();
+ walk(breakdown, report, visitor);
+ let result = visitor.root();
+
+ if (options.invert) {
+ result = invert(result);
+ }
+
+ if (typeof options.filter === "string") {
+ result = filter(result, makeFilterPredicate(options.filter));
+ }
+
+ // If the report is a delta report that was generated by diffing two other
+ // reports, make sure to use the basis totals rather than the totals of the
+ // difference.
+ if (typeof report[basisTotalBytes] === "number") {
+ result.totalBytes = report[basisTotalBytes];
+ result.totalCount = report[basisTotalCount];
+ }
+
+ // Inverting and filtering could have messed up the sort order, so do a
+ // depth-first search of the tree and ensure that siblings are sorted.
+ const comparator = options.invert ? compareBySelf : compareByTotal;
+ (function ensureSorted(node) {
+ if (node.children) {
+ node.children.sort(comparator);
+ for (let i = 0, length = node.children.length; i < length; i++) {
+ ensureSorted(node.children[i]);
+ }
+ }
+ }(result));
+
+ return result;
+};
diff --git a/devtools/shared/heapsnapshot/generate-core-dump-sources.sh b/devtools/shared/heapsnapshot/generate-core-dump-sources.sh
new file mode 100755
index 000000000..97e492ff0
--- /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 000000000..d020da727
--- /dev/null
+++ b/devtools/shared/heapsnapshot/moz.build
@@ -0,0 +1,62 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files('**'):
+ BUG_COMPONENT = ('Firefox', 'Developer Tools: Memory')
+
+if CONFIG['ENABLE_TESTS']:
+ DIRS += ['tests/gtest']
+
+XPCSHELL_TESTS_MANIFESTS += [ 'tests/unit/xpcshell.ini' ]
+MOCHITEST_MANIFESTS += [ 'tests/mochitest/mochitest.ini' ]
+MOCHITEST_CHROME_MANIFESTS += [ 'tests/mochitest/chrome.ini' ]
+
+EXPORTS.mozilla.devtools += [
+ 'AutoMemMap.h',
+ 'CoreDump.pb.h',
+ 'DeserializedNode.h',
+ 'DominatorTree.h',
+ 'FileDescriptorOutputStream.h',
+ 'HeapSnapshot.h',
+ 'HeapSnapshotTempFileHelperChild.h',
+ 'HeapSnapshotTempFileHelperParent.h',
+ 'ZeroCopyNSIOutputStream.h',
+]
+
+IPDL_SOURCES += [
+ 'PHeapSnapshotTempFileHelper.ipdl',
+]
+
+include('/ipc/chromium/chromium-config.mozbuild')
+
+SOURCES += [
+ 'AutoMemMap.cpp',
+ 'CoreDump.pb.cc',
+ 'DeserializedNode.cpp',
+ 'DominatorTree.cpp',
+ 'FileDescriptorOutputStream.cpp',
+ 'HeapSnapshot.cpp',
+ 'HeapSnapshotTempFileHelperParent.cpp',
+ 'ZeroCopyNSIOutputStream.cpp',
+]
+
+# Disable RTTI in google protocol buffer
+DEFINES['GOOGLE_PROTOBUF_NO_RTTI'] = True
+
+FINAL_LIBRARY = 'xul'
+
+DevToolsModules(
+ 'census-tree-node.js',
+ 'CensusUtils.js',
+ 'DominatorTreeNode.js',
+ 'HeapAnalysesClient.js',
+ 'HeapAnalysesWorker.js',
+ 'HeapSnapshotFileUtils.js',
+ 'shortest-paths.js',
+)
+
+if CONFIG['GNU_CXX']:
+ CXXFLAGS += ['-Wno-error=shadow']
diff --git a/devtools/shared/heapsnapshot/shortest-paths.js b/devtools/shared/heapsnapshot/shortest-paths.js
new file mode 100644
index 000000000..2d97b7de9
--- /dev/null
+++ b/devtools/shared/heapsnapshot/shortest-paths.js
@@ -0,0 +1,91 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+/**
+ * Compress a set of paths leading to `target` into a single graph, returned as
+ * a set of nodes and a set of edges.
+ *
+ * @param {NodeId} target
+ * The target node passed to `HeapSnapshot.computeShortestPaths`.
+ *
+ * @param {Array<Path>} paths
+ * An array of paths to `target`, as returned by
+ * `HeapSnapshot.computeShortestPaths`.
+ *
+ * @returns {Object}
+ * An object with two properties:
+ * - edges: An array of unique objects of the form:
+ * {
+ * from: <node ID>,
+ * to: <node ID>,
+ * name: <string or null>
+ * }
+ * - nodes: An array of unique node IDs. Every `from` and `to` id is
+ * guaranteed to be in this array exactly once.
+ */
+exports.deduplicatePaths = function (target, paths) {
+ // Use this structure to de-duplicate edges among many retaining paths from
+ // start to target.
+ //
+ // Map<FromNodeId, Map<ToNodeId, Set<EdgeName>>>
+ const deduped = new Map();
+
+ function insert(from, to, name) {
+ let toMap = deduped.get(from);
+ if (!toMap) {
+ toMap = new Map();
+ deduped.set(from, toMap);
+ }
+
+ let nameSet = toMap.get(to);
+ if (!nameSet) {
+ nameSet = new Set();
+ toMap.set(to, nameSet);
+ }
+
+ nameSet.add(name);
+ }
+
+ outer: for (let path of paths) {
+ const pathLength = path.length;
+
+ // Check for duplicate predecessors in the path, and skip paths that contain
+ // them.
+ const predecessorsSeen = new Set();
+ predecessorsSeen.add(target);
+ for (let i = 0; i < pathLength; i++) {
+ if (predecessorsSeen.has(path[i].predecessor)) {
+ continue outer;
+ }
+ predecessorsSeen.add(path[i].predecessor);
+ }
+
+ for (let i = 0; i < pathLength - 1; i++) {
+ insert(path[i].predecessor, path[i + 1].predecessor, path[i].edge);
+ }
+
+ insert(path[pathLength - 1].predecessor, target, path[pathLength - 1].edge);
+ }
+
+ const nodes = [target];
+ const edges = [];
+
+ for (let [from, toMap] of deduped) {
+ // If the second/third/etc shortest path contains the `target` anywhere
+ // other than the very last node, we could accidentally put the `target` in
+ // `nodes` more than once.
+ if (from !== target) {
+ nodes.push(from);
+ }
+
+ for (let [to, edgeNameSet] of toMap) {
+ for (let name of edgeNameSet) {
+ edges.push({ from, to, name });
+ }
+ }
+ }
+
+ return { nodes, edges };
+};
diff --git a/devtools/shared/heapsnapshot/tests/gtest/DeserializedNodeUbiNodes.cpp b/devtools/shared/heapsnapshot/tests/gtest/DeserializedNodeUbiNodes.cpp
new file mode 100644
index 000000000..e236a0acf
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/DeserializedNodeUbiNodes.cpp
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test that the `JS::ubi::Node`s we create from
+// `mozilla::devtools::DeserializedNode` instances look and behave as we would
+// like.
+
+#include "DevTools.h"
+#include "js/TypeDecls.h"
+#include "mozilla/devtools/DeserializedNode.h"
+
+using testing::Field;
+using testing::ReturnRef;
+
+// A mock DeserializedNode for testing.
+struct MockDeserializedNode : public DeserializedNode
+{
+ MockDeserializedNode(NodeId id, const char16_t* typeName, uint64_t size)
+ : DeserializedNode(id, typeName, size)
+ { }
+
+ bool addEdge(DeserializedEdge&& edge)
+ {
+ return edges.append(Move(edge));
+ }
+
+ MOCK_METHOD1(getEdgeReferent, JS::ubi::Node(const DeserializedEdge&));
+};
+
+size_t fakeMallocSizeOf(const void*) {
+ EXPECT_TRUE(false);
+ MOZ_ASSERT_UNREACHABLE("fakeMallocSizeOf should never be called because "
+ "DeserializedNodes report the deserialized size.");
+ return 0;
+}
+
+DEF_TEST(DeserializedNodeUbiNodes, {
+ const char16_t* typeName = u"TestTypeName";
+ const char* className = "MyObjectClassName";
+ const char* filename = "my-cool-filename.js";
+
+ NodeId id = uint64_t(1) << 33;
+ uint64_t size = uint64_t(1) << 60;
+ MockDeserializedNode mocked(id, typeName, size);
+ mocked.coarseType = JS::ubi::CoarseType::Script;
+ mocked.jsObjectClassName = className;
+ mocked.scriptFilename = filename;
+
+ DeserializedNode& deserialized = mocked;
+ JS::ubi::Node ubi(&deserialized);
+
+ // Test the ubi::Node accessors.
+
+ EXPECT_EQ(size, ubi.size(fakeMallocSizeOf));
+ EXPECT_EQ(typeName, ubi.typeName());
+ EXPECT_EQ(JS::ubi::CoarseType::Script, ubi.coarseType());
+ EXPECT_EQ(id, ubi.identifier());
+ EXPECT_FALSE(ubi.isLive());
+ EXPECT_EQ(ubi.jsObjectClassName(), className);
+ EXPECT_EQ(ubi.scriptFilename(), filename);
+
+ // Test the ubi::Node's edges.
+
+ UniquePtr<DeserializedNode> referent1(new MockDeserializedNode(1,
+ nullptr,
+ 10));
+ DeserializedEdge edge1(referent1->id);
+ mocked.addEdge(Move(edge1));
+ EXPECT_CALL(mocked, getEdgeReferent(EdgeTo(referent1->id)))
+ .Times(1)
+ .WillOnce(Return(JS::ubi::Node(referent1.get())));
+
+ UniquePtr<DeserializedNode> referent2(new MockDeserializedNode(2,
+ nullptr,
+ 20));
+ DeserializedEdge edge2(referent2->id);
+ mocked.addEdge(Move(edge2));
+ EXPECT_CALL(mocked, getEdgeReferent(EdgeTo(referent2->id)))
+ .Times(1)
+ .WillOnce(Return(JS::ubi::Node(referent2.get())));
+
+ UniquePtr<DeserializedNode> referent3(new MockDeserializedNode(3,
+ nullptr,
+ 30));
+ DeserializedEdge edge3(referent3->id);
+ mocked.addEdge(Move(edge3));
+ EXPECT_CALL(mocked, getEdgeReferent(EdgeTo(referent3->id)))
+ .Times(1)
+ .WillOnce(Return(JS::ubi::Node(referent3.get())));
+
+ auto range = ubi.edges(cx);
+ ASSERT_TRUE(!!range);
+
+ for ( ; !range->empty(); range->popFront()) {
+ // Nothing to do here. This loop ensures that we get each edge referent
+ // that we expect above.
+ }
+ });
diff --git a/devtools/shared/heapsnapshot/tests/gtest/DeserializedStackFrameUbiStackFrames.cpp b/devtools/shared/heapsnapshot/tests/gtest/DeserializedStackFrameUbiStackFrames.cpp
new file mode 100644
index 000000000..72e363934
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/DeserializedStackFrameUbiStackFrames.cpp
@@ -0,0 +1,91 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test that the `JS::ubi::StackFrame`s we create from
+// `mozilla::devtools::DeserializedStackFrame` instances look and behave as we would
+// like.
+
+#include "DevTools.h"
+#include "js/TypeDecls.h"
+#include "mozilla/devtools/DeserializedNode.h"
+
+using testing::Field;
+using testing::ReturnRef;
+
+// A mock DeserializedStackFrame for testing.
+struct MockDeserializedStackFrame : public DeserializedStackFrame
+{
+ MockDeserializedStackFrame() : DeserializedStackFrame() { }
+};
+
+DEF_TEST(DeserializedStackFrameUbiStackFrames, {
+ StackFrameId id = uint64_t(1) << 42;
+ uint32_t line = 1337;
+ uint32_t column = 9; // 3 space tabs!?
+ const char16_t* source = u"my-javascript-file.js";
+ const char16_t* functionDisplayName = u"myFunctionName";
+
+ MockDeserializedStackFrame mocked;
+ mocked.id = id;
+ mocked.line = line;
+ mocked.column = column;
+ mocked.source = source;
+ mocked.functionDisplayName = functionDisplayName;
+
+ DeserializedStackFrame& deserialized = mocked;
+ JS::ubi::StackFrame ubiFrame(&deserialized);
+
+ // Test the JS::ubi::StackFrame accessors.
+
+ EXPECT_EQ(id, ubiFrame.identifier());
+ EXPECT_EQ(JS::ubi::StackFrame(), ubiFrame.parent());
+ EXPECT_EQ(line, ubiFrame.line());
+ EXPECT_EQ(column, ubiFrame.column());
+ EXPECT_EQ(JS::ubi::AtomOrTwoByteChars(source), ubiFrame.source());
+ EXPECT_EQ(JS::ubi::AtomOrTwoByteChars(functionDisplayName),
+ ubiFrame.functionDisplayName());
+ EXPECT_FALSE(ubiFrame.isSelfHosted(cx));
+ EXPECT_FALSE(ubiFrame.isSystem());
+
+ JS::RootedObject savedFrame(cx);
+ EXPECT_TRUE(ubiFrame.constructSavedFrameStack(cx, &savedFrame));
+
+ uint32_t frameLine;
+ ASSERT_EQ(JS::SavedFrameResult::Ok, JS::GetSavedFrameLine(cx, savedFrame, &frameLine));
+ EXPECT_EQ(line, frameLine);
+
+ uint32_t frameColumn;
+ ASSERT_EQ(JS::SavedFrameResult::Ok, JS::GetSavedFrameColumn(cx, savedFrame, &frameColumn));
+ EXPECT_EQ(column, frameColumn);
+
+ JS::RootedObject parent(cx);
+ ASSERT_EQ(JS::SavedFrameResult::Ok, JS::GetSavedFrameParent(cx, savedFrame, &parent));
+ EXPECT_EQ(nullptr, parent);
+
+ ASSERT_EQ(NS_strlen(source), 21U);
+ char16_t sourceBuf[21] = {};
+
+ // Test when the length is shorter than the string length.
+ auto written = ubiFrame.source(RangedPtr<char16_t>(sourceBuf), 3);
+ EXPECT_EQ(written, 3U);
+ for (size_t i = 0; i < 3; i++) {
+ EXPECT_EQ(sourceBuf[i], source[i]);
+ }
+
+ written = ubiFrame.source(RangedPtr<char16_t>(sourceBuf), 21);
+ EXPECT_EQ(written, 21U);
+ for (size_t i = 0; i < 21; i++) {
+ EXPECT_EQ(sourceBuf[i], source[i]);
+ }
+
+ ASSERT_EQ(NS_strlen(functionDisplayName), 14U);
+ char16_t nameBuf[14] = {};
+
+ written = ubiFrame.functionDisplayName(RangedPtr<char16_t>(nameBuf), 14);
+ EXPECT_EQ(written, 14U);
+ for (size_t i = 0; i < 14; i++) {
+ EXPECT_EQ(nameBuf[i], functionDisplayName[i]);
+ }
+});
diff --git a/devtools/shared/heapsnapshot/tests/gtest/DevTools.h b/devtools/shared/heapsnapshot/tests/gtest/DevTools.h
new file mode 100644
index 000000000..6eb5cfe21
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/DevTools.h
@@ -0,0 +1,276 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_devtools_gtest_DevTools__
+#define mozilla_devtools_gtest_DevTools__
+
+#include "CoreDump.pb.h"
+#include "jsapi.h"
+#include "jspubtd.h"
+#include "nsCRTGlue.h"
+
+#include "gtest/gtest.h"
+#include "gmock/gmock.h"
+#include "mozilla/devtools/HeapSnapshot.h"
+#include "mozilla/dom/ChromeUtils.h"
+#include "mozilla/CycleCollectedJSContext.h"
+#include "mozilla/Move.h"
+#include "js/Principals.h"
+#include "js/UbiNode.h"
+#include "js/UniquePtr.h"
+
+using namespace mozilla;
+using namespace mozilla::devtools;
+using namespace mozilla::dom;
+using namespace testing;
+
+// GTest fixture class that all of our tests derive from.
+struct DevTools : public ::testing::Test {
+ bool _initialized;
+ JSContext* cx;
+ JSCompartment* compartment;
+ JS::Zone* zone;
+ JS::PersistentRootedObject global;
+
+ DevTools()
+ : _initialized(false),
+ cx(nullptr)
+ { }
+
+ virtual void SetUp() {
+ MOZ_ASSERT(!_initialized);
+
+ cx = getContext();
+ if (!cx)
+ return;
+
+ JS_BeginRequest(cx);
+
+ global.init(cx, createGlobal());
+ if (!global)
+ return;
+ JS_EnterCompartment(cx, global);
+
+ compartment = js::GetContextCompartment(cx);
+ zone = js::GetContextZone(cx);
+
+ _initialized = true;
+ }
+
+ JSContext* getContext() {
+ return CycleCollectedJSContext::Get()->Context();
+ }
+
+ static void reportError(JSContext* cx, const char* message, JSErrorReport* report) {
+ fprintf(stderr, "%s:%u:%s\n",
+ report->filename ? report->filename : "<no filename>",
+ (unsigned int) report->lineno,
+ message);
+ }
+
+ static const JSClass* getGlobalClass() {
+ static const JSClassOps globalClassOps = {
+ nullptr, nullptr, nullptr, nullptr,
+ nullptr, nullptr, nullptr, nullptr,
+ nullptr, nullptr, nullptr,
+ JS_GlobalObjectTraceHook
+ };
+ static const JSClass globalClass = {
+ "global", JSCLASS_GLOBAL_FLAGS,
+ &globalClassOps
+ };
+ return &globalClass;
+ }
+
+ JSObject* createGlobal()
+ {
+ /* Create the global object. */
+ JS::RootedObject newGlobal(cx);
+ JS::CompartmentOptions options;
+ options.behaviors().setVersion(JSVERSION_LATEST);
+ newGlobal = JS_NewGlobalObject(cx, getGlobalClass(), nullptr,
+ JS::FireOnNewGlobalHook, options);
+ if (!newGlobal)
+ return nullptr;
+
+ JSAutoCompartment ac(cx, newGlobal);
+
+ /* Populate the global object with the standard globals, like Object and
+ Array. */
+ if (!JS_InitStandardClasses(cx, newGlobal))
+ return nullptr;
+
+ return newGlobal;
+ }
+
+ virtual void TearDown() {
+ _initialized = false;
+
+ if (global) {
+ JS_LeaveCompartment(cx, nullptr);
+ global = nullptr;
+ }
+ if (cx)
+ JS_EndRequest(cx);
+ }
+};
+
+
+// Helper to define a test and ensure that the fixture is initialized properly.
+#define DEF_TEST(name, body) \
+ TEST_F(DevTools, name) { \
+ ASSERT_TRUE(_initialized); \
+ body \
+ }
+
+
+// Fake JS::ubi::Node implementation
+class MOZ_STACK_CLASS FakeNode
+{
+public:
+ JS::ubi::EdgeVector edges;
+ JSCompartment* compartment;
+ JS::Zone* zone;
+ size_t size;
+
+ explicit FakeNode()
+ : edges(),
+ compartment(nullptr),
+ zone(nullptr),
+ size(1)
+ { }
+};
+
+namespace JS {
+namespace ubi {
+
+template<>
+class Concrete<FakeNode> : public Base
+{
+ const char16_t* typeName() const override {
+ return concreteTypeName;
+ }
+
+ js::UniquePtr<EdgeRange> edges(JSContext*, bool) const override {
+ return js::UniquePtr<EdgeRange>(js_new<PreComputedEdgeRange>(get().edges));
+ }
+
+ Size size(mozilla::MallocSizeOf) const override {
+ return get().size;
+ }
+
+ JS::Zone* zone() const override {
+ return get().zone;
+ }
+
+ JSCompartment* compartment() const override {
+ return get().compartment;
+ }
+
+protected:
+ explicit Concrete(FakeNode* ptr) : Base(ptr) { }
+ FakeNode& get() const { return *static_cast<FakeNode*>(ptr); }
+
+public:
+ static const char16_t concreteTypeName[];
+ static void construct(void* storage, FakeNode* ptr) {
+ new (storage) Concrete(ptr);
+ }
+};
+
+const char16_t Concrete<FakeNode>::concreteTypeName[] = u"FakeNode";
+
+} // namespace ubi
+} // namespace JS
+
+void AddEdge(FakeNode& node, FakeNode& referent, const char16_t* edgeName = nullptr) {
+ char16_t* ownedEdgeName = nullptr;
+ if (edgeName) {
+ ownedEdgeName = NS_strdup(edgeName);
+ ASSERT_NE(ownedEdgeName, nullptr);
+ }
+
+ JS::ubi::Edge edge(ownedEdgeName, &referent);
+ ASSERT_TRUE(node.edges.append(mozilla::Move(edge)));
+}
+
+
+// Custom GMock Matchers
+
+// Use the testing namespace to avoid static analysis failures in the gmock
+// matcher classes that get generated from MATCHER_P macros.
+namespace testing {
+
+// Ensure that given node has the expected number of edges.
+MATCHER_P2(EdgesLength, cx, expectedLength, "") {
+ auto edges = arg.edges(cx);
+ if (!edges)
+ return false;
+
+ int actualLength = 0;
+ for ( ; !edges->empty(); edges->popFront())
+ actualLength++;
+
+ return Matcher<int>(Eq(expectedLength))
+ .MatchAndExplain(actualLength, result_listener);
+}
+
+// Get the nth edge and match it with the given matcher.
+MATCHER_P3(Edge, cx, n, matcher, "") {
+ auto edges = arg.edges(cx);
+ if (!edges)
+ return false;
+
+ int i = 0;
+ for ( ; !edges->empty(); edges->popFront()) {
+ if (i == n) {
+ return Matcher<const JS::ubi::Edge&>(matcher)
+ .MatchAndExplain(edges->front(), result_listener);
+ }
+
+ i++;
+ }
+
+ return false;
+}
+
+// Ensures that two char16_t* strings are equal.
+MATCHER_P(UTF16StrEq, str, "") {
+ return NS_strcmp(arg, str) == 0;
+}
+
+MATCHER_P(UniqueUTF16StrEq, str, "") {
+ return NS_strcmp(arg.get(), str) == 0;
+}
+
+MATCHER(UniqueIsNull, "") {
+ return arg.get() == nullptr;
+}
+
+// Matches an edge whose referent is the node with the given id.
+MATCHER_P(EdgeTo, id, "") {
+ return Matcher<const DeserializedEdge&>(Field(&DeserializedEdge::referent, id))
+ .MatchAndExplain(arg, result_listener);
+}
+
+} // namespace testing
+
+
+// A mock `Writer` class to be used with testing `WriteHeapGraph`.
+class MockWriter : public CoreDumpWriter
+{
+public:
+ virtual ~MockWriter() override { }
+ MOCK_METHOD2(writeNode, bool(const JS::ubi::Node&, CoreDumpWriter::EdgePolicy));
+ MOCK_METHOD1(writeMetadata, bool(uint64_t));
+};
+
+void ExpectWriteNode(MockWriter& writer, FakeNode& node) {
+ EXPECT_CALL(writer, writeNode(Eq(JS::ubi::Node(&node)), _))
+ .Times(1)
+ .WillOnce(Return(true));
+}
+
+#endif // mozilla_devtools_gtest_DevTools__
diff --git a/devtools/shared/heapsnapshot/tests/gtest/DoesCrossCompartmentBoundaries.cpp b/devtools/shared/heapsnapshot/tests/gtest/DoesCrossCompartmentBoundaries.cpp
new file mode 100644
index 000000000..bc517d6d9
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/DoesCrossCompartmentBoundaries.cpp
@@ -0,0 +1,73 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test that heap snapshots cross compartment boundaries when expected.
+
+#include "DevTools.h"
+
+DEF_TEST(DoesCrossCompartmentBoundaries, {
+ // Create a new global to get a new compartment.
+ JS::CompartmentOptions options;
+ JS::RootedObject newGlobal(cx, JS_NewGlobalObject(cx,
+ getGlobalClass(),
+ nullptr,
+ JS::FireOnNewGlobalHook,
+ options));
+ ASSERT_TRUE(newGlobal);
+ JSCompartment* newCompartment = nullptr;
+ {
+ JSAutoCompartment ac(cx, newGlobal);
+ ASSERT_TRUE(JS_InitStandardClasses(cx, newGlobal));
+ newCompartment = js::GetContextCompartment(cx);
+ }
+ ASSERT_TRUE(newCompartment);
+ ASSERT_NE(newCompartment, compartment);
+
+ // Our set of target compartments is both the old and new compartments.
+ JS::CompartmentSet targetCompartments;
+ ASSERT_TRUE(targetCompartments.init());
+ ASSERT_TRUE(targetCompartments.put(compartment));
+ ASSERT_TRUE(targetCompartments.put(newCompartment));
+
+ FakeNode nodeA;
+ FakeNode nodeB;
+ FakeNode nodeC;
+ FakeNode nodeD;
+
+ nodeA.compartment = compartment;
+ nodeB.compartment = nullptr;
+ nodeC.compartment = newCompartment;
+ nodeD.compartment = nullptr;
+
+ AddEdge(nodeA, nodeB);
+ AddEdge(nodeA, nodeC);
+ AddEdge(nodeB, nodeD);
+
+ ::testing::NiceMock<MockWriter> writer;
+
+ // Should serialize nodeA, because it is in one of our target compartments.
+ ExpectWriteNode(writer, nodeA);
+
+ // Should serialize nodeB, because it doesn't belong to a compartment and is
+ // therefore assumed to be shared.
+ ExpectWriteNode(writer, nodeB);
+
+ // Should also serialize nodeC, which is in our target compartments, but a
+ // different compartment than A.
+ ExpectWriteNode(writer, nodeC);
+
+ // Should serialize nodeD because it's reachable via B and both nodes B and D
+ // don't belong to a specific compartment.
+ ExpectWriteNode(writer, nodeD);
+
+ JS::AutoCheckCannotGC noGC(cx);
+
+ ASSERT_TRUE(WriteHeapGraph(cx,
+ JS::ubi::Node(&nodeA),
+ writer,
+ /* wantNames = */ false,
+ &targetCompartments,
+ noGC));
+ });
diff --git a/devtools/shared/heapsnapshot/tests/gtest/DoesntCrossCompartmentBoundaries.cpp b/devtools/shared/heapsnapshot/tests/gtest/DoesntCrossCompartmentBoundaries.cpp
new file mode 100644
index 000000000..2fe5e6ace
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/DoesntCrossCompartmentBoundaries.cpp
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test that heap snapshots walk the compartment boundaries correctly.
+
+#include "DevTools.h"
+
+DEF_TEST(DoesntCrossCompartmentBoundaries, {
+ // Create a new global to get a new compartment.
+ JS::CompartmentOptions options;
+ JS::RootedObject newGlobal(cx, JS_NewGlobalObject(cx,
+ getGlobalClass(),
+ nullptr,
+ JS::FireOnNewGlobalHook,
+ options));
+ ASSERT_TRUE(newGlobal);
+ JSCompartment* newCompartment = nullptr;
+ {
+ JSAutoCompartment ac(cx, newGlobal);
+ ASSERT_TRUE(JS_InitStandardClasses(cx, newGlobal));
+ newCompartment = js::GetContextCompartment(cx);
+ }
+ ASSERT_TRUE(newCompartment);
+ ASSERT_NE(newCompartment, compartment);
+
+ // Our set of target compartments is only the pre-existing compartment and
+ // does not include the new compartment.
+ JS::CompartmentSet targetCompartments;
+ ASSERT_TRUE(targetCompartments.init());
+ ASSERT_TRUE(targetCompartments.put(compartment));
+
+ FakeNode nodeA;
+ FakeNode nodeB;
+ FakeNode nodeC;
+
+ nodeA.compartment = compartment;
+ nodeB.compartment = nullptr;
+ nodeC.compartment = newCompartment;
+
+ AddEdge(nodeA, nodeB);
+ AddEdge(nodeB, nodeC);
+
+ ::testing::NiceMock<MockWriter> writer;
+
+ // Should serialize nodeA, because it is in our target compartments.
+ ExpectWriteNode(writer, nodeA);
+
+ // Should serialize nodeB, because it doesn't belong to a compartment and is
+ // therefore assumed to be shared.
+ ExpectWriteNode(writer, nodeB);
+
+ // But we shouldn't ever serialize nodeC.
+
+ JS::AutoCheckCannotGC noGC(cx);
+
+ ASSERT_TRUE(WriteHeapGraph(cx,
+ JS::ubi::Node(&nodeA),
+ writer,
+ /* wantNames = */ false,
+ &targetCompartments,
+ noGC));
+ });
diff --git a/devtools/shared/heapsnapshot/tests/gtest/SerializesEdgeNames.cpp b/devtools/shared/heapsnapshot/tests/gtest/SerializesEdgeNames.cpp
new file mode 100644
index 000000000..be135dbb4
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/SerializesEdgeNames.cpp
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test that edge names get serialized correctly.
+
+#include "DevTools.h"
+
+using testing::Field;
+using testing::IsNull;
+using testing::Property;
+using testing::Return;
+
+DEF_TEST(SerializesEdgeNames, {
+ FakeNode node;
+ FakeNode referent;
+
+ const char16_t edgeName[] = u"edge name";
+ const char16_t emptyStr[] = u"";
+
+ AddEdge(node, referent, edgeName);
+ AddEdge(node, referent, emptyStr);
+ AddEdge(node, referent, nullptr);
+
+ ::testing::NiceMock<MockWriter> writer;
+
+ // Should get the node with edges once.
+ EXPECT_CALL(
+ writer,
+ writeNode(AllOf(EdgesLength(cx, 3),
+ Edge(cx, 0, Field(&JS::ubi::Edge::name,
+ UniqueUTF16StrEq(edgeName))),
+ Edge(cx, 1, Field(&JS::ubi::Edge::name,
+ UniqueUTF16StrEq(emptyStr))),
+ Edge(cx, 2, Field(&JS::ubi::Edge::name,
+ UniqueIsNull()))),
+ _)
+ )
+ .Times(1)
+ .WillOnce(Return(true));
+
+ // Should get the referent node that doesn't have any edges once.
+ ExpectWriteNode(writer, referent);
+
+ JS::AutoCheckCannotGC noGC(cx);
+ ASSERT_TRUE(WriteHeapGraph(cx,
+ JS::ubi::Node(&node),
+ writer,
+ /* wantNames = */ true,
+ /* zones = */ nullptr,
+ noGC));
+ });
diff --git a/devtools/shared/heapsnapshot/tests/gtest/SerializesEverythingInHeapGraphOnce.cpp b/devtools/shared/heapsnapshot/tests/gtest/SerializesEverythingInHeapGraphOnce.cpp
new file mode 100644
index 000000000..475442df8
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/SerializesEverythingInHeapGraphOnce.cpp
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test that everything in the heap graph gets serialized once, and only once.
+
+#include "DevTools.h"
+
+DEF_TEST(SerializesEverythingInHeapGraphOnce, {
+ FakeNode nodeA;
+ FakeNode nodeB;
+ FakeNode nodeC;
+ FakeNode nodeD;
+
+ AddEdge(nodeA, nodeB);
+ AddEdge(nodeB, nodeC);
+ AddEdge(nodeC, nodeD);
+ AddEdge(nodeD, nodeA);
+
+ ::testing::NiceMock<MockWriter> writer;
+
+ // Should serialize each node once.
+ ExpectWriteNode(writer, nodeA);
+ ExpectWriteNode(writer, nodeB);
+ ExpectWriteNode(writer, nodeC);
+ ExpectWriteNode(writer, nodeD);
+
+ JS::AutoCheckCannotGC noGC(cx);
+
+ ASSERT_TRUE(WriteHeapGraph(cx,
+ JS::ubi::Node(&nodeA),
+ writer,
+ /* wantNames = */ false,
+ /* zones = */ nullptr,
+ noGC));
+ });
diff --git a/devtools/shared/heapsnapshot/tests/gtest/SerializesTypeNames.cpp b/devtools/shared/heapsnapshot/tests/gtest/SerializesTypeNames.cpp
new file mode 100644
index 000000000..a259c297b
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/SerializesTypeNames.cpp
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test that a ubi::Node's typeName gets properly serialized into a core dump.
+
+#include "DevTools.h"
+
+using testing::Property;
+using testing::Return;
+
+DEF_TEST(SerializesTypeNames, {
+ FakeNode node;
+
+ ::testing::NiceMock<MockWriter> writer;
+ EXPECT_CALL(writer, writeNode(Property(&JS::ubi::Node::typeName,
+ UTF16StrEq(u"FakeNode")),
+ _))
+ .Times(1)
+ .WillOnce(Return(true));
+
+ JS::AutoCheckCannotGC noGC(cx);
+ ASSERT_TRUE(WriteHeapGraph(cx,
+ JS::ubi::Node(&node),
+ writer,
+ /* wantNames = */ true,
+ /* zones = */ nullptr,
+ noGC));
+ });
diff --git a/devtools/shared/heapsnapshot/tests/gtest/moz.build b/devtools/shared/heapsnapshot/tests/gtest/moz.build
new file mode 100644
index 000000000..08c31e47c
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/gtest/moz.build
@@ -0,0 +1,31 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, you can obtain one at http://mozilla.org/MPL/2.0/.
+
+Library('devtoolstests')
+
+LOCAL_INCLUDES += [
+ '../..',
+]
+
+UNIFIED_SOURCES = [
+ 'DeserializedNodeUbiNodes.cpp',
+ 'DeserializedStackFrameUbiStackFrames.cpp',
+ 'DoesCrossCompartmentBoundaries.cpp',
+ 'DoesntCrossCompartmentBoundaries.cpp',
+ 'SerializesEdgeNames.cpp',
+ 'SerializesEverythingInHeapGraphOnce.cpp',
+ 'SerializesTypeNames.cpp',
+]
+
+if CONFIG['GNU_CXX']:
+ CXXFLAGS += ['-Wno-error=shadow']
+
+# THE MOCK_METHOD2 macro from gtest triggers this clang warning and it's hard
+# to work around, so we just ignore it.
+if CONFIG['CLANG_CXX']:
+ CXXFLAGS += ['-Wno-inconsistent-missing-override']
+
+FINAL_LIBRARY = 'xul-gtest'
diff --git a/devtools/shared/heapsnapshot/tests/mochitest/chrome.ini b/devtools/shared/heapsnapshot/tests/mochitest/chrome.ini
new file mode 100644
index 000000000..497b6fe37
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/mochitest/chrome.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+tags = devtools devtools-memory
+skip-if = os == 'android'
+support-files =
+
+[test_DominatorTree_01.html]
+[test_SaveHeapSnapshot.html]
+
diff --git a/devtools/shared/heapsnapshot/tests/mochitest/mochitest.ini b/devtools/shared/heapsnapshot/tests/mochitest/mochitest.ini
new file mode 100644
index 000000000..5e7aa8d10
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/mochitest/mochitest.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+tags = devtools devtools-memory
+support-files =
+
+[test_saveHeapSnapshot_e10s_01.html]
+
diff --git a/devtools/shared/heapsnapshot/tests/mochitest/test_DominatorTree_01.html b/devtools/shared/heapsnapshot/tests/mochitest/test_DominatorTree_01.html
new file mode 100644
index 000000000..1f9d8c080
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/mochitest/test_DominatorTree_01.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Sanity test that we can compute dominator trees from a heap snapshot in a web window.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>ChromeUtils.saveHeapSnapshot test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script>
+SimpleTest.waitForExplicitFinish();
+window.onload = function() {
+ const path = ChromeUtils.saveHeapSnapshot({ runtime: true });
+ const snapshot = ChromeUtils.readHeapSnapshot(path);
+
+ const dominatorTree = snapshot.computeDominatorTree();
+ ok(dominatorTree);
+ ok(dominatorTree instanceof DominatorTree);
+
+ let threw = false;
+ try {
+ new DominatorTree();
+ } catch (e) {
+ threw = true;
+ }
+ ok(threw, "Constructor shouldn't be usable");
+
+ SimpleTest.finish();
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/shared/heapsnapshot/tests/mochitest/test_SaveHeapSnapshot.html b/devtools/shared/heapsnapshot/tests/mochitest/test_SaveHeapSnapshot.html
new file mode 100644
index 000000000..f150a99c7
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/mochitest/test_SaveHeapSnapshot.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1024774 - Sanity test that we can take a heap snapshot in a web window.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>ChromeUtils.saveHeapSnapshot test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script>
+SimpleTest.waitForExplicitFinish();
+window.onload = function() {
+ ok(ChromeUtils, "The ChromeUtils interface should be exposed in chrome windows.");
+ ChromeUtils.saveHeapSnapshot({ runtime: true });
+ ok(true, "Should save a heap snapshot and shouldn't throw.");
+ SimpleTest.finish();
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/shared/heapsnapshot/tests/mochitest/test_saveHeapSnapshot_e10s_01.html b/devtools/shared/heapsnapshot/tests/mochitest/test_saveHeapSnapshot_e10s_01.html
new file mode 100644
index 000000000..15f88f8e0
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/mochitest/test_saveHeapSnapshot_e10s_01.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<!--
+Bug 1201597 - Sanity test that we can take a heap snapshot in an e10s child process.
+-->
+<html>
+<head>
+ <title>saveHeapSnapshot in e10s child processes</title>
+ <script type="application/javascript"
+ src="/tests/SimpleTest/SimpleTest.js">
+ </script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <script type="application/javascript">
+ window.onerror = function (msg, url, line, col, err) {
+ ok(false, "@" + url + ":" + line + ":" + col + ": " + msg + "\n" + err.stack);
+ };
+
+ SimpleTest.waitForExplicitFinish();
+
+ var childFrameURL = "data:text/html,<!DOCTYPE HTML><html><body></body></html>";
+
+ // This function is stringified and loaded in the child process as a frame
+ // script.
+ function childFrameScript() {
+ try {
+ ChromeUtils.saveHeapSnapshot({ runtime: true });
+ } catch (err) {
+ sendAsyncMessage("testSaveHeapSnapshot:error",
+ { error: err.toString() });
+ return;
+ }
+
+ sendAsyncMessage("testSaveHeapSnapshot:done", {});
+ }
+
+ // Kick everything off on load.
+ window.onload = function () {
+ info("window.onload fired");
+ SpecialPowers.addPermission("browser", true, document);
+ SpecialPowers.pushPrefEnv({
+ "set": [
+ ["dom.ipc.browser_frames.oop_by_default", true],
+ ["dom.mozBrowserFramesEnabled", true],
+ ["browser.pagethumbnails.capturing_disabled", true]
+ ]
+ }, function () {
+ var iframe = document.createElement("iframe");
+ SpecialPowers.wrap(iframe).mozbrowser = true;
+ iframe.id = "iframe";
+ iframe.src = childFrameURL;
+
+
+ iframe.addEventListener("mozbrowserloadend", function onLoadEnd() {
+ iframe.removeEventListener("mozbrowserloadend", onLoadEnd);
+ info("iframe done loading");
+
+ var mm = SpecialPowers.getBrowserFrameMessageManager(iframe);
+
+ function onError(e) {
+ ok(false, e.data.error);
+ }
+ mm.addMessageListener("testSaveHeapSnapshot:error", onError);
+
+ mm.addMessageListener("testSaveHeapSnapshot:done", function onMsg() {
+ mm.removeMessageListener("testSaveHeapSnapshot:done", onMsg);
+ mm.removeMessageListener("testSaveHeapSnapshot:error", onError);
+ ok(true, "Saved heap snapshot in child process");
+ SimpleTest.finish();
+ });
+
+ info("Loading frame script to save heap snapshot");
+ mm.loadFrameScript("data:,(" + encodeURI(childFrameScript.toString()) + ")();",
+ false);
+ });
+
+ info("Loading iframe");
+ document.body.appendChild(iframe);
+ });
+ };
+ </script>
+</window>
diff --git a/devtools/shared/heapsnapshot/tests/unit/.eslintrc.js b/devtools/shared/heapsnapshot/tests/unit/.eslintrc.js
new file mode 100644
index 000000000..59adf410a
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the common devtools xpcshell eslintrc config.
+ "extends": "../../../../.eslintrc.xpcshell.js"
+};
diff --git a/devtools/shared/heapsnapshot/tests/unit/Census.jsm b/devtools/shared/heapsnapshot/tests/unit/Census.jsm
new file mode 100644
index 000000000..f8fb1ce44
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/Census.jsm
@@ -0,0 +1,165 @@
+// Functions for checking results returned by
+// Debugger.Memory.prototype.takeCensus and
+// HeapSnapshot.prototype.takeCensus. Adapted from js/src/jit-test/lib/census.js.
+
+this.EXPORTED_SYMBOLS = ["Census"];
+
+this.Census = (function () {
+ const Census = {};
+
+ function dumpn(msg) {
+ dump("DBG-TEST: Census.jsm: " + msg + "\n");
+ }
+
+ // Census.walkCensus(subject, name, walker)
+ //
+ // Use |walker| to check |subject|, a census object of the sort returned by
+ // Debugger.Memory.prototype.takeCensus: a tree of objects with integers at the
+ // leaves. Use |name| as the name for |subject| in diagnostic messages. Return
+ // the number of leaves of |subject| we visited.
+ //
+ // A walker is an object with three methods:
+ //
+ // - enter(prop): Return the walker we should use to check the property of the
+ // subject census named |prop|. This is for recursing into the subobjects of
+ // the subject.
+ //
+ // - done(): Called after we have called 'enter' on every property of the
+ // subject.
+ //
+ // - check(value): Check |value|, a leaf in the subject.
+ //
+ // Walker methods are expected to simply throw if a node we visit doesn't look
+ // right.
+ Census.walkCensus = (subject, name, walker) => walk(subject, name, walker, 0);
+ function walk(subject, name, walker, count) {
+ if (typeof subject === "object") {
+ dumpn(name);
+ for (let prop in subject) {
+ count = walk(subject[prop],
+ name + "[" + uneval(prop) + "]",
+ walker.enter(prop),
+ count);
+ }
+ walker.done();
+ } else {
+ dumpn(name + " = " + uneval(subject));
+ walker.check(subject);
+ count++;
+ }
+
+ return count;
+ }
+
+ // A walker that doesn't check anything.
+ Census.walkAnything = {
+ enter: () => Census.walkAnything,
+ done: () => undefined,
+ check: () => undefined
+ };
+
+ // A walker that requires all leaves to be zeros.
+ Census.assertAllZeros = {
+ enter: () => Census.assertAllZeros,
+ done: () => undefined,
+ check: elt => { if (elt !== 0) throw new Error("Census mismatch: expected zero, found " + elt); }
+ };
+
+ function expectedObject() {
+ throw new Error("Census mismatch: subject has leaf where basis has nested object");
+ }
+
+ function expectedLeaf() {
+ throw new Error("Census mismatch: subject has nested object where basis has leaf");
+ }
+
+ // Return a function that, given a 'basis' census, returns a census walker that
+ // compares the subject census against the basis. The returned walker calls the
+ // given |compare|, |missing|, and |extra| functions as follows:
+ //
+ // - compare(subjectLeaf, basisLeaf): Check a leaf of the subject against the
+ // corresponding leaf of the basis.
+ //
+ // - missing(prop, value): Called when the subject is missing a property named
+ // |prop| which is present in the basis with value |value|.
+ //
+ // - extra(prop): Called when the subject has a property named |prop|, but the
+ // basis has no such property. This should return a walker that can check
+ // the subject's value.
+ function makeBasisChecker({compare, missing, extra}) {
+ return function makeWalker(basis) {
+ if (typeof basis === "object") {
+ var unvisited = new Set(Object.getOwnPropertyNames(basis));
+ return {
+ enter: prop => {
+ unvisited.delete(prop);
+ if (prop in basis) {
+ return makeWalker(basis[prop]);
+ } else {
+ return extra(prop);
+ }
+ },
+
+ done: () => unvisited.forEach(prop => missing(prop, basis[prop])),
+ check: expectedObject
+ };
+ } else {
+ return {
+ enter: expectedLeaf,
+ done: expectedLeaf,
+ check: elt => compare(elt, basis)
+ };
+ }
+ };
+ }
+
+ function missingProp(prop) {
+ throw new Error("Census mismatch: subject lacks property present in basis: " + prop);
+ }
+
+ function extraProp(prop) {
+ throw new Error("Census mismatch: subject has property not present in basis: " + prop);
+ }
+
+ // Return a walker that checks that the subject census has counts all equal to
+ // |basis|.
+ Census.assertAllEqual = makeBasisChecker({
+ compare: (a, b) => { if (a !== b) throw new Error("Census mismatch: expected " + a + " got " + b);},
+ missing: missingProp,
+ extra: extraProp
+ });
+
+ function ok(val) {
+ if (!val) {
+ throw new Error("Census mismatch: expected truthy, got " + val);
+ }
+ }
+
+ // Return a walker that checks that the subject census has at least as many
+ // items of each category as |basis|.
+ Census.assertAllNotLessThan = makeBasisChecker({
+ compare: (subject, basis) => ok(subject >= basis),
+ missing: missingProp,
+ extra: () => Census.walkAnything
+ });
+
+ // Return a walker that checks that the subject census has at most as many
+ // items of each category as |basis|.
+ Census.assertAllNotMoreThan = makeBasisChecker({
+ compare: (subject, basis) => ok(subject <= basis),
+ missing: missingProp,
+ extra: () => Census.walkAnything
+ });
+
+ // Return a walker that checks that the subject census has within |fudge|
+ // items of each category of the count in |basis|.
+ Census.assertAllWithin = function (fudge, basis) {
+ return makeBasisChecker({
+ compare: (subject, basis) => ok(Math.abs(subject - basis) <= fudge),
+ missing: missingProp,
+ extra: () => Census.walkAnything
+ })(basis);
+ };
+
+ return Census;
+}());
diff --git a/devtools/shared/heapsnapshot/tests/unit/Match.jsm b/devtools/shared/heapsnapshot/tests/unit/Match.jsm
new file mode 100644
index 000000000..c29e6484e
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/Match.jsm
@@ -0,0 +1,190 @@
+// A little pattern-matching library.
+//
+// Ported from js/src/tests/js1_8_5/reflect-parse/Match.js for use with devtools
+// server xpcshell tests.
+
+this.EXPORTED_SYMBOLS = ["Match"];
+
+this.Match = (function() {
+
+ function Pattern(template) {
+ // act like a constructor even as a function
+ if (!(this instanceof Pattern))
+ return new Pattern(template);
+
+ this.template = template;
+ }
+
+ Pattern.prototype = {
+ match: function(act) {
+ return match(act, this.template);
+ },
+
+ matches: function(act) {
+ try {
+ return this.match(act);
+ }
+ catch (e if e instanceof MatchError) {
+ return false;
+ }
+ },
+
+ assert: function(act, message) {
+ try {
+ return this.match(act);
+ }
+ catch (e if e instanceof MatchError) {
+ throw new Error((message || "failed match") + ": " + e.message);
+ }
+ },
+
+ toString: () => "[object Pattern]"
+ };
+
+ Pattern.ANY = new Pattern;
+ Pattern.ANY.template = Pattern.ANY;
+
+ Pattern.NUMBER = new Pattern;
+ Pattern.NUMBER.match = function (act) {
+ if (typeof act !== 'number') {
+ throw new MatchError("Expected number, got: " + quote(act));
+ }
+ }
+
+ Pattern.NATURAL = new Pattern
+ Pattern.NATURAL.match = function (act) {
+ if (typeof act !== 'number' || act !== Math.floor(act) || act < 0) {
+ throw new MatchError("Expected natural number, got: " + quote(act));
+ }
+ }
+
+ var quote = uneval;
+
+ function MatchError(msg) {
+ this.message = msg;
+ }
+
+ MatchError.prototype = {
+ toString: function() {
+ return "match error: " + this.message;
+ }
+ };
+
+ function isAtom(x) {
+ return (typeof x === "number") ||
+ (typeof x === "string") ||
+ (typeof x === "boolean") ||
+ (x === null) ||
+ (typeof x === "object" && x instanceof RegExp);
+ }
+
+ function isObject(x) {
+ return (x !== null) && (typeof x === "object");
+ }
+
+ function isFunction(x) {
+ return typeof x === "function";
+ }
+
+ function isArrayLike(x) {
+ return isObject(x) && ("length" in x);
+ }
+
+ function matchAtom(act, exp) {
+ if ((typeof exp) === "number" && isNaN(exp)) {
+ if ((typeof act) !== "number" || !isNaN(act))
+ throw new MatchError("expected NaN, got: " + quote(act));
+ return true;
+ }
+
+ if (exp === null) {
+ if (act !== null)
+ throw new MatchError("expected null, got: " + quote(act));
+ return true;
+ }
+
+ if (exp instanceof RegExp) {
+ if (!(act instanceof RegExp) || exp.source !== act.source)
+ throw new MatchError("expected " + quote(exp) + ", got: " + quote(act));
+ return true;
+ }
+
+ switch (typeof exp) {
+ case "string":
+ if (act !== exp)
+ throw new MatchError("expected " + quote(exp) + ", got " + quote(act));
+ return true;
+ case "boolean":
+ case "number":
+ if (exp !== act)
+ throw new MatchError("expected " + exp + ", got " + quote(act));
+ return true;
+ }
+
+ throw new Error("bad pattern: " + exp.toSource());
+ }
+
+ function matchObject(act, exp) {
+ if (!isObject(act))
+ throw new MatchError("expected object, got " + quote(act));
+
+ for (var key in exp) {
+ if (!(key in act))
+ throw new MatchError("expected property " + quote(key) + " not found in " + quote(act));
+ match(act[key], exp[key]);
+ }
+
+ return true;
+ }
+
+ function matchFunction(act, exp) {
+ if (!isFunction(act))
+ throw new MatchError("expected function, got " + quote(act));
+
+ if (act !== exp)
+ throw new MatchError("expected function: " + exp +
+ "\nbut got different function: " + act);
+ }
+
+ function matchArray(act, exp) {
+ if (!isObject(act) || !("length" in act))
+ throw new MatchError("expected array-like object, got " + quote(act));
+
+ var length = exp.length;
+ if (act.length !== exp.length)
+ throw new MatchError("expected array-like object of length " + length + ", got " + quote(act));
+
+ for (var i = 0; i < length; i++) {
+ if (i in exp) {
+ if (!(i in act))
+ throw new MatchError("expected array property " + i + " not found in " + quote(act));
+ match(act[i], exp[i]);
+ }
+ }
+
+ return true;
+ }
+
+ function match(act, exp) {
+ if (exp === Pattern.ANY)
+ return true;
+
+ if (exp instanceof Pattern)
+ return exp.match(act);
+
+ if (isAtom(exp))
+ return matchAtom(act, exp);
+
+ if (isArrayLike(exp))
+ return matchArray(act, exp);
+
+ if (isFunction(exp))
+ return matchFunction(act, exp);
+
+ return matchObject(act, exp);
+ }
+
+ return { Pattern: Pattern,
+ MatchError: MatchError };
+
+})();
diff --git a/devtools/shared/heapsnapshot/tests/unit/dominator-tree-worker.js b/devtools/shared/heapsnapshot/tests/unit/dominator-tree-worker.js
new file mode 100644
index 000000000..1f49ca841
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/dominator-tree-worker.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+console.log("Initializing worker.");
+
+self.onmessage = e => {
+ console.log("Starting test.");
+ try {
+ const path = ThreadSafeChromeUtils.saveHeapSnapshot({ runtime: true });
+ const snapshot = ThreadSafeChromeUtils.readHeapSnapshot(path);
+
+ const dominatorTree = snapshot.computeDominatorTree();
+ ok(dominatorTree);
+ ok(dominatorTree instanceof DominatorTree);
+
+ let threw = false;
+ try {
+ new DominatorTree();
+ } catch (e) {
+ threw = true;
+ }
+ ok(threw, "Constructor shouldn't be usable");
+ } catch (e) {
+ ok(false, "Unexpected error inside worker:\n" + e.toString() + "\n" + e.stack);
+ } finally {
+ done();
+ }
+};
+
+// Proxy assertions to the main thread.
+function ok(val, msg) {
+ console.log("ok(" + !!val + ", \"" + msg + "\")");
+ self.postMessage({
+ type: "assertion",
+ passed: !!val,
+ msg,
+ stack: Error().stack
+ });
+}
+
+// Tell the main thread we are done with the tests.
+function done() {
+ console.log("done()");
+ self.postMessage({
+ type: "done"
+ });
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/head_heapsnapshot.js b/devtools/shared/heapsnapshot/tests/unit/head_heapsnapshot.js
new file mode 100644
index 000000000..3171c8a6f
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/head_heapsnapshot.js
@@ -0,0 +1,448 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+var CC = Components.Constructor;
+
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { Match } = Cu.import("resource://test/Match.jsm", {});
+const { Census } = Cu.import("resource://test/Census.jsm", {});
+const { addDebuggerToGlobal } =
+ Cu.import("resource://gre/modules/jsdebugger.jsm", {});
+const { Task } = require("devtools/shared/task");
+
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const flags = require("devtools/shared/flags");
+const HeapAnalysesClient =
+ require("devtools/shared/heapsnapshot/HeapAnalysesClient");
+const Services = require("Services");
+const { censusReportToCensusTreeNode } = require("devtools/shared/heapsnapshot/census-tree-node");
+const CensusUtils = require("devtools/shared/heapsnapshot/CensusUtils");
+const DominatorTreeNode = require("devtools/shared/heapsnapshot/DominatorTreeNode");
+const { deduplicatePaths } = require("devtools/shared/heapsnapshot/shortest-paths");
+const { LabelAndShallowSizeVisitor } = DominatorTreeNode;
+
+
+// Always log packets when running tests. runxpcshelltests.py will throw
+// the output away anyway, unless you give it the --verbose flag.
+if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) {
+ Services.prefs.setBoolPref("devtools.debugger.log", true);
+}
+flags.wantLogging = true;
+
+const SYSTEM_PRINCIPAL = Cc["@mozilla.org/systemprincipal;1"]
+ .createInstance(Ci.nsIPrincipal);
+
+function dumpn(msg) {
+ dump("HEAPSNAPSHOT-TEST: " + msg + "\n");
+}
+
+function addTestingFunctionsToGlobal(global) {
+ global.eval(
+ `
+ const testingFunctions = Components.utils.getJSTestingFunctions();
+ for (let k in testingFunctions) {
+ this[k] = testingFunctions[k];
+ }
+ `
+ );
+ if (!global.print) {
+ global.print = do_print;
+ }
+ if (!global.newGlobal) {
+ global.newGlobal = newGlobal;
+ }
+ if (!global.Debugger) {
+ addDebuggerToGlobal(global);
+ }
+}
+
+addTestingFunctionsToGlobal(this);
+
+/**
+ * Create a new global, with all the JS shell testing functions. Similar to the
+ * newGlobal function exposed to JS shells, and useful for porting JS shell
+ * tests to xpcshell tests.
+ */
+function newGlobal() {
+ const global = new Cu.Sandbox(SYSTEM_PRINCIPAL, { freshZone: true });
+ addTestingFunctionsToGlobal(global);
+ return global;
+}
+
+function assertThrowsValue(f, val, msg) {
+ var fullmsg;
+ try {
+ f();
+ } catch (exc) {
+ if ((exc === val) === (val === val) && (val !== 0 || 1 / exc === 1 / val))
+ return;
+ fullmsg = "Assertion failed: expected exception " + val + ", got " + exc;
+ }
+ if (fullmsg === undefined)
+ fullmsg = "Assertion failed: expected exception " + val + ", no exception thrown";
+ if (msg !== undefined)
+ fullmsg += " - " + msg;
+ throw new Error(fullmsg);
+}
+
+/**
+ * Returns the full path of the file with the specified name in a
+ * platform-independent and URL-like form.
+ */
+function getFilePath(aName, aAllowMissing = false, aUsePlatformPathSeparator = false)
+{
+ let file = do_get_file(aName, aAllowMissing);
+ let path = Services.io.newFileURI(file).spec;
+ let filePrePath = "file://";
+ if ("nsILocalFileWin" in Ci &&
+ file instanceof Ci.nsILocalFileWin) {
+ filePrePath += "/";
+ }
+
+ path = path.slice(filePrePath.length);
+
+ if (aUsePlatformPathSeparator && path.match(/^\w:/)) {
+ path = path.replace(/\//g, "\\");
+ }
+
+ return path;
+}
+
+function saveNewHeapSnapshot(opts = { runtime: true }) {
+ const filePath = ChromeUtils.saveHeapSnapshot(opts);
+ ok(filePath, "Should get a file path to save the core dump to.");
+ ok(true, "Saved a heap snapshot to " + filePath);
+ return filePath;
+}
+
+function readHeapSnapshot(filePath) {
+ const snapshot = ChromeUtils.readHeapSnapshot(filePath);
+ ok(snapshot, "Should have read a heap snapshot back from " + filePath);
+ ok(snapshot instanceof HeapSnapshot, "snapshot should be an instance of HeapSnapshot");
+ return snapshot;
+}
+
+/**
+ * Save a heap snapshot to the file with the given name in the current
+ * directory, read it back as a HeapSnapshot instance, and then take a census of
+ * the heap snapshot's serialized heap graph with the provided census options.
+ *
+ * @param {Object|undefined} censusOptions
+ * Options that should be passed through to the takeCensus method. See
+ * js/src/doc/Debugger/Debugger.Memory.md for details.
+ *
+ * @param {Debugger|null} dbg
+ * If a Debugger object is given, only serialize the subgraph covered by
+ * the Debugger's debuggees. If null, serialize the whole heap graph.
+ *
+ * @param {String} fileName
+ * The file name to save the heap snapshot's core dump file to, within
+ * the current directory.
+ *
+ * @returns Census
+ */
+function saveHeapSnapshotAndTakeCensus(dbg = null, censusOptions = undefined) {
+ const snapshotOptions = dbg ? { debugger: dbg } : { runtime: true };
+ const filePath = saveNewHeapSnapshot(snapshotOptions);
+ const snapshot = readHeapSnapshot(filePath);
+
+ equal(typeof snapshot.takeCensus, "function", "snapshot should have a takeCensus method");
+
+ return snapshot.takeCensus(censusOptions);
+}
+
+/**
+ * Save a heap snapshot to disk, read it back as a HeapSnapshot instance, and
+ * then compute its dominator tree.
+ *
+ * @param {Debugger|null} dbg
+ * If a Debugger object is given, only serialize the subgraph covered by
+ * the Debugger's debuggees. If null, serialize the whole heap graph.
+ *
+ * @returns {DominatorTree}
+ */
+function saveHeapSnapshotAndComputeDominatorTree(dbg = null) {
+ const snapshotOptions = dbg ? { debugger: dbg } : { runtime: true };
+ const filePath = saveNewHeapSnapshot(snapshotOptions);
+ const snapshot = readHeapSnapshot(filePath);
+
+ equal(typeof snapshot.computeDominatorTree, "function",
+ "snapshot should have a `computeDominatorTree` method");
+
+ const dominatorTree = snapshot.computeDominatorTree();
+
+ ok(dominatorTree, "Should be able to compute a dominator tree");
+ ok(dominatorTree instanceof DominatorTree, "Should be an instance of DominatorTree");
+
+ return dominatorTree;
+}
+
+function isSavedFrame(obj) {
+ return Object.prototype.toString.call(obj) === "[object SavedFrame]";
+}
+
+function savedFrameReplacer(key, val) {
+ if (isSavedFrame(val)) {
+ return `<SavedFrame '${val.toString().split(/\n/g).shift()}'>`;
+ } else {
+ return val;
+ }
+}
+
+/**
+ * Assert that creating a CensusTreeNode from the given `report` with the
+ * specified `breakdown` creates the given `expected` CensusTreeNode.
+ *
+ * @param {Object} breakdown
+ * The census breakdown.
+ *
+ * @param {Object} report
+ * The census report.
+ *
+ * @param {Object} expected
+ * The expected CensusTreeNode result.
+ *
+ * @param {Object} options
+ * The options to pass through to `censusReportToCensusTreeNode`.
+ */
+function compareCensusViewData(breakdown, report, expected, options) {
+ dumpn("Generating CensusTreeNode from report:");
+ dumpn("breakdown: " + JSON.stringify(breakdown, null, 4));
+ dumpn("report: " + JSON.stringify(report, null, 4));
+ dumpn("expected: " + JSON.stringify(expected, savedFrameReplacer, 4));
+
+ const actual = censusReportToCensusTreeNode(breakdown, report, options);
+ dumpn("actual: " + JSON.stringify(actual, savedFrameReplacer, 4));
+
+ assertStructurallyEquivalent(actual, expected);
+}
+
+// Deep structural equivalence that can handle Map objects in addition to plain
+// objects.
+function assertStructurallyEquivalent(actual, expected, path = "root") {
+ if (actual === expected) {
+ equal(actual, expected, "actual and expected are the same");
+ return;
+ }
+
+ equal(typeof actual, typeof expected, `${path}: typeof should be the same`);
+
+ if (actual && typeof actual === "object") {
+ const actualProtoString = Object.prototype.toString.call(actual);
+ const expectedProtoString = Object.prototype.toString.call(expected);
+ equal(actualProtoString, expectedProtoString,
+ `${path}: Object.prototype.toString.call() should be the same`);
+
+ if (actualProtoString === "[object Map]") {
+ const expectedKeys = new Set([...expected.keys()]);
+
+ for (let key of actual.keys()) {
+ ok(expectedKeys.has(key),
+ `${path}: every key in actual should exist in expected: ${String(key).slice(0, 10)}`);
+ expectedKeys.delete(key);
+
+ assertStructurallyEquivalent(actual.get(key), expected.get(key),
+ path + ".get(" + String(key).slice(0, 20) + ")");
+ }
+
+ equal(expectedKeys.size, 0,
+ `${path}: every key in expected should also exist in actual, did not see ${[...expectedKeys]}`);
+ } else if (actualProtoString === "[object Set]") {
+ const expectedItems = new Set([...expected]);
+
+ for (let item of actual) {
+ ok(expectedItems.has(item),
+ `${path}: every set item in actual should exist in expected: ${item}`);
+ expectedItems.delete(item);
+ }
+
+ equal(expectedItems.size, 0,
+ `${path}: every set item in expected should also exist in actual, did not see ${[...expectedItems]}`);
+ } else {
+ const expectedKeys = new Set(Object.keys(expected));
+
+ for (let key of Object.keys(actual)) {
+ ok(expectedKeys.has(key),
+ `${path}: every key in actual should exist in expected: ${key}`);
+ expectedKeys.delete(key);
+
+ assertStructurallyEquivalent(actual[key], expected[key], path + "." + key);
+ }
+
+ equal(expectedKeys.size, 0,
+ `${path}: every key in expected should also exist in actual, did not see ${[...expectedKeys]}`);
+ }
+ } else {
+ equal(actual, expected, `${path}: primitives should be equal`);
+ }
+}
+
+/**
+ * Assert that creating a diff of the `first` and `second` census reports
+ * creates the `expected` delta-report.
+ *
+ * @param {Object} breakdown
+ * The census breakdown.
+ *
+ * @param {Object} first
+ * The first census report.
+ *
+ * @param {Object} second
+ * The second census report.
+ *
+ * @param {Object} expected
+ * The expected delta-report.
+ */
+function assertDiff(breakdown, first, second, expected) {
+ dumpn("Diffing census reports:");
+ dumpn("Breakdown: " + JSON.stringify(breakdown, null, 4));
+ dumpn("First census report: " + JSON.stringify(first, null, 4));
+ dumpn("Second census report: " + JSON.stringify(second, null, 4));
+ dumpn("Expected delta-report: " + JSON.stringify(expected, null, 4));
+
+ const actual = CensusUtils.diff(breakdown, first, second);
+ dumpn("Actual delta-report: " + JSON.stringify(actual, null, 4));
+
+ assertStructurallyEquivalent(actual, expected);
+}
+
+/**
+ * Assert that creating a label and getting a shallow size from the given node
+ * description with the specified breakdown is as expected.
+ *
+ * @param {Object} breakdown
+ * @param {Object} givenDescription
+ * @param {Number} expectedShallowSize
+ * @param {Object} expectedLabel
+ */
+function assertLabelAndShallowSize(breakdown, givenDescription, expectedShallowSize, expectedLabel) {
+ dumpn("Computing label and shallow size from node description:");
+ dumpn("Breakdown: " + JSON.stringify(breakdown, null, 4));
+ dumpn("Given description: " + JSON.stringify(givenDescription, null, 4));
+
+ const visitor = new LabelAndShallowSizeVisitor();
+ CensusUtils.walk(breakdown, description, visitor);
+
+ dumpn("Expected shallow size: " + expectedShallowSize);
+ dumpn("Actual shallow size: " + visitor.shallowSize());
+ equal(visitor.shallowSize(), expectedShallowSize, "Shallow size should be correct");
+
+ dumpn("Expected label: " + JSON.stringify(expectedLabel, null, 4));
+ dumpn("Actual label: " + JSON.stringify(visitor.label(), null, 4));
+ assertStructurallyEquivalent(visitor.label(), expectedLabel);
+}
+
+// Counter for mock DominatorTreeNode ids.
+let TEST_NODE_ID_COUNTER = 0;
+
+/**
+ * Create a mock DominatorTreeNode for testing, with sane defaults. Override any
+ * property by providing it on `opts`. Optionally pass child nodes as well.
+ *
+ * @param {Object} opts
+ * @param {Array<DominatorTreeNode>?} children
+ *
+ * @returns {DominatorTreeNode}
+ */
+function makeTestDominatorTreeNode(opts, children) {
+ const nodeId = TEST_NODE_ID_COUNTER++;
+
+ const node = Object.assign({
+ nodeId,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: (children || []).reduce((size, c) => size + c.retainedSize, 1),
+ parentId: undefined,
+ children,
+ moreChildrenAvailable: true,
+ }, opts);
+
+ if (children && children.length) {
+ children.map(c => c.parentId = node.nodeId);
+ }
+
+ return node;
+}
+
+/**
+ * Insert `newChildren` into the given dominator `tree` as specified by the
+ * `path` from the root to the node the `newChildren` should be inserted
+ * beneath. Assert that the resulting tree matches `expected`.
+ */
+function assertDominatorTreeNodeInsertion(tree, path, newChildren, moreChildrenAvailable, expected) {
+ dumpn("Inserting new children into a dominator tree:");
+ dumpn("Dominator tree: " + JSON.stringify(tree, null, 2));
+ dumpn("Path: " + JSON.stringify(path, null, 2));
+ dumpn("New children: " + JSON.stringify(newChildren, null, 2));
+ dumpn("Expected resulting tree: " + JSON.stringify(expected, null, 2));
+
+ const actual = DominatorTreeNode.insert(tree, path, newChildren, moreChildrenAvailable);
+ dumpn("Actual resulting tree: " + JSON.stringify(actual, null, 2));
+
+ assertStructurallyEquivalent(actual, expected);
+}
+
+function assertDeduplicatedPaths({ target, paths, expectedNodes, expectedEdges }) {
+ dumpn("Deduplicating paths:");
+ dumpn("target = " + target);
+ dumpn("paths = " + JSON.stringify(paths, null, 2));
+ dumpn("expectedNodes = " + expectedNodes);
+ dumpn("expectedEdges = " + JSON.stringify(expectedEdges, null, 2));
+
+ const { nodes, edges } = deduplicatePaths(target, paths);
+
+ dumpn("Actual nodes = " + nodes);
+ dumpn("Actual edges = " + JSON.stringify(edges, null, 2));
+
+ equal(nodes.length, expectedNodes.length,
+ "actual number of nodes is equal to the expected number of nodes");
+
+ equal(edges.length, expectedEdges.length,
+ "actual number of edges is equal to the expected number of edges");
+
+ const expectedNodeSet = new Set(expectedNodes);
+ const nodeSet = new Set(nodes);
+ ok(nodeSet.size === nodes.length,
+ "each returned node should be unique");
+
+ for (let node of nodes) {
+ ok(expectedNodeSet.has(node), `the ${node} node was expected`);
+ }
+
+ for (let expectedEdge of expectedEdges) {
+ let count = 0;
+ for (let edge of edges) {
+ if (edge.from === expectedEdge.from &&
+ edge.to === expectedEdge.to &&
+ edge.name === expectedEdge.name) {
+ count++;
+ }
+ }
+ equal(count, 1,
+ "should have exactly one matching edge for the expected edge = " + JSON.stringify(edge));
+ }
+}
+
+function assertCountToBucketBreakdown(breakdown, expected) {
+ dumpn("count => bucket breakdown");
+ dumpn("Initial breakdown = ", JSON.stringify(breakdown, null, 2));
+ dumpn("Expected results = ", JSON.stringify(expected, null, 2));
+
+ const actual = CensusUtils.countToBucketBreakdown(breakdown);
+ dumpn("Actual results = ", JSON.stringify(actual, null, 2));
+
+ assertStructurallyEquivalent(actual, expected);
+}
+
+/**
+ * Create a mock path entry for the given predecessor and edge.
+ */
+function pathEntry(predecessor, edge) {
+ return { predecessor, edge };
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/heap-snapshot-worker.js b/devtools/shared/heapsnapshot/tests/unit/heap-snapshot-worker.js
new file mode 100644
index 000000000..10ee70cec
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/heap-snapshot-worker.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+console.log("Initializing worker.");
+
+self.onmessage = e => {
+ console.log("Starting test.");
+ try {
+ ok(typeof ChromeUtils === "undefined",
+ "Should not have access to ChromeUtils in a worker.");
+ ok(ThreadSafeChromeUtils,
+ "Should have access to ThreadSafeChromeUtils in a worker.");
+ ok(HeapSnapshot,
+ "Should have access to HeapSnapshot in a worker.");
+
+ const filePath = ThreadSafeChromeUtils.saveHeapSnapshot({ globals: [this] });
+ ok(true, "Should be able to save a snapshot.");
+
+ const snapshot = ThreadSafeChromeUtils.readHeapSnapshot(filePath);
+ ok(snapshot, "Should be able to read a heap snapshot");
+ ok(snapshot instanceof HeapSnapshot, "Should be an instanceof HeapSnapshot");
+ } catch (e) {
+ ok(false, "Unexpected error inside worker:\n" + e.toString() + "\n" + e.stack);
+ } finally {
+ done();
+ }
+};
+
+// Proxy assertions to the main thread.
+function ok(val, msg) {
+ console.log("ok(" + !!val + ", \"" + msg + "\")");
+ self.postMessage({
+ type: "assertion",
+ passed: !!val,
+ msg,
+ stack: Error().stack
+ });
+}
+
+// Tell the main thread we are done with the tests.
+function done() {
+ console.log("done()");
+ self.postMessage({
+ type: "done"
+ });
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_LabelAndShallowSize_01.js b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_LabelAndShallowSize_01.js
new file mode 100644
index 000000000..845a0d263
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_LabelAndShallowSize_01.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can generate label structures from node description reports.
+
+const breakdown = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+};
+
+const description = {
+ objects: {
+ Function: { count: 1, bytes: 32 },
+ other: { count: 0, bytes: 0 }
+ },
+ strings: {},
+ scripts: {},
+ other: {}
+};
+
+const expected = [
+ "objects",
+ "Function"
+];
+
+const shallowSize = 32;
+
+function run_test() {
+ assertLabelAndShallowSize(breakdown, description, shallowSize, expected);
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_LabelAndShallowSize_02.js b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_LabelAndShallowSize_02.js
new file mode 100644
index 000000000..e1f32de58
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_LabelAndShallowSize_02.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can generate label structures from node description reports.
+
+const breakdown = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+};
+
+const description = {
+ objects: {
+ other: { count: 1, bytes: 10 }
+ },
+ strings: {},
+ scripts: {},
+ other: {}
+};
+
+const expected = [
+ "objects",
+ "other"
+];
+
+const shallowSize = 10;
+
+function run_test() {
+ assertLabelAndShallowSize(breakdown, description, shallowSize, expected);
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_LabelAndShallowSize_03.js b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_LabelAndShallowSize_03.js
new file mode 100644
index 000000000..ad35dcec1
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_LabelAndShallowSize_03.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can generate label structures from node description reports.
+
+const breakdown = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+};
+
+const description = {
+ objects: {
+ other: { count: 0, bytes: 0 }
+ },
+ strings: {
+ "JSString": { count: 1, bytes: 42 },
+ },
+ scripts: {},
+ other: {}
+};
+
+const expected = [
+ "strings",
+ "JSString"
+];
+
+const shallowSize = 42;
+
+function run_test() {
+ assertLabelAndShallowSize(breakdown, description, shallowSize, expected);
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_LabelAndShallowSize_04.js b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_LabelAndShallowSize_04.js
new file mode 100644
index 000000000..566ad0dab
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_LabelAndShallowSize_04.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can generate label structures from node description reports.
+
+const breakdown = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: {
+ by: "allocationStack",
+ then: { by: "count", count: true, bytes: true },
+ noStack: { by: "count", count: true, bytes: true },
+ },
+ other: { by: "count", count: true, bytes: true },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+};
+
+const stack = saveStack();
+
+const description = {
+ objects: {
+ Array: new Map([[stack, { count: 1, bytes: 512 }]]),
+ other: { count: 0, bytes: 0 }
+ },
+ strings: {},
+ scripts: {},
+ other: {}
+};
+
+const expected = [
+ "objects",
+ "Array",
+ stack
+];
+
+const shallowSize = 512;
+
+function run_test() {
+ assertLabelAndShallowSize(breakdown, description, shallowSize, expected);
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_attachShortestPaths_01.js b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_attachShortestPaths_01.js
new file mode 100644
index 000000000..24e8e2eb5
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_attachShortestPaths_01.js
@@ -0,0 +1,132 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the DominatorTreeNode.attachShortestPaths function can correctly
+// attach the deduplicated shortest retaining paths for each node it is given.
+
+const startNodeId = 9999;
+const maxNumPaths = 2;
+
+// Mock data mapping node id to shortest paths to that node id.
+const shortestPaths = new Map([
+ [1000, [
+ [pathEntry(1100, "a"), pathEntry(1200, "b")],
+ [pathEntry(1100, "c"), pathEntry(1300, "d")],
+ ]],
+ [2000, [
+ [pathEntry(2100, "e"), pathEntry(2200, "f"), pathEntry(2300, "g")]
+ ]],
+ [3000, [
+ [pathEntry(3100, "h")],
+ [pathEntry(3100, "i")],
+ [pathEntry(3100, "j")],
+ [pathEntry(3200, "k")],
+ [pathEntry(3300, "l")],
+ [pathEntry(3400, "m")],
+ ]],
+]);
+
+const actual = [
+ makeTestDominatorTreeNode({ nodeId: 1000 }),
+ makeTestDominatorTreeNode({ nodeId: 2000 }),
+ makeTestDominatorTreeNode({ nodeId: 3000 }),
+];
+
+const expected = [
+ makeTestDominatorTreeNode({
+ nodeId: 1000,
+ shortestPaths: {
+ nodes: [
+ { id: 1000, label: ["SomeType-1000"] },
+ { id: 1100, label: ["SomeType-1100"] },
+ { id: 1200, label: ["SomeType-1200"] },
+ { id: 1300, label: ["SomeType-1300"] },
+ ],
+ edges: [
+ { from: 1100, to: 1200, name: "a" },
+ { from: 1100, to: 1300, name: "c" },
+ { from: 1200, to: 1000, name: "b" },
+ { from: 1300, to: 1000, name: "d" },
+ ]
+ }
+ }),
+
+ makeTestDominatorTreeNode({
+ nodeId: 2000,
+ shortestPaths: {
+ nodes: [
+ { id: 2000, label: ["SomeType-2000"] },
+ { id: 2100, label: ["SomeType-2100"] },
+ { id: 2200, label: ["SomeType-2200"] },
+ { id: 2300, label: ["SomeType-2300"] },
+ ],
+ edges: [
+ { from: 2100, to: 2200, name: "e" },
+ { from: 2200, to: 2300, name: "f" },
+ { from: 2300, to: 2000, name: "g" },
+ ]
+ }
+ }),
+
+ makeTestDominatorTreeNode({ nodeId: 3000,
+ shortestPaths: {
+ nodes: [
+ { id: 3000, label: ["SomeType-3000"] },
+ { id: 3100, label: ["SomeType-3100"] },
+ { id: 3200, label: ["SomeType-3200"] },
+ { id: 3300, label: ["SomeType-3300"] },
+ { id: 3400, label: ["SomeType-3400"] },
+ ],
+ edges: [
+ { from: 3100, to: 3000, name: "h" },
+ { from: 3100, to: 3000, name: "i" },
+ { from: 3100, to: 3000, name: "j" },
+ { from: 3200, to: 3000, name: "k" },
+ { from: 3300, to: 3000, name: "l" },
+ { from: 3400, to: 3000, name: "m" },
+ ]
+ }
+ }),
+];
+
+const breakdown = {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true }
+};
+
+const mockSnapshot = {
+ computeShortestPaths: (start, nodeIds, max) => {
+ equal(start, startNodeId);
+ equal(max, maxNumPaths);
+
+ return new Map(nodeIds.map(nodeId => {
+ const paths = shortestPaths.get(nodeId);
+ ok(paths, "Expected computeShortestPaths call for node id = " + nodeId);
+ return [nodeId, paths];
+ }));
+ },
+
+ describeNode: (bd, nodeId) => {
+ equal(bd, breakdown);
+ return {
+ ["SomeType-" + nodeId]: {
+ count: 1,
+ bytes: 10,
+ }
+ };
+ },
+};
+
+function run_test() {
+ DominatorTreeNode.attachShortestPaths(mockSnapshot,
+ breakdown,
+ startNodeId,
+ actual,
+ maxNumPaths);
+
+ dumpn("Expected = " + JSON.stringify(expected, null, 2));
+ dumpn("Actual = " + JSON.stringify(actual, null, 2));
+
+ assertStructurallyEquivalent(expected, actual);
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_getNodeByIdAlongPath_01.js b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_getNodeByIdAlongPath_01.js
new file mode 100644
index 000000000..de2907809
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_getNodeByIdAlongPath_01.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can find the node with the given id along the specified path.
+
+const node3000 = makeTestDominatorTreeNode({ nodeId: 3000 });
+
+const node2000 = makeTestDominatorTreeNode({ nodeId: 2000 }, [
+ makeTestDominatorTreeNode({}),
+ node3000,
+ makeTestDominatorTreeNode({}),
+]);
+
+const node1000 = makeTestDominatorTreeNode({ nodeId: 1000 }, [
+ makeTestDominatorTreeNode({}),
+ node2000,
+ makeTestDominatorTreeNode({}),
+]);
+
+const tree = node1000;
+
+const path = [1000, 2000, 3000];
+
+const tests = [
+ { id: 1000, expected: node1000 },
+ { id: 2000, expected: node2000 },
+ { id: 3000, expected: node3000 },
+];
+
+function run_test() {
+ for (let { id, expected } of tests) {
+ const actual = DominatorTreeNode.getNodeByIdAlongPath(id, tree, path);
+ equal(actual, expected, `We should have got the node with id = ${id}`);
+ }
+
+ equal(null,
+ DominatorTreeNode.getNodeByIdAlongPath(999999999999, tree, path),
+ "null is returned for nodes that are not even in the tree");
+
+ const lastNodeId = tree.children[tree.children.length - 1].nodeId;
+ equal(null,
+ DominatorTreeNode.getNodeByIdAlongPath(lastNodeId, tree, path),
+ "null is returned for nodes that are not along the path");
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_insert_01.js b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_insert_01.js
new file mode 100644
index 000000000..979232ff4
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_insert_01.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can insert new children into an existing DominatorTreeNode tree.
+
+const tree = makeTestDominatorTreeNode({ nodeId: 1000 }, [
+ makeTestDominatorTreeNode({}),
+ makeTestDominatorTreeNode({ nodeId: 2000 }, [
+ makeTestDominatorTreeNode({}),
+ makeTestDominatorTreeNode({ nodeId: 3000 }),
+ makeTestDominatorTreeNode({}),
+ ]),
+ makeTestDominatorTreeNode({}),
+]);
+
+const path = [1000, 2000, 3000];
+
+const newChildren = [
+ makeTestDominatorTreeNode({ parentId: 3000 }),
+ makeTestDominatorTreeNode({ parentId: 3000 }),
+];
+
+const moreChildrenAvailable = false;
+
+const expected = {
+ nodeId: 1000,
+ parentId: undefined,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 7,
+ children: [
+ {
+ nodeId: 0,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 1000,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ {
+ nodeId: 2000,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 4,
+ parentId: 1000,
+ children: [
+ {
+ nodeId: 1,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 2000,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ {
+ nodeId: 3000,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 2000,
+ children: [
+ {
+ nodeId: 7,
+ parentId: 3000,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ {
+ nodeId: 8,
+ parentId: 3000,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ ],
+ moreChildrenAvailable: false
+ },
+ {
+ nodeId: 3,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 2000,
+ moreChildrenAvailable: true,
+ children: undefined,
+ },
+ ],
+ moreChildrenAvailable: true
+ },
+ {
+ nodeId: 5,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 1000,
+ moreChildrenAvailable: true,
+ children: undefined,
+ }
+ ],
+ moreChildrenAvailable: true
+};
+
+function run_test() {
+ assertDominatorTreeNodeInsertion(tree, path, newChildren, moreChildrenAvailable, expected);
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_insert_02.js b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_insert_02.js
new file mode 100644
index 000000000..9a8d11d0b
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_insert_02.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test attempting to insert new children into an existing DominatorTreeNode
+// tree with a bad path.
+
+const tree = makeTestDominatorTreeNode({}, [
+ makeTestDominatorTreeNode({}),
+ makeTestDominatorTreeNode({}, [
+ makeTestDominatorTreeNode({}),
+ makeTestDominatorTreeNode({}),
+ makeTestDominatorTreeNode({}),
+ ]),
+ makeTestDominatorTreeNode({}),
+]);
+
+const path = [111111, 222222, 333333];
+
+const newChildren = [
+ makeTestDominatorTreeNode({ parentId: 333333 }),
+ makeTestDominatorTreeNode({ parentId: 333333 }),
+];
+
+const moreChildrenAvailable = false;
+
+const expected = tree;
+
+function run_test() {
+ assertDominatorTreeNodeInsertion(tree, path, newChildren, moreChildrenAvailable, expected);
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_insert_03.js b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_insert_03.js
new file mode 100644
index 000000000..f8cb5eec3
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_insert_03.js
@@ -0,0 +1,117 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test inserting new children into an existing DominatorTreeNode at the root.
+
+const tree = makeTestDominatorTreeNode({ nodeId: 666 }, [
+ makeTestDominatorTreeNode({}),
+ makeTestDominatorTreeNode({}, [
+ makeTestDominatorTreeNode({}),
+ makeTestDominatorTreeNode({}),
+ makeTestDominatorTreeNode({}),
+ ]),
+ makeTestDominatorTreeNode({}),
+]);
+
+const path = [666];
+
+const newChildren = [
+ makeTestDominatorTreeNode({
+ nodeId: 777,
+ parentId: 666
+ }),
+ makeTestDominatorTreeNode({
+ nodeId: 888,
+ parentId: 666
+ }),
+];
+
+const moreChildrenAvailable = false;
+
+const expected = {
+ nodeId: 666,
+ label: undefined,
+ parentId: undefined,
+ shallowSize: 1,
+ retainedSize: 7,
+ children: [
+ {
+ nodeId: 0,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 666,
+ moreChildrenAvailable: true,
+ children: undefined
+ },
+ {
+ nodeId: 4,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 4,
+ parentId: 666,
+ children: [
+ {
+ nodeId: 1,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 4,
+ moreChildrenAvailable: true,
+ children: undefined
+ },
+ {
+ nodeId: 2,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 4,
+ moreChildrenAvailable: true,
+ children: undefined
+ },
+ {
+ nodeId: 3,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 4,
+ moreChildrenAvailable: true,
+ children: undefined
+ }
+ ],
+ moreChildrenAvailable: true
+ },
+ {
+ nodeId: 5,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 666,
+ moreChildrenAvailable: true,
+ children: undefined
+ },
+ {
+ nodeId: 777,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 666,
+ moreChildrenAvailable: true,
+ children: undefined
+ },
+ {
+ nodeId: 888,
+ label: undefined,
+ shallowSize: 1,
+ retainedSize: 1,
+ parentId: 666,
+ moreChildrenAvailable: true,
+ children: undefined
+ }
+ ],
+ moreChildrenAvailable: false
+};
+
+function run_test() {
+ assertDominatorTreeNodeInsertion(tree, path, newChildren, moreChildrenAvailable, expected);
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_partialTraversal_01.js b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_partialTraversal_01.js
new file mode 100644
index 000000000..78ec47b64
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTreeNode_partialTraversal_01.js
@@ -0,0 +1,164 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we correctly set `moreChildrenAvailable` when doing a partial
+// traversal of a dominator tree to create the initial incrementally loaded
+// `DominatorTreeNode` tree.
+
+// `tree` maps parent to children:
+//
+// 100
+// |- 200
+// | |- 500
+// | |- 600
+// | `- 700
+// |- 300
+// | |- 800
+// | |- 900
+// `- 400
+// |- 1000
+// |- 1100
+// `- 1200
+const tree = new Map([
+ [100, [200, 300, 400]],
+ [200, [500, 600, 700]],
+ [300, [800, 900]],
+ [400, [1000, 1100, 1200]]
+]);
+
+const mockDominatorTree = {
+ root: 100,
+ getRetainedSize: _ => 10,
+ getImmediatelyDominated: id => (tree.get(id) || []).slice()
+};
+
+const mockSnapshot = {
+ describeNode: _ => ({
+ objects: { count: 0, bytes: 0 },
+ strings: { count: 0, bytes: 0 },
+ scripts: { count: 0, bytes: 0 },
+ other: { SomeType: { count: 1, bytes: 10 } }
+ })
+};
+
+const breakdown = {
+ by: "coarseType",
+ objects: { by: "count", count: true, bytes: true },
+ strings: { by: "count", count: true, bytes: true },
+ scripts: { by: "count", count: true, bytes: true },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true }
+ },
+};
+
+const expected = {
+ nodeId: 100,
+ label: [
+ "other",
+ "SomeType"
+ ],
+ shallowSize: 10,
+ retainedSize: 10,
+ shortestPaths: undefined,
+ children: [
+ {
+ nodeId: 200,
+ label: [
+ "other",
+ "SomeType"
+ ],
+ shallowSize: 10,
+ retainedSize: 10,
+ parentId: 100,
+ shortestPaths: undefined,
+ children: [
+ {
+ nodeId: 500,
+ label: [
+ "other",
+ "SomeType"
+ ],
+ shallowSize: 10,
+ retainedSize: 10,
+ parentId: 200,
+ moreChildrenAvailable: false,
+ shortestPaths: undefined,
+ children: undefined
+ },
+ {
+ nodeId: 600,
+ label: [
+ "other",
+ "SomeType"
+ ],
+ shallowSize: 10,
+ retainedSize: 10,
+ parentId: 200,
+ moreChildrenAvailable: false,
+ shortestPaths: undefined,
+ children: undefined
+ }
+ ],
+ moreChildrenAvailable: true
+ },
+ {
+ nodeId: 300,
+ label: [
+ "other",
+ "SomeType"
+ ],
+ shallowSize: 10,
+ retainedSize: 10,
+ parentId: 100,
+ shortestPaths: undefined,
+ children: [
+ {
+ nodeId: 800,
+ label: [
+ "other",
+ "SomeType"
+ ],
+ shallowSize: 10,
+ retainedSize: 10,
+ parentId: 300,
+ moreChildrenAvailable: false,
+ shortestPaths: undefined,
+ children: undefined
+ },
+ {
+ nodeId: 900,
+ label: [
+ "other",
+ "SomeType"
+ ],
+ shallowSize: 10,
+ retainedSize: 10,
+ parentId: 300,
+ moreChildrenAvailable: false,
+ shortestPaths: undefined,
+ children: undefined
+ }
+ ],
+ moreChildrenAvailable: false
+ }
+ ],
+ moreChildrenAvailable: true,
+ parentId: undefined,
+};
+
+function run_test() {
+ // Traverse the whole depth of the test tree, but one short of the number of
+ // siblings. This will exercise the moreChildrenAvailable handling for
+ // siblings.
+ const actual = DominatorTreeNode.partialTraversal(mockDominatorTree,
+ mockSnapshot,
+ breakdown,
+ /* maxDepth = */ 4,
+ /* siblings = */ 2);
+
+ dumpn("Expected = " + JSON.stringify(expected, null, 2));
+ dumpn("Actual = " + JSON.stringify(actual, null, 2));
+
+ assertStructurallyEquivalent(expected, actual);
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_DominatorTree_01.js b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTree_01.js
new file mode 100644
index 000000000..e8145f658
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTree_01.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Sanity test that we can compute dominator trees.
+
+function run_test() {
+ const path = ChromeUtils.saveHeapSnapshot({ runtime: true });
+ const snapshot = ChromeUtils.readHeapSnapshot(path);
+
+ const dominatorTree = snapshot.computeDominatorTree();
+ ok(dominatorTree);
+ ok(dominatorTree instanceof DominatorTree);
+
+ let threw = false;
+ try {
+ new DominatorTree();
+ } catch (e) {
+ threw = true;
+ }
+ ok(threw, "Constructor shouldn't be usable");
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_DominatorTree_02.js b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTree_02.js
new file mode 100644
index 000000000..a518f8a27
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTree_02.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can compute dominator trees from a snapshot in a worker.
+
+add_task(function* () {
+ const worker = new ChromeWorker("resource://test/dominator-tree-worker.js");
+ worker.postMessage({});
+
+ let assertionCount = 0;
+ worker.onmessage = e => {
+ if (e.data.type !== "assertion") {
+ return;
+ }
+
+ ok(e.data.passed, e.data.msg + "\n" + e.data.stack);
+ assertionCount++;
+ };
+
+ yield waitForDone(worker);
+
+ ok(assertionCount > 0);
+ worker.terminate();
+});
+
+function waitForDone(w) {
+ return new Promise((resolve, reject) => {
+ w.onerror = e => {
+ reject();
+ ok(false, "Error in worker: " + e);
+ };
+
+ w.addEventListener("message", function listener(e) {
+ if (e.data.type === "done") {
+ w.removeEventListener("message", listener, false);
+ resolve();
+ }
+ }, false);
+ });
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_DominatorTree_03.js b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTree_03.js
new file mode 100644
index 000000000..0a14ce53d
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTree_03.js
@@ -0,0 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can get the root of dominator trees.
+
+function run_test() {
+ const dominatorTree = saveHeapSnapshotAndComputeDominatorTree();
+ equal(typeof dominatorTree.root, "number", "root should be a number");
+ equal(Math.floor(dominatorTree.root), dominatorTree.root, "root should be an integer");
+ ok(dominatorTree.root >= 0, "root should be positive");
+ ok(dominatorTree.root <= Math.pow(2, 48), "root should be less than or equal to 2^48");
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_DominatorTree_04.js b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTree_04.js
new file mode 100644
index 000000000..e5aef3fec
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTree_04.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can get the retained sizes of dominator trees.
+
+function run_test() {
+ const dominatorTree = saveHeapSnapshotAndComputeDominatorTree();
+ equal(typeof dominatorTree.getRetainedSize, "function",
+ "getRetainedSize should be a function");
+
+ const size = dominatorTree.getRetainedSize(dominatorTree.root);
+ ok(size, "should get a size for the root");
+ equal(typeof size, "number", "retained sizes should be a number");
+ equal(Math.floor(size), size, "size should be an integer");
+ ok(size > 0, "size should be positive");
+ ok(size <= Math.pow(2, 64), "size should be less than or equal to 2^64");
+
+ const bad = dominatorTree.getRetainedSize(1);
+ equal(bad, null, "null is returned for unknown node ids");
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_DominatorTree_05.js b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTree_05.js
new file mode 100644
index 000000000..c07cee994
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTree_05.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can get the set of immediately dominated nodes for any given
+// node and that this forms a tree.
+
+function run_test() {
+ var dominatorTree = saveHeapSnapshotAndComputeDominatorTree();
+ equal(typeof dominatorTree.getImmediatelyDominated, "function",
+ "getImmediatelyDominated should be a function");
+
+ // Do a traversal of the dominator tree.
+ //
+ // Note that we don't assert directly, only if we get an unexpected
+ // value. There are just way too many nodes in the heap graph to assert for
+ // every one. This test would constantly time out and assertion messages would
+ // overflow the log size.
+
+ var root = dominatorTree.root;
+ equal(dominatorTree.getImmediateDominator(root), null,
+ "The root should not have a parent");
+
+ var seen = new Set();
+ var stack = [root];
+ while (stack.length > 0) {
+ var top = stack.pop();
+
+ if (seen.has(top)) {
+ ok(false,
+ "This is a tree, not a graph: we shouldn't have multiple edges to the same node");
+ }
+ seen.add(top);
+ if (seen.size % 1000 === 0) {
+ dumpn("Progress update: seen size = " + seen.size);
+ }
+
+ var newNodes = dominatorTree.getImmediatelyDominated(top);
+ if (Object.prototype.toString.call(newNodes) !== "[object Array]") {
+ ok(false, "getImmediatelyDominated should return an array for known node ids");
+ }
+
+ var topSize = dominatorTree.getRetainedSize(top);
+
+ var lastSize = Infinity;
+ for (var i = 0; i < newNodes.length; i++) {
+ if (typeof newNodes[i] !== "number") {
+ ok(false, "Every dominated id should be a number");
+ }
+
+ if (dominatorTree.getImmediateDominator(newNodes[i]) !== top) {
+ ok(false, "child's parent should be the expected parent");
+ }
+
+ var thisSize = dominatorTree.getRetainedSize(newNodes[i]);
+
+ if (thisSize >= topSize) {
+ ok(false, "the size of children in the dominator tree should always be less than that of their parent");
+ }
+
+ if (thisSize > lastSize) {
+ ok(false,
+ "children should be sorted by greatest to least retained size, "
+ + "lastSize = " + lastSize + ", thisSize = " + thisSize);
+ }
+
+ lastSize = thisSize;
+ stack.push(newNodes[i]);
+ }
+ }
+
+ ok(true, "Successfully walked the tree");
+ dumpn("Walked " + seen.size + " nodes");
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_DominatorTree_06.js b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTree_06.js
new file mode 100644
index 000000000..680478623
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_DominatorTree_06.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the retained size of a node is the sum of its children retained
+// sizes plus its shallow size.
+
+// Note that we don't assert directly, only if we get an unexpected
+// value. There are just way too many nodes in the heap graph to assert for
+// every one. This test would constantly time out and assertion messages would
+// overflow the log size.
+function fastAssert(cond, msg) {
+ if (!cond) {
+ ok(false, msg);
+ }
+}
+
+var COUNT = { by: "count", count: false, bytes: true };
+
+function run_test() {
+ var path = saveNewHeapSnapshot();
+ var snapshot = ChromeUtils.readHeapSnapshot(path);
+ var dominatorTree = snapshot.computeDominatorTree();
+
+ // Do a traversal of the dominator tree and assert the relationship between
+ // retained size, shallow size, and children's retained sizes.
+
+ var root = dominatorTree.root;
+ var stack = [root];
+ while (stack.length > 0) {
+ var top = stack.pop();
+
+ var children = dominatorTree.getImmediatelyDominated(top);
+
+ var topRetainedSize = dominatorTree.getRetainedSize(top);
+ var topShallowSize = snapshot.describeNode(COUNT, top).bytes;
+ fastAssert(topShallowSize <= topRetainedSize,
+ "The shallow size should be less than or equal to the " +
+ "retained size");
+
+ var sumOfChildrensRetainedSizes = 0;
+ for (var i = 0; i < children.length; i++) {
+ sumOfChildrensRetainedSizes += dominatorTree.getRetainedSize(children[i]);
+ stack.push(children[i]);
+ }
+
+ fastAssert(sumOfChildrensRetainedSizes <= topRetainedSize,
+ "The sum of the children's retained sizes should be less than " +
+ "or equal to the retained size");
+ fastAssert(sumOfChildrensRetainedSizes + topShallowSize === topRetainedSize,
+ "The sum of the children's retained sizes plus the shallow " +
+ "size should be equal to the retained size");
+ }
+
+ ok(true, "Successfully walked the tree");
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_computeDominatorTree_01.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_computeDominatorTree_01.js
new file mode 100644
index 000000000..0114e0b69
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_computeDominatorTree_01.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test the HeapAnalyses{Client,Worker} "computeDominatorTree" request.
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ yield client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ const dominatorTreeId = yield client.computeDominatorTree(snapshotFilePath);
+ equal(typeof dominatorTreeId, "number",
+ "should get a dominator tree id, and it should be a number");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_computeDominatorTree_02.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_computeDominatorTree_02.js
new file mode 100644
index 000000000..6e3f5b257
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_computeDominatorTree_02.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test the HeapAnalyses{Client,Worker} "computeDominatorTree" request with bad
+// file paths.
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ const client = new HeapAnalysesClient();
+
+ let threw = false;
+ try {
+ yield client.computeDominatorTree("/etc/passwd");
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "should throw when given a bad path");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_deleteHeapSnapshot_01.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_deleteHeapSnapshot_01.js
new file mode 100644
index 000000000..7708de93c
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_deleteHeapSnapshot_01.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the HeapAnalyses{Client,Worker} can delete heap snapshots.
+
+function run_test() {
+ run_next_test();
+}
+
+const breakdown = {
+ by: "coarseType",
+ objects: { by: "count", count: true, bytes: true },
+ scripts: { by: "count", count: true, bytes: true },
+ strings: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+};
+
+add_task(function* () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ yield client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ let dominatorTreeId = yield client.computeDominatorTree(snapshotFilePath);
+ ok(true, "Should have computed the dominator tree");
+
+ yield client.deleteHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have deleted the snapshot");
+
+ let threw = false;
+ try {
+ yield client.getDominatorTree({
+ dominatorTreeId: dominatorTreeId,
+ breakdown
+ });
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "getDominatorTree on deleted tree should throw an error");
+
+ threw = false;
+ try {
+ yield client.computeDominatorTree(snapshotFilePath);
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "computeDominatorTree on deleted snapshot should throw an error");
+
+ threw = false;
+ try {
+ yield client.takeCensus(snapshotFilePath);
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "takeCensus on deleted tree should throw an error");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_deleteHeapSnapshot_02.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_deleteHeapSnapshot_02.js
new file mode 100644
index 000000000..3e25ddac4
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_deleteHeapSnapshot_02.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test deleteHeapSnapshot is a noop if the provided path matches no snapshot
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ const client = new HeapAnalysesClient();
+
+ let threw = false;
+ try {
+ yield client.deleteHeapSnapshot("path-does-not-exist");
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "deleteHeapSnapshot on non-existant path should throw an error");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_deleteHeapSnapshot_03.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_deleteHeapSnapshot_03.js
new file mode 100644
index 000000000..e648c9407
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_deleteHeapSnapshot_03.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test other dominatorTrees can still be retrieved after deleting a snapshot
+
+function run_test() {
+ run_next_test();
+}
+
+const breakdown = {
+ by: "coarseType",
+ objects: { by: "count", count: true, bytes: true },
+ scripts: { by: "count", count: true, bytes: true },
+ strings: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+};
+
+function* createSnapshotAndDominatorTree(client) {
+ let snapshotFilePath = saveNewHeapSnapshot();
+ yield client.readHeapSnapshot(snapshotFilePath);
+ let dominatorTreeId = yield client.computeDominatorTree(snapshotFilePath);
+ return { dominatorTreeId, snapshotFilePath };
+}
+
+add_task(function* () {
+ const client = new HeapAnalysesClient();
+
+ let savedSnapshots = [
+ yield createSnapshotAndDominatorTree(client),
+ yield createSnapshotAndDominatorTree(client),
+ yield createSnapshotAndDominatorTree(client)
+ ];
+ ok(true, "Create 3 snapshots and dominator trees");
+
+ yield client.deleteHeapSnapshot(savedSnapshots[1].snapshotFilePath);
+ ok(true, "Snapshot deleted");
+
+ let tree = yield client.getDominatorTree({
+ dominatorTreeId: savedSnapshots[0].dominatorTreeId,
+ breakdown
+ });
+ ok(tree, "Should get a valid tree for first snapshot");
+
+ let threw = false;
+ try {
+ yield client.getDominatorTree({
+ dominatorTreeId: savedSnapshots[1].dominatorTreeId,
+ breakdown
+ });
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "getDominatorTree on a deleted snapshot should throw an error");
+
+ tree = yield client.getDominatorTree({
+ dominatorTreeId: savedSnapshots[2].dominatorTreeId,
+ breakdown
+ });
+ ok(tree, "Should get a valid tree for third snapshot");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_getCensusIndividuals_01.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_getCensusIndividuals_01.js
new file mode 100644
index 000000000..b63ad4230
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_getCensusIndividuals_01.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the HeapAnalyses{Client,Worker} can get census individuals.
+
+function run_test() {
+ run_next_test();
+}
+
+const COUNT = { by: "count", count: true, bytes: true };
+
+const CENSUS_BREAKDOWN = {
+ by: "coarseType",
+ objects: COUNT,
+ strings: COUNT,
+ scripts: COUNT,
+ other: COUNT,
+};
+
+const LABEL_BREAKDOWN = {
+ by: "internalType",
+ then: COUNT,
+};
+
+const MAX_INDIVIDUALS = 10;
+
+add_task(function* () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ yield client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ const dominatorTreeId = yield client.computeDominatorTree(snapshotFilePath);
+ ok(true, "Should have computed dominator tree");
+
+ const { report } = yield client.takeCensus(snapshotFilePath,
+ { breakdown: CENSUS_BREAKDOWN },
+ { asTreeNode: true });
+ ok(report, "Should get a report");
+
+ let nodesWithLeafIndicesFound = 0;
+
+ yield* (function* assertCanGetIndividuals(censusNode) {
+ if (censusNode.reportLeafIndex !== undefined) {
+ nodesWithLeafIndicesFound++;
+
+ const response = yield client.getCensusIndividuals({
+ dominatorTreeId,
+ indices: DevToolsUtils.isSet(censusNode.reportLeafIndex)
+ ? censusNode.reportLeafIndex
+ : new Set([censusNode.reportLeafIndex]),
+ censusBreakdown: CENSUS_BREAKDOWN,
+ labelBreakdown: LABEL_BREAKDOWN,
+ maxRetainingPaths: 1,
+ maxIndividuals: MAX_INDIVIDUALS,
+ });
+
+ dumpn(`response = ${JSON.stringify(response, null, 4)}`);
+
+ equal(response.nodes.length, Math.min(MAX_INDIVIDUALS, censusNode.count),
+ "response.nodes.length === Math.min(MAX_INDIVIDUALS, censusNode.count)");
+
+ let lastRetainedSize = Infinity;
+ for (let individual of response.nodes) {
+ equal(typeof individual.nodeId, "number",
+ "individual.nodeId should be a number");
+ ok(individual.retainedSize <= lastRetainedSize,
+ "individual.retainedSize <= lastRetainedSize");
+ lastRetainedSize = individual.retainedSize;
+ ok(individual.shallowSize, "individual.shallowSize should exist and be non-zero");
+ ok(individual.shortestPaths, "individual.shortestPaths should exist");
+ ok(individual.shortestPaths.nodes, "individual.shortestPaths.nodes should exist");
+ ok(individual.shortestPaths.edges, "individual.shortestPaths.edges should exist");
+ ok(individual.label, "individual.label should exist");
+ }
+ }
+
+ if (censusNode.children) {
+ for (let child of censusNode.children) {
+ yield* assertCanGetIndividuals(child);
+ }
+ }
+ }(report));
+
+ equal(nodesWithLeafIndicesFound, 4, "Should have found a leaf for each coarse type");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_getCreationTime_01.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_getCreationTime_01.js
new file mode 100644
index 000000000..5df79de7a
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_getCreationTime_01.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the HeapAnalyses{Client,Worker} can get a HeapSnapshot's
+// creation time.
+
+function waitForThirtyMilliseconds() {
+ const start = Date.now();
+ while (Date.now() - start < 30) ;
+}
+
+function run_test() {
+ run_next_test();
+}
+
+const BREAKDOWN = {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true }
+};
+
+add_task(function* () {
+ const client = new HeapAnalysesClient();
+ const start = Date.now() * 1000;
+
+ // Because Date.now() is less precise than the snapshot's time stamp, give it
+ // a little bit of head room. Additionally, WinXP's timers have a granularity
+ // of only +/-15 ms.
+ waitForThirtyMilliseconds();
+ const snapshotFilePath = saveNewHeapSnapshot();
+ waitForThirtyMilliseconds();
+ const end = Date.now() * 1000;
+
+ yield client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ let threw = false;
+ try {
+ yield client.getCreationTime("/not/a/real/path", {
+ breakdown: BREAKDOWN
+ });
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "getCreationTime should throw when snapshot does not exist");
+
+ let time = yield client.getCreationTime(snapshotFilePath, {
+ breakdown: BREAKDOWN
+ });
+
+ dumpn("Start = " + start);
+ dumpn("End = " + end);
+ dumpn("Time = " + time);
+
+ ok(time >= start, "creation time occurred after start");
+ ok(time <= end, "creation time occurred before end");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_getDominatorTree_01.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_getDominatorTree_01.js
new file mode 100644
index 000000000..cedea5375
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_getDominatorTree_01.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test the HeapAnalyses{Client,Worker} "getDominatorTree" request.
+
+function run_test() {
+ run_next_test();
+}
+
+const breakdown = {
+ by: "coarseType",
+ objects: { by: "count", count: true, bytes: true },
+ scripts: { by: "count", count: true, bytes: true },
+ strings: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+};
+
+add_task(function* () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ yield client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ const dominatorTreeId = yield client.computeDominatorTree(snapshotFilePath);
+ equal(typeof dominatorTreeId, "number",
+ "should get a dominator tree id, and it should be a number");
+
+ const partialTree = yield client.getDominatorTree({
+ dominatorTreeId,
+ breakdown
+ });
+ ok(partialTree, "Should get a partial tree");
+ equal(typeof partialTree, "object",
+ "partialTree should be an object");
+
+ function checkTree(node) {
+ equal(typeof node.nodeId, "number", "each node should have an id");
+
+ if (node === partialTree) {
+ equal(node.parentId, undefined, "the root has no parent");
+ } else {
+ equal(typeof node.parentId, "number", "each node should have a parent id");
+ }
+
+ equal(typeof node.retainedSize, "number",
+ "each node should have a retained size");
+
+ ok(node.children === undefined || Array.isArray(node.children),
+ "each node either has a list of children, or undefined meaning no children loaded");
+ equal(typeof node.moreChildrenAvailable, "boolean",
+ "each node should indicate if there are more children available or not");
+
+ equal(typeof node.shortestPaths, "object",
+ "Should have shortest paths");
+ equal(typeof node.shortestPaths.nodes, "object",
+ "Should have shortest paths' nodes");
+ equal(typeof node.shortestPaths.edges, "object",
+ "Should have shortest paths' edges");
+
+ if (node.children) {
+ node.children.forEach(checkTree);
+ }
+ }
+
+ checkTree(partialTree);
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_getDominatorTree_02.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_getDominatorTree_02.js
new file mode 100644
index 000000000..fd29beece
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_getDominatorTree_02.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test the HeapAnalyses{Client,Worker} "getDominatorTree" request with bad
+// dominator tree ids.
+
+function run_test() {
+ run_next_test();
+}
+
+const breakdown = {
+ by: "coarseType",
+ objects: { by: "count", count: true, bytes: true },
+ scripts: { by: "count", count: true, bytes: true },
+ strings: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+};
+
+add_task(function* () {
+ const client = new HeapAnalysesClient();
+
+ let threw = false;
+ try {
+ yield client.getDominatorTree({ dominatorTreeId: 42, breakdown });
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "should throw when given a bad id");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_getImmediatelyDominated_01.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_getImmediatelyDominated_01.js
new file mode 100644
index 000000000..caf1c2056
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_getImmediatelyDominated_01.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test the HeapAnalyses{Client,Worker} "getImmediatelyDominated" request.
+
+function run_test() {
+ run_next_test();
+}
+
+const breakdown = {
+ by: "coarseType",
+ objects: { by: "count", count: true, bytes: true },
+ scripts: { by: "count", count: true, bytes: true },
+ strings: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+};
+
+add_task(function* () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ yield client.readHeapSnapshot(snapshotFilePath);
+ const dominatorTreeId = yield client.computeDominatorTree(snapshotFilePath);
+
+ const partialTree = yield client.getDominatorTree({
+ dominatorTreeId,
+ breakdown
+ });
+ ok(partialTree.children.length > 0,
+ "root should immediately dominate some nodes");
+
+ // First, test getting a subset of children available.
+ const response = yield client.getImmediatelyDominated({
+ dominatorTreeId,
+ breakdown,
+ nodeId: partialTree.nodeId,
+ startIndex: 0,
+ maxCount: partialTree.children.length - 1
+ });
+
+ ok(Array.isArray(response.nodes));
+ ok(response.nodes.every(node => node.parentId === partialTree.nodeId));
+ ok(response.moreChildrenAvailable);
+ equal(response.path.length, 1);
+ equal(response.path[0], partialTree.nodeId);
+
+ for (let node of response.nodes) {
+ equal(typeof node.shortestPaths, "object",
+ "Should have shortest paths");
+ equal(typeof node.shortestPaths.nodes, "object",
+ "Should have shortest paths' nodes");
+ equal(typeof node.shortestPaths.edges, "object",
+ "Should have shortest paths' edges");
+ }
+
+ // Next, test getting a subset of children available.
+ const secondResponse = yield client.getImmediatelyDominated({
+ dominatorTreeId,
+ breakdown,
+ nodeId: partialTree.nodeId,
+ startIndex: 0,
+ maxCount: Infinity
+ });
+
+ ok(Array.isArray(secondResponse.nodes));
+ ok(secondResponse.nodes.every(node => node.parentId === partialTree.nodeId));
+ ok(!secondResponse.moreChildrenAvailable);
+ equal(secondResponse.path.length, 1);
+ equal(secondResponse.path[0], partialTree.nodeId);
+
+ for (let node of secondResponse.nodes) {
+ equal(typeof node.shortestPaths, "object",
+ "Should have shortest paths");
+ equal(typeof node.shortestPaths.nodes, "object",
+ "Should have shortest paths' nodes");
+ equal(typeof node.shortestPaths.edges, "object",
+ "Should have shortest paths' edges");
+ }
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_readHeapSnapshot_01.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_readHeapSnapshot_01.js
new file mode 100644
index 000000000..0d0d58bef
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_readHeapSnapshot_01.js
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the HeapAnalyses{Client,Worker} can read heap snapshots.
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ yield client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensusDiff_01.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensusDiff_01.js
new file mode 100644
index 000000000..6f22cbad3
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensusDiff_01.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the HeapAnalyses{Client,Worker} can take diffs between censuses.
+
+function run_test() {
+ run_next_test();
+}
+
+const BREAKDOWN = {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: false },
+ other: { by: "count", count: true, bytes: false },
+};
+
+add_task(function* () {
+ const client = new HeapAnalysesClient();
+
+ const markers = [allocationMarker()];
+
+ const firstSnapshotFilePath = saveNewHeapSnapshot();
+
+ // Allocate and hold an additional AllocationMarker object so we can see it in
+ // the next heap snapshot.
+ markers.push(allocationMarker());
+
+ const secondSnapshotFilePath = saveNewHeapSnapshot();
+
+ yield client.readHeapSnapshot(firstSnapshotFilePath);
+ yield client.readHeapSnapshot(secondSnapshotFilePath);
+ ok(true, "Should have read both heap snapshot files");
+
+ const { delta } = yield client.takeCensusDiff(firstSnapshotFilePath,
+ secondSnapshotFilePath,
+ { breakdown: BREAKDOWN });
+
+ equal(delta.AllocationMarker.count, 1,
+ "There exists one new AllocationMarker in the second heap snapshot");
+
+ const { delta: deltaTreeNode } = yield client.takeCensusDiff(firstSnapshotFilePath,
+ secondSnapshotFilePath,
+ { breakdown: BREAKDOWN },
+ { asTreeNode: true });
+
+ // Have to manually set these because symbol properties aren't structured
+ // cloned.
+ delta[CensusUtils.basisTotalBytes] = deltaTreeNode.totalBytes;
+ delta[CensusUtils.basisTotalCount] = deltaTreeNode.totalCount;
+
+ compareCensusViewData(BREAKDOWN, delta, deltaTreeNode,
+ "Returning delta-census as a tree node represents same data as the report");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensusDiff_02.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensusDiff_02.js
new file mode 100644
index 000000000..f1ba9ce84
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensusDiff_02.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the HeapAnalyses{Client,Worker} can take diffs between censuses as
+// inverted trees.
+
+function run_test() {
+ run_next_test();
+}
+
+const BREAKDOWN = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+};
+
+add_task(function* () {
+ const firstSnapshotFilePath = saveNewHeapSnapshot();
+ const secondSnapshotFilePath = saveNewHeapSnapshot();
+
+ const client = new HeapAnalysesClient();
+ yield client.readHeapSnapshot(firstSnapshotFilePath);
+ yield client.readHeapSnapshot(secondSnapshotFilePath);
+
+ ok(true, "Should have read both heap snapshot files");
+
+ const { delta } = yield client.takeCensusDiff(firstSnapshotFilePath,
+ secondSnapshotFilePath,
+ { breakdown: BREAKDOWN });
+
+ const { delta: deltaTreeNode } = yield client.takeCensusDiff(firstSnapshotFilePath,
+ secondSnapshotFilePath,
+ { breakdown: BREAKDOWN },
+ { asInvertedTreeNode: true });
+
+ // Have to manually set these because symbol properties aren't structured
+ // cloned.
+ delta[CensusUtils.basisTotalBytes] = deltaTreeNode.totalBytes;
+ delta[CensusUtils.basisTotalCount] = deltaTreeNode.totalCount;
+
+ compareCensusViewData(BREAKDOWN, delta, deltaTreeNode, { invert: true });
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_01.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_01.js
new file mode 100644
index 000000000..e26981db4
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_01.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the HeapAnalyses{Client,Worker} can take censuses.
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ yield client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ const { report } = yield client.takeCensus(snapshotFilePath);
+ ok(report, "Should get a report");
+ equal(typeof report, "object", "report should be an object");
+
+ ok(report.objects);
+ ok(report.scripts);
+ ok(report.strings);
+ ok(report.other);
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_02.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_02.js
new file mode 100644
index 000000000..34494af70
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_02.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the HeapAnalyses{Client,Worker} can take censuses with breakdown
+// options.
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ yield client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ const { report } = yield client.takeCensus(snapshotFilePath, {
+ breakdown: { by: "count", count: true, bytes: true }
+ });
+
+ ok(report, "Should get a report");
+ equal(typeof report, "object", "report should be an object");
+
+ ok(report.count);
+ ok(report.bytes);
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_03.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_03.js
new file mode 100644
index 000000000..486e250b5
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_03.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the HeapAnalyses{Client,Worker} bubbles errors properly when things
+// go wrong.
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ const client = new HeapAnalysesClient();
+
+ // Snapshot file path to a file that doesn't exist.
+ let failed = false;
+ try {
+ yield client.readHeapSnapshot(getFilePath("foo-bar-baz" + Math.random(), true));
+ } catch (e) {
+ failed = true;
+ }
+ ok(failed, "should not read heap snapshots that do not exist");
+
+ // Snapshot file path to a file that is not a heap snapshot.
+ failed = false;
+ try {
+ yield client.readHeapSnapshot(getFilePath("test_HeapAnalyses_takeCensus_03.js"));
+ } catch (e) {
+ failed = true;
+ }
+ ok(failed, "should not be able to read a file that is not a heap snapshot as a heap snapshot");
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ yield client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ // Bad census breakdown options.
+ failed = false;
+ try {
+ yield client.takeCensus(snapshotFilePath, {
+ breakdown: { by: "some classification that we do not have" }
+ });
+ } catch (e) {
+ failed = true;
+ }
+ ok(failed, "should not be able to breakdown by an unknown classification");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_04.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_04.js
new file mode 100644
index 000000000..769a2d99d
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_04.js
@@ -0,0 +1,118 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the HeapAnalyses{Client,Worker} can send SavedFrame stacks from
+// by-allocation-stack reports from the worker.
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test() {
+ const client = new HeapAnalysesClient();
+
+ // Track some allocation stacks.
+
+ const g = newGlobal();
+ const dbg = new Debugger(g);
+ g.eval(` // 1
+ this.log = []; // 2
+ function f() { this.log.push(allocationMarker()); } // 3
+ function g() { this.log.push(allocationMarker()); } // 4
+ function h() { this.log.push(allocationMarker()); } // 5
+ `); // 6
+
+ // Create one allocationMarker with tracking turned off,
+ // so it will have no associated stack.
+ g.f();
+
+ dbg.memory.allocationSamplingProbability = 1;
+
+ for (let [func, n] of [ [g.f, 20],
+ [g.g, 10],
+ [g.h, 5] ]) {
+ for (let i = 0; i < n; i++) {
+ dbg.memory.trackingAllocationSites = true;
+ // All allocations of allocationMarker occur with this line as the oldest
+ // stack frame.
+ func();
+ dbg.memory.trackingAllocationSites = false;
+ }
+ }
+
+ // Take a heap snapshot.
+
+ const snapshotFilePath = saveNewHeapSnapshot({ debugger: dbg });
+ yield client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ // Run a census broken down by class name -> allocation stack so we can grab
+ // only the AllocationMarker objects we have complete control over.
+
+ const { report } = yield client.takeCensus(snapshotFilePath, {
+ breakdown: { by: "objectClass",
+ then: { by: "allocationStack",
+ then: { by: "count",
+ bytes: true,
+ count: true
+ },
+ noStack: { by: "count",
+ bytes: true,
+ count: true
+ }
+ }
+ }
+ });
+
+ // Test the generated report.
+
+ ok(report, "Should get a report");
+
+ const map = report.AllocationMarker;
+ ok(map, "Should get AllocationMarkers in the report.");
+ // From a module with a different global, and therefore a different Map
+ // constructor, so we can't use instanceof.
+ equal(map.__proto__.constructor.name, "Map");
+
+ equal(map.size, 4, "Should have 4 allocation stacks (including the lack of a stack)");
+
+ // Gather the stacks we are expecting to appear as keys, and
+ // check that there are no unexpected keys.
+ let stacks = {};
+
+ map.forEach((v, k) => {
+ if (k === "noStack") {
+ // No need to save this key.
+ } else if (k.functionDisplayName === "f" &&
+ k.parent.functionDisplayName === "test") {
+ stacks.f = k;
+ } else if (k.functionDisplayName === "g" &&
+ k.parent.functionDisplayName === "test") {
+ stacks.g = k;
+ } else if (k.functionDisplayName === "h" &&
+ k.parent.functionDisplayName === "test") {
+ stacks.h = k;
+ } else {
+ dumpn("Unexpected allocation stack:");
+ k.toString().split(/\n/g).forEach(s => dumpn(s));
+ ok(false);
+ }
+ });
+
+ ok(map.get("noStack"));
+ equal(map.get("noStack").count, 1);
+
+ ok(stacks.f);
+ ok(map.get(stacks.f));
+ equal(map.get(stacks.f).count, 20);
+
+ ok(stacks.g);
+ ok(map.get(stacks.g));
+ equal(map.get(stacks.g).count, 10);
+
+ ok(stacks.h);
+ ok(map.get(stacks.h));
+ equal(map.get(stacks.h).count, 5);
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_05.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_05.js
new file mode 100644
index 000000000..7e16d9f00
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_05.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the HeapAnalyses{Client,Worker} can take censuses and return
+// a CensusTreeNode.
+
+function run_test() {
+ run_next_test();
+}
+
+const BREAKDOWN = {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true }
+};
+
+add_task(function* () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ yield client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ const { report } = yield client.takeCensus(snapshotFilePath, {
+ breakdown: BREAKDOWN
+ });
+
+ const { report: treeNode } = yield client.takeCensus(snapshotFilePath, {
+ breakdown: BREAKDOWN
+ }, {
+ asTreeNode: true
+ });
+
+ ok(treeNode.children.length > 0, "treeNode has children");
+ ok(treeNode.children.every(type => {
+ return "name" in type &&
+ "bytes" in type &&
+ "count" in type;
+ }), "all of tree node's children have name, bytes, count");
+
+ compareCensusViewData(BREAKDOWN, report, treeNode,
+ "Returning census as a tree node represents same data as the report");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_06.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_06.js
new file mode 100644
index 000000000..7795a9700
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_06.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the HeapAnalyses{Client,Worker} can take censuses by
+// "allocationStack" and return a CensusTreeNode.
+
+function run_test() {
+ run_next_test();
+}
+
+const BREAKDOWN = {
+ by: "objectClass",
+ then: {
+ by: "allocationStack",
+ then: { by: "count", count: true, bytes: true },
+ noStack: { by: "count", count: true, bytes: true }
+ },
+ other: { by: "count", count: true, bytes: true }
+};
+
+add_task(function* () {
+ const g = newGlobal();
+ const dbg = new Debugger(g);
+
+ // 5 allocation markers with no stack.
+ g.eval(`
+ this.markers = [];
+ for (var i = 0; i < 5; i++) {
+ markers.push(allocationMarker());
+ }
+ `);
+
+ dbg.memory.allocationSamplingProbability = 1;
+ dbg.memory.trackingAllocationSites = true;
+
+ // 5 allocation markers at 5 stacks.
+ g.eval(`
+ (function shouldHaveCountOfOne() {
+ markers.push(allocationMarker());
+ markers.push(allocationMarker());
+ markers.push(allocationMarker());
+ markers.push(allocationMarker());
+ markers.push(allocationMarker());
+ }());
+ `);
+
+ // 5 allocation markers at 1 stack.
+ g.eval(`
+ (function shouldHaveCountOfFive() {
+ for (var i = 0; i < 5; i++) {
+ markers.push(allocationMarker());
+ }
+ }());
+ `);
+
+ const snapshotFilePath = saveNewHeapSnapshot({ debugger: dbg });
+
+ const client = new HeapAnalysesClient();
+ yield client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ const { report } = yield client.takeCensus(snapshotFilePath, {
+ breakdown: BREAKDOWN
+ });
+
+ const { report: treeNode } = yield client.takeCensus(snapshotFilePath, {
+ breakdown: BREAKDOWN
+ }, {
+ asTreeNode: true
+ });
+
+ const markers = treeNode.children.find(c => c.name === "AllocationMarker");
+ ok(markers);
+
+ const noStack = markers.children.find(c => c.name === "noStack");
+ equal(noStack.count, 5);
+
+ let numShouldHaveFiveFound = 0;
+ let numShouldHaveOneFound = 0;
+
+ function walk(node) {
+ if (node.children) {
+ node.children.forEach(walk);
+ }
+
+ if (!isSavedFrame(node.name)) {
+ return;
+ }
+
+ if (node.name.functionDisplayName === "shouldHaveCountOfFive") {
+ equal(node.count, 5, "shouldHaveCountOfFive should have count of five");
+ numShouldHaveFiveFound++;
+ }
+
+ if (node.name.functionDisplayName === "shouldHaveCountOfOne") {
+ equal(node.count, 1, "shouldHaveCountOfOne should have count of one");
+ numShouldHaveOneFound++;
+ }
+ }
+ markers.children.forEach(walk);
+
+ equal(numShouldHaveFiveFound, 1);
+ equal(numShouldHaveOneFound, 5);
+
+ compareCensusViewData(BREAKDOWN, report, treeNode,
+ "Returning census as a tree node represents same data as the report");
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_07.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_07.js
new file mode 100644
index 000000000..986b3aaa8
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapAnalyses_takeCensus_07.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the HeapAnalyses{Client,Worker} can take censuses and return
+// an inverted CensusTreeNode.
+
+function run_test() {
+ run_next_test();
+}
+
+const BREAKDOWN = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+};
+
+add_task(function* () {
+ const client = new HeapAnalysesClient();
+
+ const snapshotFilePath = saveNewHeapSnapshot();
+ yield client.readHeapSnapshot(snapshotFilePath);
+ ok(true, "Should have read the heap snapshot");
+
+ const { report } = yield client.takeCensus(snapshotFilePath, {
+ breakdown: BREAKDOWN
+ });
+
+ const { report: treeNode } = yield client.takeCensus(snapshotFilePath, {
+ breakdown: BREAKDOWN
+ }, {
+ asInvertedTreeNode: true
+ });
+
+ compareCensusViewData(BREAKDOWN, report, treeNode, { invert: true });
+
+ client.destroy();
+});
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_computeShortestPaths_01.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_computeShortestPaths_01.js
new file mode 100644
index 000000000..2ec577bd0
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_computeShortestPaths_01.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Sanity test that we can compute shortest paths.
+//
+// Because the actual heap graph is too unpredictable and likely to drastically
+// change as various implementation bits change, we don't test exact paths
+// here. See js/src/jsapi-tests/testUbiNode.cpp for such tests, where we can
+// control the specific graph shape and structure and so testing exact paths is
+// reliable.
+
+function run_test() {
+ const path = ChromeUtils.saveHeapSnapshot({ runtime: true });
+ const snapshot = ChromeUtils.readHeapSnapshot(path);
+
+ const dominatorTree = snapshot.computeDominatorTree();
+ const dominatedByRoot = dominatorTree.getImmediatelyDominated(dominatorTree.root)
+ .slice(0, 10);
+ ok(dominatedByRoot);
+ ok(dominatedByRoot.length);
+
+ const targetSet = new Set(dominatedByRoot);
+
+ const shortestPaths = snapshot.computeShortestPaths(dominatorTree.root, dominatedByRoot, 2);
+ ok(shortestPaths);
+ ok(shortestPaths instanceof Map);
+ ok(shortestPaths.size === targetSet.size);
+
+ for (let [target, paths] of shortestPaths) {
+ ok(targetSet.has(target),
+ "We should only get paths for our targets");
+ targetSet.delete(target);
+
+ ok(paths.length > 0,
+ "We must have at least one path, since the target is dominated by the root");
+ ok(paths.length <= 2,
+ "Should not have recorded more paths than the max requested");
+
+ dumpn("---------------------");
+ dumpn("Shortest paths for 0x" + target.toString(16) + ":");
+ for (let path of paths) {
+ dumpn(" path =");
+ for (let part of path) {
+ dumpn(" predecessor: 0x" + part.predecessor.toString(16) +
+ "; edge: " + part.edge);
+ }
+ }
+ dumpn("---------------------");
+
+ for (let path of paths) {
+ ok(path.length > 0, "Cannot have zero length paths");
+ ok(path[0].predecessor === dominatorTree.root,
+ "The first predecessor is always our start node");
+
+ for (let part of path) {
+ ok(part.predecessor, "Each part of a path has a predecessor");
+ ok(!!snapshot.describeNode({ by: "count", count: true, bytes: true},
+ part.predecessor),
+ "The predecessor is in the heap snapshot");
+ ok("edge" in part, "Each part has an (potentially null) edge property");
+ }
+ }
+ }
+
+ ok(targetSet.size === 0,
+ "We found paths for all of our targets");
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_computeShortestPaths_02.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_computeShortestPaths_02.js
new file mode 100644
index 000000000..04fe58733
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_computeShortestPaths_02.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test computing shortest paths with invalid arguments.
+
+function run_test() {
+ const path = ChromeUtils.saveHeapSnapshot({ runtime: true });
+ const snapshot = ChromeUtils.readHeapSnapshot(path);
+
+ const dominatorTree = snapshot.computeDominatorTree();
+ const target = dominatorTree.getImmediatelyDominated(dominatorTree.root).pop();
+ ok(target);
+
+ let threw = false;
+ try {
+ snapshot.computeShortestPaths(0, [target], 2);
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "invalid start node should throw");
+
+ threw = false;
+ try {
+ snapshot.computeShortestPaths(dominatorTree.root, [0], 2);
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "invalid target nodes should throw");
+
+ threw = false;
+ try {
+ snapshot.computeShortestPaths(dominatorTree.root, [], 2);
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "empty target nodes should throw");
+
+ threw = false;
+ try {
+ snapshot.computeShortestPaths(dominatorTree.root, [target], 0);
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "0 max paths should throw");
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_creationTime_01.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_creationTime_01.js
new file mode 100644
index 000000000..0d08fea16
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_creationTime_01.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// HeapSnapshot.prototype.creationTime returns the expected time.
+
+function waitForThirtyMilliseconds() {
+ const start = Date.now();
+ while (Date.now() - start < 30) ;
+}
+
+function run_test() {
+ const start = Date.now() * 1000;
+ do_print("start = " + start);
+
+ // Because Date.now() is less precise than the snapshot's time stamp, give it
+ // a little bit of head room. Additionally, WinXP's timer only has granularity
+ // of +/- 15ms.
+ waitForThirtyMilliseconds();
+ const path = ChromeUtils.saveHeapSnapshot({ runtime: true });
+ waitForThirtyMilliseconds();
+
+ const end = Date.now() * 1000;
+ do_print("end = " + end);
+
+ const snapshot = ChromeUtils.readHeapSnapshot(path);
+ do_print("snapshot.creationTime = " + snapshot.creationTime);
+
+ ok(snapshot.creationTime >= start);
+ ok(snapshot.creationTime <= end);
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_deepStack_01.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_deepStack_01.js
new file mode 100644
index 000000000..9eb11d9af
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_deepStack_01.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can save a core dump with very deep allocation stacks and read
+// it back into a HeapSnapshot.
+
+function stackDepth(stack) {
+ return stack ? 1 + stackDepth(stack.parent) : 0;
+}
+
+function run_test() {
+ // Create a Debugger observing a debuggee's allocations.
+ const debuggee = new Cu.Sandbox(null);
+ const dbg = new Debugger(debuggee);
+ dbg.memory.trackingAllocationSites = true;
+
+ // Allocate some objects in the debuggee that will have their allocation
+ // stacks recorded by the Debugger.
+
+ debuggee.eval("this.objects = []");
+ debuggee.eval(
+ (function recursiveAllocate(n) {
+ if (n <= 0)
+ return;
+
+ // Make sure to recurse before pushing the object so that when TCO is
+ // implemented sometime in the future, it doesn't invalidate this test.
+ recursiveAllocate(n - 1);
+ this.objects.push({});
+ }).toString()
+ );
+ debuggee.eval("recursiveAllocate = recursiveAllocate.bind(this);");
+ debuggee.eval("recursiveAllocate(200);");
+
+ // Now save a snapshot that will include the allocation stacks and read it
+ // back again.
+
+ const filePath = ChromeUtils.saveHeapSnapshot({ runtime: true });
+ ok(true, "Should be able to save a snapshot.");
+
+ const snapshot = ChromeUtils.readHeapSnapshot(filePath);
+ ok(snapshot, "Should be able to read a heap snapshot");
+ ok(snapshot instanceof HeapSnapshot, "Should be an instanceof HeapSnapshot");
+
+ const report = snapshot.takeCensus({
+ breakdown: { by: "allocationStack",
+ then: { by: "count", bytes: true, count: true },
+ noStack: { by: "count", bytes: true, count: true }
+ }
+ });
+
+ // Keep this synchronized with `HeapSnapshot::MAX_STACK_DEPTH`!
+ const MAX_STACK_DEPTH = 60;
+
+ let foundStacks = false;
+ report.forEach((v, k) => {
+ if (k === "noStack") {
+ return;
+ }
+
+ foundStacks = true;
+ const depth = stackDepth(k);
+ dumpn("Stack depth is " + depth);
+ ok(depth <= MAX_STACK_DEPTH,
+ "Every stack should have depth less than or equal to the maximum stack depth");
+ });
+ ok(foundStacks);
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_describeNode_01.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_describeNode_01.js
new file mode 100644
index 000000000..d79cb5a7b
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_describeNode_01.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can describe nodes with a breakdown.
+
+function run_test() {
+ const path = saveNewHeapSnapshot();
+ const snapshot = ChromeUtils.readHeapSnapshot(path);
+ ok(snapshot.describeNode);
+ equal(typeof snapshot.describeNode, "function");
+
+ const dt = snapshot.computeDominatorTree();
+
+ let threw = false;
+ try {
+ snapshot.describeNode(undefined, dt.root);
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "Should require a breakdown");
+
+ const breakdown = {
+ by: "coarseType",
+ objects: { by: "objectClass" },
+ scripts: { by: "internalType" },
+ strings: { by: "internalType" },
+ other: { by: "internalType" }
+ };
+
+ threw = false;
+ try {
+ snapshot.describeNode(breakdown, 0);
+ } catch (_) {
+ threw = true;
+ }
+ ok(threw, "Should throw when given an invalid node id");
+
+ const description = snapshot.describeNode(breakdown, dt.root);
+ ok(description);
+ ok(description.other);
+ ok(description.other["JS::ubi::RootList"]);
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_01.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_01.js
new file mode 100644
index 000000000..f3b3090b0
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_01.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// HeapSnapshot.prototype.takeCensus returns a value of an appropriate
+// shape. Ported from js/src/jit-tests/debug/Memory-takeCensus-01.js
+
+function run_test() {
+ var dbg = new Debugger;
+
+ function checkProperties(census) {
+ equal(typeof census, "object");
+ for (prop of Object.getOwnPropertyNames(census)) {
+ var desc = Object.getOwnPropertyDescriptor(census, prop);
+ equal(desc.enumerable, true);
+ equal(desc.configurable, true);
+ equal(desc.writable, true);
+ if (typeof desc.value === "object")
+ checkProperties(desc.value);
+ else
+ equal(typeof desc.value, "number");
+ }
+ }
+
+ checkProperties(saveHeapSnapshotAndTakeCensus(dbg));
+
+ var g = newGlobal();
+ dbg.addDebuggee(g);
+ checkProperties(saveHeapSnapshotAndTakeCensus(dbg));
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_02.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_02.js
new file mode 100644
index 000000000..680ac9b58
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_02.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// HeapSnapshot.prototype.takeCensus behaves plausibly as we allocate objects.
+//
+// Exact object counts vary in ways we can't predict. For example,
+// BaselineScripts can hold onto "template objects", which exist only to hold
+// the shape and type for newly created objects. When BaselineScripts are
+// discarded, these template objects go with them.
+//
+// So instead of expecting precise counts, we expect counts that are at least as
+// many as we would expect given the object graph we've built.
+//
+// Ported from js/src/jit-tests/debug/Memory-takeCensus-02.js
+
+function run_test() {
+ // A Debugger with no debuggees had better not find anything.
+ var dbg = new Debugger;
+ var census0 = saveHeapSnapshotAndTakeCensus(dbg);
+ Census.walkCensus(census0, "census0", Census.assertAllZeros);
+
+ function newGlobalWithDefs() {
+ var g = newGlobal();
+ g.eval(`
+ function times(n, fn) {
+ var a=[];
+ for (var i = 0; i<n; i++)
+ a.push(fn());
+ return a;
+ }
+ `);
+ return g;
+ }
+
+ // Allocate a large number of various types of objects, and check that census
+ // finds them.
+ var g = newGlobalWithDefs();
+ dbg.addDebuggee(g);
+
+ g.eval("var objs = times(100, () => ({}));");
+ g.eval("var rxs = times(200, () => /foo/);");
+ g.eval("var ars = times(400, () => []);");
+ g.eval("var fns = times(800, () => () => {});");
+
+ var census1 = dbg.memory.takeCensus(dbg);
+ Census.walkCensus(census1, "census1",
+ Census.assertAllNotLessThan(
+ { "objects":
+ { "Object": { count: 100 },
+ "RegExp": { count: 200 },
+ "Array": { count: 400 },
+ "Function": { count: 800 }
+ }
+ }));
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_03.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_03.js
new file mode 100644
index 000000000..25f2c3791
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_03.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// HeapSnapshot.prototype.takeCensus behaves plausibly as we add and remove
+// debuggees.
+//
+// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-03.js
+
+function run_test() {
+ var dbg = new Debugger;
+
+ var census0 = saveHeapSnapshotAndTakeCensus(dbg);
+ Census.walkCensus(census0, "census0", Census.assertAllZeros);
+
+ var g1 = newGlobal();
+ dbg.addDebuggee(g1);
+ var census1 = saveHeapSnapshotAndTakeCensus(dbg);
+ Census.walkCensus(census1, "census1", Census.assertAllNotLessThan(census0));
+
+ var g2 = newGlobal();
+ dbg.addDebuggee(g2);
+ var census2 = saveHeapSnapshotAndTakeCensus(dbg);
+ Census.walkCensus(census2, "census2", Census.assertAllNotLessThan(census1));
+
+ dbg.removeDebuggee(g2);
+ var census3 = saveHeapSnapshotAndTakeCensus(dbg);
+ Census.walkCensus(census3, "census3", Census.assertAllEqual(census1));
+
+ dbg.removeDebuggee(g1);
+ var census4 = saveHeapSnapshotAndTakeCensus(dbg);
+ Census.walkCensus(census4, "census4", Census.assertAllEqual(census0));
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_04.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_04.js
new file mode 100644
index 000000000..799844cde
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_04.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that HeapSnapshot.prototype.takeCensus finds GC roots that are on the
+// stack.
+//
+// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-04.js
+
+function run_test() {
+ var g = newGlobal();
+ var dbg = new Debugger(g);
+
+ g.eval(`
+function withAllocationMarkerOnStack(f) {
+ (function () {
+ var onStack = allocationMarker();
+ f();
+ }());
+}
+`);
+
+ equal("AllocationMarker" in saveHeapSnapshotAndTakeCensus(dbg).objects, false,
+ "There shouldn't exist any allocation markers in the census.");
+
+ var allocationMarkerCount;
+ g.withAllocationMarkerOnStack(() => {
+ const census = saveHeapSnapshotAndTakeCensus(dbg);
+ allocationMarkerCount = census.objects.AllocationMarker.count;
+ });
+
+ equal(allocationMarkerCount, 1,
+ "Should have one allocation marker in the census, because there " +
+ "was one on the stack.");
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_05.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_05.js
new file mode 100644
index 000000000..da6067624
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_05.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that HeapSnapshot.prototype.takeCensus finds cross compartment
+// wrapper GC roots.
+//
+// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-05.js
+
+function run_test() {
+ var g = newGlobal();
+ var dbg = new Debugger(g);
+
+ equal("AllocationMarker" in saveHeapSnapshotAndTakeCensus(dbg).objects, false,
+ "No allocation markers should exist in the census.");
+
+ this.ccw = g.allocationMarker();
+
+ const census = saveHeapSnapshotAndTakeCensus(dbg);
+ equal(census.objects.AllocationMarker.count, 1,
+ "Should have one allocation marker in the census, because there " +
+ "is one cross-compartment wrapper referring to it.");
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_06.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_06.js
new file mode 100644
index 000000000..0412410c0
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_06.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Check HeapSnapshot.prototype.takeCensus handling of 'breakdown' argument.
+//
+// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-06.js
+
+function run_test() {
+ var Pattern = Match.Pattern;
+
+ var g = newGlobal();
+ var dbg = new Debugger(g);
+
+ Pattern({ count: Pattern.NATURAL,
+ bytes: Pattern.NATURAL })
+ .assert(saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "count" } }));
+
+ let census = saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "count", count: false, bytes: false } });
+ equal("count" in census, false);
+ equal("bytes" in census, false);
+
+ census = saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "count", count: true, bytes: false } });
+ equal("count" in census, true);
+ equal("bytes" in census, false);
+
+ census = saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "count", count: false, bytes: true } });
+ equal("count" in census, false);
+ equal("bytes" in census, true);
+
+ census = saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "count", count: true, bytes: true } });
+ equal("count" in census, true);
+ equal("bytes" in census, true);
+
+
+ // Pattern doesn't mind objects with extra properties, so we'll restrict this
+ // list to the object classes we're pretty sure are going to stick around for
+ // the forseeable future.
+ Pattern({
+ Function: { count: Pattern.NATURAL },
+ Object: { count: Pattern.NATURAL },
+ Debugger: { count: Pattern.NATURAL },
+ Sandbox: { count: Pattern.NATURAL },
+
+ // The below are all Debugger prototype objects.
+ Source: { count: Pattern.NATURAL },
+ Environment: { count: Pattern.NATURAL },
+ Script: { count: Pattern.NATURAL },
+ Memory: { count: Pattern.NATURAL },
+ Frame: { count: Pattern.NATURAL }
+ })
+ .assert(saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "objectClass" } }));
+
+ Pattern({
+ objects: { count: Pattern.NATURAL },
+ scripts: { count: Pattern.NATURAL },
+ strings: { count: Pattern.NATURAL },
+ other: { count: Pattern.NATURAL }
+ })
+ .assert(saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "coarseType" } }));
+
+ // As for { by: 'objectClass' }, restrict our pattern to the types
+ // we predict will stick around for a long time.
+ Pattern({
+ JSString: { count: Pattern.NATURAL },
+ "js::Shape": { count: Pattern.NATURAL },
+ JSObject: { count: Pattern.NATURAL },
+ JSScript: { count: Pattern.NATURAL }
+ })
+ .assert(saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "internalType" } }));
+
+
+ // Nested breakdowns.
+
+ let coarseTypePattern = {
+ objects: { count: Pattern.NATURAL },
+ scripts: { count: Pattern.NATURAL },
+ strings: { count: Pattern.NATURAL },
+ other: { count: Pattern.NATURAL }
+ };
+
+ Pattern({
+ JSString: coarseTypePattern,
+ "js::Shape": coarseTypePattern,
+ JSObject: coarseTypePattern,
+ JSScript: coarseTypePattern,
+ })
+ .assert(saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: { by: "internalType",
+ then: { by: "coarseType" }
+ }
+ }));
+
+ Pattern({
+ Function: { count: Pattern.NATURAL },
+ Object: { count: Pattern.NATURAL },
+ Debugger: { count: Pattern.NATURAL },
+ Sandbox: { count: Pattern.NATURAL },
+ other: coarseTypePattern
+ })
+ .assert(saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: {
+ by: "objectClass",
+ then: { by: "count" },
+ other: { by: "coarseType" }
+ }
+ }));
+
+ Pattern({
+ objects: { count: Pattern.NATURAL, label: "object" },
+ scripts: { count: Pattern.NATURAL, label: "scripts" },
+ strings: { count: Pattern.NATURAL, label: "strings" },
+ other: { count: Pattern.NATURAL, label: "other" }
+ })
+ .assert(saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: {
+ by: "coarseType",
+ objects: { by: "count", label: "object" },
+ scripts: { by: "count", label: "scripts" },
+ strings: { by: "count", label: "strings" },
+ other: { by: "count", label: "other" }
+ }
+ }));
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_07.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_07.js
new file mode 100644
index 000000000..f5c36056f
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_07.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// HeapSnapshot.prototype.takeCensus breakdown: check error handling on property
+// gets.
+//
+// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-07.js
+
+function run_test() {
+ var g = newGlobal();
+ var dbg = new Debugger(g);
+
+ assertThrowsValue(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: { get by() { throw "ಠ_ಠ"; } }
+ });
+ }, "ಠ_ಠ");
+
+
+
+ assertThrowsValue(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: { by: "count", get count() { throw "ಠ_ಠ"; } }
+ });
+ }, "ಠ_ಠ");
+
+ assertThrowsValue(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: { by: "count", get bytes() { throw "ಠ_ಠ"; } }
+ });
+ }, "ಠ_ಠ");
+
+
+
+ assertThrowsValue(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: { by: "objectClass", get then() { throw "ಠ_ಠ"; } }
+ });
+ }, "ಠ_ಠ");
+
+ assertThrowsValue(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: { by: "objectClass", get other() { throw "ಠ_ಠ"; } }
+ });
+ }, "ಠ_ಠ");
+
+
+
+ assertThrowsValue(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: { by: "coarseType", get objects() { throw "ಠ_ಠ"; } }
+ });
+ }, "ಠ_ಠ");
+
+ assertThrowsValue(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: { by: "coarseType", get scripts() { throw "ಠ_ಠ"; } }
+ });
+ }, "ಠ_ಠ");
+
+ assertThrowsValue(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: { by: "coarseType", get strings() { throw "ಠ_ಠ"; } }
+ });
+ }, "ಠ_ಠ");
+
+ assertThrowsValue(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: { by: "coarseType", get other() { throw "ಠ_ಠ"; } }
+ });
+ }, "ಠ_ಠ");
+
+
+
+ assertThrowsValue(() => {
+ saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: { by: "internalType", get then() { throw "ಠ_ಠ"; } }
+ });
+ }, "ಠ_ಠ");
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_08.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_08.js
new file mode 100644
index 000000000..5934aa919
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_08.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// HeapSnapshot.prototype.takeCensus: test by: 'count' breakdown
+//
+// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-08.js
+
+function run_test() {
+ let g = newGlobal();
+ let dbg = new Debugger(g);
+
+ g.eval(`
+ var stuff = [];
+ function add(n, c) {
+ for (let i = 0; i < n; i++)
+ stuff.push(c());
+ }
+
+ let count = 0;
+
+ function obj() { return { count: count++ }; }
+ obj.factor = 1;
+
+ // This creates a closure (a function JSObject) that has captured
+ // a Call object. So each call creates two items.
+ function fun() { let v = count; return () => { return v; } }
+ fun.factor = 2;
+
+ function str() { return 'perambulator' + count++; }
+ str.factor = 1;
+
+ // Eval a fresh text each time, allocating:
+ // - a fresh ScriptSourceObject
+ // - a new JSScripts, not an eval cache hits
+ // - a fresh prototype object
+ // - a fresh Call object, since the eval makes 'ev' heavyweight
+ // - the new function itself
+ function ev() {
+ return eval(\`(function () { return \${ count++ } })\`);
+ }
+ ev.factor = 5;
+
+ // A new object (1) with a new shape (2) with a new atom (3)
+ function shape() { return { [ 'theobroma' + count++ ]: count }; }
+ shape.factor = 3;
+ `);
+
+ let baseline = 0;
+ function countIncreasedByAtLeast(n) {
+ let oldBaseline = baseline;
+
+ // Since a census counts only reachable objects, one might assume that calling
+ // GC here would have no effect on the census results. But GC also throws away
+ // JIT code and any objects it might be holding (template objects, say);
+ // takeCensus reaches those. Shake everything loose that we can, to make the
+ // census approximate reachability a bit more closely, and make our results a
+ // bit more predictable.
+ gc(g, "shrinking");
+
+ baseline = saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "count" } }).count;
+ return baseline >= oldBaseline + n;
+ }
+
+ countIncreasedByAtLeast(0);
+
+ g.add(100, g.obj);
+ ok(countIncreasedByAtLeast(g.obj.factor * 100));
+
+ g.add(100, g.fun);
+ ok(countIncreasedByAtLeast(g.fun.factor * 100));
+
+ g.add(100, g.str);
+ ok(countIncreasedByAtLeast(g.str.factor * 100));
+
+ g.add(100, g.ev);
+ ok(countIncreasedByAtLeast(g.ev.factor * 100));
+
+ g.add(100, g.shape);
+ ok(countIncreasedByAtLeast(g.shape.factor * 100));
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_09.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_09.js
new file mode 100644
index 000000000..bbacccc8d
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_09.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// HeapSnapshot.prototype.takeCensus: by: allocationStack breakdown
+//
+// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-09.js
+
+function run_test() {
+ var g = newGlobal();
+ var dbg = new Debugger(g);
+
+ g.eval(` // 1
+ var log = []; // 2
+ function f() { log.push(allocationMarker()); } // 3
+ function g() { f(); } // 4
+ function h() { f(); } // 5
+ `); // 6
+
+ // Create one allocationMarker with tracking turned off,
+ // so it will have no associated stack.
+ g.f();
+
+ dbg.memory.allocationSamplingProbability = 1;
+
+ for ([func, n] of [[g.f, 20], [g.g, 10], [g.h, 5]]) {
+ for (let i = 0; i < n; i++) {
+ dbg.memory.trackingAllocationSites = true;
+ // All allocations of allocationMarker occur with this line as the oldest
+ // stack frame.
+ func();
+ dbg.memory.trackingAllocationSites = false;
+ }
+ }
+
+ let census = saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "objectClass",
+ then: { by: "allocationStack",
+ then: { by: "count",
+ label: "haz stack"
+ },
+ noStack: { by: "count",
+ label: "no haz stack"
+ }
+ }
+ }
+ });
+
+ let map = census.AllocationMarker;
+ ok(map instanceof Map, "Should be a Map instance");
+ equal(map.size, 4, "Should have 4 allocation stacks (including the lack of a stack)");
+
+ // Gather the stacks we are expecting to appear as keys, and
+ // check that there are no unexpected keys.
+ let stacks = { };
+
+ map.forEach((v, k) => {
+ if (k === "noStack") {
+ // No need to save this key.
+ } else if (k.functionDisplayName === "f" &&
+ k.parent.functionDisplayName === "run_test") {
+ stacks.f = k;
+ } else if (k.functionDisplayName === "f" &&
+ k.parent.functionDisplayName === "g" &&
+ k.parent.parent.functionDisplayName === "run_test") {
+ stacks.fg = k;
+ } else if (k.functionDisplayName === "f" &&
+ k.parent.functionDisplayName === "h" &&
+ k.parent.parent.functionDisplayName === "run_test") {
+ stacks.fh = k;
+ } else {
+ dumpn("Unexpected allocation stack:");
+ k.toString().split(/\n/g).forEach(s => dumpn(s));
+ ok(false);
+ }
+ });
+
+ equal(map.get("noStack").label, "no haz stack");
+ equal(map.get("noStack").count, 1);
+
+ ok(stacks.f);
+ equal(map.get(stacks.f).label, "haz stack");
+ equal(map.get(stacks.f).count, 20);
+
+ ok(stacks.fg);
+ equal(map.get(stacks.fg).label, "haz stack");
+ equal(map.get(stacks.fg).count, 10);
+
+ ok(stacks.fh);
+ equal(map.get(stacks.fh).label, "haz stack");
+ equal(map.get(stacks.fh).count, 5);
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_10.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_10.js
new file mode 100644
index 000000000..a7f987f5a
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_10.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Check byte counts produced by takeCensus.
+//
+// Ported from js/src/jit-test/tests/debug/Memory-take Census-10.js
+
+function run_test() {
+ let g = newGlobal();
+ let dbg = new Debugger(g);
+
+ let sizeOfAM = byteSize(allocationMarker());
+
+ // Allocate a single allocation marker, and check that we can find it.
+ g.eval("var hold = allocationMarker();");
+ let census = saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "objectClass" } });
+ equal(census.AllocationMarker.count, 1);
+ equal(census.AllocationMarker.bytes, sizeOfAM);
+ g.hold = null;
+
+ g.eval(` // 1
+ var objs = []; // 2
+ function fnerd() { // 3
+ objs.push(allocationMarker()); // 4
+ for (let i = 0; i < 10; i++) // 5
+ objs.push(allocationMarker()); // 6
+ } // 7
+ `); // 8
+
+ dbg.memory.allocationSamplingProbability = 1;
+ dbg.memory.trackingAllocationSites = true;
+ g.fnerd();
+ dbg.memory.trackingAllocationSites = false;
+
+ census = saveHeapSnapshotAndTakeCensus(dbg, {
+ breakdown: { by: "objectClass",
+ then: { by: "allocationStack" }
+ }
+ });
+
+ let seen = 0;
+ census.AllocationMarker.forEach((v, k) => {
+ equal(k.functionDisplayName, "fnerd");
+ switch (k.line) {
+ case 4:
+ equal(v.count, 1);
+ equal(v.bytes, sizeOfAM);
+ seen++;
+ break;
+
+ case 6:
+ equal(v.count, 10);
+ equal(v.bytes, 10 * sizeOfAM);
+ seen++;
+ break;
+
+ default:
+ dumpn("Unexpected stack:");
+ k.toString().split(/\n/g).forEach(s => dumpn(s));
+ ok(false);
+ break;
+ }
+ });
+
+ equal(seen, 2);
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_11.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_11.js
new file mode 100644
index 000000000..3d898b2d1
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_11.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that Debugger.Memory.prototype.takeCensus and
+// HeapSnapshot.prototype.takeCensus return the same data for the same heap
+// graph.
+
+function doLiveAndOfflineCensus(g, dbg, opts) {
+ dbg.memory.allocationSamplingProbability = 1;
+ dbg.memory.trackingAllocationSites = true;
+ g.eval(` // 1
+ (function unsafeAtAnySpeed() { // 2
+ for (var i = 0; i < 100; i++) { // 3
+ this.markers.push(allocationMarker()); // 4
+ } // 5
+ }()); // 6
+ `); // 7
+ dbg.memory.trackingAllocationSites = false;
+
+ return {
+ live: dbg.memory.takeCensus(opts),
+ offline: saveHeapSnapshotAndTakeCensus(dbg, opts)
+ };
+}
+
+function run_test() {
+ var g = newGlobal();
+ var dbg = new Debugger(g);
+
+ g.eval("this.markers = []");
+ const markerSize = byteSize(allocationMarker());
+
+ // First, test that we get the same counts and sizes as we allocate and retain
+ // more things.
+
+ let prevCount = 0;
+ let prevBytes = 0;
+
+ for (var i = 0; i < 10; i++) {
+ const { live, offline } = doLiveAndOfflineCensus(g, dbg, {
+ breakdown: { by: "objectClass",
+ then: { by: "count"} }
+ });
+
+ equal(live.AllocationMarker.count, offline.AllocationMarker.count);
+ equal(live.AllocationMarker.bytes, offline.AllocationMarker.bytes);
+ equal(live.AllocationMarker.count, prevCount + 100);
+ equal(live.AllocationMarker.bytes, prevBytes + 100 * markerSize);
+
+ prevCount = live.AllocationMarker.count;
+ prevBytes = live.AllocationMarker.bytes;
+ }
+
+ // Second, test that the reported allocation stacks and counts and sizes at
+ // those allocation stacks match up.
+
+ const { live, offline } = doLiveAndOfflineCensus(g, dbg, {
+ breakdown: { by: "objectClass",
+ then: { by: "allocationStack"} }
+ });
+
+ equal(live.AllocationMarker.size, offline.AllocationMarker.size);
+ // One stack with the loop further above, and another stack featuring the call
+ // right above.
+ equal(live.AllocationMarker.size, 2);
+
+ // Note that because SavedFrame stacks reconstructed from an offline heap
+ // snapshot don't have the same principals as SavedFrame stacks captured from
+ // a live stack, the live and offline allocation stacks won't be identity
+ // equal, but should be structurally the same.
+
+ const liveEntries = [];
+ live.AllocationMarker.forEach((v, k) => {
+ dumpn("Allocation stack:");
+ k.toString().split(/\n/g).forEach(s => dumpn(s));
+
+ equal(k.functionDisplayName, "unsafeAtAnySpeed");
+ equal(k.line, 4);
+
+ liveEntries.push([k.toString(), v]);
+ });
+
+ const offlineEntries = [];
+ offline.AllocationMarker.forEach((v, k) => {
+ dumpn("Allocation stack:");
+ k.toString().split(/\n/g).forEach(s => dumpn(s));
+
+ equal(k.functionDisplayName, "unsafeAtAnySpeed");
+ equal(k.line, 4);
+
+ offlineEntries.push([k.toString(), v]);
+ });
+
+ const sortEntries = (a, b) => {
+ if (a[0] < b[0]) {
+ return -1;
+ } else if (a[0] > b[0]) {
+ return 1;
+ } else {
+ return 0;
+ }
+ };
+ liveEntries.sort(sortEntries);
+ offlineEntries.sort(sortEntries);
+
+ equal(liveEntries.length, live.AllocationMarker.size);
+ equal(liveEntries.length, offlineEntries.length);
+
+ for (let i = 0; i < liveEntries.length; i++) {
+ equal(liveEntries[i][0], offlineEntries[i][0]);
+ equal(liveEntries[i][1].count, offlineEntries[i][1].count);
+ equal(liveEntries[i][1].bytes, offlineEntries[i][1].bytes);
+ }
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_12.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_12.js
new file mode 100644
index 000000000..f10dd5b03
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_12.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that when we take a census and get a bucket list of ids that matched the
+// given category, that the returned ids are all in the snapshot and their
+// reported category.
+
+function run_test() {
+ const g = newGlobal();
+ const dbg = new Debugger(g);
+
+ const path = saveNewHeapSnapshot({ debugger: dbg });
+ const snapshot = readHeapSnapshot(path);
+
+ const bucket = { by: "bucket" };
+ const count = { by: "count", count: true, bytes: false };
+ const objectClassCount = { by: "objectClass", then: count, other: count };
+
+ const byClassName = snapshot.takeCensus({
+ breakdown: {
+ by: "objectClass",
+ then: bucket,
+ other: bucket,
+ }
+ });
+
+ const byClassNameCount = snapshot.takeCensus({
+ breakdown: objectClassCount
+ });
+
+ const keys = new Set(Object.keys(byClassName));
+ equal(keys.size, Object.keys(byClassNameCount).length,
+ "Should have the same number of keys.");
+ for (let k of Object.keys(byClassNameCount)) {
+ ok(keys.has(k), "Should not have any unexpected class names");
+ }
+
+ for (let key of Object.keys(byClassName)) {
+ equal(byClassNameCount[key].count, byClassName[key].length,
+ "Length of the bucket and count should be equal");
+
+ for (let id of byClassName[key]) {
+ const desc = snapshot.describeNode(objectClassCount, id);
+ equal(desc[key].count, 1,
+ "Describing the bucketed node confirms that it belongs to the category");
+ }
+ }
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_ReadHeapSnapshot.js b/devtools/shared/heapsnapshot/tests/unit/test_ReadHeapSnapshot.js
new file mode 100644
index 000000000..dde139ffd
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_ReadHeapSnapshot.js
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can read core dumps into HeapSnapshot instances.
+
+if (typeof Debugger != "function") {
+ const { addDebuggerToGlobal } = Cu.import("resource://gre/modules/jsdebugger.jsm", {});
+ addDebuggerToGlobal(this);
+}
+
+function run_test() {
+ const filePath = ChromeUtils.saveHeapSnapshot({ globals: [this] });
+ ok(true, "Should be able to save a snapshot.");
+
+ const snapshot = ChromeUtils.readHeapSnapshot(filePath);
+ ok(snapshot, "Should be able to read a heap snapshot");
+ ok(snapshot instanceof HeapSnapshot, "Should be an instanceof HeapSnapshot");
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_ReadHeapSnapshot_with_allocations.js b/devtools/shared/heapsnapshot/tests/unit/test_ReadHeapSnapshot_with_allocations.js
new file mode 100644
index 000000000..d91f36f56
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_ReadHeapSnapshot_with_allocations.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can save a core dump with allocation stacks and read it back
+// into a HeapSnapshot.
+
+if (typeof Debugger != "function") {
+ const { addDebuggerToGlobal } = Cu.import("resource://gre/modules/jsdebugger.jsm", {});
+ addDebuggerToGlobal(this);
+}
+
+function run_test() {
+ // Create a Debugger observing a debuggee's allocations.
+ const debuggee = new Cu.Sandbox(null);
+ const dbg = new Debugger(debuggee);
+ dbg.memory.trackingAllocationSites = true;
+
+ // Allocate some objects in the debuggee that will have their allocation
+ // stacks recorded by the Debugger.
+ debuggee.eval("this.objects = []");
+ for (let i = 0; i < 100; i++) {
+ debuggee.eval("this.objects.push({})");
+ }
+
+ // Now save a snapshot that will include the allocation stacks and read it
+ // back again.
+
+ const filePath = ChromeUtils.saveHeapSnapshot({ runtime: true });
+ ok(true, "Should be able to save a snapshot.");
+
+ const snapshot = ChromeUtils.readHeapSnapshot(filePath);
+ ok(snapshot, "Should be able to read a heap snapshot");
+ ok(snapshot instanceof HeapSnapshot, "Should be an instanceof HeapSnapshot");
+
+ do_test_finished();
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_ReadHeapSnapshot_worker.js b/devtools/shared/heapsnapshot/tests/unit/test_ReadHeapSnapshot_worker.js
new file mode 100644
index 000000000..76461b694
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_ReadHeapSnapshot_worker.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that we can read core dumps into HeapSnapshot instances in a worker.
+
+add_task(function* () {
+ const worker = new ChromeWorker("resource://test/heap-snapshot-worker.js");
+ worker.postMessage({});
+
+ let assertionCount = 0;
+ worker.onmessage = e => {
+ if (e.data.type !== "assertion") {
+ return;
+ }
+
+ ok(e.data.passed, e.data.msg + "\n" + e.data.stack);
+ assertionCount++;
+ };
+
+ yield waitForDone(worker);
+
+ ok(assertionCount > 0);
+ worker.terminate();
+});
+
+function waitForDone(w) {
+ return new Promise((resolve, reject) => {
+ w.onerror = e => {
+ reject();
+ ok(false, "Error in worker: " + e);
+ };
+
+ w.addEventListener("message", function listener(e) {
+ if (e.data.type === "done") {
+ w.removeEventListener("message", listener, false);
+ resolve();
+ }
+ }, false);
+ });
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_SaveHeapSnapshot.js b/devtools/shared/heapsnapshot/tests/unit/test_SaveHeapSnapshot.js
new file mode 100644
index 000000000..affd8d1e4
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_SaveHeapSnapshot.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test the ChromeUtils interface.
+
+if (typeof Debugger != "function") {
+ const { addDebuggerToGlobal } = Cu.import("resource://gre/modules/jsdebugger.jsm", {});
+ addDebuggerToGlobal(this);
+}
+
+function run_test() {
+ ok(ChromeUtils, "Should be able to get the ChromeUtils interface");
+
+ testBadParameters();
+ testGoodParameters();
+
+ do_test_finished();
+}
+
+function testBadParameters() {
+ throws(() => ChromeUtils.saveHeapSnapshot(),
+ "Should throw if arguments aren't passed in.");
+
+ throws(() => ChromeUtils.saveHeapSnapshot(null),
+ "Should throw if boundaries isn't an object.");
+
+ throws(() => ChromeUtils.saveHeapSnapshot({}),
+ "Should throw if the boundaries object doesn't have any properties.");
+
+ throws(() => ChromeUtils.saveHeapSnapshot({ runtime: true,
+ globals: [this] }),
+ "Should throw if the boundaries object has more than one property.");
+
+ throws(() => ChromeUtils.saveHeapSnapshot({ debugger: {} }),
+ "Should throw if the debuggees object is not a Debugger object");
+
+ throws(() => ChromeUtils.saveHeapSnapshot({ globals: [{}] }),
+ "Should throw if the globals array contains non-global objects.");
+
+ throws(() => ChromeUtils.saveHeapSnapshot({ runtime: false }),
+ "Should throw if runtime is supplied and is not true.");
+
+ throws(() => ChromeUtils.saveHeapSnapshot({ globals: null }),
+ "Should throw if globals is not an object.");
+
+ throws(() => ChromeUtils.saveHeapSnapshot({ globals: {} }),
+ "Should throw if globals is not an array.");
+
+ throws(() => ChromeUtils.saveHeapSnapshot({ debugger: Debugger.prototype }),
+ "Should throw if debugger is the Debugger.prototype object.");
+
+ throws(() => ChromeUtils.saveHeapSnapshot({ get globals() { return [this]; } }),
+ "Should throw if boundaries property is a getter.");
+}
+
+const makeNewSandbox = () =>
+ Cu.Sandbox(CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")());
+
+function testGoodParameters() {
+ let sandbox = makeNewSandbox();
+ let dbg = new Debugger(sandbox);
+
+ ChromeUtils.saveHeapSnapshot({ debugger: dbg });
+ ok(true, "Should be able to save a snapshot for a debuggee global.");
+
+ dbg = new Debugger;
+ let sandboxes = Array(10).fill(null).map(makeNewSandbox);
+ sandboxes.forEach(sb => dbg.addDebuggee(sb));
+
+ ChromeUtils.saveHeapSnapshot({ debugger: dbg });
+ ok(true, "Should be able to save a snapshot for many debuggee globals.");
+
+ dbg = new Debugger;
+ ChromeUtils.saveHeapSnapshot({ debugger: dbg });
+ ok(true, "Should be able to save a snapshot with no debuggee globals.");
+
+ ChromeUtils.saveHeapSnapshot({ globals: [this] });
+ ok(true, "Should be able to save a snapshot for a specific global.");
+
+ ChromeUtils.saveHeapSnapshot({ runtime: true });
+ ok(true, "Should be able to save a snapshot of the full runtime.");
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-01.js b/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-01.js
new file mode 100644
index 000000000..16038c5c4
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-01.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests CensusTreeNode with `internalType` breakdown.
+ */
+
+const BREAKDOWN = {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true }
+};
+
+const REPORT = {
+ "JSObject": {
+ "bytes": 100,
+ "count": 10,
+ },
+ "js::Shape": {
+ "bytes": 500,
+ "count": 50,
+ },
+ "JSString": {
+ "bytes": 10,
+ "count": 1,
+ },
+};
+
+const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 610,
+ count: 0,
+ totalCount: 61,
+ children: [
+ {
+ name: "js::Shape",
+ bytes: 500,
+ totalBytes: 500,
+ count: 50,
+ totalCount: 50,
+ children: undefined,
+ id: 3,
+ parent: 1,
+ reportLeafIndex: 2,
+ },
+ {
+ name: "JSObject",
+ bytes: 100,
+ totalBytes: 100,
+ count: 10,
+ totalCount: 10,
+ children: undefined,
+ id: 2,
+ parent: 1,
+ reportLeafIndex: 1,
+ },
+ {
+ name: "JSString",
+ bytes: 10,
+ totalBytes: 10,
+ count: 1,
+ totalCount: 1,
+ children: undefined,
+ id: 4,
+ parent: 1,
+ reportLeafIndex: 3,
+ },
+ ],
+ id: 1,
+ parent: undefined,
+ reportLeafIndex: undefined,
+};
+
+function run_test() {
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-02.js b/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-02.js
new file mode 100644
index 000000000..37d039954
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-02.js
@@ -0,0 +1,136 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests CensusTreeNode with `coarseType` breakdown.
+ */
+
+const countBreakdown = { by: "count", count: true, bytes: true };
+
+const BREAKDOWN = {
+ by: "coarseType",
+ objects: { by: "objectClass", then: countBreakdown },
+ strings: countBreakdown,
+ scripts: countBreakdown,
+ other: { by: "internalType", then: countBreakdown },
+};
+
+const REPORT = {
+ "objects": {
+ "Function": { bytes: 10, count: 1 },
+ "Array": { bytes: 20, count: 2 },
+ },
+ "strings": { bytes: 10, count: 1 },
+ "scripts": { bytes: 1, count: 1 },
+ "other": {
+ "js::Shape": { bytes: 30, count: 3 },
+ "js::Shape2": { bytes: 40, count: 4 }
+ },
+};
+
+const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 111,
+ count: 0,
+ totalCount: 12,
+ children: [
+ {
+ name: "other",
+ count: 0,
+ totalCount: 7,
+ bytes: 0,
+ totalBytes: 70,
+ children: [
+ {
+ name: "js::Shape2",
+ bytes: 40,
+ totalBytes: 40,
+ count: 4,
+ totalCount: 4,
+ children: undefined,
+ id: 9,
+ parent: 7,
+ reportLeafIndex: 8,
+ },
+ {
+ name: "js::Shape",
+ bytes: 30,
+ totalBytes: 30,
+ count: 3,
+ totalCount: 3,
+ children: undefined,
+ id: 8,
+ parent: 7,
+ reportLeafIndex: 7,
+ },
+ ],
+ id: 7,
+ parent: 1,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: "objects",
+ count: 0,
+ totalCount: 3,
+ bytes: 0,
+ totalBytes: 30,
+ children: [
+ {
+ name: "Array",
+ bytes: 20,
+ totalBytes: 20,
+ count: 2,
+ totalCount: 2,
+ children: undefined,
+ id: 4,
+ parent: 2,
+ reportLeafIndex: 3,
+ },
+ {
+ name: "Function",
+ bytes: 10,
+ totalBytes: 10,
+ count: 1,
+ totalCount: 1,
+ children: undefined,
+ id: 3,
+ parent: 2,
+ reportLeafIndex: 2,
+ },
+ ],
+ id: 2,
+ parent: 1,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: "strings",
+ count: 1,
+ totalCount: 1,
+ bytes: 10,
+ totalBytes: 10,
+ children: undefined,
+ id: 6,
+ parent: 1,
+ reportLeafIndex: 5,
+ },
+ {
+ name: "scripts",
+ count: 1,
+ totalCount: 1,
+ bytes: 1,
+ totalBytes: 1,
+ children: undefined,
+ id: 5,
+ parent: 1,
+ reportLeafIndex: 4,
+ },
+ ],
+ id: 1,
+ parent: undefined,
+ reportLeafIndex: undefined,
+};
+
+function run_test() {
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-03.js b/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-03.js
new file mode 100644
index 000000000..bdf932099
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-03.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests CensusTreeNode with `objectClass` breakdown.
+ */
+
+const countBreakdown = { by: "count", count: true, bytes: true };
+
+const BREAKDOWN = {
+ by: "objectClass",
+ then: countBreakdown,
+ other: { by: "internalType", then: countBreakdown }
+};
+
+const REPORT = {
+ "Function": { bytes: 10, count: 10 },
+ "Array": { bytes: 100, count: 1 },
+ "other": {
+ "JIT::CODE::NOW!!!": { bytes: 20, count: 2 },
+ "JIT::CODE::LATER!!!": { bytes: 40, count: 4 }
+ }
+};
+
+const EXPECTED = {
+ name: null,
+ count: 0,
+ totalCount: 17,
+ bytes: 0,
+ totalBytes: 170,
+ children: [
+ {
+ name: "Array",
+ bytes: 100,
+ totalBytes: 100,
+ count: 1,
+ totalCount: 1,
+ children: undefined,
+ id: 3,
+ parent: 1,
+ reportLeafIndex: 2,
+ },
+ {
+ name: "other",
+ count: 0,
+ totalCount: 6,
+ bytes: 0,
+ totalBytes: 60,
+ children: [
+ {
+ name: "JIT::CODE::LATER!!!",
+ bytes: 40,
+ totalBytes: 40,
+ count: 4,
+ totalCount: 4,
+ children: undefined,
+ id: 6,
+ parent: 4,
+ reportLeafIndex: 5,
+ },
+ {
+ name: "JIT::CODE::NOW!!!",
+ bytes: 20,
+ totalBytes: 20,
+ count: 2,
+ totalCount: 2,
+ children: undefined,
+ id: 5,
+ parent: 4,
+ reportLeafIndex: 4,
+ },
+ ],
+ id: 4,
+ parent: 1,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: "Function",
+ bytes: 10,
+ totalBytes: 10,
+ count: 10,
+ totalCount: 10,
+ children: undefined,
+ id: 2,
+ parent: 1,
+ reportLeafIndex: 1,
+ },
+ ],
+ id: 1,
+ parent: undefined,
+ reportLeafIndex: undefined,
+};
+
+function run_test() {
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-04.js b/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-04.js
new file mode 100644
index 000000000..cc0c3bac0
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-04.js
@@ -0,0 +1,159 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests CensusTreeNode with `allocationStack` breakdown.
+ */
+
+function run_test() {
+ const countBreakdown = { by: "count", count: true, bytes: true };
+
+ const BREAKDOWN = {
+ by: "allocationStack",
+ then: countBreakdown,
+ noStack: countBreakdown,
+ };
+
+ let stack1, stack2, stack3, stack4, stack5;
+
+ (function a() {
+ (function b() {
+ (function c() {
+ stack1 = saveStack(3);
+ }());
+ (function d() {
+ stack2 = saveStack(3);
+ stack3 = saveStack(3);
+ }());
+ stack4 = saveStack(2);
+ }());
+ }());
+
+ stack5 = saveStack(1);
+
+ const REPORT = new Map([
+ [stack1, { bytes: 10, count: 1 }],
+ [stack2, { bytes: 20, count: 2 }],
+ [stack3, { bytes: 30, count: 3 }],
+ [stack4, { bytes: 40, count: 4 }],
+ [stack5, { bytes: 50, count: 5 }],
+ ["noStack", { bytes: 60, count: 6 }],
+ ]);
+
+ const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 210,
+ count: 0,
+ totalCount: 21,
+ children: [
+ {
+ name: stack4.parent,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: [
+ {
+ name: stack3.parent,
+ bytes: 0,
+ totalBytes: 50,
+ count: 0,
+ totalCount: 5,
+ children: [
+ {
+ name: stack3,
+ bytes: 30,
+ totalBytes: 30,
+ count: 3,
+ totalCount: 3,
+ children: undefined,
+ id: 7,
+ parent: 5,
+ reportLeafIndex: 3,
+ },
+ {
+ name: stack2,
+ bytes: 20,
+ totalBytes: 20,
+ count: 2,
+ totalCount: 2,
+ children: undefined,
+ id: 6,
+ parent: 5,
+ reportLeafIndex: 2,
+ }
+ ],
+ id: 5,
+ parent: 2,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: stack4,
+ bytes: 40,
+ totalBytes: 40,
+ count: 4,
+ totalCount: 4,
+ children: undefined,
+ id: 8,
+ parent: 2,
+ reportLeafIndex: 4,
+ },
+ {
+ name: stack1.parent,
+ bytes: 0,
+ totalBytes: 10,
+ count: 0,
+ totalCount: 1,
+ children: [
+ {
+ name: stack1,
+ bytes: 10,
+ totalBytes: 10,
+ count: 1,
+ totalCount: 1,
+ children: undefined,
+ id: 4,
+ parent: 3,
+ reportLeafIndex: 1,
+ },
+ ],
+ id: 3,
+ parent: 2,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 2,
+ parent: 1,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: "noStack",
+ bytes: 60,
+ totalBytes: 60,
+ count: 6,
+ totalCount: 6,
+ children: undefined,
+ id: 10,
+ parent: 1,
+ reportLeafIndex: 6,
+ },
+ {
+ name: stack5,
+ bytes: 50,
+ totalBytes: 50,
+ count: 5,
+ totalCount: 5,
+ children: undefined,
+ id: 9,
+ parent: 1,
+ reportLeafIndex: 5
+ },
+ ],
+ id: 1,
+ parent: undefined,
+ reportLeafIndex: undefined,
+ };
+
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-05.js b/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-05.js
new file mode 100644
index 000000000..20fb76bd2
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-05.js
@@ -0,0 +1,145 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests CensusTreeNode with `allocationStack` => `objectClass` breakdown.
+ */
+
+function run_test() {
+ const countBreakdown = { by: "count", count: true, bytes: true };
+
+ const BREAKDOWN = {
+ by: "allocationStack",
+ then: {
+ by: "objectClass",
+ then: countBreakdown,
+ other: countBreakdown
+ },
+ noStack: countBreakdown,
+ };
+
+ let stack;
+
+ (function a() {
+ (function b() {
+ (function c() {
+ stack = saveStack(3);
+ }());
+ }());
+ }());
+
+ const REPORT = new Map([
+ [stack, { Foo: { bytes: 10, count: 1 },
+ Bar: { bytes: 20, count: 2 },
+ Baz: { bytes: 30, count: 3 },
+ other: { bytes: 40, count: 4 }
+ }],
+ ["noStack", { bytes: 50, count: 5 }],
+ ]);
+
+ const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 150,
+ count: 0,
+ totalCount: 15,
+ children: [
+ {
+ name: stack.parent.parent,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: [
+ {
+ name: stack.parent,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: [
+ {
+ name: stack,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: [
+ {
+ name: "other",
+ bytes: 40,
+ totalBytes: 40,
+ count: 4,
+ totalCount: 4,
+ children: undefined,
+ id: 8,
+ parent: 4,
+ reportLeafIndex: 5,
+ },
+ {
+ name: "Baz",
+ bytes: 30,
+ totalBytes: 30,
+ count: 3,
+ totalCount: 3,
+ children: undefined,
+ id: 7,
+ parent: 4,
+ reportLeafIndex: 4,
+ },
+ {
+ name: "Bar",
+ bytes: 20,
+ totalBytes: 20,
+ count: 2,
+ totalCount: 2,
+ children: undefined,
+ id: 6,
+ parent: 4,
+ reportLeafIndex: 3,
+ },
+ {
+ name: "Foo",
+ bytes: 10,
+ totalBytes: 10,
+ count: 1,
+ totalCount: 1,
+ children: undefined,
+ id: 5,
+ parent: 4,
+ reportLeafIndex: 2,
+ },
+ ],
+ id: 4,
+ parent: 3,
+ reportLeafIndex: undefined,
+ }
+ ],
+ id: 3,
+ parent: 2,
+ reportLeafIndex: undefined,
+ }
+ ],
+ id: 2,
+ parent: 1,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: "noStack",
+ bytes: 50,
+ totalBytes: 50,
+ count: 5,
+ totalCount: 5,
+ children: undefined,
+ id: 9,
+ parent: 1,
+ reportLeafIndex: 6,
+ },
+ ],
+ id: 1,
+ parent: undefined,
+ reportLeafIndex: undefined,
+ };
+
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-06.js b/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-06.js
new file mode 100644
index 000000000..eb1801207
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-06.js
@@ -0,0 +1,200 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test inverting CensusTreeNode with a by alloaction stack breakdown.
+ */
+
+function run_test() {
+ const BREAKDOWN = {
+ by: "allocationStack",
+ then: { by: "count", count: true, bytes: true },
+ noStack: { by: "count", count: true, bytes: true },
+ };
+
+ let stack1, stack2, stack3, stack4;
+
+ function a(n) {
+ return b(n);
+ }
+ function b(n) {
+ return c(n);
+ }
+ function c(n) {
+ return saveStack(n);
+ }
+ function d(n) {
+ return b(n);
+ }
+ function e(n) {
+ return c(n);
+ }
+
+ const abc_Stack = a(3);
+ const bc_Stack = b(2);
+ const c_Stack = c(1);
+ const dbc_Stack = d(3);
+ const ec_Stack = e(2);
+
+ const REPORT = new Map([
+ [abc_Stack, { bytes: 10, count: 1 }],
+ [ bc_Stack, { bytes: 10, count: 1 }],
+ [ c_Stack, { bytes: 10, count: 1 }],
+ [dbc_Stack, { bytes: 10, count: 1 }],
+ [ ec_Stack, { bytes: 10, count: 1 }],
+ ["noStack", { bytes: 50, count: 5 }],
+ ]);
+
+ const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: [
+ {
+ name: "noStack",
+ bytes: 50,
+ totalBytes: 50,
+ count: 5,
+ totalCount: 5,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: undefined,
+ id: 16,
+ parent: 15,
+ reportLeafIndex: undefined,
+ }
+ ],
+ id: 15,
+ parent: 14,
+ reportLeafIndex: 6,
+ },
+ {
+ name: abc_Stack,
+ bytes: 50,
+ totalBytes: 10,
+ count: 5,
+ totalCount: 1,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: undefined,
+ id: 18,
+ parent: 17,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: abc_Stack.parent,
+ bytes: 0,
+ totalBytes: 10,
+ count: 0,
+ totalCount: 1,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: undefined,
+ id: 22,
+ parent: 19,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: abc_Stack.parent.parent,
+ bytes: 0,
+ totalBytes: 10,
+ count: 0,
+ totalCount: 1,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: undefined,
+ id: 21,
+ parent: 20,
+ reportLeafIndex: undefined,
+ }
+ ],
+ id: 20,
+ parent: 19,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: dbc_Stack.parent.parent,
+ bytes: 0,
+ totalBytes: 10,
+ count: 0,
+ totalCount: 1,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: undefined,
+ id: 24,
+ parent: 23,
+ reportLeafIndex: undefined,
+ }
+ ],
+ id: 23,
+ parent: 19,
+ reportLeafIndex: undefined,
+ }
+ ],
+ id: 19,
+ parent: 17,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: ec_Stack.parent,
+ bytes: 0,
+ totalBytes: 10,
+ count: 0,
+ totalCount: 1,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: undefined,
+ id: 26,
+ parent: 25,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 25,
+ parent: 17,
+ reportLeafIndex: undefined,
+ },
+ ],
+ id: 17,
+ parent: 14,
+ reportLeafIndex: new Set([1, 2, 3, 4, 5]),
+ }
+ ],
+ id: 14,
+ parent: undefined,
+ reportLeafIndex: undefined,
+ };
+
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { invert: true });
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-07.js b/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-07.js
new file mode 100644
index 000000000..6bc085257
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-07.js
@@ -0,0 +1,200 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test inverting CensusTreeNode with a non-allocation stack breakdown.
+ */
+
+function run_test() {
+ const BREAKDOWN = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other:{
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ };
+
+ const REPORT = {
+ objects: {
+ Array: { bytes: 50, count: 5 },
+ other: { bytes: 0, count: 0 },
+ },
+ scripts: {
+ "js::jit::JitScript": { bytes: 30, count: 3 },
+ },
+ strings: {
+ JSAtom: { bytes: 60, count: 6 },
+ },
+ other: {
+ "js::Shape": { bytes: 80, count: 8 },
+ }
+ };
+
+ const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 220,
+ count: 0,
+ totalCount: 22,
+ children: [
+ {
+ name: "js::Shape",
+ bytes: 80,
+ totalBytes: 80,
+ count: 8,
+ totalCount: 8,
+ children: [
+ {
+ name: "other",
+ bytes: 0,
+ totalBytes: 80,
+ count: 0,
+ totalCount: 8,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 220,
+ count: 0,
+ totalCount: 22,
+ children: undefined,
+ id: 14,
+ parent: 13,
+ reportLeafIndex: undefined,
+ }
+ ],
+ id: 13,
+ parent: 12,
+ reportLeafIndex: undefined,
+ }
+ ],
+ id: 12,
+ parent: 11,
+ reportLeafIndex: 9,
+ },
+ {
+ name: "JSAtom",
+ bytes: 60,
+ totalBytes: 60,
+ count: 6,
+ totalCount: 6,
+ children: [
+ {
+ name: "strings",
+ bytes: 0,
+ totalBytes: 60,
+ count: 0,
+ totalCount: 6,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 220,
+ count: 0,
+ totalCount: 22,
+ children: undefined,
+ id: 17,
+ parent: 16,
+ reportLeafIndex: undefined,
+ }
+ ],
+ id: 16,
+ parent: 15,
+ reportLeafIndex: undefined,
+ }
+ ],
+ id: 15,
+ parent: 11,
+ reportLeafIndex: 7,
+ },
+ {
+ name: "Array",
+ bytes: 50,
+ totalBytes: 50,
+ count: 5,
+ totalCount: 5,
+ children: [
+ {
+ name: "objects",
+ bytes: 0,
+ totalBytes: 50,
+ count: 0,
+ totalCount: 5,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 220,
+ count: 0,
+ totalCount: 22,
+ children: undefined,
+ id: 20,
+ parent: 19,
+ reportLeafIndex: undefined,
+ }
+ ],
+ id: 19,
+ parent: 18,
+ reportLeafIndex: undefined,
+ }
+ ],
+ id: 18,
+ parent: 11,
+ reportLeafIndex: 2,
+ },
+ {
+ name: "js::jit::JitScript",
+ bytes: 30,
+ totalBytes: 30,
+ count: 3,
+ totalCount: 3,
+ children: [
+ {
+ name: "scripts",
+ bytes: 0,
+ totalBytes: 30,
+ count: 0,
+ totalCount: 3,
+ children: [
+ {
+ name: null,
+ bytes: 0,
+ totalBytes: 220,
+ count: 0,
+ totalCount: 22,
+ children: undefined,
+ id: 23,
+ parent: 22,
+ reportLeafIndex: undefined,
+ }
+ ],
+ id: 22,
+ parent: 21,
+ reportLeafIndex: undefined,
+ }
+ ],
+ id: 21,
+ parent: 11,
+ reportLeafIndex: 5,
+ },
+ ],
+ id: 11,
+ parent: undefined,
+ reportLeafIndex: undefined,
+ };
+
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { invert: true });
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-08.js b/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-08.js
new file mode 100644
index 000000000..1c686c810
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-08.js
@@ -0,0 +1,142 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test inverting CensusTreeNode with a non-allocation stack breakdown.
+ */
+
+function run_test() {
+ const BREAKDOWN = {
+ by: "filename",
+ then: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true }
+ },
+ noFilename: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true }
+ },
+ };
+
+ const REPORT = {
+ "http://example.com/app.js": {
+ JSScript: { count: 10, bytes: 100 }
+ },
+ "http://example.com/ads.js": {
+ "js::LazyScript": { count: 20, bytes: 200 }
+ },
+ "http://example.com/trackers.js": {
+ JSScript: { count: 30, bytes: 300 }
+ },
+ noFilename: {
+ "js::jit::JitCode": { count: 40, bytes: 400 }
+ }
+ };
+
+ const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 1000,
+ count: 0,
+ totalCount: 100,
+ children: [
+ {
+ name: "noFilename",
+ bytes: 0,
+ totalBytes: 400,
+ count: 0,
+ totalCount: 40,
+ children: [
+ {
+ name: "js::jit::JitCode",
+ bytes: 400,
+ totalBytes: 400,
+ count: 40,
+ totalCount: 40,
+ children: undefined,
+ id: 9,
+ parent: 8,
+ reportLeafIndex: 8,
+ }
+ ],
+ id: 8,
+ parent: 1,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: "http://example.com/trackers.js",
+ bytes: 0,
+ totalBytes: 300,
+ count: 0,
+ totalCount: 30,
+ children: [
+ {
+ name: "JSScript",
+ bytes: 300,
+ totalBytes: 300,
+ count: 30,
+ totalCount: 30,
+ children: undefined,
+ id: 7,
+ parent: 6,
+ reportLeafIndex: 6,
+ }
+ ],
+ id: 6,
+ parent: 1,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: "http://example.com/ads.js",
+ bytes: 0,
+ totalBytes: 200,
+ count: 0,
+ totalCount: 20,
+ children: [
+ {
+ name: "js::LazyScript",
+ bytes: 200,
+ totalBytes: 200,
+ count: 20,
+ totalCount: 20,
+ children: undefined,
+ id: 5,
+ parent: 4,
+ reportLeafIndex: 4,
+ }
+ ],
+ id: 4,
+ parent: 1,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: "http://example.com/app.js",
+ bytes: 0,
+ totalBytes: 100,
+ count: 0,
+ totalCount: 10,
+ children: [
+ {
+ name: "JSScript",
+ bytes: 100,
+ totalBytes: 100,
+ count: 10,
+ totalCount: 10,
+ children: undefined,
+ id: 3,
+ parent: 2,
+ reportLeafIndex: 2,
+ }
+ ],
+ id: 2,
+ parent: 1,
+ reportLeafIndex: undefined,
+ }
+ ],
+ id: 1,
+ parent: undefined,
+ reportLeafIndex: undefined,
+ };
+
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-09.js b/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-09.js
new file mode 100644
index 000000000..3efed04b0
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-09.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Test that repeatedly converting the same census report to a CensusTreeNode
+ * tree results in the same CensusTreeNode tree.
+ */
+
+function run_test() {
+ const BREAKDOWN = {
+ by: "filename",
+ then: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true }
+ },
+ noFilename: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true }
+ },
+ };
+
+ const REPORT = {
+ "http://example.com/app.js": {
+ JSScript: { count: 10, bytes: 100 }
+ },
+ "http://example.com/ads.js": {
+ "js::LazyScript": { count: 20, bytes: 200 }
+ },
+ "http://example.com/trackers.js": {
+ JSScript: { count: 30, bytes: 300 }
+ },
+ noFilename: {
+ "js::jit::JitCode": { count: 40, bytes: 400 }
+ }
+ };
+
+ const first = censusReportToCensusTreeNode(BREAKDOWN, REPORT);
+ const second = censusReportToCensusTreeNode(BREAKDOWN, REPORT);
+ const third = censusReportToCensusTreeNode(BREAKDOWN, REPORT);
+
+ assertStructurallyEquivalent(first, second);
+ assertStructurallyEquivalent(second, third);
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-10.js b/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-10.js
new file mode 100644
index 000000000..b7798f23f
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_census-tree-node-10.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Test when multiple leaves in the census report map to the same node in an
+ * inverted CensusReportTree.
+ */
+
+function run_test() {
+ const BREAKDOWN = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ strings: { by: "count", count: true, bytes: true },
+ scripts: { by: "count", count: true, bytes: true },
+ };
+
+ const REPORT = {
+ objects: {
+ Array: { count: 1, bytes: 10 },
+ },
+ other: {
+ Array: { count: 1, bytes: 10 },
+ },
+ strings: { count: 0, bytes: 0 },
+ scripts: { count: 0, bytes: 0 },
+ };
+
+ const node = censusReportToCensusTreeNode(BREAKDOWN, REPORT, { invert: true });
+
+ equal(node.children[0].name, "Array");
+ equal(node.children[0].reportLeafIndex.size, 2);
+ dumpn(`node.children[0].reportLeafIndex = ${[...node.children[0].reportLeafIndex]}`);
+ ok(node.children[0].reportLeafIndex.has(2));
+ ok(node.children[0].reportLeafIndex.has(6));
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_census_diff_01.js b/devtools/shared/heapsnapshot/tests/unit/test_census_diff_01.js
new file mode 100644
index 000000000..75977bccb
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_census_diff_01.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test diffing census reports of breakdown by "internalType".
+
+const BREAKDOWN = {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true }
+};
+
+const REPORT1 = {
+ "JSObject": {
+ "count": 10,
+ "bytes": 100,
+ },
+ "js::Shape": {
+ "count": 50,
+ "bytes": 500,
+ },
+ "JSString": {
+ "count": 0,
+ "bytes": 0,
+ },
+ "js::LazyScript": {
+ "count": 1,
+ "bytes": 10,
+ },
+};
+
+const REPORT2 = {
+ "JSObject": {
+ "count": 11,
+ "bytes": 110,
+ },
+ "js::Shape": {
+ "count": 51,
+ "bytes": 510,
+ },
+ "JSString": {
+ "count": 1,
+ "bytes": 1,
+ },
+ "js::BaseShape": {
+ "count": 1,
+ "bytes": 42,
+ },
+};
+
+const EXPECTED = {
+ "JSObject": {
+ "count": 1,
+ "bytes": 10,
+ },
+ "js::Shape": {
+ "count": 1,
+ "bytes": 10,
+ },
+ "JSString": {
+ "count": 1,
+ "bytes": 1,
+ },
+ "js::LazyScript": {
+ "count": -1,
+ "bytes": -10,
+ },
+ "js::BaseShape": {
+ "count": 1,
+ "bytes": 42,
+ },
+};
+
+function run_test() {
+ assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_census_diff_02.js b/devtools/shared/heapsnapshot/tests/unit/test_census_diff_02.js
new file mode 100644
index 000000000..169e3f036
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_census_diff_02.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test diffing census reports of breakdown by "count".
+
+const BREAKDOWN = { by: "count", count: true, bytes: true };
+
+const REPORT1 = {
+ "count": 10,
+ "bytes": 100,
+};
+
+const REPORT2 = {
+ "count": 11,
+ "bytes": 110,
+};
+
+const EXPECTED = {
+ "count": 1,
+ "bytes": 10,
+};
+
+function run_test() {
+ assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_census_diff_03.js b/devtools/shared/heapsnapshot/tests/unit/test_census_diff_03.js
new file mode 100644
index 000000000..6dbca3e40
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_census_diff_03.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test diffing census reports of breakdown by "coarseType".
+
+const BREAKDOWN = {
+ by: "coarseType",
+ objects: { by: "count", count: true, bytes: true },
+ scripts: { by: "count", count: true, bytes: true },
+ strings: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+};
+
+const REPORT1 = {
+ objects: {
+ count: 1,
+ bytes: 10,
+ },
+ scripts: {
+ count: 1,
+ bytes: 10,
+ },
+ strings: {
+ count: 1,
+ bytes: 10,
+ },
+ other: {
+ count: 3,
+ bytes: 30,
+ },
+};
+
+const REPORT2 = {
+ objects: {
+ count: 1,
+ bytes: 10,
+ },
+ scripts: {
+ count: 0,
+ bytes: 0,
+ },
+ strings: {
+ count: 2,
+ bytes: 20,
+ },
+ other: {
+ count: 4,
+ bytes: 40,
+ },
+};
+
+const EXPECTED = {
+ objects: {
+ count: 0,
+ bytes: 0,
+ },
+ scripts: {
+ count: -1,
+ bytes: -10,
+ },
+ strings: {
+ count: 1,
+ bytes: 10,
+ },
+ other: {
+ count: 1,
+ bytes: 10,
+ },
+};
+
+function run_test() {
+ assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_census_diff_04.js b/devtools/shared/heapsnapshot/tests/unit/test_census_diff_04.js
new file mode 100644
index 000000000..a10097945
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_census_diff_04.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test diffing census reports of breakdown by "objectClass".
+
+const BREAKDOWN = {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+};
+
+const REPORT1 = {
+ "Array": {
+ count: 1,
+ bytes: 100,
+ },
+ "Function": {
+ count: 10,
+ bytes: 10,
+ },
+ "other": {
+ count: 10,
+ bytes: 100,
+ }
+};
+
+const REPORT2 = {
+ "Object": {
+ count: 1,
+ bytes: 100,
+ },
+ "Function": {
+ count: 20,
+ bytes: 20,
+ },
+ "other": {
+ count: 10,
+ bytes: 100,
+ }
+};
+
+const EXPECTED = {
+ "Array": {
+ count: -1,
+ bytes: -100,
+ },
+ "Function": {
+ count: 10,
+ bytes: 10,
+ },
+ "other": {
+ count: 0,
+ bytes: 0,
+ },
+ "Object": {
+ count: 1,
+ bytes: 100,
+ },
+};
+
+function run_test() {
+ assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_census_diff_05.js b/devtools/shared/heapsnapshot/tests/unit/test_census_diff_05.js
new file mode 100644
index 000000000..b6d99f823
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_census_diff_05.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test diffing census reports of breakdown by "allocationStack".
+
+const BREAKDOWN = {
+ by: "allocationStack",
+ then: { by: "count", count: true, bytes: true },
+ noStack: { by: "count", count: true, bytes: true },
+};
+
+const stack1 = saveStack();
+const stack2 = saveStack();
+const stack3 = saveStack();
+
+const REPORT1 = new Map([
+ [stack1, { "count": 10, "bytes": 100 }],
+ [stack2, { "count": 1, "bytes": 10 }],
+]);
+
+const REPORT2 = new Map([
+ [stack2, { "count": 10, "bytes": 100 }],
+ [stack3, { "count": 1, "bytes": 10 }],
+]);
+
+const EXPECTED = new Map([
+ [stack1, { "count": -10, "bytes": -100 }],
+ [stack2, { "count": 9, "bytes": 90 }],
+ [stack3, { "count": 1, "bytes": 10 }],
+]);
+
+function run_test() {
+ assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_census_diff_06.js b/devtools/shared/heapsnapshot/tests/unit/test_census_diff_06.js
new file mode 100644
index 000000000..430ff8c9c
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_census_diff_06.js
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test diffing census reports of a "complex" and "realistic" breakdown.
+
+const BREAKDOWN = {
+ by: "coarseType",
+ objects: {
+ by: "allocationStack",
+ then: {
+ by: "objectClass",
+ then: { by: "count", count: false, bytes: true },
+ other: { by: "count", count: false, bytes: true }
+ },
+ noStack: {
+ by: "objectClass",
+ then: { by: "count", count: false, bytes: true },
+ other: { by: "count", count: false, bytes: true }
+ }
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: false, bytes: true }
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: false, bytes: true }
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: false, bytes: true }
+ },
+};
+
+const stack1 = saveStack();
+const stack2 = saveStack();
+const stack3 = saveStack();
+
+const REPORT1 = {
+ objects: new Map([
+ [stack1, { Function: { bytes: 1 },
+ Object: { bytes: 2 },
+ other: { bytes: 0 },
+ }],
+ [stack2, { Array: { bytes: 3 },
+ Date: { bytes: 4 },
+ other: { bytes: 0 },
+ }],
+ ["noStack", { Object: { bytes: 3 }}],
+ ]),
+ strings: {
+ JSAtom: { bytes: 10 },
+ JSLinearString: { bytes: 5 },
+ },
+ scripts: {
+ JSScript: { bytes: 1 },
+ "js::jit::JitCode": { bytes: 2 },
+ },
+ other: {
+ "mozilla::dom::Thing": { bytes: 1 },
+ }
+};
+
+const REPORT2 = {
+ objects: new Map([
+ [stack2, { Array: { bytes: 1 },
+ Date: { bytes: 2 },
+ other: { bytes: 3 },
+ }],
+ [stack3, { Function: { bytes: 1 },
+ Object: { bytes: 2 },
+ other: { bytes: 0 },
+ }],
+ ["noStack", { Object: { bytes: 3 }}],
+ ]),
+ strings: {
+ JSAtom: { bytes: 5 },
+ JSLinearString: { bytes: 10 },
+ },
+ scripts: {
+ JSScript: { bytes: 2 },
+ "js::LazyScript": { bytes: 42 },
+ "js::jit::JitCode": { bytes: 1 },
+ },
+ other: {
+ "mozilla::dom::OtherThing": { bytes: 1 },
+ }
+};
+
+const EXPECTED = {
+ "objects": new Map([
+ [stack1, { Function: { bytes: -1 },
+ Object: { bytes: -2 },
+ other: { bytes: 0 },
+ }],
+ [stack2, { Array: { bytes: -2 },
+ Date: { bytes: -2 },
+ other: { bytes: 3 },
+ }],
+ [stack3, { Function: { bytes: 1 },
+ Object: { bytes: 2 },
+ other: { bytes: 0 },
+ }],
+ ["noStack", { Object: { bytes: 0 }}],
+ ]),
+ "scripts": {
+ "JSScript": {
+ "bytes": 1
+ },
+ "js::jit::JitCode": {
+ "bytes": -1
+ },
+ "js::LazyScript": {
+ "bytes": 42
+ }
+ },
+ "strings": {
+ "JSAtom": {
+ "bytes": -5
+ },
+ "JSLinearString": {
+ "bytes": 5
+ }
+ },
+ "other": {
+ "mozilla::dom::Thing": {
+ "bytes": -1
+ },
+ "mozilla::dom::OtherThing": {
+ "bytes": 1
+ }
+ }
+};
+
+function run_test() {
+ assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_census_filtering_01.js b/devtools/shared/heapsnapshot/tests/unit/test_census_filtering_01.js
new file mode 100644
index 000000000..57724d7c1
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_census_filtering_01.js
@@ -0,0 +1,105 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test filtering basic CensusTreeNode trees.
+
+function run_test() {
+ const BREAKDOWN = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other:{
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ };
+
+ const REPORT = {
+ objects: {
+ Array: { bytes: 50, count: 5 },
+ UInt8Array: { bytes: 80, count: 8 },
+ Int32Array: { bytes: 320, count: 32 },
+ other: { bytes: 0, count: 0 },
+ },
+ scripts: {
+ "js::jit::JitScript": { bytes: 30, count: 3 },
+ },
+ strings: {
+ JSAtom: { bytes: 60, count: 6 },
+ },
+ other: {
+ "js::Shape": { bytes: 80, count: 8 },
+ }
+ };
+
+ const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 620,
+ count: 0,
+ totalCount: 62,
+ children: [
+ {
+ name: "objects",
+ bytes: 0,
+ totalBytes: 450,
+ count: 0,
+ totalCount: 45,
+ children: [
+ {
+ name: "Int32Array",
+ bytes: 320,
+ totalBytes: 320,
+ count: 32,
+ totalCount: 32,
+ children: undefined,
+ id: 15,
+ parent: 14,
+ reportLeafIndex: 4,
+ },
+ {
+ name: "UInt8Array",
+ bytes: 80,
+ totalBytes: 80,
+ count: 8,
+ totalCount: 8,
+ children: undefined,
+ id: 16,
+ parent: 14,
+ reportLeafIndex: 3,
+ },
+ {
+ name: "Array",
+ bytes: 50,
+ totalBytes: 50,
+ count: 5,
+ totalCount: 5,
+ children: undefined,
+ id: 17,
+ parent: 14,
+ reportLeafIndex: 2,
+ }
+ ],
+ id: 14,
+ parent: 13,
+ reportLeafIndex: undefined,
+ }
+ ],
+ id: 13,
+ parent: undefined,
+ reportLeafIndex: undefined,
+ };
+
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { filter: "Array" });
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_census_filtering_02.js b/devtools/shared/heapsnapshot/tests/unit/test_census_filtering_02.js
new file mode 100644
index 000000000..0a57ce66d
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_census_filtering_02.js
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test filtering CensusTreeNode trees with an `allocationStack` breakdown.
+
+function run_test() {
+ const countBreakdown = { by: "count", count: true, bytes: true };
+
+ const BREAKDOWN = {
+ by: "allocationStack",
+ then: countBreakdown,
+ noStack: countBreakdown,
+ };
+
+ let stack1, stack2, stack3, stack4, stack5;
+
+ (function foo() {
+ (function bar() {
+ (function baz() {
+ stack1 = saveStack(3);
+ }());
+ (function quux() {
+ stack2 = saveStack(3);
+ stack3 = saveStack(3);
+ }());
+ }());
+ stack4 = saveStack(2);
+ }());
+
+ stack5 = saveStack(1);
+
+ const REPORT = new Map([
+ [stack1, { bytes: 10, count: 1 }],
+ [stack2, { bytes: 20, count: 2 }],
+ [stack3, { bytes: 30, count: 3 }],
+ [stack4, { bytes: 40, count: 4 }],
+ [stack5, { bytes: 50, count: 5 }],
+ ["noStack", { bytes: 60, count: 6 }],
+ ]);
+
+ const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 210,
+ count: 0,
+ totalCount: 21,
+ children: [
+ {
+ name: stack1.parent.parent,
+ bytes: 0,
+ totalBytes: 60,
+ count: 0,
+ totalCount: 6,
+ children: [
+ {
+ name: stack2.parent,
+ bytes: 0,
+ totalBytes: 50,
+ count: 0,
+ totalCount: 5,
+ children: [
+ {
+ name: stack3,
+ bytes: 30,
+ totalBytes: 30,
+ count: 3,
+ totalCount: 3,
+ children: undefined,
+ id: 15,
+ parent: 14,
+ reportLeafIndex: 3,
+ },
+ {
+ name: stack2,
+ bytes: 20,
+ totalBytes: 20,
+ count: 2,
+ totalCount: 2,
+ children: undefined,
+ id: 16,
+ parent: 14,
+ reportLeafIndex: 2,
+ }
+ ],
+ id: 14,
+ parent: 13,
+ reportLeafIndex: undefined,
+ },
+ {
+ name: stack1.parent,
+ bytes: 0,
+ totalBytes: 10,
+ count: 0,
+ totalCount: 1,
+ children: [
+ {
+ name: stack1,
+ bytes: 10,
+ totalBytes: 10,
+ count: 1,
+ totalCount: 1,
+ children: undefined,
+ id: 18,
+ parent: 17,
+ reportLeafIndex: 1,
+ }
+ ],
+ id: 17,
+ parent: 13,
+ reportLeafIndex: undefined,
+ }
+ ],
+ id: 13,
+ parent: 12,
+ reportLeafIndex: undefined,
+ }
+ ],
+ id: 12,
+ parent: undefined,
+ reportLeafIndex: undefined,
+ };
+
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { filter: "bar" });
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_census_filtering_03.js b/devtools/shared/heapsnapshot/tests/unit/test_census_filtering_03.js
new file mode 100644
index 000000000..2c69a14b8
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_census_filtering_03.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test filtering with no matches.
+
+function run_test() {
+ const BREAKDOWN = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ },
+ scripts: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ strings: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ other:{
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ };
+
+ const REPORT = {
+ objects: {
+ Array: { bytes: 50, count: 5 },
+ UInt8Array: { bytes: 80, count: 8 },
+ Int32Array: { bytes: 320, count: 32 },
+ other: { bytes: 0, count: 0 },
+ },
+ scripts: {
+ "js::jit::JitScript": { bytes: 30, count: 3 },
+ },
+ strings: {
+ JSAtom: { bytes: 60, count: 6 },
+ },
+ other: {
+ "js::Shape": { bytes: 80, count: 8 },
+ }
+ };
+
+ const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 620,
+ count: 0,
+ totalCount: 62,
+ children: undefined,
+ id: 13,
+ parent: undefined,
+ reportLeafIndex: undefined,
+ };
+
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { filter: "zzzzzzzzzzzzzzzzzzzz" });
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_census_filtering_04.js b/devtools/shared/heapsnapshot/tests/unit/test_census_filtering_04.js
new file mode 100644
index 000000000..c9871436b
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_census_filtering_04.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test the filtered nodes' counts and bytes are the same as they were when
+// unfiltered.
+
+function run_test() {
+ const COUNT = { by: "count", count: true, bytes: true };
+ const INTERNAL_TYPE = { by: "internalType", then: COUNT };
+
+ const BREAKDOWN = {
+ by: "coarseType",
+ objects: { by: "objectClass", then: COUNT, other: COUNT },
+ strings: COUNT,
+ scripts: {
+ by: "filename",
+ then: INTERNAL_TYPE,
+ noFilename: INTERNAL_TYPE
+ },
+ other: INTERNAL_TYPE,
+ };
+
+ const REPORT = {
+ objects: {
+ Function: {
+ count: 7,
+ bytes: 70
+ },
+ Array: {
+ count: 6,
+ bytes: 60
+ }
+ },
+ scripts: {
+ "http://mozilla.github.io/pdf.js/build/pdf.js": {
+ "js::LazyScript": {
+ count: 4,
+ bytes: 40
+ },
+ }
+ },
+ strings: {
+ count: 2,
+ bytes: 20
+ },
+ other: {
+ "js::Shape": {
+ count: 1,
+ bytes: 10
+ }
+ }
+ };
+
+ const EXPECTED = {
+ name: null,
+ bytes: 0,
+ totalBytes: 200,
+ count: 0,
+ totalCount: 20,
+ parent: undefined,
+ children: [
+ {
+ name: "objects",
+ bytes: 0,
+ totalBytes: 130,
+ count: 0,
+ totalCount: 13,
+ children: [
+ {
+ name: "Function",
+ bytes: 70,
+ totalBytes: 70,
+ count: 7,
+ totalCount: 7,
+ id: 13,
+ parent: 12,
+ children: undefined,
+ reportLeafIndex: 2,
+ },
+ {
+ name: "Array",
+ bytes: 60,
+ totalBytes: 60,
+ count: 6,
+ totalCount: 6,
+ id: 14,
+ parent: 12,
+ children: undefined,
+ reportLeafIndex: 3,
+ },
+ ],
+ id: 12,
+ parent: 11,
+ reportLeafIndex: undefined,
+ }
+ ],
+ id: 11,
+ reportLeafIndex: undefined,
+ };
+
+ compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { filter: "objects" });
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_census_filtering_05.js b/devtools/shared/heapsnapshot/tests/unit/test_census_filtering_05.js
new file mode 100644
index 000000000..1d1f4fa55
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_census_filtering_05.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that filtered and inverted allocation stack census trees are sorted
+// properly.
+
+function run_test() {
+ const countBreakdown = { by: "count", count: true, bytes: true };
+
+ const BREAKDOWN = {
+ by: "allocationStack",
+ then: countBreakdown,
+ noStack: countBreakdown,
+ };
+
+ const stacks = [];
+
+ function foo(depth = 1) {
+ stacks.push(saveStack(depth));
+ bar(depth + 1);
+ baz(depth + 1);
+ stacks.push(saveStack(depth));
+ }
+
+ function bar(depth = 1) {
+ stacks.push(saveStack(depth));
+ stacks.push(saveStack(depth));
+ }
+
+ function baz(depth = 1) {
+ stacks.push(saveStack(depth));
+ bang(depth + 1);
+ stacks.push(saveStack(depth));
+ }
+
+ function bang(depth = 1) {
+ stacks.push(saveStack(depth));
+ stacks.push(saveStack(depth));
+ stacks.push(saveStack(depth));
+ }
+
+ foo();
+ bar();
+ baz();
+ bang();
+
+ const REPORT = new Map(stacks.map((s, i) => {
+ return [s, {
+ count: i + 1,
+ bytes: (i + 1) * 10
+ }];
+ }));
+
+ const tree = censusReportToCensusTreeNode(BREAKDOWN, REPORT, {
+ filter: "baz",
+ invert: true
+ });
+
+ dumpn("tree = " + JSON.stringify(tree, savedFrameReplacer, 4));
+
+ (function assertSortedBySelf(node) {
+ if (node.children) {
+ let lastSelfBytes = Infinity;
+ for (let child of node.children) {
+ ok(child.bytes <= lastSelfBytes, `${child.bytes} <= ${lastSelfBytes}`);
+ lastSelfBytes = child.bytes;
+ assertSortedBySelf(child);
+ }
+ }
+ }(tree));
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_countToBucketBreakdown_01.js b/devtools/shared/heapsnapshot/tests/unit/test_countToBucketBreakdown_01.js
new file mode 100644
index 000000000..e89048c33
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_countToBucketBreakdown_01.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that we can turn a breakdown with { by: "count" } leaves into a
+// breakdown with { by: "bucket" } leaves.
+
+const COUNT = { by: "count", count: true, bytes: true };
+const BUCKET = { by: "bucket" };
+
+const BREAKDOWN = {
+ by: "coarseType",
+ objects: { by: "objectClass", then: COUNT, other: COUNT },
+ strings: COUNT,
+ scripts: {
+ by: "filename",
+ then: { by: "internalType", then: COUNT },
+ noFilename: { by: "internalType", then: COUNT },
+ },
+ other: { by: "internalType", then: COUNT },
+};
+
+const EXPECTED = {
+ by: "coarseType",
+ objects: { by: "objectClass", then: BUCKET, other: BUCKET },
+ strings: BUCKET,
+ scripts: {
+ by: "filename",
+ then: { by: "internalType", then: BUCKET },
+ noFilename: { by: "internalType", then: BUCKET },
+ },
+ other: { by: "internalType", then: BUCKET },
+};
+
+function run_test() {
+ assertCountToBucketBreakdown(BREAKDOWN, EXPECTED);
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_deduplicatePaths_01.js b/devtools/shared/heapsnapshot/tests/unit/test_deduplicatePaths_01.js
new file mode 100644
index 000000000..418b49db3
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_deduplicatePaths_01.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test the behavior of the deduplicatePaths utility function.
+
+function edge(from, to, name) {
+ return { from, to, name };
+}
+
+function run_test() {
+ const a = 1;
+ const b = 2;
+ const c = 3;
+ const d = 4;
+ const e = 5;
+ const f = 6;
+ const g = 7;
+
+ dumpn("Single long path");
+ assertDeduplicatedPaths({
+ target: g,
+ paths: [
+ [
+ pathEntry(a, "e1"),
+ pathEntry(b, "e2"),
+ pathEntry(c, "e3"),
+ pathEntry(d, "e4"),
+ pathEntry(e, "e5"),
+ pathEntry(f, "e6"),
+ ]
+ ],
+ expectedNodes: [a, b, c, d, e, f, g],
+ expectedEdges: [
+ edge(a, b, "e1"),
+ edge(b, c, "e2"),
+ edge(c, d, "e3"),
+ edge(d, e, "e4"),
+ edge(e, f, "e5"),
+ edge(f, g, "e6"),
+ ]
+ });
+
+ dumpn("Multiple edges from and to the same nodes");
+ assertDeduplicatedPaths({
+ target: a,
+ paths: [
+ [pathEntry(b, "x")],
+ [pathEntry(b, "y")],
+ [pathEntry(b, "z")],
+ ],
+ expectedNodes: [a, b],
+ expectedEdges: [
+ edge(b, a, "x"),
+ edge(b, a, "y"),
+ edge(b, a, "z"),
+ ]
+ });
+
+ dumpn("Multiple paths sharing some nodes and edges");
+ assertDeduplicatedPaths({
+ target: g,
+ paths: [
+ [
+ pathEntry(a, "a->b"),
+ pathEntry(b, "b->c"),
+ pathEntry(c, "foo"),
+ ],
+ [
+ pathEntry(a, "a->b"),
+ pathEntry(b, "b->d"),
+ pathEntry(d, "bar"),
+ ],
+ [
+ pathEntry(a, "a->b"),
+ pathEntry(b, "b->e"),
+ pathEntry(e, "baz"),
+ ],
+ ],
+ expectedNodes: [a, b, c, d, e, g],
+ expectedEdges: [
+ edge(a, b, "a->b"),
+ edge(b, c, "b->c"),
+ edge(b, d, "b->d"),
+ edge(b, e, "b->e"),
+ edge(c, g, "foo"),
+ edge(d, g, "bar"),
+ edge(e, g, "baz"),
+ ]
+ });
+
+ dumpn("Second shortest path contains target itself");
+ assertDeduplicatedPaths({
+ target: g,
+ paths: [
+ [
+ pathEntry(a, "a->b"),
+ pathEntry(b, "b->g"),
+ ],
+ [
+ pathEntry(a, "a->b"),
+ pathEntry(b, "b->g"),
+ pathEntry(g, "g->f"),
+ pathEntry(f, "f->g"),
+ ],
+ ],
+ expectedNodes: [a, b, g],
+ expectedEdges: [
+ edge(a, b, "a->b"),
+ edge(b, g, "b->g"),
+ ]
+ });
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_getCensusIndividuals_01.js b/devtools/shared/heapsnapshot/tests/unit/test_getCensusIndividuals_01.js
new file mode 100644
index 000000000..9c4f60991
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_getCensusIndividuals_01.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test basic functionality of `CensusUtils.getCensusIndividuals`.
+
+function run_test() {
+ const stack1 = saveStack(1);
+ const stack2 = saveStack(1);
+ const stack3 = saveStack(1);
+
+ const COUNT = { by: "count", count: true, bytes: true };
+ const INTERNAL_TYPE = { by: "internalType", then: COUNT };
+
+ const BREAKDOWN = {
+ by: "allocationStack",
+ then: INTERNAL_TYPE,
+ noStack: INTERNAL_TYPE,
+ };
+
+ const MOCK_SNAPSHOT = {
+ takeCensus: ({ breakdown }) => {
+ assertStructurallyEquivalent(
+ breakdown,
+ CensusUtils.countToBucketBreakdown(BREAKDOWN));
+
+ // DFS Index
+ return new Map([ // 0
+ [stack1, { // 1
+ JSObject: [101, 102, 103], // 2
+ JSString: [111, 112, 113], // 3
+ }],
+ [stack2, { // 4
+ JSObject: [201, 202, 203], // 5
+ JSString: [211, 212, 213], // 6
+ }],
+ [stack3, { // 7
+ JSObject: [301, 302, 303], // 8
+ JSString: [311, 312, 313], // 9
+ }],
+ ["noStack", { // 10
+ JSObject: [401, 402, 403], // 11
+ JSString: [411, 412, 413], // 12
+ }],
+ ]);
+ }
+ };
+
+ const INDICES = new Set([3, 5, 9]);
+
+ const EXPECTED = new Set([111, 112, 113,
+ 201, 202, 203,
+ 311, 312, 313]);
+
+ const actual = new Set(CensusUtils.getCensusIndividuals(INDICES,
+ BREAKDOWN,
+ MOCK_SNAPSHOT));
+
+ assertStructurallyEquivalent(EXPECTED, actual);
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_getReportLeaves_01.js b/devtools/shared/heapsnapshot/tests/unit/test_getReportLeaves_01.js
new file mode 100644
index 000000000..4c4298b6a
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_getReportLeaves_01.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test basic functionality of `CensusUtils.getReportLeaves`.
+
+function run_test() {
+ const BREAKDOWN = {
+ by: "coarseType",
+ objects: {
+ by: "objectClass",
+ then: { by: "count", count: true, bytes: true },
+ other: { by: "count", count: true, bytes: true },
+ },
+ strings: { by: "count", count: true, bytes: true },
+ scripts: {
+ by: "filename",
+ then: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ noFilename: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ },
+ other: {
+ by: "internalType",
+ then: { by: "count", count: true, bytes: true },
+ },
+ };
+
+ const REPORT = {
+ objects: {
+ Array: { count: 6, bytes: 60 },
+ Function: { count: 1, bytes: 10 },
+ Object: { count: 1, bytes: 10 },
+ RegExp: { count: 1, bytes: 10 },
+ other: { count: 0, bytes: 0 },
+ },
+ strings: { count: 1, bytes: 10 },
+ scripts: {
+ "foo.js": {
+ JSScript: { count: 1, bytes: 10 },
+ "js::jit::IonScript": { count: 1, bytes: 10 },
+ },
+ noFilename: {
+ JSScript: { count: 1, bytes: 10 },
+ "js::jit::IonScript": { count: 1, bytes: 10 },
+ },
+ },
+ other: {
+ "js::Shape": { count: 7, bytes: 70 },
+ "js::BaseShape": { count: 1, bytes: 10 },
+ },
+ };
+
+ const root = censusReportToCensusTreeNode(BREAKDOWN, REPORT);
+ dumpn("CensusTreeNode tree = " + JSON.stringify(root, null, 4));
+
+ (function assertEveryNodeCanFindItsLeaf(node) {
+ if (node.reportLeafIndex) {
+ const [ leaf ] = CensusUtils.getReportLeaves(new Set([node.reportLeafIndex]),
+ BREAKDOWN,
+ REPORT);
+ ok(leaf, "Should be able to find leaf for a node with a reportLeafIndex = " + node.reportLeafIndex);
+ }
+
+ if (node.children) {
+ for (let child of node.children) {
+ assertEveryNodeCanFindItsLeaf(child);
+ }
+ }
+ }(root));
+
+ // Test finding multiple leaves at a time.
+
+ function find(name, node) {
+ if (node.name === name) {
+ return node;
+ }
+
+ if (node.children) {
+ for (let child of node.children) {
+ const found = find(name, child);
+ if (found) {
+ return found;
+ }
+ }
+ }
+ }
+
+ const arrayNode = find("Array", root);
+ ok(arrayNode);
+ equal(typeof arrayNode.reportLeafIndex, "number");
+
+ const shapeNode = find("js::Shape", root);
+ ok(shapeNode);
+ equal(typeof shapeNode.reportLeafIndex, "number");
+
+ const indices = new Set([arrayNode.reportLeafIndex, shapeNode.reportLeafIndex]);
+ const leaves = CensusUtils.getReportLeaves(indices, BREAKDOWN, REPORT);
+ equal(leaves.length, 2);
+
+ // `getReportLeaves` does not guarantee order of the results, so handle both
+ // cases.
+ ok(leaves.some(l => l === REPORT.objects.Array));
+ ok(leaves.some(l => l === REPORT.other["js::Shape"]));
+
+ // Test that bad indices do not yield results.
+
+ const none = CensusUtils.getReportLeaves(new Set([999999999999]), BREAKDOWN, REPORT);
+ equal(none.length, 0);
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/test_saveHeapSnapshot_e10s_01.js b/devtools/shared/heapsnapshot/tests/unit/test_saveHeapSnapshot_e10s_01.js
new file mode 100644
index 000000000..067b9effb
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_saveHeapSnapshot_e10s_01.js
@@ -0,0 +1,8 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test saving a heap snapshot in the sandboxed e10s child process.
+
+function run_test() {
+ run_test_in_child("../unit/test_SaveHeapSnapshot.js");
+}
diff --git a/devtools/shared/heapsnapshot/tests/unit/xpcshell.ini b/devtools/shared/heapsnapshot/tests/unit/xpcshell.ini
new file mode 100644
index 000000000..f84b282d1
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/xpcshell.ini
@@ -0,0 +1,98 @@
+[DEFAULT]
+tags = devtools heapsnapshot devtools-memory
+head = head_heapsnapshot.js
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+support-files =
+ Census.jsm
+ dominator-tree-worker.js
+ heap-snapshot-worker.js
+ Match.jsm
+
+[test_census_diff_01.js]
+[test_census_diff_02.js]
+[test_census_diff_03.js]
+[test_census_diff_04.js]
+[test_census_diff_05.js]
+[test_census_diff_06.js]
+[test_census_filtering_01.js]
+[test_census_filtering_02.js]
+[test_census_filtering_03.js]
+[test_census_filtering_04.js]
+[test_census_filtering_05.js]
+[test_census-tree-node-01.js]
+[test_census-tree-node-02.js]
+[test_census-tree-node-03.js]
+[test_census-tree-node-04.js]
+[test_census-tree-node-05.js]
+[test_census-tree-node-06.js]
+[test_census-tree-node-07.js]
+[test_census-tree-node-08.js]
+[test_census-tree-node-09.js]
+[test_census-tree-node-10.js]
+[test_countToBucketBreakdown_01.js]
+[test_deduplicatePaths_01.js]
+[test_DominatorTree_01.js]
+[test_DominatorTree_02.js]
+[test_DominatorTree_03.js]
+[test_DominatorTree_04.js]
+[test_DominatorTree_05.js]
+[test_DominatorTree_06.js]
+[test_DominatorTreeNode_attachShortestPaths_01.js]
+[test_DominatorTreeNode_getNodeByIdAlongPath_01.js]
+[test_DominatorTreeNode_insert_01.js]
+[test_DominatorTreeNode_insert_02.js]
+[test_DominatorTreeNode_insert_03.js]
+[test_DominatorTreeNode_LabelAndShallowSize_01.js]
+[test_DominatorTreeNode_LabelAndShallowSize_02.js]
+[test_DominatorTreeNode_LabelAndShallowSize_03.js]
+[test_DominatorTreeNode_LabelAndShallowSize_04.js]
+[test_DominatorTreeNode_partialTraversal_01.js]
+[test_getCensusIndividuals_01.js]
+[test_getReportLeaves_01.js]
+[test_HeapAnalyses_computeDominatorTree_01.js]
+[test_HeapAnalyses_computeDominatorTree_02.js]
+[test_HeapAnalyses_deleteHeapSnapshot_01.js]
+[test_HeapAnalyses_deleteHeapSnapshot_02.js]
+[test_HeapAnalyses_deleteHeapSnapshot_03.js]
+[test_HeapAnalyses_getCensusIndividuals_01.js]
+[test_HeapAnalyses_getCreationTime_01.js]
+[test_HeapAnalyses_getDominatorTree_01.js]
+[test_HeapAnalyses_getDominatorTree_02.js]
+[test_HeapAnalyses_getImmediatelyDominated_01.js]
+[test_HeapAnalyses_readHeapSnapshot_01.js]
+[test_HeapAnalyses_takeCensusDiff_01.js]
+[test_HeapAnalyses_takeCensusDiff_02.js]
+[test_HeapAnalyses_takeCensus_01.js]
+[test_HeapAnalyses_takeCensus_02.js]
+[test_HeapAnalyses_takeCensus_03.js]
+[test_HeapAnalyses_takeCensus_04.js]
+[test_HeapAnalyses_takeCensus_05.js]
+[test_HeapAnalyses_takeCensus_06.js]
+[test_HeapAnalyses_takeCensus_07.js]
+[test_HeapSnapshot_creationTime_01.js]
+[test_HeapSnapshot_deepStack_01.js]
+[test_HeapSnapshot_describeNode_01.js]
+[test_HeapSnapshot_computeShortestPaths_01.js]
+[test_HeapSnapshot_computeShortestPaths_02.js]
+[test_HeapSnapshot_takeCensus_01.js]
+[test_HeapSnapshot_takeCensus_02.js]
+[test_HeapSnapshot_takeCensus_03.js]
+[test_HeapSnapshot_takeCensus_04.js]
+[test_HeapSnapshot_takeCensus_05.js]
+[test_HeapSnapshot_takeCensus_06.js]
+[test_HeapSnapshot_takeCensus_07.js]
+[test_HeapSnapshot_takeCensus_08.js]
+[test_HeapSnapshot_takeCensus_09.js]
+[test_HeapSnapshot_takeCensus_10.js]
+[test_HeapSnapshot_takeCensus_11.js]
+[test_HeapSnapshot_takeCensus_12.js]
+[test_ReadHeapSnapshot.js]
+[test_ReadHeapSnapshot_with_allocations.js]
+skip-if = os == 'linux' # Bug 1176173
+[test_ReadHeapSnapshot_worker.js]
+skip-if = os == 'linux' # Bug 1176173
+[test_SaveHeapSnapshot.js]
+[test_saveHeapSnapshot_e10s_01.js]