summaryrefslogtreecommitdiffstats
path: root/devtools/shared/heapsnapshot/tests
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/heapsnapshot/tests')
-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
104 files changed, 7679 insertions, 0 deletions
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]