Bug 989393 - Clean up old tabs and windows. r=ttaubert

This commit is contained in:
David Rajchenbach-Teller 2014-05-14 06:12:00 -04:00
parent 379aea1acd
commit 616d7a75ab
5 changed files with 216 additions and 0 deletions

View File

@ -1018,6 +1018,8 @@ pref("browser.sessionstore.restore_pinned_tabs_on_demand", false);
pref("browser.sessionstore.upgradeBackup.latestBuildID", "");
// End-users should not run sessionstore in debug mode
pref("browser.sessionstore.debug", false);
// Forget closed windows/tabs after two weeks
pref("browser.sessionstore.cleanup.forget_closed_after", 1209600000);
// allow META refresh by default
pref("accessibility.blockautorefresh", false);

View File

@ -36,6 +36,7 @@ const OBSERVING = [
"quit-application", "browser:purge-session-history",
"browser:purge-domain-data",
"gather-telemetry",
"idle-daily",
];
// XUL Window properties to (re)store
@ -570,6 +571,9 @@ let SessionStoreInternal = {
case "gather-telemetry":
this.onGatherTelemetry();
break;
case "idle-daily":
this.onIdleDaily();
break;
}
},
@ -1428,6 +1432,39 @@ let SessionStoreInternal = {
return SessionFile.gatherTelemetry(stateString);
},
// Clean up data that has been closed a long time ago.
// Do not reschedule a save. This will wait for the next regular
// save.
onIdleDaily: function() {
// Remove old closed windows
this._cleanupOldData([this._closedWindows]);
// Remove closed tabs of closed windows
this._cleanupOldData([winData._closedTabs for (winData of this._closedWindows)]);
// Remove closed tabs of open windows
this._cleanupOldData([this._windows[key]._closedTabs for (key of Object.keys(this._windows))]);
},
// Remove "old" data from an array
_cleanupOldData: function(targets) {
const TIME_TO_LIVE = this._prefBranch.getIntPref("sessionstore.cleanup.forget_closed_after");
const now = Date.now();
for (let array of targets) {
for (let i = array.length - 1; i >= 0; --i) {
let data = array[i];
// Make sure that we have a timestamp to tell us when the target
// has been closed. If we don't have a timestamp, default to a
// safe timestamp: just now.
data.closedAt = data.closedAt || now;
if (now - data.closedAt > TIME_TO_LIVE) {
array.splice(i, 1);
}
}
}
},
/* ........ nsISessionStore API .............. */
getBrowserState: function ssi_getBrowserState() {
@ -1671,6 +1708,8 @@ let SessionStoreInternal = {
// reopen the window
let state = { windows: this._closedWindows.splice(aIndex, 1) };
delete state.windows[0].closedAt; // Window is now open.
let window = this._openWindowWithState(state);
this.windowToFocus = window;
return window;
@ -2472,6 +2511,7 @@ let SessionStoreInternal = {
} else {
delete tab.__SS_extdata;
}
delete tabData.closedAt; // Tab is now open.
// Flush all data from the content script synchronously. This is done so
// that all async messages that are still on their way to chrome will

View File

@ -61,6 +61,7 @@ support-files =
[browser_attributes.js]
[browser_broadcast.js]
[browser_capabilities.js]
[browser_cleaner.js]
[browser_dying_cache.js]
[browser_dynamic_frames.js]
[browser_form_restore_events.js]

View File

@ -0,0 +1,160 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* This test ensures that Session Restore eventually forgets about
* tabs and windows that have been closed a long time ago.
*/
"use strict";
Cu.import("resource://gre/modules/Services.jsm", this);
Cu.import("resource://gre/modules/osfile.jsm", this);
Cu.import("resource://gre/modules/Task.jsm", this);
const LONG_TIME_AGO = 1;
const URL_TAB1 = "http://example.com/browser_cleaner.js?newtab1=" + Math.random();
const URL_TAB2 = "http://example.com/browser_cleaner.js?newtab2=" + Math.random();
const URL_NEWWIN = "http://example.com/browser_cleaner.js?newwin=" + Math.random();
function isRecent(stamp) {
is(typeof stamp, "number", "This is a timestamp");
return Date.now() - stamp <= 60000;
}
function promiseCleanup () {
info("Cleaning up browser");
return promiseBrowserState(getClosedState());
};
function getClosedState() {
return Cu.cloneInto(CLOSED_STATE, {});
}
let CLOSED_STATE;
add_task(function* init() {
while (ss.getClosedWindowCount() > 0) {
ss.forgetClosedWindow(0);
}
while (ss.getClosedTabCount(window) > 0) {
ss.forgetClosedTab(window, 0);
}
});
add_task(function* test_open_and_close() {
let newTab1 = gBrowser.addTab(URL_TAB1);
yield promiseBrowserLoaded(newTab1.linkedBrowser);
let newTab2 = gBrowser.addTab(URL_TAB2);
yield promiseBrowserLoaded(newTab2.linkedBrowser);
let newWin = yield promiseNewWindowLoaded();
let tab = newWin.gBrowser.addTab(URL_NEWWIN);
yield promiseBrowserLoaded(tab.linkedBrowser);
info("1. Making sure that before closing, we don't have closedAt");
// For the moment, no "closedAt"
let state = JSON.parse(ss.getBrowserState());
is(state.windows[0].closedAt || false, false, "1. Main window doesn't have closedAt");
is(state.windows[1].closedAt || false, false, "1. Second window doesn't have closedAt");
is(state.windows[0].tabs[0].closedAt || false, false, "1. First tab doesn't have closedAt");
is(state.windows[0].tabs[1].closedAt || false, false, "1. Second tab doesn't have closedAt");
info("2. Making sure that after closing, we have closedAt");
// Now close stuff, this should add closeAt
yield promiseWindowClosed(newWin);
gBrowser.removeTab(newTab1);
gBrowser.removeTab(newTab2);
state = CLOSED_STATE = JSON.parse(ss.getBrowserState());
is(state.windows[0].closedAt || false, false, "2. Main window doesn't have closedAt");
ok(isRecent(state._closedWindows[0].closedAt), "2. Second window was closed recently");
ok(isRecent(state.windows[0]._closedTabs[0].closedAt), "2. First tab was closed recently");
ok(isRecent(state.windows[0]._closedTabs[1].closedAt), "2. Second tab was closed recently");
});
add_task(function* test_restore() {
info("3. Making sure that after restoring, we don't have closedAt");
yield promiseBrowserState(CLOSED_STATE);
let newWin = ss.undoCloseWindow(0);
yield promiseDelayedStartupFinished(newWin);
let newTab2 = ss.undoCloseTab(window, 0);
yield promiseTabRestored(newTab2);
let newTab1 = ss.undoCloseTab(window, 0);
yield promiseTabRestored(newTab1);
let state = JSON.parse(ss.getBrowserState());
is(state.windows[0].closedAt || false, false, "3. Main window doesn't have closedAt");
is(state.windows[1].closedAt || false, false, "3. Second window doesn't have closedAt");
is(state.windows[0].tabs[0].closedAt || false, false, "3. First tab doesn't have closedAt");
is(state.windows[0].tabs[1].closedAt || false, false, "3. Second tab doesn't have closedAt");
yield promiseWindowClosed(newWin);
gBrowser.removeTab(newTab1);
gBrowser.removeTab(newTab2);
});
add_task(function* test_old_data() {
info("4. Removing closedAt from the sessionstore, making sure that it is added upon idle-daily");
let state = getClosedState();
delete state._closedWindows[0].closedAt;
delete state.windows[0]._closedTabs[0].closedAt;
delete state.windows[0]._closedTabs[1].closedAt;
yield promiseBrowserState(state);
info("Sending idle-daily");
Services.obs.notifyObservers(null, "idle-daily", "");
info("Sent idle-daily");
state = JSON.parse(ss.getBrowserState());
is(state.windows[0].closedAt || false, false, "4. Main window doesn't have closedAt");
ok(isRecent(state._closedWindows[0].closedAt), "4. Second window was closed recently");
ok(isRecent(state.windows[0]._closedTabs[0].closedAt), "4. First tab was closed recently");
ok(isRecent(state.windows[0]._closedTabs[1].closedAt), "4. Second tab was closed recently");
yield promiseCleanup();
});
add_task(function* test_cleanup() {
info("5. Altering closedAt to an old date, making sure that stuff gets collected, eventually");
yield promiseCleanup();
let state = getClosedState();
state._closedWindows[0].closedAt = LONG_TIME_AGO;
state.windows[0]._closedTabs[0].closedAt = LONG_TIME_AGO;
state.windows[0]._closedTabs[1].closedAt = Date.now();
let url = state.windows[0]._closedTabs[1].state.entries[0].url;
yield promiseBrowserState(state);
info("Sending idle-daily");
Services.obs.notifyObservers(null, "idle-daily", "");
info("Sent idle-daily");
state = JSON.parse(ss.getBrowserState());
is(state._closedWindows[0], undefined, "5. Second window was forgotten");
is(state.windows[0]._closedTabs.length, 1, "5. Only one closed tab left");
is(state.windows[0]._closedTabs[0].state.entries[0].url, url, "5. The second tab is still here");
yield promiseCleanup();
});

View File

@ -88,6 +88,12 @@ function provideWindow(aCallback, aURL, aFeatures) {
// This assumes that tests will at least have some state/entries
function waitForBrowserState(aState, aSetStateCallback) {
if (typeof aState == "string") {
aState = JSON.parse(aState);
}
if (typeof aState != "object") {
throw new TypeError("Argument must be an object or a JSON representation of an object");
}
let windows = [window];
let tabsRestored = 0;
let expectedTabsRestored = 0;
@ -172,6 +178,10 @@ function waitForBrowserState(aState, aSetStateCallback) {
ss.setBrowserState(JSON.stringify(aState));
}
function promiseBrowserState(aState) {
return new Promise(resolve => waitForBrowserState(aState, resolve));
}
// Doesn't assume that the tab needs to be closed in a cleanup function.
// If that's the case, the test author should handle that in the test.
function waitForTabState(aTab, aState, aCallback) {
@ -481,6 +491,9 @@ function whenDelayedStartupFinished(aWindow, aCallback) {
}
}, "browser-delayed-startup-finished", false);
}
function promiseDelayedStartupFinished(aWindow) {
return new Promise((resolve) => whenDelayedStartupFinished(aWindow, resolve));
}
/**
* The test runner that controls the execution flow of our tests.