diff options
Diffstat (limited to 'devtools/client/memory/test')
94 files changed, 6683 insertions, 0 deletions
diff --git a/devtools/client/memory/test/browser/.eslintrc.js b/devtools/client/memory/test/browser/.eslintrc.js new file mode 100644 index 000000000..698ae9181 --- /dev/null +++ b/devtools/client/memory/test/browser/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the shared list of defined globals for mochitests. + "extends": "../../../../.eslintrc.mochitests.js" +}; diff --git a/devtools/client/memory/test/browser/browser.ini b/devtools/client/memory/test/browser/browser.ini new file mode 100644 index 000000000..dc803a335 --- /dev/null +++ b/devtools/client/memory/test/browser/browser.ini @@ -0,0 +1,31 @@ +[DEFAULT] +tags = devtools devtools-memory +subsuite = devtools +support-files = + head.js + doc_big_tree.html + doc_empty.html + doc_steady_allocation.html + !/devtools/client/framework/test/shared-head.js + !/devtools/client/framework/test/shared-redux-head.js + +[browser_memory_allocationStackDisplay_01.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] +[browser_memory_dominator_trees_02.js] +[browser_memory_filter_01.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 000000000..60cd5c456 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_allocationStackDisplay_01.js @@ -0,0 +1,39 @@ +/* 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("devtools/client/memory/actions/allocations"); +const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot"); +const censusDisplayActions = require("devtools/client/memory/actions/census-display"); +const { viewState } = require("devtools/client/memory/constants"); +const { changeView } = require("devtools/client/memory/actions/view"); + +const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html"; + +this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) { + const heapWorker = panel.panelWin.gHeapAnalysesClient; + const front = panel.panelWin.gFront; + 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"); + + yield dispatch(toggleRecordingAllocationStacks(front)); + ok(getState().allocations.recording); + + // Let some allocations build up. + yield waitForTime(500); + + yield 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 000000000..ca577bcd2 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_clear_snapshots.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests taking and then clearing snapshots. + */ + + const { treeMapState } = require("devtools/client/memory/constants"); + const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html"; + + this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) { + const { gStore, document } = panel.panelWin; + const { getState, dispatch } = 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); + yield 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("#memory-tool-container .list li"); + is(snapshotEls.length, 2, "Two snapshots visible"); + + info("Click on Clear Snapshots"); + yield 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 000000000..0deb2a078 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_diff_01.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test diffing. + +"use strict"; + +const { + snapshotState, + diffingState, + treeMapState +} = require("devtools/client/memory/constants"); + +const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html"; + +this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) { + const heapWorker = panel.panelWin.gHeapAnalysesClient; + const front = panel.panelWin.gFront; + const store = panel.panelWin.gStore; + const { getState, dispatch } = 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); + yield waitForTime(1000); + EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin); + + // Enable diffing mode. + const diffButton = doc.getElementById("diff-snapshots"); + EventUtils.synthesizeMouseAtCenter(diffButton, {}, panel.panelWin); + yield 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"); + + yield 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); + yield 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); + yield 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")); + + yield 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 000000000..b5f9e34d9 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_displays_01.js @@ -0,0 +1,41 @@ +/* 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, censusState } = require("devtools/client/memory/constants"); +const { changeView } = require("devtools/client/memory/actions/view"); + +this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) { + const { gStore, document } = panel.panelWin; + + const { dispatch } = panel.panelWin.gStore; + + function $$(selector) { + return [...document.querySelectorAll(selector)]; + } + dispatch(changeView(viewState.CENSUS)); + + yield takeSnapshot(panel.panelWin); + + yield waitUntilState(gStore, state => + state.snapshots[0].census && + state.snapshots[0].census.state === censusState.SAVED); + + info("Check coarse type heap view"); + ["Function", "js::Shape", "Object", "strings"].forEach(findNameCell); + + yield 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 span") + .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 000000000..3380d6e21 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_dominator_trees_01.js @@ -0,0 +1,147 @@ +/* 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("devtools/client/memory/constants"); +const { + expandDominatorTreeNode, +} = require("devtools/client/memory/actions/snapshot"); +const { changeView } = require("devtools/client/memory/actions/view"); + +const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_big_tree.html"; + +this.test = makeMemoryTest(TEST_URL, 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. + + yield 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 (let 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 (let 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); + yield 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); + yield 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. + + yield 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 (let 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); + yield 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 000000000..72bc52567 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_dominator_trees_02.js @@ -0,0 +1,64 @@ +/* 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("devtools/client/memory/constants"); +const { changeView } = require("devtools/client/memory/actions/view"); + +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, 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. + yield 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); + + yield 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); + + yield 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 000000000..448e2aaa9 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_filter_01.js @@ -0,0 +1,81 @@ +/* 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, + snapshotState, + viewState, + censusState, +} = require("devtools/client/memory/constants"); +const { changeViewAndRefresh, changeView } = require("devtools/client/memory/actions/view"); + +const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html"; + +this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) { + const heapWorker = panel.panelWin.gHeapAnalysesClient; + const front = panel.panelWin.gFront; + const store = panel.panelWin.gStore; + const { getState, dispatch } = store; + const doc = panel.panelWin.document; + + dispatch(changeView(viewState.CENSUS)); + + const takeSnapshotButton = doc.getElementById("take-snapshot"); + EventUtils.synthesizeMouseAtCenter(takeSnapshotButton, {}, panel.panelWin); + + yield 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); + + yield 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"); + + yield 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. + yield waitUntilDominatorTreeState(store, [dominatorTreeState.LOADED]); + ok(true, "computed and fetched the dominator tree."); + + dispatch(changeViewAndRefresh(viewState.CENSUS, heapWorker)); + ok(true, "change view back to census"); + + yield 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_individuals_01.js b/devtools/client/memory/test/browser/browser_memory_individuals_01.js new file mode 100644 index 000000000..eae8248c3 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_individuals_01.js @@ -0,0 +1,67 @@ +/* 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, + censusState, +} = require("devtools/client/memory/constants"); +const { changeViewAndRefresh, changeView } = require("devtools/client/memory/actions/view"); + +const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html"; + +this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) { + const heapWorker = panel.panelWin.gHeapAnalysesClient; + const front = panel.panelWin.gFront; + const store = panel.panelWin.gStore; + const { getState, 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); + + yield 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); + + yield 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); + + yield 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 000000000..eb7cb8aca --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_keyboard-snapshot-list.js @@ -0,0 +1,99 @@ +/* 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 { + snapshotState, + viewState, + censusState +} = require("devtools/client/memory/constants"); +const { + takeSnapshotAndCensus +} = require("devtools/client/memory/actions/snapshot"); +const { changeView } = require("devtools/client/memory/actions/view"); + +const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html"; + +this.test = makeMemoryTest(TEST_URL, 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 front = panel.panelWin.gFront; + const store = panel.panelWin.gStore; + const { dispatch } = store; + 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)); + + yield 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"); + + yield 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); + yield 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); + yield 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); + yield waitUntilSnapshotSelected(store, 1); + ok(true, "Second snapshot is selected after ACCEL+DOWN"); + + info("Click on first node."); + let firstNode = doc.querySelector(".tree .heap-tree-item-name"); + EventUtils.synthesizeMouseAtCenter(firstNode, {}, panel.panelWin); + yield 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); + yield 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); + yield 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); + yield 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 000000000..243570f83 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_keyboard.js @@ -0,0 +1,107 @@ +/* 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 { + snapshotState, + censusState, + viewState +} = require("devtools/client/memory/constants"); +const { + takeSnapshotAndCensus +} = require("devtools/client/memory/actions/snapshot"); +const { changeView } = require("devtools/client/memory/actions/view"); + +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, function* ({ tab, panel }) { + const heapWorker = panel.panelWin.gHeapAnalysesClient; + const front = panel.panelWin.gFront; + const store = panel.panelWin.gStore; + const { getState, dispatch } = store; + const doc = panel.panelWin.document; + + dispatch(changeView(viewState.CENSUS)); + + is(getState().censusDisplay.breakdown.by, "coarseType"); + + yield dispatch(takeSnapshotAndCensus(front, heapWorker)); + let census = getState().snapshots[0].census; + let root1 = census.report.children[0]; + let root2 = census.report.children[0]; + let root3 = census.report.children[0]; + let root4 = census.report.children[0]; + let child1 = root1.children[0]; + + info("Click on first node."); + let firstNode = doc.querySelector(".tree .heap-tree-item-name"); + EventUtils.synthesizeMouseAtCenter(firstNode, {}, panel.panelWin); + yield 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); + yield 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); + yield 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); + yield 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); + yield 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); + yield 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); + yield waitUntilFocused(store, root1); + ok(true, "First root is selected after pressing UP arrow."); + + info("Press RIGHT key"); + EventUtils.synthesizeKey("VK_RIGHT", {}, panel.panelWin); + yield waitUntilExpanded(store, root1); + ok(true, "Root node is expanded."); + + info("Press RIGHT key, expect first child focused."); + EventUtils.synthesizeKey("VK_RIGHT", {}, panel.panelWin); + yield 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); + yield 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 000000000..cd8770285 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_no_allocation_stacks.js @@ -0,0 +1,39 @@ +/* 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("devtools/client/memory/actions/snapshot"); +const censusDisplayActions = require("devtools/client/memory/actions/census-display"); +const { viewState } = require("devtools/client/memory/constants"); +const { changeView } = require("devtools/client/memory/actions/view"); + +const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html"; + +this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) { + const heapWorker = panel.panelWin.gHeapAnalysesClient; + const front = panel.panelWin.gFront; + const { getState, dispatch } = panel.panelWin.gStore; + const doc = panel.panelWin.document; + + dispatch(changeView(viewState.CENSUS)); + + ok(!getState().allocations.recording, + "Should not be recording allocagtions"); + + yield dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield 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 000000000..ffd481c74 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_no_auto_expand.js @@ -0,0 +1,37 @@ +/* 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("devtools/client/memory/actions/snapshot"); +const { viewState } = require("devtools/client/memory/constants"); +const { changeView } = require("devtools/client/memory/actions/view"); + +const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html"; + +this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) { + const heapWorker = panel.panelWin.gHeapAnalysesClient; + const front = panel.panelWin.gFront; + const { getState, dispatch } = panel.panelWin.gStore; + const doc = panel.panelWin.document; + + dispatch(changeView(viewState.CENSUS)); + + yield 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 (let 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 000000000..c3ed37530 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_percents_01.js @@ -0,0 +1,47 @@ +/* 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("devtools/client/memory/actions/snapshot"); +const { viewState } = require("devtools/client/memory/constants"); +const { changeView } = require("devtools/client/memory/actions/view"); + +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 (let 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, function* ({ tab, panel }) { + const heapWorker = panel.panelWin.gHeapAnalysesClient; + const front = panel.panelWin.gFront; + const { getState, dispatch } = panel.panelWin.gStore; + const doc = panel.panelWin.document; + + dispatch(changeView(viewState.CENSUS)); + + yield 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 000000000..7ab768b01 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_refresh_does_not_leak.js @@ -0,0 +1,108 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// 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 HeapSnapshotFileUtils = require("devtools/shared/heapsnapshot/HeapSnapshotFileUtils"); +const { getLabelAndShallowSize } = require("devtools/shared/heapsnapshot/DominatorTreeNode"); + +const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_empty.html"; + +function* getWindowsInSnapshot(front) { + dumpn("Taking snapshot."); + const path = yield 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, function* ({ tab, panel }) { + const heapWorker = panel.panelWin.gHeapAnalysesClient; + const front = panel.panelWin.gFront; + const store = panel.panelWin.gStore; + const { getState, dispatch } = store; + const doc = panel.panelWin.document; + + const startWindows = yield getWindowsInSnapshot(front); + dumpn("Initial windows found = " + startWindows.map(w => "0x" + w.toString(16)).join(", ")); + is(startWindows.length, 1); + + yield refreshTab(tab); + + const endWindows = yield 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 = yield 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 (let 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 (let 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 000000000..9eaea2ad2 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_simple_01.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests taking snapshots and default states. + */ + +const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html"; +const { viewState, censusState } = require("devtools/client/memory/constants"); +const { changeView } = require("devtools/client/memory/actions/view"); + +this.test = makeMemoryTest(TEST_URL, 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"); + + yield 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"); + + yield 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"); + + yield 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 000000000..8ebb7622a --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_transferHeapSnapshot_e10s_01.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// 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, function* ({ tab, panel }) { + const memoryFront = panel.panelWin.gFront; + ok(memoryFront, "Should get the MemoryFront"); + + const snapshotFilePath = yield 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(!!(yield OS.File.stat(snapshotFilePath)), + "Should have the heap snapshot file"); + + const snapshot = ChromeUtils.readHeapSnapshot(snapshotFilePath); + ok(snapshot instanceof HeapSnapshot, + "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 000000000..ca564a658 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_tree_map-01.js @@ -0,0 +1,102 @@ +/* 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("devtools/client/memory/components/tree-map/canvas-utils"); +const D3_SCRIPT = '<script type="application/javascript" ' + + 'src="chrome://devtools/content/shared/vendor/d3.js>'; +const TEST_URL = `data:text/html,<html><body>${D3_SCRIPT}</body></html>`; + +this.test = makeMemoryTest(TEST_URL, 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"); + + let 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++; + }); + + let main = canvases.main.canvas; + let zoom = canvases.zoom.canvas; + let 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 000000000..15f8b0457 --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_tree_map-02.js @@ -0,0 +1,161 @@ +/* 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("devtools/client/memory/components/tree-map/canvas-utils"); +const DragZoom = require("devtools/client/memory/components/tree-map/drag-zoom"); + +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, 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 + }); + + let rafMock = createRAFMock(); + + panelDoc.body.appendChild(div); + + let canvases = new CanvasUtils(div, 0); + let dragZoom = new DragZoom(canvases.container, 0, rafMock.raf); + let 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, 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, 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, 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"); + { + let initialX = dragZoom.translateX; + let initialY = dragZoom.translateY; + 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"); + { + let previousZoom = dragZoom.zoom; + let 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"); + { + let initialX = dragZoom.translateX; + let 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 000000000..4ad60402a --- /dev/null +++ b/devtools/client/memory/test/browser/doc_big_tree.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> + <body> + <script> + window.big = (function makeBig(depth = 0) { + var 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 000000000..ef123d8d2 --- /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 000000000..65703c878 --- /dev/null +++ b/devtools/client/memory/test/browser/doc_steady_allocation.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html> + <body> + <script> + var objects = window.objects = []; + + var allocate = this.allocate = function allocate() { + for (var 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 000000000..cb9b470ff --- /dev/null +++ b/devtools/client/memory/test/browser/head.js @@ -0,0 +1,248 @@ +/* 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/framework/test/shared-head.js", + this); + +// Load the shared Redux helpers into this compartment. +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/framework/test/shared-redux-head.js", + this); + +var { censusDisplays, snapshotState: states } = require("devtools/client/memory/constants"); +var { L10N } = require("devtools/client/memory/utils"); + +Services.prefs.setBoolPref("devtools.memory.enabled", true); + +/** + * Open the memory panel for the given tab. + */ +this.openMemoryPanel = Task.async(function* (tab) { + info("Opening memory panel."); + const target = TargetFactory.forTab(tab); + const toolbox = yield gDevTools.showToolbox(target, "memory"); + info("Memory panel shown successfully."); + let panel = toolbox.getCurrentPanel(); + return { tab, panel }; +}); + +/** + * Close the memory panel for the given tab. + */ +this.closeMemoryPanel = Task.async(function* (tab) { + info("Closing memory panel."); + const target = TargetFactory.forTab(tab); + const toolbox = gDevTools.getToolbox(target); + yield 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, function* ({ tab, panel }) { + * // Your tests go here... + * }); + */ +function makeMemoryTest(url, generator) { + return Task.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 = yield addTab(url); + const results = yield openMemoryPanel(tab); + + try { + yield* generator(results); + } catch (err) { + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(err)); + } + + yield closeMemoryPanel(tab); + yield 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) { + let predicate = () => { + let 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) { + let { gStore, document } = window; + let 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) { + let { 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}...`); + let { 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("devtools/client/memory/actions/census-display") + .setCensusDisplayAndRefresh(gHeapAnalysesClient, display)); + + return waitUntilState(window.gStore, () => { + let 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) { + let snapshots = store.getState().snapshots; + let 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) { + let predicate = () => { + let 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) => { + let 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 = []; + let mock = { timesCalled: 0 }; + + mock.nextFrame = function () { + let thisQueue = queuedFns; + queuedFns = []; + for (var 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; +} diff --git a/devtools/client/memory/test/chrome/chrome.ini b/devtools/client/memory/test/chrome/chrome.ini new file mode 100644 index 000000000..7803bcda3 --- /dev/null +++ b/devtools/client/memory/test/chrome/chrome.ini @@ -0,0 +1,20 @@ +[DEFAULT] +support-files = + head.js + +[test_CensusTreeItem_01.html] +[test_DominatorTree_01.html] +[test_DominatorTree_02.html] +[test_DominatorTree_03.html] +[test_DominatorTreeItem_01.html] +[test_Heap_01.html] +[test_Heap_02.html] +[test_Heap_03.html] +[test_Heap_04.html] +[test_Heap_05.html] +[test_List_01.html] +[test_ShortestPaths_01.html] +[test_ShortestPaths_02.html] +[test_SnapshotListItem_01.html] +[test_Toolbar_01.html] +[test_TreeMap_01.html] diff --git a/devtools/client/memory/test/chrome/head.js b/devtools/client/memory/test/chrome/head.js new file mode 100644 index 000000000..4ca5a7a7e --- /dev/null +++ b/devtools/client/memory/test/chrome/head.js @@ -0,0 +1,335 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +var { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {}); +var { require } = BrowserLoader({ + baseURI: "resource://devtools/client/memory/", + window +}); +var { Assert } = require("resource://testing-common/Assert.jsm"); +var Services = require("Services"); +var { Task } = require("devtools/shared/task"); + +var EXPECTED_DTU_ASSERT_FAILURE_COUNT = 0; + +SimpleTest.registerCleanupFunction(function () { + if (DevToolsUtils.assertionFailureCount !== EXPECTED_DTU_ASSERT_FAILURE_COUNT) { + ok(false, "Should have had the expected number of DevToolsUtils.assert() failures. Expected " + + EXPECTED_DTU_ASSERT_FAILURE_COUNT + ", got " + DevToolsUtils.assertionFailureCount); + } +}); + +var DevToolsUtils = require("devtools/shared/DevToolsUtils"); +var { immutableUpdate } = DevToolsUtils; +var flags = require("devtools/shared/flags"); +flags.testing = true; + +var constants = require("devtools/client/memory/constants"); +var { + censusDisplays, + diffingState, + labelDisplays, + dominatorTreeState, + snapshotState, + viewState, + censusState +} = constants; + +const { + L10N, +} = require("devtools/client/memory/utils"); + +var models = require("devtools/client/memory/models"); + +var Immutable = require("devtools/client/shared/vendor/immutable"); +var React = require("devtools/client/shared/vendor/react"); +var ReactDOM = require("devtools/client/shared/vendor/react-dom"); +var Heap = React.createFactory(require("devtools/client/memory/components/heap")); +var CensusTreeItem = React.createFactory(require("devtools/client/memory/components/census-tree-item")); +var DominatorTreeComponent = React.createFactory(require("devtools/client/memory/components/dominator-tree")); +var DominatorTreeItem = React.createFactory(require("devtools/client/memory/components/dominator-tree-item")); +var ShortestPaths = React.createFactory(require("devtools/client/memory/components/shortest-paths")); +var TreeMap = React.createFactory(require("devtools/client/memory/components/tree-map")); +var SnapshotListItem = React.createFactory(require("devtools/client/memory/components/snapshot-list-item")); +var List = React.createFactory(require("devtools/client/memory/components/list")); +var Toolbar = React.createFactory(require("devtools/client/memory/components/toolbar")); + +// All tests are asynchronous. +SimpleTest.waitForExplicitFinish(); + +var noop = () => {}; + +var TEST_CENSUS_TREE_ITEM_PROPS = Object.freeze({ + item: Object.freeze({ + bytes: 10, + count: 1, + totalBytes: 10, + totalCount: 1, + name: "foo", + children: [ + Object.freeze({ + bytes: 10, + count: 1, + totalBytes: 10, + totalCount: 1, + name: "bar", + }) + ] + }), + depth: 0, + arrow: ">", + focused: true, + getPercentBytes: () => 50, + getPercentCount: () => 50, + showSign: false, + onViewSourceInDebugger: noop, + inverted: false, +}); + +// Counter for mock DominatorTreeNode ids. +var 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: ["other", "SomeType"], + 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; +} + +var TEST_DOMINATOR_TREE = Object.freeze({ + dominatorTreeId: 666, + root: (function makeTree(depth = 0) { + let children; + if (depth <= 3) { + children = [ + makeTree(depth + 1), + makeTree(depth + 1), + makeTree(depth + 1), + ]; + } + return makeTestDominatorTreeNode({}, children); + }()), + expanded: new Set(), + focused: null, + error: null, + display: labelDisplays.coarseType, + activeFetchRequestCount: null, + state: dominatorTreeState.LOADED, +}); + +var TEST_DOMINATOR_TREE_PROPS = Object.freeze({ + dominatorTree: TEST_DOMINATOR_TREE, + onLoadMoreSiblings: noop, + onViewSourceInDebugger: noop, + onExpand: noop, + onCollapse: noop, +}); + +var TEST_SHORTEST_PATHS_PROPS = Object.freeze({ + graph: Object.freeze({ + nodes: [ + { id: 1, label: ["other", "SomeType"] }, + { id: 2, label: ["other", "SomeType"] }, + { id: 3, label: ["other", "SomeType"] }, + ], + edges: [ + { from: 1, to: 2, name: "1->2" }, + { from: 1, to: 3, name: "1->3" }, + { from: 2, to: 3, name: "2->3" }, + ], + }), +}); + +var TEST_SNAPSHOT = Object.freeze({ + id: 1337, + selected: true, + path: "/fake/path/to/snapshot", + census: Object.freeze({ + report: Object.freeze({ + objects: Object.freeze({ count: 4, bytes: 400 }), + scripts: Object.freeze({ count: 3, bytes: 300 }), + strings: Object.freeze({ count: 2, bytes: 200 }), + other: Object.freeze({ count: 1, bytes: 100 }), + }), + display: Object.freeze({ + displayName: "Test Display", + tooltip: "Test display tooltup", + inverted: false, + breakdown: Object.freeze({ + by: "coarseType", + objects: Object.freeze({ by: "count", count: true, bytes: true }), + scripts: Object.freeze({ by: "count", count: true, bytes: true }), + strings: Object.freeze({ by: "count", count: true, bytes: true }), + other: Object.freeze({ by: "count", count: true, bytes: true }), + }), + }), + state: censusState.SAVED, + inverted: false, + filter: null, + expanded: new Set(), + focused: null, + parentMap: Object.freeze(Object.create(null)) + }), + dominatorTree: TEST_DOMINATOR_TREE, + error: null, + imported: false, + creationTime: 0, + state: snapshotState.READ, +}); + +var TEST_HEAP_PROPS = Object.freeze({ + onSnapshotClick: noop, + onLoadMoreSiblings: noop, + onCensusExpand: noop, + onCensusCollapse: noop, + onDominatorTreeExpand: noop, + onDominatorTreeCollapse: noop, + onCensusFocus: noop, + onDominatorTreeFocus: noop, + onViewSourceInDebugger: noop, + diffing: null, + view: { state: viewState.CENSUS, }, + snapshot: TEST_SNAPSHOT, + sizes: Object.freeze({ shortestPathsSize: .5 }), + onShortestPathsResize: noop, +}); + +var TEST_TOOLBAR_PROPS = Object.freeze({ + censusDisplays: [ + censusDisplays.coarseType, + censusDisplays.allocationStack, + censusDisplays.invertedAllocationStack, + ], + censusDisplay: censusDisplays.coarseType, + onTakeSnapshotClick: noop, + onImportClick: noop, + onCensusDisplayChange: noop, + onToggleRecordAllocationStacks: noop, + allocations: models.allocations, + onToggleInverted: noop, + inverted: false, + filterString: null, + setFilterString: noop, + diffing: null, + onToggleDiffing: noop, + view: { state: viewState.CENSUS, }, + onViewChange: noop, + labelDisplays: [ + labelDisplays.coarseType, + labelDisplays.allocationStack, + ], + labelDisplay: labelDisplays.coarseType, + onLabelDisplayChange: noop, + snapshots: [], +}); + +function makeTestCensusNode() { + return { + name: "Function", + bytes: 100, + totalBytes: 100, + count: 100, + totalCount: 100, + children: [] + }; +} + +var TEST_TREE_MAP_PROPS = Object.freeze({ + treeMap: Object.freeze({ + report: { + name: null, + bytes: 0, + totalBytes: 400, + count: 0, + totalCount: 400, + children: [ + { + name: "objects", + bytes: 0, + totalBytes: 200, + count: 0, + totalCount: 200, + children: [ makeTestCensusNode(), makeTestCensusNode() ] + }, + { + name: "other", + bytes: 0, + totalBytes: 200, + count: 0, + totalCount: 200, + children: [ makeTestCensusNode(), makeTestCensusNode() ], + } + ] + } + }) +}); + +var TEST_SNAPSHOT_LIST_ITEM_PROPS = Object.freeze({ + onClick: noop, + onSave: noop, + onDelete: noop, + item: TEST_SNAPSHOT, + index: 1234, +}); + +function onNextAnimationFrame(fn) { + return () => + requestAnimationFrame(() => + requestAnimationFrame(fn)); +} + +/** + * Render the provided ReactElement in the provided HTML container. + * Returns a Promise that will resolve the rendered element as a React + * component. + */ +function renderComponent(element, container) { + return new Promise(resolve => { + let component = ReactDOM.render(element, container, + onNextAnimationFrame(() => { + dumpn("Rendered = " + container.innerHTML); + resolve(component); + })); + }); +} + +function setState(component, newState) { + return new Promise(resolve => { + component.setState(newState, onNextAnimationFrame(resolve)); + }); +} + +function setProps(component, newProps) { + return new Promise(resolve => { + component.setProps(newProps, onNextAnimationFrame(resolve)); + }); +} + +function dumpn(msg) { + dump(`MEMORY-TEST: ${msg}\n`); +} diff --git a/devtools/client/memory/test/chrome/test_CensusTreeItem_01.html b/devtools/client/memory/test/chrome/test_CensusTreeItem_01.html new file mode 100644 index 000000000..fef996330 --- /dev/null +++ b/devtools/client/memory/test/chrome/test_CensusTreeItem_01.html @@ -0,0 +1,65 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test that children pointers show up at the correct times. +--> +<head> + <meta charset="utf-8"> + <title>Tree component test</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> + <!-- Give the container height so that the whole tree is rendered. --> + <div id="container" style="height: 900px;"></div> + + <pre id="test"> + <script src="head.js" type="application/javascript;version=1.8"></script> + <script type="application/javascript;version=1.8"> + window.onload = Task.async(function* () { + try { + const container = document.getElementById("container"); + + yield renderComponent(CensusTreeItem(immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS, { + inverted: true, + depth: 0, + })), container); + + ok(!container.querySelector(".children-pointer"), + "Don't show children pointer for roots when we are inverted"); + + yield renderComponent(CensusTreeItem(immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS, { + inverted: true, + depth: 1, + })), container); + + ok(container.querySelector(".children-pointer"), + "Do show children pointer for non-roots when we are inverted"); + + yield renderComponent(CensusTreeItem(immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS, { + inverted: false, + item: immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS.item, { children: undefined }), + })), container); + + ok(!container.querySelector(".children-pointer"), + "Don't show children pointer when non-inverted and no children"); + + yield renderComponent(CensusTreeItem(immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS, { + inverted: false, + depth: 0, + item: immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS.item, { children: [{}] }), + })), container); + + ok(container.querySelector(".children-pointer"), + "Do show children pointer when non-inverted and have children"); + + } catch(e) { + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e)); + } finally { + SimpleTest.finish(); + } + }); + </script> + </pre> +</body> +</html> diff --git a/devtools/client/memory/test/chrome/test_DominatorTreeItem_01.html b/devtools/client/memory/test/chrome/test_DominatorTreeItem_01.html new file mode 100644 index 000000000..56cba7391 --- /dev/null +++ b/devtools/client/memory/test/chrome/test_DominatorTreeItem_01.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test that we don't display `JS::ubi::RootList` for the root, and instead show "GC Roots". +--> +<head> + <meta charset="utf-8"> + <title>Tree component test</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> + <!-- Give the container height so that the whole tree is rendered. --> + <div id="container" style="height: 900px;"></div> + + <pre id="test"> + <script src="head.js" type="application/javascript;version=1.8"></script> + <script type="application/javascript;version=1.8"> + window.onload = Task.async(function* () { + try { + const container = document.getElementById("container"); + + yield renderComponent(DominatorTreeItem({ + item: makeTestDominatorTreeNode({ label: ["other", "JS::ubi::RootList"] }), + depth: 0, + arrow: React.DOM.div(), + focused: true, + getPercentSize: _ => 50, + onViewSourceInDebugger: _ => { }, + }), container); + + ok(container.textContent.indexOf("JS::ubi::RootList") == -1, + "Should not display `JS::ubi::RootList`"); + ok(container.textContent.indexOf("GC Roots") >= 0, + "Should display `GC Roots` instead"); + } catch(e) { + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e)); + } finally { + SimpleTest.finish(); + } + }); + </script> + </pre> +</body> +</html> diff --git a/devtools/client/memory/test/chrome/test_DominatorTree_01.html b/devtools/client/memory/test/chrome/test_DominatorTree_01.html new file mode 100644 index 000000000..582576e49 --- /dev/null +++ b/devtools/client/memory/test/chrome/test_DominatorTree_01.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test that we show a place holder for a subtree we are lazily fetching. +--> +<head> + <meta charset="utf-8"> + <title>Tree component test</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> + <!-- Give the container height so that the whole tree is rendered. --> + <div id="container" style="height: 900px;"></div> + + <pre id="test"> + <script src="head.js" type="application/javascript;version=1.8"></script> + <script type="application/javascript;version=1.8"> + window.onload = Task.async(function* () { + try { + const container = document.getElementById("container"); + + const root = makeTestDominatorTreeNode({ moreChildrenAvailable: true}); + ok(!root.children); + + const expanded = new Set(); + expanded.add(root.nodeId); + + yield renderComponent(DominatorTreeComponent(immutableUpdate(TEST_DOMINATOR_TREE_PROPS, { + dominatorTree: immutableUpdate(TEST_DOMINATOR_TREE_PROPS.dominatorTree, { + expanded, + root, + state: dominatorTreeState.INCREMENTAL_FETCHING, + activeFetchRequestCount: 1, + }), + })), container); + + ok(container.querySelector(".subtree-fetching"), + "Expanded nodes with more children available, but no children " + + "loaded, should get a placeholder"); + } catch(e) { + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e)); + } finally { + SimpleTest.finish(); + } + }); + </script> + </pre> +</body> +</html> diff --git a/devtools/client/memory/test/chrome/test_DominatorTree_02.html b/devtools/client/memory/test/chrome/test_DominatorTree_02.html new file mode 100644 index 000000000..ffdac3263 --- /dev/null +++ b/devtools/client/memory/test/chrome/test_DominatorTree_02.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test that we show a link to load more children when some (but not all) are loaded. +--> +<head> + <meta charset="utf-8"> + <title>Tree component test</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> + <!-- Give the container height so that the whole tree is rendered. --> + <div id="container" style="height: 900px;"></div> + + <pre id="test"> + <script src="head.js" type="application/javascript;version=1.8"></script> + <script type="application/javascript;version=1.8"> + window.onload = Task.async(function* () { + try { + const container = document.getElementById("container"); + + const root = makeTestDominatorTreeNode({ moreChildrenAvailable: true }, [ + makeTestDominatorTreeNode({}), + ]); + ok(root.children); + ok(root.moreChildrenAvailable); + + const expanded = new Set(); + expanded.add(root.nodeId); + + yield renderComponent(DominatorTreeComponent(immutableUpdate(TEST_DOMINATOR_TREE_PROPS, { + dominatorTree: immutableUpdate(TEST_DOMINATOR_TREE_PROPS.dominatorTree, { + expanded, + root, + }), + })), container); + + ok(container.querySelector(".more-children"), + "Should get a link to load more children"); + } catch(e) { + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e)); + } finally { + SimpleTest.finish(); + } + }); + </script> + </pre> +</body> +</html> diff --git a/devtools/client/memory/test/chrome/test_DominatorTree_03.html b/devtools/client/memory/test/chrome/test_DominatorTree_03.html new file mode 100644 index 000000000..e9656dad8 --- /dev/null +++ b/devtools/client/memory/test/chrome/test_DominatorTree_03.html @@ -0,0 +1,75 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test that expanded DominatorTreeItems are correctly rendered and updated +--> +<head> + <meta charset="utf-8"> + <title>Tree component test</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> + <!-- Give the container height so that the whole tree is rendered. --> + <div id="container" style="height: 900px;"></div> + + <pre id="test"> + <script src="head.js" type="application/javascript;version=1.8"></script> + <script type="application/javascript;version=1.8"> + window.onload = Task.async(function* () { + try { + const container = document.getElementById("container"); + + // simple tree with one root and one child + const root = makeTestDominatorTreeNode( + { moreChildrenAvailable: false }, + [ + makeTestDominatorTreeNode({ moreChildrenAvailable: false }), + ]); + ok(root.children); + + // root node is expanded + const expanded = new Set(); + expanded.add(root.nodeId); + + let component = yield renderComponent( + DominatorTreeComponent(immutableUpdate( + TEST_DOMINATOR_TREE_PROPS, + { + dominatorTree: immutableUpdate( + TEST_DOMINATOR_TREE_PROPS.dominatorTree, + { expanded, root } + ), + })), container); + ok(true, "Dominator tree rendered"); + + is(container.querySelectorAll(".tree-node").length, 2, + "Should display two rows"); + is(container.querySelectorAll(".arrow.open").length, 1, + "Should display one expanded arrow"); + + yield setProps(component, immutableUpdate( + TEST_DOMINATOR_TREE_PROPS, + { + dominatorTree: immutableUpdate( + TEST_DOMINATOR_TREE_PROPS.dominatorTree, + { expanded: new Set(), root } + ) + })); + ok(true, "Dominator tree props updated to collapse all nodes"); + + is(container.querySelectorAll(".tree-node").length, 1, + "Should display only one row"); + is(container.querySelectorAll(".arrow.open").length, 0, + "Should display no expanded arrow"); + + } catch(e) { + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e)); + } finally { + SimpleTest.finish(); + } + }); + </script> + </pre> +</body> +</html> diff --git a/devtools/client/memory/test/chrome/test_Heap_01.html b/devtools/client/memory/test/chrome/test_Heap_01.html new file mode 100644 index 000000000..5d5e72389 --- /dev/null +++ b/devtools/client/memory/test/chrome/test_Heap_01.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test that rendering a dominator tree error is handled correctly. +--> +<head> + <meta charset="utf-8"> + <title>Tree component test</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> + <div id="container"></div> + <pre id="test"> + <script src="head.js" type="application/javascript;version=1.8"></script> + + <script type="application/javascript;version=1.8"> + window.onload = Task.async(function* () { + try { + ok(React, "Should get React"); + ok(Heap, "Should get Heap"); + + const errorMessage = "Something went wrong!"; + const container = document.getElementById("container"); + + const props = immutableUpdate(TEST_HEAP_PROPS, { + view: { state: viewState.DOMINATOR_TREE, }, + snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, { + dominatorTree: { + error: new Error(errorMessage), + state: dominatorTreeState.ERROR, + } + }) + }); + + yield renderComponent(Heap(props), container); + + ok(container.querySelector(".error"), "Should render an error view"); + ok(container.textContent.indexOf(errorMessage) !== -1, + "Should see our error message"); + } catch(e) { + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e)); + } finally { + SimpleTest.finish(); + } + }); + </script> + </pre> +</body> +</html> diff --git a/devtools/client/memory/test/chrome/test_Heap_02.html b/devtools/client/memory/test/chrome/test_Heap_02.html new file mode 100644 index 000000000..800f1044c --- /dev/null +++ b/devtools/client/memory/test/chrome/test_Heap_02.html @@ -0,0 +1,78 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test that the currently selected view is rendered. +--> +<head> + <meta charset="utf-8"> + <title>Tree component test</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> + <div id="container"></div> + <pre id="test"> + <script src="head.js" type="application/javascript;version=1.8"></script> + <script type="application/javascript;version=1.8"> + window.onload = Task.async(function* () { + try { + ok(React, "Should get React"); + ok(Heap, "Should get Heap"); + + const errorMessage = "Something went wrong!"; + const container = document.getElementById("container"); + + // Dominator tree view. + + yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, { + view: { state: viewState.DOMINATOR_TREE, }, + })), container); + + ok(container.querySelector(`[data-state=${dominatorTreeState.LOADED}]`), + "Should render the dominator tree."); + + // Census view. + + yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, { + view: { state: viewState.CENSUS, }, + })), container); + + ok(container.querySelector(`[data-state=${censusState.SAVED}]`), + "Should render the census."); + + // Diffing view. + + yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, { + view: { state: viewState.DIFFING, }, + snapshot: null, + diffing: { + firstSnapshotId: null, + secondSnapshotId: null, + census: null, + error: null, + state: diffingState.SELECTING, + }, + })), container); + + ok(container.querySelector(`[data-state=${diffingState.SELECTING}]`), + "Should render the diffing."); + + // Initial view. + + yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, { + snapshot: null, + diffing: null, + })), container); + + ok(container.querySelector("[data-state=initial]"), + "With no snapshot, nor a diffing, should render initial prompt."); + } catch(e) { + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e)); + } finally { + SimpleTest.finish(); + } + }); + </script> + </pre> +</body> +</html> diff --git a/devtools/client/memory/test/chrome/test_Heap_03.html b/devtools/client/memory/test/chrome/test_Heap_03.html new file mode 100644 index 000000000..7f0f52255 --- /dev/null +++ b/devtools/client/memory/test/chrome/test_Heap_03.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test that we show a throbber while computing and fetching dominator trees, +but not in other dominator tree states. +--> +<head> + <meta charset="utf-8"> + <title>Tree component test</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> + <div id="container"></div> + <pre id="test"> + <script src="head.js" type="application/javascript;version=1.8"></script> + <script type="application/javascript;version=1.8"> + window.onload = Task.async(function* () { + try { + const container = document.getElementById("container"); + + for (let state of [dominatorTreeState.COMPUTING, dominatorTreeState.FETCHING]) { + yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, { + view: { state: viewState.DOMINATOR_TREE, }, + snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, { + dominatorTree: immutableUpdate(TEST_HEAP_PROPS.snapshot.dominatorTree, { + state, + root: null, + dominatorTreeId: state === dominatorTreeState.FETCHING ? 1 : null, + }), + }), + })), container); + + ok(container.querySelector(".devtools-throbber"), + `Should show a throbber for state = ${state}`); + } + + for (let state of [dominatorTreeState.LOADED, dominatorTreeState.INCREMENTAL_FETCHING]) { + yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, { + view: { state: viewState.DOMINATOR_TREE, }, + snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, { + dominatorTree: immutableUpdate(TEST_HEAP_PROPS.snapshot.dominatorTree, { + state, + activeFetchRequestCount: state === dominatorTreeState.INCREMENTAL_FETCHING ? 1 : undefined, + }), + }), + })), container); + + ok(!container.querySelector(".devtools-throbber"), + `Should not show a throbber for state = ${state}`); + } + + yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, { + view: { state: viewState.DOMINATOR_TREE, }, + snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, { + dominatorTree: { + state: dominatorTreeState.ERROR, + error: new Error("example error for testing"), + }, + }), + })), container); + + ok(!container.querySelector(".devtools-throbber"), + `Should not show a throbber for ERROR state`); + } catch(e) { + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e)); + } finally { + SimpleTest.finish(); + } + }); + </script> + </pre> +</body> +</html> diff --git a/devtools/client/memory/test/chrome/test_Heap_04.html b/devtools/client/memory/test/chrome/test_Heap_04.html new file mode 100644 index 000000000..ccf4c9c6d --- /dev/null +++ b/devtools/client/memory/test/chrome/test_Heap_04.html @@ -0,0 +1,121 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test that we show the "hey you're not recording allocation stacks" message at the appropriate times. +--> +<head> + <meta charset="utf-8"> + <title>Tree component test</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> + <div id="container"></div> + <pre id="test"> + <script src="head.js" type="application/javascript;version=1.8"></script> + <script type="application/javascript;version=1.8"> + window.onload = Task.async(function* () { + try { + const container = document.getElementById("container"); + + yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, { + snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, { + census: immutableUpdate(TEST_HEAP_PROPS.snapshot.census, { + report: { + bytes: 1, + totalBytes: 1, + count: 1, + totalCount: 1, + id: 1, + parent: undefined, + children: [ + { + name: "noStack", + bytes: 1, + totalBytes: 1, + count: 1, + totalCount: 1, + children: undefined, + id: 3, + parent: 1, + } + ] + }, + display: censusDisplays.allocationStack, + }), + }), + })), container); + + ok(container.querySelector(".no-allocation-stacks"), + "When there are no allocation stacks, we should show the message"); + + yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, { + snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, { + census: immutableUpdate(TEST_HEAP_PROPS.snapshot.census, { + report: { + bytes: 1, + totalBytes: 1, + count: 1, + totalCount: 1, + id: 1, + parent: undefined, + children: [ + { + name: Cu.getJSTestingFunctions().saveStack(), + bytes: 1, + totalBytes: 1, + count: 1, + totalCount: 1, + children: undefined, + id: 2, + parent: 1, + }, + { + name: "noStack", + bytes: 1, + totalBytes: 1, + count: 1, + totalCount: 1, + children: undefined, + id: 3, + parent: 1, + } + ] + }, + display: censusDisplays.allocationStack, + }), + }), + })), container); + + ok(!container.querySelector(".no-allocation-stacks"), + "When there are allocation stacks, we should not show the message"); + + yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, { + snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, { + census: immutableUpdate(TEST_HEAP_PROPS.snapshot.census, { + report: { + bytes: 1, + totalBytes: 1, + count: 1, + totalCount: 1, + id: 1, + parent: undefined, + children: undefined + }, + display: censusDisplays.allocationStack, + }), + }), + })), container); + + ok(!container.querySelector(".no-allocation-stacks"), + "When there isn't census data, we should not show the message"); + } catch(e) { + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e)); + } finally { + SimpleTest.finish(); + } + }); + </script> + </pre> +</body> +</html> diff --git a/devtools/client/memory/test/chrome/test_Heap_05.html b/devtools/client/memory/test/chrome/test_Heap_05.html new file mode 100644 index 000000000..14365e3ab --- /dev/null +++ b/devtools/client/memory/test/chrome/test_Heap_05.html @@ -0,0 +1,132 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test that we show a message when the census results are empty. +--> +<head> + <meta charset="utf-8"> + <title>Tree component test</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> + <div id="container"></div> + <pre id="test"> + <script src="head.js" type="application/javascript;version=1.8"></script> + <script type="application/javascript;version=1.8"> + window.onload = Task.async(function* () { + try { + const container = document.getElementById("container"); + + yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, { + snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, { + census: immutableUpdate(TEST_HEAP_PROPS.snapshot.census, { + report: { + bytes: 1, + totalBytes: 1, + count: 1, + totalCount: 1, + id: 1, + parent: undefined, + children: [ + { + name: Cu.getJSTestingFunctions().saveStack(), + bytes: 1, + totalBytes: 1, + count: 1, + totalCount: 1, + children: undefined, + id: 2, + parent: 1, + }, + { + name: "noStack", + bytes: 1, + totalBytes: 1, + count: 1, + totalCount: 1, + children: undefined, + id: 3, + parent: 1, + } + ] + }, + display: censusDisplays.allocationStack, + }), + }), + })), container); + + ok(!container.querySelector(".empty"), + "When the report is not empty, we should not show the empty message"); + + // Empty Census Report + + const emptyCensus = { + report: { + bytes: 0, + totalBytes: 0, + count: 0, + totalCount: 0, + id: 1, + parent: undefined, + children: undefined, + }, + parentMap: Object.create(null), + display: censusDisplays.allocationStack, + filter: null, + expanded: new Immutable.Set(), + focused: null, + state: censusState.SAVED, + }; + + yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, { + snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, { + census: immutableUpdate(TEST_HEAP_PROPS.snapshot.census, emptyCensus), + }), + })), container); + + ok(container.querySelector(".empty"), + "When the report is empty in census view, we show the empty message"); + ok(container.textContent.indexOf(L10N.getStr("heapview.empty")) >= 0); + + // Empty Diffing Report + + yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, { + view: { state: viewState.DIFFING, }, + diffing: { + firstSnapshotId: 1, + secondSnapshotId: 2, + census: emptyCensus, + state: diffingState.TOOK_DIFF, + }, + snapshot: null, + })), container); + + ok(container.querySelector(".empty"), + "When the report is empty in diffing view, the empty message is shown"); + ok(container.textContent.indexOf(L10N.getStr("heapview.no-difference")) >= 0); + + // Empty Filtered Census + + yield renderComponent(Heap(immutableUpdate(TEST_HEAP_PROPS, { + snapshot: immutableUpdate(TEST_HEAP_PROPS.snapshot, { + census: immutableUpdate(TEST_HEAP_PROPS.snapshot.census, immutableUpdate(emptyCensus, { + filter: "zzzz" + })), + }), + })), container); + + ok(container.querySelector(".empty"), + "When the report is empty in census view w/ filter, we show the empty message"); + ok(container.textContent.indexOf(L10N.getStr("heapview.none-match")) >= 0); + + } catch(e) { + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e)); + } finally { + SimpleTest.finish(); + } + }); + </script> + </pre> +</body> +</html> diff --git a/devtools/client/memory/test/chrome/test_List_01.html b/devtools/client/memory/test/chrome/test_List_01.html new file mode 100644 index 000000000..911a7bc77 --- /dev/null +++ b/devtools/client/memory/test/chrome/test_List_01.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test to verify the delete button calls the onDelete handler for an item +--> +<head> + <meta charset="utf-8"> + <title>Tree component test</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> + <div id="container"></div> + + <pre id="test"> + <script src="head.js" type="application/javascript;version=1.8"></script> + <script type="application/javascript;version=1.8"> + window.onload = Task.async(function* () { + try { + const container = document.getElementById("container"); + + let deletedSnapshots = []; + + let snapshots = [ TEST_SNAPSHOT, TEST_SNAPSHOT, TEST_SNAPSHOT ] + .map((snapshot, index) => immutableUpdate(snapshot, { + index: snapshot.index + index + })); + + yield renderComponent( + List({ + itemComponent: SnapshotListItem, + onClick: noop, + onDelete: (item) => deletedSnapshots.push(item), + items: snapshots + }), + container + ); + + let deleteButtons = container.querySelectorAll('.delete'); + + is(container.querySelectorAll('.snapshot-list-item').length, 3, + "There are 3 list items\n"); + is(deletedSnapshots.length, 0, + "Not snapshots have been deleted\n"); + + deleteButtons[1].click(); + + is(deletedSnapshots.length, 1, "One snapshot was deleted\n"); + is(deletedSnapshots[0], snapshots[1], + "Deleted snapshot was added to the deleted list\n"); + + deleteButtons[0].click(); + + is(deletedSnapshots.length, 2, "Two snapshots were deleted\n"); + is(deletedSnapshots[1], snapshots[0], + "Deleted snapshot was added to the deleted list\n"); + + deleteButtons[2].click(); + + is(deletedSnapshots.length, 3, "Three snapshots were deleted\n"); + is(deletedSnapshots[2], snapshots[2], + "Deleted snapshot was added to the deleted list\n"); + + + } catch(e) { + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e)); + } finally { + SimpleTest.finish(); + } + }); + </script> + </pre> +</body> +</html> diff --git a/devtools/client/memory/test/chrome/test_ShortestPaths_01.html b/devtools/client/memory/test/chrome/test_ShortestPaths_01.html new file mode 100644 index 000000000..e2ad1867a --- /dev/null +++ b/devtools/client/memory/test/chrome/test_ShortestPaths_01.html @@ -0,0 +1,112 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test that the ShortestPaths component properly renders a graph of the merged shortest paths. +--> +<head> + <meta charset="utf-8"> + <title>Tree component test</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + + <script type="application/javascript" + src="chrome://devtools/content/shared/vendor/d3.js"> + </script> + <script type="application/javascript" + src="chrome://devtools/content/shared/vendor/dagre-d3.js"> + </script> +</head> +<body> + <!-- Give the container height so that the whole tree is rendered. --> + <div id="container" style="height: 900px;"></div> + + <pre id="test"> + <script src="head.js" type="application/javascript;version=1.8"></script> + <script type="application/javascript;version=1.8"> + window.onload = Task.async(function* () { + try { + const container = document.getElementById("container"); + + yield renderComponent(ShortestPaths(TEST_SHORTEST_PATHS_PROPS), container); + + let found1 = false; + let found2 = false; + let found3 = false; + + let found1to2 = false; + let found1to3 = false; + let found2to3 = false; + + const tspans = [...container.querySelectorAll("tspan")]; + for (let el of tspans) { + const text = el.textContent.trim(); + dumpn("tspan's text = " + text); + + switch (text) { + // Nodes + + case "other › SomeType @ 0x1": { + ok(!found1, "Should only find node 1 once"); + found1 = true; + break; + } + + case "other › SomeType @ 0x2": { + ok(!found2, "Should only find node 2 once"); + found2 = true; + break; + } + + case "other › SomeType @ 0x3": { + ok(!found3, "Should only find node 3 once"); + found3 = true; + break; + } + + // Edges + + case "1->2": { + ok(!found1to2, "Should only find edge 1->2 once"); + found1to2 = true; + break; + } + + case "1->3": { + ok(!found1to3, "Should only find edge 1->3 once"); + found1to3 = true; + break; + } + + case "2->3": { + ok(!found2to3, "Should only find edge 2->3 once"); + found2to3 = true; + break; + } + + // Unexpected + + default: { + ok(false, `Unexpected tspan: ${text}`); + break; + } + } + } + + ok(found1, "Should have rendered node 1"); + ok(found2, "Should have rendered node 2"); + ok(found3, "Should have rendered node 3"); + + ok(found1to2, "Should have rendered edge 1->2"); + ok(found1to3, "Should have rendered edge 1->3"); + ok(found2to3, "Should have rendered edge 2->3"); + + } catch(e) { + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e)); + } finally { + SimpleTest.finish(); + } + }); + </script> + </pre> +</body> +</html> diff --git a/devtools/client/memory/test/chrome/test_ShortestPaths_02.html b/devtools/client/memory/test/chrome/test_ShortestPaths_02.html new file mode 100644 index 000000000..cb6d48faa --- /dev/null +++ b/devtools/client/memory/test/chrome/test_ShortestPaths_02.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test that the ShortestPaths component renders a suggestion to select a node when there is no graph. +--> +<head> + <meta charset="utf-8"> + <title>Tree component test</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + + <script type="application/javascript" + src="chrome://devtools/content/shared/vendor/d3.js"> + </script> + <script type="application/javascript" + src="chrome://devtools/content/shared/vendor/dagre-d3.js"> + </script> +</head> +<body> + <!-- Give the container height so that the whole tree is rendered. --> + <div id="container" style="height: 900px;"></div> + + <pre id="test"> + <script src="head.js" type="application/javascript;version=1.8"></script> + <script type="application/javascript;version=1.8"> + window.onload = Task.async(function* () { + try { + const container = document.getElementById("container"); + + yield renderComponent(ShortestPaths(immutableUpdate(TEST_SHORTEST_PATHS_PROPS, + { graph: null })), + container); + + ok(container.textContent.indexOf(L10N.getStr("shortest-paths.select-node")) !== -1, + "The node selection prompt is displayed"); + } catch(e) { + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e)); + } finally { + SimpleTest.finish(); + } + }); + </script> + </pre> +</body> +</html> diff --git a/devtools/client/memory/test/chrome/test_SnapshotListItem_01.html b/devtools/client/memory/test/chrome/test_SnapshotListItem_01.html new file mode 100644 index 000000000..0081496ce --- /dev/null +++ b/devtools/client/memory/test/chrome/test_SnapshotListItem_01.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test to verify that the delete button only shows up for a snapshot when it has a +path. +--> +<head> + <meta charset="utf-8"> + <title>Tree component test</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> + <div id="container"></div> + + <pre id="test"> + <script src="head.js" type="application/javascript;version=1.8"></script> + <script type="application/javascript;version=1.8"> + window.onload = Task.async(function* () { + try { + const container = document.getElementById("container"); + + yield renderComponent( + SnapshotListItem(TEST_SNAPSHOT_LIST_ITEM_PROPS), + container + ); + + ok(container.querySelector('.delete'), + "Should have delete button when there is a path"); + + const pathlessProps = immutableUpdate( + TEST_SNAPSHOT_LIST_ITEM_PROPS, + {item: immutableUpdate(TEST_SNAPSHOT, {path: null})} + ); + + yield renderComponent( + SnapshotListItem(pathlessProps), + container + ); + + ok(!container.querySelector('.delete'), + "No delete button should be found if there is no path\n"); + + } catch(e) { + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e)); + } finally { + SimpleTest.finish(); + } + }); + </script> + </pre> +</body> +</html> diff --git a/devtools/client/memory/test/chrome/test_Toolbar_01.html b/devtools/client/memory/test/chrome/test_Toolbar_01.html new file mode 100644 index 000000000..57546df83 --- /dev/null +++ b/devtools/client/memory/test/chrome/test_Toolbar_01.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test that the Toolbar component shows the view switcher only at the appropriate times. +--> +<head> + <meta charset="utf-8"> + <title>Tree component test</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> + <div id="container"></div> + <pre id="test"> + <script src="head.js" type="application/javascript;version=1.8"></script> + <script type="application/javascript;version=1.8"> + window.onload = Task.async(function* () { + try { + const container = document.getElementById("container"); + + // Census and dominator tree views. + + for (let view of [viewState.CENSUS, viewState.DOMINATOR_TREE]) { + yield renderComponent(Toolbar(immutableUpdate(TEST_TOOLBAR_PROPS, { + view: { state: view }, + })), container); + + ok(container.querySelector("#select-view"), + `The view selector is shown in view = ${view}`); + } + + yield renderComponent(Toolbar(immutableUpdate(TEST_TOOLBAR_PROPS, { + view: { state: viewState.DIFFING, }, + })), container); + + ok(!container.querySelector("#select-view"), + "The view selector is NOT shown in the DIFFING view"); + } catch(e) { + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e)); + } finally { + SimpleTest.finish(); + } + }); + </script> + </pre> +</body> +</html> diff --git a/devtools/client/memory/test/chrome/test_TreeMap_01.html b/devtools/client/memory/test/chrome/test_TreeMap_01.html new file mode 100644 index 000000000..cdc293854 --- /dev/null +++ b/devtools/client/memory/test/chrome/test_TreeMap_01.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test that the Tree Map correctly renders onto 2 managed canvases. +--> +<head> + <meta charset="utf-8"> + <title>Tree component test</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + + <script type="application/javascript" + src="chrome://devtools/content/shared/vendor/d3.js"> + </script> +</head> +<body> + <!-- Give the container height so that the whole tree is rendered. --> + <div id="container" style="height: 900px;"></div> + + <pre id="test"> + <script src="head.js" type="application/javascript;version=1.8"></script> + <script type="application/javascript;version=1.8"> + window.onload = Task.async(function*() { + try { + const container = document.getElementById("container"); + + yield renderComponent(TreeMap(TEST_TREE_MAP_PROPS), container); + + let treeMapContainer = container.querySelector(".tree-map-container"); + ok(treeMapContainer, "Component creates a container"); + + let canvases = treeMapContainer.querySelectorAll("canvas"); + is(canvases.length, 2, "Creates 2 canvases"); + + } catch(e) { + ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e)); + } finally { + SimpleTest.finish(); + } + }); + </script> + </pre> +</body> +</html> diff --git a/devtools/client/memory/test/unit/.eslintrc.js b/devtools/client/memory/test/unit/.eslintrc.js new file mode 100644 index 000000000..aec096a0f --- /dev/null +++ b/devtools/client/memory/test/unit/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the shared list of defined globals for mochitests. + "extends": "../../../../.eslintrc.xpcshell.js" +}; diff --git a/devtools/client/memory/test/unit/head.js b/devtools/client/memory/test/unit/head.js new file mode 100644 index 000000000..f1335dc3d --- /dev/null +++ b/devtools/client/memory/test/unit/head.js @@ -0,0 +1,128 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; +var { console } = Cu.import("resource://gre/modules/Console.jsm", {}); +var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {}); + +var Services = require("Services"); +var DevToolsUtils = require("devtools/shared/DevToolsUtils"); +var flags = require("devtools/shared/flags"); +flags.testing = true; +flags.wantLogging = true; +flags.wantVerbose = false; + +var { OS } = require("resource://gre/modules/osfile.jsm"); +var { FileUtils } = require("resource://gre/modules/FileUtils.jsm"); +var { TargetFactory } = require("devtools/client/framework/target"); +var promise = require("promise"); +var defer = require("devtools/shared/defer"); +var { Task } = require("devtools/shared/task"); +var { expectState } = require("devtools/server/actors/common"); +var HeapSnapshotFileUtils = require("devtools/shared/heapsnapshot/HeapSnapshotFileUtils"); +var HeapAnalysesClient = require("devtools/shared/heapsnapshot/HeapAnalysesClient"); +var { addDebuggerToGlobal } = require("resource://gre/modules/jsdebugger.jsm"); +var Store = require("devtools/client/memory/store"); +var { L10N } = require("devtools/client/memory/utils"); +var SYSTEM_PRINCIPAL = Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal); + +var EXPECTED_DTU_ASSERT_FAILURE_COUNT = 0; + +do_register_cleanup(function () { + equal(DevToolsUtils.assertionFailureCount, EXPECTED_DTU_ASSERT_FAILURE_COUNT, + "Should have had the expected number of DevToolsUtils.assert() failures."); +}); + +function dumpn(msg) { + dump(`MEMORY-TEST: ${msg}\n`); +} + +function initDebugger() { + let global = new Cu.Sandbox(SYSTEM_PRINCIPAL, { freshZone: true }); + addDebuggerToGlobal(global); + return new global.Debugger(); +} + +function StubbedMemoryFront() { + this.state = "detached"; + this.recordingAllocations = false; + this.dbg = initDebugger(); +} + +StubbedMemoryFront.prototype.attach = Task.async(function* () { + this.state = "attached"; +}); + +StubbedMemoryFront.prototype.detach = Task.async(function* () { + this.state = "detached"; +}); + +StubbedMemoryFront.prototype.saveHeapSnapshot = expectState("attached", Task.async(function* () { + return ThreadSafeChromeUtils.saveHeapSnapshot({ runtime: true }); +}), "saveHeapSnapshot"); + +StubbedMemoryFront.prototype.startRecordingAllocations = expectState("attached", Task.async(function* () { + this.recordingAllocations = true; +})); + +StubbedMemoryFront.prototype.stopRecordingAllocations = expectState("attached", Task.async(function* () { + this.recordingAllocations = false; +})); + +function waitUntilSnapshotState(store, expected) { + let predicate = () => { + let snapshots = store.getState().snapshots; + do_print(snapshots.map(x => x.state)); + return snapshots.length === expected.length && + expected.every((state, i) => state === "*" || snapshots[i].state === state); + }; + do_print(`Waiting for snapshots to be of state: ${expected}`); + return waitUntilState(store, predicate); +} + +function findReportLeafIndex(node, name = null) { + if (node.reportLeafIndex && (!name || node.name === name)) { + return node.reportLeafIndex; + } + + if (node.children) { + for (let child of node.children) { + const found = findReportLeafIndex(child); + if (found) { + return found; + } + } + } + + return null; +} + +function waitUntilCensusState(store, getCensus, expected) { + let predicate = () => { + let snapshots = store.getState().snapshots; + + do_print("Current census state:" + + snapshots.map(x => getCensus(x) ? getCensus(x).state : null)); + + return snapshots.length === expected.length && + expected.every((state, i) => { + let census = getCensus(snapshots[i]); + return (state === "*") || + (!census && !state) || + (census && census.state === state); + }); + }; + do_print(`Waiting for snapshots' censuses to be of state: ${expected}`); + return waitUntilState(store, predicate); +} + +function* createTempFile() { + let file = FileUtils.getFile("TmpD", ["tmp.fxsnapshot"]); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); + let destPath = file.path; + let stat = yield OS.File.stat(destPath); + ok(stat.size === 0, "new file is 0 bytes at start"); + return destPath; +} diff --git a/devtools/client/memory/test/unit/test_action-clear-snapshots_01.js b/devtools/client/memory/test/unit/test_action-clear-snapshots_01.js new file mode 100644 index 000000000..a4e611e84 --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_01.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test clearSnapshots deletes snapshots with READ censuses + +let { takeSnapshotAndCensus, clearSnapshots } = require("devtools/client/memory/actions/snapshot"); +let { snapshotState: states, actions } = require("devtools/client/memory/constants"); +const { treeMapState } = require("devtools/client/memory/constants"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + const { getState, dispatch } = store; + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]); + ok(true, "snapshot created"); + + ok(true, "dispatch clearSnapshots action"); + let deleteEvents = Promise.all([ + waitUntilAction(store, actions.DELETE_SNAPSHOTS_START), + waitUntilAction(store, actions.DELETE_SNAPSHOTS_END) + ]); + dispatch(clearSnapshots(heapWorker)); + yield deleteEvents; + ok(true, "received delete snapshots events"); + + equal(getState().snapshots.length, 0, "no snapshot remaining"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_action-clear-snapshots_02.js b/devtools/client/memory/test/unit/test_action-clear-snapshots_02.js new file mode 100644 index 000000000..bb8c118cd --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_02.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test clearSnapshots preserves snapshots with state != READ or ERROR + +let { takeSnapshotAndCensus, clearSnapshots, takeSnapshot } = require("devtools/client/memory/actions/snapshot"); +let { snapshotState: states, treeMapState, actions } = require("devtools/client/memory/constants"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + const { getState, dispatch } = store; + + ok(true, "create a snapshot with a census in SAVED state"); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + ok(true, "create a snapshot in SAVED state"); + dispatch(takeSnapshot(front)); + yield waitUntilSnapshotState(store, [states.SAVED, states.SAVED]); + yield waitUntilCensusState(store, snapshot => snapshot.treeMap, + [treeMapState.SAVED, null]); + ok(true, "snapshots created with expected states"); + + ok(true, "dispatch clearSnapshots action"); + let deleteEvents = Promise.all([ + waitUntilAction(store, actions.DELETE_SNAPSHOTS_START), + waitUntilAction(store, actions.DELETE_SNAPSHOTS_END) + ]); + dispatch(clearSnapshots(heapWorker)); + yield deleteEvents; + ok(true, "received delete snapshots events"); + + equal(getState().snapshots.length, 1, "one snapshot remaining"); + let remainingSnapshot = getState().snapshots[0]; + equal(remainingSnapshot.treeMap, undefined, + "remaining snapshot doesn't have a treeMap property"); + equal(remainingSnapshot.census, undefined, + "remaining snapshot doesn't have a census property"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_action-clear-snapshots_03.js b/devtools/client/memory/test/unit/test_action-clear-snapshots_03.js new file mode 100644 index 000000000..ae888f858 --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_03.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test clearSnapshots deletes snapshots with state ERROR + +let { takeSnapshotAndCensus, clearSnapshots } = require("devtools/client/memory/actions/snapshot"); +let { snapshotState: states, treeMapState, actions } = require("devtools/client/memory/constants"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + const { getState, dispatch } = store; + + ok(true, "create a snapshot with a treeMap"); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilSnapshotState(store, [states.SAVED]); + ok(true, "snapshot created with a SAVED state"); + yield waitUntilCensusState(store, snapshot => snapshot.treeMap, + [treeMapState.SAVED]); + ok(true, "treeMap created with a SAVED state"); + + ok(true, "set snapshot state to error"); + let id = getState().snapshots[0].id; + dispatch({ type: actions.SNAPSHOT_ERROR, id, error: new Error("_") }); + yield waitUntilSnapshotState(store, [states.ERROR]); + ok(true, "snapshot set to error state"); + + ok(true, "dispatch clearSnapshots action"); + let deleteEvents = Promise.all([ + waitUntilAction(store, actions.DELETE_SNAPSHOTS_START), + waitUntilAction(store, actions.DELETE_SNAPSHOTS_END) + ]); + dispatch(clearSnapshots(heapWorker)); + yield deleteEvents; + ok(true, "received delete snapshots events"); + equal(getState().snapshots.length, 0, "error snapshot deleted"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_action-clear-snapshots_04.js b/devtools/client/memory/test/unit/test_action-clear-snapshots_04.js new file mode 100644 index 000000000..f5b316b1a --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_04.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test clearSnapshots deletes several snapshots + +let { takeSnapshotAndCensus, clearSnapshots } = require("devtools/client/memory/actions/snapshot"); +let { snapshotState: states, actions, treeMapState } = require("devtools/client/memory/constants"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + const { getState, dispatch } = store; + + ok(true, "create 3 snapshots with a saved census"); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilCensusState(store, snapshot => snapshot.treeMap, + [treeMapState.SAVED, treeMapState.SAVED, + treeMapState.SAVED]); + ok(true, "snapshots created with a saved census"); + + ok(true, "set first snapshot state to error"); + let id = getState().snapshots[0].id; + dispatch({ type: actions.SNAPSHOT_ERROR, id, error: new Error("_") }); + yield waitUntilSnapshotState(store, + [states.ERROR, states.READ, states.READ]); + ok(true, "first snapshot set to error state"); + + ok(true, "dispatch clearSnapshots action"); + let deleteEvents = Promise.all([ + waitUntilAction(store, actions.DELETE_SNAPSHOTS_START), + waitUntilAction(store, actions.DELETE_SNAPSHOTS_END) + ]); + dispatch(clearSnapshots(heapWorker)); + yield deleteEvents; + ok(true, "received delete snapshots events"); + + equal(getState().snapshots.length, 0, "no snapshot remaining"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_action-clear-snapshots_05.js b/devtools/client/memory/test/unit/test_action-clear-snapshots_05.js new file mode 100644 index 000000000..9b889b3a4 --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_05.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test clearSnapshots deletes several snapshots + +let { takeSnapshotAndCensus, clearSnapshots } = require("devtools/client/memory/actions/snapshot"); +let { snapshotState: states, actions, treeMapState } = require("devtools/client/memory/constants"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + const { getState, dispatch } = store; + + ok(true, "create 2 snapshots with a saved census"); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + ok(true, "snapshots created with a saved census"); + yield waitUntilCensusState(store, snapshot => snapshot.treeMap, + [treeMapState.SAVED, treeMapState.SAVED]); + + let errorHeapWorker = { + deleteHeapSnapshot: function () { + return Promise.reject("_"); + } + }; + + ok(true, "dispatch clearSnapshots action"); + let deleteEvents = Promise.all([ + waitUntilAction(store, actions.DELETE_SNAPSHOTS_START), + waitUntilAction(store, actions.DELETE_SNAPSHOTS_END), + waitUntilAction(store, actions.SNAPSHOT_ERROR), + waitUntilAction(store, actions.SNAPSHOT_ERROR), + ]); + dispatch(clearSnapshots(errorHeapWorker)); + yield deleteEvents; + ok(true, "received delete snapshots and snapshot error events"); + equal(getState().snapshots.length, 0, "no snapshot remaining"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_action-clear-snapshots_06.js b/devtools/client/memory/test/unit/test_action-clear-snapshots_06.js new file mode 100644 index 000000000..1ee32bb12 --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_06.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that clearSnapshots disables diffing when deleting snapshots + +const { + takeSnapshotAndCensus, + clearSnapshots +} = require("devtools/client/memory/actions/snapshot"); +const { + snapshotState: states, + actions, + treeMapState +} = require("devtools/client/memory/constants"); +const { + toggleDiffing, + selectSnapshotForDiffingAndRefresh +} = require("devtools/client/memory/actions/diffing"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + const { getState, dispatch } = store; + + ok(true, "create 2 snapshots with a saved census"); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilCensusState(store, snapshot => snapshot.treeMap, + [treeMapState.SAVED, treeMapState.SAVED]); + ok(true, "snapshots created with a saved census"); + + dispatch(toggleDiffing()); + dispatch(selectSnapshotForDiffingAndRefresh(heapWorker, + getState().snapshots[0])); + dispatch(selectSnapshotForDiffingAndRefresh(heapWorker, + getState().snapshots[1])); + + ok(getState().diffing, "We should be in diffing view"); + + yield waitUntilAction(store, actions.TAKE_CENSUS_DIFF_END); + ok(true, "Received TAKE_CENSUS_DIFF_END action"); + + ok(true, "Dispatch clearSnapshots action"); + let deleteEvents = Promise.all([ + waitUntilAction(store, actions.DELETE_SNAPSHOTS_START), + waitUntilAction(store, actions.DELETE_SNAPSHOTS_END) + ]); + dispatch(clearSnapshots(heapWorker)); + yield deleteEvents; + ok(true, "received delete snapshots events"); + + ok(getState().snapshots.length === 0, "Snapshots array should be empty"); + ok(!getState().diffing, "We should no longer be diffing"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_action-export-snapshot.js b/devtools/client/memory/test/unit/test_action-export-snapshot.js new file mode 100644 index 000000000..0582abbf0 --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-export-snapshot.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test exporting a snapshot to a user specified location on disk. + +let { exportSnapshot } = require("devtools/client/memory/actions/io"); +let { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot"); +let { snapshotState: states, actions, treeMapState } = require("devtools/client/memory/constants"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + const { getState, dispatch } = store; + + let destPath = yield createTempFile(); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilCensusState(store, snapshot => snapshot.treeMap, + [treeMapState.SAVED]); + + let exportEvents = Promise.all([ + waitUntilAction(store, actions.EXPORT_SNAPSHOT_START), + waitUntilAction(store, actions.EXPORT_SNAPSHOT_END) + ]); + dispatch(exportSnapshot(getState().snapshots[0], destPath)); + yield exportEvents; + + stat = yield OS.File.stat(destPath); + do_print(stat.size); + ok(stat.size > 0, "destination file is more than 0 bytes"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_action-filter-01.js b/devtools/client/memory/test/unit/test_action-filter-01.js new file mode 100644 index 000000000..e0894606d --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-filter-01.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test setting the filter string. + +let { setFilterString } = require("devtools/client/memory/actions/filter"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let store = Store(); + const { getState, dispatch } = store; + + equal(getState().filter, null, "no filter by default"); + + dispatch(setFilterString("my filter")); + equal(getState().filter, "my filter", "now we have the expected filter"); + + dispatch(setFilterString("")); + equal(getState().filter, null, "no filter again"); +}); diff --git a/devtools/client/memory/test/unit/test_action-filter-02.js b/devtools/client/memory/test/unit/test_action-filter-02.js new file mode 100644 index 000000000..31d777704 --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-filter-02.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that changing filter state properly refreshes the selected census. + +let { snapshotState: states, viewState, censusState } = require("devtools/client/memory/constants"); +let { setFilterStringAndRefresh } = require("devtools/client/memory/actions/filter"); +let { takeSnapshotAndCensus, selectSnapshotAndRefresh } = require("devtools/client/memory/actions/snapshot"); +let { setCensusDisplay } = require("devtools/client/memory/actions/census-display"); +let { changeView } = require("devtools/client/memory/actions/view"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + let { getState, dispatch } = store; + + dispatch(changeView(viewState.CENSUS)); + + equal(getState().filter, null, "no filter by default"); + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + + yield waitUntilCensusState(store, snapshot => snapshot.census, + [censusState.SAVED, + censusState.SAVED, + censusState.SAVED]); + ok(true, "saved 3 snapshots and took a census of each of them"); + + dispatch(setFilterStringAndRefresh("str", heapWorker)); + yield waitUntilCensusState(store, snapshot => snapshot.census, + [censusState.SAVED, + censusState.SAVED, + censusState.SAVING]); + ok(true, "setting filter string should recompute the selected snapshot's census"); + + equal(getState().filter, "str", "now inverted"); + + yield waitUntilCensusState(store, snapshot => snapshot.census, + [censusState.SAVED, + censusState.SAVED, + censusState.SAVED]); + + equal(getState().snapshots[0].census.filter, null); + equal(getState().snapshots[1].census.filter, null); + equal(getState().snapshots[2].census.filter, "str"); + + dispatch(selectSnapshotAndRefresh(heapWorker, getState().snapshots[1].id)); + yield waitUntilCensusState(store, snapshot => snapshot.census, + [censusState.SAVED, + censusState.SAVING, + censusState.SAVED]); + ok(true, "selecting non-inverted census should trigger a recompute"); + + yield waitUntilCensusState(store, snapshot => snapshot.census, + [censusState.SAVED, + censusState.SAVED, + censusState.SAVED]); + + equal(getState().snapshots[0].census.filter, null); + equal(getState().snapshots[1].census.filter, "str"); + equal(getState().snapshots[2].census.filter, "str"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_action-filter-03.js b/devtools/client/memory/test/unit/test_action-filter-03.js new file mode 100644 index 000000000..4d46d5361 --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-filter-03.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that changing filter state in the middle of taking a snapshot results in +// the properly fitered census. + +let { snapshotState: states, censusState, viewState } = require("devtools/client/memory/constants"); +let { setFilterString, setFilterStringAndRefresh } = require("devtools/client/memory/actions/filter"); +let { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot"); +let { changeView } = require("devtools/client/memory/actions/view"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + let { getState, dispatch } = store; + + dispatch(changeView(viewState.CENSUS)); + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilSnapshotState(store, [states.SAVING]); + + dispatch(setFilterString("str")); + + yield waitUntilCensusState(store, snapshot => snapshot.census, + [censusState.SAVED]); + equal(getState().filter, "str", + "should want filtered trees"); + equal(getState().snapshots[0].census.filter, "str", + "snapshot-we-were-in-the-middle-of-saving's census should be filtered"); + + dispatch(setFilterStringAndRefresh("", heapWorker)); + yield waitUntilCensusState(store, snapshot => snapshot.census, + [censusState.SAVING]); + ok(true, "changing filter string retriggers census"); + ok(!getState().filter, "no longer filtering"); + + dispatch(setFilterString("obj")); + yield waitUntilCensusState(store, snapshot => snapshot.census, + [censusState.SAVED]); + equal(getState().filter, "obj", "filtering for obj now"); + equal(getState().snapshots[0].census.filter, "obj", + "census-we-were-in-the-middle-of-recomputing should be filtered again"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_action-import-snapshot-and-census.js b/devtools/client/memory/test/unit/test_action-import-snapshot-and-census.js new file mode 100644 index 000000000..a7e1d366a --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-import-snapshot-and-census.js @@ -0,0 +1,98 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * Tests the task creator `importSnapshotAndCensus()` for the whole flow of + * importing a snapshot, and its sub-actions. + */ + +let { actions, snapshotState: states, treeMapState } = require("devtools/client/memory/constants"); +let { exportSnapshot, importSnapshotAndCensus } = require("devtools/client/memory/actions/io"); +let { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + let { subscribe, dispatch, getState } = store; + + let destPath = yield createTempFile(); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]); + + let exportEvents = Promise.all([ + waitUntilAction(store, actions.EXPORT_SNAPSHOT_START), + waitUntilAction(store, actions.EXPORT_SNAPSHOT_END) + ]); + dispatch(exportSnapshot(getState().snapshots[0], destPath)); + yield exportEvents; + + // Now import our freshly exported snapshot + let snapshotI = 0; + let censusI = 0; + let snapshotStates = ["IMPORTING", "READING", "READ"]; + let censusStates = ["SAVING", "SAVED"]; + let expectStates = () => { + let snapshot = getState().snapshots[1]; + if (!snapshot) { + return; + } + if (snapshotI < snapshotStates.length) { + let isCorrectState = snapshot.state === states[snapshotStates[snapshotI]]; + if (isCorrectState) { + ok(true, `Found expected snapshot state ${snapshotStates[snapshotI]}`); + snapshotI++; + } + } + if (snapshot.treeMap && censusI < censusStates.length) { + if (snapshot.treeMap.state === treeMapState[censusStates[censusI]]) { + ok(true, `Found expected census state ${censusStates[censusI]}`); + censusI++; + } + } + }; + + let unsubscribe = subscribe(expectStates); + dispatch(importSnapshotAndCensus(heapWorker, destPath)); + + yield waitUntilState(store, () => { return snapshotI === snapshotStates.length && + censusI === censusStates.length; }); + unsubscribe(); + equal(snapshotI, snapshotStates.length, "importSnapshotAndCensus() produces the correct sequence of states in a snapshot"); + equal(getState().snapshots[1].state, states.READ, "imported snapshot is in READ state"); + equal(censusI, censusStates.length, "importSnapshotAndCensus() produces the correct sequence of states in a census"); + equal(getState().snapshots[1].treeMap.state, treeMapState.SAVED, "imported snapshot is in READ state"); + ok(getState().snapshots[1].selected, "imported snapshot is selected"); + + // Check snapshot data + let snapshot1 = getState().snapshots[0]; + let snapshot2 = getState().snapshots[1]; + + equal(snapshot1.treeMap.display, snapshot2.treeMap.display, + "imported snapshot has correct display"); + + // Clone the census data so we can destructively remove the ID/parents to compare + // equal census data + let census1 = stripUnique(JSON.parse(JSON.stringify(snapshot1.treeMap.report))); + let census2 = stripUnique(JSON.parse(JSON.stringify(snapshot2.treeMap.report))); + + equal(JSON.stringify(census1), JSON.stringify(census2), "Imported snapshot has correct census"); + + function stripUnique(obj) { + let children = obj.children || []; + for (let child of children) { + delete child.id; + delete child.parent; + stripUnique(child); + } + delete obj.id; + delete obj.parent; + return obj; + } +}); diff --git a/devtools/client/memory/test/unit/test_action-import-snapshot-dominator-tree.js b/devtools/client/memory/test/unit/test_action-import-snapshot-dominator-tree.js new file mode 100644 index 000000000..868f22ccf --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-import-snapshot-dominator-tree.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * Tests `importSnapshotAndCensus()` when importing snapshots from the dominator + * tree view. The snapshot is expected to be loaded and its dominator tree + * should be computed. + */ + +let { snapshotState, dominatorTreeState, viewState, treeMapState } = + require("devtools/client/memory/constants"); +let { importSnapshotAndCensus } = require("devtools/client/memory/actions/io"); +let { changeViewAndRefresh } = require("devtools/client/memory/actions/view"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + let { subscribe, dispatch, getState } = store; + + dispatch(changeViewAndRefresh(viewState.DOMINATOR_TREE, heapWorker)); + equal(getState().view.state, viewState.DOMINATOR_TREE, + "We should now be in the DOMINATOR_TREE view"); + + let i = 0; + let expected = [ + "IMPORTING", + "READING", + "READ", + "treeMap:SAVING", + "treeMap:SAVED", + "dominatorTree:COMPUTING", + "dominatorTree:FETCHING", + "dominatorTree:LOADED", + ]; + let expectStates = () => { + let snapshot = getState().snapshots[0]; + if (snapshot && hasExpectedState(snapshot, expected[i])) { + ok(true, `Found expected state ${expected[i]}`); + i++; + } + }; + + let unsubscribe = subscribe(expectStates); + const snapshotPath = yield front.saveHeapSnapshot(); + dispatch(importSnapshotAndCensus(heapWorker, snapshotPath)); + + yield waitUntilState(store, () => i === expected.length); + unsubscribe(); + equal(i, expected.length, "importSnapshotAndCensus() produces the correct " + + "sequence of states in a snapshot"); + equal(getState().snapshots[0].dominatorTree.state, dominatorTreeState.LOADED, + "imported snapshot's dominator tree is in LOADED state"); + ok(getState().snapshots[0].selected, "imported snapshot is selected"); +}); + +/** + * Check that the provided snapshot is in the expected state. The expected state + * is a snapshotState by default. If the expected state is prefixed by + * dominatorTree, a dominatorTree is expected on the provided snapshot, in the + * corresponding state from dominatorTreeState. + */ +function hasExpectedState(snapshot, expectedState) { + let isDominatorState = expectedState.indexOf("dominatorTree:") === 0; + if (isDominatorState) { + let state = dominatorTreeState[expectedState.replace("dominatorTree:", "")]; + return snapshot.dominatorTree && snapshot.dominatorTree.state === state; + } + + let isTreeMapState = expectedState.indexOf("treeMap:") === 0; + if (isTreeMapState) { + let state = treeMapState[expectedState.replace("treeMap:", "")]; + return snapshot.treeMap && snapshot.treeMap.state === state; + } + + let state = snapshotState[expectedState]; + return snapshot.state === state; +} diff --git a/devtools/client/memory/test/unit/test_action-select-snapshot.js b/devtools/client/memory/test/unit/test_action-select-snapshot.js new file mode 100644 index 000000000..2329ab2fa --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-select-snapshot.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the reducer responding to the action `selectSnapshot(snapshot)` + */ + +let actions = require("devtools/client/memory/actions/snapshot"); +let { snapshotState: states } = require("devtools/client/memory/constants"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + yield front.attach(); + let store = Store(); + + for (let i = 0; i < 5; i++) { + store.dispatch(actions.takeSnapshot(front)); + } + + yield waitUntilState(store, ({ snapshots }) => snapshots.length === 5 && snapshots.every(isDone)); + + for (let i = 0; i < 5; i++) { + do_print(`Selecting snapshot[${i}]`); + store.dispatch(actions.selectSnapshot(store.getState().snapshots[i].id)); + yield waitUntilState(store, ({ snapshots }) => snapshots[i].selected); + + let { snapshots } = store.getState(); + ok(snapshots[i].selected, `snapshot[${i}] selected`); + equal(snapshots.filter(s => !s.selected).length, 4, "All other snapshots are unselected"); + } +}); + +function isDone(s) { return s.state === states.SAVED; } diff --git a/devtools/client/memory/test/unit/test_action-set-display-and-refresh-01.js b/devtools/client/memory/test/unit/test_action-set-display-and-refresh-01.js new file mode 100644 index 000000000..570ffdf05 --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-set-display-and-refresh-01.js @@ -0,0 +1,118 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * Tests the task creator `setCensusDisplayAndRefreshAndRefresh()` for display + * changing. We test this rather than `setCensusDisplayAndRefresh` directly, as + * we use the refresh action in the app itself composed from + * `setCensusDisplayAndRefresh`. + */ + +let { censusDisplays, snapshotState: states, censusState, viewState } = require("devtools/client/memory/constants"); +let { setCensusDisplayAndRefresh } = require("devtools/client/memory/actions/census-display"); +let { takeSnapshotAndCensus, selectSnapshotAndRefresh } = require("devtools/client/memory/actions/snapshot"); +const { changeView } = require("devtools/client/memory/actions/view"); + +function run_test() { + run_next_test(); +} + +// We test setting an invalid display, which triggers an assertion failure. +EXPECTED_DTU_ASSERT_FAILURE_COUNT = 1; + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + let { getState, dispatch } = store; + + dispatch(changeView(viewState.CENSUS)); + + // Test default display with no snapshots + equal(getState().censusDisplay.breakdown.by, "coarseType", + "default coarseType display selected at start."); + dispatch(setCensusDisplayAndRefresh(heapWorker, + censusDisplays.allocationStack)); + equal(getState().censusDisplay.breakdown.by, "allocationStack", + "display changed with no snapshots"); + + // Test invalid displays + ok(getState().errors.length === 0, "No error actions in the queue."); + dispatch(setCensusDisplayAndRefresh(heapWorker, {})); + yield waitUntilState(store, () => getState().errors.length === 1); + ok(true, "Emits an error action when passing in an invalid display object"); + + equal(getState().censusDisplay.breakdown.by, "allocationStack", + "current display unchanged when passing invalid display"); + + // Test new snapshots + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilCensusState(store, snapshot => snapshot.census, + [censusState.SAVED]); + + equal(getState().snapshots[0].census.display, censusDisplays.allocationStack, + "New snapshot's census uses correct display"); + + // Updates when changing display during `SAVING` + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilCensusState(store, snapshot => snapshot.census, + [censusState.SAVED, censusState.SAVING]); + dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.coarseType)); + yield waitUntilCensusState(store, snapshot => snapshot.census, + [censusState.SAVED, censusState.SAVED]); + equal(getState().snapshots[1].census.display, censusDisplays.coarseType, + "Changing display while saving a snapshot results in a census using the new display"); + + + // Updates when changing display during `SAVING_CENSUS` + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilCensusState(store, snapshot => snapshot.census, + [censusState.SAVED, + censusState.SAVED, + censusState.SAVING]); + dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack)); + yield waitUntilCensusState(store, snapshot => snapshot.census, + [censusState.SAVED, + censusState.SAVED, + censusState.SAVED]); + equal(getState().snapshots[2].census.display, censusDisplays.allocationStack, + "Display can be changed while saving census, stores updated display in snapshot"); + + // Updates census on currently selected snapshot when changing display + ok(getState().snapshots[2].selected, "Third snapshot currently selected"); + dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.coarseType)); + yield waitUntilState(store, state => state.snapshots[2].census.state === censusState.SAVING); + yield waitUntilState(store, state => state.snapshots[2].census.state === censusState.SAVED); + equal(getState().snapshots[2].census.display, censusDisplays.coarseType, + "Snapshot census updated when changing displays after already generating one census"); + + dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack)); + yield waitUntilState(store, state => state.snapshots[2].census.state === censusState.SAVED); + equal(getState().snapshots[2].census.display, censusDisplays.allocationStack, + "Snapshot census updated when changing displays after already generating one census"); + + // Does not update unselected censuses. + ok(!getState().snapshots[1].selected, "Second snapshot selected currently"); + equal(getState().snapshots[1].census.display, censusDisplays.coarseType, + "Second snapshot using `coarseType` display still and not yet updated to correct display"); + + // Updates to current display when switching to stale snapshot. + dispatch(selectSnapshotAndRefresh(heapWorker, getState().snapshots[1].id)); + yield waitUntilCensusState(store, snapshot => snapshot.census, + [censusState.SAVED, + censusState.SAVING, + censusState.SAVED]); + yield waitUntilCensusState(store, snapshot => snapshot.census, + [censusState.SAVED, + censusState.SAVED, + censusState.SAVED]); + + ok(getState().snapshots[1].selected, "Second snapshot selected currently"); + equal(getState().snapshots[1].census.display, censusDisplays.allocationStack, + "Second snapshot using `allocationStack` display and updated to correct display"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_action-set-display-and-refresh-02.js b/devtools/client/memory/test/unit/test_action-set-display-and-refresh-02.js new file mode 100644 index 000000000..5be5444d4 --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-set-display-and-refresh-02.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * Tests the task creator `setCensusDisplayAndRefreshAndRefresh()` for custom + * displays. + */ + +let { snapshotState: states, censusState, viewState } = require("devtools/client/memory/constants"); +let { setCensusDisplayAndRefresh } = require("devtools/client/memory/actions/census-display"); +let { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot"); +let { changeView } = require("devtools/client/memory/actions/view"); + +let CUSTOM = { + displayName: "Custom", + tooltip: "Custom tooltip", + inverted: false, + breakdown: { + by: "internalType", + then: { by: "count", bytes: true, count: false } + } +}; + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + let { getState, dispatch } = store; + + dispatch(changeView(viewState.CENSUS)); + dispatch(setCensusDisplayAndRefresh(heapWorker, CUSTOM)); + equal(getState().censusDisplay, CUSTOM, + "CUSTOM display stored in display state."); + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]); + + equal(getState().snapshots[0].census.display, CUSTOM, + "New snapshot stored CUSTOM display when done taking census"); + ok(getState().snapshots[0].census.report.children.length, "Census has some children"); + // Ensure we don't have `count` in any results + ok(getState().snapshots[0].census.report.children.every(c => !c.count), + "Census used CUSTOM display without counts"); + // Ensure we do have `bytes` in the results + ok(getState().snapshots[0].census.report.children.every(c => typeof c.bytes === "number"), + "Census used CUSTOM display with bytes"); +}); diff --git a/devtools/client/memory/test/unit/test_action-set-display.js b/devtools/client/memory/test/unit/test_action-set-display.js new file mode 100644 index 000000000..43ea975da --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-set-display.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * Tests the action creator `setCensusDisplay()` for display changing. Does not + * test refreshing the census information, check `setCensusDisplayAndRefresh` + * action for that. + */ + +let { censusDisplays, snapshotState: states, censusState, viewState } = require("devtools/client/memory/constants"); +let { setCensusDisplay } = require("devtools/client/memory/actions/census-display"); +let { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot"); +const { changeView } = require("devtools/client/memory/actions/view"); + +function run_test() { + run_next_test(); +} + +// We test setting an invalid display, which triggers an assertion failure. +EXPECTED_DTU_ASSERT_FAILURE_COUNT = 1; + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + let { getState, dispatch } = store; + + dispatch(changeView(viewState.CENSUS)); + + // Test default display with no snapshots + equal(getState().censusDisplay.breakdown.by, "coarseType", + "default coarseType display selected at start."); + + dispatch(setCensusDisplay(censusDisplays.allocationStack)); + equal(getState().censusDisplay.breakdown.by, "allocationStack", + "display changed with no snapshots"); + + // Test invalid displays + try { + dispatch(setCensusDisplay({})); + ok(false, "Throws when passing in an invalid display object"); + } catch (e) { + ok(true, "Throws when passing in an invalid display object"); + } + equal(getState().censusDisplay.breakdown.by, "allocationStack", + "current display unchanged when passing invalid display"); + + // Test new snapshots + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]); + equal(getState().snapshots[0].census.display, censusDisplays.allocationStack, + "New snapshots use the current, non-default display"); +}); diff --git a/devtools/client/memory/test/unit/test_action-take-census.js b/devtools/client/memory/test/unit/test_action-take-census.js new file mode 100644 index 000000000..3e3d30e8e --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-take-census.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the async reducer responding to the action `takeCensus(heapWorker, snapshot)` + */ + +var { snapshotState: states, censusDisplays, censusState, censusState, viewState } = require("devtools/client/memory/constants"); +var actions = require("devtools/client/memory/actions/snapshot"); +var { changeView } = require("devtools/client/memory/actions/view"); + + +function run_test() { + run_next_test(); +} + +// This tests taking a census on a snapshot that is still being read, which +// triggers an assertion failure. +EXPECTED_DTU_ASSERT_FAILURE_COUNT = 1; + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + + store.dispatch(changeView(viewState.CENSUS)); + + store.dispatch(actions.takeSnapshot(front)); + yield waitUntilState(store, () => { + let snapshots = store.getState().snapshots; + return snapshots.length === 1 && snapshots[0].state === states.SAVED; + }); + + let snapshot = store.getState().snapshots[0]; + equal(snapshot.census, null, "No census data exists yet on the snapshot."); + + // Test error case of wrong state. + store.dispatch(actions.takeCensus(heapWorker, snapshot.id)); + yield waitUntilState(store, () => store.getState().errors.length === 1); + + dumpn("Found error: " + store.getState().errors[0]); + ok(/Assertion failure/.test(store.getState().errors[0]), + "Error thrown when taking a census of a snapshot that has not been read."); + + store.dispatch(actions.readSnapshot(heapWorker, snapshot.id)); + yield waitUntilState(store, () => store.getState().snapshots[0].state === states.READ); + + store.dispatch(actions.takeCensus(heapWorker, snapshot.id)); + yield waitUntilCensusState(store, s => s.census, [censusState.SAVING]); + yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]); + + snapshot = store.getState().snapshots[0]; + ok(snapshot.census, "Snapshot has census after saved census"); + ok(snapshot.census.report.children.length, "Census is in tree node form"); + equal(snapshot.census.display, censusDisplays.coarseType, + "Snapshot stored correct display used for the census"); + +}); diff --git a/devtools/client/memory/test/unit/test_action-take-snapshot-and-census.js b/devtools/client/memory/test/unit/test_action-take-snapshot-and-census.js new file mode 100644 index 000000000..77c3b8e38 --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-take-snapshot-and-census.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the task creator `takeSnapshotAndCensus()` for the whole flow of + * taking a snapshot, and its sub-actions. + */ + +let { snapshotState: states, treeMapState } = require("devtools/client/memory/constants"); +let actions = require("devtools/client/memory/actions/snapshot"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + + let snapshotI = 0; + let censusI = 0; + let snapshotStates = ["SAVING", "SAVED", "READING", "READ"]; + let censusStates = ["SAVING", "SAVED"]; + let expectStates = () => { + let snapshot = store.getState().snapshots[0]; + if (!snapshot) { + return; + } + if (snapshotI < snapshotStates.length) { + let isCorrectState = snapshot.state === states[snapshotStates[snapshotI]]; + if (isCorrectState) { + ok(true, `Found expected snapshot state ${snapshotStates[snapshotI]}`); + snapshotI++; + } + } + if (snapshot.treeMap && censusI < censusStates.length) { + if (snapshot.treeMap.state === treeMapState[censusStates[censusI]]) { + ok(true, `Found expected census state ${censusStates[censusI]}`); + censusI++; + } + } + }; + + + let unsubscribe = store.subscribe(expectStates); + store.dispatch(actions.takeSnapshotAndCensus(front, heapWorker)); + + yield waitUntilState(store, () => { return snapshotI === snapshotStates.length && + censusI === censusStates.length; }); + unsubscribe(); + + ok(true, "takeSnapshotAndCensus() produces the correct sequence of states in a snapshot"); + let snapshot = store.getState().snapshots[0]; + ok(snapshot.treeMap, "snapshot has tree map census data"); + ok(snapshot.selected, "snapshot is selected"); +}); diff --git a/devtools/client/memory/test/unit/test_action-take-snapshot.js b/devtools/client/memory/test/unit/test_action-take-snapshot.js new file mode 100644 index 000000000..c05583b22 --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-take-snapshot.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the async reducer responding to the action `takeSnapshot(front)` + */ + +let actions = require("devtools/client/memory/actions/snapshot"); +let { snapshotState: states } = require("devtools/client/memory/constants"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + yield front.attach(); + let store = Store(); + + let unsubscribe = store.subscribe(checkState); + + let foundPendingState = false; + let foundDoneState = false; + + function checkState() { + let { snapshots } = store.getState(); + let lastSnapshot = snapshots[snapshots.length - 1]; + + if (lastSnapshot.state === states.SAVING) { + foundPendingState = true; + ok(foundPendingState, "Got state change for pending heap snapshot request"); + ok(!lastSnapshot.path, "Snapshot does not yet have a path"); + ok(!lastSnapshot.census, "Has no census data when loading"); + } + else if (lastSnapshot.state === states.SAVED) { + foundDoneState = true; + ok(foundDoneState, "Got state change for completed heap snapshot request"); + ok(foundPendingState, "SAVED state occurs after SAVING state"); + ok(lastSnapshot.path, "Snapshot fetched with a path"); + ok(snapshots.every(s => s.selected === (s.id === lastSnapshot.id)), + "Only recent snapshot is selected"); + } + } + + for (let i = 0; i < 4; i++) { + store.dispatch(actions.takeSnapshot(front)); + yield waitUntilState(store, () => foundPendingState && foundDoneState); + + // reset state trackers + foundDoneState = foundPendingState = false; + } + + unsubscribe(); +}); diff --git a/devtools/client/memory/test/unit/test_action-toggle-inverted-and-refresh-01.js b/devtools/client/memory/test/unit/test_action-toggle-inverted-and-refresh-01.js new file mode 100644 index 000000000..cd015557d --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-toggle-inverted-and-refresh-01.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that changing displays with different inverted state properly +// refreshes the selected census. + +const { + censusDisplays, + snapshotState: states, + censusState, + viewState +} = require("devtools/client/memory/constants"); +const { + setCensusDisplayAndRefresh +} = require("devtools/client/memory/actions/census-display"); +const { + takeSnapshotAndCensus, + selectSnapshotAndRefresh, +} = require("devtools/client/memory/actions/snapshot"); +const { changeView } = require("devtools/client/memory/actions/view"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + let { getState, dispatch } = store; + + dispatch(changeView(viewState.CENSUS)); + + // Select a non-inverted display. + dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack)); + equal(getState().censusDisplay.inverted, false, "not inverted by default"); + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + + yield waitUntilCensusState(store, s => s.census, [censusState.SAVED, + censusState.SAVED, + censusState.SAVED]); + ok(true, "saved 3 snapshots and took a census of each of them"); + + // Select an inverted display. + dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.invertedAllocationStack)); + + yield waitUntilCensusState(store, s => s.census, [censusState.SAVED, + censusState.SAVED, + censusState.SAVING]); + ok(true, "toggling inverted should recompute the selected snapshot's census"); + + equal(getState().censusDisplay.inverted, true, "now inverted"); + + yield waitUntilCensusState(store, s => s.census, [censusState.SAVED, + censusState.SAVED, + censusState.SAVED]); + + equal(getState().snapshots[0].census.display.inverted, false); + equal(getState().snapshots[1].census.display.inverted, false); + equal(getState().snapshots[2].census.display.inverted, true); + + dispatch(selectSnapshotAndRefresh(heapWorker, getState().snapshots[1].id)); + yield waitUntilCensusState(store, s => s.census, [censusState.SAVED, + censusState.SAVING, + censusState.SAVED]); + ok(true, "selecting non-inverted census should trigger a recompute"); + + yield waitUntilCensusState(store, s => s.census, [censusState.SAVED, + censusState.SAVED, + censusState.SAVED]); + + equal(getState().snapshots[0].census.display.inverted, false); + equal(getState().snapshots[1].census.display.inverted, true); + equal(getState().snapshots[2].census.display.inverted, true); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_action-toggle-inverted-and-refresh-02.js b/devtools/client/memory/test/unit/test_action-toggle-inverted-and-refresh-02.js new file mode 100644 index 000000000..f0cdba264 --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-toggle-inverted-and-refresh-02.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that changing inverted state in the middle of taking a snapshot results +// in an inverted census. + +const { censusDisplays, snapshotState: states, censusState, viewState } = require("devtools/client/memory/constants"); +const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot"); +const { + setCensusDisplay, + setCensusDisplayAndRefresh, +} = require("devtools/client/memory/actions/census-display"); +const { changeView } = require("devtools/client/memory/actions/view"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + let { getState, dispatch } = store; + + dispatch(changeView(viewState.CENSUS)); + + dispatch(setCensusDisplay(censusDisplays.allocationStack)); + equal(getState().censusDisplay.inverted, false, + "Should not have an inverted census display"); + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilSnapshotState(store, [states.SAVING]); + + dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.invertedAllocationStack)); + + yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]); + + ok(getState().censusDisplay.inverted, + "should want inverted trees"); + ok(getState().snapshots[0].census.display.inverted, + "snapshot-we-were-in-the-middle-of-saving's census should be inverted"); + + dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack)); + yield waitUntilCensusState(store, s => s.census, [censusState.SAVING]); + ok(true, "toggling inverted retriggers census"); + ok(!getState().censusDisplay.inverted, "no longer inverted"); + + dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.invertedAllocationStack)); + yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]); + ok(getState().censusDisplay.inverted, "inverted again"); + ok(getState().snapshots[0].census.display.inverted, + "census-we-were-in-the-middle-of-recomputing should be inverted again"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_action-toggle-inverted.js b/devtools/client/memory/test/unit/test_action-toggle-inverted.js new file mode 100644 index 000000000..665e5d822 --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-toggle-inverted.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test toggling the top level inversion state of the tree. + +const { censusDisplays } = require("devtools/client/memory/constants"); +const { setCensusDisplay } = require("devtools/client/memory/actions/census-display"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let store = Store(); + const { getState, dispatch } = store; + + dispatch(setCensusDisplay(censusDisplays.allocationStack)); + equal(getState().censusDisplay.inverted, false, + "not inverted initially"); + + dispatch(setCensusDisplay(censusDisplays.invertedAllocationStack)); + equal(getState().censusDisplay.inverted, true, "now inverted after toggling"); + + dispatch(setCensusDisplay(censusDisplays.allocationStack)); + equal(getState().censusDisplay.inverted, false, + "not inverted again after toggling again"); +}); diff --git a/devtools/client/memory/test/unit/test_action-toggle-recording-allocations.js b/devtools/client/memory/test/unit/test_action-toggle-recording-allocations.js new file mode 100644 index 000000000..ebbc17173 --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-toggle-recording-allocations.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * Test toggling the recording of allocation stacks. + */ + +let { toggleRecordingAllocationStacks } = require("devtools/client/memory/actions/allocations"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + yield front.attach(); + let store = Store(); + const { getState, dispatch } = store; + + equal(getState().allocations.recording, false, "not recording by default"); + equal(getState().allocations.togglingInProgress, false, + "not in the process of toggling by default"); + + dispatch(toggleRecordingAllocationStacks(front)); + yield waitUntilState(store, () => getState().allocations.togglingInProgress); + ok(true, "`togglingInProgress` set to true when toggling on"); + yield waitUntilState(store, () => !getState().allocations.togglingInProgress); + + equal(getState().allocations.recording, true, "now we are recording"); + ok(front.recordingAllocations, "front is recording too"); + + dispatch(toggleRecordingAllocationStacks(front)); + yield waitUntilState(store, () => getState().allocations.togglingInProgress); + ok(true, "`togglingInProgress` set to true when toggling off"); + yield waitUntilState(store, () => !getState().allocations.togglingInProgress); + + equal(getState().allocations.recording, false, "now we are not recording"); + ok(front.recordingAllocations, "front is not recording anymore"); + + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_action_diffing_01.js b/devtools/client/memory/test/unit/test_action_diffing_01.js new file mode 100644 index 000000000..24893581e --- /dev/null +++ b/devtools/client/memory/test/unit/test_action_diffing_01.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test toggling of diffing. + +const { toggleDiffing } = require("devtools/client/memory/actions/diffing"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + const { getState, dispatch } = store; + + equal(getState().diffing, null, "not diffing by default"); + + dispatch(toggleDiffing()); + ok(getState().diffing, "now diffing after toggling"); + + dispatch(toggleDiffing()); + equal(getState().diffing, null, "not diffing again after toggling again"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_action_diffing_02.js b/devtools/client/memory/test/unit/test_action_diffing_02.js new file mode 100644 index 000000000..fc68cee65 --- /dev/null +++ b/devtools/client/memory/test/unit/test_action_diffing_02.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that toggling diffing unselects all snapshots. + +const { snapshotState, censusState, viewState } = require("devtools/client/memory/constants"); +const { toggleDiffing } = require("devtools/client/memory/actions/diffing"); +const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot"); +const { changeView } = require("devtools/client/memory/actions/view"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + const { getState, dispatch } = store; + + dispatch(changeView(viewState.CENSUS)); + + equal(getState().diffing, null, "not diffing by default"); + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilCensusState(store, s => s.census, [censusState.SAVED, + censusState.SAVED, + censusState.SAVED]); + + ok(getState().snapshots.some(s => s.selected), + "One of the new snapshots is selected"); + + dispatch(toggleDiffing()); + ok(getState().diffing, "now diffing after toggling"); + + for (let s of getState().snapshots) { + ok(!s.selected, + "No snapshot should be selected after entering diffing mode"); + } + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_action_diffing_03.js b/devtools/client/memory/test/unit/test_action_diffing_03.js new file mode 100644 index 000000000..3dd901724 --- /dev/null +++ b/devtools/client/memory/test/unit/test_action_diffing_03.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test selecting snapshots for diffing. + +const { diffingState, snapshotState, viewState } = require("devtools/client/memory/constants"); +const { + toggleDiffing, + selectSnapshotForDiffing +} = require("devtools/client/memory/actions/diffing"); +const { takeSnapshot } = require("devtools/client/memory/actions/snapshot"); +const { changeView } = require("devtools/client/memory/actions/view"); + +function run_test() { + run_next_test(); +} + +// We test that you (1) cannot select a snapshot that is not in a diffable +// state, and (2) cannot select more than 2 snapshots for diffing. Both attempts +// trigger assertion failures. +EXPECTED_DTU_ASSERT_FAILURE_COUNT = 2; + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + const { getState, dispatch } = store; + + dispatch(changeView(viewState.CENSUS)); + equal(getState().diffing, null, "not diffing by default"); + + dispatch(takeSnapshot(front, heapWorker)); + dispatch(takeSnapshot(front, heapWorker)); + dispatch(takeSnapshot(front, heapWorker)); + + yield waitUntilSnapshotState(store, [snapshotState.SAVED, + snapshotState.SAVED, + snapshotState.SAVED]); + dispatch(takeSnapshot(front)); + + // Start diffing. + dispatch(toggleDiffing()); + ok(getState().diffing, "now diffing after toggling"); + equal(getState().diffing.firstSnapshotId, null, + "no first snapshot selected"); + equal(getState().diffing.secondSnapshotId, null, + "no second snapshot selected"); + equal(getState().diffing.state, diffingState.SELECTING, + "should be in diffing state SELECTING"); + + // Can't select a snapshot that is not in a diffable state. + equal(getState().snapshots[3].state, snapshotState.SAVING, + "the last snapshot is still in the process of being saved"); + dumpn("Expecting exception:"); + let threw = false; + try { + dispatch(selectSnapshotForDiffing(getState().snapshots[3])); + } catch (error) { + threw = true; + } + ok(threw, "Should not be able to select snapshots that aren't ready for diffing"); + + // Select first snapshot for diffing. + dispatch(selectSnapshotForDiffing(getState().snapshots[0])); + ok(getState().diffing, "now diffing after toggling"); + equal(getState().diffing.firstSnapshotId, getState().snapshots[0].id, + "first snapshot selected"); + equal(getState().diffing.secondSnapshotId, null, + "no second snapshot selected"); + equal(getState().diffing.state, diffingState.SELECTING, + "should still be in diffing state SELECTING"); + + // Can't diff first snapshot with itself; this is a noop. + dispatch(selectSnapshotForDiffing(getState().snapshots[0])); + ok(getState().diffing, "still diffing"); + equal(getState().diffing.firstSnapshotId, getState().snapshots[0].id, + "first snapshot still selected"); + equal(getState().diffing.secondSnapshotId, null, + "still no second snapshot selected"); + equal(getState().diffing.state, diffingState.SELECTING, + "should still be in diffing state SELECTING"); + + // Select second snapshot for diffing. + dispatch(selectSnapshotForDiffing(getState().snapshots[1])); + ok(getState().diffing, "still diffing"); + equal(getState().diffing.firstSnapshotId, getState().snapshots[0].id, + "first snapshot still selected"); + equal(getState().diffing.secondSnapshotId, getState().snapshots[1].id, + "second snapshot selected"); + + // Can't select more than two snapshots for diffing. + dumpn("Expecting exception:"); + threw = false; + try { + dispatch(selectSnapshotForDiffing(getState().snapshots[2])); + } catch (error) { + threw = true; + } + ok(threw, "Can't select more than two snapshots for diffing"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_action_diffing_04.js b/devtools/client/memory/test/unit/test_action_diffing_04.js new file mode 100644 index 000000000..2c3cd098b --- /dev/null +++ b/devtools/client/memory/test/unit/test_action_diffing_04.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we compute census diffs. + +const { + diffingState, + snapshotState, + viewState +} = require("devtools/client/memory/constants"); +const { + toggleDiffing, + selectSnapshotForDiffingAndRefresh +} = require("devtools/client/memory/actions/diffing"); +const { + takeSnapshot, + readSnapshot +} = require("devtools/client/memory/actions/snapshot"); +const { changeView } = require("devtools/client/memory/actions/view"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + const { getState, dispatch } = store; + dispatch(changeView(viewState.CENSUS)); + + equal(getState().diffing, null, "not diffing by default"); + + const s1 = yield dispatch(takeSnapshot(front, heapWorker)); + const s2 = yield dispatch(takeSnapshot(front, heapWorker)); + const s3 = yield dispatch(takeSnapshot(front, heapWorker)); + dispatch(readSnapshot(heapWorker, s1)); + dispatch(readSnapshot(heapWorker, s2)); + dispatch(readSnapshot(heapWorker, s3)); + yield waitUntilSnapshotState(store, [snapshotState.READ, + snapshotState.READ, + snapshotState.READ]); + + dispatch(toggleDiffing()); + dispatch(selectSnapshotForDiffingAndRefresh(heapWorker, + getState().snapshots[0])); + dispatch(selectSnapshotForDiffingAndRefresh(heapWorker, + getState().snapshots[1])); + + ok(getState().diffing, "We should be diffing."); + equal(getState().diffing.firstSnapshotId, getState().snapshots[0].id, + "First snapshot selected."); + equal(getState().diffing.secondSnapshotId, getState().snapshots[1].id, + "Second snapshot selected."); + + yield waitUntilState(store, + state => + state.diffing.state === diffingState.TAKING_DIFF); + ok(true, + "Selecting two snapshots for diffing should trigger computing a diff."); + + yield waitUntilState(store, + state => state.diffing.state === diffingState.TOOK_DIFF); + ok(true, "And then the diff should complete."); + ok(getState().diffing.census, "And we should have a census."); + ok(getState().diffing.census.report, "And that census should have a report."); + equal(getState().diffing.census.display, getState().censusDisplay, + "And that census should have the correct display"); + equal(getState().diffing.census.filter, getState().filter, + "And that census should have the correct filter"); + equal(getState().diffing.census.display.inverted, + getState().censusDisplay.inverted, + "And that census should have the correct inversion"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_action_diffing_05.js b/devtools/client/memory/test/unit/test_action_diffing_05.js new file mode 100644 index 000000000..e4a1491ac --- /dev/null +++ b/devtools/client/memory/test/unit/test_action_diffing_05.js @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we recompute census diffs at the appropriate times. + +const { + diffingState, + snapshotState, + censusDisplays, + viewState, +} = require("devtools/client/memory/constants"); +const { + setCensusDisplayAndRefresh, +} = require("devtools/client/memory/actions/census-display"); +const { + toggleDiffing, + selectSnapshotForDiffingAndRefresh, +} = require("devtools/client/memory/actions/diffing"); +const { + setFilterStringAndRefresh, +} = require("devtools/client/memory/actions/filter"); +const { + takeSnapshot, + readSnapshot, +} = require("devtools/client/memory/actions/snapshot"); +const { changeView } = require("devtools/client/memory/actions/view"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + const { getState, dispatch } = store; + dispatch(changeView(viewState.CENSUS)); + + yield dispatch(setCensusDisplayAndRefresh(heapWorker, + censusDisplays.allocationStack)); + equal(getState().censusDisplay.inverted, false, + "not inverted at start"); + + equal(getState().diffing, null, "not diffing by default"); + + const s1 = yield dispatch(takeSnapshot(front, heapWorker)); + const s2 = yield dispatch(takeSnapshot(front, heapWorker)); + const s3 = yield dispatch(takeSnapshot(front, heapWorker)); + dispatch(readSnapshot(heapWorker, s1)); + dispatch(readSnapshot(heapWorker, s2)); + dispatch(readSnapshot(heapWorker, s3)); + yield waitUntilSnapshotState(store, [snapshotState.READ, + snapshotState.READ, + snapshotState.READ]); + + yield dispatch(toggleDiffing()); + dispatch(selectSnapshotForDiffingAndRefresh(heapWorker, + getState().snapshots[0])); + dispatch(selectSnapshotForDiffingAndRefresh(heapWorker, + getState().snapshots[1])); + yield waitUntilState(store, + state => state.diffing.state === diffingState.TOOK_DIFF); + + const shouldTriggerRecompute = [ + { + name: "toggling inversion", + func: () => dispatch(setCensusDisplayAndRefresh( + heapWorker, + censusDisplays.invertedAllocationStack)) + }, + { + name: "filtering", + func: () => dispatch(setFilterStringAndRefresh("scr", heapWorker)) + }, + { + name: "changing displays", + func: () => + dispatch(setCensusDisplayAndRefresh(heapWorker, + censusDisplays.coarseType)) + } + ]; + + for (let { name, func } of shouldTriggerRecompute) { + dumpn(`Testing that "${name}" triggers a diff recompute`); + func(); + + yield waitUntilState(store, + state => + state.diffing.state === diffingState.TAKING_DIFF); + ok(true, "triggered diff recompute."); + + yield waitUntilState(store, + state => + state.diffing.state === diffingState.TOOK_DIFF); + ok(true, "And then the diff should complete."); + ok(getState().diffing.census, "And we should have a census."); + ok(getState().diffing.census.report, + "And that census should have a report."); + equal(getState().diffing.census.display, + getState().censusDisplay, + "And that census should have the correct display"); + equal(getState().diffing.census.filter, getState().filter, + "And that census should have the correct filter"); + equal(getState().diffing.census.display.inverted, + getState().censusDisplay.inverted, + "And that census should have the correct inversion"); + } + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_dominator_trees_01.js b/devtools/client/memory/test/unit/test_dominator_trees_01.js new file mode 100644 index 000000000..5fcf441d4 --- /dev/null +++ b/devtools/client/memory/test/unit/test_dominator_trees_01.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we can compute and fetch the dominator tree for a snapshot. + +let { + snapshotState: states, + dominatorTreeState, + treeMapState, +} = require("devtools/client/memory/constants"); +let { + takeSnapshotAndCensus, + computeAndFetchDominatorTree, +} = require("devtools/client/memory/actions/snapshot"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + let { getState, dispatch } = store; + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]); + ok(!getState().snapshots[0].dominatorTree, + "There shouldn't be a dominator tree model yet since it is not computed " + + "until we switch to the dominators view."); + + // Change to the dominator tree view. + dispatch(computeAndFetchDominatorTree(heapWorker, getState().snapshots[0].id)); + ok(getState().snapshots[0].dominatorTree, + "Should now have a dominator tree model for the selected snapshot"); + + // Wait for the dominator tree to start being computed. + yield waitUntilState(store, state => + state.snapshots[0].dominatorTree.state === dominatorTreeState.COMPUTING); + ok(true, "The dominator tree started computing"); + ok(!getState().snapshots[0].dominatorTree.root, + "When the dominator tree is computing, we should not have its root"); + + // Wait for the dominator tree to finish computing and start being fetched. + yield waitUntilState(store, state => + state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING); + ok(true, "The dominator tree started fetching"); + ok(!getState().snapshots[0].dominatorTree.root, + "When the dominator tree is fetching, we should not have its root"); + + // Wait for the dominator tree to finish being fetched. + yield waitUntilState(store, state => + state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED); + ok(true, "The dominator tree was fetched"); + ok(getState().snapshots[0].dominatorTree.root, + "When the dominator tree is loaded, we should have its root"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_dominator_trees_02.js b/devtools/client/memory/test/unit/test_dominator_trees_02.js new file mode 100644 index 000000000..b35961004 --- /dev/null +++ b/devtools/client/memory/test/unit/test_dominator_trees_02.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that selecting the dominator tree view automatically kicks off fetching +// and computing dominator trees. + +const { + snapshotState: states, + dominatorTreeState, + viewState, + treeMapState, +} = require("devtools/client/memory/constants"); +const { + takeSnapshotAndCensus, +} = require("devtools/client/memory/actions/snapshot"); +const { + changeViewAndRefresh +} = require("devtools/client/memory/actions/view"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + let { getState, dispatch } = store; + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]); + ok(!getState().snapshots[0].dominatorTree, + "There shouldn't be a dominator tree model yet since it is not computed " + + "until we switch to the dominators view."); + + dispatch(changeViewAndRefresh(viewState.DOMINATOR_TREE, heapWorker)); + ok(getState().snapshots[0].dominatorTree, + "Should now have a dominator tree model for the selected snapshot"); + + // Wait for the dominator tree to start being computed. + yield waitUntilState(store, state => + state.snapshots[0].dominatorTree.state === dominatorTreeState.COMPUTING); + ok(true, "The dominator tree started computing"); + ok(!getState().snapshots[0].dominatorTree.root, + "When the dominator tree is computing, we should not have its root"); + + // Wait for the dominator tree to finish computing and start being fetched. + yield waitUntilState(store, state => + state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING); + ok(true, "The dominator tree started fetching"); + ok(!getState().snapshots[0].dominatorTree.root, + "When the dominator tree is fetching, we should not have its root"); + + // Wait for the dominator tree to finish being fetched. + yield waitUntilState(store, state => + state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED); + ok(true, "The dominator tree was fetched"); + ok(getState().snapshots[0].dominatorTree.root, + "When the dominator tree is loaded, we should have its root"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_dominator_trees_03.js b/devtools/client/memory/test/unit/test_dominator_trees_03.js new file mode 100644 index 000000000..09e6477f4 --- /dev/null +++ b/devtools/client/memory/test/unit/test_dominator_trees_03.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that selecting the dominator tree view and then taking a snapshot +// properly kicks off fetching and computing dominator trees. + +const { + snapshotState: states, + dominatorTreeState, + viewState, +} = require("devtools/client/memory/constants"); +const { + takeSnapshotAndCensus, +} = require("devtools/client/memory/actions/snapshot"); +const { + changeView +} = require("devtools/client/memory/actions/view"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + let { getState, dispatch } = store; + + dispatch(changeView(viewState.DOMINATOR_TREE)); + equal(getState().view.state, viewState.DOMINATOR_TREE, + "We should now be in the DOMINATOR_TREE view"); + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + + // Wait for the dominator tree to start being computed. + yield waitUntilState(store, state => + state.snapshots[0] && state.snapshots[0].dominatorTree); + equal(getState().snapshots[0].dominatorTree.state, + dominatorTreeState.COMPUTING, + "The dominator tree started computing"); + ok(!getState().snapshots[0].dominatorTree.root, + "When the dominator tree is computing, we should not have its root"); + + // Wait for the dominator tree to finish computing and start being fetched. + yield waitUntilState(store, state => + state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING); + ok(true, "The dominator tree started fetching"); + ok(!getState().snapshots[0].dominatorTree.root, + "When the dominator tree is fetching, we should not have its root"); + + // Wait for the dominator tree to finish being fetched. + yield waitUntilState(store, state => + state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED); + ok(true, "The dominator tree was fetched"); + ok(getState().snapshots[0].dominatorTree.root, + "When the dominator tree is loaded, we should have its root"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_dominator_trees_04.js b/devtools/client/memory/test/unit/test_dominator_trees_04.js new file mode 100644 index 000000000..ba375c354 --- /dev/null +++ b/devtools/client/memory/test/unit/test_dominator_trees_04.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that selecting the dominator tree view while in the middle of taking a +// snapshot properly kicks off fetching and computing dominator trees. + +const { + snapshotState: states, + dominatorTreeState, + viewState, +} = require("devtools/client/memory/constants"); +const { + takeSnapshotAndCensus, +} = require("devtools/client/memory/actions/snapshot"); +const { + changeView +} = require("devtools/client/memory/actions/view"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + + for (let intermediateSnapshotState of [states.SAVING, + states.READING, + states.READ]) { + dumpn(`Testing switching to the DOMINATOR_TREE view in the middle of the ${intermediateSnapshotState} snapshot state`); + + let store = Store(); + let { getState, dispatch } = store; + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilSnapshotState(store, [intermediateSnapshotState]); + + dispatch(changeView(viewState.DOMINATOR_TREE)); + equal(getState().view.state, viewState.DOMINATOR_TREE, + "We should now be in the DOMINATOR_TREE view"); + + // Wait for the dominator tree to start being computed. + yield waitUntilState(store, state => + state.snapshots[0] && state.snapshots[0].dominatorTree); + equal(getState().snapshots[0].dominatorTree.state, + dominatorTreeState.COMPUTING, + "The dominator tree started computing"); + ok(!getState().snapshots[0].dominatorTree.root, + "When the dominator tree is computing, we should not have its root"); + + // Wait for the dominator tree to finish computing and start being fetched. + yield waitUntilState(store, state => + state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING); + ok(true, "The dominator tree started fetching"); + ok(!getState().snapshots[0].dominatorTree.root, + "When the dominator tree is fetching, we should not have its root"); + + // Wait for the dominator tree to finish being fetched. + yield waitUntilState(store, state => + state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED); + ok(true, "The dominator tree was fetched"); + ok(getState().snapshots[0].dominatorTree.root, + "When the dominator tree is loaded, we should have its root"); + } + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_dominator_trees_05.js b/devtools/client/memory/test/unit/test_dominator_trees_05.js new file mode 100644 index 000000000..42ea13e18 --- /dev/null +++ b/devtools/client/memory/test/unit/test_dominator_trees_05.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that changing the currently selected snapshot to a snapshot that does +// not have a dominator tree will automatically compute and fetch one for it. + +let { + snapshotState: states, + dominatorTreeState, + viewState, + treeMapState, +} = require("devtools/client/memory/constants"); +let { + takeSnapshotAndCensus, + selectSnapshotAndRefresh, +} = require("devtools/client/memory/actions/snapshot"); + +let { changeView } = require("devtools/client/memory/actions/view"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + let { getState, dispatch } = store; + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED, + treeMapState.SAVED]); + + ok(getState().snapshots[1].selected, "The second snapshot is selected"); + + // Change to the dominator tree view. + dispatch(changeView(viewState.DOMINATOR_TREE)); + + // Wait for the dominator tree to finish being fetched. + yield waitUntilState(store, state => + state.snapshots[1].dominatorTree && + state.snapshots[1].dominatorTree.state === dominatorTreeState.LOADED); + ok(true, "The second snapshot's dominator tree was fetched"); + + // Select the first snapshot. + dispatch(selectSnapshotAndRefresh(heapWorker, getState().snapshots[0].id)); + + // And now the first snapshot should have its dominator tree fetched and + // computed because of the new selection. + yield waitUntilState(store, state => + state.snapshots[0].dominatorTree && + state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED); + ok(true, "The first snapshot's dominator tree was fetched"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_dominator_trees_06.js b/devtools/client/memory/test/unit/test_dominator_trees_06.js new file mode 100644 index 000000000..78f869102 --- /dev/null +++ b/devtools/client/memory/test/unit/test_dominator_trees_06.js @@ -0,0 +1,127 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we can incrementally fetch a subtree of a dominator tree. + +const { + snapshotState: states, + dominatorTreeState, + viewState, +} = require("devtools/client/memory/constants"); +const { + takeSnapshotAndCensus, + selectSnapshotAndRefresh, + fetchImmediatelyDominated, +} = require("devtools/client/memory/actions/snapshot"); +const DominatorTreeLazyChildren + = require("devtools/client/memory/dominator-tree-lazy-children"); + +const { changeView } = require("devtools/client/memory/actions/view"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + let { getState, dispatch } = store; + + dispatch(changeView(viewState.DOMINATOR_TREE)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + + // Wait for the dominator tree to finish being fetched. + yield waitUntilState(store, state => + state.snapshots[0] && + state.snapshots[0].dominatorTree && + state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED); + ok(getState().snapshots[0].dominatorTree.root, + "The dominator tree was fetched"); + + // Find a node that has children, but none of them are loaded. + + function findNode(node) { + if (node.moreChildrenAvailable && !node.children) { + return node; + } + + if (node.children) { + for (let child of node.children) { + const found = findNode(child); + if (found) { + return found; + } + } + } + + return null; + } + + const oldRoot = getState().snapshots[0].dominatorTree.root; + const oldNode = findNode(oldRoot); + ok(oldNode, + "Should have found a node with children that are not loaded since we " + + "only send partial dominator trees across initially and load the rest " + + "on demand"); + ok(oldNode !== oldRoot, "But the node should not be the root"); + + const lazyChildren = new DominatorTreeLazyChildren(oldNode.nodeId, 0); + dispatch(fetchImmediatelyDominated(heapWorker, getState().snapshots[0].id, lazyChildren)); + + equal(getState().snapshots[0].dominatorTree.state, + dominatorTreeState.INCREMENTAL_FETCHING, + "Fetching immediately dominated children should put us in the " + + "INCREMENTAL_FETCHING state"); + + yield waitUntilState(store, state => + state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED); + ok(true, + "The dominator tree should go back to LOADED after the incremental " + + "fetching is done."); + + const newRoot = getState().snapshots[0].dominatorTree.root; + ok(oldRoot !== newRoot, + "When we insert new nodes, we get a new tree"); + equal(oldRoot.children.length, newRoot.children.length, + "The new tree's root should have the same number of children as the " + + "old root's"); + + let differentChildrenCount = 0; + for (let i = 0; i < oldRoot.children.length; i++) { + if (oldRoot.children[i] !== newRoot.children[i]) { + differentChildrenCount++; + } + } + equal(differentChildrenCount, 1, + "All subtrees except the subtree we inserted incrementally fetched " + + "children into should be the same because we use persistent updates"); + + // Find the new node which has the children inserted. + + function findNewNode(node) { + if (node.nodeId === oldNode.nodeId) { + return node; + } + + if (node.children) { + for (let child of node.children) { + const found = findNewNode(child); + if (found) { + return found; + } + } + } + + return null; + } + + const newNode = findNewNode(newRoot); + ok(newNode, "Should find the node in the new tree again"); + ok(newNode !== oldNode, "We did not mutate the old node in place, instead created a new node"); + ok(newNode.children, "And the new node should have the children attached"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_dominator_trees_07.js b/devtools/client/memory/test/unit/test_dominator_trees_07.js new file mode 100644 index 000000000..9121fc878 --- /dev/null +++ b/devtools/client/memory/test/unit/test_dominator_trees_07.js @@ -0,0 +1,146 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we can incrementally fetch two subtrees in the same dominator tree +// concurrently. This exercises the activeFetchRequestCount machinery. + +const { + snapshotState: states, + dominatorTreeState, + viewState, +} = require("devtools/client/memory/constants"); +const { + takeSnapshotAndCensus, + selectSnapshotAndRefresh, + fetchImmediatelyDominated, +} = require("devtools/client/memory/actions/snapshot"); +const DominatorTreeLazyChildren + = require("devtools/client/memory/dominator-tree-lazy-children"); + +const { changeView } = require("devtools/client/memory/actions/view"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + let { getState, dispatch } = store; + + dispatch(changeView(viewState.DOMINATOR_TREE)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + + // Wait for the dominator tree to finish being fetched. + yield waitUntilState(store, state => + state.snapshots[0] && + state.snapshots[0].dominatorTree && + state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED); + ok(getState().snapshots[0].dominatorTree.root, + "The dominator tree was fetched"); + + // Find a node that has more children. + + function findNode(node) { + if (node.moreChildrenAvailable && !node.children) { + return node; + } + + if (node.children) { + for (let child of node.children) { + const found = findNode(child); + if (found) { + return found; + } + } + } + + return null; + } + + const oldRoot = getState().snapshots[0].dominatorTree.root; + const oldNode = findNode(oldRoot); + ok(oldNode, "Should have found a node with more children."); + + // Find another node that has more children. + function findNodeRev(node) { + if (node.moreChildrenAvailable && !node.children) { + return node; + } + + if (node.children) { + for (let child of node.children.slice().reverse()) { + const found = findNodeRev(child); + if (found) { + return found; + } + } + } + + return null; + } + + const oldNode2 = findNodeRev(oldRoot); + ok(oldNode2, "Should have found another node with more children."); + ok(oldNode !== oldNode2, + "The second node should not be the same as the first one"); + + // Fetch both subtrees concurrently. + dispatch(fetchImmediatelyDominated(heapWorker, getState().snapshots[0].id, + new DominatorTreeLazyChildren(oldNode.nodeId, 0))); + dispatch(fetchImmediatelyDominated(heapWorker, getState().snapshots[0].id, + new DominatorTreeLazyChildren(oldNode2.nodeId, 0))); + + equal(getState().snapshots[0].dominatorTree.state, + dominatorTreeState.INCREMENTAL_FETCHING, + "Fetching immediately dominated children should put us in the " + + "INCREMENTAL_FETCHING state"); + + yield waitUntilState(store, state => + state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED); + ok(true, + "The dominator tree should go back to LOADED after the incremental " + + "fetching is done."); + + const newRoot = getState().snapshots[0].dominatorTree.root; + ok(oldRoot !== newRoot, + "When we insert new nodes, we get a new tree"); + + // Find the new node which has the children inserted. + + function findNodeWithId(id, node) { + if (node.nodeId === id) { + return node; + } + + if (node.children) { + for (let child of node.children) { + const found = findNodeWithId(id, child); + if (found) { + return found; + } + } + } + + return null; + } + + const newNode = findNodeWithId(oldNode.nodeId, newRoot); + ok(newNode, "Should find the node in the new tree again"); + ok(newNode !== oldNode, + "We did not mutate the old node in place, instead created a new node"); + ok(newNode.children.length, + "And the new node should have the new children attached"); + + const newNode2 = findNodeWithId(oldNode2.nodeId, newRoot); + ok(newNode2, "Should find the second node in the new tree again"); + ok(newNode2 !== oldNode2, + "We did not mutate the second old node in place, instead created a new node"); + ok(newNode2.children, + "And the new node should have the new children attached"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_dominator_trees_08.js b/devtools/client/memory/test/unit/test_dominator_trees_08.js new file mode 100644 index 000000000..27f35180c --- /dev/null +++ b/devtools/client/memory/test/unit/test_dominator_trees_08.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we can change the display with which we describe a dominator tree +// and that the dominator tree is re-fetched. + +const { + snapshotState: states, + dominatorTreeState, + viewState, + labelDisplays, + treeMapState +} = require("devtools/client/memory/constants"); +const { + setLabelDisplayAndRefresh +} = require("devtools/client/memory/actions/label-display"); +const { + changeView, +} = require("devtools/client/memory/actions/view"); +const { + takeSnapshotAndCensus, + computeAndFetchDominatorTree, +} = require("devtools/client/memory/actions/snapshot"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + let { getState, dispatch } = store; + + dispatch(changeView(viewState.DOMINATOR_TREE)); + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]); + ok(!getState().snapshots[0].dominatorTree, + "There shouldn't be a dominator tree model yet since it is not computed " + + "until we switch to the dominators view."); + + // Wait for the dominator tree to finish being fetched. + yield waitUntilState(store, state => + state.snapshots[0] && + state.snapshots[0].dominatorTree && + state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED); + + ok(getState().labelDisplay, + "We have a default display for describing nodes in a dominator tree"); + equal(getState().labelDisplay, + labelDisplays.coarseType, + "and the default is coarse type"); + equal(getState().labelDisplay, + getState().snapshots[0].dominatorTree.display, + "and the newly computed dominator tree has that display"); + + // Switch to the allocationStack display. + dispatch(setLabelDisplayAndRefresh( + heapWorker, + labelDisplays.allocationStack)); + + yield waitUntilState(store, state => + state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING); + ok(true, + "switching display types caused the dominator tree to be fetched " + + "again."); + + yield waitUntilState(store, state => + state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED); + equal(getState().snapshots[0].dominatorTree.display, + labelDisplays.allocationStack, + "The new dominator tree's display is allocationStack"); + equal(getState().labelDisplay, + labelDisplays.allocationStack, + "as is our requested dominator tree display"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_dominator_trees_09.js b/devtools/client/memory/test/unit/test_dominator_trees_09.js new file mode 100644 index 000000000..c1d1f7495 --- /dev/null +++ b/devtools/client/memory/test/unit/test_dominator_trees_09.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we can change the display with which we describe a dominator tree +// while the dominator tree is in the middle of being fetched. + +const { + snapshotState: states, + dominatorTreeState, + viewState, + labelDisplays, + treeMapState, +} = require("devtools/client/memory/constants"); +const { + setLabelDisplayAndRefresh +} = require("devtools/client/memory/actions/label-display"); +const { + changeView, +} = require("devtools/client/memory/actions/view"); +const { + takeSnapshotAndCensus, + computeAndFetchDominatorTree, +} = require("devtools/client/memory/actions/snapshot"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + let { getState, dispatch } = store; + + dispatch(changeView(viewState.DOMINATOR_TREE)); + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]); + ok(!getState().snapshots[0].dominatorTree, + "There shouldn't be a dominator tree model yet since it is not computed " + + "until we switch to the dominators view."); + + // Wait for the dominator tree to start fetching. + yield waitUntilState(store, state => + state.snapshots[0] && + state.snapshots[0].dominatorTree && + state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING); + + ok(getState().labelDisplay, + "We have a default display for describing nodes in a dominator tree"); + equal(getState().labelDisplay, + labelDisplays.coarseType, + "and the default is coarse type"); + equal(getState().labelDisplay, + getState().snapshots[0].dominatorTree.display, + "and the newly computed dominator tree has that display"); + + // Switch to the allocationStack display while we are still fetching the + // dominator tree. + dispatch(setLabelDisplayAndRefresh( + heapWorker, + labelDisplays.allocationStack)); + + // Wait for the dominator tree to finish being fetched. + yield waitUntilState(store, state => + state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED); + + equal(getState().snapshots[0].dominatorTree.display, + labelDisplays.allocationStack, + "The new dominator tree's display is allocationStack"); + equal(getState().labelDisplay, + labelDisplays.allocationStack, + "as is our requested dominator tree display"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_dominator_trees_10.js b/devtools/client/memory/test/unit/test_dominator_trees_10.js new file mode 100644 index 000000000..f7cf86a10 --- /dev/null +++ b/devtools/client/memory/test/unit/test_dominator_trees_10.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that we maintain focus of the selected dominator tree node across +// changing breakdowns for labeling them. + +let { + snapshotState: states, + dominatorTreeState, + labelDisplays, + viewState, +} = require("devtools/client/memory/constants"); +let { + takeSnapshotAndCensus, + focusDominatorTreeNode, +} = require("devtools/client/memory/actions/snapshot"); +const { + changeView, +} = require("devtools/client/memory/actions/view"); +const { + setLabelDisplayAndRefresh, +} = require("devtools/client/memory/actions/label-display"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + let { getState, dispatch } = store; + + dispatch(changeView(viewState.DOMINATOR_TREE)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + + // Wait for the dominator tree to finish being fetched. + yield waitUntilState(store, state => + state.snapshots[0] && + state.snapshots[0].dominatorTree && + state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED); + ok(true, "The dominator tree was fetched"); + + const root = getState().snapshots[0].dominatorTree.root; + ok(root, "When the dominator tree is loaded, we should have its root"); + + dispatch(focusDominatorTreeNode(getState().snapshots[0].id, root)); + equal(root, getState().snapshots[0].dominatorTree.focused, + "The root should be focused."); + + equal(getState().labelDisplay, labelDisplays.coarseType, + "Using labelDisplays.coarseType by default"); + dispatch(setLabelDisplayAndRefresh(heapWorker, + labelDisplays.allocationStack)); + equal(getState().labelDisplay, labelDisplays.allocationStack, + "Using labelDisplays.allocationStack now"); + + yield waitUntilState(store, state => + state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING); + ok(true, "We started re-fetching the dominator tree"); + + yield waitUntilState(store, state => + state.snapshots[0].dominatorTree.state === dominatorTreeState.LOADED); + ok(true, "The dominator tree was loaded again"); + + ok(getState().snapshots[0].dominatorTree.focused, + "Still have a focused node"); + equal(getState().snapshots[0].dominatorTree.focused.nodeId, root.nodeId, + "Focused node is the same as before"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_individuals_01.js b/devtools/client/memory/test/unit/test_individuals_01.js new file mode 100644 index 000000000..36971a7dc --- /dev/null +++ b/devtools/client/memory/test/unit/test_individuals_01.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Basic test for switching to the individuals view. + +const { + censusState, + viewState, + individualsState, +} = require("devtools/client/memory/constants"); +const { + fetchIndividuals, + takeSnapshotAndCensus, +} = require("devtools/client/memory/actions/snapshot"); +const { + changeView, +} = require("devtools/client/memory/actions/view"); + +function run_test() { + run_next_test(); +} + +const EXPECTED_INDIVIDUAL_STATES = [ + individualsState.COMPUTING_DOMINATOR_TREE, + individualsState.FETCHING, + individualsState.FETCHED, +]; + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + const { getState, dispatch } = store; + + equal(getState().individuals, null, + "no individuals state by default"); + + dispatch(changeView(viewState.CENSUS)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]); + + const root = getState().snapshots[0].census.report; + ok(root, "Should have a census"); + + const reportLeafIndex = findReportLeafIndex(root); + ok(reportLeafIndex, "Should get a reportLeafIndex"); + + const snapshotId = getState().snapshots[0].id; + ok(snapshotId, "Should have a snapshot id"); + + const breakdown = getState().snapshots[0].census.display.breakdown; + ok(breakdown, "Should have a breakdown"); + + dispatch(fetchIndividuals(heapWorker, snapshotId, breakdown, + reportLeafIndex)); + + // Wait for each expected state. + for (let state of EXPECTED_INDIVIDUAL_STATES) { + yield waitUntilState(store, s => { + return s.view.state === viewState.INDIVIDUALS && + s.individuals && + s.individuals.state === state; + }); + ok(true, `Reached state = ${state}`); + } + + ok(getState().individuals, "Should have individuals state"); + ok(getState().individuals.nodes, "Should have individuals nodes"); + ok(getState().individuals.nodes.length > 0, + "Should have a positive number of nodes"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_individuals_02.js b/devtools/client/memory/test/unit/test_individuals_02.js new file mode 100644 index 000000000..9e23e2c4f --- /dev/null +++ b/devtools/client/memory/test/unit/test_individuals_02.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test switching to the individuals view when we are in the middle of computing +// a dominator tree. + +const { + censusState, + dominatorTreeState, + viewState, + individualsState, +} = require("devtools/client/memory/constants"); +const { + fetchIndividuals, + takeSnapshotAndCensus, + computeDominatorTree, +} = require("devtools/client/memory/actions/snapshot"); +const { + changeView, +} = require("devtools/client/memory/actions/view"); + +function run_test() { + run_next_test(); +} + +const EXPECTED_INDIVIDUAL_STATES = [ + individualsState.COMPUTING_DOMINATOR_TREE, + individualsState.FETCHING, + individualsState.FETCHED, +]; + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + const { getState, dispatch } = store; + + equal(getState().individuals, null, + "no individuals state by default"); + + dispatch(changeView(viewState.CENSUS)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]); + + const root = getState().snapshots[0].census.report; + ok(root, "Should have a census"); + + const reportLeafIndex = findReportLeafIndex(root); + ok(reportLeafIndex, "Should get a reportLeafIndex"); + + const snapshotId = getState().snapshots[0].id; + ok(snapshotId, "Should have a snapshot id"); + + const breakdown = getState().snapshots[0].census.display.breakdown; + ok(breakdown, "Should have a breakdown"); + + // Start computing a dominator tree. + + dispatch(computeDominatorTree(heapWorker, snapshotId)); + equal(getState().snapshots[0].dominatorTree.state, + dominatorTreeState.COMPUTING, + "Should be computing dominator tree"); + + // Fetch individuals in the middle of computing the dominator tree. + + dispatch(fetchIndividuals(heapWorker, snapshotId, breakdown, + reportLeafIndex)); + + // Wait for each expected state. + for (let state of EXPECTED_INDIVIDUAL_STATES) { + yield waitUntilState(store, s => { + return s.view.state === viewState.INDIVIDUALS && + s.individuals && + s.individuals.state === state; + }); + ok(true, `Reached state = ${state}`); + } + + ok(getState().individuals, "Should have individuals state"); + ok(getState().individuals.nodes, "Should have individuals nodes"); + ok(getState().individuals.nodes.length > 0, + "Should have a positive number of nodes"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_individuals_03.js b/devtools/client/memory/test/unit/test_individuals_03.js new file mode 100644 index 000000000..54460b737 --- /dev/null +++ b/devtools/client/memory/test/unit/test_individuals_03.js @@ -0,0 +1,106 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test switching to the individuals view when we are in the diffing view. + +const { + censusState, + diffingState, + viewState, + individualsState, +} = require("devtools/client/memory/constants"); +const { + fetchIndividuals, + takeSnapshotAndCensus, +} = require("devtools/client/memory/actions/snapshot"); +const { + changeView, + popViewAndRefresh, +} = require("devtools/client/memory/actions/view"); +const { + selectSnapshotForDiffingAndRefresh, +} = require("devtools/client/memory/actions/diffing"); + +function run_test() { + run_next_test(); +} + +const EXPECTED_INDIVIDUAL_STATES = [ + individualsState.COMPUTING_DOMINATOR_TREE, + individualsState.FETCHING, + individualsState.FETCHED, +]; + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + const { getState, dispatch } = store; + + dispatch(changeView(viewState.CENSUS)); + + // Take two snapshots and diff them from each other. + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilCensusState(store, s => s.census, [censusState.SAVED, + censusState.SAVED]); + + dispatch(changeView(viewState.DIFFING)); + dispatch(selectSnapshotForDiffingAndRefresh(heapWorker, getState().snapshots[0])); + dispatch(selectSnapshotForDiffingAndRefresh(heapWorker, getState().snapshots[1])); + + yield waitUntilState(store, state => { + return state.diffing && + state.diffing.state === diffingState.TOOK_DIFF; + }); + ok(getState().diffing.census); + + // Fetch individuals. + + const root = getState().diffing.census.report; + ok(root, "Should have a census"); + + const reportLeafIndex = findReportLeafIndex(root); + ok(reportLeafIndex, "Should get a reportLeafIndex"); + + const snapshotId = getState().diffing.secondSnapshotId; + ok(snapshotId, "Should have a snapshot id"); + + const breakdown = getState().censusDisplay.breakdown; + ok(breakdown, "Should have a breakdown"); + + dispatch(fetchIndividuals(heapWorker, snapshotId, breakdown, + reportLeafIndex)); + + for (let state of EXPECTED_INDIVIDUAL_STATES) { + yield waitUntilState(store, s => { + return s.view.state === viewState.INDIVIDUALS && + s.individuals && + s.individuals.state === state; + }); + ok(true, `Reached state = ${state}`); + } + + ok(getState().individuals, "Should have individuals state"); + ok(getState().individuals.nodes, "Should have individuals nodes"); + ok(getState().individuals.nodes.length > 0, + "Should have a positive number of nodes"); + + // Pop the view back to the diffing. + + dispatch(popViewAndRefresh(heapWorker)); + + yield waitUntilState(store, state => { + return state.diffing && + state.diffing.state === diffingState.TOOK_DIFF; + }); + + ok(getState().diffing.census.report, + "We have our census diff again after popping back to the last view"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_individuals_04.js b/devtools/client/memory/test/unit/test_individuals_04.js new file mode 100644 index 000000000..c160d555f --- /dev/null +++ b/devtools/client/memory/test/unit/test_individuals_04.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test showing individual Array objects. + +const { + censusState, + viewState, + individualsState, +} = require("devtools/client/memory/constants"); +const { + fetchIndividuals, + takeSnapshotAndCensus, +} = require("devtools/client/memory/actions/snapshot"); +const { + changeView, +} = require("devtools/client/memory/actions/view"); +const { + setFilterString, +} = require("devtools/client/memory/actions/filter"); + +function run_test() { + run_next_test(); +} + +const EXPECTED_INDIVIDUAL_STATES = [ + individualsState.COMPUTING_DOMINATOR_TREE, + individualsState.FETCHING, + individualsState.FETCHED, +]; + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + const { getState, dispatch } = store; + + dispatch(changeView(viewState.CENSUS)); + dispatch(setFilterString("Array")); + + // Take a snapshot and wait for the census to finish. + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]); + + // Fetch individuals. + + const root = getState().snapshots[0].census.report; + ok(root, "Should have a census"); + + const reportLeafIndex = findReportLeafIndex(root, "Array"); + ok(reportLeafIndex, "Should get a reportLeafIndex for Array"); + + const snapshotId = getState().snapshots[0].id; + ok(snapshotId, "Should have a snapshot id"); + + const breakdown = getState().censusDisplay.breakdown; + ok(breakdown, "Should have a breakdown"); + + dispatch(fetchIndividuals(heapWorker, snapshotId, breakdown, + reportLeafIndex)); + + for (let state of EXPECTED_INDIVIDUAL_STATES) { + yield waitUntilState(store, s => { + return s.view.state === viewState.INDIVIDUALS && + s.individuals && + s.individuals.state === state; + }); + ok(true, `Reached state = ${state}`); + } + + ok(getState().individuals, "Should have individuals state"); + ok(getState().individuals.nodes, "Should have individuals nodes"); + ok(getState().individuals.nodes.length > 0, + "Should have a positive number of nodes"); + + // Assert that all the individuals are `Array`s. + + for (let node of getState().individuals.nodes) { + dumpn("Checking node: " + node.label.join(" > ")); + ok(node.label.find(part => part === "Array"), + "The node should be an Array node"); + } + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_individuals_05.js b/devtools/client/memory/test/unit/test_individuals_05.js new file mode 100644 index 000000000..1f8873826 --- /dev/null +++ b/devtools/client/memory/test/unit/test_individuals_05.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test showing individual objects that do not have allocation stacks. + +const { + censusState, + viewState, + individualsState, + censusDisplays, +} = require("devtools/client/memory/constants"); +const { + fetchIndividuals, + takeSnapshotAndCensus, +} = require("devtools/client/memory/actions/snapshot"); +const { + changeView, +} = require("devtools/client/memory/actions/view"); +const { + setCensusDisplay, +} = require("devtools/client/memory/actions/census-display"); + +function run_test() { + run_next_test(); +} + +const EXPECTED_INDIVIDUAL_STATES = [ + individualsState.COMPUTING_DOMINATOR_TREE, + individualsState.FETCHING, + individualsState.FETCHED, +]; + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + const { getState, dispatch } = store; + + dispatch(changeView(viewState.CENSUS)); + dispatch(setCensusDisplay(censusDisplays.invertedAllocationStack)); + + // Take a snapshot and wait for the census to finish. + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]); + + // Fetch individuals. + + const root = getState().snapshots[0].census.report; + ok(root, "Should have a census"); + + const reportLeafIndex = findReportLeafIndex(root, "noStack"); + ok(reportLeafIndex, "Should get a reportLeafIndex for noStack"); + + const snapshotId = getState().snapshots[0].id; + ok(snapshotId, "Should have a snapshot id"); + + const breakdown = getState().censusDisplay.breakdown; + ok(breakdown, "Should have a breakdown"); + + dispatch(fetchIndividuals(heapWorker, snapshotId, breakdown, + reportLeafIndex)); + + for (let state of EXPECTED_INDIVIDUAL_STATES) { + yield waitUntilState(store, s => { + return s.view.state === viewState.INDIVIDUALS && + s.individuals && + s.individuals.state === state; + }); + ok(true, `Reached state = ${state}`); + } + + ok(getState().individuals, "Should have individuals state"); + ok(getState().individuals.nodes, "Should have individuals nodes"); + ok(getState().individuals.nodes.length > 0, + "Should have a positive number of nodes"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_individuals_06.js b/devtools/client/memory/test/unit/test_individuals_06.js new file mode 100644 index 000000000..6147b8181 --- /dev/null +++ b/devtools/client/memory/test/unit/test_individuals_06.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that clearing the current individuals' snapshot leaves the individuals +// view. + +const { + censusState, + viewState, + individualsState, +} = require("devtools/client/memory/constants"); +const { + fetchIndividuals, + takeSnapshotAndCensus, + clearSnapshots, +} = require("devtools/client/memory/actions/snapshot"); +const { + changeView, +} = require("devtools/client/memory/actions/view"); + +function run_test() { + run_next_test(); +} + +const EXPECTED_INDIVIDUAL_STATES = [ + individualsState.COMPUTING_DOMINATOR_TREE, + individualsState.FETCHING, + individualsState.FETCHED, +]; + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + const { getState, dispatch } = store; + + dispatch(changeView(viewState.CENSUS)); + + // Take a snapshot and wait for the census to finish. + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]); + + // Fetch individuals. + + const root = getState().snapshots[0].census.report; + ok(root, "Should have a census"); + + const reportLeafIndex = findReportLeafIndex(root); + ok(reportLeafIndex, "Should get a reportLeafIndex"); + + const snapshotId = getState().snapshots[0].id; + ok(snapshotId, "Should have a snapshot id"); + + const breakdown = getState().censusDisplay.breakdown; + ok(breakdown, "Should have a breakdown"); + + dispatch(fetchIndividuals(heapWorker, snapshotId, breakdown, + reportLeafIndex)); + + for (let state of EXPECTED_INDIVIDUAL_STATES) { + yield waitUntilState(store, s => { + return s.view.state === viewState.INDIVIDUALS && + s.individuals && + s.individuals.state === state; + }); + ok(true, `Reached state = ${state}`); + } + + ok(getState().individuals, "Should have individuals state"); + ok(getState().individuals.nodes, "Should have individuals nodes"); + ok(getState().individuals.nodes.length > 0, + "Should have a positive number of nodes"); + + dispatch(clearSnapshots(heapWorker)); + + equal(getState().view.state, viewState.CENSUS, + "Went back to census view"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_pop_view_01.js b/devtools/client/memory/test/unit/test_pop_view_01.js new file mode 100644 index 000000000..9e6a17095 --- /dev/null +++ b/devtools/client/memory/test/unit/test_pop_view_01.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test popping views from each intermediate individuals model state. + +const { + censusState, + viewState, + individualsState, +} = require("devtools/client/memory/constants"); +const { + fetchIndividuals, + takeSnapshotAndCensus, +} = require("devtools/client/memory/actions/snapshot"); +const { + changeView, + popViewAndRefresh, +} = require("devtools/client/memory/actions/view"); + +function run_test() { + run_next_test(); +} + +const TEST_STATES = [ + individualsState.COMPUTING_DOMINATOR_TREE, + individualsState.FETCHING, + individualsState.FETCHED, +]; + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + const { getState, dispatch } = store; + + equal(getState().individuals, null, + "no individuals state by default"); + + dispatch(changeView(viewState.CENSUS)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]); + + const root = getState().snapshots[0].census.report; + ok(root, "Should have a census"); + + const reportLeafIndex = findReportLeafIndex(root); + ok(reportLeafIndex, "Should get a reportLeafIndex"); + + const snapshotId = getState().snapshots[0].id; + ok(snapshotId, "Should have a snapshot id"); + + const breakdown = getState().snapshots[0].census.display.breakdown; + ok(breakdown, "Should have a breakdown"); + + for (let state of TEST_STATES) { + dumpn(`Testing popping back to the old view from state = ${state}`); + + dispatch(fetchIndividuals(heapWorker, snapshotId, breakdown, + reportLeafIndex)); + + // Wait for the expected test state. + yield waitUntilState(store, s => { + return s.view.state === viewState.INDIVIDUALS && + s.individuals && + s.individuals.state === state; + }); + ok(true, `Reached state = ${state}`); + + // Pop back to the CENSUS state. + dispatch(popViewAndRefresh(heapWorker)); + yield waitUntilState(store, s => { + return s.view.state === viewState.CENSUS; + }); + ok(!getState().individuals, "Should no longer have individuals"); + } + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_tree-map-01.js b/devtools/client/memory/test/unit/test_tree-map-01.js new file mode 100644 index 000000000..15d9a765a --- /dev/null +++ b/devtools/client/memory/test/unit/test_tree-map-01.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { drawBox } = require("devtools/client/memory/components/tree-map/draw"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let fillRectValues, strokeRectValues; + let ctx = { + fillRect: (...args) => fillRectValues = args, + strokeRect: (...args) => strokeRectValues = args + }; + let node = { + x: 20, + y: 30, + dx: 50, + dy: 70, + type: "other", + depth: 2 + }; + let padding = [10, 10]; + let borderWidth = () => 1; + let dragZoom = { + offsetX: 0, + offsetY: 0, + zoom: 0 + }; + drawBox(ctx, node, borderWidth, dragZoom, padding); + ok(true, JSON.stringify([ctx, fillRectValues, strokeRectValues])); + equal(ctx.fillStyle, "hsl(210,60%,70%)", "The fillStyle is set"); + equal(ctx.strokeStyle, "hsl(210,60%,35%)", "The strokeStyle is set"); + equal(ctx.lineWidth, 1, "The lineWidth is set"); + deepEqual(fillRectValues, [10.5, 20.5, 49, 69], "Draws a filled rectangle"); + deepEqual(strokeRectValues, [10.5, 20.5, 49, 69], "Draws a stroked rectangle"); + + + dragZoom.zoom = 0.5; + + drawBox(ctx, node, borderWidth, dragZoom, padding); + ok(true, JSON.stringify([ctx, fillRectValues, strokeRectValues])); + deepEqual(fillRectValues, [15.5, 30.5, 74, 104], + "Draws a zoomed filled rectangle"); + deepEqual(strokeRectValues, [15.5, 30.5, 74, 104], + "Draws a zoomed stroked rectangle"); + + dragZoom.offsetX = 110; + dragZoom.offsetY = 130; + + drawBox(ctx, node, borderWidth, dragZoom, padding); + deepEqual(fillRectValues, [-94.5, -99.5, 74, 104], + "Draws a zoomed and offset filled rectangle"); + deepEqual(strokeRectValues, [-94.5, -99.5, 74, 104], + "Draws a zoomed and offset stroked rectangle"); +}); diff --git a/devtools/client/memory/test/unit/test_tree-map-02.js b/devtools/client/memory/test/unit/test_tree-map-02.js new file mode 100644 index 000000000..4f7200f0a --- /dev/null +++ b/devtools/client/memory/test/unit/test_tree-map-02.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { drawText } = require("devtools/client/memory/components/tree-map/draw"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + // Mock out the Canvas2dContext + let ctx = { + fillText: (...args) => fillTextValues.push(args), + measureText: (text) => { + let width = text ? text.length * 10 : 0; + return { width }; + } + }; + let node = { + x: 20, + y: 30, + dx: 500, + dy: 70, + name: "Example Node", + totalBytes: 1200, + totalCount: 100 + }; + let ratio = 0; + let borderWidth = () => 1; + let dragZoom = { + offsetX: 0, + offsetY: 0, + zoom: 0 + }; + let fillTextValues = []; + let padding = [10, 10]; + + drawText(ctx, node, borderWidth, ratio, dragZoom, padding); + deepEqual(fillTextValues[0], ["Example Node", 11.5, 21.5], + "Fills in the full node name"); + deepEqual(fillTextValues[1], ["1KiB 100 count", 141.5, 21.5], + "Includes the full byte and count information"); + + fillTextValues = []; + node.dx = 250; + drawText(ctx, node, borderWidth, ratio, dragZoom, padding); + + deepEqual(fillTextValues[0], ["Example Node", 11.5, 21.5], + "Fills in the full node name"); + deepEqual(fillTextValues[1], undefined, + "Drops off the byte and count information if not enough room"); + + fillTextValues = []; + node.dx = 100; + drawText(ctx, node, borderWidth, ratio, dragZoom, padding); + + deepEqual(fillTextValues[0], ["Exampl...", 11.5, 21.5], + "Cuts the name with ellipsis"); + deepEqual(fillTextValues[1], undefined, + "Drops off the byte and count information if not enough room"); + + fillTextValues = []; + node.dx = 40; + drawText(ctx, node, borderWidth, ratio, dragZoom, padding); + + deepEqual(fillTextValues[0], ["...", 11.5, 21.5], + "Shows only ellipsis when smaller"); + deepEqual(fillTextValues[1], undefined, + "Drops off the byte and count information if not enough room"); + + fillTextValues = []; + node.dx = 20; + drawText(ctx, node, borderWidth, ratio, dragZoom, padding); + + deepEqual(fillTextValues[0], undefined, + "Draw nothing when not enough room"); + deepEqual(fillTextValues[1], undefined, + "Drops off the byte and count information if not enough room"); +}); diff --git a/devtools/client/memory/test/unit/test_utils-get-snapshot-totals.js b/devtools/client/memory/test/unit/test_utils-get-snapshot-totals.js new file mode 100644 index 000000000..c4560fb07 --- /dev/null +++ b/devtools/client/memory/test/unit/test_utils-get-snapshot-totals.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * Tests that we use the correct snapshot aggregate value + * in `utils.getSnapshotTotals(snapshot)` + */ + +const { censusDisplays, snapshotState: states, viewState, censusState } = require("devtools/client/memory/constants"); +const { getSnapshotTotals } = require("devtools/client/memory/utils"); +const { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot"); +const { setCensusDisplayAndRefresh } = require("devtools/client/memory/actions/census-display"); +const { changeView } = require("devtools/client/memory/actions/view"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + let { getState, dispatch } = store; + + dispatch(changeView(viewState.CENSUS)); + + yield dispatch(setCensusDisplayAndRefresh(heapWorker, + censusDisplays.allocationStack)); + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]); + + ok(!getState().snapshots[0].census.display.inverted, "Snapshot is not inverted"); + + let census = getState().snapshots[0].census; + let result = aggregate(census.report); + let totalBytes = result.bytes; + let totalCount = result.count; + + ok(totalBytes > 0, "counted up bytes in the census"); + ok(totalCount > 0, "counted up count in the census"); + + result = getSnapshotTotals(getState().snapshots[0].census); + equal(totalBytes, result.bytes, "getSnapshotTotals reuslted in correct bytes"); + equal(totalCount, result.count, "getSnapshotTotals reuslted in correct count"); + + dispatch(setCensusDisplayAndRefresh(heapWorker, + censusDisplays.invertedAllocationStack)); + + yield waitUntilCensusState(store, s => s.census, [censusState.SAVING]); + yield waitUntilCensusState(store, s => s.census, [censusState.SAVED]); + ok(getState().snapshots[0].census.display.inverted, "Snapshot is inverted"); + + result = getSnapshotTotals(getState().snapshots[0].census); + equal(totalBytes, result.bytes, + "getSnapshotTotals reuslted in correct bytes when inverted"); + equal(totalCount, result.count, + "getSnapshotTotals reuslted in correct count when inverted"); +}); + +function aggregate(report) { + let totalBytes = report.bytes; + let totalCount = report.count; + for (let child of (report.children || [])) { + let { bytes, count } = aggregate(child); + totalBytes += bytes; + totalCount += count; + } + return { bytes: totalBytes, count: totalCount }; +} diff --git a/devtools/client/memory/test/unit/test_utils.js b/devtools/client/memory/test/unit/test_utils.js new file mode 100644 index 000000000..f6deddfc9 --- /dev/null +++ b/devtools/client/memory/test/unit/test_utils.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/** + * Tests the task creator `takeSnapshotAndCensus()` for the whole flow of + * taking a snapshot, and its sub-actions. Tests the formatNumber and + * formatPercent methods. + */ + +let utils = require("devtools/client/memory/utils"); +let { snapshotState: states, viewState } = require("devtools/client/memory/constants"); +let { Preferences } = require("resource://gre/modules/Preferences.jsm"); + +function run_test() { + run_next_test(); +} + +add_task(function* () { + let s1 = utils.createSnapshot({ view: { state: viewState.CENSUS } }); + let s2 = utils.createSnapshot({ view: { state: viewState.CENSUS } }); + equal(s1.state, states.SAVING, "utils.createSnapshot() creates snapshot in saving state"); + ok(s1.id !== s2.id, "utils.createSnapshot() creates snapshot with unique ids"); + + let custom = { by: "internalType", then: { by: "count", bytes: true }}; + Preferences.set("devtools.memory.custom-census-displays", JSON.stringify({ "My Display": custom })); + + equal(utils.getCustomCensusDisplays()["My Display"].by, custom.by, + "utils.getCustomCensusDisplays() returns custom displays"); + + ok(true, "test formatNumber util functions"); + equal(utils.formatNumber(12), "12", "formatNumber returns 12 for 12"); + + equal(utils.formatNumber(0), "0", "formatNumber returns 0 for 0"); + equal(utils.formatNumber(-0), "0", "formatNumber returns 0 for -0"); + equal(utils.formatNumber(+0), "0", "formatNumber returns 0 for +0"); + + equal(utils.formatNumber(1234567), "1 234 567", + "formatNumber adds a space every 3rd digit"); + equal(utils.formatNumber(12345678), "12 345 678", + "formatNumber adds a space every 3rd digit"); + equal(utils.formatNumber(123456789), "123 456 789", + "formatNumber adds a space every 3rd digit"); + + equal(utils.formatNumber(12, true), "+12", + "formatNumber can display number sign"); + equal(utils.formatNumber(-12, true), "-12", + "formatNumber can display number sign (negative)"); + + ok(true, "test formatPercent util functions"); + equal(utils.formatPercent(12), "12%", "formatPercent returns 12% for 12"); + equal(utils.formatPercent(12345), "12 345%", + "formatPercent returns 12 345% for 12345"); + + equal(utils.formatAbbreviatedBytes(12), "12B", "Formats bytes"); + equal(utils.formatAbbreviatedBytes(12345), "12KiB", "Formats kilobytes"); + equal(utils.formatAbbreviatedBytes(12345678), "11MiB", "Formats megabytes"); + equal(utils.formatAbbreviatedBytes(12345678912), "11GiB", "Formats gigabytes"); + + equal(utils.hslToStyle(0.5, 0.6, 0.7), + "hsl(180,60%,70%)", "hslToStyle converts an array to a style string"); + equal(utils.hslToStyle(0, 0, 0), + "hsl(0,0%,0%)", "hslToStyle converts an array to a style string"); + equal(utils.hslToStyle(1, 1, 1), + "hsl(360,100%,100%)", "hslToStyle converts an array to a style string"); + + equal(utils.lerp(5, 7, 0), 5, "lerp return first number for 0"); + equal(utils.lerp(5, 7, 1), 7, "lerp return second number for 1"); + equal(utils.lerp(5, 7, 0.5), 6, "lerp interpolates the numbers for 0.5"); +}); diff --git a/devtools/client/memory/test/unit/xpcshell.ini b/devtools/client/memory/test/unit/xpcshell.ini new file mode 100644 index 000000000..dade269c3 --- /dev/null +++ b/devtools/client/memory/test/unit/xpcshell.ini @@ -0,0 +1,56 @@ +[DEFAULT] +tags = devtools devtools-memory +head = head.js ../../../framework/test/shared-redux-head.js +tail = +firefox-appdir = browser +skip-if = toolkit == 'android' + +[test_action_diffing_01.js] +[test_action_diffing_02.js] +[test_action_diffing_03.js] +[test_action_diffing_04.js] +[test_action_diffing_05.js] +[test_action-clear-snapshots_01.js] +[test_action-clear-snapshots_02.js] +[test_action-clear-snapshots_03.js] +[test_action-clear-snapshots_04.js] +[test_action-clear-snapshots_05.js] +[test_action-clear-snapshots_06.js] +[test_action-export-snapshot.js] +[test_action-filter-01.js] +[test_action-filter-02.js] +[test_action-filter-03.js] +[test_action-import-snapshot-and-census.js] +[test_action-import-snapshot-dominator-tree.js] +[test_action-select-snapshot.js] +[test_action-set-display.js] +[test_action-set-display-and-refresh-01.js] +[test_action-set-display-and-refresh-02.js] +[test_action-take-census.js] +[test_action-take-snapshot.js] +[test_action-take-snapshot-and-census.js] +[test_action-toggle-inverted.js] +[test_action-toggle-inverted-and-refresh-01.js] +[test_action-toggle-inverted-and-refresh-02.js] +[test_action-toggle-recording-allocations.js] +[test_dominator_trees_01.js] +[test_dominator_trees_02.js] +[test_dominator_trees_03.js] +[test_dominator_trees_04.js] +[test_dominator_trees_05.js] +[test_dominator_trees_06.js] +[test_dominator_trees_07.js] +[test_dominator_trees_08.js] +[test_dominator_trees_09.js] +[test_dominator_trees_10.js] +[test_individuals_01.js] +[test_individuals_02.js] +[test_individuals_03.js] +[test_individuals_04.js] +[test_individuals_05.js] +[test_individuals_06.js] +[test_pop_view_01.js] +[test_tree-map-01.js] +[test_tree-map-02.js] +[test_utils.js] +[test_utils-get-snapshot-totals.js] |