diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /devtools/client/memory/test/browser | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/memory/test/browser')
25 files changed, 2147 insertions, 0 deletions
diff --git a/devtools/client/memory/test/browser/browser.ini b/devtools/client/memory/test/browser/browser.ini new file mode 100644 index 0000000000..0d2e770ec5 --- /dev/null +++ b/devtools/client/memory/test/browser/browser.ini @@ -0,0 +1,37 @@ +[DEFAULT] +tags = devtools devtools-memory +subsuite = devtools +support-files = + head.js + doc_big_tree.html + doc_empty.html + doc_steady_allocation.html + !/devtools/client/shared/test/shared-head.js + !/devtools/client/shared/test/telemetry-test-helpers.js + +[browser_memory_allocationStackDisplay_01.js] +skip-if = debug # bug 1219554 +[browser_memory_allocationStackDisplay_02.js] +skip-if = debug # bug 1219554 +[browser_memory_displays_01.js] +[browser_memory_clear_snapshots.js] +[browser_memory_diff_01.js] +[browser_memory_dominator_trees_01.js] +skip-if = ccov # bug 1347244 +[browser_memory_dominator_trees_02.js] +skip-if = ccov # bug 1347244 +[browser_memory_filter_01.js] +skip-if = ccov # bug 1347244 +[browser_memory_fission_switch_target.js] +[browser_memory_individuals_01.js] +[browser_memory_keyboard.js] +[browser_memory_keyboard-snapshot-list.js] +[browser_memory_no_allocation_stacks.js] +[browser_memory_no_auto_expand.js] +skip-if = debug # bug 1219554 +[browser_memory_percents_01.js] +[browser_memory_refresh_does_not_leak.js] +[browser_memory_simple_01.js] +[browser_memory_transferHeapSnapshot_e10s_01.js] +[browser_memory_tree_map-01.js] +[browser_memory_tree_map-02.js] diff --git a/devtools/client/memory/test/browser/browser_memory_allocationStackDisplay_01.js b/devtools/client/memory/test/browser/browser_memory_allocationStackDisplay_01.js new file mode 100644 index 0000000000..b526025b17 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_allocationStackDisplay_01.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Sanity test that we can show allocation stack displays in the tree. + +"use strict"; + +const { + toggleRecordingAllocationStacks, +} = require("resource://devtools/client/memory/actions/allocations.js"); +const { + takeSnapshotAndCensus, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const censusDisplayActions = require("resource://devtools/client/memory/actions/census-display.js"); +const { viewState } = require("resource://devtools/client/memory/constants.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +const TEST_URL = + "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html"; + +this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) { + const heapWorker = panel.panelWin.gHeapAnalysesClient; + const { getState, dispatch } = panel.panelWin.gStore; + const front = getState().front; + const doc = panel.panelWin.document; + + dispatch(changeView(viewState.CENSUS)); + + dispatch( + censusDisplayActions.setCensusDisplay( + censusDisplays.invertedAllocationStack + ) + ); + is(getState().censusDisplay.breakdown.by, "allocationStack"); + + await dispatch(toggleRecordingAllocationStacks(panel._commands)); + ok(getState().allocations.recording); + + // Let some allocations build up. + await waitForTime(500); + + await dispatch(takeSnapshotAndCensus(front, heapWorker)); + + const names = [...doc.querySelectorAll(".frame-link-function-display-name")]; + ok(names.length, "Should have rendered some allocation stack tree items"); + ok( + names.some(e => !!e.textContent.trim()), + "And at least some of them should have functionDisplayNames" + ); +}); diff --git a/devtools/client/memory/test/browser/browser_memory_allocationStackDisplay_02.js b/devtools/client/memory/test/browser/browser_memory_allocationStackDisplay_02.js new file mode 100644 index 0000000000..34492187a2 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_allocationStackDisplay_02.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Sanity test that we can show allocation stack work when loading a new page + +"use strict"; + +const { + toggleRecordingAllocationStacks, +} = require("resource://devtools/client/memory/actions/allocations.js"); +const { + takeSnapshotAndCensus, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const censusDisplayActions = require("resource://devtools/client/memory/actions/census-display.js"); +const { viewState } = require("resource://devtools/client/memory/constants.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +const TEST_URL = + "https://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html"; + +this.test = makeMemoryTest("about:blank", async function ({ tab, panel }) { + const heapWorker = panel.panelWin.gHeapAnalysesClient; + const { getState, dispatch } = panel.panelWin.gStore; + const doc = panel.panelWin.document; + + dispatch(changeView(viewState.CENSUS)); + + dispatch( + censusDisplayActions.setCensusDisplay( + censusDisplays.invertedAllocationStack + ) + ); + is(getState().censusDisplay.breakdown.by, "allocationStack"); + + await dispatch(toggleRecordingAllocationStacks(panel._commands)); + ok(getState().allocations.recording); + + await navigateTo(TEST_URL); + + const front = getState().front; + await dispatch(takeSnapshotAndCensus(front, heapWorker)); + + const names = [...doc.querySelectorAll(".frame-link-function-display-name")]; + ok(names.length, "Should have rendered some allocation stack tree items"); + ok( + names.some(e => !!e.textContent.trim()), + "And at least some of them should have functionDisplayNames" + ); +}); diff --git a/devtools/client/memory/test/browser/browser_memory_clear_snapshots.js b/devtools/client/memory/test/browser/browser_memory_clear_snapshots.js new file mode 100644 index 0000000000..52eecd2838 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_clear_snapshots.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests taking and then clearing snapshots. + */ + +const { + treeMapState, +} = require("resource://devtools/client/memory/constants.js"); +const TEST_URL = + "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html"; + +this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) { + const { gStore, document } = panel.panelWin; + const { getState } = gStore; + + let snapshotEls = document.querySelectorAll( + "#memory-tool-container .list li" + ); + is(getState().snapshots.length, 0, "Starts with no snapshots in store"); + is(snapshotEls.length, 0, "No snapshots visible"); + + info("Take two snapshots"); + takeSnapshot(panel.panelWin); + takeSnapshot(panel.panelWin); + takeSnapshot(panel.panelWin); + await waitUntilState( + gStore, + state => + state.snapshots.length === 3 && + state.snapshots[0].treeMap && + state.snapshots[1].treeMap && + state.snapshots[2].treeMap && + state.snapshots[0].treeMap.state === treeMapState.SAVED && + state.snapshots[1].treeMap.state === treeMapState.SAVED && + state.snapshots[2].treeMap.state === treeMapState.SAVED + ); + + snapshotEls = document.querySelectorAll("#memory-tool-container .list li"); + is(snapshotEls.length, 3, "Three snapshots visible"); + is( + document.querySelectorAll(".selected").length, + 1, + "One selected snapshot visible" + ); + ok(snapshotEls[2].classList.contains("selected"), "Third snapshot selected"); + + info("Clicking on first snapshot delete button"); + document.querySelectorAll(".delete")[0].click(); + + await waitUntilState( + gStore, + state => + state.snapshots.length === 2 && + state.snapshots[0].treeMap && + state.snapshots[1].treeMap && + state.snapshots[0].treeMap.state === treeMapState.SAVED && + state.snapshots[1].treeMap.state === treeMapState.SAVED + ); + + snapshotEls = document.querySelectorAll(".snapshot-list-item"); + is(snapshotEls.length, 2, "Two snapshots visible"); + // Bug 1476289 + ok( + !snapshotEls[0].classList.contains("selected"), + "First snapshot not selected" + ); + ok(snapshotEls[1].classList.contains("selected"), "Second snapshot selected"); + + info("Click on Clear Snapshots"); + await clearSnapshots(panel.panelWin); + is(getState().snapshots.length, 0, "No snapshots in store"); + snapshotEls = document.querySelectorAll("#memory-tool-container .list li"); + is(snapshotEls.length, 0, "No snapshot visible"); +}); diff --git a/devtools/client/memory/test/browser/browser_memory_diff_01.js b/devtools/client/memory/test/browser/browser_memory_diff_01.js new file mode 100644 index 0000000000..7b6487565f --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_diff_01.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test diffing. + +"use strict"; + +const { + diffingState, + treeMapState, +} = require("resource://devtools/client/memory/constants.js"); + +const TEST_URL = + "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html"; + +this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) { + const store = panel.panelWin.gStore; + const { getState } = store; + const doc = panel.panelWin.document; + + ok(!getState().diffing, "Not diffing by default."); + + // Take two snapshots. + const takeSnapshotButton = doc.getElementById("take-snapshot"); + EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin); + await waitForTime(1000); + EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin); + + // Enable diffing mode. + const diffButton = doc.getElementById("diff-snapshots"); + EventUtils.synthesizeMouseAtCenter(diffButton, {}, panel.panelWin); + await waitUntilState( + store, + state => !!state.diffing && state.diffing.state === diffingState.SELECTING + ); + ok(true, "Clicking the diffing button put us into the diffing state."); + is(getDisplayedSnapshotStatus(doc), "Select the baseline snapshot"); + + await waitUntilState( + store, + state => + state.snapshots.length === 2 && + state.snapshots[0].treeMap && + state.snapshots[1].treeMap && + state.snapshots[0].treeMap.state === treeMapState.SAVED && + state.snapshots[1].treeMap.state === treeMapState.SAVED + ); + + const listItems = [...doc.querySelectorAll(".snapshot-list-item")]; + is(listItems.length, 2, "Should have two snapshot list items"); + + // Select the first snapshot. + EventUtils.synthesizeMouseAtCenter(listItems[0], {}, panel.panelWin); + await waitUntilState( + store, + state => + state.diffing.state === diffingState.SELECTING && + state.diffing.firstSnapshotId + ); + is( + getDisplayedSnapshotStatus(doc), + "Select the snapshot to compare to the baseline" + ); + + // Select the second snapshot. + EventUtils.synthesizeMouseAtCenter(listItems[1], {}, panel.panelWin); + await waitUntilState( + store, + state => state.diffing.state === diffingState.TAKING_DIFF + ); + ok(true, "Selecting two snapshots for diffing triggers computing the diff"); + + // .startsWith because the ellipsis is lost in translation. + ok(getDisplayedSnapshotStatus(doc).startsWith("Computing difference")); + + await waitUntilState( + store, + state => state.diffing.state === diffingState.TOOK_DIFF + ); + ok(true, "And that diff is computed successfully"); + is(getDisplayedSnapshotStatus(doc), null, "No status text anymore"); + ok( + doc.querySelector(".heap-tree-item"), + "And instead we should be showing the tree" + ); +}); diff --git a/devtools/client/memory/test/browser/browser_memory_displays_01.js b/devtools/client/memory/test/browser/browser_memory_displays_01.js new file mode 100644 index 0000000000..89296b77f4 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_displays_01.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * Tests that the heap tree renders rows based on the display + */ + +const TEST_URL = + "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html"; +const { viewState } = require("resource://devtools/client/memory/constants.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) { + const { gStore, document } = panel.panelWin; + + const { dispatch } = panel.panelWin.gStore; + + function $$(selector) { + return [...document.querySelectorAll(selector)]; + } + dispatch(changeView(viewState.CENSUS)); + + await takeSnapshot(panel.panelWin); + + await waitUntilState( + gStore, + state => + state.snapshots[0].census && + state.snapshots[0].census.state === censusState.SAVED + ); + + info("Check coarse type heap view"); + + ["Function", "js::PropMap", "Object", "strings"].forEach(findNameCell); + + await setCensusDisplay(panel.panelWin, censusDisplays.allocationStack); + info("Check allocation stack heap view"); + [L10N.getStr("tree-item.nostack")].forEach(findNameCell); + + function findNameCell(name) { + const el = $$(".tree .heap-tree-item-name").find( + e => e.textContent === name + ); + ok(el, `Found heap tree item cell for ${name}.`); + } +}); diff --git a/devtools/client/memory/test/browser/browser_memory_dominator_trees_01.js b/devtools/client/memory/test/browser/browser_memory_dominator_trees_01.js new file mode 100644 index 0000000000..1faaf365a1 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_dominator_trees_01.js @@ -0,0 +1,178 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Sanity test for dominator trees, their focused nodes, and keyboard navigating +// through nodes across incrementally fetching subtrees. + +"use strict"; + +const { + dominatorTreeState, + viewState, +} = require("resource://devtools/client/memory/constants.js"); +const { + expandDominatorTreeNode, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +const TEST_URL = + "http://example.com/browser/devtools/client/memory/test/browser/doc_big_tree.html"; + +this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) { + // Taking snapshots and computing dominator trees is slow :-/ + requestLongerTimeout(4); + + const store = panel.panelWin.gStore; + const { getState, dispatch } = store; + const doc = panel.panelWin.document; + + dispatch(changeView(viewState.DOMINATOR_TREE)); + + // Take a snapshot. + + const takeSnapshotButton = doc.getElementById("take-snapshot"); + EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin); + + // Wait for the dominator tree to be computed and fetched. + + await waitUntilDominatorTreeState(store, [dominatorTreeState.LOADED]); + ok(true, "Computed and fetched the dominator tree."); + + // Expand all the dominator tree nodes that are eagerly fetched, except for + // the leaves which will trigger fetching their lazily loaded subtrees. + + const id = getState().snapshots[0].id; + const root = getState().snapshots[0].dominatorTree.root; + (function expandAllEagerlyFetched(node = root) { + if (!node.moreChildrenAvailable || node.children) { + dispatch(expandDominatorTreeNode(id, node)); + } + + if (node.children) { + for (const child of node.children) { + expandAllEagerlyFetched(child); + } + } + })(); + + // Find the deepest eagerly loaded node: one which has more children but none + // of them are loaded. + + const deepest = (function findDeepest(node = root) { + if (node.moreChildrenAvailable && !node.children) { + return node; + } + + if (node.children) { + for (const child of node.children) { + const found = findDeepest(child); + if (found) { + return found; + } + } + } + + return null; + })(); + + ok(deepest, "Found the deepest node"); + ok( + !getState().snapshots[0].dominatorTree.expanded.has(deepest.nodeId), + "The deepest node should not be expanded" + ); + + // Select the deepest node. + + EventUtils.synthesizeMouseAtCenter( + doc.querySelector(`.node-${deepest.nodeId}`), + {}, + panel.panelWin + ); + await waitUntilState( + store, + state => state.snapshots[0].dominatorTree.focused.nodeId === deepest.nodeId + ); + ok( + doc.querySelector(`.node-${deepest.nodeId}`).classList.contains("focused"), + "The deepest node should be focused now" + ); + + // Expand the deepest node, which triggers an incremental fetch of its lazily + // loaded subtree. + + EventUtils.synthesizeKey("VK_RIGHT", {}, panel.panelWin); + await waitUntilState(store, state => + state.snapshots[0].dominatorTree.expanded.has(deepest.nodeId) + ); + is( + getState().snapshots[0].dominatorTree.state, + dominatorTreeState.INCREMENTAL_FETCHING, + "Expanding the deepest node should start an incremental fetch of its subtree" + ); + ok( + doc.querySelector(`.node-${deepest.nodeId}`).classList.contains("focused"), + "The deepest node should still be focused after expansion" + ); + + // Wait for the incremental fetch to complete. + + await waitUntilState( + store, + state => + state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED + ); + ok(true, "And the incremental fetch completes."); + ok( + doc.querySelector(`.node-${deepest.nodeId}`).classList.contains("focused"), + "The deepest node should still be focused after we have loaded its children" + ); + + // Find the most up-to-date version of the node whose children we just + // incrementally fetched. + + const newDeepest = (function findNewDeepest( + node = getState().snapshots[0].dominatorTree.root + ) { + if (node.nodeId === deepest.nodeId) { + return node; + } + + if (node.children) { + for (const child of node.children) { + const found = findNewDeepest(child); + if (found) { + return found; + } + } + } + + return null; + })(); + + ok(newDeepest, "We found the up-to-date version of deepest"); + ok(newDeepest.children, "And its children are loaded"); + ok(newDeepest.children.length, "And there are more than 0 children"); + + const firstChild = newDeepest.children[0]; + ok(firstChild, "deepest should have a first child"); + ok( + doc.querySelector(`.node-${firstChild.nodeId}`), + "and the first child should exist in the dom" + ); + + // Select the newly loaded first child by pressing the right arrow once more. + + EventUtils.synthesizeKey("VK_RIGHT", {}, panel.panelWin); + await waitUntilState( + store, + state => state.snapshots[0].dominatorTree.focused === firstChild + ); + ok( + doc + .querySelector(`.node-${firstChild.nodeId}`) + .classList.contains("focused"), + "The first child should now be focused" + ); +}); diff --git a/devtools/client/memory/test/browser/browser_memory_dominator_trees_02.js b/devtools/client/memory/test/browser/browser_memory_dominator_trees_02.js new file mode 100644 index 0000000000..fea3603c52 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_dominator_trees_02.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Integration test for mouse interaction in the dominator tree + +"use strict"; + +const { + dominatorTreeState, + viewState, +} = require("resource://devtools/client/memory/constants.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +const TEST_URL = + "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html"; + +function clickOnNodeArrow(node, panel) { + EventUtils.synthesizeMouseAtCenter( + node.querySelector(".arrow"), + {}, + panel.panelWin + ); +} + +this.test = makeMemoryTest(TEST_URL, async function ({ panel }) { + // Taking snapshots and computing dominator trees is slow :-/ + requestLongerTimeout(4); + + const store = panel.panelWin.gStore; + const { getState, dispatch } = store; + const doc = panel.panelWin.document; + + dispatch(changeView(viewState.DOMINATOR_TREE)); + + // Take a snapshot. + const takeSnapshotButton = doc.getElementById("take-snapshot"); + EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin); + + // Wait for the dominator tree to be computed and fetched. + await waitUntilState( + store, + state => + state.snapshots[0] && + state.snapshots[0].dominatorTree && + state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED + ); + ok(true, "Computed and fetched the dominator tree."); + + const root = getState().snapshots[0].dominatorTree.root; + ok( + getState().snapshots[0].dominatorTree.expanded.has(root.nodeId), + "Root node is expanded by default" + ); + + // Click on root arrow to collapse the root element + const rootNode = doc.querySelector(`.node-${root.nodeId}`); + clickOnNodeArrow(rootNode, panel); + + await waitUntilState( + store, + state => + state.snapshots[0] && + state.snapshots[0].dominatorTree && + !state.snapshots[0].dominatorTree.expanded.has(root.nodeId) + ); + ok(true, "Root node collapsed"); + + // Click on root arrow to expand it again + clickOnNodeArrow(rootNode, panel); + + await waitUntilState( + store, + state => + state.snapshots[0] && + state.snapshots[0].dominatorTree && + state.snapshots[0].dominatorTree.expanded.has(root.nodeId) + ); + ok(true, "Root node is expanded again"); +}); diff --git a/devtools/client/memory/test/browser/browser_memory_filter_01.js b/devtools/client/memory/test/browser/browser_memory_filter_01.js new file mode 100644 index 0000000000..00dcfdb951 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_filter_01.js @@ -0,0 +1,107 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Sanity test that we can show allocation stack displays in the tree. + +"use strict"; + +const { + dominatorTreeState, + viewState, +} = require("resource://devtools/client/memory/constants.js"); +const { + changeViewAndRefresh, + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +const TEST_URL = + "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html"; + +this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) { + const heapWorker = panel.panelWin.gHeapAnalysesClient; + const store = panel.panelWin.gStore; + const { dispatch } = store; + const doc = panel.panelWin.document; + + dispatch(changeView(viewState.CENSUS)); + + const takeSnapshotButton = doc.getElementById("take-snapshot"); + EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin); + + await waitUntilState( + store, + state => + state.snapshots.length === 1 && + state.snapshots[0].census && + state.snapshots[0].census.state === censusState.SAVING + ); + + let filterInput = doc.getElementById("filter"); + EventUtils.synthesizeMouseAtCenter(filterInput, {}, panel.panelWin); + EventUtils.sendString("js::Shape", panel.panelWin); + + await waitUntilState( + store, + state => + state.snapshots.length === 1 && + state.snapshots[0].census && + state.snapshots[0].census.state === censusState.SAVING + ); + ok(true, "adding a filter string should trigger census recompute"); + + await waitUntilState( + store, + state => + state.snapshots.length === 1 && + state.snapshots[0].census && + state.snapshots[0].census.state === censusState.SAVED + ); + + let nameElem = doc.querySelector(".heap-tree-item-field.heap-tree-item-name"); + ok(nameElem, "Should get a tree item row with a name"); + is( + nameElem.textContent.trim(), + "js::Shape", + "the tree item should be the one we filtered for" + ); + is( + filterInput.value, + "js::Shape", + "and filter input contains the user value" + ); + + // Now switch the dominator view, then switch back to census view + // and check that the filter word is still correctly applied + dispatch(changeViewAndRefresh(viewState.DOMINATOR_TREE, heapWorker)); + ok(true, "change view to dominator tree"); + + // Wait for the dominator tree to be computed and fetched. + await waitUntilDominatorTreeState(store, [dominatorTreeState.LOADED]); + ok(true, "computed and fetched the dominator tree."); + + dispatch(changeViewAndRefresh(viewState.CENSUS, heapWorker)); + ok(true, "change view back to census"); + + await waitUntilState( + store, + state => + state.snapshots.length === 1 && + state.snapshots[0].census && + state.snapshots[0].census.state === censusState.SAVED + ); + + nameElem = doc.querySelector(".heap-tree-item-field.heap-tree-item-name"); + filterInput = doc.getElementById("filter"); + + ok(nameElem, "Should still get a tree item row with a name"); + is( + nameElem.textContent.trim(), + "js::Shape", + "the tree item should still be the one we filtered for" + ); + is( + filterInput.value, + "js::Shape", + "and filter input still contains the user value" + ); +}); diff --git a/devtools/client/memory/test/browser/browser_memory_fission_switch_target.js b/devtools/client/memory/test/browser/browser_memory_fission_switch_target.js new file mode 100644 index 0000000000..5d0d474d76 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_fission_switch_target.js @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test top-level target switching for memory panel. + +const { + treeMapState, +} = require("resource://devtools/client/memory/constants.js"); +const PARENT_PROCESS_URI = "about:robots"; +const CONTENT_PROCESS_URI = + "data:text/html,<section>content process page</section>"; +const EXPECTED_ELEMENT_IN_PARENT_PROCESS = "button"; +const EXPECTED_ELEMENT_IN_CONTENT_PROCESS = "section"; + +add_task(async () => { + info("Open the memory panel with empty page"); + const tab = await addTab(); + const { panel } = await openMemoryPanel(tab); + const { gStore: store } = panel.panelWin; + + info("Open a page running on the content process"); + await navigateTo(CONTENT_PROCESS_URI); + await takeAndWaitSnapshot( + panel.panelWin, + store, + EXPECTED_ELEMENT_IN_CONTENT_PROCESS + ); + ok(true, "Can take a snapshot for content process page correctly"); + + info("Navigate to a page running on parent process"); + await navigateTo(PARENT_PROCESS_URI); + await takeAndWaitSnapshot( + panel.panelWin, + store, + EXPECTED_ELEMENT_IN_PARENT_PROCESS + ); + ok(true, "Can take a snapshot for parent process page correctly"); + + info("Return to a page running on content process again"); + await navigateTo(CONTENT_PROCESS_URI); + await takeAndWaitSnapshot( + panel.panelWin, + store, + EXPECTED_ELEMENT_IN_CONTENT_PROCESS + ); + ok( + true, + "Can take a snapshot for content process page correctly after switching targets twice" + ); +}); + +async function takeAndWaitSnapshot(window, store, expectedElement) { + await asyncWaitUntil(async () => { + await takeSnapshot(window); + + await waitUntilState( + store, + state => + state.snapshots[0].treeMap && + state.snapshots[0].treeMap.state === treeMapState.SAVED + ); + + const snapshot = store.getState().snapshots[0]; + const nodeNames = getNodeNames(snapshot); + + await clearSnapshots(window); + + return nodeNames.includes(expectedElement); + }); +} + +function getNodeNames(snapshot) { + const domNodePart = snapshot.treeMap.report.children.find( + child => child.name === "domNode" + ); + return domNodePart.children.map(child => child.name.toLowerCase()); +} diff --git a/devtools/client/memory/test/browser/browser_memory_individuals_01.js b/devtools/client/memory/test/browser/browser_memory_individuals_01.js new file mode 100644 index 0000000000..f54154b949 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_individuals_01.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Sanity test that we can show census group individuals, and then go back to +// the previous view. + +"use strict"; + +const { + individualsState, + viewState, +} = require("resource://devtools/client/memory/constants.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +const TEST_URL = + "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html"; + +this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) { + const store = panel.panelWin.gStore; + const { dispatch } = store; + const doc = panel.panelWin.document; + + dispatch(changeView(viewState.CENSUS)); + + // Take a snapshot and wait for the census to finish. + + const takeSnapshotButton = doc.getElementById("take-snapshot"); + EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin); + + await waitUntilState(store, state => { + return ( + state.snapshots.length === 1 && + state.snapshots[0].census && + state.snapshots[0].census.state === censusState.SAVED + ); + }); + + // Click on the first individuals button found, and wait for the individuals + // to be fetched. + + const individualsButton = doc.querySelector(".individuals-button"); + EventUtils.synthesizeMouseAtCenter(individualsButton, {}, panel.panelWin); + + await waitUntilState(store, state => { + return ( + state.view.state === viewState.INDIVIDUALS && + state.individuals && + state.individuals.state === individualsState.FETCHED + ); + }); + + ok( + doc.getElementById("shortest-paths"), + "Should be showing the shortest paths component" + ); + ok(doc.querySelector(".heap-tree-item"), "Should be showing the individuals"); + + // Go back to the previous view. + + const popViewButton = doc.getElementById("pop-view-button"); + ok(popViewButton, "Should be showing the #pop-view-button"); + EventUtils.synthesizeMouseAtCenter(popViewButton, {}, panel.panelWin); + + await waitUntilState(store, state => { + return state.view.state === viewState.CENSUS; + }); + + ok( + !doc.getElementById("shortest-paths"), + "Should not be showing the shortest paths component anymore" + ); +}); diff --git a/devtools/client/memory/test/browser/browser_memory_keyboard-snapshot-list.js b/devtools/client/memory/test/browser/browser_memory_keyboard-snapshot-list.js new file mode 100644 index 0000000000..7a2e2e12f7 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_keyboard-snapshot-list.js @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that using ACCEL+UP/DOWN, the user can navigate between snapshots. + +"use strict"; + +const { viewState } = require("resource://devtools/client/memory/constants.js"); +const { + takeSnapshotAndCensus, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +const TEST_URL = + "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html"; + +this.test = makeMemoryTest(TEST_URL, async function ({ panel }) { + // Creating snapshots already takes ~25 seconds on linux 32 debug machines + // which makes the test very likely to go over the allowed timeout + requestLongerTimeout(2); + + const heapWorker = panel.panelWin.gHeapAnalysesClient; + const store = panel.panelWin.gStore; + const { dispatch } = store; + const front = store.getState().front; + const doc = panel.panelWin.document; + + dispatch(changeView(viewState.CENSUS)); + + info("Take 3 snapshots"); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + + await waitUntilState( + store, + state => + state.snapshots.length == 3 && + state.snapshots.every( + s => s.census && s.census.state === censusState.SAVED + ) + ); + ok(true, "All snapshots censuses are in SAVED state"); + + await waitUntilSnapshotSelected(store, 2); + ok(true, "Third snapshot selected after creating all snapshots."); + + info("Press ACCEL+UP key, expect second snapshot selected."); + EventUtils.synthesizeKey("VK_UP", { accelKey: true }, panel.panelWin); + await waitUntilSnapshotSelected(store, 1); + ok(true, "Second snapshot selected after alt+UP."); + + info("Press ACCEL+UP key, expect first snapshot selected."); + EventUtils.synthesizeKey("VK_UP", { accelKey: true }, panel.panelWin); + await waitUntilSnapshotSelected(store, 0); + ok(true, "First snapshot is selected after ACCEL+UP"); + + info("Check ACCEL+UP is a noop when the first snapshot is selected."); + EventUtils.synthesizeKey("VK_UP", { accelKey: true }, panel.panelWin); + // We assume the snapshot selection should be synchronous here. + is(getSelectedSnapshotIndex(store), 0, "First snapshot is still selected"); + + info("Press ACCEL+DOWN key, expect second snapshot selected."); + EventUtils.synthesizeKey("VK_DOWN", { accelKey: true }, panel.panelWin); + await waitUntilSnapshotSelected(store, 1); + ok(true, "Second snapshot is selected after ACCEL+DOWN"); + + info("Click on first node."); + const firstNode = doc.querySelector(".tree .heap-tree-item-name"); + EventUtils.synthesizeMouseAtCenter(firstNode, {}, panel.panelWin); + await waitUntilState( + store, + state => + state.snapshots[1].census.focused === + state.snapshots[1].census.report.children[0] + ); + ok(true, "First root is selected after click."); + + info("Press DOWN key, expect second root focused."); + EventUtils.synthesizeKey("VK_DOWN", {}, panel.panelWin); + await waitUntilState( + store, + state => + state.snapshots[1].census.focused === + state.snapshots[1].census.report.children[1] + ); + ok(true, "Second root is selected after pressing DOWN."); + is(getSelectedSnapshotIndex(store), 1, "Second snapshot is still selected"); + + info("Press UP key, expect second root focused."); + EventUtils.synthesizeKey("VK_UP", {}, panel.panelWin); + await waitUntilState( + store, + state => + state.snapshots[1].census.focused === + state.snapshots[1].census.report.children[0] + ); + ok(true, "First root is selected after pressing UP."); + is(getSelectedSnapshotIndex(store), 1, "Second snapshot is still selected"); + + info("Press ACCEL+DOWN key, expect third snapshot selected."); + EventUtils.synthesizeKey("VK_DOWN", { accelKey: true }, panel.panelWin); + await waitUntilSnapshotSelected(store, 2); + ok(true, "Thirdˆ snapshot is selected after ACCEL+DOWN"); + + info("Check ACCEL+DOWN is a noop when the last snapshot is selected."); + EventUtils.synthesizeKey("VK_DOWN", { accelKey: true }, panel.panelWin); + // We assume the snapshot selection should be synchronous here. + is(getSelectedSnapshotIndex(store), 2, "Third snapshot is still selected"); +}); diff --git a/devtools/client/memory/test/browser/browser_memory_keyboard.js b/devtools/client/memory/test/browser/browser_memory_keyboard.js new file mode 100644 index 0000000000..0fba33c456 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_keyboard.js @@ -0,0 +1,111 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Bug 1246570 - Check that when pressing on LEFT arrow, the parent tree node +// gets focused. + +"use strict"; + +const { viewState } = require("resource://devtools/client/memory/constants.js"); +const { + takeSnapshotAndCensus, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +const TEST_URL = + "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html"; + +function waitUntilFocused(store, node) { + return waitUntilState( + store, + state => + state.snapshots.length === 1 && + state.snapshots[0].census && + state.snapshots[0].census.state === censusState.SAVED && + state.snapshots[0].census.focused && + state.snapshots[0].census.focused === node + ); +} + +function waitUntilExpanded(store, node) { + return waitUntilState( + store, + state => + state.snapshots[0] && + state.snapshots[0].census && + state.snapshots[0].census.expanded.has(node.id) + ); +} + +this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) { + const heapWorker = panel.panelWin.gHeapAnalysesClient; + const store = panel.panelWin.gStore; + const { getState, dispatch } = store; + const front = getState().front; + const doc = panel.panelWin.document; + + dispatch(changeView(viewState.CENSUS)); + + is(getState().censusDisplay.breakdown.by, "coarseType"); + + await dispatch(takeSnapshotAndCensus(front, heapWorker)); + const census = getState().snapshots[0].census; + const root1 = census.report.children[0]; + const root2 = census.report.children[0]; + const root3 = census.report.children[0]; + const root4 = census.report.children[0]; + const child1 = root1.children[0]; + + info("Click on first node."); + const firstNode = doc.querySelector(".tree .heap-tree-item-name"); + EventUtils.synthesizeMouseAtCenter(firstNode, {}, panel.panelWin); + await waitUntilFocused(store, root1); + ok(true, "First root is selected after click."); + + info("Press DOWN key, expect second root focused."); + EventUtils.synthesizeKey("VK_DOWN", {}, panel.panelWin); + await waitUntilFocused(store, root2); + ok(true, "Second root is selected after pressing DOWN arrow."); + + info("Press DOWN key, expect third root focused."); + EventUtils.synthesizeKey("VK_DOWN", {}, panel.panelWin); + await waitUntilFocused(store, root3); + ok(true, "Third root is selected after pressing DOWN arrow."); + + info("Press DOWN key, expect fourth root focused."); + EventUtils.synthesizeKey("VK_DOWN", {}, panel.panelWin); + await waitUntilFocused(store, root4); + ok(true, "Fourth root is selected after pressing DOWN arrow."); + + info("Press UP key, expect third root focused."); + EventUtils.synthesizeKey("VK_UP", {}, panel.panelWin); + await waitUntilFocused(store, root3); + ok(true, "Third root is selected after pressing UP arrow."); + + info("Press UP key, expect second root focused."); + EventUtils.synthesizeKey("VK_UP", {}, panel.panelWin); + await waitUntilFocused(store, root2); + ok(true, "Second root is selected after pressing UP arrow."); + + info("Press UP key, expect first root focused."); + EventUtils.synthesizeKey("VK_UP", {}, panel.panelWin); + await waitUntilFocused(store, root1); + ok(true, "First root is selected after pressing UP arrow."); + + info("Press RIGHT key"); + EventUtils.synthesizeKey("VK_RIGHT", {}, panel.panelWin); + await waitUntilExpanded(store, root1); + ok(true, "Root node is expanded."); + + info("Press RIGHT key, expect first child focused."); + EventUtils.synthesizeKey("VK_RIGHT", {}, panel.panelWin); + await waitUntilFocused(store, child1); + ok(true, "First child is selected after pressing RIGHT arrow."); + + info("Press LEFT key, expect first root focused."); + EventUtils.synthesizeKey("VK_LEFT", {}, panel.panelWin); + await waitUntilFocused(store, root1); + ok(true, "First root is selected after pressing LEFT arrow."); +}); diff --git a/devtools/client/memory/test/browser/browser_memory_no_allocation_stacks.js b/devtools/client/memory/test/browser/browser_memory_no_allocation_stacks.js new file mode 100644 index 0000000000..b8b09b35d0 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_no_allocation_stacks.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Sanity test that we can show allocation stack displays in the tree. + +"use strict"; + +const { + takeSnapshotAndCensus, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const censusDisplayActions = require("resource://devtools/client/memory/actions/census-display.js"); +const { viewState } = require("resource://devtools/client/memory/constants.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +const TEST_URL = + "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html"; + +this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) { + const heapWorker = panel.panelWin.gHeapAnalysesClient; + const { getState, dispatch } = panel.panelWin.gStore; + const front = getState().front; + const doc = panel.panelWin.document; + + dispatch(changeView(viewState.CENSUS)); + + ok(!getState().allocations.recording, "Should not be recording allocagtions"); + + await dispatch(takeSnapshotAndCensus(front, heapWorker)); + await dispatch( + censusDisplayActions.setCensusDisplayAndRefresh( + heapWorker, + censusDisplays.allocationStack + ) + ); + + is( + getState().censusDisplay.breakdown.by, + "allocationStack", + "Should be using allocation stack breakdown" + ); + + ok( + !getState().allocations.recording, + "Should still not be recording allocagtions" + ); + + ok( + doc.querySelector(".no-allocation-stacks"), + "Because we did not record allocations, " + + "the no-allocation-stack warning should be visible" + ); +}); diff --git a/devtools/client/memory/test/browser/browser_memory_no_auto_expand.js b/devtools/client/memory/test/browser/browser_memory_no_auto_expand.js new file mode 100644 index 0000000000..9704d925d1 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_no_auto_expand.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Bug 1221150 - Ensure that census trees do not accidentally auto expand +// when clicking on the allocation stacks checkbox. + +"use strict"; + +const { + takeSnapshotAndCensus, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { viewState } = require("resource://devtools/client/memory/constants.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +const TEST_URL = + "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html"; + +this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) { + const heapWorker = panel.panelWin.gHeapAnalysesClient; + const { getState, dispatch } = panel.panelWin.gStore; + const front = getState().front; + const doc = panel.panelWin.document; + + dispatch(changeView(viewState.CENSUS)); + + await dispatch(takeSnapshotAndCensus(front, heapWorker)); + + is(getState().allocations.recording, false); + const recordingCheckbox = doc.getElementById( + "record-allocation-stacks-checkbox" + ); + EventUtils.synthesizeMouseAtCenter(recordingCheckbox, {}, panel.panelWin); + is(getState().allocations.recording, true); + + const nameElems = [ + ...doc.querySelectorAll(".heap-tree-item-field.heap-tree-item-name"), + ]; + + for (const el of nameElems) { + dumpn(`Found ${el.textContent.trim()}`); + is( + el.style.marginInlineStart, + "0px", + "None of the elements should be an indented/expanded child" + ); + } +}); diff --git a/devtools/client/memory/test/browser/browser_memory_percents_01.js b/devtools/client/memory/test/browser/browser_memory_percents_01.js new file mode 100644 index 0000000000..67c4f4368b --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_percents_01.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Sanity test that we calculate percentages in the tree. + +"use strict"; + +const { + takeSnapshotAndCensus, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { viewState } = require("resource://devtools/client/memory/constants.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +const TEST_URL = + "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html"; + +function checkCells(cells) { + ok(cells.length > 1, "Should have found some"); + // Ignore the first header cell. + for (const cell of cells.slice(1)) { + const percent = cell.querySelector(".heap-tree-percent"); + ok(percent, "should have a percent cell"); + ok( + percent.textContent.match(/^\d?\d%$/), + "should be of the form nn% or n%" + ); + } +} + +this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) { + const heapWorker = panel.panelWin.gHeapAnalysesClient; + const { getState, dispatch } = panel.panelWin.gStore; + const front = getState().front; + const doc = panel.panelWin.document; + + dispatch(changeView(viewState.CENSUS)); + + await dispatch(takeSnapshotAndCensus(front, heapWorker)); + is( + getState().censusDisplay.breakdown.by, + "coarseType", + "Should be using coarse type breakdown" + ); + + const bytesCells = [...doc.querySelectorAll(".heap-tree-item-bytes")]; + checkCells(bytesCells); + + const totalBytesCells = [ + ...doc.querySelectorAll(".heap-tree-item-total-bytes"), + ]; + checkCells(totalBytesCells); + + const countCells = [...doc.querySelectorAll(".heap-tree-item-count")]; + checkCells(countCells); + + const totalCountCells = [ + ...doc.querySelectorAll(".heap-tree-item-total-count"), + ]; + checkCells(totalCountCells); +}); diff --git a/devtools/client/memory/test/browser/browser_memory_refresh_does_not_leak.js b/devtools/client/memory/test/browser/browser_memory_refresh_does_not_leak.js new file mode 100644 index 0000000000..e0ee1eee41 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_refresh_does_not_leak.js @@ -0,0 +1,139 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* global ChromeUtils */ + +// Test that refreshing the page with devtools open does not leak the old +// windows from previous navigations. +// +// IF THIS TEST STARTS FAILING, YOU ARE LEAKING EVERY WINDOW EVER NAVIGATED TO +// WHILE DEVTOOLS ARE OPEN! THIS IS NOT SPECIFIC TO THE MEMORY TOOL ONLY! + +"use strict"; + +const { + getLabelAndShallowSize, +} = require("resource://devtools/shared/heapsnapshot/DominatorTreeNode.js"); + +const TEST_URL = + "http://example.com/browser/devtools/client/memory/test/browser/doc_empty.html"; + +async function getWindowsInSnapshot(front) { + dumpn("Taking snapshot."); + const path = await front.saveHeapSnapshot(); + dumpn("Took snapshot with path = " + path); + const snapshot = ChromeUtils.readHeapSnapshot(path); + dumpn("Read snapshot into memory, taking census."); + const report = snapshot.takeCensus({ + breakdown: { + by: "objectClass", + then: { by: "bucket" }, + other: { by: "count", count: true, bytes: false }, + }, + }); + dumpn("Took census, window count = " + report.Window.count); + return report.Window; +} + +const DESCRIPTION = { + by: "coarseType", + objects: { + by: "objectClass", + then: { by: "count", count: true, bytes: false }, + other: { by: "count", count: true, bytes: false }, + }, + strings: { by: "count", count: true, bytes: false }, + scripts: { + by: "internalType", + then: { by: "count", count: true, bytes: false }, + }, + other: { + by: "internalType", + then: { by: "count", count: true, bytes: false }, + }, +}; + +this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) { + let front = panel.panelWin.gStore.getState().front; + + const startWindows = await getWindowsInSnapshot(front); + dumpn( + "Initial windows found = " + + startWindows.map(w => "0x" + w.toString(16)).join(", ") + ); + is(startWindows.length, 1); + + await reloadBrowser(); + + // Update the front as we may have switched to a new target and a new memory front + front = panel.panelWin.gStore.getState().front; + + const endWindows = await getWindowsInSnapshot(front); + is(endWindows.length, 1); + + if (endWindows.length === 1) { + return; + } + + dumpn("Test failed, diagnosing leaking windows."); + dumpn( + "(This may fail if a moving GC has relocated the initial Window objects.)" + ); + + dumpn("Taking full runtime snapshot."); + const path = await front.saveHeapSnapshot({ boundaries: { runtime: true } }); + dumpn("Full runtime's snapshot path = " + path); + + dumpn("Reading full runtime heap snapshot."); + const snapshot = ChromeUtils.readHeapSnapshot(path); + dumpn("Done reading full runtime heap snapshot."); + + const dominatorTree = snapshot.computeDominatorTree(); + const paths = snapshot.computeShortestPaths( + dominatorTree.root, + startWindows, + 50 + ); + + for (let i = 0; i < startWindows.length; i++) { + dumpn( + "Shortest retaining paths for leaking Window 0x" + + startWindows[i].toString(16) + + " =========================" + ); + let j = 0; + for (const retainingPath of paths.get(startWindows[i])) { + if (retainingPath.find(part => part.predecessor === startWindows[i])) { + // Skip paths that loop out from the target window and back to it again. + continue; + } + + dumpn( + " Path #" + + ++j + + ": --------------------------------------------------------------------" + ); + for (const part of retainingPath) { + const { label } = getLabelAndShallowSize( + part.predecessor, + snapshot, + DESCRIPTION + ); + dumpn( + " 0x" + + part.predecessor.toString(16) + + " (" + + label.join(" > ") + + ")" + ); + dumpn(" |"); + dumpn(" " + part.edge); + dumpn(" |"); + dumpn(" V"); + } + dumpn( + " 0x" + startWindows[i].toString(16) + " (objects > Window)" + ); + } + } +}); diff --git a/devtools/client/memory/test/browser/browser_memory_simple_01.js b/devtools/client/memory/test/browser/browser_memory_simple_01.js new file mode 100644 index 0000000000..a983e23395 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_simple_01.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests taking snapshots and default states. + */ + +const TEST_URL = + "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html"; +const { viewState } = require("resource://devtools/client/memory/constants.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) { + const { gStore, document } = panel.panelWin; + const { getState, dispatch } = gStore; + + dispatch(changeView(viewState.CENSUS)); + + let snapshotEls = document.querySelectorAll( + "#memory-tool-container .list li" + ); + is(getState().snapshots.length, 0, "Starts with no snapshots in store"); + is(snapshotEls.length, 0, "No snapshots rendered"); + + await takeSnapshot(panel.panelWin); + snapshotEls = document.querySelectorAll("#memory-tool-container .list li"); + is(getState().snapshots.length, 1, "One snapshot was created in store"); + is(snapshotEls.length, 1, "One snapshot was rendered"); + ok( + snapshotEls[0].classList.contains("selected"), + "Only snapshot has `selected` class" + ); + + await takeSnapshot(panel.panelWin); + snapshotEls = document.querySelectorAll("#memory-tool-container .list li"); + is(getState().snapshots.length, 2, "Two snapshots created in store"); + is(snapshotEls.length, 2, "Two snapshots rendered"); + ok( + !snapshotEls[0].classList.contains("selected"), + "First snapshot no longer has `selected` class" + ); + ok( + snapshotEls[1].classList.contains("selected"), + "Second snapshot has `selected` class" + ); + + await waitUntilCensusState(gStore, s => s.census, [ + censusState.SAVED, + censusState.SAVED, + ]); + + ok( + document.querySelector(".heap-tree-item-name"), + "Should have rendered some tree items" + ); +}); diff --git a/devtools/client/memory/test/browser/browser_memory_transferHeapSnapshot_e10s_01.js b/devtools/client/memory/test/browser/browser_memory_transferHeapSnapshot_e10s_01.js new file mode 100644 index 0000000000..c647836332 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_transferHeapSnapshot_e10s_01.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* global ChromeUtils, HeapSnapshot */ + +// Test that we can save a heap snapshot and transfer it over the RDP in e10s +// where the child process is sandboxed and so we have to use +// HeapSnapshotFileActor to get the heap snapshot file. + +"use strict"; + +const TEST_URL = "data:text/html,<html><body></body></html>"; + +this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) { + const memoryFront = panel.panelWin.gStore.getState().front; + ok(memoryFront, "Should get the MemoryFront"); + + const snapshotFilePath = await memoryFront.saveHeapSnapshot({ + // Force a copy so that we go through the HeapSnapshotFileActor's + // transferHeapSnapshot request and exercise this code path on e10s. + forceCopy: true, + }); + + ok( + !!(await IOUtils.stat(snapshotFilePath)), + "Should have the heap snapshot file" + ); + + const snapshot = ChromeUtils.readHeapSnapshot(snapshotFilePath); + ok( + HeapSnapshot.isInstance(snapshot), + "And we should be able to read a HeapSnapshot instance from the file" + ); +}); diff --git a/devtools/client/memory/test/browser/browser_memory_tree_map-01.js b/devtools/client/memory/test/browser/browser_memory_tree_map-01.js new file mode 100644 index 0000000000..c65b7fc079 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_tree_map-01.js @@ -0,0 +1,136 @@ +/* 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/. */ + +// Make sure the canvases are created correctly + +"use strict"; + +const CanvasUtils = require("resource://devtools/client/memory/components/tree-map/canvas-utils.js"); +const D3_SCRIPT = + '<script type="application/javascript" ' + + 'src="chrome://global/content/third_party/d3/d3.js">'; +const TEST_URL = `data:text/html,<html><body>${D3_SCRIPT}</body></html>`; + +this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) { + const document = panel.panelWin.document; + const window = panel.panelWin; + const div = document.createElement("div"); + + Object.assign(div.style, { + width: "100px", + height: "200px", + position: "absolute", + }); + + document.body.appendChild(div); + + info("Create the canvases"); + + const canvases = new CanvasUtils(div, 0); + + info("Test the shape of the returned object"); + + is(typeof canvases, "object", "Canvases create an object"); + is(typeof canvases.emit, "function", "Decorated with an EventEmitter"); + is(typeof canvases.on, "function", "Decorated with an EventEmitter"); + is(div.children[0], canvases.container, "Div has the container"); + ok( + canvases.main.canvas instanceof window.HTMLCanvasElement, + "Creates the main canvas" + ); + ok( + canvases.zoom.canvas instanceof window.HTMLCanvasElement, + "Creates the zoom canvas" + ); + ok( + canvases.main.ctx instanceof window.CanvasRenderingContext2D, + "Creates the main canvas context" + ); + ok( + canvases.zoom.ctx instanceof window.CanvasRenderingContext2D, + "Creates the zoom canvas context" + ); + + info("Test resizing"); + + let timesResizeCalled = 0; + canvases.on("resize", function () { + timesResizeCalled++; + }); + + const main = canvases.main.canvas; + const zoom = canvases.zoom.canvas; + const ratio = window.devicePixelRatio; + + is( + main.width, + 100 * ratio, + "Main canvas width is the same as the parent div" + ); + is( + main.height, + 200 * ratio, + "Main canvas height is the same as the parent div" + ); + is( + zoom.width, + 100 * ratio, + "Zoom canvas width is the same as the parent div" + ); + is( + zoom.height, + 200 * ratio, + "Zoom canvas height is the same as the parent div" + ); + is(timesResizeCalled, 0, "Resize was not emitted"); + + div.style.width = "500px"; + div.style.height = "700px"; + + window.dispatchEvent(new Event("resize")); + + is( + main.width, + 500 * ratio, + "Main canvas width is resized to be the same as the parent div" + ); + is( + main.height, + 700 * ratio, + "Main canvas height is resized to be the same as the parent div" + ); + is( + zoom.width, + 500 * ratio, + "Zoom canvas width is resized to be the same as the parent div" + ); + is( + zoom.height, + 700 * ratio, + "Zoom canvas height is resized to be the same as the parent div" + ); + is(timesResizeCalled, 1, "'resize' was emitted was emitted"); + + div.style.width = "1100px"; + div.style.height = "1300px"; + + canvases.destroy(); + window.dispatchEvent(new Event("resize")); + + is(main.width, 500 * ratio, "Main canvas width is not resized after destroy"); + is( + main.height, + 700 * ratio, + "Main canvas height is not resized after destroy" + ); + is(zoom.width, 500 * ratio, "Zoom canvas width is not resized after destroy"); + is( + zoom.height, + 700 * ratio, + "Zoom canvas height is not resized after destroy" + ); + is(timesResizeCalled, 1, "onResize was not called again"); + + document.body.removeChild(div); +}); diff --git a/devtools/client/memory/test/browser/browser_memory_tree_map-02.js b/devtools/client/memory/test/browser/browser_memory_tree_map-02.js new file mode 100644 index 0000000000..890ede23b5 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_tree_map-02.js @@ -0,0 +1,199 @@ +/* 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 the drag and zooming behavior + +"use strict"; + +const CanvasUtils = require("resource://devtools/client/memory/components/tree-map/canvas-utils.js"); +const DragZoom = require("resource://devtools/client/memory/components/tree-map/drag-zoom.js"); + +const TEST_URL = "data:text/html,<html><body></body></html>"; +const PIXEL_SCROLL_MODE = 0; +const PIXEL_DELTA = 10; +const MAX_RAF_LOOP = 1000; + +this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) { + const panelWin = panel.panelWin; + const panelDoc = panelWin.document; + const div = panelDoc.createElement("div"); + + Object.assign(div.style, { + width: "100px", + height: "200px", + position: "absolute", + left: 0, + top: 0, + }); + + const rafMock = createRAFMock(); + + panelDoc.body.appendChild(div); + + const canvases = new CanvasUtils(div, 0); + const dragZoom = new DragZoom(canvases.container, 0, rafMock.raf); + const style = canvases.container.style; + + info("Check initial state of dragZoom"); + { + is(dragZoom.zoom, 0, "Zooming starts at 0"); + is(dragZoom.smoothZoom, 0, "Smoothed zooming starts at 0"); + is(rafMock.timesCalled, 0, "No RAFs have been queued"); + is( + style.transform, + "translate(0px) scale(1)", + "No transforms have been done." + ); + + canvases.container.dispatchEvent( + new WheelEvent("wheel", { + deltaY: -PIXEL_DELTA, + deltaMode: PIXEL_SCROLL_MODE, + }) + ); + + is( + style.transform, + "translate(0px) scale(1.05)", + "The div has been slightly scaled." + ); + is( + dragZoom.zoom, + PIXEL_DELTA * dragZoom.ZOOM_SPEED, + "The zoom was increased" + ); + ok( + floatEquality(dragZoom.smoothZoom, 0.05), + "The smooth zoom is between the initial value and the target" + ); + is(rafMock.timesCalled, 1, "A RAF has been queued"); + } + + info("RAF will eventually stop once the smooth values approach the target"); + { + let i; + let lastCallCount; + for (i = 0; i < MAX_RAF_LOOP; i++) { + if (lastCallCount === rafMock.timesCalled) { + break; + } + lastCallCount = rafMock.timesCalled; + rafMock.nextFrame(); + } + is( + style.transform, + "translate(0px) scale(1.1)", + "The scale has been fully applied" + ); + is( + dragZoom.zoom, + dragZoom.smoothZoom, + "The smooth and target zoom values match" + ); + isnot(MAX_RAF_LOOP, i, "The RAF loop correctly stopped"); + } + + info("Dragging correctly translates the div"); + { + div.dispatchEvent( + new MouseEvent("mousemove", { + clientX: 10, + clientY: 10, + }) + ); + div.dispatchEvent(new MouseEvent("mousedown")); + div.dispatchEvent( + new MouseEvent("mousemove", { + clientX: 20, + clientY: 20, + }) + ); + div.dispatchEvent(new MouseEvent("mouseup")); + + is( + style.transform, + "translate(2.5px, 5px) scale(1.1)", + "The style is correctly translated" + ); + ok( + floatEquality(dragZoom.translateX, 5), + "Translate X moved by some pixel amount" + ); + ok( + floatEquality(dragZoom.translateY, 10), + "Translate Y moved by some pixel amount" + ); + } + + info("Zooming centers around the mouse"); + { + canvases.container.dispatchEvent( + new WheelEvent("wheel", { + deltaY: -PIXEL_DELTA, + deltaMode: PIXEL_SCROLL_MODE, + }) + ); + // Run through the RAF loop to zoom in towards that value. + let lastCallCount; + for (let i = 0; i < MAX_RAF_LOOP; i++) { + if (lastCallCount === rafMock.timesCalled) { + break; + } + lastCallCount = rafMock.timesCalled; + rafMock.nextFrame(); + } + is( + style.transform, + "translate(8.18182px, 18.1818px) scale(1.2)", + "Zooming affects the translation to keep the mouse centered" + ); + ok( + floatEquality(dragZoom.translateX, 8.181818181818185), + "Translate X was affected by the mouse position" + ); + ok( + floatEquality(dragZoom.translateY, 18.18181818181817), + "Translate Y was affected by the mouse position" + ); + is(dragZoom.zoom, 0.2, "Zooming starts at 0"); + } + + dragZoom.destroy(); + + info("Scroll isn't tracked after destruction"); + { + const previousZoom = dragZoom.zoom; + const previousSmoothZoom = dragZoom.smoothZoom; + + canvases.container.dispatchEvent( + new WheelEvent("wheel", { + deltaY: -PIXEL_DELTA, + deltaMode: PIXEL_SCROLL_MODE, + }) + ); + + is(dragZoom.zoom, previousZoom, "The zoom stayed the same"); + is( + dragZoom.smoothZoom, + previousSmoothZoom, + "The smooth zoom stayed the same" + ); + } + + info("Translation isn't tracked after destruction"); + { + const initialX = dragZoom.translateX; + const initialY = dragZoom.translateY; + + div.dispatchEvent(new MouseEvent("mousedown")); + div.dispatchEvent(new MouseEvent("mousemove"), { + clientX: 40, + clientY: 40, + }); + div.dispatchEvent(new MouseEvent("mouseup")); + is(dragZoom.translateX, initialX, "The translationX didn't change"); + is(dragZoom.translateY, initialY, "The translationY didn't change"); + } + panelDoc.body.removeChild(div); +}); diff --git a/devtools/client/memory/test/browser/doc_big_tree.html b/devtools/client/memory/test/browser/doc_big_tree.html new file mode 100644 index 0000000000..9fe74cd28b --- /dev/null +++ b/devtools/client/memory/test/browser/doc_big_tree.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"/> + </head> + <body> + <script> + "use strict"; + + window.big = (function makeBig(depth = 0) { + let big = Array(5); + big.fill(undefined); + if (depth < 5) { + big = big.map(_ => makeBig(depth + 1)); + } + return big; + }()); + </script> + </body> +</html> diff --git a/devtools/client/memory/test/browser/doc_empty.html b/devtools/client/memory/test/browser/doc_empty.html new file mode 100644 index 0000000000..ef123d8d20 --- /dev/null +++ b/devtools/client/memory/test/browser/doc_empty.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"/> + </head> + <body> + This is an empty window. + </body> +</html> diff --git a/devtools/client/memory/test/browser/doc_steady_allocation.html b/devtools/client/memory/test/browser/doc_steady_allocation.html new file mode 100644 index 0000000000..3e168507fa --- /dev/null +++ b/devtools/client/memory/test/browser/doc_steady_allocation.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8" /> + </head> + <body> + <script> + "use strict"; + + var objects = window.objects = []; + var allocate = this.allocate = function allocate() { + for (let i = 0; i < 100; i++) { + objects.push({}); + } + setTimeout(allocate, 10); + }; + + allocate(); + </script> + </body> +</html> diff --git a/devtools/client/memory/test/browser/head.js b/devtools/client/memory/test/browser/head.js new file mode 100644 index 0000000000..8c8b3e580d --- /dev/null +++ b/devtools/client/memory/test/browser/head.js @@ -0,0 +1,269 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Load the shared test helpers into this compartment. +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js", + this +); + +var { + censusDisplays, + censusState, + snapshotState: states, +} = require("resource://devtools/client/memory/constants.js"); +var { L10N } = require("resource://devtools/client/memory/utils.js"); + +Services.prefs.setBoolPref("devtools.memory.enabled", true); + +/** + * Open the memory panel for the given tab. + */ +this.openMemoryPanel = async function (tab) { + info("Opening memory panel."); + const toolbox = await gDevTools.showToolboxForTab(tab, { toolId: "memory" }); + info("Memory panel shown successfully."); + const panel = toolbox.getCurrentPanel(); + return { tab, panel }; +}; + +/** + * Close the memory panel for the given tab. + */ +this.closeMemoryPanel = async function (tab) { + info("Closing memory panel."); + const toolbox = await gDevTools.getToolboxForTab(tab); + await toolbox.destroy(); + info("Closed memory panel successfully."); +}; + +/** + * Return a test function that adds a tab with the given url, opens the memory + * panel, runs the given generator, closes the memory panel, removes the tab, + * and finishes. + * + * Example usage: + * + * this.test = makeMemoryTest(TEST_URL, async function ({ tab, panel }) { + * // Your tests go here... + * }); + */ +function makeMemoryTest(url, generator) { + return async function () { + waitForExplicitFinish(); + + // It can take a long time to save a snapshot to disk, read the snapshots + // back from disk, and finally perform analyses on them. + requestLongerTimeout(2); + + const tab = await addTab(url); + const results = await openMemoryPanel(tab); + + try { + await generator(results); + } catch (err) { + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(err)); + } + + await closeMemoryPanel(tab); + await removeTab(tab); + + finish(); + }; +} + +function dumpn(msg) { + dump(`MEMORY-TEST: ${msg}\n`); +} + +/** + * Returns a promise that will resolve when the provided store matches + * the expected array. expectedStates is an array of dominatorTree states. + * Expectations : + * - store.getState().snapshots.length == expected.length + * - snapshots[i].dominatorTree.state == expected[i] + * + * @param {Store} store + * @param {Array<string>} expectedStates [description] + * @return {Promise} + */ +function waitUntilDominatorTreeState(store, expected) { + const predicate = () => { + const snapshots = store.getState().snapshots; + return ( + snapshots.length === expected.length && + expected.every((state, i) => { + return ( + snapshots[i].dominatorTree && + snapshots[i].dominatorTree.state === state + ); + }) + ); + }; + info(`Waiting for dominator trees to be of state: ${expected}`); + return waitUntilState(store, predicate); +} + +function takeSnapshot(window) { + const { gStore, document } = window; + const snapshotCount = gStore.getState().snapshots.length; + info("Taking snapshot..."); + document.querySelector(".devtools-toolbar .take-snapshot").click(); + return waitUntilState( + gStore, + () => gStore.getState().snapshots.length === snapshotCount + 1 + ); +} + +function clearSnapshots(window) { + const { gStore, document } = window; + document.querySelector(".devtools-toolbar .clear-snapshots").click(); + return waitUntilState(gStore, () => + gStore + .getState() + .snapshots.every(snapshot => snapshot.state !== states.READ) + ); +} + +/** + * Sets the current requested display and waits for the selected snapshot to use + * it and complete the new census that entails. + */ +function setCensusDisplay(window, display) { + info(`Setting census display to ${display}...`); + const { gStore, gHeapAnalysesClient } = window; + // XXX: Should handle this via clicking the DOM, but React doesn't + // fire the onChange event, so just change it in the store. + // window.document.querySelector(`.select-display`).value = type; + gStore.dispatch( + require("resource://devtools/client/memory/actions/census-display.js").setCensusDisplayAndRefresh( + gHeapAnalysesClient, + display + ) + ); + + return waitUntilState(window.gStore, () => { + const selected = window.gStore.getState().snapshots.find(s => s.selected); + return ( + selected.state === states.READ && + selected.census && + selected.census.state === censusState.SAVED && + selected.census.display === display + ); + }); +} + +/** + * Get the snapshot tatus text currently displayed, or null if none is + * displayed. + * + * @param {Document} document + */ +function getDisplayedSnapshotStatus(document) { + const status = document.querySelector(".snapshot-status"); + return status ? status.textContent.trim() : null; +} + +/** + * Get the index of the currently selected snapshot. + * + * @return {Number} + */ +function getSelectedSnapshotIndex(store) { + const snapshots = store.getState().snapshots; + const selectedSnapshot = snapshots.find(s => s.selected); + return snapshots.indexOf(selectedSnapshot); +} + +/** + * Returns a promise that will resolve when the snapshot with provided index + * becomes selected. + * + * @return {Promise} + */ +function waitUntilSnapshotSelected(store, snapshotIndex) { + return waitUntilState( + store, + state => + state.snapshots[snapshotIndex] && + state.snapshots[snapshotIndex].selected === true + ); +} + +/** + * Wait until the state has censuses in a certain state. + * + * @return {Promise} + */ +function waitUntilCensusState(store, getCensus, expected) { + const predicate = () => { + const snapshots = store.getState().snapshots; + + info( + "Current census state:" + + snapshots.map(x => (getCensus(x) ? getCensus(x).state : null)) + ); + + return ( + snapshots.length === expected.length && + expected.every((state, i) => { + const census = getCensus(snapshots[i]); + return ( + state === "*" || + (!census && !state) || + (census && census.state === state) + ); + }) + ); + }; + info(`Waiting for snapshot censuses to be of state: ${expected}`); + return waitUntilState(store, predicate); +} + +/** + * Mock out the requestAnimationFrame. + * + * @return {Object} + * @function nextFrame + * Call the last queued function + * @function raf + * The mocked raf function + * @function timesCalled + * How many times the RAF has been called + */ +function createRAFMock() { + let queuedFns = []; + const mock = { timesCalled: 0 }; + + mock.nextFrame = function () { + const thisQueue = queuedFns; + queuedFns = []; + for (let i = 0; i < thisQueue.length; i++) { + thisQueue[i](); + } + }; + + mock.raf = function (fn) { + mock.timesCalled++; + queuedFns.push(fn); + }; + return mock; +} + +/** + * Test to see if two floats are equivalent. + * + * @param {Float} a + * @param {Float} b + * @return {Boolean} + */ +function floatEquality(a, b) { + const EPSILON = 0.00000000001; + const equals = Math.abs(a - b) < EPSILON; + if (!equals) { + info(`${a} not equal to ${b}`); + } + return equals; +} |