diff options
Diffstat (limited to 'devtools/client/memory/test/xpcshell')
52 files changed, 4381 insertions, 0 deletions
diff --git a/devtools/client/memory/test/xpcshell/.eslintrc.js b/devtools/client/memory/test/xpcshell/.eslintrc.js new file mode 100644 index 0000000000..f1618e83c2 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/.eslintrc.js @@ -0,0 +1,14 @@ +"use strict"; + +module.exports = { + // Extend from the shared list of defined globals for mochitests. + extends: "../../../../.eslintrc.xpcshell.js", + rules: { + "no-unused-vars": [ + "error", + { + vars: "local", + }, + ], + }, +}; diff --git a/devtools/client/memory/test/xpcshell/head.js b/devtools/client/memory/test/xpcshell/head.js new file mode 100644 index 0000000000..84369bd49d --- /dev/null +++ b/devtools/client/memory/test/xpcshell/head.js @@ -0,0 +1,187 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// via xpcshell.ini +/* import-globals-from ../../../shared/test/shared-head.js */ + +Services.prefs.setBoolPref("devtools.testing", true); +Services.prefs.setBoolPref("devtools.debugger.log", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.testing"); + Services.prefs.clearUserPref("devtools.debugger.log"); +}); + +var { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" +); +var { expectState } = require("resource://devtools/server/actors/common.js"); +var HeapSnapshotFileUtils = require("resource://devtools/shared/heapsnapshot/HeapSnapshotFileUtils.js"); +var HeapAnalysesClient = require("resource://devtools/shared/heapsnapshot/HeapAnalysesClient.js"); +var { addDebuggerToGlobal } = ChromeUtils.importESModule( + "resource://gre/modules/jsdebugger.sys.mjs" +); +var Store = require("resource://devtools/client/memory/store.js"); +var { L10N } = require("resource://devtools/client/memory/utils.js"); +var SYSTEM_PRINCIPAL = Cc["@mozilla.org/systemprincipal;1"].createInstance( + Ci.nsIPrincipal +); + +var EXPECTED_DTU_ASSERT_FAILURE_COUNT = 0; + +registerCleanupFunction(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() { + const global = new Cu.Sandbox(SYSTEM_PRINCIPAL, { freshZone: true }); + addDebuggerToGlobal(global); + return new global.Debugger(); +} + +function StubbedMemoryFront() { + this.state = "detached"; + this.dbg = initDebugger(); +} + +StubbedMemoryFront.prototype.attach = async function () { + this.state = "attached"; +}; + +StubbedMemoryFront.prototype.detach = async function () { + this.state = "detached"; +}; + +StubbedMemoryFront.prototype.saveHeapSnapshot = expectState( + "attached", + async function () { + return ChromeUtils.saveHeapSnapshot({ runtime: true }); + }, + "saveHeapSnapshot" +); + +StubbedMemoryFront.prototype.startRecordingAllocations = expectState( + "attached", + async function () {} +); + +StubbedMemoryFront.prototype.stopRecordingAllocations = expectState( + "attached", + async function () {} +); + +function waitUntilSnapshotState(store, expected) { + const predicate = () => { + const snapshots = store.getState().snapshots; + info(snapshots.map(x => x.state)); + return ( + snapshots.length === expected.length && + expected.every( + (state, i) => state === "*" || snapshots[i].state === state + ) + ); + }; + info(`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 (const child of node.children) { + const found = findReportLeafIndex(child); + if (found) { + return found; + } + } + } + + return null; +} + +function waitUntilCensusState(store, getCensus, expected) { + const predicate = () => { + const snapshots = store.getState().snapshots; + + info( + "Current census state:" + + snapshots.map(x => (getCensus(x) ? getCensus(x).state : null)) + ); + + return ( + snapshots.length === expected.length && + expected.every((state, i) => { + const census = getCensus(snapshots[i]); + return ( + state === "*" || + (!census && !state) || + (census && census.state === state) + ); + }) + ); + }; + info(`Waiting for snapshots' censuses to be of state: ${expected}`); + return waitUntilState(store, predicate); +} + +async function createTempFile() { + const file = FileUtils.getFile("TmpD", ["tmp.fxsnapshot"]); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); + const destPath = file.path; + const stat = await IOUtils.stat(destPath); + ok(stat.size === 0, "new file is 0 bytes at start"); + return destPath; +} + +// This is a copy of the same method from shared-head.js as +// xpcshell test aren't using shared-head.js +/** + * Wait for a specific action type to be dispatched. + * + * If the action is async and defines a `status` property, this helper will wait + * for the status to reach either "error" or "done". + * + * @param {Object} store + * Redux store where the action should be dispatched. + * @param {String} actionType + * The actionType to wait for. + * @param {Number} repeat + * Optional, number of time the action is expected to be dispatched. + * Defaults to 1 + * @return {Promise} + */ +function waitForDispatch(store, actionType, repeat = 1) { + let count = 0; + return new Promise(resolve => { + store.dispatch({ + type: "@@service/waitUntil", + predicate: action => { + const isDone = + !action.status || + action.status === "done" || + action.status === "error"; + + if (action.type === actionType && isDone && ++count == repeat) { + return true; + } + + return false; + }, + run: (dispatch, getState, action) => { + resolve(action); + }, + }); + }); +} diff --git a/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_01.js b/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_01.js new file mode 100644 index 0000000000..9511ba413f --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_01.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test clearSnapshots deletes snapshots with READ censuses + +const { + takeSnapshotAndCensus, + clearSnapshots, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { actions } = require("resource://devtools/client/memory/constants.js"); +const { + treeMapState, +} = require("resource://devtools/client/memory/constants.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { getState, dispatch } = store; + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + await waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]); + ok(true, "snapshot created"); + + ok(true, "dispatch clearSnapshots action"); + const deleteEvents = Promise.all([ + waitForDispatch(store, actions.DELETE_SNAPSHOTS_START), + waitForDispatch(store, actions.DELETE_SNAPSHOTS_END), + ]); + dispatch(clearSnapshots(heapWorker)); + await deleteEvents; + ok(true, "received delete snapshots events"); + + equal(getState().snapshots.length, 0, "no snapshot remaining"); + + heapWorker.destroy(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_02.js b/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_02.js new file mode 100644 index 0000000000..09f1fa6b54 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_02.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test clearSnapshots preserves snapshots with state != READ or ERROR + +const { + takeSnapshotAndCensus, + clearSnapshots, + takeSnapshot, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + snapshotState: states, + treeMapState, + actions, +} = require("resource://devtools/client/memory/constants.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const 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)); + await waitUntilSnapshotState(store, [states.SAVED, states.SAVED]); + await waitUntilCensusState(store, snapshot => snapshot.treeMap, [ + treeMapState.SAVED, + null, + ]); + ok(true, "snapshots created with expected states"); + + ok(true, "dispatch clearSnapshots action"); + const deleteEvents = Promise.all([ + waitForDispatch(store, actions.DELETE_SNAPSHOTS_START), + waitForDispatch(store, actions.DELETE_SNAPSHOTS_END), + ]); + dispatch(clearSnapshots(heapWorker)); + await deleteEvents; + ok(true, "received delete snapshots events"); + + equal(getState().snapshots.length, 1, "one snapshot remaining"); + const 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(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_03.js b/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_03.js new file mode 100644 index 0000000000..b920c5264b --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_03.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test clearSnapshots deletes snapshots with state ERROR + +const { + takeSnapshotAndCensus, + clearSnapshots, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + snapshotState: states, + treeMapState, + actions, +} = require("resource://devtools/client/memory/constants.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { getState, dispatch } = store; + + ok(true, "create a snapshot with a treeMap"); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + await waitUntilSnapshotState(store, [states.SAVED]); + ok(true, "snapshot created with a SAVED state"); + await waitUntilCensusState(store, snapshot => snapshot.treeMap, [ + treeMapState.SAVED, + ]); + ok(true, "treeMap created with a SAVED state"); + + ok(true, "set snapshot state to error"); + const id = getState().snapshots[0].id; + dispatch({ type: actions.SNAPSHOT_ERROR, id, error: new Error("_") }); + await waitUntilSnapshotState(store, [states.ERROR]); + ok(true, "snapshot set to error state"); + + ok(true, "dispatch clearSnapshots action"); + const deleteEvents = Promise.all([ + waitForDispatch(store, actions.DELETE_SNAPSHOTS_START), + waitForDispatch(store, actions.DELETE_SNAPSHOTS_END), + ]); + dispatch(clearSnapshots(heapWorker)); + await deleteEvents; + ok(true, "received delete snapshots events"); + equal(getState().snapshots.length, 0, "error snapshot deleted"); + + heapWorker.destroy(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_04.js b/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_04.js new file mode 100644 index 0000000000..723029c90b --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_04.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test clearSnapshots deletes several snapshots + +const { + takeSnapshotAndCensus, + clearSnapshots, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + snapshotState: states, + actions, + treeMapState, +} = require("resource://devtools/client/memory/constants.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const 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)); + await 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"); + const id = getState().snapshots[0].id; + dispatch({ type: actions.SNAPSHOT_ERROR, id, error: new Error("_") }); + await waitUntilSnapshotState(store, [states.ERROR, states.READ, states.READ]); + ok(true, "first snapshot set to error state"); + + ok(true, "dispatch clearSnapshots action"); + const deleteEvents = Promise.all([ + waitForDispatch(store, actions.DELETE_SNAPSHOTS_START), + waitForDispatch(store, actions.DELETE_SNAPSHOTS_END), + ]); + dispatch(clearSnapshots(heapWorker)); + await deleteEvents; + ok(true, "received delete snapshots events"); + + equal(getState().snapshots.length, 0, "no snapshot remaining"); + + heapWorker.destroy(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_05.js b/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_05.js new file mode 100644 index 0000000000..485e0f0513 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_05.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test clearSnapshots deletes several snapshots + +const { + takeSnapshotAndCensus, + clearSnapshots, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + actions, + treeMapState, +} = require("resource://devtools/client/memory/constants.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const 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"); + await waitUntilCensusState(store, snapshot => snapshot.treeMap, [ + treeMapState.SAVED, + treeMapState.SAVED, + ]); + + const errorHeapWorker = { + deleteHeapSnapshot() { + return Promise.reject("_"); + }, + }; + + ok(true, "dispatch clearSnapshots action"); + const deleteEvents = Promise.all([ + waitForDispatch(store, actions.DELETE_SNAPSHOTS_START), + waitForDispatch(store, actions.DELETE_SNAPSHOTS_END), + waitForDispatch(store, actions.SNAPSHOT_ERROR), + waitForDispatch(store, actions.SNAPSHOT_ERROR), + ]); + dispatch(clearSnapshots(errorHeapWorker)); + await deleteEvents; + ok(true, "received delete snapshots and snapshot error events"); + equal(getState().snapshots.length, 0, "no snapshot remaining"); + + heapWorker.destroy(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_06.js b/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_06.js new file mode 100644 index 0000000000..d7f1426827 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_action-clear-snapshots_06.js @@ -0,0 +1,64 @@ +/* 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("resource://devtools/client/memory/actions/snapshot.js"); +const { + actions, + treeMapState, +} = require("resource://devtools/client/memory/constants.js"); +const { + toggleDiffing, + selectSnapshotForDiffingAndRefresh, +} = require("resource://devtools/client/memory/actions/diffing.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { getState, dispatch } = store; + + ok(true, "create 2 snapshots with a saved census"); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + await 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"); + + await waitForDispatch(store, actions.TAKE_CENSUS_DIFF_END); + ok(true, "Received TAKE_CENSUS_DIFF_END action"); + + ok(true, "Dispatch clearSnapshots action"); + const deleteEvents = Promise.all([ + waitForDispatch(store, actions.DELETE_SNAPSHOTS_START), + waitForDispatch(store, actions.DELETE_SNAPSHOTS_END), + ]); + dispatch(clearSnapshots(heapWorker)); + await 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(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_action-export-snapshot.js b/devtools/client/memory/test/xpcshell/test_action-export-snapshot.js new file mode 100644 index 0000000000..8dc2bc6bf1 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_action-export-snapshot.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test exporting a snapshot to a user specified location on disk. + +const { + exportSnapshot, +} = require("resource://devtools/client/memory/actions/io.js"); +const { + takeSnapshotAndCensus, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + actions, + treeMapState, +} = require("resource://devtools/client/memory/constants.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { getState, dispatch } = store; + + const destPath = await createTempFile(); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + await waitUntilCensusState(store, snapshot => snapshot.treeMap, [ + treeMapState.SAVED, + ]); + + const exportEvents = Promise.all([ + waitForDispatch(store, actions.EXPORT_SNAPSHOT_START), + waitForDispatch(store, actions.EXPORT_SNAPSHOT_END), + ]); + dispatch(exportSnapshot(getState().snapshots[0], destPath)); + await exportEvents; + + const stat = await IOUtils.stat(destPath); + info(stat.size); + ok(stat.size > 0, "destination file is more than 0 bytes"); + + heapWorker.destroy(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_action-filter-01.js b/devtools/client/memory/test/xpcshell/test_action-filter-01.js new file mode 100644 index 0000000000..b16e66a62c --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_action-filter-01.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test setting the filter string. + +const { + setFilterString, +} = require("resource://devtools/client/memory/actions/filter.js"); + +add_task(async function () { + const 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/xpcshell/test_action-filter-02.js b/devtools/client/memory/test/xpcshell/test_action-filter-02.js new file mode 100644 index 0000000000..828b976aa3 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_action-filter-02.js @@ -0,0 +1,88 @@ +/* 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. + +const { + viewState, + censusState, +} = require("resource://devtools/client/memory/constants.js"); +const { + setFilterStringAndRefresh, +} = require("resource://devtools/client/memory/actions/filter.js"); +const { + takeSnapshotAndCensus, + selectSnapshotAndRefresh, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { 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)); + + await 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)); + await 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"); + + await 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)); + await waitUntilCensusState(store, snapshot => snapshot.census, [ + censusState.SAVED, + censusState.SAVING, + censusState.SAVED, + ]); + ok(true, "selecting non-inverted census should trigger a recompute"); + + await 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(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_action-filter-03.js b/devtools/client/memory/test/xpcshell/test_action-filter-03.js new file mode 100644 index 0000000000..2fcc3e9156 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_action-filter-03.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that changing filter state in the middle of taking a snapshot results in +// the properly fitered census. + +const { + snapshotState: states, + censusState, + viewState, +} = require("resource://devtools/client/memory/constants.js"); +const { + setFilterString, + setFilterStringAndRefresh, +} = require("resource://devtools/client/memory/actions/filter.js"); +const { + takeSnapshotAndCensus, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { getState, dispatch } = store; + + dispatch(changeView(viewState.CENSUS)); + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + await waitUntilSnapshotState(store, [states.SAVING]); + + dispatch(setFilterString("str")); + + await 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)); + await waitUntilCensusState(store, snapshot => snapshot.census, [ + censusState.SAVING, + ]); + ok(true, "changing filter string retriggers census"); + ok(!getState().filter, "no longer filtering"); + + dispatch(setFilterString("obj")); + await 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(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_action-import-snapshot-and-census.js b/devtools/client/memory/test/xpcshell/test_action-import-snapshot-and-census.js new file mode 100644 index 0000000000..3e97f7c45f --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_action-import-snapshot-and-census.js @@ -0,0 +1,135 @@ +/* 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. + */ + +const { + actions, + snapshotState: states, + treeMapState, +} = require("resource://devtools/client/memory/constants.js"); +const { + exportSnapshot, + importSnapshotAndCensus, +} = require("resource://devtools/client/memory/actions/io.js"); +const { + takeSnapshotAndCensus, +} = require("resource://devtools/client/memory/actions/snapshot.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { subscribe, dispatch, getState } = store; + + const destPath = await createTempFile(); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + await waitUntilCensusState(store, s => s.treeMap, [treeMapState.SAVED]); + + const exportEvents = Promise.all([ + waitForDispatch(store, actions.EXPORT_SNAPSHOT_START), + waitForDispatch(store, actions.EXPORT_SNAPSHOT_END), + ]); + dispatch(exportSnapshot(getState().snapshots[0], destPath)); + await exportEvents; + + // Now import our freshly exported snapshot + let snapshotI = 0; + let censusI = 0; + const snapshotStates = ["IMPORTING", "READING", "READ"]; + const censusStates = ["SAVING", "SAVED"]; + const expectStates = () => { + const snapshot = getState().snapshots[1]; + if (!snapshot) { + return; + } + if (snapshotI < snapshotStates.length) { + const 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++; + } + } + }; + + const unsubscribe = subscribe(expectStates); + dispatch(importSnapshotAndCensus(heapWorker, destPath)); + + await 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 + const snapshot1 = getState().snapshots[0]; + const 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 + const census1 = stripUnique( + JSON.parse(JSON.stringify(snapshot1.treeMap.report)) + ); + const census2 = stripUnique( + JSON.parse(JSON.stringify(snapshot2.treeMap.report)) + ); + + equal( + JSON.stringify(census1), + JSON.stringify(census2), + "Imported snapshot has correct census" + ); + + function stripUnique(obj) { + const children = obj.children || []; + for (const 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/xpcshell/test_action-import-snapshot-dominator-tree.js b/devtools/client/memory/test/xpcshell/test_action-import-snapshot-dominator-tree.js new file mode 100644 index 0000000000..3c11268e94 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_action-import-snapshot-dominator-tree.js @@ -0,0 +1,100 @@ +/* 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. + */ + +const { + snapshotState, + dominatorTreeState, + viewState, + treeMapState, +} = require("resource://devtools/client/memory/constants.js"); +const { + importSnapshotAndCensus, +} = require("resource://devtools/client/memory/actions/io.js"); +const { + changeViewAndRefresh, +} = require("resource://devtools/client/memory/actions/view.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { 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; + const expected = [ + "IMPORTING", + "READING", + "READ", + "treeMap:SAVING", + "treeMap:SAVED", + "dominatorTree:COMPUTING", + "dominatorTree:FETCHING", + "dominatorTree:LOADED", + ]; + const expectStates = () => { + const snapshot = getState().snapshots[0]; + if (snapshot && hasExpectedState(snapshot, expected[i])) { + ok(true, `Found expected state ${expected[i]}`); + i++; + } + }; + + const unsubscribe = subscribe(expectStates); + const snapshotPath = await front.saveHeapSnapshot(); + dispatch(importSnapshotAndCensus(heapWorker, snapshotPath)); + + await 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) { + const isDominatorState = expectedState.indexOf("dominatorTree:") === 0; + if (isDominatorState) { + const state = + dominatorTreeState[expectedState.replace("dominatorTree:", "")]; + return snapshot.dominatorTree && snapshot.dominatorTree.state === state; + } + + const isTreeMapState = expectedState.indexOf("treeMap:") === 0; + if (isTreeMapState) { + const state = treeMapState[expectedState.replace("treeMap:", "")]; + return snapshot.treeMap && snapshot.treeMap.state === state; + } + + const state = snapshotState[expectedState]; + return snapshot.state === state; +} diff --git a/devtools/client/memory/test/xpcshell/test_action-select-snapshot.js b/devtools/client/memory/test/xpcshell/test_action-select-snapshot.js new file mode 100644 index 0000000000..f4abac423b --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_action-select-snapshot.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests the reducer responding to the action `selectSnapshot(snapshot)` + */ + +const actions = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + snapshotState: states, +} = require("resource://devtools/client/memory/constants.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + await front.attach(); + const store = Store(); + + for (let i = 0; i < 5; i++) { + store.dispatch(actions.takeSnapshot(front)); + } + + await waitUntilState( + store, + ({ snapshots }) => snapshots.length === 5 && snapshots.every(isDone) + ); + + for (let i = 0; i < 5; i++) { + info(`Selecting snapshot[${i}]`); + store.dispatch(actions.selectSnapshot(store.getState().snapshots[i].id)); + await waitUntilState(store, ({ snapshots }) => snapshots[i].selected); + + const { 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/xpcshell/test_action-set-display-and-refresh-01.js b/devtools/client/memory/test/xpcshell/test_action-set-display-and-refresh-01.js new file mode 100644 index 0000000000..39845c1f7b --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_action-set-display-and-refresh-01.js @@ -0,0 +1,182 @@ +/* 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`. + */ + +const { + censusDisplays, + censusState, + viewState, +} = require("resource://devtools/client/memory/constants.js"); +const { + setCensusDisplayAndRefresh, +} = require("resource://devtools/client/memory/actions/census-display.js"); +const { + takeSnapshotAndCensus, + selectSnapshotAndRefresh, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +// We test setting an invalid display, which triggers an assertion failure. +EXPECTED_DTU_ASSERT_FAILURE_COUNT = 1; + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { 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, {})); + await 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)); + await 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)); + await waitUntilCensusState(store, snapshot => snapshot.census, [ + censusState.SAVED, + censusState.SAVING, + ]); + dispatch(setCensusDisplayAndRefresh(heapWorker, censusDisplays.coarseType)); + await 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)); + await waitUntilCensusState(store, snapshot => snapshot.census, [ + censusState.SAVED, + censusState.SAVED, + censusState.SAVING, + ]); + dispatch( + setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack) + ); + await 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)); + await waitUntilState( + store, + state => state.snapshots[2].census.state === censusState.SAVING + ); + await 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) + ); + await 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)); + await waitUntilCensusState(store, snapshot => snapshot.census, [ + censusState.SAVED, + censusState.SAVING, + censusState.SAVED, + ]); + await 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(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_action-set-display-and-refresh-02.js b/devtools/client/memory/test/xpcshell/test_action-set-display-and-refresh-02.js new file mode 100644 index 0000000000..2b2b91bc97 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_action-set-display-and-refresh-02.js @@ -0,0 +1,74 @@ +/* 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. + */ + +const { + censusState, + viewState, +} = require("resource://devtools/client/memory/constants.js"); +const { + setCensusDisplayAndRefresh, +} = require("resource://devtools/client/memory/actions/census-display.js"); +const { + takeSnapshotAndCensus, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +const CUSTOM = { + displayName: "Custom", + tooltip: "Custom tooltip", + inverted: false, + breakdown: { + by: "internalType", + then: { by: "count", bytes: true, count: false }, + }, +}; + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { 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)); + await 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/xpcshell/test_action-set-display.js b/devtools/client/memory/test/xpcshell/test_action-set-display.js new file mode 100644 index 0000000000..dfc14f609a --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_action-set-display.js @@ -0,0 +1,74 @@ +/* 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. + */ + +const { + censusDisplays, + censusState, + viewState, +} = require("resource://devtools/client/memory/constants.js"); +const { + setCensusDisplay, +} = require("resource://devtools/client/memory/actions/census-display.js"); +const { + takeSnapshotAndCensus, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +// We test setting an invalid display, which triggers an assertion failure. +EXPECTED_DTU_ASSERT_FAILURE_COUNT = 1; + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { 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)); + await 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/xpcshell/test_action-take-census.js b/devtools/client/memory/test/xpcshell/test_action-take-census.js new file mode 100644 index 0000000000..005208024b --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_action-take-census.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests the async reducer responding to the action `takeCensus(heapWorker, snapshot)` + */ + +var { + snapshotState: states, + censusDisplays, + censusState, + viewState, +} = require("resource://devtools/client/memory/constants.js"); +var actions = require("resource://devtools/client/memory/actions/snapshot.js"); +var { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +// 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(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + + store.dispatch(changeView(viewState.CENSUS)); + + store.dispatch(actions.takeSnapshot(front)); + await waitUntilState(store, () => { + const 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)); + await 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)); + await waitUntilState( + store, + () => store.getState().snapshots[0].state === states.READ + ); + + store.dispatch(actions.takeCensus(heapWorker, snapshot.id)); + await waitUntilCensusState(store, s => s.census, [censusState.SAVING]); + await 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/xpcshell/test_action-take-snapshot-and-census.js b/devtools/client/memory/test/xpcshell/test_action-take-snapshot-and-census.js new file mode 100644 index 0000000000..ce7d166aca --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_action-take-snapshot-and-census.js @@ -0,0 +1,65 @@ +/* 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. + */ + +const { + snapshotState: states, + treeMapState, +} = require("resource://devtools/client/memory/constants.js"); +const actions = require("resource://devtools/client/memory/actions/snapshot.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + + let snapshotI = 0; + let censusI = 0; + const snapshotStates = ["SAVING", "SAVED", "READING", "READ"]; + const censusStates = ["SAVING", "SAVED"]; + const expectStates = () => { + const snapshot = store.getState().snapshots[0]; + if (!snapshot) { + return; + } + if (snapshotI < snapshotStates.length) { + const 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++; + } + } + }; + + const unsubscribe = store.subscribe(expectStates); + store.dispatch(actions.takeSnapshotAndCensus(front, heapWorker)); + + await waitUntilState(store, () => { + return ( + snapshotI === snapshotStates.length && censusI === censusStates.length + ); + }); + unsubscribe(); + + ok( + true, + "takeSnapshotAndCensus() produces the correct sequence of states in a snapshot" + ); + const 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/xpcshell/test_action-take-snapshot.js b/devtools/client/memory/test/xpcshell/test_action-take-snapshot.js new file mode 100644 index 0000000000..e8bd46898c --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_action-take-snapshot.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests the async reducer responding to the action `takeSnapshot(front)` + */ + +const actions = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + snapshotState: states, +} = require("resource://devtools/client/memory/constants.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + await front.attach(); + const store = Store(); + + const unsubscribe = store.subscribe(checkState); + + let foundPendingState = false; + let foundDoneState = false; + + function checkState() { + const { snapshots } = store.getState(); + const 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)); + await waitUntilState(store, () => foundPendingState && foundDoneState); + + // reset state trackers + foundDoneState = foundPendingState = false; + } + + unsubscribe(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_action-toggle-inverted-and-refresh-01.js b/devtools/client/memory/test/xpcshell/test_action-toggle-inverted-and-refresh-01.js new file mode 100644 index 0000000000..6744390ecd --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_action-toggle-inverted-and-refresh-01.js @@ -0,0 +1,98 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that changing displays with different inverted state properly +// refreshes the selected census. + +const { + censusDisplays, + censusState, + viewState, +} = require("resource://devtools/client/memory/constants.js"); +const { + setCensusDisplayAndRefresh, +} = require("resource://devtools/client/memory/actions/census-display.js"); +const { + takeSnapshotAndCensus, + selectSnapshotAndRefresh, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { 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)); + + await 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 + ) + ); + + await 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"); + + await 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)); + await waitUntilCensusState(store, s => s.census, [ + censusState.SAVED, + censusState.SAVING, + censusState.SAVED, + ]); + ok(true, "selecting non-inverted census should trigger a recompute"); + + await 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(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_action-toggle-inverted-and-refresh-02.js b/devtools/client/memory/test/xpcshell/test_action-toggle-inverted-and-refresh-02.js new file mode 100644 index 0000000000..7047f6410e --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_action-toggle-inverted-and-refresh-02.js @@ -0,0 +1,82 @@ +/* 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("resource://devtools/client/memory/constants.js"); +const { + takeSnapshotAndCensus, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + setCensusDisplay, + setCensusDisplayAndRefresh, +} = require("resource://devtools/client/memory/actions/census-display.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { 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)); + await waitUntilSnapshotState(store, [states.SAVING]); + + dispatch( + setCensusDisplayAndRefresh( + heapWorker, + censusDisplays.invertedAllocationStack + ) + ); + + await 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) + ); + await 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 + ) + ); + await 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(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_action-toggle-inverted.js b/devtools/client/memory/test/xpcshell/test_action-toggle-inverted.js new file mode 100644 index 0000000000..8d946303fa --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_action-toggle-inverted.js @@ -0,0 +1,31 @@ +/* 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("resource://devtools/client/memory/constants.js"); +const { + setCensusDisplay, +} = require("resource://devtools/client/memory/actions/census-display.js"); + +add_task(async function () { + const 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/xpcshell/test_action-toggle-recording-allocations.js b/devtools/client/memory/test/xpcshell/test_action-toggle-recording-allocations.js new file mode 100644 index 0000000000..9ff428b093 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_action-toggle-recording-allocations.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test toggling the recording of allocation stacks. + */ + +const { + toggleRecordingAllocationStacks, +} = require("resource://devtools/client/memory/actions/allocations.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + await front.attach(); + // Implement the minimal mock, doing nothing to make toggleRecordingAllocationStacks pass + const commands = { + targetCommand: { + hasTargetWatcherSupport() { + return true; + }, + }, + targetConfigurationCommand: { + updateConfiguration() {}, + }, + }; + const 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(commands)); + await waitUntilState(store, () => getState().allocations.togglingInProgress); + ok(true, "`togglingInProgress` set to true when toggling on"); + await waitUntilState(store, () => !getState().allocations.togglingInProgress); + + equal(getState().allocations.recording, true, "now we are recording"); + + dispatch(toggleRecordingAllocationStacks(commands)); + await waitUntilState(store, () => getState().allocations.togglingInProgress); + ok(true, "`togglingInProgress` set to true when toggling off"); + await waitUntilState(store, () => !getState().allocations.togglingInProgress); + + equal(getState().allocations.recording, false, "now we are not recording"); + + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_action_diffing_01.js b/devtools/client/memory/test/xpcshell/test_action_diffing_01.js new file mode 100644 index 0000000000..ede92d0ed7 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_action_diffing_01.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test toggling of diffing. + +const { + toggleDiffing, +} = require("resource://devtools/client/memory/actions/diffing.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const 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(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_action_diffing_02.js b/devtools/client/memory/test/xpcshell/test_action_diffing_02.js new file mode 100644 index 0000000000..b0c4f21fc1 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_action_diffing_02.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that toggling diffing unselects all snapshots. + +const { + censusState, + viewState, +} = require("resource://devtools/client/memory/constants.js"); +const { + toggleDiffing, +} = require("resource://devtools/client/memory/actions/diffing.js"); +const { + takeSnapshotAndCensus, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const 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)); + await 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 (const s of getState().snapshots) { + ok( + !s.selected, + "No snapshot should be selected after entering diffing mode" + ); + } + + heapWorker.destroy(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_action_diffing_03.js b/devtools/client/memory/test/xpcshell/test_action_diffing_03.js new file mode 100644 index 0000000000..8db0e8568b --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_action_diffing_03.js @@ -0,0 +1,147 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test selecting snapshots for diffing. + +const { + diffingState, + snapshotState, + viewState, +} = require("resource://devtools/client/memory/constants.js"); +const { + toggleDiffing, + selectSnapshotForDiffing, +} = require("resource://devtools/client/memory/actions/diffing.js"); +const { + takeSnapshot, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +// 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(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const 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)); + + await 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(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_action_diffing_04.js b/devtools/client/memory/test/xpcshell/test_action_diffing_04.js new file mode 100644 index 0000000000..92a57276bb --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_action_diffing_04.js @@ -0,0 +1,101 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that we compute census diffs. + +const { + diffingState, + snapshotState, + viewState, +} = require("resource://devtools/client/memory/constants.js"); +const { + toggleDiffing, + selectSnapshotForDiffingAndRefresh, +} = require("resource://devtools/client/memory/actions/diffing.js"); +const { + takeSnapshot, + readSnapshot, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { getState, dispatch } = store; + dispatch(changeView(viewState.CENSUS)); + + equal(getState().diffing, null, "not diffing by default"); + + const s1 = await dispatch(takeSnapshot(front, heapWorker)); + const s2 = await dispatch(takeSnapshot(front, heapWorker)); + const s3 = await dispatch(takeSnapshot(front, heapWorker)); + dispatch(readSnapshot(heapWorker, s1)); + dispatch(readSnapshot(heapWorker, s2)); + dispatch(readSnapshot(heapWorker, s3)); + await 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." + ); + + await waitUntilState( + store, + state => state.diffing.state === diffingState.TAKING_DIFF + ); + ok( + true, + "Selecting two snapshots for diffing should trigger computing a diff." + ); + + await 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(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_action_diffing_05.js b/devtools/client/memory/test/xpcshell/test_action_diffing_05.js new file mode 100644 index 0000000000..885ae5c1d4 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_action_diffing_05.js @@ -0,0 +1,134 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that we recompute census diffs at the appropriate times. + +const { + diffingState, + snapshotState, + censusDisplays, + viewState, +} = require("resource://devtools/client/memory/constants.js"); +const { + setCensusDisplayAndRefresh, +} = require("resource://devtools/client/memory/actions/census-display.js"); +const { + toggleDiffing, + selectSnapshotForDiffingAndRefresh, +} = require("resource://devtools/client/memory/actions/diffing.js"); +const { + setFilterStringAndRefresh, +} = require("resource://devtools/client/memory/actions/filter.js"); +const { + takeSnapshot, + readSnapshot, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { getState, dispatch } = store; + dispatch(changeView(viewState.CENSUS)); + + await dispatch( + setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack) + ); + equal(getState().censusDisplay.inverted, false, "not inverted at start"); + + equal(getState().diffing, null, "not diffing by default"); + + const s1 = await dispatch(takeSnapshot(front, heapWorker)); + const s2 = await dispatch(takeSnapshot(front, heapWorker)); + const s3 = await dispatch(takeSnapshot(front, heapWorker)); + dispatch(readSnapshot(heapWorker, s1)); + dispatch(readSnapshot(heapWorker, s2)); + dispatch(readSnapshot(heapWorker, s3)); + await waitUntilSnapshotState(store, [ + snapshotState.READ, + snapshotState.READ, + snapshotState.READ, + ]); + + await dispatch(toggleDiffing()); + dispatch( + selectSnapshotForDiffingAndRefresh(heapWorker, getState().snapshots[0]) + ); + dispatch( + selectSnapshotForDiffingAndRefresh(heapWorker, getState().snapshots[1]) + ); + await 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 (const { name, func } of shouldTriggerRecompute) { + dumpn(`Testing that "${name}" triggers a diff recompute`); + func(); + + await waitUntilState( + store, + state => state.diffing.state === diffingState.TAKING_DIFF + ); + ok(true, "triggered diff recompute."); + + await 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(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_dominator_trees_01.js b/devtools/client/memory/test/xpcshell/test_dominator_trees_01.js new file mode 100644 index 0000000000..9b8bbb2533 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_01.js @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that we can compute and fetch the dominator tree for a snapshot. + +const { + dominatorTreeState, + treeMapState, +} = require("resource://devtools/client/memory/constants.js"); +const { + takeSnapshotAndCensus, + computeAndFetchDominatorTree, +} = require("resource://devtools/client/memory/actions/snapshot.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { getState, dispatch } = store; + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + await 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. + await 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. + await 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. + await 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(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_dominator_trees_02.js b/devtools/client/memory/test/xpcshell/test_dominator_trees_02.js new file mode 100644 index 0000000000..eb38591286 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_02.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that selecting the dominator tree view automatically kicks off fetching +// and computing dominator trees. + +const { + dominatorTreeState, + viewState, + treeMapState, +} = require("resource://devtools/client/memory/constants.js"); +const { + takeSnapshotAndCensus, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + changeViewAndRefresh, +} = require("resource://devtools/client/memory/actions/view.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { getState, dispatch } = store; + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + await 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. + await 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. + await 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. + await 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(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_dominator_trees_03.js b/devtools/client/memory/test/xpcshell/test_dominator_trees_03.js new file mode 100644 index 0000000000..e46c1969c6 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_03.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that selecting the dominator tree view and then taking a snapshot +// properly kicks off fetching and computing dominator trees. + +const { + dominatorTreeState, + viewState, +} = require("resource://devtools/client/memory/constants.js"); +const { + takeSnapshotAndCensus, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { 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. + await 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. + await 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. + await 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(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_dominator_trees_04.js b/devtools/client/memory/test/xpcshell/test_dominator_trees_04.js new file mode 100644 index 0000000000..d9814590a9 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_04.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// 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("resource://devtools/client/memory/constants.js"); +const { + takeSnapshotAndCensus, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + + for (const intermediateSnapshotState of [ + states.SAVING, + states.READING, + states.READ, + ]) { + dumpn( + "Testing switching to the DOMINATOR_TREE view in the middle of the " + + `${intermediateSnapshotState} snapshot state` + ); + + const store = Store(); + const { getState, dispatch } = store; + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + await 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. + await 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. + await 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. + await 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(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_dominator_trees_05.js b/devtools/client/memory/test/xpcshell/test_dominator_trees_05.js new file mode 100644 index 0000000000..7f6ed31bb3 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_05.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// 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. + +const { + dominatorTreeState, + viewState, + treeMapState, +} = require("resource://devtools/client/memory/constants.js"); +const { + takeSnapshotAndCensus, + selectSnapshotAndRefresh, +} = require("resource://devtools/client/memory/actions/snapshot.js"); + +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { getState, dispatch } = store; + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + await 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. + await 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. + await 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(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_dominator_trees_06.js b/devtools/client/memory/test/xpcshell/test_dominator_trees_06.js new file mode 100644 index 0000000000..cf32c10a9c --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_06.js @@ -0,0 +1,152 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that we can incrementally fetch a subtree of a dominator tree. + +const { + dominatorTreeState, + viewState, +} = require("resource://devtools/client/memory/constants.js"); +const { + takeSnapshotAndCensus, + fetchImmediatelyDominated, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const DominatorTreeLazyChildren = require("resource://devtools/client/memory/dominator-tree-lazy-children.js"); + +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { getState, dispatch } = store; + + dispatch(changeView(viewState.DOMINATOR_TREE)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + + // Wait for the dominator tree to finish being fetched. + await 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 (const 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" + ); + + await 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 (const 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(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_dominator_trees_07.js b/devtools/client/memory/test/xpcshell/test_dominator_trees_07.js new file mode 100644 index 0000000000..7185aab5c0 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_07.js @@ -0,0 +1,174 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that we can incrementally fetch two subtrees in the same dominator tree +// concurrently. This exercises the activeFetchRequestCount machinery. + +const { + dominatorTreeState, + viewState, +} = require("resource://devtools/client/memory/constants.js"); +const { + takeSnapshotAndCensus, + fetchImmediatelyDominated, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const DominatorTreeLazyChildren = require("resource://devtools/client/memory/dominator-tree-lazy-children.js"); + +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { getState, dispatch } = store; + + dispatch(changeView(viewState.DOMINATOR_TREE)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + + // Wait for the dominator tree to finish being fetched. + await 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 (const 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 (const 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" + ); + + await 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 (const 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(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_dominator_trees_08.js b/devtools/client/memory/test/xpcshell/test_dominator_trees_08.js new file mode 100644 index 0000000000..e61c8b66b8 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_08.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that we can change the display with which we describe a dominator tree +// and that the dominator tree is re-fetched. + +const { + dominatorTreeState, + viewState, + labelDisplays, + treeMapState, +} = require("resource://devtools/client/memory/constants.js"); +const { + setLabelDisplayAndRefresh, +} = require("resource://devtools/client/memory/actions/label-display.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); +const { + takeSnapshotAndCensus, +} = require("resource://devtools/client/memory/actions/snapshot.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { getState, dispatch } = store; + + dispatch(changeView(viewState.DOMINATOR_TREE)); + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + await 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. + await 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) + ); + + await waitUntilState( + store, + state => + state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING + ); + ok( + true, + "switching display types caused the dominator tree to be fetched " + + "again." + ); + + await 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(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_dominator_trees_09.js b/devtools/client/memory/test/xpcshell/test_dominator_trees_09.js new file mode 100644 index 0000000000..b87964970e --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_09.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// 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 { + dominatorTreeState, + viewState, + labelDisplays, + treeMapState, +} = require("resource://devtools/client/memory/constants.js"); +const { + setLabelDisplayAndRefresh, +} = require("resource://devtools/client/memory/actions/label-display.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); +const { + takeSnapshotAndCensus, +} = require("resource://devtools/client/memory/actions/snapshot.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { getState, dispatch } = store; + + dispatch(changeView(viewState.DOMINATOR_TREE)); + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + await 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. + await 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. + await 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(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_dominator_trees_10.js b/devtools/client/memory/test/xpcshell/test_dominator_trees_10.js new file mode 100644 index 0000000000..f6dc791461 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_dominator_trees_10.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that we maintain focus of the selected dominator tree node across +// changing breakdowns for labeling them. + +const { + dominatorTreeState, + labelDisplays, + viewState, +} = require("resource://devtools/client/memory/constants.js"); +const { + takeSnapshotAndCensus, + focusDominatorTreeNode, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); +const { + setLabelDisplayAndRefresh, +} = require("resource://devtools/client/memory/actions/label-display.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { getState, dispatch } = store; + + dispatch(changeView(viewState.DOMINATOR_TREE)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + + // Wait for the dominator tree to finish being fetched. + await 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" + ); + + await waitUntilState( + store, + state => + state.snapshots[0].dominatorTree.state === dominatorTreeState.FETCHING + ); + ok(true, "We started re-fetching the dominator tree"); + + await 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(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_individuals_01.js b/devtools/client/memory/test/xpcshell/test_individuals_01.js new file mode 100644 index 0000000000..9278537136 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_individuals_01.js @@ -0,0 +1,77 @@ +/* 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("resource://devtools/client/memory/constants.js"); +const { + fetchIndividuals, + takeSnapshotAndCensus, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +const EXPECTED_INDIVIDUAL_STATES = [ + individualsState.COMPUTING_DOMINATOR_TREE, + individualsState.FETCHING, + individualsState.FETCHED, +]; + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { getState, dispatch } = store; + + equal(getState().individuals, null, "no individuals state by default"); + + dispatch(changeView(viewState.CENSUS)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + await 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 (const state of EXPECTED_INDIVIDUAL_STATES) { + await 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, + "Should have a positive number of nodes" + ); + + heapWorker.destroy(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_individuals_02.js b/devtools/client/memory/test/xpcshell/test_individuals_02.js new file mode 100644 index 0000000000..76d34187a0 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_individuals_02.js @@ -0,0 +1,91 @@ +/* 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("resource://devtools/client/memory/constants.js"); +const { + fetchIndividuals, + takeSnapshotAndCensus, + computeDominatorTree, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +const EXPECTED_INDIVIDUAL_STATES = [ + individualsState.COMPUTING_DOMINATOR_TREE, + individualsState.FETCHING, + individualsState.FETCHED, +]; + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { getState, dispatch } = store; + + equal(getState().individuals, null, "no individuals state by default"); + + dispatch(changeView(viewState.CENSUS)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + await 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 (const state of EXPECTED_INDIVIDUAL_STATES) { + await 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, + "Should have a positive number of nodes" + ); + + heapWorker.destroy(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_individuals_03.js b/devtools/client/memory/test/xpcshell/test_individuals_03.js new file mode 100644 index 0000000000..db31bcf347 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_individuals_03.js @@ -0,0 +1,114 @@ +/* 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("resource://devtools/client/memory/constants.js"); +const { + fetchIndividuals, + takeSnapshotAndCensus, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + changeView, + popViewAndRefresh, +} = require("resource://devtools/client/memory/actions/view.js"); +const { + selectSnapshotForDiffingAndRefresh, +} = require("resource://devtools/client/memory/actions/diffing.js"); + +const EXPECTED_INDIVIDUAL_STATES = [ + individualsState.COMPUTING_DOMINATOR_TREE, + individualsState.FETCHING, + individualsState.FETCHED, +]; + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const 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)); + await 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]) + ); + + await 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 (const state of EXPECTED_INDIVIDUAL_STATES) { + await 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, + "Should have a positive number of nodes" + ); + + // Pop the view back to the diffing. + + dispatch(popViewAndRefresh(heapWorker)); + + await 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(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_individuals_04.js b/devtools/client/memory/test/xpcshell/test_individuals_04.js new file mode 100644 index 0000000000..f2cd71d4b9 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_individuals_04.js @@ -0,0 +1,93 @@ +/* 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("resource://devtools/client/memory/constants.js"); +const { + fetchIndividuals, + takeSnapshotAndCensus, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); +const { + setFilterString, +} = require("resource://devtools/client/memory/actions/filter.js"); + +const EXPECTED_INDIVIDUAL_STATES = [ + individualsState.COMPUTING_DOMINATOR_TREE, + individualsState.FETCHING, + individualsState.FETCHED, +]; + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const 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)); + await 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 (const state of EXPECTED_INDIVIDUAL_STATES) { + await 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, + "Should have a positive number of nodes" + ); + + // Assert that all the individuals are `Array`s. + + for (const 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(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_individuals_05.js b/devtools/client/memory/test/xpcshell/test_individuals_05.js new file mode 100644 index 0000000000..506751916a --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_individuals_05.js @@ -0,0 +1,84 @@ +/* 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("resource://devtools/client/memory/constants.js"); +const { + fetchIndividuals, + takeSnapshotAndCensus, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); +const { + setCensusDisplay, +} = require("resource://devtools/client/memory/actions/census-display.js"); + +const EXPECTED_INDIVIDUAL_STATES = [ + individualsState.COMPUTING_DOMINATOR_TREE, + individualsState.FETCHING, + individualsState.FETCHED, +]; + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const 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)); + await 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 (const state of EXPECTED_INDIVIDUAL_STATES) { + await 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, + "Should have a positive number of nodes" + ); + + heapWorker.destroy(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_individuals_06.js b/devtools/client/memory/test/xpcshell/test_individuals_06.js new file mode 100644 index 0000000000..54aa759464 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_individuals_06.js @@ -0,0 +1,85 @@ +/* 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("resource://devtools/client/memory/constants.js"); +const { + fetchIndividuals, + takeSnapshotAndCensus, + clearSnapshots, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +const EXPECTED_INDIVIDUAL_STATES = [ + individualsState.COMPUTING_DOMINATOR_TREE, + individualsState.FETCHING, + individualsState.FETCHED, +]; + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { getState, dispatch } = store; + + dispatch(changeView(viewState.CENSUS)); + + // Take a snapshot and wait for the census to finish. + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + await 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 (const state of EXPECTED_INDIVIDUAL_STATES) { + await 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, + "Should have a positive number of nodes" + ); + + dispatch(clearSnapshots(heapWorker)); + + equal(getState().view.state, viewState.CENSUS, "Went back to census view"); + + heapWorker.destroy(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_pop_view_01.js b/devtools/client/memory/test/xpcshell/test_pop_view_01.js new file mode 100644 index 0000000000..da44d1d125 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_pop_view_01.js @@ -0,0 +1,80 @@ +/* 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("resource://devtools/client/memory/constants.js"); +const { + fetchIndividuals, + takeSnapshotAndCensus, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + changeView, + popViewAndRefresh, +} = require("resource://devtools/client/memory/actions/view.js"); + +const TEST_STATES = [ + individualsState.COMPUTING_DOMINATOR_TREE, + individualsState.FETCHING, + individualsState.FETCHED, +]; + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { getState, dispatch } = store; + + equal(getState().individuals, null, "no individuals state by default"); + + dispatch(changeView(viewState.CENSUS)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + await 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 (const 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. + await 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)); + await waitUntilState(store, s => { + return s.view.state === viewState.CENSUS; + }); + ok(!getState().individuals, "Should no longer have individuals"); + } + + heapWorker.destroy(); + await front.detach(); +}); diff --git a/devtools/client/memory/test/xpcshell/test_tree-map-01.js b/devtools/client/memory/test/xpcshell/test_tree-map-01.js new file mode 100644 index 0000000000..1e91a43ec9 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_tree-map-01.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + drawBox, +} = require("resource://devtools/client/memory/components/tree-map/draw.js"); + +add_task(async function () { + let fillRectValues, strokeRectValues; + const ctx = { + fillRect: (...args) => { + fillRectValues = args; + }, + strokeRect: (...args) => { + strokeRectValues = args; + }, + }; + const node = { + x: 20, + y: 30, + dx: 50, + dy: 70, + type: "other", + depth: 2, + }; + const padding = [10, 10]; + const borderWidth = () => 1; + const dragZoom = { + offsetX: 0, + offsetY: 0, + zoom: 0, + }; + drawBox(ctx, node, borderWidth, dragZoom, padding); + ok(true, JSON.stringify([ctx, fillRectValues, strokeRectValues])); + equal(ctx.fillStyle, "hsl(204,60%,70%)", "The fillStyle is set"); + equal(ctx.strokeStyle, "hsl(204,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/xpcshell/test_tree-map-02.js b/devtools/client/memory/test/xpcshell/test_tree-map-02.js new file mode 100644 index 0000000000..528c59dcfa --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_tree-map-02.js @@ -0,0 +1,105 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + drawText, +} = require("resource://devtools/client/memory/components/tree-map/draw.js"); + +add_task(async function () { + // Mock out the Canvas2dContext + const ctx = { + fillText: (...args) => fillTextValues.push(args), + measureText: text => { + const width = text ? text.length * 10 : 0; + return { width }; + }, + }; + const node = { + x: 20, + y: 30, + dx: 500, + dy: 70, + name: "Example Node", + totalBytes: 1200, + totalCount: 100, + }; + const ratio = 0; + const borderWidth = () => 1; + const dragZoom = { + offsetX: 0, + offsetY: 0, + zoom: 0, + }; + let fillTextValues = []; + const 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/xpcshell/test_utils-get-snapshot-totals.js b/devtools/client/memory/test/xpcshell/test_utils-get-snapshot-totals.js new file mode 100644 index 0000000000..13ad5958a7 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_utils-get-snapshot-totals.js @@ -0,0 +1,103 @@ +/* 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, + viewState, + censusState, +} = require("resource://devtools/client/memory/constants.js"); +const { + getSnapshotTotals, +} = require("resource://devtools/client/memory/utils.js"); +const { + takeSnapshotAndCensus, +} = require("resource://devtools/client/memory/actions/snapshot.js"); +const { + setCensusDisplayAndRefresh, +} = require("resource://devtools/client/memory/actions/census-display.js"); +const { + changeView, +} = require("resource://devtools/client/memory/actions/view.js"); + +add_task(async function () { + const front = new StubbedMemoryFront(); + const heapWorker = new HeapAnalysesClient(); + await front.attach(); + const store = Store(); + const { getState, dispatch } = store; + + dispatch(changeView(viewState.CENSUS)); + + await dispatch( + setCensusDisplayAndRefresh(heapWorker, censusDisplays.allocationStack) + ); + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + await waitUntilCensusState(store, s => s.census, [censusState.SAVED]); + + ok( + !getState().snapshots[0].census.display.inverted, + "Snapshot is not inverted" + ); + + const census = getState().snapshots[0].census; + let result = aggregate(census.report); + const totalBytes = result.bytes; + const 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 + ) + ); + + await waitUntilCensusState(store, s => s.census, [censusState.SAVING]); + await 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 (const child of report.children || []) { + const { bytes, count } = aggregate(child); + totalBytes += bytes; + totalCount += count; + } + return { bytes: totalBytes, count: totalCount }; +} diff --git a/devtools/client/memory/test/xpcshell/test_utils.js b/devtools/client/memory/test/xpcshell/test_utils.js new file mode 100644 index 0000000000..1d58552e56 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/test_utils.js @@ -0,0 +1,116 @@ +/* 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. + */ + +const utils = require("resource://devtools/client/memory/utils.js"); +const { + snapshotState: states, + viewState, +} = require("resource://devtools/client/memory/constants.js"); +const { Preferences } = ChromeUtils.importESModule( + "resource://gre/modules/Preferences.sys.mjs" +); + +add_task(async function () { + const s1 = utils.createSnapshot({ view: { state: viewState.CENSUS } }); + const 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" + ); + + const 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/xpcshell/xpcshell.ini b/devtools/client/memory/test/xpcshell/xpcshell.ini new file mode 100644 index 0000000000..a20ddb1462 --- /dev/null +++ b/devtools/client/memory/test/xpcshell/xpcshell.ini @@ -0,0 +1,58 @@ +[DEFAULT] +tags = devtools devtools-memory +head = ../../../shared/test/shared-head.js head.js +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] +skip-if = tsan # Unreasonably slow, bug 1612707 +[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] +skip-if = tsan # Unreasonably slow, bug 1612707 +[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] +skip-if = tsan # Times out, bug 1612707 +[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] |