diff --git a/devtools/client/locales/en-US/memory.properties b/devtools/client/locales/en-US/memory.properties index 472f9c678c3b..bfe70533e732 100644 --- a/devtools/client/locales/en-US/memory.properties +++ b/devtools/client/locales/en-US/memory.properties @@ -84,6 +84,10 @@ take-snapshot=Take snapshot # initiates importing a snapshot. import-snapshot=Import… +# LOCALIZATION NOTE (clear-snapshots): The label describing the button that clears +# existing snapshot. +clear-snapshots=Clear + # LOCALIZATION NOTE (diff-snapshots): The label for the button that initiates # selecting two snapshots to diff with each other. diff-snapshots=+/- diff --git a/devtools/client/memory/actions/snapshot.js b/devtools/client/memory/actions/snapshot.js index 97bfdbaf67ab..9eb41a916a79 100644 --- a/devtools/client/memory/actions/snapshot.js +++ b/devtools/client/memory/actions/snapshot.js @@ -406,6 +406,32 @@ const selectSnapshot = exports.selectSnapshot = function (id) { }; }; +/** + * Delete all snapshots that are in the SAVED_CENSUS or ERROR state + * + * @param {HeapAnalysesClient} heapWorker + */ +const clearSnapshots = exports.clearSnapshots = function (heapWorker) { + return function*(dispatch, getState) { + let snapshots = getState().snapshots.filter( + s => s.state === states.SAVED_CENSUS || s.state === states.ERROR); + + let ids = snapshots.map(s => s.id); + + dispatch({ type: actions.DELETE_SNAPSHOTS_START, ids }); + + Promise.all(snapshots.map(s => { + heapWorker.deleteHeapSnapshot(s.path) + .catch(error => { + reportException("clearSnapshots", error); + dispatch({ type: actions.SNAPSHOT_ERROR, id: s.id, error }); + }); + })); + + dispatch({ type: actions.DELETE_SNAPSHOTS_END, ids }); + }; +}; + /** * Expand the given node in the snapshot's census report. * diff --git a/devtools/client/memory/app.js b/devtools/client/memory/app.js index f39bd247840c..861c93412027 100644 --- a/devtools/client/memory/app.js +++ b/devtools/client/memory/app.js @@ -22,6 +22,7 @@ const { pickFileAndExportSnapshot, pickFileAndImportSnapshotAndCensus } = requir const { selectSnapshotAndRefresh, takeSnapshotAndCensus, + clearSnapshots, fetchImmediatelyDominated, expandCensusNode, collapseCensusNode, @@ -98,6 +99,7 @@ const MemoryApp = createClass({ snapshots, breakdowns: getBreakdownDisplayData(), onImportClick: () => dispatch(pickFileAndImportSnapshotAndCensus(heapWorker)), + onClearSnapshotsClick: () => dispatch(clearSnapshots(heapWorker)), onTakeSnapshotClick: () => dispatch(takeSnapshotAndCensus(front, heapWorker)), onBreakdownChange: breakdown => dispatch(setBreakdownAndRefresh(heapWorker, breakdownNameToSpec(breakdown))), diff --git a/devtools/client/memory/components/toolbar.js b/devtools/client/memory/components/toolbar.js index 18bd997c3ca0..551f3023d2ac 100644 --- a/devtools/client/memory/components/toolbar.js +++ b/devtools/client/memory/components/toolbar.js @@ -17,6 +17,7 @@ const Toolbar = module.exports = createClass({ })).isRequired, onTakeSnapshotClick: PropTypes.func.isRequired, onImportClick: PropTypes.func.isRequired, + onClearSnapshotsClick: PropTypes.func.isRequired, onBreakdownChange: PropTypes.func.isRequired, onToggleRecordAllocationStacks: PropTypes.func.isRequired, allocations: models.allocations, @@ -40,6 +41,7 @@ const Toolbar = module.exports = createClass({ let { onTakeSnapshotClick, onImportClick, + onClearSnapshotsClick, onBreakdownChange, breakdowns, dominatorTreeBreakdowns, @@ -190,7 +192,15 @@ const Toolbar = module.exports = createClass({ "data-text-only": true, }, L10N.getStr("import-snapshot") - ) + ), + + dom.button({ + id: "clear-snapshots", + className: "devtools-toolbarbutton clear-snapshots devtools-button", + onClick: onClearSnapshotsClick, + title: L10N.getStr("clear-snapshots"), + "data-text-only": true, + }, L10N.getStr("clear-snapshots")) ), dom.label( diff --git a/devtools/client/memory/constants.js b/devtools/client/memory/constants.js index 8e257c4e4c9b..0204cb00a635 100644 --- a/devtools/client/memory/constants.js +++ b/devtools/client/memory/constants.js @@ -50,6 +50,10 @@ actions.IMPORT_SNAPSHOT_ERROR = "import-snapshot-error"; // Fired by UI to select a snapshot to view. actions.SELECT_SNAPSHOT = "select-snapshot"; +// Fired to delete a provided list of snapshots +actions.DELETE_SNAPSHOTS_START = "delete-snapshots-start"; +actions.DELETE_SNAPSHOTS_END = "delete-snapshots-end"; + // Fired to toggle tree inversion on or off. actions.TOGGLE_INVERTED = "toggle-inverted"; diff --git a/devtools/client/memory/reducers/snapshots.js b/devtools/client/memory/reducers/snapshots.js index c006c37716a8..b79bfbe78b6c 100644 --- a/devtools/client/memory/reducers/snapshots.js +++ b/devtools/client/memory/reducers/snapshots.js @@ -138,6 +138,14 @@ handlers[actions.SELECT_SNAPSHOT] = function (snapshots, { id }) { return snapshots.map(s => immutableUpdate(s, { selected: s.id === id })); }; +handlers[actions.DELETE_SNAPSHOTS_START] = function (snapshots, { ids }) { + return snapshots.filter(s => ids.indexOf(s.id) === -1); +}; + +handlers[actions.DELETE_SNAPSHOTS_END] = function (snapshots) { + return snapshots; +}; + handlers[actions.CHANGE_VIEW] = function (snapshots, { view }) { return view === viewState.DIFFING ? snapshots.map(s => immutableUpdate(s, { selected: false })) diff --git a/devtools/client/memory/test/browser/browser.ini b/devtools/client/memory/test/browser/browser.ini index a0122a76d898..86b011a59973 100644 --- a/devtools/client/memory/test/browser/browser.ini +++ b/devtools/client/memory/test/browser/browser.ini @@ -18,6 +18,8 @@ support-files = [browser_memory_no_auto_expand.js] skip-if = debug # bug 1219554 [browser_memory_percents_01.js] +[browser_memory-clear-snapshots.js] + skip-if = debug # bug 1219554 [browser_memory-simple-01.js] skip-if = debug # bug 1219554 [browser_memory_transferHeapSnapshot_e10s_01.js] diff --git a/devtools/client/memory/test/browser/browser_memory-clear-snapshots.js b/devtools/client/memory/test/browser/browser_memory-clear-snapshots.js new file mode 100644 index 000000000000..173f0ba8c63b --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory-clear-snapshots.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests taking and then clearing snapshots. + */ + +const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html"; + +this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) { + const { gStore, document } = panel.panelWin; + const { getState, dispatch } = gStore; + + let snapshotEls = document.querySelectorAll("#memory-tool-container .list li"); + is(getState().snapshots.length, 0, "Starts with no snapshots in store"); + is(snapshotEls.length, 0, "No snapshots visible"); + + info("Take two snapshots"); + yield takeSnapshot(panel.panelWin); + yield takeSnapshot(panel.panelWin); + yield waitUntilSnapshotState(gStore, [states.SAVED_CENSUS, states.SAVED_CENSUS]); + + snapshotEls = document.querySelectorAll("#memory-tool-container .list li"); + is(snapshotEls.length, 2, "Two snapshots visible"); + + info("Click on Clear Snapshots"); + yield clearSnapshots(panel.panelWin); + is(getState().snapshots.length, 0, "No snapshots in store"); + snapshotEls = document.querySelectorAll("#memory-tool-container .list li"); + is(snapshotEls.length, 0, "No snapshot visible"); +}); diff --git a/devtools/client/memory/test/browser/head.js b/devtools/client/memory/test/browser/head.js index ad2235a99b42..114085f7e553 100644 --- a/devtools/client/memory/test/browser/head.js +++ b/devtools/client/memory/test/browser/head.js @@ -104,6 +104,14 @@ function takeSnapshot (window) { return waitUntilState(gStore, () => gStore.getState().snapshots.length === snapshotCount + 1); } +function clearSnapshots (window) { + let { gStore, document } = window; + document.querySelector(".devtools-toolbar .clear-snapshots").click(); + return waitUntilState(gStore, () => gStore.getState().snapshots.every( + (snapshot) => snapshot.state !== states.SAVED_CENSUS) + ); +} + /** * Sets breakdown and waits for currently selected breakdown to use it * and be completed the census. diff --git a/devtools/client/memory/test/unit/test_action-clear-snapshots_01.js b/devtools/client/memory/test/unit/test_action-clear-snapshots_01.js new file mode 100644 index 000000000000..3ccdbbb05231 --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_01.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test clearSnapshots deletes snapshots with state SAVED_CENSUS + +let { takeSnapshotAndCensus, clearSnapshots } = require("devtools/client/memory/actions/snapshot"); +let { snapshotState: states, actions } = require("devtools/client/memory/constants"); + +function run_test() { + run_next_test(); +} + +add_task(function *() { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + const { getState, dispatch } = store; + + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]); + ok(true, "snapshot created"); + + ok(true, "dispatch clearSnapshots action"); + let deleteEvents = Promise.all([ + waitUntilAction(store, actions.DELETE_SNAPSHOTS_START), + waitUntilAction(store, actions.DELETE_SNAPSHOTS_END) + ]); + dispatch(clearSnapshots(heapWorker)); + yield deleteEvents; + ok(true, "received delete snapshots events"); + + equal(getState().snapshots.length, 0, "no snapshot remaining"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_action-clear-snapshots_02.js b/devtools/client/memory/test/unit/test_action-clear-snapshots_02.js new file mode 100644 index 000000000000..9baa43778c6b --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_02.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test clearSnapshots preserves snapshots with state != SAVED_CENSUS or ERROR + +let { takeSnapshotAndCensus, clearSnapshots, takeSnapshot } = require("devtools/client/memory/actions/snapshot"); +let { snapshotState: states, actions } = require("devtools/client/memory/constants"); + +function run_test() { + run_next_test(); +} + +add_task(function *() { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + const { getState, dispatch } = store; + + ok(true, "create a snapshot in SAVED_CENSUS state"); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + ok(true, "create a snapshot in SAVED state"); + dispatch(takeSnapshot(front)); + yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVED]); + ok(true, "snapshots created with expected states"); + + ok(true, "dispatch clearSnapshots action"); + let deleteEvents = Promise.all([ + waitUntilAction(store, actions.DELETE_SNAPSHOTS_START), + waitUntilAction(store, actions.DELETE_SNAPSHOTS_END) + ]); + dispatch(clearSnapshots(heapWorker)); + yield deleteEvents; + ok(true, "received delete snapshots events"); + + equal(getState().snapshots.length, 1, "one snapshot remaining"); + let remainingSnapshot = getState().snapshots[0]; + notEqual(remainingSnapshot.state, states.SAVED_CENSUS, + "remaining snapshot doesn't have the SAVED_CENSUS state"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_action-clear-snapshots_03.js b/devtools/client/memory/test/unit/test_action-clear-snapshots_03.js new file mode 100644 index 000000000000..159f2c743cd0 --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_03.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test clearSnapshots deletes snapshots with state ERROR + +let { takeSnapshotAndCensus, clearSnapshots } = require("devtools/client/memory/actions/snapshot"); +let { snapshotState: states, actions } = require("devtools/client/memory/constants"); + +function run_test() { + run_next_test(); +} + +add_task(function *() { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + const { getState, dispatch } = store; + + ok(true, "create a snapshot with SAVED_CENSUS state"); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]); + ok(true, "snapshot created with SAVED_CENSUS state"); + + ok(true, "set snapshot state to error"); + let id = getState().snapshots[0].id; + dispatch({ type: actions.SNAPSHOT_ERROR, id, error: new Error("_") }); + yield waitUntilSnapshotState(store, [states.ERROR]); + ok(true, "snapshot set to error state"); + + ok(true, "dispatch clearSnapshots action"); + let deleteEvents = Promise.all([ + waitUntilAction(store, actions.DELETE_SNAPSHOTS_START), + waitUntilAction(store, actions.DELETE_SNAPSHOTS_END) + ]); + dispatch(clearSnapshots(heapWorker)); + yield deleteEvents; + ok(true, "received delete snapshots events"); + equal(getState().snapshots.length, 0, "error snapshot deleted"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_action-clear-snapshots_04.js b/devtools/client/memory/test/unit/test_action-clear-snapshots_04.js new file mode 100644 index 000000000000..9eefd5421675 --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_04.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test clearSnapshots deletes several snapshots + +let { takeSnapshotAndCensus, clearSnapshots } = require("devtools/client/memory/actions/snapshot"); +let { snapshotState: states, actions } = require("devtools/client/memory/constants"); + +function run_test() { + run_next_test(); +} + +add_task(function *() { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + const { getState, dispatch } = store; + + ok(true, "create 3 snapshots in SAVED_CENSUS state"); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + ok(true, "snapshots created in SAVED_CENSUS state"); + yield waitUntilSnapshotState(store, + [states.SAVED_CENSUS, states.SAVED_CENSUS, states.SAVED_CENSUS]); + + ok(true, "set first snapshot state to error"); + let id = getState().snapshots[0].id; + dispatch({ type: actions.SNAPSHOT_ERROR, id, error: new Error("_") }); + yield waitUntilSnapshotState(store, + [states.ERROR, states.SAVED_CENSUS, states.SAVED_CENSUS]); + ok(true, "first snapshot set to error state"); + + ok(true, "dispatch clearSnapshots action"); + let deleteEvents = Promise.all([ + waitUntilAction(store, actions.DELETE_SNAPSHOTS_START), + waitUntilAction(store, actions.DELETE_SNAPSHOTS_END) + ]); + dispatch(clearSnapshots(heapWorker)); + yield deleteEvents; + ok(true, "received delete snapshots events"); + + equal(getState().snapshots.length, 0, "no snapshot remaining"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/test_action-clear-snapshots_05.js b/devtools/client/memory/test/unit/test_action-clear-snapshots_05.js new file mode 100644 index 000000000000..0b601f49710a --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-clear-snapshots_05.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test clearSnapshots deletes several snapshots + +let { takeSnapshotAndCensus, clearSnapshots } = require("devtools/client/memory/actions/snapshot"); +let { snapshotState: states, actions } = require("devtools/client/memory/constants"); + +function run_test() { + run_next_test(); +} + +add_task(function *() { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + const { getState, dispatch } = store; + + ok(true, "create 3 snapshots in SAVED_CENSUS state"); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + ok(true, "snapshots created in SAVED_CENSUS state"); + yield waitUntilSnapshotState(store, [states.SAVED_CENSUS, states.SAVED_CENSUS]); + + let errorHeapWorker = { + deleteHeapSnapshot: function() { + return Promise.reject("_"); + } + }; + + ok(true, "dispatch clearSnapshots action"); + let deleteEvents = Promise.all([ + waitUntilAction(store, actions.DELETE_SNAPSHOTS_START), + waitUntilAction(store, actions.DELETE_SNAPSHOTS_END), + waitUntilAction(store, actions.SNAPSHOT_ERROR), + waitUntilAction(store, actions.SNAPSHOT_ERROR), + ]); + dispatch(clearSnapshots(errorHeapWorker)); + yield deleteEvents; + ok(true, "received delete snapshots and snapshot error events"); + equal(getState().snapshots.length, 0, "no snapshot remaining"); + + heapWorker.destroy(); + yield front.detach(); +}); diff --git a/devtools/client/memory/test/unit/xpcshell.ini b/devtools/client/memory/test/unit/xpcshell.ini index 98ec7fa2bc44..9597233eaa69 100644 --- a/devtools/client/memory/test/unit/xpcshell.ini +++ b/devtools/client/memory/test/unit/xpcshell.ini @@ -10,6 +10,11 @@ skip-if = toolkit == 'android' || toolkit == 'gonk' [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-export-snapshot.js] [test_action-filter-01.js] [test_action-filter-02.js] diff --git a/devtools/client/themes/memory.css b/devtools/client/themes/memory.css index 85a05f438636..76c2f8853a03 100644 --- a/devtools/client/themes/memory.css +++ b/devtools/client/themes/memory.css @@ -58,9 +58,10 @@ html, body, #app, #memory-tool { * We want this to be exactly at a `--sidebar-width` distance from the * toolbar's start boundary. A `.devtools-toolbar` has a 3px start padding. */ - flex: 0 0 calc(var(--sidebar-width) - 3px); + flex: 0 0 calc(var(--sidebar-width) - 4px); border-inline-end: 1px solid var(--theme-splitter-color); margin-inline-end: 5px; + padding-right: 1px; } .devtools-toolbar > .toolbar-group { @@ -111,8 +112,13 @@ html, body, #app, #memory-tool { * Due to toolbar styles of `.devtools-toolbarbutton:not([label])` which overrides * .devtools-toolbarbutton's min-width of 78px, reset the min-width. */ -#import-snapshot { - min-width: 78px; +#import-snapshot, +#clear-snapshots { + -moz-box-align: center; + flex-grow: 1; + padding: 1px; + margin: 2px 1px; + min-width: unset; } .spacer {