Bug 604699 - Store thumbnails in the browser's image cache rather than in sessionstore [r=ian,sdwilsh, a=blocking2.0:final+]

(Based on patch from Sean Dunn <seanedunn@yahoo.com>.)
This commit is contained in:
Tim Taubert 2011-02-19 19:22:49 +01:00
Родитель 793df06f72
Коммит e4dfcc281c
9 изменённых файлов: 365 добавлений и 119 удалений

Просмотреть файл

@ -47,6 +47,8 @@ let Storage = {
GROUPS_DATA_IDENTIFIER: "tabview-groups",
TAB_DATA_IDENTIFIER: "tabview-tab",
UI_DATA_IDENTIFIER: "tabview-ui",
CACHE_CLIENT_IDENTIFIER: "tabview-cache",
CACHE_PREFIX: "moz-panorama:",
// ----------
// Function: init
@ -55,12 +57,28 @@ let Storage = {
this._sessionStore =
Cc["@mozilla.org/browser/sessionstore;1"].
getService(Ci.nsISessionStore);
// Create stream-based cache session for tabview
let cacheService =
Cc["@mozilla.org/network/cache-service;1"].
getService(Ci.nsICacheService);
this._cacheSession = cacheService.createSession(
this.CACHE_CLIENT_IDENTIFIER, Ci.nsICache.STORE_ON_DISK, true);
this.StringInputStream = Components.Constructor(
"@mozilla.org/io/string-input-stream;1", "nsIStringInputStream",
"setData");
this.StorageStream = Components.Constructor(
"@mozilla.org/storagestream;1", "nsIStorageStream",
"init");
},
// ----------
// Function: uninit
uninit: function Storage_uninit () {
this._sessionStore = null;
this._cacheSession = null;
this.StringInputStream = null;
this.StorageStream = null;
},
// ----------
@ -89,37 +107,196 @@ let Storage = {
}
},
// ----------
// Function: _openCacheEntry
// Opens a cache entry for the given <url> and requests access <access>.
// Calls <successCallback>(entry) when the entry was successfully opened with
// requested access rights. Otherwise calls <errorCallback>().
_openCacheEntry: function Storage__openCacheEntry(url, access, successCallback, errorCallback) {
let onCacheEntryAvailable = function (entry, accessGranted, status) {
if (entry && access == accessGranted && Components.isSuccessCode(status)) {
successCallback(entry);
} else {
entry && entry.close();
errorCallback();
}
}
let key = this.CACHE_PREFIX + url;
// switch to synchronous mode if parent window is about to close
if (UI.isDOMWindowClosing) {
let entry = this._cacheSession.openCacheEntry(key, access, true);
let status = Components.results.NS_OK;
onCacheEntryAvailable(entry, entry.accessGranted, status);
} else {
let listener = new CacheListener(onCacheEntryAvailable);
this._cacheSession.asyncOpenCacheEntry(key, access, listener);
}
},
// ----------
// Function: saveThumbnail
// Saves the <imageData> to the cache using the given <url> as key.
// Calls <callback>(status) when finished (passing true or false indicating
// whether the operation succeeded).
saveThumbnail: function Storage_saveThumbnail(url, imageData, callback) {
Utils.assert(url, "url");
Utils.assert(imageData, "imageData");
Utils.assert(typeof callback == "function", "callback arg must be a function");
let self = this;
let StringInputStream = this.StringInputStream;
let onCacheEntryAvailable = function (entry) {
let outputStream = entry.openOutputStream(0);
let cleanup = function () {
outputStream.close();
entry.close();
}
// switch to synchronous mode if parent window is about to close
if (UI.isDOMWindowClosing) {
outputStream.write(imageData, imageData.length);
cleanup();
callback(true);
return;
}
// asynchronous mode
let inputStream = new StringInputStream(imageData, imageData.length);
gNetUtil.asyncCopy(inputStream, outputStream, function (result) {
cleanup();
inputStream.close();
callback(Components.isSuccessCode(result));
});
}
let onCacheEntryUnavailable = function () {
callback(false);
}
this._openCacheEntry(url, Ci.nsICache.ACCESS_WRITE,
onCacheEntryAvailable, onCacheEntryUnavailable);
},
// ----------
// Function: loadThumbnail
// Asynchrously loads image data from the cache using the given <url> as key.
// Calls <callback>(status, data) when finished, passing true or false
// (indicating whether the operation succeeded) and the retrieved image data.
loadThumbnail: function Storage_loadThumbnail(url, callback) {
Utils.assert(url, "url");
Utils.assert(typeof callback == "function", "callback arg must be a function");
let self = this;
let onCacheEntryAvailable = function (entry) {
let imageChunks = [];
let nativeInputStream = entry.openInputStream(0);
const CHUNK_SIZE = 0x10000; // 65k
const PR_UINT32_MAX = 0xFFFFFFFF;
let storageStream = new self.StorageStream(CHUNK_SIZE, PR_UINT32_MAX, null);
let storageOutStream = storageStream.getOutputStream(0);
let cleanup = function () {
nativeInputStream.close();
storageStream.close();
storageOutStream.close();
entry.close();
}
gNetUtil.asyncCopy(nativeInputStream, storageOutStream, function (result) {
// cancel if parent window has already been closed
if (typeof UI == "undefined") {
cleanup();
return;
}
let imageData = null;
let isSuccess = Components.isSuccessCode(result);
if (isSuccess) {
let storageInStream = storageStream.newInputStream(0);
imageData = gNetUtil.readInputStreamToString(storageInStream,
storageInStream.available());
storageInStream.close();
}
cleanup();
callback(isSuccess, imageData);
});
}
let onCacheEntryUnavailable = function () {
callback(false);
}
this._openCacheEntry(url, Ci.nsICache.ACCESS_READ,
onCacheEntryAvailable, onCacheEntryUnavailable);
},
// ----------
// Function: saveTab
// Saves the data for a single tab.
saveTab: function Storage_saveTab(tab, data) {
Utils.assert(tab, "tab");
if (data != null) {
let imageData = data.imageData;
// Remove imageData from payload
delete data.imageData;
if (imageData != null) {
this.saveThumbnail(data.url, imageData, function (status) {
if (status) {
// Notify subscribers
tab._tabViewTabItem._sendToSubscribers("savedCachedImageData");
} else {
Utils.log("Error while saving thumbnail: " + e);
}
});
}
}
this._sessionStore.setTabValue(tab, this.TAB_DATA_IDENTIFIER,
JSON.stringify(data));
// Notify subscribers
if (data && data.imageData && tab._tabViewTabItem)
tab._tabViewTabItem._sendToSubscribers("savedImageData");
},
// ----------
// Function: getTabData
// Returns the data object associated with a single tab.
getTabData: function Storage_getTabData(tab) {
// Load tab data from session store and return it. Asynchrously loads the tab's
// thumbnail from the cache and calls <callback>(imageData) when done.
getTabData: function Storage_getTabData(tab, callback) {
Utils.assert(tab, "tab");
Utils.assert(typeof callback == "function", "callback arg must be a function");
let existingData = null;
var existingData = null;
try {
var tabData = this._sessionStore.getTabValue(tab, this.TAB_DATA_IDENTIFIER);
let tabData = this._sessionStore.getTabValue(tab, this.TAB_DATA_IDENTIFIER);
if (tabData != "") {
existingData = JSON.parse(tabData);
}
} catch (e) {
// getWindowValue will fail if the property doesn't exist
// getTabValue will fail if the property doesn't exist.
Utils.log(e);
}
if (existingData) {
this.loadThumbnail(existingData.url, function (status, imageData) {
if (status) {
callback(imageData);
// Notify subscribers
tab._tabViewTabItem._sendToSubscribers("loadedCachedImageData");
} else {
Utils.log("Error while loading thumbnail: " + e);
}
});
}
return existingData;
},
@ -224,3 +401,20 @@ let Storage = {
return existingData;
}
};
// ##########
// Class: CacheListener
// Generic CacheListener for feeding to asynchronous cache calls.
// Calls <callback>(entry, access, status) when the requested cache entry
// is available.
function CacheListener(callback) {
Utils.assert(typeof callback == "function", "callback arg must be a function");
this.callback = callback;
};
CacheListener.prototype = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsICacheListener]),
onCacheEntryAvailable: function (entry, access, status) {
this.callback(entry, access, status);
}
};

