mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Merge fx-team to m-c.
--HG-- rename : mobile/android/base/UpdateServiceHelper.java => mobile/android/base/updater/UpdateServiceHelper.java
This commit is contained in:
commit
cc6799fe38
@ -123,6 +123,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory",
|
||||
"resource:///modules/sessionstore/SessionHistory.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "_SessionFile",
|
||||
"resource:///modules/sessionstore/_SessionFile.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "TabStateCache",
|
||||
"resource:///modules/sessionstore/TabStateCache.jsm");
|
||||
|
||||
#ifdef MOZ_CRASHREPORTER
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter",
|
||||
@ -4179,126 +4181,6 @@ function TabData(obj = null) {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A cache for tabs data.
|
||||
*
|
||||
* This cache implements a weak map from tabs (as XUL elements)
|
||||
* to tab data (as instances of TabData).
|
||||
*
|
||||
* Note that we should never cache private data, as:
|
||||
* - that data is used very seldom by SessionStore;
|
||||
* - caching private data in addition to public data is memory consuming.
|
||||
*/
|
||||
let TabStateCache = {
|
||||
_data: new WeakMap(),
|
||||
|
||||
/**
|
||||
* Tells whether an entry is in the cache.
|
||||
*
|
||||
* @param {XULElement} aKey The tab or the associated browser.
|
||||
* @return {bool} Whether there's a cached entry for the given tab.
|
||||
*/
|
||||
has: function (aTab) {
|
||||
let key = this._normalizeToBrowser(aTab);
|
||||
return this._data.has(key);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add or replace an entry in the cache.
|
||||
*
|
||||
* @param {XULElement} aTab The key, which may be either a tab
|
||||
* or the corresponding browser. The binding will disappear
|
||||
* if the tab/browser is destroyed.
|
||||
* @param {TabData} aValue The data associated to |aTab|.
|
||||
*/
|
||||
set: function(aTab, aValue) {
|
||||
let key = this._normalizeToBrowser(aTab);
|
||||
if (!(aValue instanceof TabData)) {
|
||||
throw new TypeError("Attempting to cache a non TabData");
|
||||
}
|
||||
this._data.set(key, aValue);
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the tab data associated with a tab.
|
||||
*
|
||||
* @param {XULElement} aKey The tab or the associated browser.
|
||||
*
|
||||
* @return {TabData|undefined} The data if available, |undefined|
|
||||
* otherwise.
|
||||
*/
|
||||
get: function(aKey) {
|
||||
let key = this._normalizeToBrowser(aKey);
|
||||
let result = this._data.get(key);
|
||||
TabStateCacheTelemetry.recordAccess(!!result);
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete the tab data associated with a tab.
|
||||
*
|
||||
* @param {XULElement} aKey The tab or the associated browser.
|
||||
*
|
||||
* Noop of there is no tab data associated with the tab.
|
||||
*/
|
||||
delete: function(aKey) {
|
||||
let key = this._normalizeToBrowser(aKey);
|
||||
this._data.delete(key);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete all tab data.
|
||||
*/
|
||||
clear: function() {
|
||||
TabStateCacheTelemetry.recordClear();
|
||||
this._data.clear();
|
||||
},
|
||||
|
||||
/**
|
||||
* Update in place a piece of data.
|
||||
*
|
||||
* @param {XULElement} aKey The tab or the associated browser.
|
||||
* If the tab/browser is not present, do nothing.
|
||||
* @param {string} aField The field to update.
|
||||
* @param {*} aValue The new value to place in the field.
|
||||
*/
|
||||
updateField: function(aKey, aField, aValue) {
|
||||
let key = this._normalizeToBrowser(aKey);
|
||||
let data = this._data.get(key);
|
||||
if (data) {
|
||||
data[aField] = aValue;
|
||||
}
|
||||
TabStateCacheTelemetry.recordAccess(!!data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a given field from a cached tab state.
|
||||
*
|
||||
* @param {XULElement} aKey The tab or the associated browser.
|
||||
* If the tab/browser is not present, do nothing.
|
||||
* @param {string} aField The field to remove.
|
||||
*/
|
||||
removeField: function(aKey, aField) {
|
||||
let key = this._normalizeToBrowser(aKey);
|
||||
let data = this._data.get(key);
|
||||
if (data && aField in data) {
|
||||
delete data[aField];
|
||||
}
|
||||
TabStateCacheTelemetry.recordAccess(!!data);
|
||||
},
|
||||
|
||||
_normalizeToBrowser: function(aKey) {
|
||||
let nodeName = aKey.localName;
|
||||
if (nodeName == "tab") {
|
||||
return aKey.linkedBrowser;
|
||||
}
|
||||
if (nodeName == "browser") {
|
||||
return aKey;
|
||||
}
|
||||
throw new TypeError("Key is neither a tab nor a browser: " + nodeName);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Module that contains tab state collection methods.
|
||||
*/
|
||||
@ -4347,7 +4229,7 @@ let TabState = {
|
||||
let options = {omitSessionHistory: true,
|
||||
omitSessionStorage: true,
|
||||
omitDocShellCapabilities: true};
|
||||
let tabData = new TabData(this._collectBaseTabData(tab, options));
|
||||
let tabData = this._collectBaseTabData(tab, options);
|
||||
|
||||
// Apply collected data.
|
||||
tabData.entries = history.entries;
|
||||
@ -4401,7 +4283,7 @@ let TabState = {
|
||||
return TabStateCache.get(tab);
|
||||
}
|
||||
|
||||
let tabData = new TabData(this._collectBaseTabData(tab));
|
||||
let tabData = this._collectBaseTabData(tab);
|
||||
if (this._updateTextAndScrollDataForTab(tab, tabData)) {
|
||||
TabStateCache.set(tab, tabData);
|
||||
}
|
||||
@ -4715,68 +4597,4 @@ let TabState = {
|
||||
}
|
||||
};
|
||||
|
||||
let TabStateCacheTelemetry = {
|
||||
// Total number of hits during the session
|
||||
_hits: 0,
|
||||
// Total number of misses during the session
|
||||
_misses: 0,
|
||||
// Total number of clears during the session
|
||||
_clears: 0,
|
||||
// |true| once we have been initialized
|
||||
_initialized: false,
|
||||
|
||||
/**
|
||||
* Record a cache access.
|
||||
*
|
||||
* @param {boolean} isHit If |true|, the access was a hit, otherwise
|
||||
* a miss.
|
||||
*/
|
||||
recordAccess: function(isHit) {
|
||||
this._init();
|
||||
if (isHit) {
|
||||
++this._hits;
|
||||
} else {
|
||||
++this._misses;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Record a cache clear
|
||||
*/
|
||||
recordClear: function() {
|
||||
this._init();
|
||||
++this._clears;
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize the telemetry.
|
||||
*/
|
||||
_init: function() {
|
||||
if (this._initialized) {
|
||||
// Avoid double initialization
|
||||
return;
|
||||
}
|
||||
this._initialized = true;
|
||||
Services.obs.addObserver(this, "profile-before-change", false);
|
||||
},
|
||||
|
||||
observe: function() {
|
||||
Services.obs.removeObserver(this, "profile-before-change");
|
||||
|
||||
// Record hit/miss rate
|
||||
let accesses = this._hits + this._misses;
|
||||
if (accesses == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._fillHistogram("HIT_RATE", this._hits, accesses);
|
||||
this._fillHistogram("CLEAR_RATIO", this._clears, accesses);
|
||||
},
|
||||
|
||||
_fillHistogram: function(suffix, positive, total) {
|
||||
let PREFIX = "FX_SESSION_RESTORE_TABSTATECACHE_";
|
||||
let histo = Services.telemetry.getHistogramById(PREFIX + suffix);
|
||||
let rate = Math.floor( ( positive * 100 ) / total );
|
||||
histo.add(rate);
|
||||
}
|
||||
};
|
||||
|
292
browser/components/sessionstore/src/TabStateCache.jsm
Normal file
292
browser/components/sessionstore/src/TabStateCache.jsm
Normal file
@ -0,0 +1,292 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["TabStateCache"];
|
||||
|
||||
const Cu = Components.utils;
|
||||
Cu.import("resource://gre/modules/Services.jsm", this);
|
||||
|
||||
|
||||
/**
|
||||
* A cache for tabs data.
|
||||
*
|
||||
* This cache implements a weak map from tabs (as XUL elements)
|
||||
* to tab data (as objects).
|
||||
*
|
||||
* Note that we should never cache private data, as:
|
||||
* - that data is used very seldom by SessionStore;
|
||||
* - caching private data in addition to public data is memory consuming.
|
||||
*/
|
||||
this.TabStateCache = Object.freeze({
|
||||
/**
|
||||
* Tells whether an entry is in the cache.
|
||||
*
|
||||
* @param {XULElement} aKey The tab or the associated browser.
|
||||
* @return {bool} Whether there's a cached entry for the given tab.
|
||||
*/
|
||||
has: function (aTab) {
|
||||
return TabStateCacheInternal.has(aTab);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add or replace an entry in the cache.
|
||||
*
|
||||
* @param {XULElement} aTab The key, which may be either a tab
|
||||
* or the corresponding browser. The binding will disappear
|
||||
* if the tab/browser is destroyed.
|
||||
* @param {*} aValue The data associated to |aTab|.
|
||||
*/
|
||||
set: function(aTab, aValue) {
|
||||
return TabStateCacheInternal.set(aTab, aValue);
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the tab data associated with a tab.
|
||||
*
|
||||
* @param {XULElement} aKey The tab or the associated browser.
|
||||
*
|
||||
* @return {*|undefined} The data if available, |undefined|
|
||||
* otherwise.
|
||||
*/
|
||||
get: function(aKey) {
|
||||
return TabStateCacheInternal.get(aKey);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete the tab data associated with a tab.
|
||||
*
|
||||
* @param {XULElement} aKey The tab or the associated browser.
|
||||
*
|
||||
* Noop of there is no tab data associated with the tab.
|
||||
*/
|
||||
delete: function(aKey) {
|
||||
return TabStateCacheInternal.delete(aKey);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete all tab data.
|
||||
*/
|
||||
clear: function() {
|
||||
return TabStateCacheInternal.clear();
|
||||
},
|
||||
|
||||
/**
|
||||
* Update in place a piece of data.
|
||||
*
|
||||
* @param {XULElement} aKey The tab or the associated browser.
|
||||
* If the tab/browser is not present, do nothing.
|
||||
* @param {string} aField The field to update.
|
||||
* @param {*} aValue The new value to place in the field.
|
||||
*/
|
||||
updateField: function(aKey, aField, aValue) {
|
||||
return TabStateCacheInternal.updateField(aKey, aField, aValue);
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a given field from a cached tab state.
|
||||
*
|
||||
* @param {XULElement} aKey The tab or the associated browser.
|
||||
* If the tab/browser is not present, do nothing.
|
||||
* @param {string} aField The field to remove.
|
||||
*/
|
||||
removeField: function(aKey, aField) {
|
||||
return TabStateCacheInternal.removeField(aKey, aField);
|
||||
},
|
||||
|
||||
/**
|
||||
* Total number of cache hits during the session.
|
||||
*/
|
||||
get hits() {
|
||||
return TabStateCacheTelemetry.hits;
|
||||
},
|
||||
|
||||
/**
|
||||
* Total number of cache misses during the session.
|
||||
*/
|
||||
get misses() {
|
||||
return TabStateCacheTelemetry.misses;
|
||||
},
|
||||
|
||||
/**
|
||||
* Total number of cache clears during the session.
|
||||
*/
|
||||
get clears() {
|
||||
return TabStateCacheTelemetry.clears;
|
||||
},
|
||||
});
|
||||
|
||||
let TabStateCacheInternal = {
|
||||
_data: new WeakMap(),
|
||||
|
||||
/**
|
||||
* Tells whether an entry is in the cache.
|
||||
*
|
||||
* @param {XULElement} aKey The tab or the associated browser.
|
||||
* @return {bool} Whether there's a cached entry for the given tab.
|
||||
*/
|
||||
has: function (aTab) {
|
||||
let key = this._normalizeToBrowser(aTab);
|
||||
return this._data.has(key);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add or replace an entry in the cache.
|
||||
*
|
||||
* @param {XULElement} aTab The key, which may be either a tab
|
||||
* or the corresponding browser. The binding will disappear
|
||||
* if the tab/browser is destroyed.
|
||||
* @param {*} aValue The data associated to |aTab|.
|
||||
*/
|
||||
set: function(aTab, aValue) {
|
||||
let key = this._normalizeToBrowser(aTab);
|
||||
this._data.set(key, aValue);
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the tab data associated with a tab.
|
||||
*
|
||||
* @param {XULElement} aKey The tab or the associated browser.
|
||||
*
|
||||
* @return {*|undefined} The data if available, |undefined|
|
||||
* otherwise.
|
||||
*/
|
||||
get: function(aKey) {
|
||||
let key = this._normalizeToBrowser(aKey);
|
||||
let result = this._data.get(key);
|
||||
TabStateCacheTelemetry.recordAccess(!!result);
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete the tab data associated with a tab.
|
||||
*
|
||||
* @param {XULElement} aKey The tab or the associated browser.
|
||||
*
|
||||
* Noop of there is no tab data associated with the tab.
|
||||
*/
|
||||
delete: function(aKey) {
|
||||
let key = this._normalizeToBrowser(aKey);
|
||||
this._data.delete(key);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete all tab data.
|
||||
*/
|
||||
clear: function() {
|
||||
TabStateCacheTelemetry.recordClear();
|
||||
this._data.clear();
|
||||
},
|
||||
|
||||
/**
|
||||
* Update in place a piece of data.
|
||||
*
|
||||
* @param {XULElement} aKey The tab or the associated browser.
|
||||
* If the tab/browser is not present, do nothing.
|
||||
* @param {string} aField The field to update.
|
||||
* @param {*} aValue The new value to place in the field.
|
||||
*/
|
||||
updateField: function(aKey, aField, aValue) {
|
||||
let key = this._normalizeToBrowser(aKey);
|
||||
let data = this._data.get(key);
|
||||
if (data) {
|
||||
data[aField] = aValue;
|
||||
}
|
||||
TabStateCacheTelemetry.recordAccess(!!data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a given field from a cached tab state.
|
||||
*
|
||||
* @param {XULElement} aKey The tab or the associated browser.
|
||||
* If the tab/browser is not present, do nothing.
|
||||
* @param {string} aField The field to remove.
|
||||
*/
|
||||
removeField: function(aKey, aField) {
|
||||
let key = this._normalizeToBrowser(aKey);
|
||||
let data = this._data.get(key);
|
||||
if (data && aField in data) {
|
||||
delete data[aField];
|
||||
}
|
||||
TabStateCacheTelemetry.recordAccess(!!data);
|
||||
},
|
||||
|
||||
_normalizeToBrowser: function(aKey) {
|
||||
let nodeName = aKey.localName;
|
||||
if (nodeName == "tab") {
|
||||
return aKey.linkedBrowser;
|
||||
}
|
||||
if (nodeName == "browser") {
|
||||
return aKey;
|
||||
}
|
||||
throw new TypeError("Key is neither a tab nor a browser: " + nodeName);
|
||||
}
|
||||
};
|
||||
|
||||
let TabStateCacheTelemetry = {
|
||||
// Total number of hits during the session
|
||||
hits: 0,
|
||||
// Total number of misses during the session
|
||||
misses: 0,
|
||||
// Total number of clears during the session
|
||||
clears: 0,
|
||||
// |true| once we have been initialized
|
||||
_initialized: false,
|
||||
|
||||
/**
|
||||
* Record a cache access.
|
||||
*
|
||||
* @param {boolean} isHit If |true|, the access was a hit, otherwise
|
||||
* a miss.
|
||||
*/
|
||||
recordAccess: function(isHit) {
|
||||
this._init();
|
||||
if (isHit) {
|
||||
++this.hits;
|
||||
} else {
|
||||
++this.misses;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Record a cache clear
|
||||
*/
|
||||
recordClear: function() {
|
||||
this._init();
|
||||
++this.clears;
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize the telemetry.
|
||||
*/
|
||||
_init: function() {
|
||||
if (this._initialized) {
|
||||
// Avoid double initialization
|
||||
return;
|
||||
}
|
||||
this._initialized = true;
|
||||
Services.obs.addObserver(this, "profile-before-change", false);
|
||||
},
|
||||
|
||||
observe: function() {
|
||||
Services.obs.removeObserver(this, "profile-before-change");
|
||||
|
||||
// Record hit/miss rate
|
||||
let accesses = this.hits + this.misses;
|
||||
if (accesses == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._fillHistogram("HIT_RATE", this.hits, accesses);
|
||||
this._fillHistogram("CLEAR_RATIO", this.clears, accesses);
|
||||
},
|
||||
|
||||
_fillHistogram: function(suffix, positive, total) {
|
||||
let PREFIX = "FX_SESSION_RESTORE_TABSTATECACHE_";
|
||||
let histo = Services.telemetry.getHistogramById(PREFIX + suffix);
|
||||
let rate = Math.floor( ( positive * 100 ) / total );
|
||||
histo.add(rate);
|
||||
}
|
||||
};
|
@ -22,6 +22,7 @@ EXTRA_JS_MODULES = [
|
||||
'SessionMigration.jsm',
|
||||
'SessionStorage.jsm',
|
||||
'SessionWorker.js',
|
||||
'TabStateCache.jsm',
|
||||
'XPathGenerator.jsm',
|
||||
'_SessionFile.jsm',
|
||||
]
|
||||
|
@ -20,6 +20,7 @@ MOCHITEST_BROWSER_FILES = \
|
||||
browser_input_sample.html \
|
||||
browser_pageshow.js \
|
||||
browser_sessionStorage.js \
|
||||
browser_tabStateCache.js \
|
||||
browser_upgrade_backup.js \
|
||||
browser_windowRestore_perwindowpb.js \
|
||||
browser_248970_b_perwindowpb.js \
|
||||
|
@ -7,26 +7,6 @@ Cu.import("resource://gre/modules/Task.jsm", Scope);
|
||||
Cu.import("resource://gre/modules/Promise.jsm", Scope);
|
||||
let {Task, Promise} = Scope;
|
||||
|
||||
function promiseBrowserLoaded(aBrowser) {
|
||||
let deferred = Promise.defer();
|
||||
whenBrowserLoaded(aBrowser, () => deferred.resolve());
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function forceWriteState() {
|
||||
let deferred = Promise.defer();
|
||||
const PREF = "browser.sessionstore.interval";
|
||||
const TOPIC = "sessionstore-state-write";
|
||||
|
||||
Services.obs.addObserver(function observe() {
|
||||
Services.obs.removeObserver(observe, TOPIC);
|
||||
Services.prefs.clearUserPref(PREF);
|
||||
deferred.resolve();
|
||||
}, TOPIC, false);
|
||||
|
||||
Services.prefs.setIntPref(PREF, 0);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function waitForStorageChange(aTab) {
|
||||
let deferred = Promise.defer();
|
||||
@ -57,7 +37,7 @@ function test() {
|
||||
// Flush loading and next save, call getBrowserState()
|
||||
// a few times to ensure that everything is cached.
|
||||
yield promiseBrowserLoaded(tab.linkedBrowser);
|
||||
yield forceWriteState();
|
||||
yield forceSaveState();
|
||||
info("Calling getBrowserState() to populate cache");
|
||||
ss.getBrowserState();
|
||||
|
||||
@ -66,7 +46,7 @@ function test() {
|
||||
win.sessionStorage[SESSION_STORAGE_KEY] = SESSION_STORAGE_VALUE;
|
||||
let storageChanged = yield storageChangedPromise;
|
||||
ok(storageChanged, "Changing sessionStorage triggered the right message");
|
||||
yield forceWriteState();
|
||||
yield forceSaveState();
|
||||
|
||||
let state = ss.getBrowserState();
|
||||
ok(state.indexOf(SESSION_STORAGE_KEY) != -1, "Key appears in state");
|
||||
@ -78,7 +58,7 @@ function test() {
|
||||
win.localStorage[LOCAL_STORAGE_KEY] = LOCAL_STORAGE_VALUE;
|
||||
storageChanged = yield storageChangedPromise;
|
||||
ok(!storageChanged, "Changing localStorage did not trigger a message");
|
||||
yield forceWriteState();
|
||||
yield forceSaveState();
|
||||
|
||||
state = ss.getBrowserState();
|
||||
ok(state.indexOf(LOCAL_STORAGE_KEY) == -1, "Key does not appear in state");
|
||||
|
141
browser/components/sessionstore/test/browser_tabStateCache.js
Normal file
141
browser/components/sessionstore/test/browser_tabStateCache.js
Normal file
@ -0,0 +1,141 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
let wrapper = {};
|
||||
Cu.import("resource:///modules/sessionstore/TabStateCache.jsm", wrapper);
|
||||
let {TabStateCache} = wrapper;
|
||||
|
||||
// The number of tabs that are present in the browser but that we're not dealing
|
||||
// with. This should be one (for an empty about:blank), but let's not make this
|
||||
// a magic number.
|
||||
let numberOfUntrackedTabs;
|
||||
|
||||
// Arbitrary URL prefix, used to generate the URL of pages we visit
|
||||
const URL_PREFIX = "http://example.org:80/";
|
||||
|
||||
/**
|
||||
* Check tab state cache telemetry statistics before and after an operation.
|
||||
*
|
||||
* @param {function} f The operation being measured. If it returns a promise,
|
||||
* we wait until the promise is resolved before proceeding.
|
||||
* @return {promise}
|
||||
*/
|
||||
function getTelemetryDelta(f) {
|
||||
return Task.spawn(function() {
|
||||
let KEYS = ["hits", "misses", "clears"];
|
||||
let old = {};
|
||||
for (let key of KEYS) {
|
||||
old[key] = TabStateCache[key];
|
||||
}
|
||||
yield f();
|
||||
let result = {};
|
||||
for (let key of KEYS) {
|
||||
result[key] = TabStateCache[key] - old[key];
|
||||
}
|
||||
ok(result.hits >= 0, "Sanity check: hits have not decreased");
|
||||
ok(result.misses >= 0, "Sanity check: misses have not decreased");
|
||||
ok(result.clears >= 0, "Sanity check: clears have not decreased");
|
||||
throw new Task.Result(result);
|
||||
});
|
||||
}
|
||||
|
||||
add_task(function init() {
|
||||
// Start with an empty cache
|
||||
closeAllButPrimaryWindow();
|
||||
TabStateCache.clear();
|
||||
numberOfUntrackedTabs = gBrowser.tabs.length;
|
||||
info("Starting with " + numberOfUntrackedTabs + " tabs");
|
||||
});
|
||||
|
||||
add_task(function add_remove() {
|
||||
info("Adding the first tab");
|
||||
// Initialize one tab, save to initialize cache
|
||||
let tab1 = gBrowser.addTab(URL_PREFIX + "?tab1");
|
||||
yield promiseBrowserLoaded(tab1.linkedBrowser);
|
||||
yield getTelemetryDelta(forceSaveState);
|
||||
|
||||
// Save/collect again a few times, ensure that we always hit
|
||||
info("Save/collect a few times with one tab");
|
||||
for (let collector of [forceSaveState, ss.getBrowserState]) {
|
||||
for (let i = 0; i < 5; ++i) {
|
||||
let PREFIX = "Trivial test " + i + " using " + collector.name + ": ";
|
||||
let delta = yield getTelemetryDelta(collector);
|
||||
is(delta.hits, numberOfUntrackedTabs + 1, PREFIX + " has at least one hit " + delta.hits);
|
||||
is(delta.misses, 0, PREFIX + " has no miss");
|
||||
is(delta.clears, 0, PREFIX + " has no clear");
|
||||
}
|
||||
}
|
||||
|
||||
// Add a second tab, ensure that we have both hits and misses
|
||||
info("Adding the second tab");
|
||||
let tab2 = gBrowser.addTab(URL_PREFIX + "?tab2");
|
||||
yield promiseBrowserLoaded(tab2.linkedBrowser);
|
||||
|
||||
let PREFIX = "Adding second tab: ";
|
||||
ok(!TabStateCache.has(tab2), PREFIX + " starts out of the cache");
|
||||
let delta = yield getTelemetryDelta(forceSaveState);
|
||||
is(delta.hits, numberOfUntrackedTabs + 2, PREFIX + " we hit all tabs, thanks to prefetching");
|
||||
is(delta.misses, 0, PREFIX + " we missed no tabs, thanks to prefetching");
|
||||
is(delta.clears, 0, PREFIX + " has no clear");
|
||||
|
||||
// Save/collect again a few times, ensure that we always hit
|
||||
info("Save/collect a few times with two tabs");
|
||||
for (let collector of [forceSaveState, ss.getBrowserState]) {
|
||||
for (let i = 0; i < 5; ++i) {
|
||||
let PREFIX = "With two tabs " + i + " using " + collector.name + ": ";
|
||||
let delta = yield getTelemetryDelta(collector);
|
||||
is(delta.hits, numberOfUntrackedTabs + 2, PREFIX + " both tabs hit");
|
||||
is(delta.misses, 0, PREFIX + " has no miss");
|
||||
is(delta.clears, 0, PREFIX + " has no clear");
|
||||
}
|
||||
}
|
||||
|
||||
info("Removing second tab");
|
||||
gBrowser.removeTab(tab2);
|
||||
PREFIX = "Removing second tab: ";
|
||||
delta = yield getTelemetryDelta(forceSaveState);
|
||||
is(delta.hits, numberOfUntrackedTabs + 1, PREFIX + " we hit for one tab");
|
||||
is(delta.misses, 0, PREFIX + " has no miss");
|
||||
is(delta.clears, 0, PREFIX + " has no clear");
|
||||
|
||||
info("Removing first tab");
|
||||
gBrowser.removeTab(tab1);
|
||||
});
|
||||
|
||||
add_task(function browsing() {
|
||||
info("Opening first browsing tab");
|
||||
let tab1 = gBrowser.addTab(URL_PREFIX + "?do_not_move_from_here");
|
||||
let browser1 = tab1.linkedBrowser;
|
||||
yield promiseBrowserLoaded(browser1);
|
||||
yield forceSaveState();
|
||||
|
||||
info("Opening second browsing tab");
|
||||
let tab2 = gBrowser.addTab(URL_PREFIX + "?start_browsing_from_here");
|
||||
let browser2 = tab2.linkedBrowser;
|
||||
yield promiseBrowserLoaded(browser2);
|
||||
|
||||
for (let i = 0; i < 4; ++i) {
|
||||
let url = URL_PREFIX + "?browsing" + i; // Arbitrary url, easy to recognize
|
||||
let PREFIX = "Browsing to " + url;
|
||||
info(PREFIX);
|
||||
let delta = yield getTelemetryDelta(function() {
|
||||
return Task.spawn(function() {
|
||||
// Move to new URI then save session
|
||||
let promise = promiseBrowserLoaded(browser2);
|
||||
browser2.loadURI(url);
|
||||
yield promise;
|
||||
ok(!TabStateCache.has(browser2), PREFIX + " starts out of the cache");
|
||||
yield forceSaveState();
|
||||
});
|
||||
});
|
||||
is(delta.hits, numberOfUntrackedTabs + 2, PREFIX + " has all hits, thanks to prefetching");
|
||||
is(delta.misses, 0, PREFIX + " has no miss, thanks to prefetching");
|
||||
is(delta.clears, 0, PREFIX + " has no clear");
|
||||
}
|
||||
gBrowser.removeTab(tab2);
|
||||
gBrowser.removeTab(tab1);
|
||||
});
|
||||
|
||||
|
@ -240,6 +240,34 @@ function waitForSaveState(aCallback) {
|
||||
Services.prefs.getIntPref("browser.sessionstore.interval");
|
||||
return waitForTopic("sessionstore-state-write", timeout, aCallback);
|
||||
}
|
||||
function promiseSaveState() {
|
||||
let deferred = Promise.defer();
|
||||
waitForSaveState(isSuccessful => {
|
||||
if (isSuccessful) {
|
||||
deferred.resolve();
|
||||
} else {
|
||||
deferred.reject(new Error("timeout"));
|
||||
}});
|
||||
return deferred.promise;
|
||||
}
|
||||
function forceSaveState() {
|
||||
let promise = promiseSaveState();
|
||||
const PREF = "browser.sessionstore.interval";
|
||||
// Set interval to an arbitrary non-0 duration
|
||||
// to ensure that setting it to 0 will notify observers
|
||||
Services.prefs.setIntPref(PREF, 1000);
|
||||
Services.prefs.setIntPref(PREF, 0);
|
||||
return promise.then(
|
||||
function onSuccess(x) {
|
||||
Services.prefs.clearUserPref(PREF);
|
||||
return x;
|
||||
},
|
||||
function onError(x) {
|
||||
Services.prefs.clearUserPref(PREF);
|
||||
throw x;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function whenBrowserLoaded(aBrowser, aCallback = next) {
|
||||
aBrowser.addEventListener("load", function onLoad() {
|
||||
@ -247,6 +275,22 @@ function whenBrowserLoaded(aBrowser, aCallback = next) {
|
||||
executeSoon(aCallback);
|
||||
}, true);
|
||||
}
|
||||
function promiseBrowserLoaded(aBrowser) {
|
||||
let deferred = Promise.defer();
|
||||
whenBrowserLoaded(aBrowser, deferred.resolve);
|
||||
return deferred.promise;
|
||||
}
|
||||
function whenBrowserUnloaded(aBrowser, aContainer, aCallback = next) {
|
||||
aBrowser.addEventListener("unload", function onUnload() {
|
||||
aBrowser.removeEventListener("unload", onUnload, true);
|
||||
executeSoon(aCallback);
|
||||
}, true);
|
||||
}
|
||||
function promiseBrowserUnloaded(aBrowser, aContainer) {
|
||||
let deferred = Promise.defer();
|
||||
whenBrowserUnloaded(aBrowser, aContainer, deferred.resolve);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function whenWindowLoaded(aWindow, aCallback = next) {
|
||||
aWindow.addEventListener("load", function windowLoadListener() {
|
||||
|
@ -75,6 +75,38 @@ struct Paths {
|
||||
nsString tmpDir;
|
||||
nsString profileDir;
|
||||
nsString localProfileDir;
|
||||
/**
|
||||
* The user's home directory
|
||||
*/
|
||||
nsString homeDir;
|
||||
/**
|
||||
* The user's desktop directory, if there is one. Otherwise this is
|
||||
* the same as homeDir.
|
||||
*/
|
||||
nsString desktopDir;
|
||||
|
||||
#if defined(XP_WIN)
|
||||
/**
|
||||
* The user's application data directory.
|
||||
*/
|
||||
nsString winAppDataDir;
|
||||
/**
|
||||
* The programs subdirectory in the user's start menu directory.
|
||||
*/
|
||||
nsString winStartMenuProgsDir;
|
||||
#endif // defined(XP_WIN)
|
||||
|
||||
#if defined(XP_MACOSX)
|
||||
/**
|
||||
* The user's Library directory.
|
||||
*/
|
||||
nsString macUserLibDir;
|
||||
/**
|
||||
* The Application directory, that stores applications installed in the
|
||||
* system.
|
||||
*/
|
||||
nsString macLocalApplicationsDir;
|
||||
#endif // defined(XP_MACOSX)
|
||||
|
||||
Paths()
|
||||
{
|
||||
@ -82,6 +114,18 @@ struct Paths {
|
||||
tmpDir.SetIsVoid(true);
|
||||
profileDir.SetIsVoid(true);
|
||||
localProfileDir.SetIsVoid(true);
|
||||
homeDir.SetIsVoid(true);
|
||||
desktopDir.SetIsVoid(true);
|
||||
|
||||
#if defined(XP_WIN)
|
||||
winAppDataDir.SetIsVoid(true);
|
||||
winStartMenuProgsDir.SetIsVoid(true);
|
||||
#endif // defined(XP_WIN)
|
||||
|
||||
#if defined(XP_MACOSX)
|
||||
macUserLibDir.SetIsVoid(true);
|
||||
macLocalApplicationsDir.SetIsVoid(true);
|
||||
#endif // defined(XP_MACOSX)
|
||||
}
|
||||
};
|
||||
|
||||
@ -209,6 +253,18 @@ nsresult InitOSFileConstants()
|
||||
// some platforms or in non-Firefox embeddings of Gecko).
|
||||
|
||||
GetPathToSpecialDir(NS_OS_TEMP_DIR, paths->tmpDir);
|
||||
GetPathToSpecialDir(NS_OS_HOME_DIR, paths->homeDir);
|
||||
GetPathToSpecialDir(NS_OS_DESKTOP_DIR, paths->desktopDir);
|
||||
|
||||
#if defined(XP_WIN)
|
||||
GetPathToSpecialDir(NS_WIN_APPDATA_DIR, paths->winAppDataDir);
|
||||
GetPathToSpecialDir(NS_WIN_PROGRAMS_DIR, paths->winStartMenuProgsDir);
|
||||
#endif // defined(XP_WIN)
|
||||
|
||||
#if defined(XP_MACOSX)
|
||||
GetPathToSpecialDir(NS_MAC_USER_LIB_DIR, paths->macUserLibDir);
|
||||
GetPathToSpecialDir(NS_OSX_LOCAL_APPLICATIONS_DIR, paths->macLocalApplicationsDir);
|
||||
#endif // defined(XP_MACOSX)
|
||||
|
||||
gPaths = paths.forget();
|
||||
return NS_OK;
|
||||
@ -769,6 +825,34 @@ bool DefineOSFileConstants(JSContext *cx, JS::Handle<JSObject*> global)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SetStringProperty(cx, objPath, "homeDir", gPaths->homeDir)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SetStringProperty(cx, objPath, "desktopDir", gPaths->desktopDir)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined(XP_WIN)
|
||||
if (!SetStringProperty(cx, objPath, "winAppDataDir", gPaths->winAppDataDir)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SetStringProperty(cx, objPath, "winStartMenuProgsDir", gPaths->winStartMenuProgsDir)) {
|
||||
return false;
|
||||
}
|
||||
#endif // defined(XP_WIN)
|
||||
|
||||
#if defined(XP_MACOSX)
|
||||
if (!SetStringProperty(cx, objPath, "macUserLibDir", gPaths->macUserLibDir)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SetStringProperty(cx, objPath, "macLocalApplicationsDir", gPaths->macLocalApplicationsDir)) {
|
||||
return false;
|
||||
}
|
||||
#endif // defined(XP_MACOSX)
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -835,18 +835,24 @@ abstract public class GeckoApp
|
||||
void showButtonToast(final String message, final String buttonText,
|
||||
final String buttonIcon, final String buttonId) {
|
||||
BitmapUtils.getDrawable(GeckoApp.this, buttonIcon, new BitmapUtils.BitmapLoader() {
|
||||
public void onBitmapFound(Drawable d) {
|
||||
mToast.show(false, message, buttonText, d, new ButtonToast.ToastListener() {
|
||||
@Override
|
||||
public void onButtonClicked() {
|
||||
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Toast:Click", buttonId));
|
||||
}
|
||||
public void onBitmapFound(final Drawable d) {
|
||||
|
||||
ThreadUtils.postToUiThread(new Runnable() {
|
||||
@Override
|
||||
public void onToastHidden(ButtonToast.ReasonHidden reason) {
|
||||
if (reason == ButtonToast.ReasonHidden.TIMEOUT) {
|
||||
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Toast:Hidden", buttonId));
|
||||
}
|
||||
public void run() {
|
||||
mToast.show(false, message, buttonText, d, new ButtonToast.ToastListener() {
|
||||
@Override
|
||||
public void onButtonClicked() {
|
||||
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Toast:Click", buttonId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onToastHidden(ButtonToast.ReasonHidden reason) {
|
||||
if (reason == ButtonToast.ReasonHidden.TIMEOUT) {
|
||||
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Toast:Hidden", buttonId));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -157,7 +157,6 @@ FENNEC_JAVA_FILES = \
|
||||
TextSelectionHandle.java \
|
||||
ThumbnailHelper.java \
|
||||
TouchEventInterceptor.java \
|
||||
UpdateServiceHelper.java \
|
||||
VideoPlayer.java \
|
||||
WebAppAllocator.java \
|
||||
WebAppImpl.java \
|
||||
@ -253,6 +252,8 @@ FENNEC_JAVA_FILES = \
|
||||
menu/MenuPopup.java \
|
||||
preferences/SearchPreferenceCategory.java \
|
||||
preferences/SearchEnginePreference.java \
|
||||
updater/UpdateServiceHelper.java \
|
||||
updater/UpdateService.java \
|
||||
widget/ActivityChooserModel.java \
|
||||
widget/ButtonToast.java \
|
||||
widget/ArrowPopup.java \
|
||||
@ -267,7 +268,6 @@ FENNEC_JAVA_FILES = \
|
||||
widget/TwoWayView.java \
|
||||
GeckoNetworkManager.java \
|
||||
GeckoScreenOrientationListener.java \
|
||||
UpdateService.java \
|
||||
GeckoUpdateReceiver.java \
|
||||
ReferrerReceiver.java \
|
||||
$(NULL)
|
||||
|
@ -30,7 +30,8 @@ let WebAppRT = {
|
||||
// Set a future policy version to avoid the telemetry prompt.
|
||||
pref("toolkit.telemetry.prompted", 999),
|
||||
pref("toolkit.telemetry.notifiedOptOut", 999),
|
||||
pref("media.useAudioChannelService", true)
|
||||
pref("media.useAudioChannelService", true),
|
||||
pref("dom.mozTCPSocket.enabled", true),
|
||||
],
|
||||
|
||||
init: function(aStatus, aUrl, aCallback) {
|
||||
|
@ -4154,7 +4154,7 @@ var BrowserEventHandler = {
|
||||
if (this._scrollableElement != null) {
|
||||
// Discard if it's the top-level scrollable, we let Java handle this
|
||||
let doc = BrowserApp.selectedBrowser.contentDocument;
|
||||
if (this._scrollableElement != doc.body && this._scrollableElement != doc.documentElement)
|
||||
if (this._scrollableElement != doc.documentElement)
|
||||
sendMessageToJava({ type: "Panning:Override" });
|
||||
}
|
||||
}
|
||||
@ -4241,7 +4241,6 @@ var BrowserEventHandler = {
|
||||
|
||||
let doc = BrowserApp.selectedBrowser.contentDocument;
|
||||
if (this._scrollableElement == null ||
|
||||
this._scrollableElement == doc.body ||
|
||||
this._scrollableElement == doc.documentElement) {
|
||||
sendMessageToJava({ type: "Panning:CancelOverride" });
|
||||
return;
|
||||
@ -4612,15 +4611,14 @@ var BrowserEventHandler = {
|
||||
let scrollable = false;
|
||||
while (elem) {
|
||||
/* Element is scrollable if its scroll-size exceeds its client size, and:
|
||||
* - It has overflow 'auto' or 'scroll'
|
||||
* - It's a textarea
|
||||
* - It's an HTML/BODY node
|
||||
* - It has overflow 'auto' or 'scroll', or
|
||||
* - It's a textarea or HTML node, or
|
||||
* - It's a select element showing multiple rows
|
||||
*/
|
||||
if (checkElem) {
|
||||
if ((elem.scrollTopMax > 0 || elem.scrollLeftMax > 0) &&
|
||||
(this._hasScrollableOverflow(elem) ||
|
||||
elem.mozMatchesSelector("html, body, textarea")) ||
|
||||
elem.mozMatchesSelector("html, textarea")) ||
|
||||
(elem instanceof HTMLSelectElement && (elem.size > 1 || elem.multiple))) {
|
||||
scrollable = true;
|
||||
break;
|
||||
@ -6539,6 +6537,13 @@ var SearchEngines = {
|
||||
prompted: Services.prefs.getBoolPref(this.PREF_SUGGEST_PROMPTED)
|
||||
}
|
||||
});
|
||||
|
||||
// Send a speculative connection to the default engine.
|
||||
let connector = Services.io.QueryInterface(Ci.nsISpeculativeConnect);
|
||||
let searchURI = Services.search.defaultEngine.getSubmission("dummy").uri;
|
||||
let callbacks = window.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsILoadContext);
|
||||
connector.speculativeConnect(searchURI, callbacks);
|
||||
},
|
||||
|
||||
_handleSearchEnginesGetAll: function _handleSearchEnginesGetAll(rv) {
|
||||
|
@ -101,6 +101,10 @@ let HomeBanner = {
|
||||
if (Object.keys(this._messages).length == 1) {
|
||||
Services.obs.addObserver(this, "HomeBanner:Get", false);
|
||||
Services.obs.addObserver(this, "HomeBanner:Click", false);
|
||||
|
||||
// Send a message to Java, in case there's an active HomeBanner
|
||||
// waiting for a response.
|
||||
this._handleGet();
|
||||
}
|
||||
|
||||
return message.id;
|
||||
|
@ -483,6 +483,9 @@ pref("toolkit.telemetry.debugSlowSql", false);
|
||||
pref("toolkit.identity.enabled", false);
|
||||
pref("toolkit.identity.debug", false);
|
||||
|
||||
// AsyncShutdown delay before crashing in case of shutdown freeze
|
||||
pref("toolkit.asyncshutdown.timeout.crash", 60000);
|
||||
|
||||
// Enable deprecation warnings.
|
||||
pref("devtools.errorconsole.deprecation_warnings", true);
|
||||
|
||||
|
@ -22,16 +22,6 @@ let main = this;
|
||||
function test() {
|
||||
info("test_osfile_front.xul: Starting test");
|
||||
|
||||
// Test OS.Constants.Path
|
||||
|
||||
Components.classes["@mozilla.org/net/osfileconstantsservice;1"].
|
||||
getService(Components.interfaces.nsIOSFileConstantsService).
|
||||
init();
|
||||
Components.utils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
is(OS.Constants.Path.tmpDir, Services.dirsvc.get("TmpD", Components.interfaces.nsIFile).path, "OS.Constants.Path.tmpDir is correct");
|
||||
is(OS.Constants.Path.profileDir, Services.dirsvc.get("ProfD", Components.interfaces.nsIFile).path, "OS.Constants.Path.profileDir is correct");
|
||||
|
||||
// Test the OS.File worker
|
||||
|
||||
worker = new ChromeWorker("worker_test_osfile_front.js");
|
||||
|
@ -0,0 +1,45 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
Components.utils.import("resource://gre/modules/osfile.jsm");
|
||||
Components.utils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
function compare_paths(ospath, key) {
|
||||
let file;
|
||||
try {
|
||||
file = Services.dirsvc.get(key, Components.interfaces.nsIFile);
|
||||
} catch(ex) {}
|
||||
|
||||
if (file) {
|
||||
do_check_true(!!ospath);
|
||||
do_check_eq(ospath, file.path);
|
||||
} else {
|
||||
do_check_false(!!ospath);
|
||||
}
|
||||
}
|
||||
|
||||
add_test(function() {
|
||||
do_check_true(!!OS.Constants.Path.tmpDir);
|
||||
do_check_eq(OS.Constants.Path.tmpDir, Services.dirsvc.get("TmpD", Components.interfaces.nsIFile).path);
|
||||
|
||||
do_check_true(!!OS.Constants.Path.homeDir);
|
||||
do_check_eq(OS.Constants.Path.homeDir, Services.dirsvc.get("Home", Components.interfaces.nsIFile).path);
|
||||
|
||||
do_check_true(!!OS.Constants.Path.desktopDir);
|
||||
do_check_eq(OS.Constants.Path.desktopDir, Services.dirsvc.get("Desk", Components.interfaces.nsIFile).path);
|
||||
|
||||
compare_paths(OS.Constants.Path.winAppDataDir, "AppData");
|
||||
compare_paths(OS.Constants.Path.winStartMenuProgsDir, "Progs");
|
||||
|
||||
compare_paths(OS.Constants.Path.macUserLibDir, "ULibDir");
|
||||
compare_paths(OS.Constants.Path.macLocalApplicationsDir, "LocApp");
|
||||
|
||||
run_next_test();
|
||||
});
|
@ -8,3 +8,4 @@ tail =
|
||||
[test_profiledir.js]
|
||||
[test_logging.js]
|
||||
[test_creationDate.js]
|
||||
[test_path_constants.js]
|
||||
|
32
toolkit/crashreporter/test/unit/test_crash_AsyncShutdown.js
Normal file
32
toolkit/crashreporter/test/unit/test_crash_AsyncShutdown.js
Normal file
@ -0,0 +1,32 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
function setup_crash() {
|
||||
Components.utils.import("resource://gre/modules/AsyncShutdown.jsm", this);
|
||||
Components.utils.import("resource://gre/modules/Services.jsm", this);
|
||||
Components.utils.import("resource://gre/modules/Promise.jsm", this);
|
||||
|
||||
Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true);
|
||||
Services.prefs.setIntPref("toolkit.asyncshutdown.crash_timeout", 10);
|
||||
|
||||
let TOPIC = "testing-async-shutdown-crash";
|
||||
let phase = AsyncShutdown._getPhase(TOPIC);
|
||||
phase.addBlocker("A blocker that is never satisfied", function() {
|
||||
dump("Installing blocker\n");
|
||||
let deferred = Promise.defer();
|
||||
return deferred.promise;
|
||||
});
|
||||
|
||||
Services.obs.notifyObservers(null, TOPIC, null);
|
||||
dump("Waiting for crash\n");
|
||||
}
|
||||
|
||||
function after_crash(mdump, extra) {
|
||||
let info = JSON.parse(extra.AsyncShutdownTimeout);
|
||||
do_check_true(info.phase == "testing-async-shutdown-crash");
|
||||
do_check_true(info.conditions.indexOf("A blocker that is never satisfied") != -1);
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
do_crash(setup_crash, after_crash);
|
||||
}
|
@ -24,3 +24,5 @@ run-if = os == 'win'
|
||||
run-if = os == 'win' || os == 'linux'
|
||||
# we need to skip this due to bug 838613
|
||||
skip-if = os=='linux' && bits==32
|
||||
|
||||
[test_crash_AsyncShutdown.js]
|
||||
|
@ -46,12 +46,45 @@
|
||||
const Cu = Components.utils;
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
|
||||
Cu.import("resource://gre/modules/Services.jsm", this);
|
||||
Cu.import("resource://gre/modules/Promise.jsm", this);
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
||||
"resource://gre/modules/Promise.jsm");
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "gDebug",
|
||||
"@mozilla.org/xpcom/debug;1", "nsIDebug");
|
||||
Object.defineProperty(this, "gCrashReporter", {
|
||||
get: function() {
|
||||
delete this.gCrashReporter;
|
||||
try {
|
||||
let reporter = Cc["@mozilla.org/xre/app-info;1"].
|
||||
getService(Ci.nsICrashReporter);
|
||||
return this.gCrashReporter = reporter;
|
||||
} catch (ex) {
|
||||
return this.gCrashReporter = null;
|
||||
}
|
||||
},
|
||||
configurable: true
|
||||
});
|
||||
|
||||
// Display timeout warnings after 10 seconds
|
||||
const DELAY_WARNING_MS = 10 * 1000;
|
||||
|
||||
|
||||
// Crash the process if shutdown is really too long
|
||||
// (allowing for sleep).
|
||||
const PREF_DELAY_CRASH_MS = "toolkit.asyncshutdown.crash_timeout";
|
||||
let DELAY_CRASH_MS = 60 * 1000; // One minute
|
||||
try {
|
||||
DELAY_CRASH_MS = Services.prefs.getIntPref(PREF_DELAY_CRASH_MS);
|
||||
} catch (ex) {
|
||||
// Ignore errors
|
||||
}
|
||||
Services.prefs.addObserver(PREF_DELAY_CRASH_MS, function() {
|
||||
DELAY_CRASH_MS = Services.prefs.getIntPref(PREF_DELAY_CRASH_MS);
|
||||
}, false);
|
||||
|
||||
|
||||
/**
|
||||
* Display a warning.
|
||||
*
|
||||
@ -59,15 +92,47 @@ const DELAY_WARNING_MS = 10 * 1000;
|
||||
* that the UX will not be available to display warnings on the
|
||||
* console. We therefore use dump() rather than Cu.reportError().
|
||||
*/
|
||||
function warn(msg, error = null) {
|
||||
dump("WARNING: " + msg + "\n");
|
||||
function log(msg, prefix = "", error = null) {
|
||||
dump(prefix + msg + "\n");
|
||||
if (error) {
|
||||
dump("WARNING: " + error + "\n");
|
||||
dump(prefix + error + "\n");
|
||||
if (typeof error == "object" && "stack" in error) {
|
||||
dump("WARNING: " + error.stack + "\n");
|
||||
dump(prefix + error.stack + "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
function warn(msg, error = null) {
|
||||
return log(msg, "WARNING: ", error);
|
||||
}
|
||||
function err(msg, error = null) {
|
||||
return log(msg, "ERROR: ", error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Countdown for a given duration, skipping beats if the computer is too busy,
|
||||
* sleeping or otherwise unavailable.
|
||||
*
|
||||
* @param {number} delay An approximate delay to wait in milliseconds (rounded
|
||||
* up to the closest second).
|
||||
*
|
||||
* @return Deferred
|
||||
*/
|
||||
function looseTimer(delay) {
|
||||
let DELAY_BEAT = 1000;
|
||||
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||
let beats = Math.ceil(delay / DELAY_BEAT);
|
||||
let deferred = Promise.defer();
|
||||
timer.initWithCallback(function() {
|
||||
if (beats <= 0) {
|
||||
deferred.resolve();
|
||||
}
|
||||
--beats;
|
||||
}, DELAY_BEAT, Ci.nsITimer.TYPE_REPEATING_PRECISE_CAN_SKIP);
|
||||
// Ensure that the timer is both canceled once we are done with it
|
||||
// and not garbage-collected until then.
|
||||
deferred.promise.then(() => timer.cancel(), () => timer.cancel());
|
||||
return deferred;
|
||||
}
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["AsyncShutdown"];
|
||||
|
||||
@ -202,8 +267,13 @@ Spinner.prototype = {
|
||||
return;
|
||||
}
|
||||
|
||||
// The promises for which we are waiting.
|
||||
let allPromises = [];
|
||||
|
||||
// Information to determine and report to the user which conditions
|
||||
// are not satisfied yet.
|
||||
let allMonitors = [];
|
||||
|
||||
for (let {condition, name} of conditions) {
|
||||
// Gather all completion conditions
|
||||
|
||||
@ -226,20 +296,25 @@ Spinner.prototype = {
|
||||
// If the promise takes too long to be resolved/rejected,
|
||||
// we need to notify the user.
|
||||
//
|
||||
// Future versions may decide to crash Firefox rather than
|
||||
// let it loop and suck battery power forever.
|
||||
// If it takes way too long, we need to crash.
|
||||
|
||||
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||
timer.initWithCallback(function() {
|
||||
let msg = "A phase completion condition is" +
|
||||
" taking too long to complete." +
|
||||
" Condition: " + name +
|
||||
" Condition: " + monitor.name +
|
||||
" Phase: " + topic;
|
||||
warn(msg);
|
||||
}, DELAY_WARNING_MS, Ci.nsITimer.TYPE_ONE_SHOT);
|
||||
|
||||
let monitor = {
|
||||
isFrozen: true,
|
||||
name: name
|
||||
};
|
||||
condition = condition.then(function onSuccess() {
|
||||
timer.cancel();
|
||||
timer.cancel(); // As a side-effect, this prevents |timer| from
|
||||
// being garbage-collected too early.
|
||||
monitor.isFrozen = false;
|
||||
}, function onError(error) {
|
||||
timer.cancel();
|
||||
let msg = "A completion condition encountered an error" +
|
||||
@ -247,8 +322,9 @@ Spinner.prototype = {
|
||||
" Condition: " + name +
|
||||
" Phase: " + topic;
|
||||
warn(msg, error);
|
||||
monitor.isFrozen = false;
|
||||
});
|
||||
|
||||
allMonitors.push(monitor);
|
||||
allPromises.push(condition);
|
||||
|
||||
} catch (error) {
|
||||
@ -275,8 +351,52 @@ Spinner.prototype = {
|
||||
|
||||
let satisfied = false; // |true| once we have satisfied all conditions
|
||||
|
||||
// If after DELAY_CRASH_MS (approximately one minute, adjusted to take
|
||||
// into account sleep and otherwise busy computer) we have not finished
|
||||
// this shutdown phase, we assume that the shutdown is somehow frozen,
|
||||
// presumably deadlocked. At this stage, the only thing we can do to
|
||||
// avoid leaving the user's computer in an unstable (and battery-sucking)
|
||||
// situation is report the issue and crash.
|
||||
let timeToCrash = looseTimer(DELAY_CRASH_MS);
|
||||
timeToCrash.promise.then(
|
||||
function onTimeout() {
|
||||
// Report the problem as best as we can, then crash.
|
||||
let frozen = [];
|
||||
for (let {name, isFrozen} of allMonitors) {
|
||||
if (isFrozen) {
|
||||
frozen.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
let msg = "At least one completion condition failed to complete" +
|
||||
" within a reasonable amount of time. Causing a crash to" +
|
||||
" ensure that we do not leave the user with an unresponsive" +
|
||||
" process draining resources." +
|
||||
" Conditions: " + frozen.join(", ") +
|
||||
" Phase: " + topic;
|
||||
err(msg);
|
||||
if (gCrashReporter) {
|
||||
let data = {
|
||||
phase: topic,
|
||||
conditions: frozen
|
||||
};
|
||||
gCrashReporter.annotateCrashReport("AsyncShutdownTimeout",
|
||||
JSON.stringify(data));
|
||||
} else {
|
||||
warn("No crash reporter available");
|
||||
}
|
||||
|
||||
let error = new Error();
|
||||
gDebug.abort(error.fileName, error.lineNumber + 1);
|
||||
},
|
||||
function onSatisfied() {
|
||||
// The promise has been rejected, which means that we have satisfied
|
||||
// all completion conditions.
|
||||
});
|
||||
|
||||
promise = promise.then(function() {
|
||||
satisfied = true;
|
||||
timeToCrash.reject();
|
||||
}/* No error is possible here*/);
|
||||
|
||||
// Now, spin the event loop
|
||||
|
@ -6,7 +6,7 @@ support-files =
|
||||
propertyLists/bug710259_propertyListXML.plist
|
||||
chromeappsstore.sqlite
|
||||
|
||||
[test_phase.js]
|
||||
[test_AsyncShutdown.js]
|
||||
[test_dict.js]
|
||||
[test_FileUtils.js]
|
||||
[test_Http.js]
|
||||
|
Loading…
Reference in New Issue
Block a user