diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /devtools/shared/heapsnapshot/tests | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream/115.8.0esr.tar.xz firefox-esr-upstream/115.8.0esr.zip |
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/shared/heapsnapshot/tests')
107 files changed, 8228 insertions, 0 deletions
diff --git a/devtools/shared/heapsnapshot/tests/browser/browser.ini b/devtools/shared/heapsnapshot/tests/browser/browser.ini new file mode 100644 index 0000000000..f3799a5ef8 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/browser/browser.ini @@ -0,0 +1,8 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + !/devtools/client/shared/test/shared-head.js + !/devtools/client/shared/test/telemetry-test-helpers.js + +[browser_saveHeapSnapshot_e10s_01.js] diff --git a/devtools/shared/heapsnapshot/tests/browser/browser_saveHeapSnapshot_e10s_01.js b/devtools/shared/heapsnapshot/tests/browser/browser_saveHeapSnapshot_e10s_01.js new file mode 100644 index 0000000000..1fc25341b8 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/browser/browser_saveHeapSnapshot_e10s_01.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Bug 1201597 - Test to verify that we can take a heap snapshot in an e10s child process. + */ + +"use strict"; + +add_task(async function () { + // Create a minimal browser + const browser = document.createXULElement("browser"); + browser.setAttribute("type", "content"); + document.body.appendChild(browser); + await BrowserTestUtils.browserLoaded(browser); + + info("Save heap snapshot"); + const result = await SpecialPowers.spawn(browser, [], () => { + try { + ChromeUtils.saveHeapSnapshot({ runtime: true }); + } catch (err) { + return err.toString(); + } + + return ""; + }); + is(result, "", "result of saveHeapSnapshot"); + + browser.remove(); +}); diff --git a/devtools/shared/heapsnapshot/tests/chrome/chrome.ini b/devtools/shared/heapsnapshot/tests/chrome/chrome.ini new file mode 100644 index 0000000000..497b6fe37e --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/chrome/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/chrome/test_DominatorTree_01.html b/devtools/shared/heapsnapshot/tests/chrome/test_DominatorTree_01.html new file mode 100644 index 0000000000..61e60ae209 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/chrome/test_DominatorTree_01.html @@ -0,0 +1,40 @@ +<!DOCTYPE HTML> +<html> +<!-- +Sanity test that we can compute dominator trees from a heap snapshot in a web window. +--> +<head> + <meta charset="utf-8"> + <title>ChromeUtils.saveHeapSnapshot test</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> +<script> +"use strict"; +/* global window, ChromeUtils, DominatorTree */ + +SimpleTest.waitForExplicitFinish(); +window.onload = function() { + const path = ChromeUtils.saveHeapSnapshot({ runtime: true }); + const snapshot = ChromeUtils.readHeapSnapshot(path); + + const dominatorTree = snapshot.computeDominatorTree(); + ok(dominatorTree); + ok(DominatorTree.isInstance(dominatorTree)); + + let threw = false; + try { + new DominatorTree(); + } catch (e) { + threw = true; + } + ok(threw, "Constructor shouldn't be usable"); + + SimpleTest.finish(); +}; +</script> +</pre> +</body> +</html> diff --git a/devtools/shared/heapsnapshot/tests/chrome/test_SaveHeapSnapshot.html b/devtools/shared/heapsnapshot/tests/chrome/test_SaveHeapSnapshot.html new file mode 100644 index 0000000000..53831bfaa2 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/chrome/test_SaveHeapSnapshot.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<!-- +Bug 1024774 - Sanity test that we can take a heap snapshot in a web window. +--> +<head> + <meta charset="utf-8"> + <title>ChromeUtils.saveHeapSnapshot test</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<pre id="test"> +<script> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +window.onload = function() { + ok(ChromeUtils, "The ChromeUtils interface should be exposed in chrome windows."); + ChromeUtils.saveHeapSnapshot({ runtime: true }); + ok(true, "Should save a heap snapshot and shouldn't throw."); + SimpleTest.finish(); +}; +</script> +</pre> +</body> +</html> diff --git a/devtools/shared/heapsnapshot/tests/gtest/DeserializedNodeUbiNodes.cpp b/devtools/shared/heapsnapshot/tests/gtest/DeserializedNodeUbiNodes.cpp new file mode 100644 index 0000000000..dc24d13e98 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/gtest/DeserializedNodeUbiNodes.cpp @@ -0,0 +1,95 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Test that the `JS::ubi::Node`s we create from +// `mozilla::devtools::DeserializedNode` instances look and behave as we would +// like. + +#include "DevTools.h" +#include "js/TypeDecls.h" +#include "mozilla/devtools/DeserializedNode.h" + +using testing::Field; +using testing::ReturnRef; + +// A mock DeserializedNode for testing. +struct MockDeserializedNode : public DeserializedNode { + MockDeserializedNode(NodeId id, const char16_t* typeName, uint64_t size) + : DeserializedNode(id, typeName, size) {} + + bool addEdge(DeserializedEdge&& edge) { + return edges.append(std::move(edge)); + } + + MOCK_METHOD1(getEdgeReferent, JS::ubi::Node(const DeserializedEdge&)); +}; + +size_t fakeMallocSizeOf(const void*) { + EXPECT_TRUE(false); + MOZ_ASSERT_UNREACHABLE( + "fakeMallocSizeOf should never be called because " + "DeserializedNodes report the deserialized size."); + return 0; +} + +DEF_TEST(DeserializedNodeUbiNodes, { + const char16_t* typeName = u"TestTypeName"; + const char* className = "MyObjectClassName"; + const char* filename = "my-cool-filename.js"; + + NodeId id = uint64_t(1) << 33; + uint64_t size = uint64_t(1) << 60; + MockDeserializedNode mocked(id, typeName, size); + mocked.coarseType = JS::ubi::CoarseType::Script; + mocked.jsObjectClassName = className; + mocked.scriptFilename = filename; + + DeserializedNode& deserialized = mocked; + JS::ubi::Node ubi(&deserialized); + + // Test the ubi::Node accessors. + + EXPECT_EQ(size, ubi.size(fakeMallocSizeOf)); + EXPECT_EQ(typeName, ubi.typeName()); + EXPECT_EQ(JS::ubi::CoarseType::Script, ubi.coarseType()); + EXPECT_EQ(id, ubi.identifier()); + EXPECT_FALSE(ubi.isLive()); + EXPECT_EQ(ubi.jsObjectClassName(), className); + EXPECT_EQ(ubi.scriptFilename(), filename); + + // Test the ubi::Node's edges. + + UniquePtr<DeserializedNode> referent1( + new MockDeserializedNode(1, nullptr, 10)); + DeserializedEdge edge1(referent1->id); + mocked.addEdge(std::move(edge1)); + EXPECT_CALL(mocked, getEdgeReferent(EdgeTo(referent1->id))) + .Times(1) + .WillOnce(Return(JS::ubi::Node(referent1.get()))); + + UniquePtr<DeserializedNode> referent2( + new MockDeserializedNode(2, nullptr, 20)); + DeserializedEdge edge2(referent2->id); + mocked.addEdge(std::move(edge2)); + EXPECT_CALL(mocked, getEdgeReferent(EdgeTo(referent2->id))) + .Times(1) + .WillOnce(Return(JS::ubi::Node(referent2.get()))); + + UniquePtr<DeserializedNode> referent3( + new MockDeserializedNode(3, nullptr, 30)); + DeserializedEdge edge3(referent3->id); + mocked.addEdge(std::move(edge3)); + EXPECT_CALL(mocked, getEdgeReferent(EdgeTo(referent3->id))) + .Times(1) + .WillOnce(Return(JS::ubi::Node(referent3.get()))); + + auto range = ubi.edges(cx); + ASSERT_TRUE(!!range); + + for (; !range->empty(); range->popFront()) { + // Nothing to do here. This loop ensures that we get each edge referent + // that we expect above. + } +}); diff --git a/devtools/shared/heapsnapshot/tests/gtest/DeserializedStackFrameUbiStackFrames.cpp b/devtools/shared/heapsnapshot/tests/gtest/DeserializedStackFrameUbiStackFrames.cpp new file mode 100644 index 0000000000..37e1d426f8 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/gtest/DeserializedStackFrameUbiStackFrames.cpp @@ -0,0 +1,96 @@ +/* -*- 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/SavedFrameAPI.h" +#include "js/TypeDecls.h" +#include "mozilla/devtools/DeserializedNode.h" + +using testing::Field; +using testing::ReturnRef; + +// A mock DeserializedStackFrame for testing. +struct MockDeserializedStackFrame : public DeserializedStackFrame { + MockDeserializedStackFrame() : 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::Rooted<JSObject*> savedFrame(cx); + EXPECT_TRUE(ubiFrame.constructSavedFrameStack(cx, &savedFrame)); + + JSPrincipals* principals = JS::GetRealmPrincipals(js::GetContextRealm(cx)); + + uint32_t frameLine; + ASSERT_EQ(JS::SavedFrameResult::Ok, + JS::GetSavedFrameLine(cx, principals, savedFrame, &frameLine)); + EXPECT_EQ(line, frameLine); + + uint32_t frameColumn; + ASSERT_EQ(JS::SavedFrameResult::Ok, + JS::GetSavedFrameColumn(cx, principals, savedFrame, &frameColumn)); + EXPECT_EQ(column, frameColumn); + + JS::Rooted<JSObject*> parent(cx); + ASSERT_EQ(JS::SavedFrameResult::Ok, + JS::GetSavedFrameParent(cx, principals, savedFrame, &parent)); + EXPECT_EQ(nullptr, parent); + + ASSERT_EQ(NS_strlen(source), 21U); + char16_t sourceBuf[21] = {}; + + // Test when the length is shorter than the string length. + auto written = ubiFrame.source(RangedPtr<char16_t>(sourceBuf), 3); + EXPECT_EQ(written, 3U); + for (size_t i = 0; i < 3; i++) { + EXPECT_EQ(sourceBuf[i], source[i]); + } + + written = ubiFrame.source(RangedPtr<char16_t>(sourceBuf), 21); + EXPECT_EQ(written, 21U); + for (size_t i = 0; i < 21; i++) { + EXPECT_EQ(sourceBuf[i], source[i]); + } + + ASSERT_EQ(NS_strlen(functionDisplayName), 14U); + char16_t nameBuf[14] = {}; + + written = ubiFrame.functionDisplayName(RangedPtr<char16_t>(nameBuf), 14); + EXPECT_EQ(written, 14U); + for (size_t i = 0; i < 14; i++) { + EXPECT_EQ(nameBuf[i], functionDisplayName[i]); + } +}); diff --git a/devtools/shared/heapsnapshot/tests/gtest/DevTools.cpp b/devtools/shared/heapsnapshot/tests/gtest/DevTools.cpp new file mode 100644 index 0000000000..8e89d5ecd7 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/gtest/DevTools.cpp @@ -0,0 +1,7 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "DevTools.h" +const char16_t JS::ubi::Concrete<FakeNode>::concreteTypeName[] = u"FakeNode"; diff --git a/devtools/shared/heapsnapshot/tests/gtest/DevTools.h b/devtools/shared/heapsnapshot/tests/gtest/DevTools.h new file mode 100644 index 0000000000..35a16c9182 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/gtest/DevTools.h @@ -0,0 +1,214 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_devtools_gtest_DevTools__ +#define mozilla_devtools_gtest_DevTools__ + +#include <utility> + +#include "CoreDump.pb.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "js/Principals.h" +#include "js/UbiNode.h" +#include "js/UniquePtr.h" +#include "jsapi.h" +#include "jspubtd.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/devtools/HeapSnapshot.h" +#include "mozilla/dom/ChromeUtils.h" +#include "nsCRTGlue.h" + +using namespace mozilla; +using namespace mozilla::devtools; +using namespace mozilla::dom; +using namespace testing; + +// GTest fixture class that all of our tests derive from. +struct DevTools : public ::testing::Test { + bool _initialized; + JSContext* cx; + JS::Compartment* compartment; + JS::Zone* zone; + JS::PersistentRooted<JSObject*> global; + + DevTools() : _initialized(false), cx(nullptr) {} + + virtual void SetUp() { + MOZ_ASSERT(!_initialized); + + cx = getContext(); + if (!cx) return; + + global.init(cx, createGlobal()); + if (!global) return; + JS::EnterRealm(cx, global); + + compartment = js::GetContextCompartment(cx); + zone = js::GetContextZone(cx); + + _initialized = true; + } + + JSContext* getContext() { return CycleCollectedJSContext::Get()->Context(); } + + static void reportError(JSContext* cx, const char* message, + JSErrorReport* report) { + fprintf(stderr, "%s:%u:%s\n", + report->filename ? report->filename : "<no filename>", + (unsigned int)report->lineno, message); + } + + static const JSClass* getGlobalClass() { + static const JSClass globalClass = {"global", JSCLASS_GLOBAL_FLAGS, + &JS::DefaultGlobalClassOps}; + return &globalClass; + } + + JSObject* createGlobal() { + /* Create the global object. */ + JS::RealmOptions options; + return JS_NewGlobalObject(cx, getGlobalClass(), nullptr, + JS::FireOnNewGlobalHook, options); + } + + virtual void TearDown() { + _initialized = false; + + if (global) { + JS::LeaveRealm(cx, nullptr); + global = nullptr; + } + } +}; + +// Helper to define a test and ensure that the fixture is initialized properly. +#define DEF_TEST(name, body) \ + TEST_F(DevTools, name) { \ + ASSERT_TRUE(_initialized); \ + body \ + } + +// Fake JS::ubi::Node implementation +class MOZ_STACK_CLASS FakeNode { + public: + JS::ubi::EdgeVector edges; + JS::Compartment* compartment; + JS::Zone* zone; + size_t size; + + explicit FakeNode() : 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; } + + JS::Compartment* compartment() const override { return get().compartment; } + + protected: + explicit Concrete(FakeNode* ptr) : Base(ptr) {} + FakeNode& get() const { return *static_cast<FakeNode*>(ptr); } + + public: + static const char16_t concreteTypeName[]; + static void construct(void* storage, FakeNode* ptr) { + new (storage) Concrete(ptr); + } +}; + +} // namespace ubi +} // namespace JS + +inline void AddEdge(FakeNode& node, FakeNode& referent, + const char16_t* edgeName = nullptr) { + char16_t* ownedEdgeName = nullptr; + if (edgeName) { + ownedEdgeName = NS_xstrdup(edgeName); + } + + JS::ubi::Edge edge(ownedEdgeName, &referent); + ASSERT_TRUE(node.edges.append(std::move(edge))); +} + +// Custom GMock Matchers + +// Use the testing namespace to avoid static analysis failures in the gmock +// matcher classes that get generated from MATCHER_P macros. +namespace testing { + +// Ensure that given node has the expected number of edges. +MATCHER_P2(EdgesLength, cx, expectedLength, "") { + auto edges = arg.edges(cx); + if (!edges) return false; + + int actualLength = 0; + for (; !edges->empty(); edges->popFront()) actualLength++; + + return Matcher<int>(Eq(expectedLength)) + .MatchAndExplain(actualLength, result_listener); +} + +// Get the nth edge and match it with the given matcher. +MATCHER_P3(Edge, cx, n, matcher, "") { + auto edges = arg.edges(cx); + if (!edges) return false; + + int i = 0; + for (; !edges->empty(); edges->popFront()) { + if (i == n) { + return Matcher<const JS::ubi::Edge&>(matcher).MatchAndExplain( + edges->front(), result_listener); + } + + i++; + } + + return false; +} + +// Ensures that two char16_t* strings are equal. +MATCHER_P(UTF16StrEq, str, "") { return NS_strcmp(arg, str) == 0; } + +MATCHER_P(UniqueUTF16StrEq, str, "") { return NS_strcmp(arg.get(), str) == 0; } + +MATCHER(UniqueIsNull, "") { return arg.get() == nullptr; } + +// Matches an edge whose referent is the node with the given id. +MATCHER_P(EdgeTo, id, "") { + return Matcher<const DeserializedEdge&>( + Field(&DeserializedEdge::referent, id)) + .MatchAndExplain(arg, result_listener); +} + +} // namespace testing + +// A mock `Writer` class to be used with testing `WriteHeapGraph`. +class MockWriter : public CoreDumpWriter { + public: + virtual ~MockWriter() override {} + MOCK_METHOD2(writeNode, + bool(const JS::ubi::Node&, CoreDumpWriter::EdgePolicy)); + MOCK_METHOD1(writeMetadata, bool(uint64_t)); +}; + +inline void ExpectWriteNode(MockWriter& writer, FakeNode& node) { + EXPECT_CALL(writer, writeNode(Eq(JS::ubi::Node(&node)), _)) + .Times(1) + .WillOnce(Return(true)); +} + +#endif // mozilla_devtools_gtest_DevTools__ diff --git a/devtools/shared/heapsnapshot/tests/gtest/DoesCrossCompartmentBoundaries.cpp b/devtools/shared/heapsnapshot/tests/gtest/DoesCrossCompartmentBoundaries.cpp new file mode 100644 index 0000000000..25c387d308 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/gtest/DoesCrossCompartmentBoundaries.cpp @@ -0,0 +1,67 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Test that heap snapshots cross compartment boundaries when expected. + +#include "DevTools.h" + +DEF_TEST(DoesCrossCompartmentBoundaries, { + // Create a new global to get a new compartment. + JS::RealmOptions options; + JS::Rooted<JSObject*> newGlobal( + cx, JS_NewGlobalObject(cx, getGlobalClass(), nullptr, + JS::FireOnNewGlobalHook, options)); + ASSERT_TRUE(newGlobal); + JS::Compartment* newCompartment = nullptr; + { + JSAutoRealm ar(cx, newGlobal); + ASSERT_TRUE(JS::InitRealmStandardClasses(cx)); + newCompartment = js::GetContextCompartment(cx); + } + ASSERT_TRUE(newCompartment); + ASSERT_NE(newCompartment, compartment); + + // Our set of target compartments is both the old and new compartments. + JS::CompartmentSet targetCompartments; + ASSERT_TRUE(targetCompartments.put(compartment)); + ASSERT_TRUE(targetCompartments.put(newCompartment)); + + FakeNode nodeA; + FakeNode nodeB; + FakeNode nodeC; + FakeNode nodeD; + + nodeA.compartment = compartment; + nodeB.compartment = nullptr; + nodeC.compartment = newCompartment; + nodeD.compartment = nullptr; + + AddEdge(nodeA, nodeB); + AddEdge(nodeA, nodeC); + AddEdge(nodeB, nodeD); + + ::testing::NiceMock<MockWriter> writer; + + // Should serialize nodeA, because it is in one of our target compartments. + ExpectWriteNode(writer, nodeA); + + // Should serialize nodeB, because it doesn't belong to a compartment and is + // therefore assumed to be shared. + ExpectWriteNode(writer, nodeB); + + // Should also serialize nodeC, which is in our target compartments, but a + // different compartment than A. + ExpectWriteNode(writer, nodeC); + + // Should serialize nodeD because it's reachable via B and both nodes B and D + // don't belong to a specific compartment. + ExpectWriteNode(writer, nodeD); + + JS::AutoCheckCannotGC noGC(cx); + + ASSERT_TRUE(WriteHeapGraph(cx, JS::ubi::Node(&nodeA), writer, + /* wantNames = */ false, &targetCompartments, + noGC)); +}); diff --git a/devtools/shared/heapsnapshot/tests/gtest/DoesntCrossCompartmentBoundaries.cpp b/devtools/shared/heapsnapshot/tests/gtest/DoesntCrossCompartmentBoundaries.cpp new file mode 100644 index 0000000000..f6c6b11619 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/gtest/DoesntCrossCompartmentBoundaries.cpp @@ -0,0 +1,58 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Test that heap snapshots walk the compartment boundaries correctly. + +#include "DevTools.h" + +DEF_TEST(DoesntCrossCompartmentBoundaries, { + // Create a new global to get a new compartment. + JS::RealmOptions options; + JS::Rooted<JSObject*> newGlobal( + cx, JS_NewGlobalObject(cx, getGlobalClass(), nullptr, + JS::FireOnNewGlobalHook, options)); + ASSERT_TRUE(newGlobal); + JS::Compartment* newCompartment = nullptr; + { + JSAutoRealm ar(cx, newGlobal); + ASSERT_TRUE(JS::InitRealmStandardClasses(cx)); + newCompartment = js::GetContextCompartment(cx); + } + ASSERT_TRUE(newCompartment); + ASSERT_NE(newCompartment, compartment); + + // Our set of target compartments is only the pre-existing compartment and + // does not include the new compartment. + JS::CompartmentSet targetCompartments; + ASSERT_TRUE(targetCompartments.put(compartment)); + + FakeNode nodeA; + FakeNode nodeB; + FakeNode nodeC; + + nodeA.compartment = compartment; + nodeB.compartment = nullptr; + nodeC.compartment = newCompartment; + + AddEdge(nodeA, nodeB); + AddEdge(nodeB, nodeC); + + ::testing::NiceMock<MockWriter> writer; + + // Should serialize nodeA, because it is in our target compartments. + ExpectWriteNode(writer, nodeA); + + // Should serialize nodeB, because it doesn't belong to a compartment and is + // therefore assumed to be shared. + ExpectWriteNode(writer, nodeB); + + // But we shouldn't ever serialize nodeC. + + JS::AutoCheckCannotGC noGC(cx); + + ASSERT_TRUE(WriteHeapGraph(cx, JS::ubi::Node(&nodeA), writer, + /* wantNames = */ false, &targetCompartments, + noGC)); +}); diff --git a/devtools/shared/heapsnapshot/tests/gtest/SerializesEdgeNames.cpp b/devtools/shared/heapsnapshot/tests/gtest/SerializesEdgeNames.cpp new file mode 100644 index 0000000000..ab47941e39 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/gtest/SerializesEdgeNames.cpp @@ -0,0 +1,49 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Test that edge names get serialized correctly. + +#include "DevTools.h" + +using testing::Field; +using testing::IsNull; +using testing::Property; +using testing::Return; + +DEF_TEST(SerializesEdgeNames, { + FakeNode node; + FakeNode referent; + + const char16_t edgeName[] = u"edge name"; + const char16_t emptyStr[] = u""; + + AddEdge(node, referent, edgeName); + AddEdge(node, referent, emptyStr); + AddEdge(node, referent, nullptr); + + ::testing::NiceMock<MockWriter> writer; + + // Should get the node with edges once. + EXPECT_CALL( + writer, + writeNode( + AllOf(EdgesLength(cx, 3), + Edge(cx, 0, + Field(&JS::ubi::Edge::name, UniqueUTF16StrEq(edgeName))), + Edge(cx, 1, + Field(&JS::ubi::Edge::name, UniqueUTF16StrEq(emptyStr))), + Edge(cx, 2, Field(&JS::ubi::Edge::name, UniqueIsNull()))), + _)) + .Times(1) + .WillOnce(Return(true)); + + // Should get the referent node that doesn't have any edges once. + ExpectWriteNode(writer, referent); + + JS::AutoCheckCannotGC noGC(cx); + ASSERT_TRUE(WriteHeapGraph(cx, JS::ubi::Node(&node), writer, + /* wantNames = */ true, + /* zones = */ nullptr, noGC)); +}); diff --git a/devtools/shared/heapsnapshot/tests/gtest/SerializesEverythingInHeapGraphOnce.cpp b/devtools/shared/heapsnapshot/tests/gtest/SerializesEverythingInHeapGraphOnce.cpp new file mode 100644 index 0000000000..d71c86703c --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/gtest/SerializesEverythingInHeapGraphOnce.cpp @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Test that everything in the heap graph gets serialized once, and only once. + +#include "DevTools.h" + +DEF_TEST(SerializesEverythingInHeapGraphOnce, { + FakeNode nodeA; + FakeNode nodeB; + FakeNode nodeC; + FakeNode nodeD; + + AddEdge(nodeA, nodeB); + AddEdge(nodeB, nodeC); + AddEdge(nodeC, nodeD); + AddEdge(nodeD, nodeA); + + ::testing::NiceMock<MockWriter> writer; + + // Should serialize each node once. + ExpectWriteNode(writer, nodeA); + ExpectWriteNode(writer, nodeB); + ExpectWriteNode(writer, nodeC); + ExpectWriteNode(writer, nodeD); + + JS::AutoCheckCannotGC noGC(cx); + + ASSERT_TRUE(WriteHeapGraph(cx, JS::ubi::Node(&nodeA), writer, + /* wantNames = */ false, + /* zones = */ nullptr, noGC)); +}); diff --git a/devtools/shared/heapsnapshot/tests/gtest/SerializesTypeNames.cpp b/devtools/shared/heapsnapshot/tests/gtest/SerializesTypeNames.cpp new file mode 100644 index 0000000000..4c29b28832 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/gtest/SerializesTypeNames.cpp @@ -0,0 +1,27 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Test that a ubi::Node's typeName gets properly serialized into a core dump. + +#include "DevTools.h" + +using testing::Property; +using testing::Return; + +DEF_TEST(SerializesTypeNames, { + FakeNode node; + + ::testing::NiceMock<MockWriter> writer; + EXPECT_CALL( + writer, + writeNode(Property(&JS::ubi::Node::typeName, UTF16StrEq(u"FakeNode")), _)) + .Times(1) + .WillOnce(Return(true)); + + JS::AutoCheckCannotGC noGC(cx); + ASSERT_TRUE(WriteHeapGraph(cx, JS::ubi::Node(&node), writer, + /* wantNames = */ true, + /* zones = */ nullptr, noGC)); +}); diff --git a/devtools/shared/heapsnapshot/tests/gtest/moz.build b/devtools/shared/heapsnapshot/tests/gtest/moz.build new file mode 100644 index 0000000000..880d7b334e --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/gtest/moz.build @@ -0,0 +1,32 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, you can obtain one at http://mozilla.org/MPL/2.0/. + +Library("devtoolstests") + +LOCAL_INCLUDES += [ + "../..", +] + +DEFINES["GOOGLE_PROTOBUF_NO_RTTI"] = True +DEFINES["GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER"] = True + +UNIFIED_SOURCES = [ + "DeserializedNodeUbiNodes.cpp", + "DeserializedStackFrameUbiStackFrames.cpp", + "DevTools.cpp", + "DoesCrossCompartmentBoundaries.cpp", + "DoesntCrossCompartmentBoundaries.cpp", + "SerializesEdgeNames.cpp", + "SerializesEverythingInHeapGraphOnce.cpp", + "SerializesTypeNames.cpp", +] + +# THE MOCK_METHOD2 macro from gtest triggers this clang warning and it's hard +# to work around, so we just ignore it. +if CONFIG["CC_TYPE"] == "clang": + CXXFLAGS += ["-Wno-inconsistent-missing-override"] + +FINAL_LIBRARY = "xul-gtest" diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/.eslintrc.js b/devtools/shared/heapsnapshot/tests/xpcshell/.eslintrc.js new file mode 100644 index 0000000000..8611c174f5 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the common devtools xpcshell eslintrc config. + extends: "../../../../.eslintrc.xpcshell.js", +}; diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/Census.sys.mjs b/devtools/shared/heapsnapshot/tests/xpcshell/Census.sys.mjs new file mode 100644 index 0000000000..0e86f8b055 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/Census.sys.mjs @@ -0,0 +1,176 @@ +// Functions for checking results returned by +// Debugger.Memory.prototype.takeCensus and +// HeapSnapshot.prototype.takeCensus. Adapted from js/src/jit-test/lib/census.js. + +export const Census = {}; +function dumpn(msg) { + dump("DBG-TEST: Census.jsm: " + msg + "\n"); +} + +// Census.walkCensus(subject, name, walker) +// +// Use |walker| to check |subject|, a census object of the sort returned by +// Debugger.Memory.prototype.takeCensus: a tree of objects with integers at the +// leaves. Use |name| as the name for |subject| in diagnostic messages. Return +// the number of leaves of |subject| we visited. +// +// A walker is an object with three methods: +// +// - enter(prop): Return the walker we should use to check the property of the +// subject census named |prop|. This is for recursing into the subobjects of +// the subject. +// +// - done(): Called after we have called 'enter' on every property of the +// subject. +// +// - check(value): Check |value|, a leaf in the subject. +// +// Walker methods are expected to simply throw if a node we visit doesn't look +// right. +Census.walkCensus = (subject, name, walker) => walk(subject, name, walker, 0); +function walk(subject, name, walker, count) { + if (typeof subject === "object") { + dumpn(name); + for (const prop in subject) { + count = walk( + subject[prop], + name + "[" + uneval(prop) + "]", + walker.enter(prop), + count + ); + } + walker.done(); + } else { + dumpn(name + " = " + uneval(subject)); + walker.check(subject); + count++; + } + + return count; +} + +// A walker that doesn't check anything. +Census.walkAnything = { + enter: () => Census.walkAnything, + done: () => undefined, + check: () => undefined, +}; + +// A walker that requires all leaves to be zeros. +Census.assertAllZeros = { + enter: () => Census.assertAllZeros, + done: () => undefined, + check: elt => { + if (elt !== 0) { + throw new Error("Census mismatch: expected zero, found " + elt); + } + }, +}; + +function expectedObject() { + throw new Error( + "Census mismatch: subject has leaf where basis has nested object" + ); +} + +function expectedLeaf() { + throw new Error( + "Census mismatch: subject has nested object where basis has leaf" + ); +} + +// Return a function that, given a 'basis' census, returns a census walker that +// compares the subject census against the basis. The returned walker calls the +// given |compare|, |missing|, and |extra| functions as follows: +// +// - compare(subjectLeaf, basisLeaf): Check a leaf of the subject against the +// corresponding leaf of the basis. +// +// - missing(prop, value): Called when the subject is missing a property named +// |prop| which is present in the basis with value |value|. +// +// - extra(prop): Called when the subject has a property named |prop|, but the +// basis has no such property. This should return a walker that can check +// the subject's value. +function makeBasisChecker({ compare, missing, extra }) { + return function makeWalker(basis) { + if (typeof basis === "object") { + const unvisited = new Set(Object.getOwnPropertyNames(basis)); + return { + enter: prop => { + unvisited.delete(prop); + if (prop in basis) { + return makeWalker(basis[prop]); + } + + return extra(prop); + }, + + done: () => unvisited.forEach(prop => missing(prop, basis[prop])), + check: expectedObject, + }; + } + + return { + enter: expectedLeaf, + done: expectedLeaf, + check: elt => compare(elt, basis), + }; + }; +} + +function missingProp(prop) { + throw new Error( + "Census mismatch: subject lacks property present in basis: " + prop + ); +} + +function extraProp(prop) { + throw new Error( + "Census mismatch: subject has property not present in basis: " + prop + ); +} + +// Return a walker that checks that the subject census has counts all equal to +// |basis|. +Census.assertAllEqual = makeBasisChecker({ + compare: (a, b) => { + if (a !== b) { + throw new Error("Census mismatch: expected " + a + " got " + b); + } + }, + missing: missingProp, + extra: extraProp, +}); + +function ok(val) { + if (!val) { + throw new Error("Census mismatch: expected truthy, got " + val); + } +} + +// Return a walker that checks that the subject census has at least as many +// items of each category as |basis|. +Census.assertAllNotLessThan = makeBasisChecker({ + compare: (subject, basis) => ok(subject >= basis), + missing: missingProp, + extra: () => Census.walkAnything, +}); + +// Return a walker that checks that the subject census has at most as many +// items of each category as |basis|. +Census.assertAllNotMoreThan = makeBasisChecker({ + compare: (subject, basis) => ok(subject <= basis), + missing: missingProp, + extra: () => Census.walkAnything, +}); + +// Return a walker that checks that the subject census has within |fudge| +// items of each category of the count in |basis|. +Census.assertAllWithin = function (fudge, basis) { + return makeBasisChecker({ + compare: (subject, base) => ok(Math.abs(subject - base) <= fudge), + missing: missingProp, + extra: () => Census.walkAnything, + })(basis); +}; diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/Match.sys.mjs b/devtools/shared/heapsnapshot/tests/xpcshell/Match.sys.mjs new file mode 100644 index 0000000000..76312db86a --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/Match.sys.mjs @@ -0,0 +1,218 @@ +// A little pattern-matching library. +// +// Ported from js/src/tests/js1_8_5/reflect-parse/Match.js for use with devtools +// server xpcshell tests. + +export const Match = (function () { + function Pattern(template) { + // act like a constructor even as a function + if (!(this instanceof Pattern)) { + return new Pattern(template); + } + + this.template = template; + } + + Pattern.prototype = { + match(act) { + return match(act, this.template); + }, + + matches(act) { + try { + return this.match(act); + } catch (e) { + if (e instanceof MatchError) { + return false; + } + } + return false; + }, + + assert(act, message) { + try { + return this.match(act); + } catch (e) { + if (e instanceof MatchError) { + throw new Error((message || "failed match") + ": " + e.message); + } + } + return false; + }, + + toString: () => "[object Pattern]", + }; + + Pattern.ANY = new Pattern(); + Pattern.ANY.template = Pattern.ANY; + + Pattern.NUMBER = new Pattern(); + Pattern.NUMBER.match = function (act) { + if (typeof act !== "number") { + throw new MatchError("Expected number, got: " + quote(act)); + } + }; + + Pattern.NATURAL = new Pattern(); + Pattern.NATURAL.match = function (act) { + if (typeof act !== "number" || act !== Math.floor(act) || act < 0) { + throw new MatchError("Expected natural number, got: " + quote(act)); + } + }; + + const quote = uneval; + + function MatchError(msg) { + this.message = msg; + } + + MatchError.prototype = { + toString() { + return "match error: " + this.message; + }, + }; + + function isAtom(x) { + return ( + typeof x === "number" || + typeof x === "string" || + typeof x === "boolean" || + x === null || + (typeof x === "object" && x instanceof RegExp) + ); + } + + function isObject(x) { + return x !== null && typeof x === "object"; + } + + function isFunction(x) { + return typeof x === "function"; + } + + function isArrayLike(x) { + return isObject(x) && "length" in x; + } + + function matchAtom(act, exp) { + if (typeof exp === "number" && isNaN(exp)) { + if (typeof act !== "number" || !isNaN(act)) { + throw new MatchError("expected NaN, got: " + quote(act)); + } + return true; + } + + if (exp === null) { + if (act !== null) { + throw new MatchError("expected null, got: " + quote(act)); + } + return true; + } + + if (exp instanceof RegExp) { + if (!(act instanceof RegExp) || exp.source !== act.source) { + throw new MatchError("expected " + quote(exp) + ", got: " + quote(act)); + } + return true; + } + + switch (typeof exp) { + case "string": + if (act !== exp) { + throw new MatchError( + "expected " + quote(exp) + ", got " + quote(act) + ); + } + return true; + case "boolean": + case "number": + if (exp !== act) { + throw new MatchError("expected " + exp + ", got " + quote(act)); + } + return true; + } + + throw new Error("bad pattern: " + exp.toSource()); + } + + function matchObject(act, exp) { + if (!isObject(act)) { + throw new MatchError("expected object, got " + quote(act)); + } + + for (const key in exp) { + if (!(key in act)) { + throw new MatchError( + "expected property " + quote(key) + " not found in " + quote(act) + ); + } + match(act[key], exp[key]); + } + + return true; + } + + function matchFunction(act, exp) { + if (!isFunction(act)) { + throw new MatchError("expected function, got " + quote(act)); + } + + if (act !== exp) { + throw new MatchError( + "expected function: " + exp + "\nbut got different function: " + act + ); + } + } + + function matchArray(act, exp) { + if (!isObject(act) || !("length" in act)) { + throw new MatchError("expected array-like object, got " + quote(act)); + } + + const length = exp.length; + if (act.length !== exp.length) { + throw new MatchError( + "expected array-like object of length " + length + ", got " + quote(act) + ); + } + + for (let i = 0; i < length; i++) { + if (i in exp) { + if (!(i in act)) { + throw new MatchError( + "expected array property " + i + " not found in " + quote(act) + ); + } + match(act[i], exp[i]); + } + } + + return true; + } + + function match(act, exp) { + if (exp === Pattern.ANY) { + return true; + } + + if (exp instanceof Pattern) { + return exp.match(act); + } + + if (isAtom(exp)) { + return matchAtom(act, exp); + } + + if (isArrayLike(exp)) { + return matchArray(act, exp); + } + + if (isFunction(exp)) { + return matchFunction(act, exp); + } + + return matchObject(act, exp); + } + + return { Pattern, MatchError }; +})(); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/dominator-tree-worker.js b/devtools/shared/heapsnapshot/tests/xpcshell/dominator-tree-worker.js new file mode 100644 index 0000000000..e50bd80932 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/dominator-tree-worker.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env mozilla/chrome-worker */ + +"use strict"; + +console.log("Initializing worker."); + +self.onmessage = e => { + console.log("Starting test."); + try { + const path = ChromeUtils.saveHeapSnapshot({ runtime: true }); + const snapshot = ChromeUtils.readHeapSnapshot(path); + + const dominatorTree = snapshot.computeDominatorTree(); + ok(dominatorTree); + ok(DominatorTree.isInstance(dominatorTree)); + + let threw = false; + try { + new DominatorTree(); + } catch (excp) { + threw = true; + } + ok(threw, "Constructor shouldn't be usable"); + } catch (ex) { + ok( + false, + "Unexpected error inside worker:\n" + ex.toString() + "\n" + ex.stack + ); + } finally { + done(); + } +}; + +// Proxy assertions to the main thread. +function ok(val, msg) { + console.log("ok(" + !!val + ', "' + msg + '")'); + self.postMessage({ + type: "assertion", + passed: !!val, + msg, + stack: Error().stack, + }); +} + +// Tell the main thread we are done with the tests. +function done() { + console.log("done()"); + self.postMessage({ + type: "done", + }); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/head_heapsnapshot.js b/devtools/shared/heapsnapshot/tests/xpcshell/head_heapsnapshot.js new file mode 100644 index 0000000000..6085a37f61 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/head_heapsnapshot.js @@ -0,0 +1,550 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; +/* exported Cr, CC, Match, Census, Task, DevToolsUtils, HeapAnalysesClient, + assertThrows, getFilePath, saveHeapSnapshotAndTakeCensus, + saveHeapSnapshotAndComputeDominatorTree, compareCensusViewData, assertDiff, + assertLabelAndShallowSize, makeTestDominatorTreeNode, + assertDominatorTreeNodeInsertion, assertDeduplicatedPaths, + assertCountToBucketBreakdown, pathEntry */ + +var CC = Components.Constructor; + +const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); +const { Match } = ChromeUtils.importESModule("resource://test/Match.sys.mjs"); +const { Census } = ChromeUtils.importESModule("resource://test/Census.sys.mjs"); +const { addDebuggerToGlobal } = ChromeUtils.importESModule( + "resource://gre/modules/jsdebugger.sys.mjs" +); + +const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); +const HeapAnalysesClient = require("resource://devtools/shared/heapsnapshot/HeapAnalysesClient.js"); +const { + censusReportToCensusTreeNode, +} = require("resource://devtools/shared/heapsnapshot/census-tree-node.js"); +const CensusUtils = require("resource://devtools/shared/heapsnapshot/CensusUtils.js"); +const DominatorTreeNode = require("resource://devtools/shared/heapsnapshot/DominatorTreeNode.js"); +const { + deduplicatePaths, +} = require("resource://devtools/shared/heapsnapshot/shortest-paths.js"); +const { LabelAndShallowSizeVisitor } = DominatorTreeNode; + +// Always log packets when running tests. runxpcshelltests.py will throw +// the output away anyway, unless you give it the --verbose flag. +if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) { + Services.prefs.setBoolPref("devtools.debugger.log", true); + Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.debugger.log"); + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); + }); +} + +const SYSTEM_PRINCIPAL = Cc["@mozilla.org/systemprincipal;1"].createInstance( + Ci.nsIPrincipal +); + +function dumpn(msg) { + dump("HEAPSNAPSHOT-TEST: " + msg + "\n"); +} + +function addTestingFunctionsToGlobal(global) { + global.eval( + ` + const testingFunctions = Components.utils.getJSTestingFunctions(); + for (let k in testingFunctions) { + this[k] = testingFunctions[k]; + } + ` + ); + if (!global.print) { + global.print = info; + } + if (!global.newGlobal) { + global.newGlobal = newGlobal; + } + if (!global.Debugger) { + addDebuggerToGlobal(global); + } +} + +addTestingFunctionsToGlobal(this); + +/** + * Create a new global, with all the JS shell testing functions. Similar to the + * newGlobal function exposed to JS shells, and useful for porting JS shell + * tests to xpcshell tests. + */ +function newGlobal() { + const global = new Cu.Sandbox(SYSTEM_PRINCIPAL, { freshZone: true }); + addTestingFunctionsToGlobal(global); + return global; +} + +function assertThrows(f, val, msg) { + let fullmsg; + try { + f(); + } catch (exc) { + if (exc === val && (val !== 0 || 1 / exc === 1 / val)) { + return; + } else if (exc instanceof Error && exc.message === val) { + return; + } + fullmsg = "Assertion failed: expected exception " + val + ", got " + exc; + } + if (fullmsg === undefined) { + fullmsg = + "Assertion failed: expected exception " + val + ", no exception thrown"; + } + if (msg !== undefined) { + fullmsg += " - " + msg; + } + throw new Error(fullmsg); +} + +/** + * Returns the full path of the file with the specified name in a + * platform-independent and URL-like form. + */ +function getFilePath( + name, + allowMissing = false, + usePlatformPathSeparator = false +) { + const file = do_get_file(name, allowMissing); + let path = Services.io.newFileURI(file).spec; + let filePrePath = "file://"; + if ("nsILocalFileWin" in Ci && file instanceof Ci.nsILocalFileWin) { + filePrePath += "/"; + } + + path = path.slice(filePrePath.length); + + if (usePlatformPathSeparator && path.match(/^\w:/)) { + path = path.replace(/\//g, "\\"); + } + + return path; +} + +function saveNewHeapSnapshot(opts = { runtime: true }) { + const filePath = ChromeUtils.saveHeapSnapshot(opts); + ok(filePath, "Should get a file path to save the core dump to."); + ok(true, "Saved a heap snapshot to " + filePath); + return filePath; +} + +function readHeapSnapshot(filePath) { + const snapshot = ChromeUtils.readHeapSnapshot(filePath); + ok(snapshot, "Should have read a heap snapshot back from " + filePath); + ok( + HeapSnapshot.isInstance(snapshot), + "snapshot should be an instance of HeapSnapshot" + ); + return snapshot; +} + +/** + * Save a heap snapshot to the file with the given name in the current + * directory, read it back as a HeapSnapshot instance, and then take a census of + * the heap snapshot's serialized heap graph with the provided census options. + * + * @param {Object|undefined} censusOptions + * Options that should be passed through to the takeCensus method. See + * js/src/doc/Debugger/Debugger.Memory.md for details. + * + * @param {Debugger|null} dbg + * If a Debugger object is given, only serialize the subgraph covered by + * the Debugger's debuggees. If null, serialize the whole heap graph. + * + * @param {String} fileName + * The file name to save the heap snapshot's core dump file to, within + * the current directory. + * + * @returns Census + */ +function saveHeapSnapshotAndTakeCensus(dbg = null, censusOptions = undefined) { + const snapshotOptions = dbg ? { debugger: dbg } : { runtime: true }; + const filePath = saveNewHeapSnapshot(snapshotOptions); + const snapshot = readHeapSnapshot(filePath); + + equal( + typeof snapshot.takeCensus, + "function", + "snapshot should have a takeCensus method" + ); + + return snapshot.takeCensus(censusOptions); +} + +/** + * Save a heap snapshot to disk, read it back as a HeapSnapshot instance, and + * then compute its dominator tree. + * + * @param {Debugger|null} dbg + * If a Debugger object is given, only serialize the subgraph covered by + * the Debugger's debuggees. If null, serialize the whole heap graph. + * + * @returns {DominatorTree} + */ +function saveHeapSnapshotAndComputeDominatorTree(dbg = null) { + const snapshotOptions = dbg ? { debugger: dbg } : { runtime: true }; + const filePath = saveNewHeapSnapshot(snapshotOptions); + const snapshot = readHeapSnapshot(filePath); + + equal( + typeof snapshot.computeDominatorTree, + "function", + "snapshot should have a `computeDominatorTree` method" + ); + + const dominatorTree = snapshot.computeDominatorTree(); + + ok(dominatorTree, "Should be able to compute a dominator tree"); + ok( + DominatorTree.isInstance(dominatorTree), + "Should be an instance of DominatorTree" + ); + + return dominatorTree; +} + +function isSavedFrame(obj) { + return Object.prototype.toString.call(obj) === "[object SavedFrame]"; +} + +function savedFrameReplacer(key, val) { + if (isSavedFrame(val)) { + return `<SavedFrame '${val.toString().split(/\n/g).shift()}'>`; + } + return val; +} + +/** + * Assert that creating a CensusTreeNode from the given `report` with the + * specified `breakdown` creates the given `expected` CensusTreeNode. + * + * @param {Object} breakdown + * The census breakdown. + * + * @param {Object} report + * The census report. + * + * @param {Object} expected + * The expected CensusTreeNode result. + * + * @param {Object} options + * The options to pass through to `censusReportToCensusTreeNode`. + */ +function compareCensusViewData(breakdown, report, expected, options) { + dumpn("Generating CensusTreeNode from report:"); + dumpn("breakdown: " + JSON.stringify(breakdown, null, 4)); + dumpn("report: " + JSON.stringify(report, null, 4)); + dumpn("expected: " + JSON.stringify(expected, savedFrameReplacer, 4)); + + const actual = censusReportToCensusTreeNode(breakdown, report, options); + dumpn("actual: " + JSON.stringify(actual, savedFrameReplacer, 4)); + + assertStructurallyEquivalent(actual, expected); +} + +// Deep structural equivalence that can handle Map objects in addition to plain +// objects. +function assertStructurallyEquivalent(actual, expected, path = "root") { + if (actual === expected) { + equal(actual, expected, "actual and expected are the same"); + return; + } + + equal(typeof actual, typeof expected, `${path}: typeof should be the same`); + + if (actual && typeof actual === "object") { + const actualProtoString = Object.prototype.toString.call(actual); + const expectedProtoString = Object.prototype.toString.call(expected); + equal( + actualProtoString, + expectedProtoString, + `${path}: Object.prototype.toString.call() should be the same` + ); + + if (actualProtoString === "[object Map]") { + const expectedKeys = new Set([...expected.keys()]); + + for (const key of actual.keys()) { + ok( + expectedKeys.has(key), + `${path}: every key in actual is expected: ${String(key).slice( + 0, + 10 + )}` + ); + expectedKeys.delete(key); + + assertStructurallyEquivalent( + actual.get(key), + expected.get(key), + path + ".get(" + String(key).slice(0, 20) + ")" + ); + } + + equal( + expectedKeys.size, + 0, + `${path}: every key in expected should also exist in actual,\ + did not see ${[...expectedKeys]}` + ); + } else if (actualProtoString === "[object Set]") { + const expectedItems = new Set([...expected]); + + for (const item of actual) { + ok( + expectedItems.has(item), + `${path}: every set item in actual should exist in expected: ${item}` + ); + expectedItems.delete(item); + } + + equal( + expectedItems.size, + 0, + `${path}: every set item in expected should also exist in actual,\ + did not see ${[...expectedItems]}` + ); + } else { + const expectedKeys = new Set(Object.keys(expected)); + + for (const key of Object.keys(actual)) { + ok( + expectedKeys.has(key), + `${path}: every key in actual should exist in expected: ${key}` + ); + expectedKeys.delete(key); + + assertStructurallyEquivalent( + actual[key], + expected[key], + path + "." + key + ); + } + + equal( + expectedKeys.size, + 0, + `${path}: every key in expected should also exist in actual,\ + did not see ${[...expectedKeys]}` + ); + } + } else { + equal(actual, expected, `${path}: primitives should be equal`); + } +} + +/** + * Assert that creating a diff of the `first` and `second` census reports + * creates the `expected` delta-report. + * + * @param {Object} breakdown + * The census breakdown. + * + * @param {Object} first + * The first census report. + * + * @param {Object} second + * The second census report. + * + * @param {Object} expected + * The expected delta-report. + */ +function assertDiff(breakdown, first, second, expected) { + dumpn("Diffing census reports:"); + dumpn("Breakdown: " + JSON.stringify(breakdown, null, 4)); + dumpn("First census report: " + JSON.stringify(first, null, 4)); + dumpn("Second census report: " + JSON.stringify(second, null, 4)); + dumpn("Expected delta-report: " + JSON.stringify(expected, null, 4)); + + const actual = CensusUtils.diff(breakdown, first, second); + dumpn("Actual delta-report: " + JSON.stringify(actual, null, 4)); + + assertStructurallyEquivalent(actual, expected); +} + +/** + * Assert that creating a label and getting a shallow size from the given node + * description with the specified breakdown is as expected. + * + * @param {Object} breakdown + * @param {Object} givenDescription + * @param {Number} expectedShallowSize + * @param {Object} expectedLabel + */ +function assertLabelAndShallowSize( + breakdown, + givenDescription, + expectedShallowSize, + expectedLabel +) { + dumpn("Computing label and shallow size from node description:"); + dumpn("Breakdown: " + JSON.stringify(breakdown, null, 4)); + dumpn("Given description: " + JSON.stringify(givenDescription, null, 4)); + + const visitor = new LabelAndShallowSizeVisitor(); + CensusUtils.walk(breakdown, givenDescription, visitor); + + dumpn("Expected shallow size: " + expectedShallowSize); + dumpn("Actual shallow size: " + visitor.shallowSize()); + equal( + visitor.shallowSize(), + expectedShallowSize, + "Shallow size should be correct" + ); + + dumpn("Expected label: " + JSON.stringify(expectedLabel, null, 4)); + dumpn("Actual label: " + JSON.stringify(visitor.label(), null, 4)); + assertStructurallyEquivalent(visitor.label(), expectedLabel); +} + +// Counter for mock DominatorTreeNode ids. +let TEST_NODE_ID_COUNTER = 0; + +/** + * Create a mock DominatorTreeNode for testing, with sane defaults. Override any + * property by providing it on `opts`. Optionally pass child nodes as well. + * + * @param {Object} opts + * @param {Array<DominatorTreeNode>?} children + * + * @returns {DominatorTreeNode} + */ +function makeTestDominatorTreeNode(opts, children) { + const nodeId = TEST_NODE_ID_COUNTER++; + + const node = Object.assign( + { + nodeId, + label: undefined, + shallowSize: 1, + retainedSize: (children || []).reduce( + (size, c) => size + c.retainedSize, + 1 + ), + parentId: undefined, + children, + moreChildrenAvailable: true, + }, + opts + ); + + if (children && children.length) { + children.map(c => (c.parentId = node.nodeId)); + } + + return node; +} + +/** + * Insert `newChildren` into the given dominator `tree` as specified by the + * `path` from the root to the node the `newChildren` should be inserted + * beneath. Assert that the resulting tree matches `expected`. + */ +function assertDominatorTreeNodeInsertion( + tree, + path, + newChildren, + moreChildrenAvailable, + expected +) { + dumpn("Inserting new children into a dominator tree:"); + dumpn("Dominator tree: " + JSON.stringify(tree, null, 2)); + dumpn("Path: " + JSON.stringify(path, null, 2)); + dumpn("New children: " + JSON.stringify(newChildren, null, 2)); + dumpn("Expected resulting tree: " + JSON.stringify(expected, null, 2)); + + const actual = DominatorTreeNode.insert( + tree, + path, + newChildren, + moreChildrenAvailable + ); + dumpn("Actual resulting tree: " + JSON.stringify(actual, null, 2)); + + assertStructurallyEquivalent(actual, expected); +} + +function assertDeduplicatedPaths({ + target, + paths, + expectedNodes, + expectedEdges, +}) { + dumpn("Deduplicating paths:"); + dumpn("target = " + target); + dumpn("paths = " + JSON.stringify(paths, null, 2)); + dumpn("expectedNodes = " + expectedNodes); + dumpn("expectedEdges = " + JSON.stringify(expectedEdges, null, 2)); + + const { nodes, edges } = deduplicatePaths(target, paths); + + dumpn("Actual nodes = " + nodes); + dumpn("Actual edges = " + JSON.stringify(edges, null, 2)); + + equal( + nodes.length, + expectedNodes.length, + "actual number of nodes is equal to the expected number of nodes" + ); + + equal( + edges.length, + expectedEdges.length, + "actual number of edges is equal to the expected number of edges" + ); + + const expectedNodeSet = new Set(expectedNodes); + const nodeSet = new Set(nodes); + ok(nodeSet.size === nodes.length, "each returned node should be unique"); + + for (const node of nodes) { + ok(expectedNodeSet.has(node), `the ${node} node was expected`); + } + + for (const expectedEdge of expectedEdges) { + let count = 0; + for (const edge of edges) { + if ( + edge.from === expectedEdge.from && + edge.to === expectedEdge.to && + edge.name === expectedEdge.name + ) { + count++; + } + } + equal( + count, + 1, + "should have exactly one matching edge for the expected edge = " + + JSON.stringify(expectedEdge) + ); + } +} + +function assertCountToBucketBreakdown(breakdown, expected) { + dumpn("count => bucket breakdown"); + dumpn("Initial breakdown = ", JSON.stringify(breakdown, null, 2)); + dumpn("Expected results = ", JSON.stringify(expected, null, 2)); + + const actual = CensusUtils.countToBucketBreakdown(breakdown); + dumpn("Actual results = ", JSON.stringify(actual, null, 2)); + + assertStructurallyEquivalent(actual, expected); +} + +/** + * Create a mock path entry for the given predecessor and edge. + */ +function pathEntry(predecessor, edge) { + return { predecessor, edge }; +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/heap-snapshot-worker.js b/devtools/shared/heapsnapshot/tests/xpcshell/heap-snapshot-worker.js new file mode 100644 index 0000000000..c80c83c633 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/heap-snapshot-worker.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env mozilla/chrome-worker */ + +"use strict"; + +console.log("Initializing worker."); + +self.onmessage = ex => { + console.log("Starting test."); + try { + ok(ChromeUtils, "Should have access to ChromeUtils in a worker."); + ok(HeapSnapshot, "Should have access to HeapSnapshot in a worker."); + + const filePath = ChromeUtils.saveHeapSnapshot({ globals: [this] }); + ok(true, "Should be able to save a snapshot."); + + const snapshot = ChromeUtils.readHeapSnapshot(filePath); + ok(snapshot, "Should be able to read a heap snapshot"); + ok( + HeapSnapshot.isInstance(snapshot), + "Should be an instanceof HeapSnapshot" + ); + } catch (e) { + ok( + false, + "Unexpected error inside worker:\n" + e.toString() + "\n" + e.stack + ); + } finally { + done(); + } +}; + +// Proxy assertions to the main thread. +function ok(val, msg) { + console.log("ok(" + !!val + ', "' + msg + '")'); + self.postMessage({ + type: "assertion", + passed: !!val, + msg, + stack: Error().stack, + }); +} + +// Tell the main thread we are done with the tests. +function done() { + console.log("done()"); + self.postMessage({ + type: "done", + }); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_01.js new file mode 100644 index 0000000000..7d4560e6dc --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_01.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that we can generate label structures from node description reports. + +const breakdown = { + by: "coarseType", + objects: { + by: "objectClass", + then: { by: "count", count: true, bytes: true }, + other: { by: "count", count: true, bytes: true }, + }, + strings: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + scripts: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + other: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + domNode: { + by: "descriptiveType", + then: { by: "count", count: true, bytes: true }, + }, +}; + +const description = { + objects: { + Function: { count: 1, bytes: 32 }, + other: { count: 0, bytes: 0 }, + }, + strings: {}, + scripts: {}, + other: {}, + domNode: {}, +}; + +const expected = ["objects", "Function"]; + +const shallowSize = 32; + +function run_test() { + assertLabelAndShallowSize(breakdown, description, shallowSize, expected); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_02.js new file mode 100644 index 0000000000..cd424afdd0 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_02.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that we can generate label structures from node description reports. + +const breakdown = { + by: "coarseType", + objects: { + by: "objectClass", + then: { by: "count", count: true, bytes: true }, + other: { by: "count", count: true, bytes: true }, + }, + strings: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + scripts: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + other: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + domNode: { + by: "descriptiveType", + then: { by: "count", count: true, bytes: true }, + }, +}; + +const description = { + objects: { + other: { count: 1, bytes: 10 }, + }, + strings: {}, + scripts: {}, + other: {}, + domNode: {}, +}; + +const expected = ["objects", "other"]; + +const shallowSize = 10; + +function run_test() { + assertLabelAndShallowSize(breakdown, description, shallowSize, expected); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_03.js new file mode 100644 index 0000000000..098e3efc4f --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_03.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that we can generate label structures from node description reports. + +const breakdown = { + by: "coarseType", + objects: { + by: "objectClass", + then: { by: "count", count: true, bytes: true }, + other: { by: "count", count: true, bytes: true }, + }, + strings: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + scripts: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + other: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + domNode: { + by: "descriptiveType", + then: { by: "count", count: true, bytes: true }, + }, +}; + +const description = { + objects: { + other: { count: 0, bytes: 0 }, + }, + strings: { + JSString: { count: 1, bytes: 42 }, + }, + scripts: {}, + other: {}, + domNode: {}, +}; + +const expected = ["strings", "JSString"]; + +const shallowSize = 42; + +function run_test() { + assertLabelAndShallowSize(breakdown, description, shallowSize, expected); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_04.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_04.js new file mode 100644 index 0000000000..a087c39a2a --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_LabelAndShallowSize_04.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that we can generate label structures from node description reports. + +const breakdown = { + by: "coarseType", + objects: { + by: "objectClass", + then: { + by: "allocationStack", + then: { by: "count", count: true, bytes: true }, + noStack: { by: "count", count: true, bytes: true }, + }, + other: { by: "count", count: true, bytes: true }, + }, + strings: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + scripts: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + other: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + domNode: { + by: "descriptiveType", + then: { by: "count", count: true, bytes: true }, + }, +}; + +const stack = saveStack(); + +const description = { + objects: { + Array: new Map([[stack, { count: 1, bytes: 512 }]]), + other: { count: 0, bytes: 0 }, + }, + strings: {}, + scripts: {}, + other: {}, + domNode: {}, +}; + +const expected = ["objects", "Array", stack]; + +const shallowSize = 512; + +function run_test() { + assertLabelAndShallowSize(breakdown, description, shallowSize, expected); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_attachShortestPaths_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_attachShortestPaths_01.js new file mode 100644 index 0000000000..07894c67b1 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_attachShortestPaths_01.js @@ -0,0 +1,141 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the DominatorTreeNode.attachShortestPaths function can correctly +// attach the deduplicated shortest retaining paths for each node it is given. + +const startNodeId = 9999; +const maxNumPaths = 2; + +// Mock data mapping node id to shortest paths to that node id. +const shortestPaths = new Map([ + [ + 1000, + [ + [pathEntry(1100, "a"), pathEntry(1200, "b")], + [pathEntry(1100, "c"), pathEntry(1300, "d")], + ], + ], + [2000, [[pathEntry(2100, "e"), pathEntry(2200, "f"), pathEntry(2300, "g")]]], + [ + 3000, + [ + [pathEntry(3100, "h")], + [pathEntry(3100, "i")], + [pathEntry(3100, "j")], + [pathEntry(3200, "k")], + [pathEntry(3300, "l")], + [pathEntry(3400, "m")], + ], + ], +]); + +const actual = [ + makeTestDominatorTreeNode({ nodeId: 1000 }), + makeTestDominatorTreeNode({ nodeId: 2000 }), + makeTestDominatorTreeNode({ nodeId: 3000 }), +]; + +const expected = [ + makeTestDominatorTreeNode({ + nodeId: 1000, + shortestPaths: { + nodes: [ + { id: 1000, label: ["SomeType-1000"] }, + { id: 1100, label: ["SomeType-1100"] }, + { id: 1200, label: ["SomeType-1200"] }, + { id: 1300, label: ["SomeType-1300"] }, + ], + edges: [ + { from: 1100, to: 1200, name: "a" }, + { from: 1100, to: 1300, name: "c" }, + { from: 1200, to: 1000, name: "b" }, + { from: 1300, to: 1000, name: "d" }, + ], + }, + }), + + makeTestDominatorTreeNode({ + nodeId: 2000, + shortestPaths: { + nodes: [ + { id: 2000, label: ["SomeType-2000"] }, + { id: 2100, label: ["SomeType-2100"] }, + { id: 2200, label: ["SomeType-2200"] }, + { id: 2300, label: ["SomeType-2300"] }, + ], + edges: [ + { from: 2100, to: 2200, name: "e" }, + { from: 2200, to: 2300, name: "f" }, + { from: 2300, to: 2000, name: "g" }, + ], + }, + }), + + makeTestDominatorTreeNode({ + nodeId: 3000, + shortestPaths: { + nodes: [ + { id: 3000, label: ["SomeType-3000"] }, + { id: 3100, label: ["SomeType-3100"] }, + { id: 3200, label: ["SomeType-3200"] }, + { id: 3300, label: ["SomeType-3300"] }, + { id: 3400, label: ["SomeType-3400"] }, + ], + edges: [ + { from: 3100, to: 3000, name: "h" }, + { from: 3100, to: 3000, name: "i" }, + { from: 3100, to: 3000, name: "j" }, + { from: 3200, to: 3000, name: "k" }, + { from: 3300, to: 3000, name: "l" }, + { from: 3400, to: 3000, name: "m" }, + ], + }, + }), +]; + +const breakdown = { + by: "internalType", + then: { by: "count", count: true, bytes: true }, +}; + +const mockSnapshot = { + computeShortestPaths: (start, nodeIds, max) => { + equal(start, startNodeId); + equal(max, maxNumPaths); + + return new Map( + nodeIds.map(nodeId => { + const paths = shortestPaths.get(nodeId); + ok(paths, "Expected computeShortestPaths call for node id = " + nodeId); + return [nodeId, paths]; + }) + ); + }, + + describeNode: (bd, nodeId) => { + equal(bd, breakdown); + return { + ["SomeType-" + nodeId]: { + count: 1, + bytes: 10, + }, + }; + }, +}; + +function run_test() { + DominatorTreeNode.attachShortestPaths( + mockSnapshot, + breakdown, + startNodeId, + actual, + maxNumPaths + ); + + dumpn("Expected = " + JSON.stringify(expected, null, 2)); + dumpn("Actual = " + JSON.stringify(actual, null, 2)); + + assertStructurallyEquivalent(expected, actual); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_getNodeByIdAlongPath_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_getNodeByIdAlongPath_01.js new file mode 100644 index 0000000000..be1f210c3e --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_getNodeByIdAlongPath_01.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that we can find the node with the given id along the specified path. + +const node3000 = makeTestDominatorTreeNode({ nodeId: 3000 }); + +const node2000 = makeTestDominatorTreeNode({ nodeId: 2000 }, [ + makeTestDominatorTreeNode({}), + node3000, + makeTestDominatorTreeNode({}), +]); + +const node1000 = makeTestDominatorTreeNode({ nodeId: 1000 }, [ + makeTestDominatorTreeNode({}), + node2000, + makeTestDominatorTreeNode({}), +]); + +const tree = node1000; + +const path = [1000, 2000, 3000]; + +const tests = [ + { id: 1000, expected: node1000 }, + { id: 2000, expected: node2000 }, + { id: 3000, expected: node3000 }, +]; + +function run_test() { + for (const { id, expected } of tests) { + const actual = DominatorTreeNode.getNodeByIdAlongPath(id, tree, path); + equal(actual, expected, `We should have got the node with id = ${id}`); + } + + equal( + null, + DominatorTreeNode.getNodeByIdAlongPath(999999999999, tree, path), + "null is returned for nodes that are not even in the tree" + ); + + const lastNodeId = tree.children[tree.children.length - 1].nodeId; + equal( + null, + DominatorTreeNode.getNodeByIdAlongPath(lastNodeId, tree, path), + "null is returned for nodes that are not along the path" + ); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_01.js new file mode 100644 index 0000000000..7567c473e0 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_01.js @@ -0,0 +1,119 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that we can insert new children into an existing DominatorTreeNode tree. + +const tree = makeTestDominatorTreeNode({ nodeId: 1000 }, [ + makeTestDominatorTreeNode({}), + makeTestDominatorTreeNode({ nodeId: 2000 }, [ + makeTestDominatorTreeNode({}), + makeTestDominatorTreeNode({ nodeId: 3000 }), + makeTestDominatorTreeNode({}), + ]), + makeTestDominatorTreeNode({}), +]); + +const path = [1000, 2000, 3000]; + +const newChildren = [ + makeTestDominatorTreeNode({ parentId: 3000 }), + makeTestDominatorTreeNode({ parentId: 3000 }), +]; + +const moreChildrenAvailable = false; + +const expected = { + nodeId: 1000, + parentId: undefined, + label: undefined, + shallowSize: 1, + retainedSize: 7, + children: [ + { + nodeId: 0, + label: undefined, + shallowSize: 1, + retainedSize: 1, + parentId: 1000, + moreChildrenAvailable: true, + children: undefined, + }, + { + nodeId: 2000, + label: undefined, + shallowSize: 1, + retainedSize: 4, + parentId: 1000, + children: [ + { + nodeId: 1, + label: undefined, + shallowSize: 1, + retainedSize: 1, + parentId: 2000, + moreChildrenAvailable: true, + children: undefined, + }, + { + nodeId: 3000, + label: undefined, + shallowSize: 1, + retainedSize: 1, + parentId: 2000, + children: [ + { + nodeId: 7, + parentId: 3000, + label: undefined, + shallowSize: 1, + retainedSize: 1, + moreChildrenAvailable: true, + children: undefined, + }, + { + nodeId: 8, + parentId: 3000, + label: undefined, + shallowSize: 1, + retainedSize: 1, + moreChildrenAvailable: true, + children: undefined, + }, + ], + moreChildrenAvailable: false, + }, + { + nodeId: 3, + label: undefined, + shallowSize: 1, + retainedSize: 1, + parentId: 2000, + moreChildrenAvailable: true, + children: undefined, + }, + ], + moreChildrenAvailable: true, + }, + { + nodeId: 5, + label: undefined, + shallowSize: 1, + retainedSize: 1, + parentId: 1000, + moreChildrenAvailable: true, + children: undefined, + }, + ], + moreChildrenAvailable: true, +}; + +function run_test() { + assertDominatorTreeNodeInsertion( + tree, + path, + newChildren, + moreChildrenAvailable, + expected + ); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_02.js new file mode 100644 index 0000000000..b0b80c3c95 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_02.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test attempting to insert new children into an existing DominatorTreeNode +// tree with a bad path. + +const tree = makeTestDominatorTreeNode({}, [ + makeTestDominatorTreeNode({}), + makeTestDominatorTreeNode({}, [ + makeTestDominatorTreeNode({}), + makeTestDominatorTreeNode({}), + makeTestDominatorTreeNode({}), + ]), + makeTestDominatorTreeNode({}), +]); + +const path = [111111, 222222, 333333]; + +const newChildren = [ + makeTestDominatorTreeNode({ parentId: 333333 }), + makeTestDominatorTreeNode({ parentId: 333333 }), +]; + +const moreChildrenAvailable = false; + +const expected = tree; + +function run_test() { + assertDominatorTreeNodeInsertion( + tree, + path, + newChildren, + moreChildrenAvailable, + expected + ); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_03.js new file mode 100644 index 0000000000..552ed72735 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_insert_03.js @@ -0,0 +1,124 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test inserting new children into an existing DominatorTreeNode at the root. + +const tree = makeTestDominatorTreeNode({ nodeId: 666 }, [ + makeTestDominatorTreeNode({}), + makeTestDominatorTreeNode({}, [ + makeTestDominatorTreeNode({}), + makeTestDominatorTreeNode({}), + makeTestDominatorTreeNode({}), + ]), + makeTestDominatorTreeNode({}), +]); + +const path = [666]; + +const newChildren = [ + makeTestDominatorTreeNode({ + nodeId: 777, + parentId: 666, + }), + makeTestDominatorTreeNode({ + nodeId: 888, + parentId: 666, + }), +]; + +const moreChildrenAvailable = false; + +const expected = { + nodeId: 666, + label: undefined, + parentId: undefined, + shallowSize: 1, + retainedSize: 7, + children: [ + { + nodeId: 0, + label: undefined, + shallowSize: 1, + retainedSize: 1, + parentId: 666, + moreChildrenAvailable: true, + children: undefined, + }, + { + nodeId: 4, + label: undefined, + shallowSize: 1, + retainedSize: 4, + parentId: 666, + children: [ + { + nodeId: 1, + label: undefined, + shallowSize: 1, + retainedSize: 1, + parentId: 4, + moreChildrenAvailable: true, + children: undefined, + }, + { + nodeId: 2, + label: undefined, + shallowSize: 1, + retainedSize: 1, + parentId: 4, + moreChildrenAvailable: true, + children: undefined, + }, + { + nodeId: 3, + label: undefined, + shallowSize: 1, + retainedSize: 1, + parentId: 4, + moreChildrenAvailable: true, + children: undefined, + }, + ], + moreChildrenAvailable: true, + }, + { + nodeId: 5, + label: undefined, + shallowSize: 1, + retainedSize: 1, + parentId: 666, + moreChildrenAvailable: true, + children: undefined, + }, + { + nodeId: 777, + label: undefined, + shallowSize: 1, + retainedSize: 1, + parentId: 666, + moreChildrenAvailable: true, + children: undefined, + }, + { + nodeId: 888, + label: undefined, + shallowSize: 1, + retainedSize: 1, + parentId: 666, + moreChildrenAvailable: true, + children: undefined, + }, + ], + moreChildrenAvailable: false, +}; + +function run_test() { + assertDominatorTreeNodeInsertion( + tree, + path, + newChildren, + moreChildrenAvailable, + expected + ); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_partialTraversal_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_partialTraversal_01.js new file mode 100644 index 0000000000..6da0587f57 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTreeNode_partialTraversal_01.js @@ -0,0 +1,150 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that we correctly set `moreChildrenAvailable` when doing a partial +// traversal of a dominator tree to create the initial incrementally loaded +// `DominatorTreeNode` tree. + +// `tree` maps parent to children: +// +// 100 +// |- 200 +// | |- 500 +// | |- 600 +// | `- 700 +// |- 300 +// | |- 800 +// | |- 900 +// `- 400 +// |- 1000 +// |- 1100 +// `- 1200 +const tree = new Map([ + [100, [200, 300, 400]], + [200, [500, 600, 700]], + [300, [800, 900]], + [400, [1000, 1100, 1200]], +]); + +const mockDominatorTree = { + root: 100, + getRetainedSize: _ => 10, + getImmediatelyDominated: id => (tree.get(id) || []).slice(), +}; + +const mockSnapshot = { + describeNode: _ => ({ + objects: { count: 0, bytes: 0 }, + strings: { count: 0, bytes: 0 }, + scripts: { count: 0, bytes: 0 }, + other: { SomeType: { count: 1, bytes: 10 } }, + domNode: { count: 0, bytes: 0 }, + }), +}; + +const breakdown = { + by: "coarseType", + objects: { by: "count", count: true, bytes: true }, + strings: { by: "count", count: true, bytes: true }, + scripts: { by: "count", count: true, bytes: true }, + other: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + domNode: { by: "count", count: true, bytes: true }, +}; + +const expected = { + nodeId: 100, + label: ["other", "SomeType"], + shallowSize: 10, + retainedSize: 10, + shortestPaths: undefined, + children: [ + { + nodeId: 200, + label: ["other", "SomeType"], + shallowSize: 10, + retainedSize: 10, + parentId: 100, + shortestPaths: undefined, + children: [ + { + nodeId: 500, + label: ["other", "SomeType"], + shallowSize: 10, + retainedSize: 10, + parentId: 200, + moreChildrenAvailable: false, + shortestPaths: undefined, + children: undefined, + }, + { + nodeId: 600, + label: ["other", "SomeType"], + shallowSize: 10, + retainedSize: 10, + parentId: 200, + moreChildrenAvailable: false, + shortestPaths: undefined, + children: undefined, + }, + ], + moreChildrenAvailable: true, + }, + { + nodeId: 300, + label: ["other", "SomeType"], + shallowSize: 10, + retainedSize: 10, + parentId: 100, + shortestPaths: undefined, + children: [ + { + nodeId: 800, + label: ["other", "SomeType"], + shallowSize: 10, + retainedSize: 10, + parentId: 300, + moreChildrenAvailable: false, + shortestPaths: undefined, + children: undefined, + }, + { + nodeId: 900, + label: ["other", "SomeType"], + shallowSize: 10, + retainedSize: 10, + parentId: 300, + moreChildrenAvailable: false, + shortestPaths: undefined, + children: undefined, + }, + ], + moreChildrenAvailable: false, + }, + ], + moreChildrenAvailable: true, + parentId: undefined, +}; + +function run_test() { + // Traverse the whole depth of the test tree, but one short of the number of + // siblings. This will exercise the moreChildrenAvailable handling for + // siblings. + const actual = DominatorTreeNode.partialTraversal( + mockDominatorTree, + mockSnapshot, + breakdown, + // maxDepth + 4, + // siblings + 2 + ); + + dumpn("Expected = " + JSON.stringify(expected, null, 2)); + dumpn("Actual = " + JSON.stringify(actual, null, 2)); + + assertStructurallyEquivalent(expected, actual); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_01.js new file mode 100644 index 0000000000..f98094fd32 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_01.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Sanity test that we can compute dominator trees. + +function run_test() { + const path = ChromeUtils.saveHeapSnapshot({ runtime: true }); + const snapshot = ChromeUtils.readHeapSnapshot(path); + + const dominatorTree = snapshot.computeDominatorTree(); + ok(dominatorTree); + ok(DominatorTree.isInstance(dominatorTree)); + + let threw = false; + try { + new DominatorTree(); + } catch (e) { + threw = true; + } + ok(threw, "Constructor shouldn't be usable"); + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_02.js new file mode 100644 index 0000000000..ca8d0533ab --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_02.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that we can compute dominator trees from a snapshot in a worker. + +add_task(async function () { + const worker = new ChromeWorker("resource://test/dominator-tree-worker.js"); + worker.postMessage({}); + + let assertionCount = 0; + worker.onmessage = e => { + if (e.data.type !== "assertion") { + return; + } + + ok(e.data.passed, e.data.msg + "\n" + e.data.stack); + assertionCount++; + }; + + await waitForDone(worker); + + 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); + resolve(); + } + }); + }); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_03.js new file mode 100644 index 0000000000..013a93697a --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_03.js @@ -0,0 +1,21 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that we can get the root of dominator trees. + +function run_test() { + const dominatorTree = saveHeapSnapshotAndComputeDominatorTree(); + equal(typeof dominatorTree.root, "number", "root should be a number"); + equal( + Math.floor(dominatorTree.root), + dominatorTree.root, + "root should be an integer" + ); + 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/xpcshell/test_DominatorTree_04.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_04.js new file mode 100644 index 0000000000..a669c7b52b --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_04.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that we can get the retained sizes of dominator trees. + +function run_test() { + const dominatorTree = saveHeapSnapshotAndComputeDominatorTree(); + equal( + typeof dominatorTree.getRetainedSize, + "function", + "getRetainedSize should be a function" + ); + + const size = dominatorTree.getRetainedSize(dominatorTree.root); + ok(size, "should get a size for the root"); + equal(typeof size, "number", "retained sizes should be a number"); + equal(Math.floor(size), size, "size should be an integer"); + 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/xpcshell/test_DominatorTree_05.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_05.js new file mode 100644 index 0000000000..23abf1bd74 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_05.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that we can get the set of immediately dominated nodes for any given +// node and that this forms a tree. + +function run_test() { + const dominatorTree = saveHeapSnapshotAndComputeDominatorTree(); + equal( + typeof dominatorTree.getImmediatelyDominated, + "function", + "getImmediatelyDominated should be a function" + ); + + // Do a traversal of the dominator tree. + // + // Note that we don't assert directly, only if we get an unexpected + // value. There are just way too many nodes in the heap graph to assert for + // every one. This test would constantly time out and assertion messages would + // overflow the log size. + + const root = dominatorTree.root; + equal( + dominatorTree.getImmediateDominator(root), + null, + "The root should not have a parent" + ); + + const seen = new Set(); + const stack = [root]; + while (stack.length) { + const top = stack.pop(); + + if (seen.has(top)) { + ok( + false, + "This is a tree, not a graph: we shouldn't have " + + "multiple edges to the same node" + ); + } + seen.add(top); + if (seen.size % 1000 === 0) { + dumpn("Progress update: seen size = " + seen.size); + } + + const newNodes = dominatorTree.getImmediatelyDominated(top); + if (Object.prototype.toString.call(newNodes) !== "[object Array]") { + ok( + false, + "getImmediatelyDominated should return an array for known node ids" + ); + } + + const topSize = dominatorTree.getRetainedSize(top); + + let lastSize = Infinity; + for (let i = 0; i < newNodes.length; i++) { + if (typeof newNodes[i] !== "number") { + ok(false, "Every dominated id should be a number"); + } + + if (dominatorTree.getImmediateDominator(newNodes[i]) !== top) { + ok(false, "child's parent should be the expected parent"); + } + + const thisSize = dominatorTree.getRetainedSize(newNodes[i]); + + if (thisSize >= topSize) { + ok( + false, + "the size of children in the dominator tree should" + + " always be less than that of their parent" + ); + } + + if (thisSize > lastSize) { + ok( + false, + "children should be sorted by greatest to least retained size, " + + "lastSize = " + + lastSize + + ", thisSize = " + + thisSize + ); + } + + lastSize = thisSize; + stack.push(newNodes[i]); + } + } + + ok(true, "Successfully walked the tree"); + dumpn("Walked " + seen.size + " nodes"); + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_06.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_06.js new file mode 100644 index 0000000000..fdd4191c5f --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_DominatorTree_06.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the retained size of a node is the sum of its children retained +// sizes plus its shallow size. + +// Note that we don't assert directly, only if we get an unexpected +// value. There are just way too many nodes in the heap graph to assert for +// every one. This test would constantly time out and assertion messages would +// overflow the log size. +function fastAssert(cond, msg) { + if (!cond) { + ok(false, msg); + } +} + +const COUNT = { by: "count", count: false, bytes: true }; + +function run_test() { + const path = saveNewHeapSnapshot(); + const snapshot = ChromeUtils.readHeapSnapshot(path); + const dominatorTree = snapshot.computeDominatorTree(); + + // Do a traversal of the dominator tree and assert the relationship between + // retained size, shallow size, and children's retained sizes. + + const root = dominatorTree.root; + const stack = [root]; + while (stack.length) { + const top = stack.pop(); + + const children = dominatorTree.getImmediatelyDominated(top); + + const topRetainedSize = dominatorTree.getRetainedSize(top); + const topShallowSize = snapshot.describeNode(COUNT, top).bytes; + fastAssert( + topShallowSize <= topRetainedSize, + "The shallow size should be less than or equal to the " + "retained size" + ); + + let sumOfChildrensRetainedSizes = 0; + for (let i = 0; i < children.length; i++) { + sumOfChildrensRetainedSizes += dominatorTree.getRetainedSize(children[i]); + stack.push(children[i]); + } + + fastAssert( + sumOfChildrensRetainedSizes <= topRetainedSize, + "The sum of the children's retained sizes should be less than " + + "or equal to the retained size" + ); + fastAssert( + sumOfChildrensRetainedSizes + topShallowSize === topRetainedSize, + "The sum of the children's retained sizes plus the shallow " + + "size should be equal to the retained size" + ); + } + + ok(true, "Successfully walked the tree"); + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_computeDominatorTree_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_computeDominatorTree_01.js new file mode 100644 index 0000000000..ba622f55df --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_computeDominatorTree_01.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test the HeapAnalyses{Client,Worker} "computeDominatorTree" request. + +add_task(async function () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + await client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + const dominatorTreeId = await client.computeDominatorTree(snapshotFilePath); + equal( + typeof dominatorTreeId, + "number", + "should get a dominator tree id, and it should be a number" + ); + + client.destroy(); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_computeDominatorTree_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_computeDominatorTree_02.js new file mode 100644 index 0000000000..c3ee76be13 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_computeDominatorTree_02.js @@ -0,0 +1,20 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test the HeapAnalyses{Client,Worker} "computeDominatorTree" request with bad +// file paths. + +add_task(async function () { + const client = new HeapAnalysesClient(); + + let threw = false; + try { + await client.computeDominatorTree("/etc/passwd"); + } catch (_) { + threw = true; + } + ok(threw, "should throw when given a bad path"); + + client.destroy(); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_01.js new file mode 100644 index 0000000000..b85e4b19fc --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_01.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the HeapAnalyses{Client,Worker} can delete heap snapshots. + +const breakdown = { + by: "coarseType", + objects: { by: "count", count: true, bytes: true }, + scripts: { by: "count", count: true, bytes: true }, + strings: { by: "count", count: true, bytes: true }, + other: { by: "count", count: true, bytes: true }, +}; + +add_task(async function () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + await client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + const dominatorTreeId = await client.computeDominatorTree(snapshotFilePath); + ok(true, "Should have computed the dominator tree"); + + await client.deleteHeapSnapshot(snapshotFilePath); + ok(true, "Should have deleted the snapshot"); + + let threw = false; + try { + await client.getDominatorTree({ + dominatorTreeId, + breakdown, + }); + } catch (_) { + threw = true; + } + ok(threw, "getDominatorTree on deleted tree should throw an error"); + + threw = false; + try { + await client.computeDominatorTree(snapshotFilePath); + } catch (_) { + threw = true; + } + ok(threw, "computeDominatorTree on deleted snapshot should throw an error"); + + threw = false; + try { + await client.takeCensus(snapshotFilePath); + } catch (_) { + threw = true; + } + ok(threw, "takeCensus on deleted tree should throw an error"); + + client.destroy(); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_02.js new file mode 100644 index 0000000000..6960081afd --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_02.js @@ -0,0 +1,19 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test deleteHeapSnapshot is a noop if the provided path matches no snapshot + +add_task(async function () { + const client = new HeapAnalysesClient(); + + let threw = false; + try { + await client.deleteHeapSnapshot("path-does-not-exist"); + } catch (_) { + threw = true; + } + ok(threw, "deleteHeapSnapshot on non-existant path should throw an error"); + + client.destroy(); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_03.js new file mode 100644 index 0000000000..c84622f633 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_deleteHeapSnapshot_03.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test other dominatorTrees can still be retrieved after deleting a snapshot + +const breakdown = { + by: "coarseType", + objects: { by: "count", count: true, bytes: true }, + scripts: { by: "count", count: true, bytes: true }, + strings: { by: "count", count: true, bytes: true }, + other: { by: "count", count: true, bytes: true }, + domNode: { by: "count", count: true, bytes: true }, +}; + +async function createSnapshotAndDominatorTree(client) { + const snapshotFilePath = saveNewHeapSnapshot(); + await client.readHeapSnapshot(snapshotFilePath); + const dominatorTreeId = await client.computeDominatorTree(snapshotFilePath); + return { dominatorTreeId, snapshotFilePath }; +} + +add_task(async function () { + const client = new HeapAnalysesClient(); + + const savedSnapshots = [ + await createSnapshotAndDominatorTree(client), + await createSnapshotAndDominatorTree(client), + await createSnapshotAndDominatorTree(client), + ]; + ok(true, "Create 3 snapshots and dominator trees"); + + await client.deleteHeapSnapshot(savedSnapshots[1].snapshotFilePath); + ok(true, "Snapshot deleted"); + + let tree = await client.getDominatorTree({ + dominatorTreeId: savedSnapshots[0].dominatorTreeId, + breakdown, + }); + ok(tree, "Should get a valid tree for first snapshot"); + + let threw = false; + try { + await client.getDominatorTree({ + dominatorTreeId: savedSnapshots[1].dominatorTreeId, + breakdown, + }); + } catch (_) { + threw = true; + } + ok(threw, "getDominatorTree on a deleted snapshot should throw an error"); + + tree = await client.getDominatorTree({ + dominatorTreeId: savedSnapshots[2].dominatorTreeId, + breakdown, + }); + ok(tree, "Should get a valid tree for third snapshot"); + + client.destroy(); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getCensusIndividuals_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getCensusIndividuals_01.js new file mode 100644 index 0000000000..2cbd9cc325 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getCensusIndividuals_01.js @@ -0,0 +1,110 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the HeapAnalyses{Client,Worker} can get census individuals. + +const COUNT = { by: "count", count: true, bytes: true }; + +const CENSUS_BREAKDOWN = { + by: "coarseType", + objects: COUNT, + strings: COUNT, + scripts: COUNT, + other: COUNT, + domNode: COUNT, +}; + +const LABEL_BREAKDOWN = { + by: "internalType", + then: COUNT, +}; + +const MAX_INDIVIDUALS = 10; + +add_task(async function () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + await client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + const dominatorTreeId = await client.computeDominatorTree(snapshotFilePath); + ok(true, "Should have computed dominator tree"); + + const { report } = await client.takeCensus( + snapshotFilePath, + { breakdown: CENSUS_BREAKDOWN }, + { asTreeNode: true } + ); + ok(report, "Should get a report"); + + let nodesWithLeafIndicesFound = 0; + + await (async function assertCanGetIndividuals(censusNode) { + if (censusNode.reportLeafIndex !== undefined) { + nodesWithLeafIndicesFound++; + + const response = await client.getCensusIndividuals({ + dominatorTreeId, + indices: DevToolsUtils.isSet(censusNode.reportLeafIndex) + ? censusNode.reportLeafIndex + : new Set([censusNode.reportLeafIndex]), + censusBreakdown: CENSUS_BREAKDOWN, + labelBreakdown: LABEL_BREAKDOWN, + maxRetainingPaths: 1, + maxIndividuals: MAX_INDIVIDUALS, + }); + + dumpn(`response = ${JSON.stringify(response, null, 4)}`); + + equal( + response.nodes.length, + Math.min(MAX_INDIVIDUALS, censusNode.count), + "response.nodes.length === Math.min(MAX_INDIVIDUALS, censusNode.count)" + ); + + let lastRetainedSize = Infinity; + for (const individual of response.nodes) { + equal( + typeof individual.nodeId, + "number", + "individual.nodeId should be a number" + ); + 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 (const child of censusNode.children) { + await assertCanGetIndividuals(child); + } + } + })(report); + + equal( + nodesWithLeafIndicesFound, + 4, + "Should have found a leaf for each coarse type" + ); + + client.destroy(); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getCreationTime_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getCreationTime_01.js new file mode 100644 index 0000000000..505e1a1dc6 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getCreationTime_01.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the HeapAnalyses{Client,Worker} can get a HeapSnapshot's +// creation time. + +function waitForThirtyMilliseconds() { + const start = Date.now(); + while (Date.now() - start < 30) { + // do nothing + } +} + +const BREAKDOWN = { + by: "internalType", + then: { by: "count", count: true, bytes: true }, +}; + +add_task(async function () { + const client = new HeapAnalysesClient(); + const start = Date.now() * 1000; + + // Because Date.now() is less precise than the snapshot's time stamp, give it + // a little bit of head room. Additionally, WinXP's timers have a granularity + // of only +/-15 ms. + waitForThirtyMilliseconds(); + const snapshotFilePath = saveNewHeapSnapshot(); + waitForThirtyMilliseconds(); + const end = Date.now() * 1000; + + await client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + let threw = false; + try { + await client.getCreationTime("/not/a/real/path", { + breakdown: BREAKDOWN, + }); + } catch (_) { + threw = true; + } + ok(threw, "getCreationTime should throw when snapshot does not exist"); + + const time = await client.getCreationTime(snapshotFilePath, { + breakdown: BREAKDOWN, + }); + + dumpn("Start = " + start); + dumpn("End = " + end); + dumpn("Time = " + time); + + 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/xpcshell/test_HeapAnalyses_getDominatorTree_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getDominatorTree_01.js new file mode 100644 index 0000000000..9035624ca2 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getDominatorTree_01.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test the HeapAnalyses{Client,Worker} "getDominatorTree" request. + +const breakdown = { + by: "coarseType", + objects: { by: "count", count: true, bytes: true }, + scripts: { by: "count", count: true, bytes: true }, + strings: { by: "count", count: true, bytes: true }, + other: { by: "count", count: true, bytes: true }, + domNode: { by: "count", count: true, bytes: true }, +}; + +add_task(async function () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + await client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + const dominatorTreeId = await client.computeDominatorTree(snapshotFilePath); + equal( + typeof dominatorTreeId, + "number", + "should get a dominator tree id, and it should be a number" + ); + + const partialTree = await client.getDominatorTree({ + dominatorTreeId, + breakdown, + }); + ok(partialTree, "Should get a partial tree"); + equal(typeof partialTree, "object", "partialTree should be an object"); + + function checkTree(node) { + equal(typeof node.nodeId, "number", "each node should have an id"); + + if (node === partialTree) { + equal(node.parentId, undefined, "the root has no parent"); + } else { + equal( + typeof node.parentId, + "number", + "each node should have a parent id" + ); + } + + equal( + typeof node.retainedSize, + "number", + "each node should have a retained size" + ); + + ok( + node.children === undefined || Array.isArray(node.children), + "each node either has a list of children, " + + "or undefined meaning no children loaded" + ); + equal( + typeof node.moreChildrenAvailable, + "boolean", + "each node should indicate if there are more children available or not" + ); + + equal(typeof node.shortestPaths, "object", "Should have shortest paths"); + equal( + typeof node.shortestPaths.nodes, + "object", + "Should have shortest paths' nodes" + ); + equal( + typeof node.shortestPaths.edges, + "object", + "Should have shortest paths' edges" + ); + + if (node.children) { + node.children.forEach(checkTree); + } + } + + checkTree(partialTree); + + client.destroy(); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getDominatorTree_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getDominatorTree_02.js new file mode 100644 index 0000000000..83a7cbbd3d --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getDominatorTree_02.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test the HeapAnalyses{Client,Worker} "getDominatorTree" request with bad +// dominator tree ids. + +const breakdown = { + by: "coarseType", + objects: { by: "count", count: true, bytes: true }, + scripts: { by: "count", count: true, bytes: true }, + strings: { by: "count", count: true, bytes: true }, + other: { by: "count", count: true, bytes: true }, +}; + +add_task(async function () { + const client = new HeapAnalysesClient(); + + let threw = false; + try { + await client.getDominatorTree({ dominatorTreeId: 42, breakdown }); + } catch (_) { + threw = true; + } + ok(threw, "should throw when given a bad id"); + + client.destroy(); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getImmediatelyDominated_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getImmediatelyDominated_01.js new file mode 100644 index 0000000000..47860870cb --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_getImmediatelyDominated_01.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test the HeapAnalyses{Client,Worker} "getImmediatelyDominated" request. + +const breakdown = { + by: "coarseType", + objects: { by: "count", count: true, bytes: true }, + scripts: { by: "count", count: true, bytes: true }, + strings: { by: "count", count: true, bytes: true }, + other: { by: "count", count: true, bytes: true }, + domNode: { by: "count", count: true, bytes: true }, +}; + +add_task(async function () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + await client.readHeapSnapshot(snapshotFilePath); + const dominatorTreeId = await client.computeDominatorTree(snapshotFilePath); + + const partialTree = await client.getDominatorTree({ + dominatorTreeId, + breakdown, + }); + ok( + !!partialTree.children.length, + "root should immediately dominate some nodes" + ); + + // First, test getting a subset of children available. + const response = await client.getImmediatelyDominated({ + dominatorTreeId, + breakdown, + nodeId: partialTree.nodeId, + startIndex: 0, + maxCount: partialTree.children.length - 1, + }); + + ok(Array.isArray(response.nodes)); + ok(response.nodes.every(node => node.parentId === partialTree.nodeId)); + ok(response.moreChildrenAvailable); + equal(response.path.length, 1); + equal(response.path[0], partialTree.nodeId); + + for (const node of response.nodes) { + equal(typeof node.shortestPaths, "object", "Should have shortest paths"); + equal( + typeof node.shortestPaths.nodes, + "object", + "Should have shortest paths' nodes" + ); + equal( + typeof node.shortestPaths.edges, + "object", + "Should have shortest paths' edges" + ); + } + + // Next, test getting a subset of children available. + const secondResponse = await client.getImmediatelyDominated({ + dominatorTreeId, + breakdown, + nodeId: partialTree.nodeId, + startIndex: 0, + maxCount: Infinity, + }); + + ok(Array.isArray(secondResponse.nodes)); + ok(secondResponse.nodes.every(node => node.parentId === partialTree.nodeId)); + ok(!secondResponse.moreChildrenAvailable); + equal(secondResponse.path.length, 1); + equal(secondResponse.path[0], partialTree.nodeId); + + for (const node of secondResponse.nodes) { + equal(typeof node.shortestPaths, "object", "Should have shortest paths"); + equal( + typeof node.shortestPaths.nodes, + "object", + "Should have shortest paths' nodes" + ); + equal( + typeof node.shortestPaths.edges, + "object", + "Should have shortest paths' edges" + ); + } + + client.destroy(); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_readHeapSnapshot_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_readHeapSnapshot_01.js new file mode 100644 index 0000000000..dea8269a92 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_readHeapSnapshot_01.js @@ -0,0 +1,15 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the HeapAnalyses{Client,Worker} can read heap snapshots. + +add_task(async function () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + await client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + client.destroy(); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensusDiff_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensusDiff_01.js new file mode 100644 index 0000000000..9703229438 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensusDiff_01.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the HeapAnalyses{Client,Worker} can take diffs between censuses. + +const BREAKDOWN = { + by: "objectClass", + then: { by: "count", count: true, bytes: false }, + other: { by: "count", count: true, bytes: false }, +}; + +add_task(async function () { + const client = new HeapAnalysesClient(); + + const markers = [allocationMarker()]; + + const firstSnapshotFilePath = saveNewHeapSnapshot(); + + // Allocate and hold an additional AllocationMarker object so we can see it in + // the next heap snapshot. + markers.push(allocationMarker()); + + const secondSnapshotFilePath = saveNewHeapSnapshot(); + + await client.readHeapSnapshot(firstSnapshotFilePath); + await client.readHeapSnapshot(secondSnapshotFilePath); + ok(true, "Should have read both heap snapshot files"); + + const { delta } = await client.takeCensusDiff( + firstSnapshotFilePath, + secondSnapshotFilePath, + { breakdown: BREAKDOWN } + ); + + equal( + delta.AllocationMarker.count, + 1, + "There exists one new AllocationMarker in the second heap snapshot" + ); + + const { delta: deltaTreeNode } = await client.takeCensusDiff( + firstSnapshotFilePath, + secondSnapshotFilePath, + { breakdown: BREAKDOWN }, + { asTreeNode: true } + ); + + // Have to manually set these because symbol properties aren't structured + // cloned. + delta[CensusUtils.basisTotalBytes] = deltaTreeNode.totalBytes; + delta[CensusUtils.basisTotalCount] = deltaTreeNode.totalCount; + + compareCensusViewData( + BREAKDOWN, + delta, + deltaTreeNode, + "Returning delta-census as a tree node represents same data as the report" + ); + + client.destroy(); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensusDiff_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensusDiff_02.js new file mode 100644 index 0000000000..d5d988f78f --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensusDiff_02.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the HeapAnalyses{Client,Worker} can take diffs between censuses as +// inverted trees. + +const BREAKDOWN = { + by: "coarseType", + objects: { + by: "objectClass", + then: { by: "count", count: true, bytes: true }, + other: { by: "count", count: true, bytes: true }, + }, + scripts: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + strings: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + other: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + domNode: { + by: "descriptiveType", + then: { by: "count", count: true, bytes: true }, + }, +}; + +add_task(async function () { + const firstSnapshotFilePath = saveNewHeapSnapshot(); + const secondSnapshotFilePath = saveNewHeapSnapshot(); + + const client = new HeapAnalysesClient(); + await client.readHeapSnapshot(firstSnapshotFilePath); + await client.readHeapSnapshot(secondSnapshotFilePath); + + ok(true, "Should have read both heap snapshot files"); + + const { delta } = await client.takeCensusDiff( + firstSnapshotFilePath, + secondSnapshotFilePath, + { breakdown: BREAKDOWN } + ); + + const { delta: deltaTreeNode } = await client.takeCensusDiff( + firstSnapshotFilePath, + secondSnapshotFilePath, + { breakdown: BREAKDOWN }, + { asInvertedTreeNode: true } + ); + + // Have to manually set these because symbol properties aren't structured + // cloned. + delta[CensusUtils.basisTotalBytes] = deltaTreeNode.totalBytes; + delta[CensusUtils.basisTotalCount] = deltaTreeNode.totalCount; + + compareCensusViewData(BREAKDOWN, delta, deltaTreeNode, { invert: true }); + + client.destroy(); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_01.js new file mode 100644 index 0000000000..55da2bf4b8 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_01.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the HeapAnalyses{Client,Worker} can take censuses. + +add_task(async function () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + await client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + const { report } = await client.takeCensus(snapshotFilePath); + ok(report, "Should get a report"); + equal(typeof report, "object", "report should be an object"); + + ok(report.objects); + ok(report.scripts); + ok(report.strings); + ok(report.other); + + client.destroy(); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_02.js new file mode 100644 index 0000000000..5b1dcefe03 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_02.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the HeapAnalyses{Client,Worker} can take censuses with breakdown +// options. + +add_task(async function () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + await client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + const { report } = await client.takeCensus(snapshotFilePath, { + breakdown: { by: "count", count: true, bytes: true }, + }); + + ok(report, "Should get a report"); + equal(typeof report, "object", "report should be an object"); + + ok(report.count); + ok(report.bytes); + + client.destroy(); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_03.js new file mode 100644 index 0000000000..0dfda73f1f --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_03.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the HeapAnalyses{Client,Worker} bubbles errors properly when things +// go wrong. + +add_task(async function () { + const client = new HeapAnalysesClient(); + + // Snapshot file path to a file that doesn't exist. + let failed = false; + try { + await client.readHeapSnapshot( + getFilePath("foo-bar-baz" + Math.random(), true) + ); + } catch (e) { + failed = true; + } + ok(failed, "should not read heap snapshots that do not exist"); + + // Snapshot file path to a file that is not a heap snapshot. + failed = false; + try { + await client.readHeapSnapshot( + getFilePath("test_HeapAnalyses_takeCensus_03.js") + ); + } catch (e) { + failed = true; + } + ok( + failed, + "should not be able to read a file " + + "that is not a heap snapshot as a heap snapshot" + ); + + const snapshotFilePath = saveNewHeapSnapshot(); + await client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + // Bad census breakdown options. + failed = false; + try { + await client.takeCensus(snapshotFilePath, { + breakdown: { by: "some classification that we do not have" }, + }); + } catch (e) { + failed = true; + } + ok(failed, "should not be able to breakdown by an unknown classification"); + + client.destroy(); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_04.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_04.js new file mode 100644 index 0000000000..26f78ad4f7 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_04.js @@ -0,0 +1,133 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the HeapAnalyses{Client,Worker} can send SavedFrame stacks from +// by-allocation-stack reports from the worker. + +add_task(async function test() { + const client = new HeapAnalysesClient(); + + // Track some allocation stacks. + + const g = newGlobal(); + const dbg = new Debugger(g); + g.eval(` // 1 + this.log = []; // 2 + function f() { this.log.push(allocationMarker()); } // 3 + function g() { this.log.push(allocationMarker()); } // 4 + function h() { this.log.push(allocationMarker()); } // 5 + `); + + // Create one allocationMarker with tracking turned off, + // so it will have no associated stack. + g.f(); + + dbg.memory.allocationSamplingProbability = 1; + + for (const [func, n] of [ + [g.f, 20], + [g.g, 10], + [g.h, 5], + ]) { + for (let i = 0; i < n; i++) { + dbg.memory.trackingAllocationSites = true; + // All allocations of allocationMarker occur with this line as the oldest + // stack frame. + func(); + dbg.memory.trackingAllocationSites = false; + } + } + + // Take a heap snapshot. + + const snapshotFilePath = saveNewHeapSnapshot({ debugger: dbg }); + await client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + // Run a census broken down by class name -> allocation stack so we can grab + // only the AllocationMarker objects we have complete control over. + + const { report } = await client.takeCensus(snapshotFilePath, { + breakdown: { + by: "objectClass", + then: { + by: "allocationStack", + then: { + by: "count", + bytes: true, + count: true, + }, + noStack: { + by: "count", + bytes: true, + count: true, + }, + }, + }, + }); + + // Test the generated report. + + ok(report, "Should get a report"); + + const map = report.AllocationMarker; + ok(map, "Should get AllocationMarkers in the report."); + // From a module with a different global, and therefore a different Map + // constructor, so we can't use instanceof. + equal(Object.getPrototypeOf(map).constructor.name, "Map"); + + equal( + map.size, + 4, + "Should have 4 allocation stacks (including the lack of a stack)" + ); + + // Gather the stacks we are expecting to appear as keys, and + // check that there are no unexpected keys. + const stacks = {}; + + map.forEach((v, k) => { + if (k === "noStack") { + // No need to save this key. + } else if ( + k.functionDisplayName === "f" && + k.parent.functionDisplayName === "test" + ) { + stacks.f = k; + } else if ( + k.functionDisplayName === "g" && + k.parent.functionDisplayName === "test" + ) { + stacks.g = k; + } else if ( + k.functionDisplayName === "h" && + k.parent.functionDisplayName === "test" + ) { + stacks.h = k; + } else { + dumpn("Unexpected allocation stack:"); + k.toString() + .split(/\n/g) + .forEach(s => dumpn(s)); + ok(false); + } + }); + + ok(map.get("noStack")); + equal(map.get("noStack").count, 1); + + ok(stacks.f); + ok(map.get(stacks.f)); + equal(map.get(stacks.f).count, 20); + + ok(stacks.g); + ok(map.get(stacks.g)); + equal(map.get(stacks.g).count, 10); + + ok(stacks.h); + ok(map.get(stacks.h)); + equal(map.get(stacks.h).count, 5); + + client.destroy(); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_05.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_05.js new file mode 100644 index 0000000000..951a3b3133 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_05.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the HeapAnalyses{Client,Worker} can take censuses and return +// a CensusTreeNode. + +const BREAKDOWN = { + by: "internalType", + then: { by: "count", count: true, bytes: true }, +}; + +add_task(async function () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + await client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + const { report } = await client.takeCensus(snapshotFilePath, { + breakdown: BREAKDOWN, + }); + + const { report: treeNode } = await client.takeCensus( + snapshotFilePath, + { + breakdown: BREAKDOWN, + }, + { + asTreeNode: true, + } + ); + + ok(!!treeNode.children.length, "treeNode has children"); + ok( + treeNode.children.every(type => { + return "name" in type && "bytes" in type && "count" in type; + }), + "all of tree node's children have name, bytes, count" + ); + + compareCensusViewData( + BREAKDOWN, + report, + treeNode, + "Returning census as a tree node represents same data as the report" + ); + + client.destroy(); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_06.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_06.js new file mode 100644 index 0000000000..d9fdaa3708 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_06.js @@ -0,0 +1,114 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the HeapAnalyses{Client,Worker} can take censuses by +// "allocationStack" and return a CensusTreeNode. + +const BREAKDOWN = { + by: "objectClass", + then: { + by: "allocationStack", + then: { by: "count", count: true, bytes: true }, + noStack: { by: "count", count: true, bytes: true }, + }, + other: { by: "count", count: true, bytes: true }, +}; + +add_task(async function () { + const g = newGlobal(); + const dbg = new Debugger(g); + + // 5 allocation markers with no stack. + g.eval(` + this.markers = []; + for (var i = 0; i < 5; i++) { + markers.push(allocationMarker()); + } + `); + + dbg.memory.allocationSamplingProbability = 1; + dbg.memory.trackingAllocationSites = true; + + // 5 allocation markers at 5 stacks. + g.eval(` + (function shouldHaveCountOfOne() { + markers.push(allocationMarker()); + markers.push(allocationMarker()); + markers.push(allocationMarker()); + markers.push(allocationMarker()); + markers.push(allocationMarker()); + }()); + `); + + // 5 allocation markers at 1 stack. + g.eval(` + (function shouldHaveCountOfFive() { + for (var i = 0; i < 5; i++) { + markers.push(allocationMarker()); + } + }()); + `); + + const snapshotFilePath = saveNewHeapSnapshot({ debugger: dbg }); + + const client = new HeapAnalysesClient(); + await client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + const { report } = await client.takeCensus(snapshotFilePath, { + breakdown: BREAKDOWN, + }); + + const { report: treeNode } = await client.takeCensus( + snapshotFilePath, + { + breakdown: BREAKDOWN, + }, + { + asTreeNode: true, + } + ); + + const markers = treeNode.children.find(c => c.name === "AllocationMarker"); + ok(markers); + + const noStack = markers.children.find(c => c.name === "noStack"); + equal(noStack.count, 5); + + let numShouldHaveFiveFound = 0; + let numShouldHaveOneFound = 0; + + function walk(node) { + if (node.children) { + node.children.forEach(walk); + } + + if (!isSavedFrame(node.name)) { + return; + } + + if (node.name.functionDisplayName === "shouldHaveCountOfFive") { + equal(node.count, 5, "shouldHaveCountOfFive should have count of five"); + numShouldHaveFiveFound++; + } + + if (node.name.functionDisplayName === "shouldHaveCountOfOne") { + equal(node.count, 1, "shouldHaveCountOfOne should have count of one"); + numShouldHaveOneFound++; + } + } + markers.children.forEach(walk); + + equal(numShouldHaveFiveFound, 1); + equal(numShouldHaveOneFound, 5); + + compareCensusViewData( + BREAKDOWN, + report, + treeNode, + "Returning census as a tree node represents same data as the report" + ); + + client.destroy(); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_07.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_07.js new file mode 100644 index 0000000000..3f603122d3 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapAnalyses_takeCensus_07.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that the HeapAnalyses{Client,Worker} can take censuses and return +// an inverted CensusTreeNode. + +const BREAKDOWN = { + by: "coarseType", + objects: { + by: "objectClass", + then: { by: "count", count: true, bytes: true }, + other: { by: "count", count: true, bytes: true }, + }, + scripts: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + strings: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + other: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + domNode: { + by: "descriptiveType", + then: { by: "count", count: true, bytes: true }, + }, +}; + +add_task(async function () { + const client = new HeapAnalysesClient(); + + const snapshotFilePath = saveNewHeapSnapshot(); + await client.readHeapSnapshot(snapshotFilePath); + ok(true, "Should have read the heap snapshot"); + + const { report } = await client.takeCensus(snapshotFilePath, { + breakdown: BREAKDOWN, + }); + + const { report: treeNode } = await client.takeCensus( + snapshotFilePath, + { + breakdown: BREAKDOWN, + }, + { + asInvertedTreeNode: true, + } + ); + + compareCensusViewData(BREAKDOWN, report, treeNode, { invert: true }); + + client.destroy(); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_computeShortestPaths_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_computeShortestPaths_01.js new file mode 100644 index 0000000000..47d974050b --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_computeShortestPaths_01.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Sanity test that we can compute shortest paths. +// +// Because the actual heap graph is too unpredictable and likely to drastically +// change as various implementation bits change, we don't test exact paths +// here. See js/src/jsapi-tests/testUbiNode.cpp for such tests, where we can +// control the specific graph shape and structure and so testing exact paths is +// reliable. + +function run_test() { + const path = ChromeUtils.saveHeapSnapshot({ runtime: true }); + const snapshot = ChromeUtils.readHeapSnapshot(path); + + const dominatorTree = snapshot.computeDominatorTree(); + const dominatedByRoot = dominatorTree + .getImmediatelyDominated(dominatorTree.root) + .slice(0, 10); + ok(dominatedByRoot); + ok(dominatedByRoot.length); + + const targetSet = new Set(dominatedByRoot); + + const shortestPaths = snapshot.computeShortestPaths( + dominatorTree.root, + dominatedByRoot, + 2 + ); + ok(shortestPaths); + ok(shortestPaths instanceof Map); + ok(shortestPaths.size === targetSet.size); + + for (const [target, paths] of shortestPaths) { + ok(targetSet.has(target), "We should only get paths for our targets"); + targetSet.delete(target); + + ok( + !!paths.length, + "We must have at least one path, since the target is dominated by the root" + ); + ok( + paths.length <= 2, + "Should not have recorded more paths than the max requested" + ); + + dumpn("---------------------"); + dumpn("Shortest paths for 0x" + target.toString(16) + ":"); + for (const pth of paths) { + dumpn(" path ="); + for (const part of pth) { + dumpn( + " predecessor: 0x" + + part.predecessor.toString(16) + + "; edge: " + + part.edge + ); + } + } + dumpn("---------------------"); + + for (const path2 of paths) { + ok(!!path2.length, "Cannot have zero length paths"); + ok( + path2[0].predecessor === dominatorTree.root, + "The first predecessor is always our start node" + ); + + for (const part of path2) { + ok(part.predecessor, "Each part of a path has a predecessor"); + ok( + !!snapshot.describeNode( + { by: "count", count: true, bytes: true }, + part.predecessor + ), + "The predecessor is in the heap snapshot" + ); + ok("edge" in part, "Each part has an (potentially null) edge property"); + } + } + } + + ok(targetSet.size === 0, "We found paths for all of our targets"); + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_computeShortestPaths_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_computeShortestPaths_02.js new file mode 100644 index 0000000000..714986c601 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_computeShortestPaths_02.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test computing shortest paths with invalid arguments. + +function run_test() { + const path = ChromeUtils.saveHeapSnapshot({ runtime: true }); + const snapshot = ChromeUtils.readHeapSnapshot(path); + + const dominatorTree = snapshot.computeDominatorTree(); + const target = dominatorTree + .getImmediatelyDominated(dominatorTree.root) + .pop(); + ok(target); + + let threw = false; + try { + snapshot.computeShortestPaths(0, [target], 2); + } catch (_) { + threw = true; + } + ok(threw, "invalid start node should throw"); + + threw = false; + try { + snapshot.computeShortestPaths(dominatorTree.root, [0], 2); + } catch (_) { + threw = true; + } + ok(threw, "invalid target nodes should throw"); + + threw = false; + try { + snapshot.computeShortestPaths(dominatorTree.root, [], 2); + } catch (_) { + threw = true; + } + ok(threw, "empty target nodes should throw"); + + threw = false; + try { + snapshot.computeShortestPaths(dominatorTree.root, [target], 0); + } catch (_) { + threw = true; + } + ok(threw, "0 max paths should throw"); + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_creationTime_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_creationTime_01.js new file mode 100644 index 0000000000..490ef1d6d0 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_creationTime_01.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// HeapSnapshot.prototype.creationTime returns the expected time. + +function waitForThirtyMilliseconds() { + const start = Date.now(); + while (Date.now() - start < 30) { + // do nothing + } +} + +function run_test() { + const start = Date.now() * 1000; + info("start = " + start); + + // Because Date.now() is less precise than the snapshot's time stamp, give it + // a little bit of head room. Additionally, WinXP's timer only has granularity + // of +/- 15ms. + waitForThirtyMilliseconds(); + const path = ChromeUtils.saveHeapSnapshot({ runtime: true }); + waitForThirtyMilliseconds(); + + const end = Date.now() * 1000; + info("end = " + end); + + const snapshot = ChromeUtils.readHeapSnapshot(path); + info("snapshot.creationTime = " + snapshot.creationTime); + + ok(snapshot.creationTime >= start); + ok(snapshot.creationTime <= end); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_deepStack_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_deepStack_01.js new file mode 100644 index 0000000000..9c59d60fc8 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_deepStack_01.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that we can save a core dump with very deep allocation stacks and read +// it back into a HeapSnapshot. + +function stackDepth(stack) { + return stack ? 1 + stackDepth(stack.parent) : 0; +} + +function run_test() { + Services.prefs.setBoolPref( + "security.allow_parent_unrestricted_js_loads", + true + ); + Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); + Services.prefs.setBoolPref("security.allow_eval_in_parent_process", true); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_parent_unrestricted_js_loads"); + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); + Services.prefs.clearUserPref("security.allow_eval_in_parent_process"); + }); + + // Create a Debugger observing a debuggee's allocations. + const debuggee = new Cu.Sandbox(null); + const dbg = new Debugger(debuggee); + dbg.memory.trackingAllocationSites = true; + + // Allocate some objects in the debuggee that will have their allocation + // stacks recorded by the Debugger. + + debuggee.eval("this.objects = []"); + debuggee.eval( + function recursiveAllocate(n) { + if (n <= 0) { + return; + } + + // Make sure to recurse before pushing the object so that when TCO is + // implemented sometime in the future, it doesn't invalidate this test. + recursiveAllocate(n - 1); + this.objects.push({}); + }.toString() + ); + debuggee.eval("recursiveAllocate = recursiveAllocate.bind(this);"); + debuggee.eval("recursiveAllocate(200);"); + + // Now save a snapshot that will include the allocation stacks and read it + // back again. + + const filePath = ChromeUtils.saveHeapSnapshot({ runtime: true }); + ok(true, "Should be able to save a snapshot."); + + const snapshot = ChromeUtils.readHeapSnapshot(filePath); + ok(snapshot, "Should be able to read a heap snapshot"); + ok(HeapSnapshot.isInstance(snapshot), "Should be an instanceof HeapSnapshot"); + + const report = snapshot.takeCensus({ + breakdown: { + by: "allocationStack", + then: { by: "count", bytes: true, count: true }, + noStack: { by: "count", bytes: true, count: true }, + }, + }); + + // Keep this synchronized with `HeapSnapshot::MAX_STACK_DEPTH`! + const MAX_STACK_DEPTH = 60; + + let foundStacks = false; + report.forEach((v, k) => { + if (k === "noStack") { + return; + } + + foundStacks = true; + const depth = stackDepth(k); + dumpn("Stack depth is " + depth); + 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/xpcshell/test_HeapSnapshot_describeNode_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_describeNode_01.js new file mode 100644 index 0000000000..8597fb4edf --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_describeNode_01.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that we can describe nodes with a breakdown. + +function run_test() { + const path = saveNewHeapSnapshot(); + const snapshot = ChromeUtils.readHeapSnapshot(path); + ok(snapshot.describeNode); + equal(typeof snapshot.describeNode, "function"); + + const dt = snapshot.computeDominatorTree(); + + let threw = false; + try { + snapshot.describeNode(undefined, dt.root); + } catch (_) { + threw = true; + } + ok(threw, "Should require a breakdown"); + + const breakdown = { + by: "coarseType", + objects: { by: "objectClass" }, + scripts: { by: "internalType" }, + strings: { by: "internalType" }, + other: { by: "internalType" }, + }; + + threw = false; + try { + snapshot.describeNode(breakdown, 0); + } catch (_) { + threw = true; + } + ok(threw, "Should throw when given an invalid node id"); + + const description = snapshot.describeNode(breakdown, dt.root); + ok(description); + ok(description.other); + ok(description.other["JS::ubi::RootList"]); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_getObjectNodeId_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_getObjectNodeId_01.js new file mode 100644 index 0000000000..058f8deba8 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_getObjectNodeId_01.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test ChromeUtils.getObjectNodeId() + +function run_test() { + // Create a test object, which we want to analyse + const testObject = { + foo: { + bar: {}, + }, + }; + + const path = ChromeUtils.saveHeapSnapshot({ runtime: true }); + const snapshot = ChromeUtils.readHeapSnapshot(path); + + // Get the NodeId for our test object + const objectNodeIdRoot = ChromeUtils.getObjectNodeId(testObject); + const objectNodeIdFoo = ChromeUtils.getObjectNodeId(testObject.foo); + const objectNodeIdBar = ChromeUtils.getObjectNodeId(testObject.foo.bar); + + // Also try to ensure that this is the right object via its retained path + const shortestPaths = snapshot.computeShortestPaths( + objectNodeIdRoot, + [objectNodeIdBar], + 50 + ); + ok(shortestPaths); + ok(shortestPaths instanceof Map); + ok( + shortestPaths.size == 1, + "We get only one path between the root object and bar object" + ); + + const paths = shortestPaths.get(objectNodeIdBar); + ok(paths.length == 1, "There is only one path between root and bar"); + ok( + paths[0].length == 2, + "The shortest path is made of two edges: foo and bar" + ); + + const [path1, path2] = paths[0]; + ok( + path1.predecessor == objectNodeIdRoot, + "The first edge goes from the root object" + ); + ok(path1.edge == "foo", "The first edge is the foo attribute"); + + ok( + path2.predecessor == objectNodeIdFoo, + "The second edge goes from the foo object" + ); + ok(path2.edge == "bar", "The first edge is the bar attribute"); + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_01.js new file mode 100644 index 0000000000..f4a7836be3 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_01.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// HeapSnapshot.prototype.takeCensus returns a value of an appropriate +// shape. Ported from js/src/jit-tests/debug/Memory-takeCensus-01.js + +function run_test() { + const dbg = new Debugger(); + + function checkProperties(census) { + equal(typeof census, "object"); + for (const prop of Object.getOwnPropertyNames(census)) { + const desc = Object.getOwnPropertyDescriptor(census, prop); + equal(desc.enumerable, true); + equal(desc.configurable, true); + equal(desc.writable, true); + if (typeof desc.value === "object") { + checkProperties(desc.value); + } else { + equal(typeof desc.value, "number"); + } + } + } + + checkProperties(saveHeapSnapshotAndTakeCensus(dbg)); + + const g = newGlobal(); + dbg.addDebuggee(g); + checkProperties(saveHeapSnapshotAndTakeCensus(dbg)); + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_02.js new file mode 100644 index 0000000000..a26e9a96d8 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_02.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// HeapSnapshot.prototype.takeCensus behaves plausibly as we allocate objects. +// +// Exact object counts vary in ways we can't predict. For example, +// BaselineScripts can hold onto "template objects", which exist only to hold +// the shape and type for newly created objects. When BaselineScripts are +// discarded, these template objects go with them. +// +// So instead of expecting precise counts, we expect counts that are at least as +// many as we would expect given the object graph we've built. +// +// Ported from js/src/jit-tests/debug/Memory-takeCensus-02.js + +function run_test() { + // A Debugger with no debuggees had better not find anything. + const dbg = new Debugger(); + const census0 = saveHeapSnapshotAndTakeCensus(dbg); + Census.walkCensus(census0, "census0", Census.assertAllZeros); + + function newGlobalWithDefs() { + const g = newGlobal(); + g.eval(` + function times(n, fn) { + var a=[]; + for (var i = 0; i<n; i++) + a.push(fn()); + return a; + } + `); + return g; + } + + // Allocate a large number of various types of objects, and check that census + // finds them. + const g = newGlobalWithDefs(); + dbg.addDebuggee(g); + + g.eval("var objs = times(100, () => ({}));"); + g.eval("var rxs = times(200, () => /foo/);"); + g.eval("var ars = times(400, () => []);"); + g.eval("var fns = times(800, () => () => {});"); + + const census1 = dbg.memory.takeCensus(dbg); + Census.walkCensus( + census1, + "census1", + Census.assertAllNotLessThan({ + objects: { + Object: { count: 100 }, + RegExp: { count: 200 }, + Array: { count: 400 }, + Function: { count: 800 }, + }, + }) + ); + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_03.js new file mode 100644 index 0000000000..06c574d4e0 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_03.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// HeapSnapshot.prototype.takeCensus behaves plausibly as we add and remove +// debuggees. +// +// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-03.js + +function run_test() { + const dbg = new Debugger(); + + const census0 = saveHeapSnapshotAndTakeCensus(dbg); + Census.walkCensus(census0, "census0", Census.assertAllZeros); + + const g1 = newGlobal(); + dbg.addDebuggee(g1); + const census1 = saveHeapSnapshotAndTakeCensus(dbg); + Census.walkCensus(census1, "census1", Census.assertAllNotLessThan(census0)); + + const g2 = newGlobal(); + dbg.addDebuggee(g2); + const census2 = saveHeapSnapshotAndTakeCensus(dbg); + Census.walkCensus(census2, "census2", Census.assertAllNotLessThan(census1)); + + dbg.removeDebuggee(g2); + const census3 = saveHeapSnapshotAndTakeCensus(dbg); + Census.walkCensus(census3, "census3", Census.assertAllEqual(census1)); + + dbg.removeDebuggee(g1); + const census4 = saveHeapSnapshotAndTakeCensus(dbg); + Census.walkCensus(census4, "census4", Census.assertAllEqual(census0)); + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_04.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_04.js new file mode 100644 index 0000000000..ac484c8815 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_04.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that HeapSnapshot.prototype.takeCensus finds GC roots that are on the +// stack. +// +// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-04.js + +function run_test() { + const g = newGlobal(); + const dbg = new Debugger(g); + + g.eval(` +function withAllocationMarkerOnStack(f) { + (function () { + var onStack = allocationMarker(); + f(); + }()); +} +`); + + equal( + "AllocationMarker" in saveHeapSnapshotAndTakeCensus(dbg).objects, + false, + "There shouldn't exist any allocation markers in the census." + ); + + let allocationMarkerCount; + g.withAllocationMarkerOnStack(() => { + const census = saveHeapSnapshotAndTakeCensus(dbg); + allocationMarkerCount = census.objects.AllocationMarker.count; + }); + + equal( + allocationMarkerCount, + 1, + "Should have one allocation marker in the census, because there " + + "was one on the stack." + ); + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_05.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_05.js new file mode 100644 index 0000000000..22c5324c68 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_05.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that HeapSnapshot.prototype.takeCensus finds cross compartment +// wrapper GC roots. +// +// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-05.js + +/* eslint-disable strict */ +function run_test() { + const g = newGlobal(); + const dbg = new Debugger(g); + + equal( + "AllocationMarker" in saveHeapSnapshotAndTakeCensus(dbg).objects, + false, + "No allocation markers should exist in the census." + ); + + this.ccw = g.allocationMarker(); + + const census = saveHeapSnapshotAndTakeCensus(dbg); + equal( + census.objects.AllocationMarker.count, + 1, + "Should have one allocation marker in the census, because there " + + "is one cross-compartment wrapper referring to it." + ); + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_06.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_06.js new file mode 100644 index 0000000000..2b639411f9 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_06.js @@ -0,0 +1,127 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Check HeapSnapshot.prototype.takeCensus handling of 'breakdown' argument. +// +// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-06.js + +function run_test() { + const Pattern = Match.Pattern; + + const g = newGlobal(); + const dbg = new Debugger(g); + + Pattern({ count: Pattern.NATURAL, bytes: Pattern.NATURAL }).assert( + saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "count" } }) + ); + + let census = saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { by: "count", count: false, bytes: false }, + }); + equal("count" in census, false); + equal("bytes" in census, false); + + census = saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { by: "count", count: true, bytes: false }, + }); + equal("count" in census, true); + equal("bytes" in census, false); + + census = saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { by: "count", count: false, bytes: true }, + }); + equal("count" in census, false); + equal("bytes" in census, true); + + census = saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { by: "count", count: true, bytes: true }, + }); + equal("count" in census, true); + equal("bytes" in census, true); + + // Pattern doesn't mind objects with extra properties, so we'll restrict this + // list to the object classes we're pretty sure are going to stick around for + // the forseeable future. + Pattern({ + Function: { count: Pattern.NATURAL }, + Object: { count: Pattern.NATURAL }, + DebuggerPrototype: { count: Pattern.NATURAL }, + Sandbox: { count: Pattern.NATURAL }, + }).assert( + saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "objectClass" } }) + ); + + Pattern({ + objects: { count: Pattern.NATURAL }, + scripts: { count: Pattern.NATURAL }, + strings: { count: Pattern.NATURAL }, + other: { count: Pattern.NATURAL }, + }).assert( + saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "coarseType" } }) + ); + + // As for { by: 'objectClass' }, restrict our pattern to the types + // we predict will stick around for a long time. + Pattern({ + JSString: { count: Pattern.NATURAL }, + "js::Shape": { count: Pattern.NATURAL }, + JSObject: { count: Pattern.NATURAL }, + }).assert( + saveHeapSnapshotAndTakeCensus(dbg, { breakdown: { by: "internalType" } }) + ); + + // Nested breakdowns. + + const coarseTypePattern = { + objects: { count: Pattern.NATURAL }, + scripts: { count: Pattern.NATURAL }, + strings: { count: Pattern.NATURAL }, + other: { count: Pattern.NATURAL }, + }; + + Pattern({ + JSString: coarseTypePattern, + "js::Shape": coarseTypePattern, + JSObject: coarseTypePattern, + }).assert( + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { by: "internalType", then: { by: "coarseType" } }, + }) + ); + + Pattern({ + Function: { count: Pattern.NATURAL }, + Object: { count: Pattern.NATURAL }, + DebuggerPrototype: { count: Pattern.NATURAL }, + Sandbox: { count: Pattern.NATURAL }, + other: coarseTypePattern, + }).assert( + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { + by: "objectClass", + then: { by: "count" }, + other: { by: "coarseType" }, + }, + }) + ); + + Pattern({ + objects: { count: Pattern.NATURAL, label: "object" }, + scripts: { count: Pattern.NATURAL, label: "scripts" }, + strings: { count: Pattern.NATURAL, label: "strings" }, + other: { count: Pattern.NATURAL, label: "other" }, + }).assert( + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { + by: "coarseType", + objects: { by: "count", label: "object" }, + scripts: { by: "count", label: "scripts" }, + strings: { by: "count", label: "strings" }, + other: { by: "count", label: "other" }, + }, + }) + ); + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_07.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_07.js new file mode 100644 index 0000000000..56bec51074 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_07.js @@ -0,0 +1,124 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// HeapSnapshot.prototype.takeCensus breakdown: check error handling on property +// gets. +// +// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-07.js + +function run_test() { + const g = newGlobal(); + const dbg = new Debugger(g); + + assertThrows(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { + get by() { + throw Error("ಠ_ಠ"); + }, + }, + }); + }, "ಠ_ಠ"); + + assertThrows(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { + by: "count", + get count() { + throw Error("ಠ_ಠ"); + }, + }, + }); + }, "ಠ_ಠ"); + + assertThrows(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { + by: "count", + get bytes() { + throw Error("ಠ_ಠ"); + }, + }, + }); + }, "ಠ_ಠ"); + + assertThrows(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { + by: "objectClass", + get then() { + throw Error("ಠ_ಠ"); + }, + }, + }); + }, "ಠ_ಠ"); + + assertThrows(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { + by: "objectClass", + get other() { + throw Error("ಠ_ಠ"); + }, + }, + }); + }, "ಠ_ಠ"); + + assertThrows(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { + by: "coarseType", + get objects() { + throw Error("ಠ_ಠ"); + }, + }, + }); + }, "ಠ_ಠ"); + + assertThrows(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { + by: "coarseType", + get scripts() { + throw Error("ಠ_ಠ"); + }, + }, + }); + }, "ಠ_ಠ"); + + assertThrows(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { + by: "coarseType", + get strings() { + throw Error("ಠ_ಠ"); + }, + }, + }); + }, "ಠ_ಠ"); + + assertThrows(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { + by: "coarseType", + get other() { + throw Error("ಠ_ಠ"); + }, + }, + }); + }, "ಠ_ಠ"); + + assertThrows(() => { + saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { + by: "internalType", + get then() { + throw Error("ಠ_ಠ"); + }, + }, + }); + }, "ಠ_ಠ"); + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_08.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_08.js new file mode 100644 index 0000000000..f40d367cd6 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_08.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// HeapSnapshot.prototype.takeCensus: test by: 'count' breakdown +// +// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-08.js + +function run_test() { + const g = newGlobal(); + const dbg = new Debugger(g); + + g.eval(` + var stuff = []; + function add(n, c) { + for (let i = 0; i < n; i++) + stuff.push(c()); + } + + let count = 0; + + function obj() { return { count: count++ }; } + obj.factor = 1; + + // This creates a closure (a function JSObject) that has captured + // a Call object. So each call creates two items. + function fun() { let v = count; return () => { return v; } } + fun.factor = 2; + + function str() { return 'perambulator' + count++; } + str.factor = 1; + + // Eval a fresh text each time, allocating: + // - a fresh ScriptSourceObject + // - a new JSScripts, not an eval cache hits + // - a fresh prototype object + // - a fresh Call object, since the eval makes 'ev' heavyweight + // - the new function itself + function ev() { + return eval(\`(function () { return \${ count++ } })\`); + } + ev.factor = 5; + + // A new object (1) with a new shape (2) with a new atom (3) + function shape() { return { [ 'theobroma' + count++ ]: count }; } + shape.factor = 3; + `); + + let baseline = 0; + function countIncreasedByAtLeast(n) { + const oldBaseline = baseline; + + // Since a census counts only reachable objects, one might assume that calling + // GC here would have no effect on the census results. But GC also throws away + // JIT code and any objects it might be holding (template objects, say); + // takeCensus reaches those. Shake everything loose that we can, to make the + // census approximate reachability a bit more closely, and make our results a + // bit more predictable. + gc(g, "shrinking"); + + baseline = saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { by: "count" }, + }).count; + return baseline >= oldBaseline + n; + } + + countIncreasedByAtLeast(0); + + g.add(100, g.obj); + ok(countIncreasedByAtLeast(g.obj.factor * 100)); + + g.add(100, g.fun); + ok(countIncreasedByAtLeast(g.fun.factor * 100)); + + g.add(100, g.str); + ok(countIncreasedByAtLeast(g.str.factor * 100)); + + g.add(100, g.ev); + ok(countIncreasedByAtLeast(g.ev.factor * 100)); + + g.add(100, g.shape); + ok(countIncreasedByAtLeast(g.shape.factor * 100)); + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_09.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_09.js new file mode 100644 index 0000000000..99830d75cb --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_09.js @@ -0,0 +1,111 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// HeapSnapshot.prototype.takeCensus: by: allocationStack breakdown +// +// Ported from js/src/jit-test/tests/debug/Memory-takeCensus-09.js + +function run_test() { + const g = newGlobal(); + const dbg = new Debugger(g); + + g.eval(` // 1 + var log = []; // 2 + function f() { log.push(allocationMarker()); } // 3 + function g() { f(); } // 4 + function h() { f(); } // 5 + `); + + // Create one allocationMarker with tracking turned off, + // so it will have no associated stack. + g.f(); + + dbg.memory.allocationSamplingProbability = 1; + + for (const [func, n] of [ + [g.f, 20], + [g.g, 10], + [g.h, 5], + ]) { + for (let i = 0; i < n; i++) { + dbg.memory.trackingAllocationSites = true; + // All allocations of allocationMarker occur with this line as the oldest + // stack frame. + func(); + dbg.memory.trackingAllocationSites = false; + } + } + + const census = saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { + by: "objectClass", + then: { + by: "allocationStack", + then: { by: "count", label: "haz stack" }, + noStack: { + by: "count", + label: "no haz stack", + }, + }, + }, + }); + + const map = census.AllocationMarker; + ok(map instanceof Map, "Should be a Map instance"); + equal( + map.size, + 4, + "Should have 4 allocation stacks (including the lack of a stack)" + ); + + // Gather the stacks we are expecting to appear as keys, and + // check that there are no unexpected keys. + const stacks = {}; + + map.forEach((v, k) => { + if (k === "noStack") { + // No need to save this key. + } else if ( + k.functionDisplayName === "f" && + k.parent.functionDisplayName === "run_test" + ) { + stacks.f = k; + } else if ( + k.functionDisplayName === "f" && + k.parent.functionDisplayName === "g" && + k.parent.parent.functionDisplayName === "run_test" + ) { + stacks.fg = k; + } else if ( + k.functionDisplayName === "f" && + k.parent.functionDisplayName === "h" && + k.parent.parent.functionDisplayName === "run_test" + ) { + stacks.fh = k; + } else { + dumpn("Unexpected allocation stack:"); + k.toString() + .split(/\n/g) + .forEach(s => dumpn(s)); + ok(false); + } + }); + + equal(map.get("noStack").label, "no haz stack"); + equal(map.get("noStack").count, 1); + + ok(stacks.f); + equal(map.get(stacks.f).label, "haz stack"); + equal(map.get(stacks.f).count, 20); + + ok(stacks.fg); + equal(map.get(stacks.fg).label, "haz stack"); + equal(map.get(stacks.fg).count, 10); + + ok(stacks.fh); + equal(map.get(stacks.fh).label, "haz stack"); + equal(map.get(stacks.fh).count, 5); + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_10.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_10.js new file mode 100644 index 0000000000..9b0e0f8c74 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_10.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Check byte counts produced by takeCensus. +// +// Note that tracking allocation sites adds unique IDs to objects which +// increases their size, making it hard to test reported sizes exactly. +// +// Ported from js/src/jit-test/tests/debug/Memory-take Census-10.js + +function run_test() { + const g = newGlobal(); + const dbg = new Debugger(g); + + const sizeOfAM = byteSize(allocationMarker()); + + // Allocate a single allocation marker, and check that we can find it. + g.eval("var hold = allocationMarker();"); + let census = saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { by: "objectClass" }, + }); + equal(census.AllocationMarker.count, 1); + equal(census.AllocationMarker.bytes >= sizeOfAM, true); + g.hold = null; + + g.eval(` // 1 + var objs = []; // 2 + function fnerd() { // 3 + objs.push(allocationMarker()); // 4 + for (let i = 0; i < 10; i++) // 5 + objs.push(allocationMarker()); // 6 + } // 7 + `); + + dbg.memory.allocationSamplingProbability = 1; + dbg.memory.trackingAllocationSites = true; + g.fnerd(); + dbg.memory.trackingAllocationSites = false; + + census = saveHeapSnapshotAndTakeCensus(dbg, { + breakdown: { by: "objectClass", then: { by: "allocationStack" } }, + }); + + let seen = 0; + census.AllocationMarker.forEach((v, k) => { + equal(k.functionDisplayName, "fnerd"); + switch (k.line) { + case 4: + equal(v.count, 1); + equal(v.bytes >= sizeOfAM, true); + seen++; + break; + + case 6: + equal(v.count, 10); + equal(v.bytes >= 10 * sizeOfAM, true); + seen++; + break; + + default: + dumpn("Unexpected stack:"); + k.toString() + .split(/\n/g) + .forEach(s => dumpn(s)); + ok(false); + break; + } + }); + + equal(seen, 2); + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_11.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_11.js new file mode 100644 index 0000000000..151437bb41 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_11.js @@ -0,0 +1,131 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that Debugger.Memory.prototype.takeCensus and +// HeapSnapshot.prototype.takeCensus return the same data for the same heap +// graph. + +function doLiveAndOfflineCensus(g, dbg, opts) { + dbg.memory.allocationSamplingProbability = 1; + dbg.memory.trackingAllocationSites = true; + g.eval(` // 1 + (function unsafeAtAnySpeed() { // 2 + for (var i = 0; i < 100; i++) { // 3 + this.markers.push(allocationMarker()); // 4 + } // 5 + }()); // 6 + `); + dbg.memory.trackingAllocationSites = false; + + return { + live: dbg.memory.takeCensus(opts), + offline: saveHeapSnapshotAndTakeCensus(dbg, opts), + }; +} + +function getMarkerSize(g, dbg) { + dbg.memory.allocationSamplingProbability = 1; + dbg.memory.trackingAllocationSites = true; + g.eval("var hold = allocationMarker();"); + dbg.memory.trackingAllocationSites = false; + const live = dbg.memory.takeCensus({ + breakdown: { by: "objectClass", then: { by: "count" } }, + }); + g.hold = null; + equal(live.AllocationMarker.count, 1); + return live.AllocationMarker.bytes; +} + +function run_test() { + const g = newGlobal(); + const dbg = new Debugger(g); + + g.eval("this.markers = []"); + const markerSize = getMarkerSize(g, dbg); + + // First, test that we get the same counts and sizes as we allocate and retain + // more things. + + let prevCount = 0; + let prevBytes = 0; + + for (let i = 0; i < 10; i++) { + const { live, offline } = doLiveAndOfflineCensus(g, dbg, { + breakdown: { by: "objectClass", then: { by: "count" } }, + }); + + equal(live.AllocationMarker.count, offline.AllocationMarker.count); + equal(live.AllocationMarker.bytes, offline.AllocationMarker.bytes); + equal(live.AllocationMarker.count, prevCount + 100); + equal(live.AllocationMarker.bytes, prevBytes + 100 * markerSize); + + prevCount = live.AllocationMarker.count; + prevBytes = live.AllocationMarker.bytes; + } + + // Second, test that the reported allocation stacks and counts and sizes at + // those allocation stacks match up. + + const { live, offline } = doLiveAndOfflineCensus(g, dbg, { + breakdown: { by: "objectClass", then: { by: "allocationStack" } }, + }); + + equal(live.AllocationMarker.size, offline.AllocationMarker.size); + // One stack with the loop further above, and another stack featuring the call + // right above. + equal(live.AllocationMarker.size, 2); + + // Note that because SavedFrame stacks reconstructed from an offline heap + // snapshot don't have the same principals as SavedFrame stacks captured from + // a live stack, the live and offline allocation stacks won't be identity + // equal, but should be structurally the same. + + const liveEntries = []; + live.AllocationMarker.forEach((v, k) => { + dumpn("Allocation stack:"); + k.toString() + .split(/\n/g) + .forEach(s => dumpn(s)); + + equal(k.functionDisplayName, "unsafeAtAnySpeed"); + equal(k.line, 4); + + liveEntries.push([k.toString(), v]); + }); + + const offlineEntries = []; + offline.AllocationMarker.forEach((v, k) => { + dumpn("Allocation stack:"); + k.toString() + .split(/\n/g) + .forEach(s => dumpn(s)); + + equal(k.functionDisplayName, "unsafeAtAnySpeed"); + equal(k.line, 4); + + offlineEntries.push([k.toString(), v]); + }); + + const sortEntries = (a, b) => { + if (a[0] < b[0]) { + return -1; + } else if (a[0] > b[0]) { + return 1; + } + return 0; + }; + liveEntries.sort(sortEntries); + offlineEntries.sort(sortEntries); + + equal(liveEntries.length, live.AllocationMarker.size); + equal(liveEntries.length, offlineEntries.length); + + for (let i = 0; i < liveEntries.length; i++) { + equal(liveEntries[i][0], offlineEntries[i][0]); + equal(liveEntries[i][1].count, offlineEntries[i][1].count); + equal(liveEntries[i][1].bytes, offlineEntries[i][1].bytes); + } + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_12.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_12.js new file mode 100644 index 0000000000..f3a72102b0 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_HeapSnapshot_takeCensus_12.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that when we take a census and get a bucket list of ids that matched the +// given category, that the returned ids are all in the snapshot and their +// reported category. + +function run_test() { + const g = newGlobal(); + const dbg = new Debugger(g); + + const path = saveNewHeapSnapshot({ debugger: dbg }); + const snapshot = readHeapSnapshot(path); + + const bucket = { by: "bucket" }; + const count = { by: "count", count: true, bytes: false }; + const objectClassCount = { by: "objectClass", then: count, other: count }; + + const byClassName = snapshot.takeCensus({ + breakdown: { + by: "objectClass", + then: bucket, + other: bucket, + }, + }); + + const byClassNameCount = snapshot.takeCensus({ + breakdown: objectClassCount, + }); + + const keys = new Set(Object.keys(byClassName)); + equal( + keys.size, + Object.keys(byClassNameCount).length, + "Should have the same number of keys." + ); + for (const k of Object.keys(byClassNameCount)) { + ok(keys.has(k), "Should not have any unexpected class names"); + } + + for (const key of Object.keys(byClassName)) { + equal( + byClassNameCount[key].count, + byClassName[key].length, + "Length of the bucket and count should be equal" + ); + + for (const id of byClassName[key]) { + const desc = snapshot.describeNode(objectClassCount, id); + equal( + desc[key].count, + 1, + "Describing the bucketed node confirms that it belongs to the category" + ); + } + } + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot.js new file mode 100644 index 0000000000..b7a3c9c2f8 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we can read core dumps into HeapSnapshot instances. +/* eslint-disable strict */ +if (typeof Debugger != "function") { + const { addDebuggerToGlobal } = ChromeUtils.importESModule( + "resource://gre/modules/jsdebugger.sys.mjs" + ); + addDebuggerToGlobal(globalThis); +} + +function run_test() { + const filePath = ChromeUtils.saveHeapSnapshot({ globals: [this] }); + ok(true, "Should be able to save a snapshot."); + + const snapshot = ChromeUtils.readHeapSnapshot(filePath); + ok(snapshot, "Should be able to read a heap snapshot"); + ok(HeapSnapshot.isInstance(snapshot), "Should be an instanceof HeapSnapshot"); + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_with_allocations.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_with_allocations.js new file mode 100644 index 0000000000..273736abcd --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_with_allocations.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that we can save a core dump with allocation stacks and read it back +// into a HeapSnapshot. + +if (typeof Debugger != "function") { + const { addDebuggerToGlobal } = ChromeUtils.importESModule( + "resource://gre/modules/jsdebugger.sys.mjs" + ); + addDebuggerToGlobal(globalThis); +} + +function run_test() { + Services.prefs.setBoolPref( + "security.allow_parent_unrestricted_js_loads", + true + ); + Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); + Services.prefs.setBoolPref("security.allow_eval_in_parent_process", true); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_parent_unrestricted_js_loads"); + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); + Services.prefs.clearUserPref("security.allow_eval_in_parent_process"); + }); + + // Create a Debugger observing a debuggee's allocations. + const debuggee = new Cu.Sandbox(null); + const dbg = new Debugger(debuggee); + dbg.memory.trackingAllocationSites = true; + + // Allocate some objects in the debuggee that will have their allocation + // stacks recorded by the Debugger. + debuggee.eval("this.objects = []"); + for (let i = 0; i < 100; i++) { + debuggee.eval("this.objects.push({})"); + } + + // Now save a snapshot that will include the allocation stacks and read it + // back again. + + const filePath = ChromeUtils.saveHeapSnapshot({ runtime: true }); + ok(true, "Should be able to save a snapshot."); + + const snapshot = ChromeUtils.readHeapSnapshot(filePath); + ok(snapshot, "Should be able to read a heap snapshot"); + ok(HeapSnapshot.isInstance(snapshot), "Should be an instanceof HeapSnapshot"); + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_with_utf8_paths.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_with_utf8_paths.js new file mode 100644 index 0000000000..aecbd9cdda --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_with_utf8_paths.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we can read core dumps with a UTF8 path into HeapSnapshot instances. +/* eslint-disable strict */ +add_task(async function () { + const fileNameWithRussianCharacters = + "Снимок памяти Click.ru 08.06.2020 (Firefox dump).fxsnapshot"; + const filePathWithRussianCharacters = PathUtils.join( + PathUtils.tempDir, + fileNameWithRussianCharacters + ); + + const filePath = ChromeUtils.saveHeapSnapshot({ globals: [this] }); + ok(true, "Should be able to save a snapshot."); + + await IOUtils.copy(filePath, filePathWithRussianCharacters); + + ok( + await IOUtils.exists(filePathWithRussianCharacters), + `We could copy the file to the expected path ${filePathWithRussianCharacters}` + ); + + const snapshot = ChromeUtils.readHeapSnapshot(filePathWithRussianCharacters); + ok(snapshot, "Should be able to read a heap snapshot from an utf8 path"); + ok(HeapSnapshot.isInstance(snapshot), "Should be an instanceof HeapSnapshot"); +}); diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_worker.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_worker.js new file mode 100644 index 0000000000..52d4b31e29 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_ReadHeapSnapshot_worker.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that we can read core dumps into HeapSnapshot instances in a worker. +add_task(async function () { + const worker = new ChromeWorker("resource://test/heap-snapshot-worker.js"); + worker.postMessage({}); + + let assertionCount = 0; + worker.onmessage = e => { + if (e.data.type !== "assertion") { + return; + } + + ok(e.data.passed, e.data.msg + "\n" + e.data.stack); + assertionCount++; + }; + + await waitForDone(worker); + + 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); + resolve(); + } + }); + }); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_SaveHeapSnapshot.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_SaveHeapSnapshot.js new file mode 100644 index 0000000000..33367af7f7 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_SaveHeapSnapshot.js @@ -0,0 +1,121 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test the ChromeUtils interface. +// eslint-disable-next-line +if (typeof Debugger != "function") { + const { addDebuggerToGlobal } = ChromeUtils.importESModule( + "resource://gre/modules/jsdebugger.sys.mjs" + ); + addDebuggerToGlobal(globalThis); +} + +function run_test() { + ok(ChromeUtils, "Should be able to get the ChromeUtils interface"); + + testBadParameters(); + testGoodParameters(); + + do_test_finished(); +} + +function testBadParameters() { + Assert.throws( + () => ChromeUtils.saveHeapSnapshot(), + /NS_ERROR_ILLEGAL_VALUE/, + "Should throw if arguments aren't passed in." + ); + + Assert.throws( + () => ChromeUtils.saveHeapSnapshot(null), + /NS_ERROR_ILLEGAL_VALUE/, + "Should throw if boundaries isn't an object." + ); + + Assert.throws( + () => ChromeUtils.saveHeapSnapshot({}), + /NS_ERROR_ILLEGAL_VALUE/, + "Should throw if the boundaries object doesn't have any properties." + ); + + Assert.throws( + () => ChromeUtils.saveHeapSnapshot({ runtime: true, globals: [this] }), + /NS_ERROR_ILLEGAL_VALUE/, + "Should throw if the boundaries object has more than one property." + ); + + Assert.throws( + () => ChromeUtils.saveHeapSnapshot({ debugger: {} }), + /NS_ERROR_ILLEGAL_VALUE/, + "Should throw if the debuggees object is not a Debugger object" + ); + + Assert.throws( + () => ChromeUtils.saveHeapSnapshot({ globals: [{}] }), + /NS_ERROR_ILLEGAL_VALUE/, + "Should throw if the globals array contains non-global objects." + ); + + Assert.throws( + () => ChromeUtils.saveHeapSnapshot({ runtime: false }), + /NS_ERROR_ILLEGAL_VALUE/, + "Should throw if runtime is supplied and is not true." + ); + + Assert.throws( + () => ChromeUtils.saveHeapSnapshot({ globals: null }), + /TypeError:.*can't be converted to a sequence/, + "Should throw if globals is not an object." + ); + + Assert.throws( + () => ChromeUtils.saveHeapSnapshot({ globals: {} }), + /TypeError:.*can't be converted to a sequence/, + "Should throw if globals is not an array." + ); + + Assert.throws( + () => ChromeUtils.saveHeapSnapshot({ debugger: Debugger.prototype }), + /NS_ERROR_ILLEGAL_VALUE/, + "Should throw if debugger is the Debugger.prototype object." + ); + + Assert.throws( + () => + ChromeUtils.saveHeapSnapshot({ + get globals() { + return [this]; + }, + }), + /NS_ERROR_ILLEGAL_VALUE/, + "Should throw if boundaries property is a getter." + ); +} + +const makeNewSandbox = () => + Cu.Sandbox(CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")()); + +function testGoodParameters() { + const sandbox = makeNewSandbox(); + let dbg = new Debugger(sandbox); + + ChromeUtils.saveHeapSnapshot({ debugger: dbg }); + ok(true, "Should be able to save a snapshot for a debuggee global."); + + dbg = new Debugger(); + const sandboxes = Array(10).fill(null).map(makeNewSandbox); + sandboxes.forEach(sb => dbg.addDebuggee(sb)); + + ChromeUtils.saveHeapSnapshot({ debugger: dbg }); + ok(true, "Should be able to save a snapshot for many debuggee globals."); + + dbg = new Debugger(); + ChromeUtils.saveHeapSnapshot({ debugger: dbg }); + ok(true, "Should be able to save a snapshot with no debuggee globals."); + + ChromeUtils.saveHeapSnapshot({ globals: [this] }); + ok(true, "Should be able to save a snapshot for a specific global."); + + ChromeUtils.saveHeapSnapshot({ runtime: true }); + ok(true, "Should be able to save a snapshot of the full runtime."); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-01.js new file mode 100644 index 0000000000..625d359675 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-01.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * Tests CensusTreeNode with `internalType` breakdown. + */ + +const BREAKDOWN = { + by: "internalType", + then: { by: "count", count: true, bytes: true }, +}; + +const REPORT = { + JSObject: { + bytes: 100, + count: 10, + }, + "js::Shape": { + bytes: 500, + count: 50, + }, + JSString: { + bytes: 10, + count: 1, + }, +}; + +const EXPECTED = { + name: null, + bytes: 0, + totalBytes: 610, + count: 0, + totalCount: 61, + children: [ + { + name: "js::Shape", + bytes: 500, + totalBytes: 500, + count: 50, + totalCount: 50, + children: undefined, + id: 3, + parent: 1, + reportLeafIndex: 2, + }, + { + name: "JSObject", + bytes: 100, + totalBytes: 100, + count: 10, + totalCount: 10, + children: undefined, + id: 2, + parent: 1, + reportLeafIndex: 1, + }, + { + name: "JSString", + bytes: 10, + totalBytes: 10, + count: 1, + totalCount: 1, + children: undefined, + id: 4, + parent: 1, + reportLeafIndex: 3, + }, + ], + id: 1, + parent: undefined, + reportLeafIndex: undefined, +}; + +function run_test() { + compareCensusViewData(BREAKDOWN, REPORT, EXPECTED); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-02.js new file mode 100644 index 0000000000..75ed87c25b --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-02.js @@ -0,0 +1,139 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * Tests CensusTreeNode with `coarseType` breakdown. + */ + +const countBreakdown = { by: "count", count: true, bytes: true }; + +const BREAKDOWN = { + by: "coarseType", + objects: { by: "objectClass", then: countBreakdown }, + strings: countBreakdown, + scripts: countBreakdown, + other: { by: "internalType", then: countBreakdown }, + domNode: countBreakdown, +}; + +const REPORT = { + objects: { + Function: { bytes: 10, count: 1 }, + Array: { bytes: 20, count: 2 }, + }, + strings: { bytes: 10, count: 1 }, + scripts: { bytes: 1, count: 1 }, + other: { + "js::Shape": { bytes: 30, count: 3 }, + "js::Shape2": { bytes: 40, count: 4 }, + }, + domNode: { bytes: 0, count: 0 }, +}; + +const EXPECTED = { + name: null, + bytes: 0, + totalBytes: 111, + count: 0, + totalCount: 12, + children: [ + { + name: "other", + count: 0, + totalCount: 7, + bytes: 0, + totalBytes: 70, + children: [ + { + name: "js::Shape2", + bytes: 40, + totalBytes: 40, + count: 4, + totalCount: 4, + children: undefined, + id: 9, + parent: 7, + reportLeafIndex: 8, + }, + { + name: "js::Shape", + bytes: 30, + totalBytes: 30, + count: 3, + totalCount: 3, + children: undefined, + id: 8, + parent: 7, + reportLeafIndex: 7, + }, + ], + id: 7, + parent: 1, + reportLeafIndex: undefined, + }, + { + name: "objects", + count: 0, + totalCount: 3, + bytes: 0, + totalBytes: 30, + children: [ + { + name: "Array", + bytes: 20, + totalBytes: 20, + count: 2, + totalCount: 2, + children: undefined, + id: 4, + parent: 2, + reportLeafIndex: 3, + }, + { + name: "Function", + bytes: 10, + totalBytes: 10, + count: 1, + totalCount: 1, + children: undefined, + id: 3, + parent: 2, + reportLeafIndex: 2, + }, + ], + id: 2, + parent: 1, + reportLeafIndex: undefined, + }, + { + name: "strings", + count: 1, + totalCount: 1, + bytes: 10, + totalBytes: 10, + children: undefined, + id: 6, + parent: 1, + reportLeafIndex: 5, + }, + { + name: "scripts", + count: 1, + totalCount: 1, + bytes: 1, + totalBytes: 1, + children: undefined, + id: 5, + parent: 1, + reportLeafIndex: 4, + }, + ], + id: 1, + parent: undefined, + reportLeafIndex: undefined, +}; + +function run_test() { + compareCensusViewData(BREAKDOWN, REPORT, EXPECTED); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-03.js new file mode 100644 index 0000000000..e0ca7dc15d --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-03.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * Tests CensusTreeNode with `objectClass` breakdown. + */ + +const countBreakdown = { by: "count", count: true, bytes: true }; + +const BREAKDOWN = { + by: "objectClass", + then: countBreakdown, + other: { by: "internalType", then: countBreakdown }, +}; + +const REPORT = { + Function: { bytes: 10, count: 10 }, + Array: { bytes: 100, count: 1 }, + other: { + "JIT::CODE::NOW!!!": { bytes: 20, count: 2 }, + "JIT::CODE::LATER!!!": { bytes: 40, count: 4 }, + }, +}; + +const EXPECTED = { + name: null, + count: 0, + totalCount: 17, + bytes: 0, + totalBytes: 170, + children: [ + { + name: "Array", + bytes: 100, + totalBytes: 100, + count: 1, + totalCount: 1, + children: undefined, + id: 3, + parent: 1, + reportLeafIndex: 2, + }, + { + name: "other", + count: 0, + totalCount: 6, + bytes: 0, + totalBytes: 60, + children: [ + { + name: "JIT::CODE::LATER!!!", + bytes: 40, + totalBytes: 40, + count: 4, + totalCount: 4, + children: undefined, + id: 6, + parent: 4, + reportLeafIndex: 5, + }, + { + name: "JIT::CODE::NOW!!!", + bytes: 20, + totalBytes: 20, + count: 2, + totalCount: 2, + children: undefined, + id: 5, + parent: 4, + reportLeafIndex: 4, + }, + ], + id: 4, + parent: 1, + reportLeafIndex: undefined, + }, + { + name: "Function", + bytes: 10, + totalBytes: 10, + count: 10, + totalCount: 10, + children: undefined, + id: 2, + parent: 1, + reportLeafIndex: 1, + }, + ], + id: 1, + parent: undefined, + reportLeafIndex: undefined, +}; + +function run_test() { + compareCensusViewData(BREAKDOWN, REPORT, EXPECTED); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-04.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-04.js new file mode 100644 index 0000000000..e1cc32d697 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-04.js @@ -0,0 +1,160 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * Tests CensusTreeNode with `allocationStack` breakdown. + */ + +function run_test() { + const countBreakdown = { by: "count", count: true, bytes: true }; + + const BREAKDOWN = { + by: "allocationStack", + then: countBreakdown, + noStack: countBreakdown, + }; + + let stack1, stack2, stack3, stack4; + + (function a() { + (function b() { + (function c() { + stack1 = saveStack(3); + })(); + (function d() { + stack2 = saveStack(3); + stack3 = saveStack(3); + })(); + stack4 = saveStack(2); + })(); + })(); + + const stack5 = saveStack(1); + + const REPORT = new Map([ + [stack1, { bytes: 10, count: 1 }], + [stack2, { bytes: 20, count: 2 }], + [stack3, { bytes: 30, count: 3 }], + [stack4, { bytes: 40, count: 4 }], + [stack5, { bytes: 50, count: 5 }], + ["noStack", { bytes: 60, count: 6 }], + ]); + + const EXPECTED = { + name: null, + bytes: 0, + totalBytes: 210, + count: 0, + totalCount: 21, + children: [ + { + name: stack4.parent, + bytes: 0, + totalBytes: 100, + count: 0, + totalCount: 10, + children: [ + { + name: stack3.parent, + bytes: 0, + totalBytes: 50, + count: 0, + totalCount: 5, + children: [ + { + name: stack3, + bytes: 30, + totalBytes: 30, + count: 3, + totalCount: 3, + children: undefined, + id: 7, + parent: 5, + reportLeafIndex: 3, + }, + { + name: stack2, + bytes: 20, + totalBytes: 20, + count: 2, + totalCount: 2, + children: undefined, + id: 6, + parent: 5, + reportLeafIndex: 2, + }, + ], + id: 5, + parent: 2, + reportLeafIndex: undefined, + }, + { + name: stack4, + bytes: 40, + totalBytes: 40, + count: 4, + totalCount: 4, + children: undefined, + id: 8, + parent: 2, + reportLeafIndex: 4, + }, + { + name: stack1.parent, + bytes: 0, + totalBytes: 10, + count: 0, + totalCount: 1, + children: [ + { + name: stack1, + bytes: 10, + totalBytes: 10, + count: 1, + totalCount: 1, + children: undefined, + id: 4, + parent: 3, + reportLeafIndex: 1, + }, + ], + id: 3, + parent: 2, + reportLeafIndex: undefined, + }, + ], + id: 2, + parent: 1, + reportLeafIndex: undefined, + }, + { + name: "noStack", + bytes: 60, + totalBytes: 60, + count: 6, + totalCount: 6, + children: undefined, + id: 10, + parent: 1, + reportLeafIndex: 6, + }, + { + name: stack5, + bytes: 50, + totalBytes: 50, + count: 5, + totalCount: 5, + children: undefined, + id: 9, + parent: 1, + reportLeafIndex: 5, + }, + ], + id: 1, + parent: undefined, + reportLeafIndex: undefined, + }; + + compareCensusViewData(BREAKDOWN, REPORT, EXPECTED); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-05.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-05.js new file mode 100644 index 0000000000..fbd9944507 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-05.js @@ -0,0 +1,150 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * Tests CensusTreeNode with `allocationStack` => `objectClass` breakdown. + */ + +function run_test() { + const countBreakdown = { by: "count", count: true, bytes: true }; + + const BREAKDOWN = { + by: "allocationStack", + then: { + by: "objectClass", + then: countBreakdown, + other: countBreakdown, + }, + noStack: countBreakdown, + }; + + let stack; + + (function a() { + (function b() { + (function c() { + stack = saveStack(3); + })(); + })(); + })(); + + const REPORT = new Map([ + [ + stack, + { + Foo: { bytes: 10, count: 1 }, + Bar: { bytes: 20, count: 2 }, + Baz: { bytes: 30, count: 3 }, + other: { bytes: 40, count: 4 }, + }, + ], + ["noStack", { bytes: 50, count: 5 }], + ]); + + const EXPECTED = { + name: null, + bytes: 0, + totalBytes: 150, + count: 0, + totalCount: 15, + children: [ + { + name: stack.parent.parent, + bytes: 0, + totalBytes: 100, + count: 0, + totalCount: 10, + children: [ + { + name: stack.parent, + bytes: 0, + totalBytes: 100, + count: 0, + totalCount: 10, + children: [ + { + name: stack, + bytes: 0, + totalBytes: 100, + count: 0, + totalCount: 10, + children: [ + { + name: "other", + bytes: 40, + totalBytes: 40, + count: 4, + totalCount: 4, + children: undefined, + id: 8, + parent: 4, + reportLeafIndex: 5, + }, + { + name: "Baz", + bytes: 30, + totalBytes: 30, + count: 3, + totalCount: 3, + children: undefined, + id: 7, + parent: 4, + reportLeafIndex: 4, + }, + { + name: "Bar", + bytes: 20, + totalBytes: 20, + count: 2, + totalCount: 2, + children: undefined, + id: 6, + parent: 4, + reportLeafIndex: 3, + }, + { + name: "Foo", + bytes: 10, + totalBytes: 10, + count: 1, + totalCount: 1, + children: undefined, + id: 5, + parent: 4, + reportLeafIndex: 2, + }, + ], + id: 4, + parent: 3, + reportLeafIndex: undefined, + }, + ], + id: 3, + parent: 2, + reportLeafIndex: undefined, + }, + ], + id: 2, + parent: 1, + reportLeafIndex: undefined, + }, + { + name: "noStack", + bytes: 50, + totalBytes: 50, + count: 5, + totalCount: 5, + children: undefined, + id: 9, + parent: 1, + reportLeafIndex: 6, + }, + ], + id: 1, + parent: undefined, + reportLeafIndex: undefined, + }; + + compareCensusViewData(BREAKDOWN, REPORT, EXPECTED); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-06.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-06.js new file mode 100644 index 0000000000..70cc4a2696 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-06.js @@ -0,0 +1,199 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * Test inverting CensusTreeNode with a by alloaction stack breakdown. + */ + +function run_test() { + const BREAKDOWN = { + by: "allocationStack", + then: { by: "count", count: true, bytes: true }, + noStack: { by: "count", count: true, bytes: true }, + }; + + function a(n) { + return b(n); + } + function b(n) { + return c(n); + } + function c(n) { + return saveStack(n); + } + function d(n) { + return b(n); + } + function e(n) { + return c(n); + } + + const abc_Stack = a(3); + const bc_Stack = b(2); + const c_Stack = c(1); + const dbc_Stack = d(3); + const ec_Stack = e(2); + + const REPORT = new Map([ + [abc_Stack, { bytes: 10, count: 1 }], + [bc_Stack, { bytes: 10, count: 1 }], + [c_Stack, { bytes: 10, count: 1 }], + [dbc_Stack, { bytes: 10, count: 1 }], + [ec_Stack, { bytes: 10, count: 1 }], + ["noStack", { bytes: 50, count: 5 }], + ]); + + const EXPECTED = { + name: null, + bytes: 0, + totalBytes: 100, + count: 0, + totalCount: 10, + children: [ + { + name: "noStack", + bytes: 50, + totalBytes: 50, + count: 5, + totalCount: 5, + children: [ + { + name: null, + bytes: 0, + totalBytes: 100, + count: 0, + totalCount: 10, + children: undefined, + id: 16, + parent: 15, + reportLeafIndex: undefined, + }, + ], + id: 15, + parent: 14, + reportLeafIndex: 6, + }, + { + name: abc_Stack, + bytes: 50, + totalBytes: 10, + count: 5, + totalCount: 1, + children: [ + { + name: null, + bytes: 0, + totalBytes: 100, + count: 0, + totalCount: 10, + children: undefined, + id: 18, + parent: 17, + reportLeafIndex: undefined, + }, + { + name: abc_Stack.parent, + bytes: 0, + totalBytes: 10, + count: 0, + totalCount: 1, + children: [ + { + name: null, + bytes: 0, + totalBytes: 100, + count: 0, + totalCount: 10, + children: undefined, + id: 22, + parent: 19, + reportLeafIndex: undefined, + }, + { + name: abc_Stack.parent.parent, + bytes: 0, + totalBytes: 10, + count: 0, + totalCount: 1, + children: [ + { + name: null, + bytes: 0, + totalBytes: 100, + count: 0, + totalCount: 10, + children: undefined, + id: 21, + parent: 20, + reportLeafIndex: undefined, + }, + ], + id: 20, + parent: 19, + reportLeafIndex: undefined, + }, + { + name: dbc_Stack.parent.parent, + bytes: 0, + totalBytes: 10, + count: 0, + totalCount: 1, + children: [ + { + name: null, + bytes: 0, + totalBytes: 100, + count: 0, + totalCount: 10, + children: undefined, + id: 24, + parent: 23, + reportLeafIndex: undefined, + }, + ], + id: 23, + parent: 19, + reportLeafIndex: undefined, + }, + ], + id: 19, + parent: 17, + reportLeafIndex: undefined, + }, + { + name: ec_Stack.parent, + bytes: 0, + totalBytes: 10, + count: 0, + totalCount: 1, + children: [ + { + name: null, + bytes: 0, + totalBytes: 100, + count: 0, + totalCount: 10, + children: undefined, + id: 26, + parent: 25, + reportLeafIndex: undefined, + }, + ], + id: 25, + parent: 17, + reportLeafIndex: undefined, + }, + ], + id: 17, + parent: 14, + reportLeafIndex: new Set([1, 2, 3, 4, 5]), + }, + ], + id: 14, + parent: undefined, + reportLeafIndex: undefined, + }; + + compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { invert: true }); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-07.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-07.js new file mode 100644 index 0000000000..009848b9e6 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-07.js @@ -0,0 +1,206 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * Test inverting CensusTreeNode with a non-allocation stack breakdown. + */ + +function run_test() { + const BREAKDOWN = { + by: "coarseType", + objects: { + by: "objectClass", + then: { by: "count", count: true, bytes: true }, + other: { by: "count", count: true, bytes: true }, + }, + scripts: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + strings: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + other: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + domNode: { + by: "descriptiveType", + then: { by: "count", count: true, bytes: true }, + }, + }; + + const REPORT = { + objects: { + Array: { bytes: 50, count: 5 }, + other: { bytes: 0, count: 0 }, + }, + scripts: { + "js::jit::JitScript": { bytes: 30, count: 3 }, + }, + strings: { + JSAtom: { bytes: 60, count: 6 }, + }, + other: { + "js::Shape": { bytes: 80, count: 8 }, + }, + domNode: {}, + }; + + const EXPECTED = { + name: null, + bytes: 0, + totalBytes: 220, + count: 0, + totalCount: 22, + children: [ + { + name: "js::Shape", + bytes: 80, + totalBytes: 80, + count: 8, + totalCount: 8, + children: [ + { + name: "other", + bytes: 0, + totalBytes: 80, + count: 0, + totalCount: 8, + children: [ + { + name: null, + bytes: 0, + totalBytes: 220, + count: 0, + totalCount: 22, + children: undefined, + id: 15, + parent: 14, + reportLeafIndex: undefined, + }, + ], + id: 14, + parent: 13, + reportLeafIndex: undefined, + }, + ], + id: 13, + parent: 12, + reportLeafIndex: 9, + }, + { + name: "JSAtom", + bytes: 60, + totalBytes: 60, + count: 6, + totalCount: 6, + children: [ + { + name: "strings", + bytes: 0, + totalBytes: 60, + count: 0, + totalCount: 6, + children: [ + { + name: null, + bytes: 0, + totalBytes: 220, + count: 0, + totalCount: 22, + children: undefined, + id: 18, + parent: 17, + reportLeafIndex: undefined, + }, + ], + id: 17, + parent: 16, + reportLeafIndex: undefined, + }, + ], + id: 16, + parent: 12, + reportLeafIndex: 7, + }, + { + name: "Array", + bytes: 50, + totalBytes: 50, + count: 5, + totalCount: 5, + children: [ + { + name: "objects", + bytes: 0, + totalBytes: 50, + count: 0, + totalCount: 5, + children: [ + { + name: null, + bytes: 0, + totalBytes: 220, + count: 0, + totalCount: 22, + children: undefined, + id: 21, + parent: 20, + reportLeafIndex: undefined, + }, + ], + id: 20, + parent: 19, + reportLeafIndex: undefined, + }, + ], + id: 19, + parent: 12, + reportLeafIndex: 2, + }, + { + name: "js::jit::JitScript", + bytes: 30, + totalBytes: 30, + count: 3, + totalCount: 3, + children: [ + { + name: "scripts", + bytes: 0, + totalBytes: 30, + count: 0, + totalCount: 3, + children: [ + { + name: null, + bytes: 0, + totalBytes: 220, + count: 0, + totalCount: 22, + children: undefined, + id: 24, + parent: 23, + reportLeafIndex: undefined, + }, + ], + id: 23, + parent: 22, + reportLeafIndex: undefined, + }, + ], + id: 22, + parent: 12, + reportLeafIndex: 5, + }, + ], + id: 12, + parent: undefined, + reportLeafIndex: undefined, + }; + + compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { invert: true }); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-08.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-08.js new file mode 100644 index 0000000000..0e8e2ebcc1 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-08.js @@ -0,0 +1,143 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * Test inverting CensusTreeNode with a non-allocation stack breakdown. + */ + +function run_test() { + const BREAKDOWN = { + by: "filename", + then: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + noFilename: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + }; + + const REPORT = { + "http://example.com/app.js": { + JSScript: { count: 10, bytes: 100 }, + }, + "http://example.com/ads.js": { + "js::LazyScript": { count: 20, bytes: 200 }, + }, + "http://example.com/trackers.js": { + JSScript: { count: 30, bytes: 300 }, + }, + noFilename: { + "js::jit::JitCode": { count: 40, bytes: 400 }, + }, + }; + + const EXPECTED = { + name: null, + bytes: 0, + totalBytes: 1000, + count: 0, + totalCount: 100, + children: [ + { + name: "noFilename", + bytes: 0, + totalBytes: 400, + count: 0, + totalCount: 40, + children: [ + { + name: "js::jit::JitCode", + bytes: 400, + totalBytes: 400, + count: 40, + totalCount: 40, + children: undefined, + id: 9, + parent: 8, + reportLeafIndex: 8, + }, + ], + id: 8, + parent: 1, + reportLeafIndex: undefined, + }, + { + name: "http://example.com/trackers.js", + bytes: 0, + totalBytes: 300, + count: 0, + totalCount: 30, + children: [ + { + name: "JSScript", + bytes: 300, + totalBytes: 300, + count: 30, + totalCount: 30, + children: undefined, + id: 7, + parent: 6, + reportLeafIndex: 6, + }, + ], + id: 6, + parent: 1, + reportLeafIndex: undefined, + }, + { + name: "http://example.com/ads.js", + bytes: 0, + totalBytes: 200, + count: 0, + totalCount: 20, + children: [ + { + name: "js::LazyScript", + bytes: 200, + totalBytes: 200, + count: 20, + totalCount: 20, + children: undefined, + id: 5, + parent: 4, + reportLeafIndex: 4, + }, + ], + id: 4, + parent: 1, + reportLeafIndex: undefined, + }, + { + name: "http://example.com/app.js", + bytes: 0, + totalBytes: 100, + count: 0, + totalCount: 10, + children: [ + { + name: "JSScript", + bytes: 100, + totalBytes: 100, + count: 10, + totalCount: 10, + children: undefined, + id: 3, + parent: 2, + reportLeafIndex: 2, + }, + ], + id: 2, + parent: 1, + reportLeafIndex: undefined, + }, + ], + id: 1, + parent: undefined, + reportLeafIndex: undefined, + }; + + compareCensusViewData(BREAKDOWN, REPORT, EXPECTED); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-09.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-09.js new file mode 100644 index 0000000000..5f91e5e67b --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-09.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * Test that repeatedly converting the same census report to a CensusTreeNode + * tree results in the same CensusTreeNode tree. + */ + +function run_test() { + const BREAKDOWN = { + by: "filename", + then: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + noFilename: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + }; + + const REPORT = { + "http://example.com/app.js": { + JSScript: { count: 10, bytes: 100 }, + }, + "http://example.com/ads.js": { + "js::LazyScript": { count: 20, bytes: 200 }, + }, + "http://example.com/trackers.js": { + JSScript: { count: 30, bytes: 300 }, + }, + noFilename: { + "js::jit::JitCode": { count: 40, bytes: 400 }, + }, + }; + + const first = censusReportToCensusTreeNode(BREAKDOWN, REPORT); + const second = censusReportToCensusTreeNode(BREAKDOWN, REPORT); + const third = censusReportToCensusTreeNode(BREAKDOWN, REPORT); + + assertStructurallyEquivalent(first, second); + assertStructurallyEquivalent(second, third); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-10.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-10.js new file mode 100644 index 0000000000..ba783f8e47 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census-tree-node-10.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * Test when multiple leaves in the census report map to the same node in an + * inverted CensusReportTree. + */ + +function run_test() { + const BREAKDOWN = { + by: "coarseType", + objects: { + by: "objectClass", + then: { by: "count", count: true, bytes: true }, + }, + other: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + strings: { by: "count", count: true, bytes: true }, + scripts: { by: "count", count: true, bytes: true }, + domNode: { by: "count", count: true, bytes: true }, + }; + + const REPORT = { + objects: { + Array: { count: 1, bytes: 10 }, + }, + other: { + Array: { count: 1, bytes: 10 }, + }, + strings: { count: 0, bytes: 0 }, + scripts: { count: 0, bytes: 0 }, + domNode: { count: 0, bytes: 0 }, + }; + + const node = censusReportToCensusTreeNode(BREAKDOWN, REPORT, { + invert: true, + }); + + equal(node.children[0].name, "Array"); + equal(node.children[0].reportLeafIndex.size, 2); + dumpn( + `node.children[0].reportLeafIndex = ${[ + ...node.children[0].reportLeafIndex, + ]}` + ); + ok(node.children[0].reportLeafIndex.has(2)); + ok(node.children[0].reportLeafIndex.has(6)); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_01.js new file mode 100644 index 0000000000..c90b34af70 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_01.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test diffing census reports of breakdown by "internalType". + +const BREAKDOWN = { + by: "internalType", + then: { by: "count", count: true, bytes: true }, +}; + +const REPORT1 = { + JSObject: { + count: 10, + bytes: 100, + }, + "js::Shape": { + count: 50, + bytes: 500, + }, + JSString: { + count: 0, + bytes: 0, + }, + "js::LazyScript": { + count: 1, + bytes: 10, + }, +}; + +const REPORT2 = { + JSObject: { + count: 11, + bytes: 110, + }, + "js::Shape": { + count: 51, + bytes: 510, + }, + JSString: { + count: 1, + bytes: 1, + }, + "js::BaseShape": { + count: 1, + bytes: 42, + }, +}; + +const EXPECTED = { + JSObject: { + count: 1, + bytes: 10, + }, + "js::Shape": { + count: 1, + bytes: 10, + }, + JSString: { + count: 1, + bytes: 1, + }, + "js::LazyScript": { + count: -1, + bytes: -10, + }, + "js::BaseShape": { + count: 1, + bytes: 42, + }, +}; + +function run_test() { + assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_02.js new file mode 100644 index 0000000000..cefe76abde --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_02.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test diffing census reports of breakdown by "count". + +const BREAKDOWN = { by: "count", count: true, bytes: true }; + +const REPORT1 = { + count: 10, + bytes: 100, +}; + +const REPORT2 = { + count: 11, + bytes: 110, +}; + +const EXPECTED = { + count: 1, + bytes: 10, +}; + +function run_test() { + assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_03.js new file mode 100644 index 0000000000..e625f4e0be --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_03.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test diffing census reports of breakdown by "coarseType". + +const BREAKDOWN = { + by: "coarseType", + objects: { by: "count", count: true, bytes: true }, + scripts: { by: "count", count: true, bytes: true }, + strings: { by: "count", count: true, bytes: true }, + other: { by: "count", count: true, bytes: true }, + domNode: { by: "count", count: true, bytes: true }, +}; + +const REPORT1 = { + objects: { + count: 1, + bytes: 10, + }, + scripts: { + count: 1, + bytes: 10, + }, + strings: { + count: 1, + bytes: 10, + }, + other: { + count: 3, + bytes: 30, + }, + domNode: { + count: 0, + bytes: 0, + }, +}; + +const REPORT2 = { + objects: { + count: 1, + bytes: 10, + }, + scripts: { + count: 0, + bytes: 0, + }, + strings: { + count: 2, + bytes: 20, + }, + other: { + count: 4, + bytes: 40, + }, + domNode: { + count: 0, + bytes: 0, + }, +}; + +const EXPECTED = { + objects: { + count: 0, + bytes: 0, + }, + scripts: { + count: -1, + bytes: -10, + }, + strings: { + count: 1, + bytes: 10, + }, + other: { + count: 1, + bytes: 10, + }, + domNode: { + count: 0, + bytes: 0, + }, +}; + +function run_test() { + assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_04.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_04.js new file mode 100644 index 0000000000..036a33804f --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_04.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test diffing census reports of breakdown by "objectClass". + +const BREAKDOWN = { + by: "objectClass", + then: { by: "count", count: true, bytes: true }, + other: { by: "count", count: true, bytes: true }, +}; + +const REPORT1 = { + Array: { + count: 1, + bytes: 100, + }, + Function: { + count: 10, + bytes: 10, + }, + other: { + count: 10, + bytes: 100, + }, +}; + +const REPORT2 = { + Object: { + count: 1, + bytes: 100, + }, + Function: { + count: 20, + bytes: 20, + }, + other: { + count: 10, + bytes: 100, + }, +}; + +const EXPECTED = { + Array: { + count: -1, + bytes: -100, + }, + Function: { + count: 10, + bytes: 10, + }, + other: { + count: 0, + bytes: 0, + }, + Object: { + count: 1, + bytes: 100, + }, +}; + +function run_test() { + assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_05.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_05.js new file mode 100644 index 0000000000..1ce0cfeb5b --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_05.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test diffing census reports of breakdown by "allocationStack". + +const BREAKDOWN = { + by: "allocationStack", + then: { by: "count", count: true, bytes: true }, + noStack: { by: "count", count: true, bytes: true }, +}; + +const stack1 = saveStack(); +const stack2 = saveStack(); +const stack3 = saveStack(); + +const REPORT1 = new Map([ + [stack1, { count: 10, bytes: 100 }], + [stack2, { count: 1, bytes: 10 }], +]); + +const REPORT2 = new Map([ + [stack2, { count: 10, bytes: 100 }], + [stack3, { count: 1, bytes: 10 }], +]); + +const EXPECTED = new Map([ + [stack1, { count: -10, bytes: -100 }], + [stack2, { count: 9, bytes: 90 }], + [stack3, { count: 1, bytes: 10 }], +]); + +function run_test() { + assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_06.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_06.js new file mode 100644 index 0000000000..b805de48dd --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_diff_06.js @@ -0,0 +1,139 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test diffing census reports of a "complex" and "realistic" breakdown. + +const BREAKDOWN = { + by: "coarseType", + objects: { + by: "allocationStack", + then: { + by: "objectClass", + then: { by: "count", count: false, bytes: true }, + other: { by: "count", count: false, bytes: true }, + }, + noStack: { + by: "objectClass", + then: { by: "count", count: false, bytes: true }, + other: { by: "count", count: false, bytes: true }, + }, + }, + strings: { + by: "internalType", + then: { by: "count", count: false, bytes: true }, + }, + scripts: { + by: "internalType", + then: { by: "count", count: false, bytes: true }, + }, + other: { + by: "internalType", + then: { by: "count", count: false, bytes: true }, + }, + domNode: { + by: "internalType", + then: { by: "count", count: false, bytes: true }, + }, +}; + +const stack1 = saveStack(); +const stack2 = saveStack(); +const stack3 = saveStack(); + +const REPORT1 = { + objects: new Map([ + [ + stack1, + { Function: { bytes: 1 }, Object: { bytes: 2 }, other: { bytes: 0 } }, + ], + [stack2, { Array: { bytes: 3 }, Date: { bytes: 4 }, other: { bytes: 0 } }], + ["noStack", { Object: { bytes: 3 } }], + ]), + strings: { + JSAtom: { bytes: 10 }, + JSLinearString: { bytes: 5 }, + }, + scripts: { + JSScript: { bytes: 1 }, + "js::jit::JitCode": { bytes: 2 }, + }, + other: { + "mozilla::dom::Thing": { bytes: 1 }, + }, + domNode: {}, +}; + +const REPORT2 = { + objects: new Map([ + [stack2, { Array: { bytes: 1 }, Date: { bytes: 2 }, other: { bytes: 3 } }], + [ + stack3, + { Function: { bytes: 1 }, Object: { bytes: 2 }, other: { bytes: 0 } }, + ], + ["noStack", { Object: { bytes: 3 } }], + ]), + strings: { + JSAtom: { bytes: 5 }, + JSLinearString: { bytes: 10 }, + }, + scripts: { + JSScript: { bytes: 2 }, + "js::LazyScript": { bytes: 42 }, + "js::jit::JitCode": { bytes: 1 }, + }, + other: { + "mozilla::dom::OtherThing": { bytes: 1 }, + }, + domNode: {}, +}; + +const EXPECTED = { + objects: new Map([ + [ + stack1, + { Function: { bytes: -1 }, Object: { bytes: -2 }, other: { bytes: 0 } }, + ], + [ + stack2, + { Array: { bytes: -2 }, Date: { bytes: -2 }, other: { bytes: 3 } }, + ], + [ + stack3, + { Function: { bytes: 1 }, Object: { bytes: 2 }, other: { bytes: 0 } }, + ], + ["noStack", { Object: { bytes: 0 } }], + ]), + scripts: { + JSScript: { + bytes: 1, + }, + "js::jit::JitCode": { + bytes: -1, + }, + "js::LazyScript": { + bytes: 42, + }, + }, + strings: { + JSAtom: { + bytes: -5, + }, + JSLinearString: { + bytes: 5, + }, + }, + other: { + "mozilla::dom::Thing": { + bytes: -1, + }, + "mozilla::dom::OtherThing": { + bytes: 1, + }, + }, + domNode: {}, +}; + +function run_test() { + assertDiff(BREAKDOWN, REPORT1, REPORT2, EXPECTED); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_01.js new file mode 100644 index 0000000000..961023b2c2 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_01.js @@ -0,0 +1,111 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test filtering basic CensusTreeNode trees. + +function run_test() { + const BREAKDOWN = { + by: "coarseType", + objects: { + by: "objectClass", + then: { by: "count", count: true, bytes: true }, + other: { by: "count", count: true, bytes: true }, + }, + scripts: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + strings: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + other: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + domNode: { + by: "descriptiveType", + then: { by: "count", count: true, bytes: true }, + }, + }; + + const REPORT = { + objects: { + Array: { bytes: 50, count: 5 }, + UInt8Array: { bytes: 80, count: 8 }, + Int32Array: { bytes: 320, count: 32 }, + other: { bytes: 0, count: 0 }, + }, + scripts: { + "js::jit::JitScript": { bytes: 30, count: 3 }, + }, + strings: { + JSAtom: { bytes: 60, count: 6 }, + }, + other: { + "js::Shape": { bytes: 80, count: 8 }, + }, + domNode: {}, + }; + + const EXPECTED = { + name: null, + bytes: 0, + totalBytes: 620, + count: 0, + totalCount: 62, + children: [ + { + name: "objects", + bytes: 0, + totalBytes: 450, + count: 0, + totalCount: 45, + children: [ + { + name: "Int32Array", + bytes: 320, + totalBytes: 320, + count: 32, + totalCount: 32, + children: undefined, + id: 16, + parent: 15, + reportLeafIndex: 4, + }, + { + name: "UInt8Array", + bytes: 80, + totalBytes: 80, + count: 8, + totalCount: 8, + children: undefined, + id: 17, + parent: 15, + reportLeafIndex: 3, + }, + { + name: "Array", + bytes: 50, + totalBytes: 50, + count: 5, + totalCount: 5, + children: undefined, + id: 18, + parent: 15, + reportLeafIndex: 2, + }, + ], + id: 15, + parent: 14, + reportLeafIndex: undefined, + }, + ], + id: 14, + parent: undefined, + reportLeafIndex: undefined, + }; + + compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { filter: "Array" }); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_02.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_02.js new file mode 100644 index 0000000000..9915acb678 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_02.js @@ -0,0 +1,125 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test filtering CensusTreeNode trees with an `allocationStack` breakdown. + +function run_test() { + const countBreakdown = { by: "count", count: true, bytes: true }; + + const BREAKDOWN = { + by: "allocationStack", + then: countBreakdown, + noStack: countBreakdown, + }; + + let stack1, stack2, stack3, stack4; + + (function foo() { + (function bar() { + (function baz() { + stack1 = saveStack(3); + })(); + (function quux() { + stack2 = saveStack(3); + stack3 = saveStack(3); + })(); + })(); + stack4 = saveStack(2); + })(); + + const stack5 = saveStack(1); + + const REPORT = new Map([ + [stack1, { bytes: 10, count: 1 }], + [stack2, { bytes: 20, count: 2 }], + [stack3, { bytes: 30, count: 3 }], + [stack4, { bytes: 40, count: 4 }], + [stack5, { bytes: 50, count: 5 }], + ["noStack", { bytes: 60, count: 6 }], + ]); + + const EXPECTED = { + name: null, + bytes: 0, + totalBytes: 210, + count: 0, + totalCount: 21, + children: [ + { + name: stack1.parent.parent, + bytes: 0, + totalBytes: 60, + count: 0, + totalCount: 6, + children: [ + { + name: stack2.parent, + bytes: 0, + totalBytes: 50, + count: 0, + totalCount: 5, + children: [ + { + name: stack3, + bytes: 30, + totalBytes: 30, + count: 3, + totalCount: 3, + children: undefined, + id: 15, + parent: 14, + reportLeafIndex: 3, + }, + { + name: stack2, + bytes: 20, + totalBytes: 20, + count: 2, + totalCount: 2, + children: undefined, + id: 16, + parent: 14, + reportLeafIndex: 2, + }, + ], + id: 14, + parent: 13, + reportLeafIndex: undefined, + }, + { + name: stack1.parent, + bytes: 0, + totalBytes: 10, + count: 0, + totalCount: 1, + children: [ + { + name: stack1, + bytes: 10, + totalBytes: 10, + count: 1, + totalCount: 1, + children: undefined, + id: 18, + parent: 17, + reportLeafIndex: 1, + }, + ], + id: 17, + parent: 13, + reportLeafIndex: undefined, + }, + ], + id: 13, + parent: 12, + reportLeafIndex: undefined, + }, + ], + id: 12, + parent: undefined, + reportLeafIndex: undefined, + }; + + compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { filter: "bar" }); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_03.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_03.js new file mode 100644 index 0000000000..dc144bac2e --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_03.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test filtering with no matches. + +function run_test() { + const BREAKDOWN = { + by: "coarseType", + objects: { + by: "objectClass", + then: { by: "count", count: true, bytes: true }, + other: { by: "count", count: true, bytes: true }, + }, + scripts: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + strings: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + other: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + domNode: { + by: "descriptiveType", + then: { by: "count", count: true, bytes: true }, + }, + }; + + const REPORT = { + objects: { + Array: { bytes: 50, count: 5 }, + UInt8Array: { bytes: 80, count: 8 }, + Int32Array: { bytes: 320, count: 32 }, + other: { bytes: 0, count: 0 }, + }, + scripts: { + "js::jit::JitScript": { bytes: 30, count: 3 }, + }, + strings: { + JSAtom: { bytes: 60, count: 6 }, + }, + other: { + "js::Shape": { bytes: 80, count: 8 }, + }, + domNode: {}, + }; + + const EXPECTED = { + name: null, + bytes: 0, + totalBytes: 620, + count: 0, + totalCount: 62, + children: undefined, + id: 14, + parent: undefined, + reportLeafIndex: undefined, + }; + + compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { + filter: "zzzzzzzzzzzzzzzzzzzz", + }); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_04.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_04.js new file mode 100644 index 0000000000..7e37b2c5da --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_04.js @@ -0,0 +1,105 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test the filtered nodes' counts and bytes are the same as they were when +// unfiltered. + +function run_test() { + const COUNT = { by: "count", count: true, bytes: true }; + const INTERNAL_TYPE = { by: "internalType", then: COUNT }; + + const BREAKDOWN = { + by: "coarseType", + objects: { by: "objectClass", then: COUNT, other: COUNT }, + strings: COUNT, + scripts: { + by: "filename", + then: INTERNAL_TYPE, + noFilename: INTERNAL_TYPE, + }, + other: INTERNAL_TYPE, + domNode: { by: "descriptiveType", then: COUNT, other: COUNT }, + }; + + const REPORT = { + objects: { + Function: { + count: 7, + bytes: 70, + }, + Array: { + count: 6, + bytes: 60, + }, + }, + scripts: { + "http://mozilla.github.io/pdf.js/build/pdf.js": { + "js::LazyScript": { + count: 4, + bytes: 40, + }, + }, + }, + strings: { + count: 2, + bytes: 20, + }, + other: { + "js::Shape": { + count: 1, + bytes: 10, + }, + }, + domNode: {}, + }; + + const EXPECTED = { + name: null, + bytes: 0, + totalBytes: 200, + count: 0, + totalCount: 20, + parent: undefined, + children: [ + { + name: "objects", + bytes: 0, + totalBytes: 130, + count: 0, + totalCount: 13, + children: [ + { + name: "Function", + bytes: 70, + totalBytes: 70, + count: 7, + totalCount: 7, + id: 14, + parent: 13, + children: undefined, + reportLeafIndex: 2, + }, + { + name: "Array", + bytes: 60, + totalBytes: 60, + count: 6, + totalCount: 6, + id: 15, + parent: 13, + children: undefined, + reportLeafIndex: 3, + }, + ], + id: 13, + parent: 12, + reportLeafIndex: undefined, + }, + ], + id: 12, + reportLeafIndex: undefined, + }; + + compareCensusViewData(BREAKDOWN, REPORT, EXPECTED, { filter: "objects" }); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_05.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_05.js new file mode 100644 index 0000000000..3202a4b39f --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_census_filtering_05.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that filtered and inverted allocation stack census trees are sorted +// properly. + +function run_test() { + const countBreakdown = { by: "count", count: true, bytes: true }; + + const BREAKDOWN = { + by: "allocationStack", + then: countBreakdown, + noStack: countBreakdown, + }; + + const stacks = []; + + function foo(depth = 1) { + stacks.push(saveStack(depth)); + bar(depth + 1); + baz(depth + 1); + stacks.push(saveStack(depth)); + } + + function bar(depth = 1) { + stacks.push(saveStack(depth)); + stacks.push(saveStack(depth)); + } + + function baz(depth = 1) { + stacks.push(saveStack(depth)); + bang(depth + 1); + stacks.push(saveStack(depth)); + } + + function bang(depth = 1) { + stacks.push(saveStack(depth)); + stacks.push(saveStack(depth)); + stacks.push(saveStack(depth)); + } + + foo(); + bar(); + baz(); + bang(); + + const REPORT = new Map( + stacks.map((s, i) => { + return [ + s, + { + count: i + 1, + bytes: (i + 1) * 10, + }, + ]; + }) + ); + + const tree = censusReportToCensusTreeNode(BREAKDOWN, REPORT, { + filter: "baz", + invert: true, + }); + + dumpn("tree = " + JSON.stringify(tree, savedFrameReplacer, 4)); + + (function assertSortedBySelf(node) { + if (node.children) { + let lastSelfBytes = Infinity; + for (const child of node.children) { + ok(child.bytes <= lastSelfBytes, `${child.bytes} <= ${lastSelfBytes}`); + lastSelfBytes = child.bytes; + assertSortedBySelf(child); + } + } + })(tree); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_countToBucketBreakdown_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_countToBucketBreakdown_01.js new file mode 100644 index 0000000000..e89048c333 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_countToBucketBreakdown_01.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that we can turn a breakdown with { by: "count" } leaves into a +// breakdown with { by: "bucket" } leaves. + +const COUNT = { by: "count", count: true, bytes: true }; +const BUCKET = { by: "bucket" }; + +const BREAKDOWN = { + by: "coarseType", + objects: { by: "objectClass", then: COUNT, other: COUNT }, + strings: COUNT, + scripts: { + by: "filename", + then: { by: "internalType", then: COUNT }, + noFilename: { by: "internalType", then: COUNT }, + }, + other: { by: "internalType", then: COUNT }, +}; + +const EXPECTED = { + by: "coarseType", + objects: { by: "objectClass", then: BUCKET, other: BUCKET }, + strings: BUCKET, + scripts: { + by: "filename", + then: { by: "internalType", then: BUCKET }, + noFilename: { by: "internalType", then: BUCKET }, + }, + other: { by: "internalType", then: BUCKET }, +}; + +function run_test() { + assertCountToBucketBreakdown(BREAKDOWN, EXPECTED); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_deduplicatePaths_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_deduplicatePaths_01.js new file mode 100644 index 0000000000..fc2864f5f4 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_deduplicatePaths_01.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test the behavior of the deduplicatePaths utility function. + +function edge(from, to, name) { + return { from, to, name }; +} + +function run_test() { + const a = 1; + const b = 2; + const c = 3; + const d = 4; + const e = 5; + const f = 6; + const g = 7; + + dumpn("Single long path"); + assertDeduplicatedPaths({ + target: g, + paths: [ + [ + pathEntry(a, "e1"), + pathEntry(b, "e2"), + pathEntry(c, "e3"), + pathEntry(d, "e4"), + pathEntry(e, "e5"), + pathEntry(f, "e6"), + ], + ], + expectedNodes: [a, b, c, d, e, f, g], + expectedEdges: [ + edge(a, b, "e1"), + edge(b, c, "e2"), + edge(c, d, "e3"), + edge(d, e, "e4"), + edge(e, f, "e5"), + edge(f, g, "e6"), + ], + }); + + dumpn("Multiple edges from and to the same nodes"); + assertDeduplicatedPaths({ + target: a, + paths: [[pathEntry(b, "x")], [pathEntry(b, "y")], [pathEntry(b, "z")]], + expectedNodes: [a, b], + expectedEdges: [edge(b, a, "x"), edge(b, a, "y"), edge(b, a, "z")], + }); + + dumpn("Multiple paths sharing some nodes and edges"); + assertDeduplicatedPaths({ + target: g, + paths: [ + [pathEntry(a, "a->b"), pathEntry(b, "b->c"), pathEntry(c, "foo")], + [pathEntry(a, "a->b"), pathEntry(b, "b->d"), pathEntry(d, "bar")], + [pathEntry(a, "a->b"), pathEntry(b, "b->e"), pathEntry(e, "baz")], + ], + expectedNodes: [a, b, c, d, e, g], + expectedEdges: [ + edge(a, b, "a->b"), + edge(b, c, "b->c"), + edge(b, d, "b->d"), + edge(b, e, "b->e"), + edge(c, g, "foo"), + edge(d, g, "bar"), + edge(e, g, "baz"), + ], + }); + + dumpn("Second shortest path contains target itself"); + assertDeduplicatedPaths({ + target: g, + paths: [ + [pathEntry(a, "a->b"), pathEntry(b, "b->g")], + [ + pathEntry(a, "a->b"), + pathEntry(b, "b->g"), + pathEntry(g, "g->f"), + pathEntry(f, "f->g"), + ], + ], + expectedNodes: [a, b, g], + expectedEdges: [edge(a, b, "a->b"), edge(b, g, "b->g")], + }); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_getCensusIndividuals_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_getCensusIndividuals_01.js new file mode 100644 index 0000000000..6963f3f33a --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_getCensusIndividuals_01.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test basic functionality of `CensusUtils.getCensusIndividuals`. + +function run_test() { + const stack1 = saveStack(1); + const stack2 = saveStack(1); + const stack3 = saveStack(1); + + const COUNT = { by: "count", count: true, bytes: true }; + const INTERNAL_TYPE = { by: "internalType", then: COUNT }; + + const BREAKDOWN = { + by: "allocationStack", + then: INTERNAL_TYPE, + noStack: INTERNAL_TYPE, + }; + + const MOCK_SNAPSHOT = { + takeCensus: ({ breakdown }) => { + assertStructurallyEquivalent( + breakdown, + CensusUtils.countToBucketBreakdown(BREAKDOWN) + ); + + // DFS Index + // prettier-ignore + return new Map([ // 0 + [stack1, { // 1 + JSObject: [101, 102, 103], // 2 + JSString: [111, 112, 113], // 3 + }], + [stack2, { // 4 + JSObject: [201, 202, 203], // 5 + JSString: [211, 212, 213], // 6 + }], + [stack3, { // 7 + JSObject: [301, 302, 303], // 8 + JSString: [311, 312, 313], // 9 + }], + ["noStack", { // 10 + JSObject: [401, 402, 403], // 11 + JSString: [411, 412, 413], // 12 + }], + ]); + }, + }; + + const INDICES = new Set([3, 5, 9]); + + const EXPECTED = new Set([111, 112, 113, 201, 202, 203, 311, 312, 313]); + + const actual = new Set( + CensusUtils.getCensusIndividuals(INDICES, BREAKDOWN, MOCK_SNAPSHOT) + ); + + assertStructurallyEquivalent(EXPECTED, actual); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_getReportLeaves_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_getReportLeaves_01.js new file mode 100644 index 0000000000..04aae90e99 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_getReportLeaves_01.js @@ -0,0 +1,135 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test basic functionality of `CensusUtils.getReportLeaves`. + +function run_test() { + const BREAKDOWN = { + by: "coarseType", + objects: { + by: "objectClass", + then: { by: "count", count: true, bytes: true }, + other: { by: "count", count: true, bytes: true }, + }, + strings: { by: "count", count: true, bytes: true }, + scripts: { + by: "filename", + then: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + noFilename: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + }, + other: { + by: "internalType", + then: { by: "count", count: true, bytes: true }, + }, + domNode: { + by: "descriptiveType", + then: { by: "count", count: true, bytes: true }, + }, + }; + + const REPORT = { + objects: { + Array: { count: 6, bytes: 60 }, + Function: { count: 1, bytes: 10 }, + Object: { count: 1, bytes: 10 }, + RegExp: { count: 1, bytes: 10 }, + other: { count: 0, bytes: 0 }, + }, + strings: { count: 1, bytes: 10 }, + scripts: { + "foo.js": { + JSScript: { count: 1, bytes: 10 }, + "js::jit::IonScript": { count: 1, bytes: 10 }, + }, + noFilename: { + JSScript: { count: 1, bytes: 10 }, + "js::jit::IonScript": { count: 1, bytes: 10 }, + }, + }, + other: { + "js::Shape": { count: 7, bytes: 70 }, + "js::BaseShape": { count: 1, bytes: 10 }, + }, + domNode: {}, + }; + + const root = censusReportToCensusTreeNode(BREAKDOWN, REPORT); + dumpn("CensusTreeNode tree = " + JSON.stringify(root, null, 4)); + + (function assertEveryNodeCanFindItsLeaf(node) { + if (node.reportLeafIndex) { + const [leaf] = CensusUtils.getReportLeaves( + new Set([node.reportLeafIndex]), + BREAKDOWN, + REPORT + ); + ok( + leaf, + "Should be able to find leaf " + + "for a node with a reportLeafIndex = " + + node.reportLeafIndex + ); + } + + if (node.children) { + for (const child of node.children) { + assertEveryNodeCanFindItsLeaf(child); + } + } + })(root); + + // Test finding multiple leaves at a time. + + function find(name, node) { + if (node.name === name) { + return node; + } + + if (node.children) { + for (const child of node.children) { + const found = find(name, child); + if (found) { + return found; + } + } + } + + return undefined; + } + + const arrayNode = find("Array", root); + ok(arrayNode); + equal(typeof arrayNode.reportLeafIndex, "number"); + + const shapeNode = find("js::Shape", root); + ok(shapeNode); + equal(typeof shapeNode.reportLeafIndex, "number"); + + const indices = new Set([ + arrayNode.reportLeafIndex, + shapeNode.reportLeafIndex, + ]); + const leaves = CensusUtils.getReportLeaves(indices, BREAKDOWN, REPORT); + equal(leaves.length, 2); + + // `getReportLeaves` does not guarantee order of the results, so handle both + // cases. + ok(leaves.some(l => l === REPORT.objects.Array)); + ok(leaves.some(l => l === REPORT.other["js::Shape"])); + + // Test that bad indices do not yield results. + + const none = CensusUtils.getReportLeaves( + new Set([999999999999]), + BREAKDOWN, + REPORT + ); + equal(none.length, 0); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/test_saveHeapSnapshot_e10s_01.js b/devtools/shared/heapsnapshot/tests/xpcshell/test_saveHeapSnapshot_e10s_01.js new file mode 100644 index 0000000000..0a46618003 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/test_saveHeapSnapshot_e10s_01.js @@ -0,0 +1,9 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test saving a heap snapshot in the sandboxed e10s child process. + +function run_test() { + run_test_in_child("test_SaveHeapSnapshot.js"); +} diff --git a/devtools/shared/heapsnapshot/tests/xpcshell/xpcshell.ini b/devtools/shared/heapsnapshot/tests/xpcshell/xpcshell.ini new file mode 100644 index 0000000000..fca04273ea --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/xpcshell/xpcshell.ini @@ -0,0 +1,100 @@ +[DEFAULT] +tags = devtools heapsnapshot devtools-memory +head = head_heapsnapshot.js +firefox-appdir = browser +skip-if = toolkit == 'android' + +support-files = + Census.sys.mjs + dominator-tree-worker.js + heap-snapshot-worker.js + Match.sys.mjs + +[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] +skip-if = tsan # Unreasonably slow, bug 1612707 +[test_HeapAnalyses_readHeapSnapshot_01.js] +[test_HeapAnalyses_takeCensusDiff_01.js] +[test_HeapAnalyses_takeCensusDiff_02.js] +[test_HeapAnalyses_takeCensus_01.js] +[test_HeapAnalyses_takeCensus_02.js] +[test_HeapAnalyses_takeCensus_03.js] +[test_HeapAnalyses_takeCensus_04.js] +[test_HeapAnalyses_takeCensus_05.js] +[test_HeapAnalyses_takeCensus_06.js] +[test_HeapAnalyses_takeCensus_07.js] +[test_HeapSnapshot_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_getObjectNodeId_01.js] +[test_HeapSnapshot_takeCensus_01.js] +[test_HeapSnapshot_takeCensus_02.js] +[test_HeapSnapshot_takeCensus_03.js] +[test_HeapSnapshot_takeCensus_04.js] +[test_HeapSnapshot_takeCensus_05.js] +[test_HeapSnapshot_takeCensus_06.js] +[test_HeapSnapshot_takeCensus_07.js] +[test_HeapSnapshot_takeCensus_08.js] +[test_HeapSnapshot_takeCensus_09.js] +[test_HeapSnapshot_takeCensus_10.js] +[test_HeapSnapshot_takeCensus_11.js] +[test_HeapSnapshot_takeCensus_12.js] +[test_ReadHeapSnapshot.js] +[test_ReadHeapSnapshot_with_utf8_paths.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] |