Просмотреть файл

@ -328,46 +328,55 @@ TabItem.prototype = Utils.extend(new Item(), new Subscribable(), {
Utils.assertThrow(!this._reconnected, "shouldn't already be reconnected");
Utils.assertThrow(this.tab, "should have a xul:tab");
let tabData = Storage.getTabData(this.tab);
if (tabData && TabItems.storageSanity(tabData)) {
if (this.parent)
this.parent.remove(this, {immediately: true});
let tabData = null;
let self = this;
let imageDataCb = function(imageData) {
Utils.assertThrow(tabData, "tabData");
tabData.imageData = imageData;
this.setBounds(tabData.bounds, true);
let currentUrl = self.tab.linkedBrowser.currentURI.spec;
// If we have a cached image, then show it if the loaded URL matches
// what the cache is from, OR the loaded URL is blank, which means
// that the page hasn't loaded yet.
if (tabData.imageData &&
(tabData.url == currentUrl || currentUrl == 'about:blank')) {
self.showCachedData(tabData);
}
};
// getTabData returns the sessionstore contents, but passes
// a callback to run when the thumbnail is finally loaded.
tabData = Storage.getTabData(this.tab, imageDataCb);
if (tabData && TabItems.storageSanity(tabData)) {
if (self.parent)
self.parent.remove(self, {immediately: true});
self.setBounds(tabData.bounds, true);
if (Utils.isPoint(tabData.userSize))
this.userSize = new Point(tabData.userSize);
self.userSize = new Point(tabData.userSize);
if (tabData.groupID) {
var groupItem = GroupItems.groupItem(tabData.groupID);
if (groupItem) {
groupItem.add(this, {immediately: true});
groupItem.add(self, {immediately: true});
// if it matches the selected tab or no active tab and the browser
// if it matches the selected tab or no active tab and the browser
// tab is hidden, the active group item would be set.
if (this.tab == gBrowser.selectedTab ||
(!GroupItems.getActiveGroupItem() && !this.tab.hidden))
GroupItems.setActiveGroupItem(this.parent);
if (self.tab == gBrowser.selectedTab ||
(!GroupItems.getActiveGroupItem() && !self.tab.hidden))
GroupItems.setActiveGroupItem(self.parent);
}
}
let currentUrl = this.tab.linkedBrowser.currentURI.spec;
// If we have a cached image, then show it if the loaded URL matches
// what the cache is from, OR the loaded URL is blank, which means
// that the page hasn't loaded yet.
if (tabData.imageData && (tabData.url == currentUrl ||
currentUrl == 'about:blank'))
this.showCachedData(tabData);
} else {
// create tab by double click is handled in UI_init().
if (!TabItems.creatingNewOrphanTab)
GroupItems.newTab(this, {immediately: true});
GroupItems.newTab(self, {immediately: true});
}
this._reconnected = true;
this.save();
this._sendToSubscribers("reconnected");
self._reconnected = true;
self.save();
self._sendToSubscribers("reconnected");
},
// ----------

Просмотреть файл

@ -23,6 +23,12 @@ XPCOMUtils.defineLazyGetter(this, "gPrivateBrowsing", function() {
getService(Ci.nsIPrivateBrowsingService);
});
XPCOMUtils.defineLazyGetter(this, "gNetUtil", function() {
var obj = {};
Cu.import("resource://gre/modules/NetUtil.jsm", obj);
return obj.NetUtil;
});
var gWindow = window.parent;
var gBrowser = gWindow.gBrowser;
var gTabView = gWindow.TabView;

Просмотреть файл

@ -127,6 +127,11 @@ let UI = {
// Used to keep track of how many calls to storageBusy vs storageReady.
_storageBusyCount: 0,
// Variable: isDOMWindowClosing
// Tells wether we already received the "domwindowclosed" event and the parent
// windows is about to close.
isDOMWindowClosing: false,
// ----------
// Function: init
// Must be called after the object is created.
@ -241,6 +246,7 @@ let UI = {
// ___ setup observer to save canvas images
function domWinClosedObserver(subject, topic, data) {
if (topic == "domwindowclosed" && subject == gWindow) {
self.isDOMWindowClosing = true;
if (self.isTabViewVisible())
GroupItems.removeHiddenGroups();
TabItems.saveAll(true);

Просмотреть файл

@ -78,6 +78,7 @@ _BROWSER_FILES = \
browser_tabview_bug600812.js \
browser_tabview_bug602432.js \
browser_tabview_bug604098.js \
browser_tabview_bug604699.js \
browser_tabview_bug606657.js \
browser_tabview_bug606905.js \
browser_tabview_bug608037.js \

Просмотреть файл

@ -1,42 +1,6 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is tabview bug 597248 test.
*
* The Initial Developer of the Original Code is
* Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2010
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Raymond Lee <raymond@appcoast.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
let newWin;
let restoredWin;
let newTabOne;
let newTabTwo;
let restoredNewTabOneLoaded = false;
@ -45,45 +9,31 @@ let frameInitialized = false;
function test() {
waitForExplicitFinish();
// open a new window
newWin = openDialog(getBrowserURL(), "_blank", "chrome,all,dialog=no");
newWin.addEventListener("load", function(event) {
newWin.removeEventListener("load", arguments.callee, false);
setupOne();
}, false);
newWindowWithTabView(setupOne);
}
function setupOne() {
let loadedCount = 0;
let allLoaded = function() {
if (++loadedCount == 2) {
newWin.addEventListener("tabviewshown", setupTwo, false);
newWin.TabView.toggle();
}
}
newTabOne = newWin.gBrowser.tabs[0];
newTabTwo = newWin.gBrowser.addTab();
load(newTabOne, "http://mochi.test:8888/browser/browser/base/content/test/tabview/search1.html", allLoaded);
load(newTabTwo, "http://mochi.test:8888/browser/browser/base/content/test/tabview/dummy_page.html", allLoaded);
function setupOne(win) {
win.TabView.firstUseExperienced = true;
win.gBrowser.addTab("http://mochi.test:8888/browser/browser/base/content/test/tabview/search1.html");
win.gBrowser.addTab("http://mochi.test:8888/browser/browser/base/content/test/tabview/dummy_page.html");
afterAllTabsLoaded(function () setupTwo(win), win);
}
function setupTwo() {
newWin.removeEventListener("tabviewshown", setupTwo, false);
let contentWindow = newWin.document.getElementById("tab-view").contentWindow;
function setupTwo(win) {
let contentWindow = win.TabView.getContentWindow();
let tabItems = contentWindow.TabItems.getItems();
is(tabItems.length, 2, "There should be 2 tab items before closing");
is(tabItems.length, 3, "There should be 3 tab items before closing");
let numTabsToSave = tabItems.length;
// force all canvases to update, and hook in imageData save detection
tabItems.forEach(function(tabItem) {
contentWindow.TabItems._update(tabItem.tab);
tabItem.addSubscriber(tabItem, "savedImageData", function(item) {
item.removeSubscriber(item, "savedImageData");
tabItem.addSubscriber(tabItem, "savedCachedImageData", function(item) {
item.removeSubscriber(item, "savedCachedImageData");
--numTabsToSave;
});
});
@ -93,13 +43,12 @@ function setupTwo() {
Services.obs.removeObserver(
xulWindowDestory, "xul-window-destroyed", false);
newWin = null;
// "xul-window-destroyed" is just fired just before a XUL window is
// destroyed so restore window and test it after a delay
executeSoon(function() {
restoredWin = undoCloseWindow();
restoredWin.addEventListener("load", function(event) {
restoredWin.removeEventListener("load", arguments.callee, false);
let restoredWin = undoCloseWindow();
restoredWin.addEventListener("load", function onLoad(event) {
restoredWin.removeEventListener("load", onLoad, false);
// ensure that closed tabs have been saved
is(numTabsToSave, 0, "All tabs were saved when window was closed.");
@ -122,7 +71,7 @@ function setupTwo() {
restoredWin.addEventListener(
"tabviewframeinitialized", onTabViewFrameInitialized, false);
is(restoredWin.gBrowser.tabs.length, 2, "The total number of tabs is 2");
is(restoredWin.gBrowser.tabs.length, 3, "The total number of tabs is 3");
/*
// bug 615954 happens too often so we disable this until we have a fix
@ -140,10 +89,10 @@ function setupTwo() {
Services.obs.addObserver(
xulWindowDestory, "xul-window-destroyed", false);
newWin.close();
win.close();
}
let gTabsProgressListener = {
/*let gTabsProgressListener = {
onStateChange: function(browser, webProgress, request, stateFlags, status) {
// ensure about:blank doesn't trigger the code
if ((stateFlags & Ci.nsIWebProgressListener.STATE_STOP) &&
@ -231,13 +180,4 @@ function updateAndCheck() {
// clean up and finish
restoredWin.close();
finish();
}
function load(tab, url, callback) {
tab.linkedBrowser.addEventListener("load", function (event) {
tab.linkedBrowser.removeEventListener("load", arguments.callee, true);
callback();
}, true);
tab.linkedBrowser.loadURI(url);
}
}*/

Просмотреть файл

@ -0,0 +1,81 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
function test() {
let url = "http://non.existant/url";
let cw;
let finishTest = function () {
is(1, gBrowser.tabs.length, "there is one tab, only");
ok(!TabView.isVisible(), "tabview is not visible");
finish();
}
waitForExplicitFinish();
let testErroneousLoading = function () {
cw.Storage.loadThumbnail(url, function (status, data) {
ok(!status, "thumbnail entry failed to load");
is(null, data, "no thumbnail data received");
next();
});
}
let testAsynchronousSaving = function () {
let saved = false;
let data = "thumbnail-data-asynchronous";
cw.Storage.saveThumbnail(url, data, function (status) {
ok(status, "thumbnail entry was saved");
ok(saved, "thumbnail was saved asynchronously");
cw.Storage.loadThumbnail(url, function (status, imageData) {
ok(status, "thumbnail entry was loaded");
is(imageData, data, "valid thumbnail data received");
next();
});
});
saved = true;
}
let testSynchronousSaving = function () {
let saved = false;
let data = "thumbnail-data-synchronous";
cw.UI.isDOMWindowClosing = true;
registerCleanupFunction(function () cw.UI.isDOMWindowClosing = false);
cw.Storage.saveThumbnail(url, data, function (status) {
ok(status, "thumbnail entry was saved");
ok(!saved, "thumbnail was saved synchronously");
cw.Storage.loadThumbnail(url, function (status, imageData) {
ok(status, "thumbnail entry was loaded");
is(imageData, data, "valid thumbnail data received");
cw.UI.isDOMWindowClosing = false;
next();
});
});
saved = true;
}
let tests = [testErroneousLoading, testAsynchronousSaving, testSynchronousSaving];
let next = function () {
let test = tests.shift();
if (test)
test();
else
hideTabView(finishTest);
}
showTabView(function () {
registerCleanupFunction(function () TabView.hide());
cw = TabView.getContentWindow();
next();
});
}

Просмотреть файл

@ -102,7 +102,8 @@ function test() {
gBrowser.loadOneTab('http://mochi.test:8888/', {inBackground: true});
afterAllTabsLoaded(function () {
duplicateTabIn(gBrowser.selectedTab, 'current');
// Valid choices for 'where' are window|tabshifted|tab
duplicateTabIn(gBrowser.selectedTab, 'tab');
afterAllTabsLoaded(function () {
assertNumberOfVisibleTabs(3);

Просмотреть файл

@ -19,10 +19,18 @@ function test() {
whenTabAttrModified(tab, function () {
tabItem = tab._tabViewTabItem;
cw.TabItems.resumeReconnecting();
ok(tabItem.isShowingCachedData(), 'tabItem shows cached data');
testChangeUrlAfterReconnect();
// Hook into loadedCachedImageData since loading cached thumbnails
// is asynchronous.
tabItem.addSubscriber(tabItem, "loadedCachedImageData", function(item) {
item.removeSubscriber(item, "loadedCachedImageData");
ok(tabItem.isShowingCachedData(), 'tabItem shows cached data');
testChangeUrlAfterReconnect();
});
cw.TabItems.resumeReconnecting();
});
});
}