зеркало из https://github.com/mozilla/gecko-dev.git
Merge mozilla-central to mozilla-inbound
This commit is contained in:
Коммит
d7b5eebe96
|
@ -15,7 +15,7 @@
|
|||
<project name="platform_build" path="build" remote="b2g" revision="ef937d1aca7c4cf89ecb5cc43ae8c21c2000a9db">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="fb7414fa6f5dbb898adc5bd2bbd9fb75df0d0054"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="f37be8b44cb7c3a147b9615ab76743b760f08eeb"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2aa4a75c63cd6e93870a8bddbba45f863cbfd9a3"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
|
||||
|
@ -138,7 +138,7 @@
|
|||
<project name="platform/system/core" path="system/core" revision="a626f6c0ef9e88586569331bd7387b569eaa5ed2"/>
|
||||
<project name="u-boot" path="u-boot" revision="f1502910977ac88f43da7bf9277c3523ad4b0b2f"/>
|
||||
<project name="vendor/sprd/gps" path="vendor/sprd/gps" revision="4c59900937dc2e978b7b14b7f1ea617e3d5d550e"/>
|
||||
<project name="vendor/sprd/open-source" path="vendor/sprd/open-source" revision="c5206aa084ad36037b8ee4b405a71ec7bd88b41c"/>
|
||||
<project name="vendor/sprd/open-source" path="vendor/sprd/open-source" revision="e503b1d14d7fdee532b8f391407299da193c1b2d"/>
|
||||
<project name="vendor/sprd/partner" path="vendor/sprd/partner" revision="8649c7145972251af11b0639997edfecabfc7c2e"/>
|
||||
<project name="vendor/sprd/proprietories" path="vendor/sprd/proprietories" revision="d2466593022f7078aaaf69026adf3367c2adb7bb"/>
|
||||
</manifest>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="fb7414fa6f5dbb898adc5bd2bbd9fb75df0d0054"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="f37be8b44cb7c3a147b9615ab76743b760f08eeb"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2aa4a75c63cd6e93870a8bddbba45f863cbfd9a3"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
|
||||
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="93f9ba577f68d772093987c2f1c0a4ae293e1802"/>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</project>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="fb7414fa6f5dbb898adc5bd2bbd9fb75df0d0054"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="f37be8b44cb7c3a147b9615ab76743b760f08eeb"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2aa4a75c63cd6e93870a8bddbba45f863cbfd9a3"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
|
||||
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="d99ab92d0b829a6c78b5284481d5b236d3901f11"/>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<project name="platform_build" path="build" remote="b2g" revision="ef937d1aca7c4cf89ecb5cc43ae8c21c2000a9db">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="fb7414fa6f5dbb898adc5bd2bbd9fb75df0d0054"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="f37be8b44cb7c3a147b9615ab76743b760f08eeb"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2aa4a75c63cd6e93870a8bddbba45f863cbfd9a3"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<project name="platform_build" path="build" remote="b2g" revision="52775e03a2d8532429dff579cb2cd56718e488c3">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="fb7414fa6f5dbb898adc5bd2bbd9fb75df0d0054"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="f37be8b44cb7c3a147b9615ab76743b760f08eeb"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2aa4a75c63cd6e93870a8bddbba45f863cbfd9a3"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="fb7414fa6f5dbb898adc5bd2bbd9fb75df0d0054"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="f37be8b44cb7c3a147b9615ab76743b760f08eeb"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2aa4a75c63cd6e93870a8bddbba45f863cbfd9a3"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
|
||||
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="93f9ba577f68d772093987c2f1c0a4ae293e1802"/>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<project name="platform_build" path="build" remote="b2g" revision="ef937d1aca7c4cf89ecb5cc43ae8c21c2000a9db">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="fb7414fa6f5dbb898adc5bd2bbd9fb75df0d0054"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="f37be8b44cb7c3a147b9615ab76743b760f08eeb"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2aa4a75c63cd6e93870a8bddbba45f863cbfd9a3"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</project>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="fb7414fa6f5dbb898adc5bd2bbd9fb75df0d0054"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="f37be8b44cb7c3a147b9615ab76743b760f08eeb"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2aa4a75c63cd6e93870a8bddbba45f863cbfd9a3"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
|
||||
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="d99ab92d0b829a6c78b5284481d5b236d3901f11"/>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"git": {
|
||||
"git_revision": "fb7414fa6f5dbb898adc5bd2bbd9fb75df0d0054",
|
||||
"git_revision": "f37be8b44cb7c3a147b9615ab76743b760f08eeb",
|
||||
"remote": "https://git.mozilla.org/releases/gaia.git",
|
||||
"branch": ""
|
||||
},
|
||||
"revision": "800a7cc9e5d11f54a98b891b9f083d419255734e",
|
||||
"revision": "08be467e00c7787c979055ba16f57b7cd84ea7a3",
|
||||
"repo_path": "integration/gaia-central"
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</project>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="fb7414fa6f5dbb898adc5bd2bbd9fb75df0d0054"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="f37be8b44cb7c3a147b9615ab76743b760f08eeb"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2aa4a75c63cd6e93870a8bddbba45f863cbfd9a3"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
|
||||
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="d99ab92d0b829a6c78b5284481d5b236d3901f11"/>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<project name="platform_build" path="build" remote="b2g" revision="52775e03a2d8532429dff579cb2cd56718e488c3">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="fb7414fa6f5dbb898adc5bd2bbd9fb75df0d0054"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="f37be8b44cb7c3a147b9615ab76743b760f08eeb"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="2aa4a75c63cd6e93870a8bddbba45f863cbfd9a3"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
|
||||
|
|
|
@ -1442,6 +1442,7 @@ pref("devtools.performance.ui.show-platform-data", false);
|
|||
pref("devtools.performance.ui.show-idle-blocks", true);
|
||||
pref("devtools.performance.ui.enable-memory", false);
|
||||
pref("devtools.performance.ui.enable-framerate", true);
|
||||
pref("devtools.performance.ui.show-jit-optimizations", false);
|
||||
|
||||
// The default cache UI setting
|
||||
pref("devtools.cache.disabled", false);
|
||||
|
|
|
@ -4,11 +4,24 @@
|
|||
const PRELOAD_PREF = "browser.newtab.preload";
|
||||
|
||||
gDirectorySource = "data:application/json," + JSON.stringify({
|
||||
"directory": [{
|
||||
"enhanced": [{
|
||||
url: "http://example.com/",
|
||||
enhancedImageURI: "data:image/png;base64,helloWORLD",
|
||||
title: "title",
|
||||
type: "organic",
|
||||
}],
|
||||
"directory": [{
|
||||
url: "http://example1.com/",
|
||||
enhancedImageURI: "data:image/png;base64,helloWORLD2",
|
||||
title: "title1",
|
||||
type: "organic"
|
||||
}],
|
||||
"suggested": [{
|
||||
url: "http://example1.com/2",
|
||||
imageURI: "data:image/png;base64,helloWORLD3",
|
||||
title: "title2",
|
||||
type: "affiliate",
|
||||
frecent_sites: ["test.com"]
|
||||
}]
|
||||
});
|
||||
|
||||
|
@ -33,7 +46,7 @@ function runTests() {
|
|||
};
|
||||
}
|
||||
|
||||
// Make the page have a directory link followed by a history link
|
||||
// Make the page have a directory link, enhanced link, and history link
|
||||
yield setLinks("-1");
|
||||
|
||||
// Test with enhanced = false
|
||||
|
@ -52,19 +65,29 @@ function runTests() {
|
|||
({type, enhanced, title} = getData(0));
|
||||
is(type, "organic", "directory link is organic");
|
||||
isnot(enhanced, "", "directory link has enhanced image");
|
||||
is(title, "title1");
|
||||
|
||||
({type, enhanced, title} = getData(1));
|
||||
is(type, "enhanced", "history link is enhanced");
|
||||
isnot(enhanced, "", "history link has enhanced image");
|
||||
is(title, "title");
|
||||
|
||||
is(getData(1), null, "history link pushed out by directory link");
|
||||
is(getData(2), null, "there are only 2 links, directory and enhanced history");
|
||||
|
||||
// Test with a pinned link
|
||||
setPinnedLinks("-1");
|
||||
yield addNewTabPageTab();
|
||||
({type, enhanced, title} = getData(0));
|
||||
is(type, "history", "pinned history link is not enhanced");
|
||||
is(enhanced, "", "pinned history link doesn't have enhanced image");
|
||||
is(title, "site#-1");
|
||||
is(type, "enhanced", "pinned history link is enhanced");
|
||||
isnot(enhanced, "", "pinned history link has enhanced image");
|
||||
is(title, "title");
|
||||
|
||||
is(getData(1), null, "directory link pushed out by pinned history link");
|
||||
({type, enhanced, title} = getData(1));
|
||||
is(type, "organic", "directory link is organic");
|
||||
isnot(enhanced, "", "directory link has enhanced image");
|
||||
is(title, "title1");
|
||||
|
||||
is(getData(2), null, "directory link pushed out by pinned history link");
|
||||
|
||||
// Test pinned link with enhanced = false
|
||||
yield addNewTabPageTab();
|
||||
|
@ -78,4 +101,42 @@ function runTests() {
|
|||
|
||||
ok(getContentDocument().getElementById("newtab-intro-what"),
|
||||
"'What is this page?' link exists");
|
||||
|
||||
yield unpinCell(0);
|
||||
|
||||
|
||||
|
||||
// Test that a suggested tile is not enhanced by a directory tile
|
||||
let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite;
|
||||
NewTabUtils.isTopPlacesSite = () => true;
|
||||
yield setLinks("-1");
|
||||
|
||||
// Test with enhanced = false
|
||||
yield addNewTabPageTab();
|
||||
yield customizeNewTabPage("classic");
|
||||
({type, enhanced, title} = getData(0));
|
||||
isnot(type, "enhanced", "history link is not enhanced");
|
||||
is(enhanced, "", "history link has no enhanced image");
|
||||
is(title, "site#-1");
|
||||
|
||||
is(getData(1), null, "there is only one link and it's a history link");
|
||||
|
||||
|
||||
// Test with enhanced = true
|
||||
yield addNewTabPageTab();
|
||||
yield customizeNewTabPage("enhanced");
|
||||
|
||||
// Suggested link was not enhanced by directory link with same domain
|
||||
({type, enhanced, title} = getData(0));
|
||||
is(type, "affiliate", "suggested link is affiliate");
|
||||
is(enhanced, "", "suggested link has no enhanced image");
|
||||
is(title, "title2");
|
||||
|
||||
// Enhanced history link shows up second
|
||||
({type, enhanced, title} = getData(1));
|
||||
is(type, "enhanced", "pinned history link is enhanced");
|
||||
isnot(enhanced, "", "pinned history link has enhanced image");
|
||||
is(title, "title");
|
||||
|
||||
is(getData(2), null, "there is only a suggested link followed by an enhanced history link");
|
||||
}
|
||||
|
|
|
@ -40,6 +40,21 @@ Please be sure to execute
|
|||
|
||||
from the top level before requesting review on a patch.
|
||||
|
||||
Linting
|
||||
=======
|
||||
run-all-loop-tests.sh will take care of this for you automatically, after
|
||||
you've installed the dependencies by typing:
|
||||
|
||||
( cd standalone ; make install )
|
||||
|
||||
If you install eslint and the react plugin globally:
|
||||
|
||||
npm install -g eslint
|
||||
npm install -g eslint-plugin-react
|
||||
|
||||
You can also run it by hand in the browser/components/loop directory:
|
||||
|
||||
eslint .
|
||||
|
||||
Front-End Unit Tests
|
||||
====================
|
||||
|
|
|
@ -12,12 +12,12 @@ set -e
|
|||
# Main tests
|
||||
|
||||
LOOPDIR=browser/components/loop
|
||||
#ESLINT=standalone/node_modules/.bin/eslint
|
||||
#if [ -x "${LOOPDIR}/${ESLINT}" ]; then
|
||||
# echo 'running eslint; see http://eslint.org/docs/rules/ for error info'
|
||||
# (cd ${LOOPDIR} && ./${ESLINT} .)
|
||||
# echo 'eslint run finished.'
|
||||
#fi
|
||||
ESLINT=standalone/node_modules/.bin/eslint
|
||||
if [ -x "${LOOPDIR}/${ESLINT}" ]; then
|
||||
echo 'running eslint; see http://eslint.org/docs/rules/ for error info'
|
||||
(cd ${LOOPDIR} && ./${ESLINT} .)
|
||||
echo 'eslint run finished.'
|
||||
fi
|
||||
|
||||
./mach xpcshell-test ${LOOPDIR}/
|
||||
./mach marionette-test ${LOOPDIR}/manifest.ini
|
||||
|
|
|
@ -48,11 +48,6 @@ Then point your browser at:
|
|||
**Note:** the provided static file server for web contents is **not** intended
|
||||
for production use.
|
||||
|
||||
Code linting
|
||||
------------
|
||||
|
||||
$ make lint
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
|
|
|
@ -382,6 +382,11 @@ BrowserGlue.prototype = {
|
|||
else if (data == "force-places-init") {
|
||||
this._initPlaces(false);
|
||||
}
|
||||
else if (data == "smart-bookmarks-init") {
|
||||
this.ensurePlacesDefaultQueriesInitialized().then(() => {
|
||||
Services.obs.notifyObservers(null, "test-smart-bookmarks-done", null);
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "initial-migration-will-import-default-bookmarks":
|
||||
this._migrationImportsDefaultBookmarks = true;
|
||||
|
@ -1497,7 +1502,7 @@ BrowserGlue.prototype = {
|
|||
// Now apply distribution customized bookmarks.
|
||||
// This should always run after Places initialization.
|
||||
this._distributionCustomizer.applyBookmarks();
|
||||
this.ensurePlacesDefaultQueriesInitialized();
|
||||
yield this.ensurePlacesDefaultQueriesInitialized();
|
||||
}
|
||||
else {
|
||||
// An import operation is about to run.
|
||||
|
@ -1532,7 +1537,7 @@ BrowserGlue.prototype = {
|
|||
this._distributionCustomizer.applyBookmarks();
|
||||
// Ensure that smart bookmarks are created once the operation is
|
||||
// complete.
|
||||
this.ensurePlacesDefaultQueriesInitialized();
|
||||
yield this.ensurePlacesDefaultQueriesInitialized();
|
||||
} catch (e) {
|
||||
Cu.reportError(e);
|
||||
}
|
||||
|
@ -1576,7 +1581,7 @@ BrowserGlue.prototype = {
|
|||
.getHistogramById("PLACES_BACKUPS_DAYSFROMLAST")
|
||||
.add(backupAge);
|
||||
} catch (ex) {
|
||||
Components.utils.reportError("Unable to report telemetry.");
|
||||
Cu.reportError("Unable to report telemetry.");
|
||||
}
|
||||
|
||||
if (backupAge > BOOKMARKS_BACKUP_MAX_INTERVAL_DAYS)
|
||||
|
@ -2041,12 +2046,11 @@ BrowserGlue.prototype = {
|
|||
this._sanitizer.sanitize(aParentWindow);
|
||||
},
|
||||
|
||||
ensurePlacesDefaultQueriesInitialized:
|
||||
function BG_ensurePlacesDefaultQueriesInitialized() {
|
||||
// This is actual version of the smart bookmarks, must be increased every
|
||||
// time smart bookmarks change.
|
||||
ensurePlacesDefaultQueriesInitialized: Task.async(function* () {
|
||||
// This is the current smart bookmarks version, it must be increased every
|
||||
// time they change.
|
||||
// When adding a new smart bookmark below, its newInVersion property must
|
||||
// be set to the version it has been added in, we will compare its value
|
||||
// be set to the version it has been added in. We will compare its value
|
||||
// to users' smartBookmarksVersion and add new smart bookmarks without
|
||||
// recreating old deleted ones.
|
||||
const SMART_BOOKMARKS_VERSION = 7;
|
||||
|
@ -2062,160 +2066,128 @@ BrowserGlue.prototype = {
|
|||
smartBookmarksCurrentVersion = Services.prefs.getIntPref(SMART_BOOKMARKS_PREF);
|
||||
} catch(ex) {}
|
||||
|
||||
// If version is current or smart bookmarks are disabled, just bail out.
|
||||
// If version is current, or smart bookmarks are disabled, bail out.
|
||||
if (smartBookmarksCurrentVersion == -1 ||
|
||||
smartBookmarksCurrentVersion >= SMART_BOOKMARKS_VERSION) {
|
||||
return;
|
||||
}
|
||||
|
||||
let batch = {
|
||||
runBatched: function BG_EPDQI_runBatched() {
|
||||
let menuIndex = 0;
|
||||
let toolbarIndex = 0;
|
||||
let bundle = Services.strings.createBundle("chrome://browser/locale/places/places.properties");
|
||||
try {
|
||||
let menuIndex = 0;
|
||||
let toolbarIndex = 0;
|
||||
let bundle = Services.strings.createBundle("chrome://browser/locale/places/places.properties");
|
||||
let queryOptions = Ci.nsINavHistoryQueryOptions;
|
||||
|
||||
let smartBookmarks = {
|
||||
MostVisited: {
|
||||
title: bundle.GetStringFromName("mostVisitedTitle"),
|
||||
uri: NetUtil.newURI("place:sort=" +
|
||||
Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING +
|
||||
"&maxResults=" + MAX_RESULTS),
|
||||
parent: PlacesUtils.toolbarFolderId,
|
||||
get position() { return toolbarIndex++; },
|
||||
newInVersion: 1
|
||||
},
|
||||
RecentlyBookmarked: {
|
||||
title: bundle.GetStringFromName("recentlyBookmarkedTitle"),
|
||||
uri: NetUtil.newURI("place:folder=BOOKMARKS_MENU" +
|
||||
"&folder=UNFILED_BOOKMARKS" +
|
||||
"&folder=TOOLBAR" +
|
||||
"&queryType=" +
|
||||
Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS +
|
||||
"&sort=" +
|
||||
Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING +
|
||||
"&maxResults=" + MAX_RESULTS +
|
||||
"&excludeQueries=1"),
|
||||
parent: PlacesUtils.bookmarksMenuFolderId,
|
||||
get position() { return menuIndex++; },
|
||||
newInVersion: 1
|
||||
},
|
||||
RecentTags: {
|
||||
title: bundle.GetStringFromName("recentTagsTitle"),
|
||||
uri: NetUtil.newURI("place:"+
|
||||
"type=" +
|
||||
Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY +
|
||||
"&sort=" +
|
||||
Ci.nsINavHistoryQueryOptions.SORT_BY_LASTMODIFIED_DESCENDING +
|
||||
"&maxResults=" + MAX_RESULTS),
|
||||
parent: PlacesUtils.bookmarksMenuFolderId,
|
||||
get position() { return menuIndex++; },
|
||||
newInVersion: 1
|
||||
},
|
||||
};
|
||||
let smartBookmarks = {
|
||||
MostVisited: {
|
||||
title: bundle.GetStringFromName("mostVisitedTitle"),
|
||||
url: "place:sort=" + queryOptions.SORT_BY_VISITCOUNT_DESCENDING +
|
||||
"&maxResults=" + MAX_RESULTS,
|
||||
parentGuid: PlacesUtils.bookmarks.toolbarGuid,
|
||||
newInVersion: 1
|
||||
},
|
||||
RecentlyBookmarked: {
|
||||
title: bundle.GetStringFromName("recentlyBookmarkedTitle"),
|
||||
url: "place:folder=BOOKMARKS_MENU" +
|
||||
"&folder=UNFILED_BOOKMARKS" +
|
||||
"&folder=TOOLBAR" +
|
||||
"&queryType=" + queryOptions.QUERY_TYPE_BOOKMARKS +
|
||||
"&sort=" + queryOptions.SORT_BY_DATEADDED_DESCENDING +
|
||||
"&maxResults=" + MAX_RESULTS +
|
||||
"&excludeQueries=1",
|
||||
parentGuid: PlacesUtils.bookmarks.menuGuid,
|
||||
newInVersion: 1
|
||||
},
|
||||
RecentTags: {
|
||||
title: bundle.GetStringFromName("recentTagsTitle"),
|
||||
url: "place:type=" + queryOptions.RESULTS_AS_TAG_QUERY +
|
||||
"&sort=" + queryOptions.SORT_BY_LASTMODIFIED_DESCENDING +
|
||||
"&maxResults=" + MAX_RESULTS,
|
||||
parentGuid: PlacesUtils.bookmarks.menuGuid,
|
||||
newInVersion: 1
|
||||
},
|
||||
};
|
||||
|
||||
if (Services.metro && Services.metro.supported) {
|
||||
smartBookmarks.Windows8Touch = {
|
||||
title: PlacesUtils.getString("windows8TouchTitle"),
|
||||
get uri() {
|
||||
let metroBookmarksRoot = PlacesUtils.annotations.getItemsWithAnnotation('metro/bookmarksRoot', {});
|
||||
if (metroBookmarksRoot.length > 0) {
|
||||
return NetUtil.newURI("place:folder=" +
|
||||
metroBookmarksRoot[0] +
|
||||
"&queryType=" +
|
||||
Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS +
|
||||
"&sort=" +
|
||||
Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING +
|
||||
"&maxResults=" + MAX_RESULTS +
|
||||
"&excludeQueries=1")
|
||||
}
|
||||
return null;
|
||||
},
|
||||
parent: PlacesUtils.bookmarksMenuFolderId,
|
||||
get position() { return menuIndex++; },
|
||||
newInVersion: 7
|
||||
};
|
||||
}
|
||||
|
||||
// Set current itemId, parent and position if Smart Bookmark exists,
|
||||
// we will use these informations to create the new version at the same
|
||||
// position.
|
||||
let smartBookmarkItemIds = PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
|
||||
smartBookmarkItemIds.forEach(function (itemId) {
|
||||
let queryId = PlacesUtils.annotations.getItemAnnotation(itemId, SMART_BOOKMARKS_ANNO);
|
||||
if (queryId in smartBookmarks) {
|
||||
let smartBookmark = smartBookmarks[queryId];
|
||||
if (!smartBookmark.uri) {
|
||||
PlacesUtils.bookmarks.removeItem(itemId);
|
||||
return;
|
||||
}
|
||||
smartBookmark.itemId = itemId;
|
||||
smartBookmark.parent = PlacesUtils.bookmarks.getFolderIdForItem(itemId);
|
||||
smartBookmark.updatedPosition = PlacesUtils.bookmarks.getItemIndex(itemId);
|
||||
}
|
||||
else {
|
||||
// We don't remove old Smart Bookmarks because user could still
|
||||
// find them useful, or could have personalized them.
|
||||
// Instead we remove the Smart Bookmark annotation.
|
||||
PlacesUtils.annotations.removeItemAnnotation(itemId, SMART_BOOKMARKS_ANNO);
|
||||
}
|
||||
});
|
||||
|
||||
for (let queryId in smartBookmarks) {
|
||||
// Set current guid, parentGuid and index of existing Smart Bookmarks.
|
||||
// We will use those to create a new version of the bookmark at the same
|
||||
// position.
|
||||
let smartBookmarkItemIds = PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
|
||||
for (let itemId of smartBookmarkItemIds) {
|
||||
let queryId = PlacesUtils.annotations.getItemAnnotation(itemId, SMART_BOOKMARKS_ANNO);
|
||||
if (queryId in smartBookmarks) {
|
||||
// Known smart bookmark.
|
||||
let smartBookmark = smartBookmarks[queryId];
|
||||
smartBookmark.guid = yield PlacesUtils.promiseItemGuid(itemId);
|
||||
|
||||
// We update or create only changed or new smart bookmarks.
|
||||
// Also we respect user choices, so we won't try to create a smart
|
||||
// bookmark if it has been removed.
|
||||
if (smartBookmarksCurrentVersion > 0 &&
|
||||
smartBookmark.newInVersion <= smartBookmarksCurrentVersion &&
|
||||
!smartBookmark.itemId || !smartBookmark.uri)
|
||||
if (!smartBookmark.url) {
|
||||
yield PlacesUtils.bookmarks.remove(smartBookmark.guid);
|
||||
continue;
|
||||
|
||||
// Remove old version of the smart bookmark if it exists, since it
|
||||
// will be replaced in place.
|
||||
if (smartBookmark.itemId) {
|
||||
PlacesUtils.bookmarks.removeItem(smartBookmark.itemId);
|
||||
}
|
||||
|
||||
// Create the new smart bookmark and store its updated itemId.
|
||||
smartBookmark.itemId =
|
||||
PlacesUtils.bookmarks.insertBookmark(smartBookmark.parent,
|
||||
smartBookmark.uri,
|
||||
smartBookmark.updatedPosition || smartBookmark.position,
|
||||
smartBookmark.title);
|
||||
PlacesUtils.annotations.setItemAnnotation(smartBookmark.itemId,
|
||||
SMART_BOOKMARKS_ANNO,
|
||||
queryId, 0,
|
||||
PlacesUtils.annotations.EXPIRE_NEVER);
|
||||
let bm = yield PlacesUtils.bookmarks.fetch(smartBookmark.guid);
|
||||
smartBookmark.parentGuid = bm.parentGuid;
|
||||
smartBookmark.index = bm.index;
|
||||
}
|
||||
|
||||
// If we are creating all Smart Bookmarks from ground up, add a
|
||||
// separator below them in the bookmarks menu.
|
||||
if (smartBookmarksCurrentVersion == 0 &&
|
||||
smartBookmarkItemIds.length == 0) {
|
||||
let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId,
|
||||
menuIndex);
|
||||
// Don't add a separator if the menu was empty or there is one already.
|
||||
if (id != -1 &&
|
||||
PlacesUtils.bookmarks.getItemType(id) != PlacesUtils.bookmarks.TYPE_SEPARATOR) {
|
||||
PlacesUtils.bookmarks.insertSeparator(PlacesUtils.bookmarksMenuFolderId,
|
||||
menuIndex);
|
||||
}
|
||||
else {
|
||||
// We don't remove old Smart Bookmarks because user could still
|
||||
// find them useful, or could have personalized them.
|
||||
// Instead we remove the Smart Bookmark annotation.
|
||||
PlacesUtils.annotations.removeItemAnnotation(itemId, SMART_BOOKMARKS_ANNO);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
PlacesUtils.bookmarks.runInBatchMode(batch, null);
|
||||
}
|
||||
catch(ex) {
|
||||
Components.utils.reportError(ex);
|
||||
}
|
||||
finally {
|
||||
for (let queryId of Object.keys(smartBookmarks)) {
|
||||
let smartBookmark = smartBookmarks[queryId];
|
||||
|
||||
// We update or create only changed or new smart bookmarks.
|
||||
// Also we respect user choices, so we won't try to create a smart
|
||||
// bookmark if it has been removed.
|
||||
if (smartBookmarksCurrentVersion > 0 &&
|
||||
smartBookmark.newInVersion <= smartBookmarksCurrentVersion &&
|
||||
!smartBookmark.guid || !smartBookmark.url)
|
||||
continue;
|
||||
|
||||
// Remove old version of the smart bookmark if it exists, since it
|
||||
// will be replaced in place.
|
||||
if (smartBookmark.guid) {
|
||||
yield PlacesUtils.bookmarks.remove(smartBookmark.guid);
|
||||
}
|
||||
|
||||
// Create the new smart bookmark and store its updated guid.
|
||||
if (!("index" in smartBookmark)) {
|
||||
if (smartBookmark.parentGuid == PlacesUtils.bookmarks.toolbarGuid)
|
||||
smartBookmark.index = toolbarIndex++;
|
||||
else if (smartBookmark.parentGuid == PlacesUtils.bookmarks.menuGuid)
|
||||
smartBookmark.index = menuIndex++;
|
||||
}
|
||||
smartBookmark = yield PlacesUtils.bookmarks.insert(smartBookmark);
|
||||
let itemId = yield PlacesUtils.promiseItemId(smartBookmark.guid);
|
||||
PlacesUtils.annotations.setItemAnnotation(itemId,
|
||||
SMART_BOOKMARKS_ANNO,
|
||||
queryId, 0,
|
||||
PlacesUtils.annotations.EXPIRE_NEVER);
|
||||
}
|
||||
|
||||
// If we are creating all Smart Bookmarks from ground up, add a
|
||||
// separator below them in the bookmarks menu.
|
||||
if (smartBookmarksCurrentVersion == 0 &&
|
||||
smartBookmarkItemIds.length == 0) {
|
||||
let bm = yield PlacesUtils.bookmarks.fetch({ parentGuid: PlacesUtils.bookmarks.menuGuid,
|
||||
index: menuIndex });
|
||||
// Don't add a separator if the menu was empty or there is one already.
|
||||
if (bm && bm.type != PlacesUtils.bookmarks.TYPE_SEPARATOR) {
|
||||
yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
|
||||
parentGuid: PlacesUtils.bookmarks.menuGuid,
|
||||
index: menuIndex });
|
||||
}
|
||||
}
|
||||
} catch(ex) {
|
||||
Cu.reportError(ex);
|
||||
} finally {
|
||||
Services.prefs.setIntPref(SMART_BOOKMARKS_PREF, SMART_BOOKMARKS_VERSION);
|
||||
Services.prefs.savePrefFile(null);
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
// this returns the most recent non-popup browser window
|
||||
getMostRecentBrowserWindow: function BG_getMostRecentBrowserWindow() {
|
||||
|
|
|
@ -23,7 +23,7 @@ interface nsIDOMWindow;
|
|||
*
|
||||
*/
|
||||
|
||||
[scriptable, uuid(781df699-17dc-4237-b3d7-876ddb7085e3)]
|
||||
[scriptable, uuid(ea083cb7-6b9d-4695-8b34-2e8f6ce2a860)]
|
||||
interface nsIBrowserGlue : nsISupports
|
||||
{
|
||||
/**
|
||||
|
@ -35,11 +35,6 @@ interface nsIBrowserGlue : nsISupports
|
|||
*/
|
||||
void sanitize(in nsIDOMWindow aParentWindow);
|
||||
|
||||
/**
|
||||
* Add Smart Bookmarks special queries to bookmarks menu and toolbar folder.
|
||||
*/
|
||||
void ensurePlacesDefaultQueriesInitialized();
|
||||
|
||||
/**
|
||||
* Gets the most recent window that's a browser (but not a popup)
|
||||
*/
|
||||
|
|
|
@ -90,3 +90,32 @@ let createCorruptDB = Task.async(function* () {
|
|||
// Check there's a DB now.
|
||||
Assert.ok((yield OS.File.exists(dbPath)), "should have a DB now");
|
||||
});
|
||||
|
||||
/**
|
||||
* Rebuilds smart bookmarks listening to console output to report any message or
|
||||
* exception generated.
|
||||
*
|
||||
* @return {Promise}
|
||||
* Resolved when done.
|
||||
*/
|
||||
function rebuildSmartBookmarks() {
|
||||
let consoleListener = {
|
||||
observe(aMsg) {
|
||||
do_throw("Got console message: " + aMsg.message);
|
||||
},
|
||||
QueryInterface: XPCOMUtils.generateQI([ Ci.nsIConsoleListener ]),
|
||||
};
|
||||
Services.console.reset();
|
||||
Services.console.registerListener(consoleListener);
|
||||
do_register_cleanup(() => {
|
||||
try {
|
||||
Services.console.unregisterListener(consoleListener);
|
||||
} catch (ex) { /* will likely fail */ }
|
||||
});
|
||||
Cc["@mozilla.org/browser/browserglue;1"]
|
||||
.getService(Ci.nsIObserver)
|
||||
.observe(null, "browser-glue-test", "smart-bookmarks-init");
|
||||
return promiseTopicObserved("test-smart-bookmarks-done").then(() => {
|
||||
Services.console.unregisterListener(consoleListener);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ function run_test() {
|
|||
|
||||
add_task(function* smart_bookmarks_disabled() {
|
||||
Services.prefs.setIntPref("browser.places.smartBookmarksVersion", -1);
|
||||
gluesvc.ensurePlacesDefaultQueriesInitialized();
|
||||
yield rebuildSmartBookmarks();
|
||||
|
||||
let smartBookmarkItemIds =
|
||||
PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
|
||||
|
@ -31,7 +31,7 @@ add_task(function* smart_bookmarks_disabled() {
|
|||
|
||||
add_task(function* create_smart_bookmarks() {
|
||||
Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0);
|
||||
gluesvc.ensurePlacesDefaultQueriesInitialized();
|
||||
yield rebuildSmartBookmarks();
|
||||
|
||||
let smartBookmarkItemIds =
|
||||
PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
|
||||
|
@ -51,7 +51,7 @@ add_task(function* remove_smart_bookmark_and_restore() {
|
|||
yield PlacesUtils.bookmarks.remove(guid);
|
||||
Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0);
|
||||
|
||||
gluesvc.ensurePlacesDefaultQueriesInitialized();
|
||||
yield rebuildSmartBookmarks();
|
||||
smartBookmarkItemIds =
|
||||
PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
|
||||
Assert.equal(smartBookmarkItemIds.length, smartBookmarksCount);
|
||||
|
@ -88,7 +88,7 @@ add_task(function* move_smart_bookmark_rename_and_restore() {
|
|||
|
||||
// restore
|
||||
Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0);
|
||||
gluesvc.ensurePlacesDefaultQueriesInitialized();
|
||||
yield rebuildSmartBookmarks();
|
||||
|
||||
smartBookmarkItemIds =
|
||||
PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
|
||||
|
|
|
@ -35,27 +35,6 @@ function countFolderChildren(aFolderItemId) {
|
|||
return cc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds smart bookmarks listening to console output to report any message or
|
||||
* exception generated when calling ensurePlacesDefaultQueriesInitialized().
|
||||
*/
|
||||
function rebuildSmartBookmarks() {
|
||||
let consoleListener = {
|
||||
observe: function(aMsg) {
|
||||
do_throw("Got console message: " + aMsg.message);
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([
|
||||
Ci.nsIConsoleListener
|
||||
]),
|
||||
};
|
||||
Services.console.reset();
|
||||
Services.console.registerListener(consoleListener);
|
||||
Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIBrowserGlue)
|
||||
.ensurePlacesDefaultQueriesInitialized();
|
||||
Services.console.unregisterListener(consoleListener);
|
||||
}
|
||||
|
||||
add_task(function* setup() {
|
||||
// Initialize browserGlue, but remove it's listener to places-init-complete.
|
||||
let bg = Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIObserver);
|
||||
|
@ -89,7 +68,7 @@ add_task(function* test_version_0() {
|
|||
// Set preferences.
|
||||
Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 0);
|
||||
|
||||
rebuildSmartBookmarks();
|
||||
yield rebuildSmartBookmarks();
|
||||
|
||||
// Count items.
|
||||
Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
|
||||
|
@ -126,7 +105,7 @@ add_task(function* test_version_change() {
|
|||
// Set preferences.
|
||||
Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1);
|
||||
|
||||
rebuildSmartBookmarks();
|
||||
yield rebuildSmartBookmarks();
|
||||
|
||||
// Count items.
|
||||
Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
|
||||
|
@ -173,7 +152,7 @@ add_task(function* test_version_change_pos() {
|
|||
// Set preferences.
|
||||
Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1);
|
||||
|
||||
rebuildSmartBookmarks();
|
||||
yield rebuildSmartBookmarks();
|
||||
|
||||
// Count items.
|
||||
Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
|
||||
|
@ -240,7 +219,7 @@ add_task(function* test_version_change_pos_moved() {
|
|||
// Set preferences.
|
||||
Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1);
|
||||
|
||||
rebuildSmartBookmarks();
|
||||
yield rebuildSmartBookmarks();
|
||||
|
||||
// Count items.
|
||||
Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
|
||||
|
@ -294,7 +273,7 @@ add_task(function* test_recreation() {
|
|||
// Set preferences.
|
||||
Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1);
|
||||
|
||||
rebuildSmartBookmarks();
|
||||
yield rebuildSmartBookmarks();
|
||||
|
||||
// Count items.
|
||||
// We should not have recreated the smart bookmark on toolbar.
|
||||
|
@ -320,7 +299,7 @@ add_task(function* test_recreation_version_0() {
|
|||
// Set preferences.
|
||||
Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 0);
|
||||
|
||||
rebuildSmartBookmarks();
|
||||
yield rebuildSmartBookmarks();
|
||||
|
||||
// Count items.
|
||||
// We should not have recreated the smart bookmark on toolbar.
|
||||
|
|
|
@ -26,16 +26,6 @@ XPCOMUtils.defineLazyGetter(this, "SyncUtils", function() {
|
|||
return Utils;
|
||||
});
|
||||
|
||||
{ // Prevent the parent log setup from leaking into the global scope.
|
||||
let parentLog = Log.repository.getLogger("readinglist");
|
||||
parentLog.level = Preferences.get("browser.readinglist.logLevel", Log.Level.Warn);
|
||||
Preferences.observe("browser.readinglist.logLevel", value => {
|
||||
parentLog.level = value;
|
||||
});
|
||||
let formatter = new Log.BasicFormatter();
|
||||
parentLog.addAppender(new Log.ConsoleAppender(formatter));
|
||||
parentLog.addAppender(new Log.DumpAppender(formatter));
|
||||
}
|
||||
let log = Log.repository.getLogger("readinglist.api");
|
||||
|
||||
|
||||
|
@ -100,6 +90,30 @@ const SYNC_STATUS_PROPERTIES_STATUS = `
|
|||
unread
|
||||
`.trim().split(/\s+/);
|
||||
|
||||
function ReadingListError(message) {
|
||||
this.message = message;
|
||||
this.name = this.constructor.name;
|
||||
this.stack = (new Error()).stack;
|
||||
|
||||
// Consumers can set this to an Error that this ReadingListError wraps.
|
||||
this.originalError = null;
|
||||
}
|
||||
ReadingListError.prototype = new Error();
|
||||
ReadingListError.prototype.constructor = ReadingListError;
|
||||
|
||||
function ReadingListExistsError(message) {
|
||||
message = message || "The item already exists";
|
||||
ReadingListError.call(this, message);
|
||||
}
|
||||
ReadingListExistsError.prototype = new ReadingListError();
|
||||
ReadingListExistsError.prototype.constructor = ReadingListExistsError;
|
||||
|
||||
function ReadingListDeletedError(message) {
|
||||
message = message || "The item has been deleted";
|
||||
ReadingListError.call(this, message);
|
||||
}
|
||||
ReadingListDeletedError.prototype = new ReadingListError();
|
||||
ReadingListDeletedError.prototype.constructor = ReadingListDeletedError;
|
||||
|
||||
/**
|
||||
* A reading list contains ReadingListItems.
|
||||
|
@ -161,6 +175,12 @@ function ReadingListImpl(store) {
|
|||
|
||||
ReadingListImpl.prototype = {
|
||||
|
||||
Error: {
|
||||
Error: ReadingListError,
|
||||
Exists: ReadingListExistsError,
|
||||
Deleted: ReadingListDeletedError,
|
||||
},
|
||||
|
||||
ItemRecordProperties: ITEM_RECORD_PROPERTIES,
|
||||
|
||||
SyncStatus: {
|
||||
|
@ -303,7 +323,7 @@ ReadingListImpl.prototype = {
|
|||
addItem: Task.async(function* (record) {
|
||||
record = normalizeRecord(record);
|
||||
if (!record.url) {
|
||||
throw new Error("The item must have a url");
|
||||
throw new ReadingListError("The item to be added must have a url");
|
||||
}
|
||||
if (!("addedOn" in record)) {
|
||||
record.addedOn = Date.now();
|
||||
|
@ -319,9 +339,9 @@ ReadingListImpl.prototype = {
|
|||
record.syncStatus = SYNC_STATUS_NEW;
|
||||
}
|
||||
|
||||
log.debug("addingItem with guid: ${guid}, url: ${url}", record);
|
||||
log.debug("Adding item with guid: ${guid}, url: ${url}", record);
|
||||
yield this._store.addItem(record);
|
||||
log.trace("added item with guid: ${guid}, url: ${url}", record);
|
||||
log.trace("Added item with guid: ${guid}, url: ${url}", record);
|
||||
this._invalidateIterators();
|
||||
let item = this._itemFromRecord(record);
|
||||
this._callListeners("onItemAdded", item);
|
||||
|
@ -345,13 +365,16 @@ ReadingListImpl.prototype = {
|
|||
* Error on error.
|
||||
*/
|
||||
updateItem: Task.async(function* (item) {
|
||||
if (item._deleted) {
|
||||
throw new ReadingListDeletedError("The item to be updated has been deleted");
|
||||
}
|
||||
if (!item._record.url) {
|
||||
throw new Error("The item must have a url");
|
||||
throw new ReadingListError("The item to be updated must have a url");
|
||||
}
|
||||
this._ensureItemBelongsToList(item);
|
||||
log.debug("updatingItem with guid: ${guid}, url: ${url}", item._record);
|
||||
log.debug("Updating item with guid: ${guid}, url: ${url}", item._record);
|
||||
yield this._store.updateItem(item._record);
|
||||
log.trace("finished update of item guid: ${guid}, url: ${url}", item._record);
|
||||
log.trace("Finished updating item with guid: ${guid}, url: ${url}", item._record);
|
||||
this._invalidateIterators();
|
||||
this._callListeners("onItemUpdated", item);
|
||||
}),
|
||||
|
@ -367,16 +390,23 @@ ReadingListImpl.prototype = {
|
|||
* Error on error.
|
||||
*/
|
||||
deleteItem: Task.async(function* (item) {
|
||||
if (item._deleted) {
|
||||
throw new ReadingListDeletedError("The item has already been deleted");
|
||||
}
|
||||
this._ensureItemBelongsToList(item);
|
||||
|
||||
log.debug("Deleting item with guid: ${guid}, url: ${url}");
|
||||
|
||||
// If the item is new and therefore hasn't been synced yet, delete it from
|
||||
// the store. Otherwise mark it as deleted but don't actually delete it so
|
||||
// that its status can be synced.
|
||||
if (item._record.syncStatus == SYNC_STATUS_NEW) {
|
||||
log.debug("deleteItem guid: ${guid}, url: ${url} - item is local so really deleting it", item._record);
|
||||
log.debug("Item is new, truly deleting it", item._record);
|
||||
yield this._store.deleteItemByURL(item.url);
|
||||
}
|
||||
else {
|
||||
log.debug("Item has been synced, only marking it as deleted",
|
||||
item._record);
|
||||
// To prevent data leakage, only keep the record fields needed to sync
|
||||
// the deleted status: guid and syncStatus.
|
||||
let newRecord = {};
|
||||
|
@ -385,12 +415,12 @@ ReadingListImpl.prototype = {
|
|||
}
|
||||
newRecord.guid = item._record.guid;
|
||||
newRecord.syncStatus = SYNC_STATUS_DELETED;
|
||||
log.debug("deleteItem guid: ${guid}, url: ${url} - item has been synced so updating to deleted state", item._record);
|
||||
yield this._store.updateItemByGUID(newRecord);
|
||||
}
|
||||
|
||||
log.trace("finished db operation deleting item with guid: ${guid}, url: ${url}", item._record);
|
||||
log.trace("Finished deleting item with guid: ${guid}, url: ${url}", item._record);
|
||||
item.list = null;
|
||||
item._deleted = true;
|
||||
// failing to remove the item from the map points at something bad!
|
||||
if (!this._itemsByNormalizedURL.delete(item.url)) {
|
||||
log.error("Failed to remove item from the map", item);
|
||||
|
@ -576,7 +606,7 @@ ReadingListImpl.prototype = {
|
|||
|
||||
_ensureItemBelongsToList(item) {
|
||||
if (!item || !item._ensureBelongsToList) {
|
||||
throw new Error("The item is not a ReadingListItem");
|
||||
throw new ReadingListError("The item is not a ReadingListItem");
|
||||
}
|
||||
item._ensureBelongsToList();
|
||||
},
|
||||
|
@ -596,6 +626,7 @@ let _unserializable = () => {}; // See comments in the ReadingListItem ctor.
|
|||
*/
|
||||
function ReadingListItem(record={}) {
|
||||
this._record = record;
|
||||
this._deleted = false;
|
||||
|
||||
// |this._unserializable| works around a problem when sending one of these
|
||||
// items via a message manager. If |this.list| is set, the item can't be
|
||||
|
@ -844,9 +875,11 @@ ReadingListItem.prototype = {
|
|||
* @return Promise<null> Resolved when the list has been updated.
|
||||
*/
|
||||
delete: Task.async(function* () {
|
||||
if (this._deleted) {
|
||||
throw new ReadingListDeletedError("The item has already been deleted");
|
||||
}
|
||||
this._ensureBelongsToList();
|
||||
yield this.list.deleteItem(this);
|
||||
this.delete = () => Promise.reject("The item has already been deleted");
|
||||
}),
|
||||
|
||||
toJSON() {
|
||||
|
@ -903,7 +936,7 @@ ReadingListItem.prototype = {
|
|||
|
||||
_ensureBelongsToList() {
|
||||
if (!this.list) {
|
||||
throw new Error("The item must belong to a reading list");
|
||||
throw new ReadingListError("The item must belong to a list");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -996,7 +1029,7 @@ ReadingListItemIterator.prototype = {
|
|||
|
||||
_ensureValid() {
|
||||
if (this.invalid) {
|
||||
throw new Error("The iterator has been invalidated");
|
||||
throw new ReadingListError("The iterator has been invalidated");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -1014,7 +1047,7 @@ function normalizeRecord(nonNormalizedRecord) {
|
|||
let record = {};
|
||||
for (let prop in nonNormalizedRecord) {
|
||||
if (ITEM_RECORD_PROPERTIES.indexOf(prop) < 0) {
|
||||
throw new Error("Unrecognized item property: " + prop);
|
||||
throw new ReadingListError("Unrecognized item property: " + prop);
|
||||
}
|
||||
switch (prop) {
|
||||
case "url":
|
||||
|
|
|
@ -27,7 +27,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
|
|||
*/
|
||||
this.SQLiteStore = function SQLiteStore(pathRelativeToProfileDir) {
|
||||
this.pathRelativeToProfileDir = pathRelativeToProfileDir;
|
||||
this._ensureConnection(pathRelativeToProfileDir);
|
||||
};
|
||||
|
||||
this.SQLiteStore.prototype = {
|
||||
|
@ -44,7 +43,7 @@ this.SQLiteStore.prototype = {
|
|||
* Rejected with an Error on error.
|
||||
*/
|
||||
count: Task.async(function* (userOptsList=[], controlOpts={}) {
|
||||
let [sql, args] = sqlWhereFromOptions(userOptsList, controlOpts);
|
||||
let [sql, args] = sqlWhereFromOptions(userOptsList, controlOpts);
|
||||
let count = 0;
|
||||
let conn = yield this._connectionPromise;
|
||||
yield conn.executeCached(`
|
||||
|
@ -91,9 +90,14 @@ this.SQLiteStore.prototype = {
|
|||
paramNames.push(`:${propName}`);
|
||||
}
|
||||
let conn = yield this._connectionPromise;
|
||||
yield conn.executeCached(`
|
||||
INSERT INTO items (${colNames}) VALUES (${paramNames});
|
||||
`, item);
|
||||
try {
|
||||
yield conn.executeCached(`
|
||||
INSERT INTO items (${colNames}) VALUES (${paramNames});
|
||||
`, item);
|
||||
}
|
||||
catch (err) {
|
||||
throwExistsError(err);
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
|
@ -156,34 +160,39 @@ this.SQLiteStore.prototype = {
|
|||
this._destroyPromise = Task.spawn(function* () {
|
||||
let conn = yield this._connectionPromise;
|
||||
yield conn.close();
|
||||
this._connectionPromise = Promise.reject("Store destroyed");
|
||||
this.__connectionPromise = Promise.reject("Store destroyed");
|
||||
}.bind(this));
|
||||
}
|
||||
return this._destroyPromise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates the database connection if it hasn't been created already.
|
||||
*
|
||||
* @param pathRelativeToProfileDir The path of the database file relative to
|
||||
* the profile directory.
|
||||
* Promise<Sqlite.OpenedConnection>
|
||||
*/
|
||||
_ensureConnection: Task.async(function* (pathRelativeToProfileDir) {
|
||||
if (!this._connectionPromise) {
|
||||
this._connectionPromise = Task.spawn(function* () {
|
||||
let conn = yield Sqlite.openConnection({
|
||||
path: pathRelativeToProfileDir,
|
||||
sharedMemoryCache: false,
|
||||
});
|
||||
Sqlite.shutdown.addBlocker("readinglist/SQLiteStore: Destroy",
|
||||
this.destroy.bind(this));
|
||||
yield conn.execute(`
|
||||
PRAGMA locking_mode = EXCLUSIVE;
|
||||
`);
|
||||
yield this._checkSchema(conn);
|
||||
return conn;
|
||||
}.bind(this));
|
||||
get _connectionPromise() {
|
||||
if (!this.__connectionPromise) {
|
||||
this.__connectionPromise = this._createConnection();
|
||||
}
|
||||
return this.__connectionPromise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates the database connection.
|
||||
*
|
||||
* @return Promise<Sqlite.OpenedConnection>
|
||||
*/
|
||||
_createConnection: Task.async(function* () {
|
||||
let conn = yield Sqlite.openConnection({
|
||||
path: this.pathRelativeToProfileDir,
|
||||
sharedMemoryCache: false,
|
||||
});
|
||||
Sqlite.shutdown.addBlocker("readinglist/SQLiteStore: Destroy",
|
||||
this.destroy.bind(this));
|
||||
yield conn.execute(`
|
||||
PRAGMA locking_mode = EXCLUSIVE;
|
||||
`);
|
||||
yield this._checkSchema(conn);
|
||||
return conn;
|
||||
}),
|
||||
|
||||
/**
|
||||
|
@ -203,16 +212,18 @@ this.SQLiteStore.prototype = {
|
|||
}
|
||||
let conn = yield this._connectionPromise;
|
||||
if (!item[keyProp]) {
|
||||
throw new Error("Item must have " + keyProp);
|
||||
throw new ReadingList.Error.Error("Item must have " + keyProp);
|
||||
}
|
||||
try {
|
||||
yield conn.executeCached(`
|
||||
UPDATE items SET ${assignments} WHERE ${keyProp} = :${keyProp};
|
||||
`, item);
|
||||
}
|
||||
catch (err) {
|
||||
throwExistsError(err);
|
||||
}
|
||||
yield conn.executeCached(`
|
||||
UPDATE items SET ${assignments} WHERE ${keyProp} = :${keyProp};
|
||||
`, item);
|
||||
}),
|
||||
|
||||
// Promise<Sqlite.OpenedConnection>
|
||||
_connectionPromise: null,
|
||||
|
||||
// The current schema version.
|
||||
_schemaVersion: 1,
|
||||
|
||||
|
@ -287,6 +298,26 @@ function itemFromRow(row) {
|
|||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the given Error indicates that a unique constraint failed, then wraps that
|
||||
* error in a ReadingList.Error.Exists and throws it. Otherwise throws the
|
||||
* given error.
|
||||
*
|
||||
* @param err An Error object.
|
||||
*/
|
||||
function throwExistsError(err) {
|
||||
let match =
|
||||
/UNIQUE constraint failed: items\.([a-zA-Z0-9_]+)/.exec(err.message);
|
||||
if (match) {
|
||||
let newErr = new ReadingList.Error.Exists(
|
||||
"An item with the following property already exists: " + match[1]
|
||||
);
|
||||
newErr.originalError = err;
|
||||
err = newErr;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the back part of a SELECT statement generated from the given list of
|
||||
* options.
|
||||
|
|
|
@ -360,6 +360,11 @@ SyncImpl.prototype = {
|
|||
|
||||
// Update local items based on the response.
|
||||
for (let serverRecord of response.body.items) {
|
||||
if (serverRecord.deleted) {
|
||||
// _deleteItemForGUID is a no-op if no item exists with the GUID.
|
||||
yield this._deleteItemForGUID(serverRecord.id);
|
||||
continue;
|
||||
}
|
||||
let localItem = yield this._itemForGUID(serverRecord.id);
|
||||
if (localItem) {
|
||||
if (localItem.serverLastModified == serverRecord.last_modified) {
|
||||
|
@ -372,20 +377,22 @@ SyncImpl.prototype = {
|
|||
// the material-changes phase.
|
||||
// TODO
|
||||
|
||||
if (serverRecord.deleted) {
|
||||
yield this._deleteItemForGUID(serverRecord.id);
|
||||
continue;
|
||||
}
|
||||
yield this._updateItemWithServerRecord(localItem, serverRecord);
|
||||
continue;
|
||||
}
|
||||
// new item
|
||||
// A potentially new item. addItem() will fail here when an item was
|
||||
// added to the local list between the time we uploaded new items and
|
||||
// now.
|
||||
let localRecord = localRecordFromServerRecord(serverRecord);
|
||||
try {
|
||||
yield this.list.addItem(localRecord);
|
||||
} catch (ex) {
|
||||
log.warn("Failed to add a new item from server record ${serverRecord}: ${ex}",
|
||||
{serverRecord, ex});
|
||||
if (ex instanceof ReadingList.Error.Exists) {
|
||||
log.debug("Tried to add an item that already exists.");
|
||||
} else {
|
||||
log.error("Error adding an item from server record ${serverRecord} ${ex}",
|
||||
{ serverRecord, ex });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -428,14 +435,24 @@ SyncImpl.prototype = {
|
|||
*/
|
||||
_updateItemWithServerRecord: Task.async(function* (localItem, serverRecord) {
|
||||
if (!localItem) {
|
||||
throw new Error("Item should exist");
|
||||
// The item may have been deleted from the local list between the time we
|
||||
// saw that it needed updating and now.
|
||||
log.debug("Tried to update a null local item from server record",
|
||||
serverRecord);
|
||||
return;
|
||||
}
|
||||
localItem._record = localRecordFromServerRecord(serverRecord);
|
||||
try {
|
||||
yield this.list.updateItem(localItem);
|
||||
} catch (ex) {
|
||||
log.warn("Failed to update an item from server record ${serverRecord}: ${ex}",
|
||||
{serverRecord, ex});
|
||||
// The item may have been deleted from the local list after we fetched it.
|
||||
if (ex instanceof ReadingList.Error.Deleted) {
|
||||
log.debug("Tried to update an item that was deleted from server record",
|
||||
serverRecord);
|
||||
} else {
|
||||
log.error("Error updating an item from server record ${serverRecord} ${ex}",
|
||||
{ serverRecord, ex });
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
|
@ -455,8 +472,8 @@ SyncImpl.prototype = {
|
|||
try {
|
||||
yield this.list.deleteItem(item);
|
||||
} catch (ex) {
|
||||
log.warn("Failed delete local item with id ${guid}: ${ex}",
|
||||
{guid, ex});
|
||||
log.error("Failed delete local item with id ${guid} ${ex}",
|
||||
{ guid, ex });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -468,8 +485,8 @@ SyncImpl.prototype = {
|
|||
try {
|
||||
this.list._store.deleteItemByGUID(guid);
|
||||
} catch (ex) {
|
||||
log.warn("Failed to delete local item with id ${guid}: ${ex}",
|
||||
{guid, ex});
|
||||
log.error("Failed to delete local item with id ${guid} ${ex}",
|
||||
{ guid, ex });
|
||||
}
|
||||
}),
|
||||
|
||||
|
@ -488,7 +505,7 @@ SyncImpl.prototype = {
|
|||
}),
|
||||
|
||||
_handleUnexpectedResponse(contextMsgFragment, response) {
|
||||
log.warn(`Unexpected response ${contextMsgFragment}`, response);
|
||||
log.error(`Unexpected response ${contextMsgFragment}`, response);
|
||||
},
|
||||
|
||||
// TODO: Wipe this pref when user logs out.
|
||||
|
|
|
@ -92,7 +92,8 @@ add_task(function* constraints() {
|
|||
catch (e) {
|
||||
err = e;
|
||||
}
|
||||
checkError(err);
|
||||
Assert.ok(err);
|
||||
Assert.ok(err instanceof ReadingList.Error.Exists);
|
||||
|
||||
// add a new item with an existing guid
|
||||
let item = kindOfClone(gItems[0]);
|
||||
|
@ -104,7 +105,8 @@ add_task(function* constraints() {
|
|||
catch (e) {
|
||||
err = e;
|
||||
}
|
||||
checkError(err);
|
||||
Assert.ok(err);
|
||||
Assert.ok(err instanceof ReadingList.Error.Exists);
|
||||
|
||||
// add a new item with an existing url
|
||||
item = kindOfClone(gItems[0]);
|
||||
|
@ -116,7 +118,8 @@ add_task(function* constraints() {
|
|||
catch (e) {
|
||||
err = e;
|
||||
}
|
||||
checkError(err);
|
||||
Assert.ok(err);
|
||||
Assert.ok(err instanceof ReadingList.Error.Exists);
|
||||
|
||||
// add a new item with an existing resolvedURL
|
||||
item = kindOfClone(gItems[0]);
|
||||
|
@ -128,7 +131,8 @@ add_task(function* constraints() {
|
|||
catch (e) {
|
||||
err = e;
|
||||
}
|
||||
checkError(err);
|
||||
Assert.ok(err);
|
||||
Assert.ok(err instanceof ReadingList.Error.Exists);
|
||||
|
||||
// add a new item with no url
|
||||
item = kindOfClone(gItems[0]);
|
||||
|
@ -141,8 +145,9 @@ add_task(function* constraints() {
|
|||
err = e;
|
||||
}
|
||||
Assert.ok(err);
|
||||
Assert.ok(err instanceof Cu.getGlobalForObject(ReadingList).Error, err);
|
||||
Assert.equal(err.message, "The item must have a url");
|
||||
Assert.ok(err instanceof ReadingList.Error.Error);
|
||||
Assert.ok(!(err instanceof ReadingList.Error.Exists));
|
||||
Assert.ok(!(err instanceof ReadingList.Error.Deleted));
|
||||
|
||||
// update an item with no url
|
||||
item = (yield gList.item({ guid: gItems[0].guid }));
|
||||
|
@ -158,8 +163,9 @@ add_task(function* constraints() {
|
|||
}
|
||||
item._record.url = oldURL;
|
||||
Assert.ok(err);
|
||||
Assert.ok(err instanceof Cu.getGlobalForObject(ReadingList).Error, err);
|
||||
Assert.equal(err.message, "The item must have a url");
|
||||
Assert.ok(err instanceof ReadingList.Error.Error);
|
||||
Assert.ok(!(err instanceof ReadingList.Error.Exists));
|
||||
Assert.ok(!(err instanceof ReadingList.Error.Deleted));
|
||||
|
||||
// add an item with a bogus property
|
||||
item = kindOfClone(gItems[0]);
|
||||
|
@ -172,8 +178,9 @@ add_task(function* constraints() {
|
|||
err = e;
|
||||
}
|
||||
Assert.ok(err);
|
||||
Assert.ok(err.message);
|
||||
Assert.ok(err.message.indexOf("Unrecognized item property:") >= 0);
|
||||
Assert.ok(err instanceof ReadingList.Error.Error);
|
||||
Assert.ok(!(err instanceof ReadingList.Error.Exists));
|
||||
Assert.ok(!(err instanceof ReadingList.Error.Deleted));
|
||||
|
||||
// add a new item with no guid, which is allowed
|
||||
item = kindOfClone(gItems[0]);
|
||||
|
@ -666,7 +673,21 @@ add_task(function* deleteItem() {
|
|||
});
|
||||
let item = (yield iter.items(1))[0];
|
||||
Assert.ok(item);
|
||||
item.delete();
|
||||
let {url, guid} = item;
|
||||
Assert.ok((yield gList.itemForURL(url)), "should be able to get the item by URL before deletion");
|
||||
Assert.ok((yield gList.item({guid})), "should be able to get the item by GUID before deletion");
|
||||
|
||||
yield item.delete();
|
||||
try {
|
||||
yield item.delete();
|
||||
Assert.ok(false, "should not successfully delete the item a second time")
|
||||
} catch(ex) {
|
||||
Assert.ok(ex instanceof ReadingList.Error.Deleted);
|
||||
}
|
||||
|
||||
Assert.ok(!(yield gList.itemForURL(url)), "should fail to get a deleted item by URL");
|
||||
Assert.ok(!(yield gList.item({guid})), "should fail to get a deleted item by GUID");
|
||||
|
||||
gItems[0].list = null;
|
||||
Assert.equal((yield gList.count()), gItems.length - 1);
|
||||
let items = [];
|
||||
|
@ -677,6 +698,12 @@ add_task(function* deleteItem() {
|
|||
|
||||
// delete second item with list.deleteItem()
|
||||
yield gList.deleteItem(items[0]);
|
||||
try {
|
||||
yield gList.deleteItem(items[0]);
|
||||
Assert.ok(false, "should not successfully delete the item a second time")
|
||||
} catch(ex) {
|
||||
Assert.ok(ex instanceof ReadingList.Error.Deleted);
|
||||
}
|
||||
gItems[1].list = null;
|
||||
Assert.equal((yield gList.count()), gItems.length - 2);
|
||||
items = [];
|
||||
|
@ -728,11 +755,6 @@ function checkItems(actualItems, expectedItems) {
|
|||
}
|
||||
}
|
||||
|
||||
function checkError(err) {
|
||||
Assert.ok(err);
|
||||
Assert.ok(err instanceof Cu.getGlobalForObject(Sqlite).Error, err);
|
||||
}
|
||||
|
||||
function kindOfClone(item) {
|
||||
let newItem = {};
|
||||
for (let prop in item) {
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
Cu.import("resource:///modules/readinglist/ReadingList.jsm");
|
||||
Cu.import("resource:///modules/readinglist/SQLiteStore.jsm");
|
||||
Cu.import("resource://gre/modules/Sqlite.jsm");
|
||||
|
||||
|
@ -58,7 +59,10 @@ add_task(function* constraints() {
|
|||
catch (e) {
|
||||
err = e;
|
||||
}
|
||||
checkError(err, "UNIQUE constraint failed");
|
||||
Assert.ok(err);
|
||||
Assert.ok(err instanceof ReadingList.Error.Exists);
|
||||
Assert.ok(err.message);
|
||||
Assert.ok(err.message.indexOf("An item with the following property already exists:") >= 0);
|
||||
|
||||
// add a new item with an existing guid
|
||||
function kindOfClone(item) {
|
||||
|
@ -80,7 +84,10 @@ add_task(function* constraints() {
|
|||
catch (e) {
|
||||
err = e;
|
||||
}
|
||||
checkError(err, "UNIQUE constraint failed: items.guid");
|
||||
Assert.ok(err);
|
||||
Assert.ok(err instanceof ReadingList.Error.Exists);
|
||||
Assert.ok(err.message);
|
||||
Assert.ok(err.message.indexOf("An item with the following property already exists: guid") >= 0);
|
||||
|
||||
// add a new item with an existing url
|
||||
item = kindOfClone(gItems[0]);
|
||||
|
@ -92,7 +99,10 @@ add_task(function* constraints() {
|
|||
catch (e) {
|
||||
err = e;
|
||||
}
|
||||
checkError(err, "UNIQUE constraint failed: items.url");
|
||||
Assert.ok(err);
|
||||
Assert.ok(err instanceof ReadingList.Error.Exists);
|
||||
Assert.ok(err.message);
|
||||
Assert.ok(err.message.indexOf("An item with the following property already exists: url") >= 0);
|
||||
|
||||
// update an item with an existing url
|
||||
item.guid = gItems[1].guid;
|
||||
|
@ -106,7 +116,10 @@ add_task(function* constraints() {
|
|||
// The failure actually happens on items.guid, not items.url, because the item
|
||||
// is first looked up by url, and then its other properties are updated on the
|
||||
// resulting row.
|
||||
checkError(err, "UNIQUE constraint failed: items.guid");
|
||||
Assert.ok(err);
|
||||
Assert.ok(err instanceof ReadingList.Error.Exists);
|
||||
Assert.ok(err.message);
|
||||
Assert.ok(err.message.indexOf("An item with the following property already exists: guid") >= 0);
|
||||
|
||||
// add a new item with an existing resolvedURL
|
||||
item = kindOfClone(gItems[0]);
|
||||
|
@ -118,7 +131,10 @@ add_task(function* constraints() {
|
|||
catch (e) {
|
||||
err = e;
|
||||
}
|
||||
checkError(err, "UNIQUE constraint failed: items.resolvedURL");
|
||||
Assert.ok(err);
|
||||
Assert.ok(err instanceof ReadingList.Error.Exists);
|
||||
Assert.ok(err.message);
|
||||
Assert.ok(err.message.indexOf("An item with the following property already exists: resolvedURL") >= 0);
|
||||
|
||||
// update an item with an existing resolvedURL
|
||||
item.url = gItems[1].url;
|
||||
|
@ -129,7 +145,10 @@ add_task(function* constraints() {
|
|||
catch (e) {
|
||||
err = e;
|
||||
}
|
||||
checkError(err, "UNIQUE constraint failed: items.resolvedURL");
|
||||
Assert.ok(err);
|
||||
Assert.ok(err instanceof ReadingList.Error.Exists);
|
||||
Assert.ok(err.message);
|
||||
Assert.ok(err.message.indexOf("An item with the following property already exists: resolvedURL") >= 0);
|
||||
|
||||
// add a new item with no guid, which is allowed
|
||||
item = kindOfClone(gItems[0]);
|
||||
|
@ -312,10 +331,3 @@ function checkItems(actualItems, expectedItems) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkError(err, expectedMsgSubstring) {
|
||||
Assert.ok(err);
|
||||
Assert.ok(err instanceof Cu.getGlobalForObject(Sqlite).Error);
|
||||
Assert.ok(err.message);
|
||||
Assert.ok(err.message.indexOf(expectedMsgSubstring) >= 0, err.message);
|
||||
}
|
||||
|
|
|
@ -103,6 +103,7 @@ browser.jar:
|
|||
content/browser/devtools/performance/views/details-memory-call-tree.js (performance/views/details-memory-call-tree.js)
|
||||
content/browser/devtools/performance/views/details-memory-flamegraph.js (performance/views/details-memory-flamegraph.js)
|
||||
content/browser/devtools/performance/views/recordings.js (performance/views/recordings.js)
|
||||
content/browser/devtools/performance/views/jit-optimizations.js (performance/views/jit-optimizations.js)
|
||||
content/browser/devtools/responsivedesign/resize-commands.js (responsivedesign/resize-commands.js)
|
||||
content/browser/devtools/commandline.css (commandline/commandline.css)
|
||||
content/browser/devtools/commandlineoutput.xhtml (commandline/commandlineoutput.xhtml)
|
||||
|
|
|
@ -17,6 +17,8 @@ devtools.lazyRequireGetter(this, "EventEmitter",
|
|||
devtools.lazyRequireGetter(this, "DevToolsUtils",
|
||||
"devtools/toolkit/DevToolsUtils");
|
||||
|
||||
devtools.lazyRequireGetter(this, "TreeWidget",
|
||||
"devtools/shared/widgets/TreeWidget", true);
|
||||
devtools.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
|
||||
"devtools/shared/timeline/global", true);
|
||||
devtools.lazyRequireGetter(this, "L10N",
|
||||
|
@ -39,6 +41,8 @@ devtools.lazyRequireGetter(this, "ThreadNode",
|
|||
"devtools/shared/profiler/tree-model", true);
|
||||
devtools.lazyRequireGetter(this, "FrameNode",
|
||||
"devtools/shared/profiler/tree-model", true);
|
||||
devtools.lazyRequireGetter(this, "JITOptimizations",
|
||||
"devtools/shared/profiler/jit", true);
|
||||
devtools.lazyRequireGetter(this, "OptionsView",
|
||||
"devtools/shared/options-view", true);
|
||||
|
||||
|
@ -100,6 +104,11 @@ const EVENTS = {
|
|||
// When the PerformanceController has new recording data
|
||||
TIMELINE_DATA: "Performance:TimelineData",
|
||||
|
||||
// Emitted by the JITOptimizationsView when it renders new optimization
|
||||
// data and clears the optimization data
|
||||
OPTIMIZATIONS_RESET: "Performance:UI:OptimizationsReset",
|
||||
OPTIMIZATIONS_RENDERED: "Performance:UI:OptimizationsRendered",
|
||||
|
||||
// Emitted by the OverviewView when more data has been rendered
|
||||
OVERVIEW_RENDERED: "Performance:UI:OverviewRendered",
|
||||
FRAMERATE_GRAPH_RENDERED: "Performance:UI:OverviewFramerateRendered",
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
<script type="application/javascript" src="performance/views/details-memory-flamegraph.js"/>
|
||||
<script type="application/javascript" src="performance/views/details.js"/>
|
||||
<script type="application/javascript" src="performance/views/recordings.js"/>
|
||||
<script type="application/javascript" src="performance/views/jit-optimizations.js"/>
|
||||
|
||||
<popupset id="performance-options-popupset">
|
||||
<menupopup id="performance-filter-menupopup"/>
|
||||
|
@ -61,6 +62,11 @@
|
|||
data-pref="flatten-tree-recursion"
|
||||
label="&profilerUI.flattenTreeRecursion;"
|
||||
tooltiptext="&profilerUI.flattenTreeRecursion.tooltiptext;"/>
|
||||
<menuitem id="option-show-jit-optimizations"
|
||||
type="checkbox"
|
||||
data-pref="show-jit-optimizations"
|
||||
label="&profilerUI.showJITOptimizations;"
|
||||
tooltiptext="&profilerUI.showJITOptimizations.tooltiptext;"/>
|
||||
</menupopup>
|
||||
</popupset>
|
||||
|
||||
|
@ -162,35 +168,49 @@
|
|||
height="150"/>
|
||||
</hbox>
|
||||
|
||||
<vbox id="js-calltree-view" flex="1">
|
||||
<hbox class="call-tree-headers-container">
|
||||
<label class="plain call-tree-header"
|
||||
type="duration"
|
||||
crop="end"
|
||||
value="&profilerUI.table.totalDuration2;"/>
|
||||
<label class="plain call-tree-header"
|
||||
type="percentage"
|
||||
crop="end"
|
||||
value="&profilerUI.table.totalPercentage;"/>
|
||||
<label class="plain call-tree-header"
|
||||
type="self-duration"
|
||||
crop="end"
|
||||
value="&profilerUI.table.selfDuration2;"/>
|
||||
<label class="plain call-tree-header"
|
||||
type="self-percentage"
|
||||
crop="end"
|
||||
value="&profilerUI.table.selfPercentage;"/>
|
||||
<label class="plain call-tree-header"
|
||||
type="samples"
|
||||
crop="end"
|
||||
value="&profilerUI.table.samples;"/>
|
||||
<label class="plain call-tree-header"
|
||||
type="function"
|
||||
crop="end"
|
||||
value="&profilerUI.table.function;"/>
|
||||
</hbox>
|
||||
<vbox class="call-tree-cells-container" flex="1"/>
|
||||
</vbox>
|
||||
<hbox id="js-profile-view" flex="1">
|
||||
<vbox id="js-calltree-view" flex="1">
|
||||
<hbox class="call-tree-headers-container">
|
||||
<label class="plain call-tree-header"
|
||||
type="duration"
|
||||
crop="end"
|
||||
value="&profilerUI.table.totalDuration2;"/>
|
||||
<label class="plain call-tree-header"
|
||||
type="percentage"
|
||||
crop="end"
|
||||
value="&profilerUI.table.totalPercentage;"/>
|
||||
<label class="plain call-tree-header"
|
||||
type="self-duration"
|
||||
crop="end"
|
||||
value="&profilerUI.table.selfDuration2;"/>
|
||||
<label class="plain call-tree-header"
|
||||
type="self-percentage"
|
||||
crop="end"
|
||||
value="&profilerUI.table.selfPercentage;"/>
|
||||
<label class="plain call-tree-header"
|
||||
type="samples"
|
||||
crop="end"
|
||||
value="&profilerUI.table.samples;"/>
|
||||
<label class="plain call-tree-header"
|
||||
type="function"
|
||||
crop="end"
|
||||
value="&profilerUI.table.function;"/>
|
||||
</hbox>
|
||||
<vbox class="call-tree-cells-container" flex="1"/>
|
||||
</vbox>
|
||||
<splitter id="js-call-tree-splitter" class="devtools-side-splitter"/>
|
||||
<vbox id="jit-optimizations-view" hidden="true">
|
||||
<toolbar id="jit-optimizations-toolbar" class="devtools-toolbar">
|
||||
<hbox id="jit-optimizations-header">
|
||||
<span class="jit-optimizations-title">&profilerUI.JITOptimizationsTitle;</span>
|
||||
<span class="header-function-name" />
|
||||
<span class="header-file opt-url debugger-link" />
|
||||
<span class="header-line opt-line" />
|
||||
</hbox>
|
||||
</toolbar>
|
||||
<vbox id="jit-optimizations-raw-view"></vbox>
|
||||
</vbox>
|
||||
</hbox>
|
||||
|
||||
<hbox id="js-flamegraph-view" flex="1">
|
||||
</hbox>
|
||||
|
|
|
@ -41,6 +41,9 @@ support-files =
|
|||
#[browser_perf-front-profiler-06.js]
|
||||
[browser_perf-front-01.js]
|
||||
[browser_perf-front-02.js]
|
||||
[browser_perf-jit-view-01.js]
|
||||
[browser_perf-jit-model-01.js]
|
||||
[browser_perf-jit-model-02.js]
|
||||
[browser_perf-jump-to-debugger-01.js]
|
||||
[browser_perf-jump-to-debugger-02.js]
|
||||
[browser_perf-options-01.js]
|
||||
|
@ -95,6 +98,7 @@ support-files =
|
|||
[browser_profiler_tree-model-03.js]
|
||||
[browser_profiler_tree-model-04.js]
|
||||
[browser_profiler_tree-model-05.js]
|
||||
[browser_profiler_tree-model-06.js]
|
||||
[browser_profiler_tree-view-01.js]
|
||||
[browser_profiler_tree-view-02.js]
|
||||
[browser_profiler_tree-view-03.js]
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Tests that JITOptimizations track optimization sites and create
|
||||
* an OptimizationSiteProfile when adding optimization sites, like from the
|
||||
* FrameNode, and the returning of that data is as expected.
|
||||
*/
|
||||
|
||||
function test() {
|
||||
let { JITOptimizations } = devtools.require("devtools/shared/profiler/jit");
|
||||
|
||||
let jit = new JITOptimizations(gOpts);
|
||||
|
||||
jit.addOptimizationSite(1);
|
||||
jit.addOptimizationSite(1);
|
||||
jit.addOptimizationSite(0);
|
||||
jit.addOptimizationSite(0);
|
||||
jit.addOptimizationSite(1);
|
||||
jit.addOptimizationSite(2);
|
||||
|
||||
let sites = jit.getOptimizationSites();
|
||||
|
||||
let [first, second, third] = sites;
|
||||
|
||||
is(first.id, 1, "Ordered by samples count, descending");
|
||||
is(first.samples, 3, "first OptimizationSiteProfile has correct sample count");
|
||||
is(first.data, gOpts[1], "includes OptimizationSite as reference under `data`");
|
||||
is(second.id, 0, "Ordered by samples count, descending");
|
||||
is(second.samples, 2, "second OptimizationSiteProfile has correct sample count");
|
||||
is(second.data, gOpts[0], "includes OptimizationSite as reference under `data`");
|
||||
is(third.id, 2, "Ordered by samples count, descending");
|
||||
is(third.samples, 1, "third OptimizationSiteProfile has correct sample count");
|
||||
is(third.data, gOpts[2], "includes OptimizationSite as reference under `data`");
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
let gOpts = [{
|
||||
line: 12,
|
||||
column: 2,
|
||||
types: [{ mirType: "Object", site: "A (http://foo/bar/bar:12)", types: [
|
||||
{ keyedBy: "constructor", name: "Foo", location: "A (http://foo/bar/baz:12)" },
|
||||
{ keyedBy: "primitive", location: "self-hosted" }
|
||||
]}],
|
||||
attempts: [
|
||||
{ outcome: "Failure1", strategy: "SomeGetter1" },
|
||||
{ outcome: "Failure2", strategy: "SomeGetter2" },
|
||||
{ outcome: "Inlined", strategy: "SomeGetter3" },
|
||||
]
|
||||
}, {
|
||||
line: 34,
|
||||
types: [{ mirType: "Int32", site: "Receiver" }], // use no types
|
||||
attempts: [
|
||||
{ outcome: "Failure1", strategy: "SomeGetter1" },
|
||||
{ outcome: "Failure2", strategy: "SomeGetter2" },
|
||||
{ outcome: "Failure3", strategy: "SomeGetter3" },
|
||||
]
|
||||
}, {
|
||||
line: 78,
|
||||
types: [{ mirType: "Object", site: "A (http://foo/bar/bar:12)", types: [
|
||||
{ keyedBy: "constructor", name: "Foo", location: "A (http://foo/bar/baz:12)" },
|
||||
{ keyedBy: "primitive", location: "self-hosted" }
|
||||
]}],
|
||||
attempts: [
|
||||
{ outcome: "Failure1", strategy: "SomeGetter1" },
|
||||
{ outcome: "Failure2", strategy: "SomeGetter2" },
|
||||
{ outcome: "GenericSuccess", strategy: "SomeGetter3" },
|
||||
]
|
||||
}];
|
|
@ -0,0 +1,77 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Tests that JITOptimizations create OptimizationSites, and the underlying
|
||||
* OptimizationSites methods work as expected.
|
||||
*/
|
||||
|
||||
function test() {
|
||||
let { JITOptimizations, OptimizationSite } = devtools.require("devtools/shared/profiler/jit");
|
||||
|
||||
let jit = new JITOptimizations(gOpts);
|
||||
|
||||
jit.addOptimizationSite(1);
|
||||
jit.addOptimizationSite(1);
|
||||
jit.addOptimizationSite(0);
|
||||
jit.addOptimizationSite(0);
|
||||
jit.addOptimizationSite(1);
|
||||
jit.addOptimizationSite(2);
|
||||
|
||||
let sites = jit.getOptimizationSites();
|
||||
|
||||
let [first, second, third] = sites;
|
||||
|
||||
/* hasSuccessfulOutcome */
|
||||
is(first.hasSuccessfulOutcome(), false, "optSite.hasSuccessfulOutcome() returns expected (1)");
|
||||
is(second.hasSuccessfulOutcome(), true, "optSite.hasSuccessfulOutcome() returns expected (2)");
|
||||
is(third.hasSuccessfulOutcome(), true, "optSite.hasSuccessfulOutcome() returns expected (3)");
|
||||
|
||||
/* getAttempts */
|
||||
is(first.getAttempts().length, 2, "optSite.getAttempts() has the correct amount of attempts (1)");
|
||||
is(second.getAttempts().length, 5, "optSite.getAttempts() has the correct amount of attempts (2)");
|
||||
is(third.getAttempts().length, 3, "optSite.getAttempts() has the correct amount of attempts (3)");
|
||||
|
||||
/* getIonTypes */
|
||||
is(first.getIonTypes().length, 1, "optSite.getIonTypes() has the correct amount of IonTypes (1)");
|
||||
is(second.getIonTypes().length, 2, "optSite.getIonTypes() has the correct amount of IonTypes (2)");
|
||||
is(third.getIonTypes().length, 1, "optSite.getIonTypes() has the correct amount of IonTypes (3)");
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
let gOpts = [{
|
||||
line: 12,
|
||||
column: 2,
|
||||
types: [{ mirType: "Object", site: "A (http://foo/bar/bar:12)", types: [
|
||||
{ keyedBy: "constructor", name: "Foo", location: "A (http://foo/bar/baz:12)" },
|
||||
{ keyedBy: "constructor", location: "A (http://foo/bar/baz:12)" }
|
||||
]}, { mirType: "Int32", site: "A (http://foo/bar/bar:12)", types: [
|
||||
{ keyedBy: "primitive", location: "self-hosted" }
|
||||
]}],
|
||||
attempts: [
|
||||
{ outcome: "Failure1", strategy: "SomeGetter1" },
|
||||
{ outcome: "Failure1", strategy: "SomeGetter1" },
|
||||
{ outcome: "Failure1", strategy: "SomeGetter1" },
|
||||
{ outcome: "Failure2", strategy: "SomeGetter2" },
|
||||
{ outcome: "Inlined", strategy: "SomeGetter3" },
|
||||
]
|
||||
}, {
|
||||
line: 34,
|
||||
types: [{ mirType: "Int32", site: "Receiver" }], // use no types
|
||||
attempts: [
|
||||
{ outcome: "Failure1", strategy: "SomeGetter1" },
|
||||
{ outcome: "Failure2", strategy: "SomeGetter2" },
|
||||
]
|
||||
}, {
|
||||
line: 78,
|
||||
types: [{ mirType: "Object", site: "A (http://foo/bar/bar:12)", types: [
|
||||
{ keyedBy: "constructor", name: "Foo", location: "A (http://foo/bar/baz:12)" },
|
||||
{ keyedBy: "primitive", location: "self-hosted" }
|
||||
]}],
|
||||
attempts: [
|
||||
{ outcome: "Failure1", strategy: "SomeGetter1" },
|
||||
{ outcome: "Failure2", strategy: "SomeGetter2" },
|
||||
{ outcome: "GenericSuccess", strategy: "SomeGetter3" },
|
||||
]
|
||||
}];
|
|
@ -0,0 +1,162 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Tests that the JIT Optimizations view renders optimization data
|
||||
* if on, and displays selected frames on focus.
|
||||
*/
|
||||
|
||||
Services.prefs.setBoolPref(INVERT_PREF, false);
|
||||
|
||||
function spawnTest () {
|
||||
let { panel } = yield initPerformance(SIMPLE_URL);
|
||||
let { EVENTS, $, $$, window, PerformanceController } = panel.panelWin;
|
||||
let { OverviewView, DetailsView, JITOptimizationsView, JsCallTreeView, RecordingsView } = panel.panelWin;
|
||||
|
||||
let profilerData = { threads: [{samples: gSamples, optimizations: gOpts}] };
|
||||
|
||||
is(Services.prefs.getBoolPref(JIT_PREF), false, "show JIT Optimizations pref off by default");
|
||||
|
||||
// Make two recordings, so we have one to switch to later, as the
|
||||
// second one will have fake sample data
|
||||
yield startRecording(panel);
|
||||
yield stopRecording(panel);
|
||||
|
||||
yield startRecording(panel);
|
||||
yield stopRecording(panel);
|
||||
|
||||
yield DetailsView.selectView("js-calltree");
|
||||
|
||||
yield injectAndRenderProfilerData();
|
||||
|
||||
yield checkFrame(1, [0, 1]);
|
||||
yield checkFrame(2, [1]);
|
||||
yield checkFrame(3);
|
||||
|
||||
let select = once(PerformanceController, EVENTS.RECORDING_SELECTED);
|
||||
let reset = once(JITOptimizationsView, EVENTS.OPTIMIZATIONS_RESET);
|
||||
RecordingsView.selectedIndex = 0;
|
||||
yield Promise.all([select, reset]);
|
||||
ok(true, "JITOptimizations view correctly reset when switching recordings.");
|
||||
|
||||
yield teardown(panel);
|
||||
finish();
|
||||
|
||||
function *injectAndRenderProfilerData() {
|
||||
// Get current recording and inject our mock data
|
||||
info("Injecting mock profile data");
|
||||
let recording = PerformanceController.getCurrentRecording();
|
||||
recording._profile = profilerData;
|
||||
|
||||
is($("#jit-optimizations-view").hidden, true, "JIT Optimizations panel is hidden when pref off.");
|
||||
|
||||
// Force a rerender
|
||||
let rendered = once(JsCallTreeView, EVENTS.JS_CALL_TREE_RENDERED);
|
||||
JsCallTreeView.render();
|
||||
yield rendered;
|
||||
|
||||
is($("#jit-optimizations-view").hidden, true, "JIT Optimizations panel still hidden when rerendered");
|
||||
Services.prefs.setBoolPref(JIT_PREF, true);
|
||||
is($("#jit-optimizations-view").hidden, false, "JIT Optimizations should be visible when pref is on");
|
||||
ok($("#jit-optimizations-view").classList.contains("empty"),
|
||||
"JIT Optimizations view has empty message when no frames selected.");
|
||||
|
||||
Services.prefs.setBoolPref(JIT_PREF, false);
|
||||
}
|
||||
|
||||
function *checkFrame (frameIndex, expectedOptsIndex=[]) {
|
||||
// Click the frame
|
||||
let rendered = once(JITOptimizationsView, EVENTS.OPTIMIZATIONS_RENDERED);
|
||||
mousedown(window, $$(".call-tree-item")[frameIndex]);
|
||||
Services.prefs.setBoolPref(JIT_PREF, true);
|
||||
yield rendered;
|
||||
ok(true, "JITOptimizationsView rendered when enabling with the current frame node selected");
|
||||
|
||||
let isEmpty = $("#jit-optimizations-view").classList.contains("empty");
|
||||
if (expectedOptsIndex.length === 0) {
|
||||
ok(isEmpty, "JIT Optimizations view has an empty message when selecting a frame without opt data.");
|
||||
return;
|
||||
} else {
|
||||
ok(!isEmpty, "JIT Optimizations view has no empty message.");
|
||||
}
|
||||
|
||||
// Need the value of the optimizations in its array, as its
|
||||
// an index used internally by the view to uniquely ID the opt
|
||||
for (let i of expectedOptsIndex) {
|
||||
let opt = gOpts[i];
|
||||
let { types: ionTypes, attempts } = opt;
|
||||
|
||||
// Check attempts
|
||||
is($$(`.tree-widget-container li[data-id='["${i}","${i}-attempts"]'] .tree-widget-children .tree-widget-item`).length, attempts.length,
|
||||
`found ${attempts.length} attempts`);
|
||||
|
||||
for (let j = 0; j < ionTypes.length; j++) {
|
||||
ok($(`.tree-widget-container li[data-id='["${i}","${i}-types","${i}-types-${j}"]']`),
|
||||
"found an ion type row");
|
||||
}
|
||||
|
||||
// The second optimization should display optimization failures.
|
||||
let warningIcon = $(`.tree-widget-container li[data-id='["${i}"]'] .opt-icon[severity=warning]`);
|
||||
if (i === 1) {
|
||||
ok(warningIcon, "did find a warning icon for all strategies failing.");
|
||||
} else {
|
||||
ok(!warningIcon, "did not find a warning icon for no successful strategies");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let gSamples = [{
|
||||
time: 5,
|
||||
frames: [
|
||||
{ location: "(root)" },
|
||||
{ location: "A (http://foo/bar/baz:12)", optsIndex: 0 },
|
||||
{ location: "B (http://foo/bar/baz:34)", optsIndex: 1 },
|
||||
{ location: "C (http://foo/bar/baz:56)" }
|
||||
]
|
||||
}, {
|
||||
time: 5 + 1,
|
||||
frames: [
|
||||
{ location: "(root)" },
|
||||
{ location: "A (http://foo/bar/baz:12)" },
|
||||
{ location: "B (http://foo/bar/baz:34)" },
|
||||
]
|
||||
}, {
|
||||
time: 5 + 1 + 2,
|
||||
frames: [
|
||||
{ location: "(root)" },
|
||||
{ location: "A (http://foo/bar/baz:12)", optsIndex: 1 },
|
||||
{ location: "B (http://foo/bar/baz:34)" },
|
||||
]
|
||||
}, {
|
||||
time: 5 + 1 + 2 + 7,
|
||||
frames: [
|
||||
{ location: "(root)" },
|
||||
{ location: "A (http://foo/bar/baz:12)", optsIndex: 0 },
|
||||
{ location: "E (http://foo/bar/baz:90)" },
|
||||
{ location: "F (http://foo/bar/baz:99)" }
|
||||
]
|
||||
}];
|
||||
|
||||
// Array of OptimizationSites
|
||||
let gOpts = [{
|
||||
line: 12,
|
||||
column: 2,
|
||||
types: [{ mirType: "Object", site: "A (http://foo/bar/bar:12)", types: [
|
||||
{ keyedBy: "constructor", name: "Foo", location: "A (http://foo/bar/baz:12)" },
|
||||
{ keyedBy: "primitive", location: "self-hosted" }
|
||||
]}],
|
||||
attempts: [
|
||||
{ outcome: "Failure1", strategy: "SomeGetter1" },
|
||||
{ outcome: "Failure2", strategy: "SomeGetter2" },
|
||||
{ outcome: "Inlined", strategy: "SomeGetter3" },
|
||||
]
|
||||
}, {
|
||||
line: 34,
|
||||
types: [{ mirType: "Int32", site: "Receiver" }], // use no types
|
||||
attempts: [
|
||||
{ outcome: "Failure1", strategy: "SomeGetter1" },
|
||||
{ outcome: "Failure2", strategy: "SomeGetter2" },
|
||||
{ outcome: "Failure3", strategy: "SomeGetter3" },
|
||||
]
|
||||
}];
|
|
@ -0,0 +1,103 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Tests that when constructing FrameNodes, if optimization data is available,
|
||||
* the FrameNodes have the correct optimization data after iterating over samples.
|
||||
*/
|
||||
|
||||
let time = 1;
|
||||
|
||||
let samples = [{
|
||||
time: time++,
|
||||
frames: [
|
||||
{ location: "(root)" },
|
||||
{ location: "A", optsIndex: 0 },
|
||||
{ location: "B" },
|
||||
{ location: "C" }
|
||||
]
|
||||
}, {
|
||||
time: time++,
|
||||
frames: [
|
||||
{ location: "(root)" },
|
||||
{ location: "A", optsIndex: 0 },
|
||||
{ location: "D" },
|
||||
{ location: "C" }
|
||||
]
|
||||
}, {
|
||||
time: time++,
|
||||
frames: [
|
||||
{ location: "(root)" },
|
||||
{ location: "A", optsIndex: 1 },
|
||||
{ location: "E", optsIndex: 2 },
|
||||
{ location: "C" }
|
||||
],
|
||||
}, {
|
||||
time: time++,
|
||||
frames: [
|
||||
{ location: "(root)" },
|
||||
{ location: "A" },
|
||||
{ location: "B" },
|
||||
{ location: "F" }
|
||||
]
|
||||
}];
|
||||
|
||||
// Array of OptimizationSites
|
||||
let gOpts = [{
|
||||
line: 12,
|
||||
column: 2,
|
||||
types: [{ mirType: "Object", site: "A (http://foo/bar/bar:12)", types: [
|
||||
{ keyedBy: "constructor", name: "Foo", location: "A (http://foo/bar/baz:12)" },
|
||||
{ keyedBy: "primitive", location: "self-hosted" }
|
||||
]}],
|
||||
attempts: [
|
||||
{ outcome: "Failure1", strategy: "SomeGetter1" },
|
||||
{ outcome: "Failure2", strategy: "SomeGetter2" },
|
||||
{ outcome: "Inlined", strategy: "SomeGetter3" },
|
||||
]
|
||||
}, {
|
||||
line: 34,
|
||||
types: [{ mirType: "Int32", site: "Receiver" }], // use no types
|
||||
attempts: [
|
||||
{ outcome: "Failure1", strategy: "SomeGetter1" },
|
||||
{ outcome: "Failure2", strategy: "SomeGetter2" },
|
||||
{ outcome: "Failure3", strategy: "SomeGetter3" },
|
||||
]
|
||||
}, {
|
||||
line: 78,
|
||||
types: [{ mirType: "Object", site: "A (http://foo/bar/bar:12)", types: [
|
||||
{ keyedBy: "constructor", name: "Foo", location: "A (http://foo/bar/baz:12)" },
|
||||
{ keyedBy: "primitive", location: "self-hosted" }
|
||||
]}],
|
||||
attempts: [
|
||||
{ outcome: "Failure1", strategy: "SomeGetter1" },
|
||||
{ outcome: "Failure2", strategy: "SomeGetter2" },
|
||||
{ outcome: "GenericSuccess", strategy: "SomeGetter3" },
|
||||
]
|
||||
}];
|
||||
|
||||
function test() {
|
||||
let { ThreadNode } = devtools.require("devtools/shared/profiler/tree-model");
|
||||
|
||||
let root = new ThreadNode(samples, { optimizations: gOpts });
|
||||
|
||||
let A = root.calls.A;
|
||||
|
||||
let opts = A.getOptimizations();
|
||||
let sites = opts.getOptimizationSites();
|
||||
is(sites.length, 2, "Frame A has two optimization sites.");
|
||||
is(sites[0].samples, 2, "first opt site has 2 samples.");
|
||||
is(sites[1].samples, 1, "second opt site has 1 sample.");
|
||||
|
||||
let E = A.calls.E;
|
||||
opts = E.getOptimizations();
|
||||
sites = opts.getOptimizationSites();
|
||||
is(sites.length, 1, "Frame E has one optimization site.");
|
||||
is(sites[0].samples, 1, "first opt site has 1 samples.");
|
||||
|
||||
let D = A.calls.D;
|
||||
ok(!D.getOptimizations(),
|
||||
"frames that do not have any opts data do not have JITOptimizations instances.");
|
||||
|
||||
finish();
|
||||
}
|
|
@ -34,6 +34,7 @@ const IDLE_PREF = "devtools.performance.ui.show-idle-blocks";
|
|||
const INVERT_PREF = "devtools.performance.ui.invert-call-tree";
|
||||
const INVERT_FLAME_PREF = "devtools.performance.ui.invert-flame-graph";
|
||||
const FLATTEN_PREF = "devtools.performance.ui.flatten-tree-recursion";
|
||||
const JIT_PREF = "devtools.performance.ui.show-jit-optimizations";
|
||||
|
||||
// All tests are asynchronous.
|
||||
waitForExplicitFinish();
|
||||
|
@ -48,6 +49,7 @@ let DEFAULT_PREFS = [
|
|||
"devtools.performance.ui.show-idle-blocks",
|
||||
"devtools.performance.ui.enable-memory",
|
||||
"devtools.performance.ui.enable-framerate",
|
||||
"devtools.performance.ui.show-jit-optimizations",
|
||||
].reduce((prefs, pref) => {
|
||||
prefs[pref] = Services.prefs.getBoolPref(pref);
|
||||
return prefs;
|
||||
|
|
|
@ -23,12 +23,18 @@ let JsCallTreeView = Heritage.extend(DetailsSubview, {
|
|||
|
||||
this._onPrefChanged = this._onPrefChanged.bind(this);
|
||||
this._onLink = this._onLink.bind(this);
|
||||
|
||||
this.container = $("#js-calltree-view .call-tree-cells-container");
|
||||
|
||||
JITOptimizationsView.initialize();
|
||||
},
|
||||
|
||||
/**
|
||||
* Unbinds events.
|
||||
*/
|
||||
destroy: function () {
|
||||
this.container = null;
|
||||
JITOptimizationsView.destroy();
|
||||
DetailsSubview.destroy.call(this);
|
||||
},
|
||||
|
||||
|
@ -37,10 +43,12 @@ let JsCallTreeView = Heritage.extend(DetailsSubview, {
|
|||
*
|
||||
* @param object interval [optional]
|
||||
* The { startTime, endTime }, in milliseconds.
|
||||
* @param object options [optional]
|
||||
* Additional options for new the call tree.
|
||||
*/
|
||||
render: function (interval={}, options={}) {
|
||||
render: function (interval={}) {
|
||||
let options = {
|
||||
contentOnly: !PerformanceController.getOption("show-platform-data"),
|
||||
invertTree: PerformanceController.getOption("invert-call-tree")
|
||||
};
|
||||
let recording = PerformanceController.getCurrentRecording();
|
||||
let profile = recording.getProfile();
|
||||
let threadNode = this._prepareCallTree(profile, interval, options);
|
||||
|
@ -64,16 +72,11 @@ let JsCallTreeView = Heritage.extend(DetailsSubview, {
|
|||
*/
|
||||
_prepareCallTree: function (profile, { startTime, endTime }, options) {
|
||||
let threadSamples = profile.threads[0].samples;
|
||||
let contentOnly = !PerformanceController.getOption("show-platform-data");
|
||||
let invertTree = PerformanceController.getOption("invert-call-tree");
|
||||
let optimizations = profile.threads[0].optimizations;
|
||||
let { contentOnly, invertTree } = options;
|
||||
|
||||
let threadNode = new ThreadNode(threadSamples,
|
||||
{ startTime, endTime, contentOnly, invertTree });
|
||||
|
||||
// If we have an empty profile (no samples), then don't invert the tree, as
|
||||
// it would hide the root node and a completely blank call tree space can be
|
||||
// mis-interpreted as an error.
|
||||
options.inverted = invertTree && threadNode.samples > 0;
|
||||
{ startTime, endTime, contentOnly, invertTree, optimizations });
|
||||
|
||||
return threadNode;
|
||||
},
|
||||
|
@ -82,31 +85,38 @@ let JsCallTreeView = Heritage.extend(DetailsSubview, {
|
|||
* Renders the call tree.
|
||||
*/
|
||||
_populateCallTree: function (frameNode, options={}) {
|
||||
// If we have an empty profile (no samples), then don't invert the tree, as
|
||||
// it would hide the root node and a completely blank call tree space can be
|
||||
// mis-interpreted as an error.
|
||||
let inverted = options.invertTree && frameNode.samples > 0;
|
||||
|
||||
let root = new CallView({
|
||||
frame: frameNode,
|
||||
inverted: options.inverted,
|
||||
inverted: inverted,
|
||||
// Root nodes are hidden in inverted call trees.
|
||||
hidden: options.inverted,
|
||||
hidden: inverted,
|
||||
// Call trees should only auto-expand when not inverted. Passing undefined
|
||||
// will default to the CALL_TREE_AUTO_EXPAND depth.
|
||||
autoExpandDepth: options.inverted ? 0 : undefined,
|
||||
autoExpandDepth: inverted ? 0 : undefined
|
||||
});
|
||||
|
||||
// Bind events.
|
||||
root.on("link", this._onLink);
|
||||
|
||||
// Pipe "focus" events to the view, mostly for tests
|
||||
root.on("focus", () => this.emit("focus"));
|
||||
// Pipe "focus" events to the view, used by
|
||||
// tests and JITOptimizationsView.
|
||||
root.on("focus", (_, node) => this.emit("focus", node));
|
||||
|
||||
// Clear out other call trees.
|
||||
let container = $("#js-calltree-view > .call-tree-cells-container");
|
||||
container.innerHTML = "";
|
||||
root.attachTo(container);
|
||||
this.container.innerHTML = "";
|
||||
root.attachTo(this.container);
|
||||
|
||||
// When platform data isn't shown, hide the cateogry labels, since they're
|
||||
// only available for C++ frames.
|
||||
let contentOnly = !PerformanceController.getOption("show-platform-data");
|
||||
root.toggleCategories(!contentOnly);
|
||||
root.toggleCategories(options.contentOnly);
|
||||
|
||||
// Return the CallView for tests
|
||||
return root;
|
||||
},
|
||||
|
||||
toString: () => "[object JsCallTreeView]"
|
||||
|
|
|
@ -19,7 +19,7 @@ let DetailsView = {
|
|||
requires: ["timeline"]
|
||||
},
|
||||
"js-calltree": {
|
||||
id: "js-calltree-view",
|
||||
id: "js-profile-view",
|
||||
view: JsCallTreeView
|
||||
},
|
||||
"js-flamegraph": {
|
||||
|
|
|
@ -0,0 +1,409 @@
|
|||
/* 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";
|
||||
|
||||
const URL_LABEL_TOOLTIP = L10N.getStr("table.url.tooltiptext");
|
||||
const OPTIMIZATION_FAILURE = L10N.getStr("jit.optimizationFailure");
|
||||
const JIT_SAMPLES = L10N.getStr("jit.samples");
|
||||
const JIT_EMPTY_TEXT = L10N.getStr("jit.empty");
|
||||
|
||||
/**
|
||||
* View for rendering JIT Optimization data. The terminology and types
|
||||
* used here can be referenced:
|
||||
* @see browser/devtools/shared/profiler/jit.js
|
||||
*/
|
||||
|
||||
let JITOptimizationsView = {
|
||||
|
||||
_currentFrame: null,
|
||||
|
||||
/**
|
||||
* Initialization function called when the tool starts up.
|
||||
*/
|
||||
initialize: function () {
|
||||
this.reset = this.reset.bind(this);
|
||||
this._onFocusFrame = this._onFocusFrame.bind(this);
|
||||
this._toggleVisibility = this._toggleVisibility.bind(this);
|
||||
|
||||
this.el = $("#jit-optimizations-view");
|
||||
|
||||
this.tree = new TreeWidget($("#jit-optimizations-raw-view"), {
|
||||
sorted: false,
|
||||
emptyText: JIT_EMPTY_TEXT
|
||||
});
|
||||
|
||||
// Start the tree by resetting.
|
||||
this.reset();
|
||||
|
||||
this._toggleVisibility();
|
||||
|
||||
PerformanceController.on(EVENTS.RECORDING_SELECTED, this.reset);
|
||||
PerformanceController.on(EVENTS.PREF_CHANGED, this._toggleVisibility);
|
||||
JsCallTreeView.on("focus", this._onFocusFrame);
|
||||
},
|
||||
|
||||
/**
|
||||
* Destruction function called when the tool cleans up.
|
||||
*/
|
||||
destroy: function () {
|
||||
this.tree = null;
|
||||
PerformanceController.off(EVENTS.RECORDING_SELECTED, this.reset);
|
||||
PerformanceController.off(EVENTS.PREF_CHANGED, this._toggleVisibility);
|
||||
JsCallTreeView.off("focus", this._onFocusFrame);
|
||||
},
|
||||
|
||||
/**
|
||||
* Takes a FrameNode, with corresponding optimization data to be displayed
|
||||
* in the view.
|
||||
*
|
||||
* @param {FrameNode} frameNode
|
||||
*/
|
||||
setCurrentFrame: function (frameNode) {
|
||||
if (frameNode !== this.getCurrentFrame()) {
|
||||
this._currentFrame = frameNode;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the current frame node for this view.
|
||||
*
|
||||
* @return {?FrameNode}
|
||||
*/
|
||||
getCurrentFrame: function (frameNode) {
|
||||
return this._currentFrame;
|
||||
},
|
||||
|
||||
/**
|
||||
* Clears out data in the tree, sets to an empty state,
|
||||
* and removes current frame.
|
||||
*/
|
||||
reset: function () {
|
||||
this.setCurrentFrame(null);
|
||||
this.clear();
|
||||
this.el.classList.add("empty");
|
||||
this.emit(EVENTS.OPTIMIZATIONS_RESET);
|
||||
this.emit(EVENTS.OPTIMIZATIONS_RENDERED, this.getCurrentFrame());
|
||||
},
|
||||
|
||||
/**
|
||||
* Clears out data in the tree.
|
||||
*/
|
||||
clear: function () {
|
||||
this.tree.clear();
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper to determine whether or not this view should be enabled.
|
||||
*/
|
||||
isEnabled: function () {
|
||||
return PerformanceController.getOption("show-jit-optimizations");
|
||||
},
|
||||
|
||||
/**
|
||||
* Takes a JITOptimizations object and builds a view containing all attempted
|
||||
* optimizations for this frame. This view is very verbose and meant for those
|
||||
* who understand JIT compilers.
|
||||
*/
|
||||
render: function () {
|
||||
if (!this.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let frameNode = this.getCurrentFrame();
|
||||
|
||||
if (!frameNode) {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
let view = this.tree;
|
||||
|
||||
// Set header information, even if the frame node
|
||||
// does not have any optimization data
|
||||
let frameData = frameNode.getInfo();
|
||||
this._setHeaders(frameData);
|
||||
this.clear();
|
||||
|
||||
if (!frameNode.hasOptimizations()) {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
this.el.classList.remove("empty");
|
||||
|
||||
// An array of sorted OptimizationSites.
|
||||
let sites = frameNode.getOptimizations().getOptimizationSites();
|
||||
|
||||
for (let site of sites) {
|
||||
this._renderSite(view, site, frameData);
|
||||
}
|
||||
|
||||
this.emit(EVENTS.OPTIMIZATIONS_RENDERED, this.getCurrentFrame());
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates an entry in the tree widget for an optimization site.
|
||||
*/
|
||||
_renderSite: function (view, site, frameData) {
|
||||
let { id, samples, data } = site;
|
||||
let { types, attempts } = data;
|
||||
let siteNode = this._createSiteNode(frameData, site);
|
||||
|
||||
// Cast `id` to a string so TreeWidget doesn't think it does not exist
|
||||
id = id + "";
|
||||
|
||||
view.add([{ id: id, node: siteNode }]);
|
||||
|
||||
// Add types -- Ion types are the parent, with
|
||||
// the observed types as children.
|
||||
view.add([id, { id: `${id}-types`, label: `Types (${types.length})` }]);
|
||||
this._renderIonType(view, site);
|
||||
|
||||
// Add attempts
|
||||
view.add([id, { id: `${id}-attempts`, label: `Attempts (${attempts.length})` }]);
|
||||
for (let i = attempts.length - 1; i >= 0; i--) {
|
||||
let node = this._createAttemptNode(attempts[i]);
|
||||
view.add([id, `${id}-attempts`, { node }]);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders all Ion types from an optimization site, with its children
|
||||
* ObservedTypes.
|
||||
*/
|
||||
_renderIonType: function (view, site) {
|
||||
let { id, data: { types }} = site;
|
||||
// Cast `id` to a string so TreeWidget doesn't think it does not exist
|
||||
id = id + "";
|
||||
for (let i = 0; i < types.length; i++) {
|
||||
let ionType = types[i];
|
||||
|
||||
let ionNode = this._createIonNode(ionType);
|
||||
view.add([id, `${id}-types`, { id: `${id}-types-${i}`, node: ionNode }]);
|
||||
for (let observedType of (ionType.types || [])) {
|
||||
let node = this._createObservedTypeNode(observedType);
|
||||
view.add([id, `${id}-types`, `${id}-types-${i}`, { node }]);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates an element for insertion in the raw view for an OptimizationSite.
|
||||
*/
|
||||
|
||||
_createSiteNode: function (frameData, site) {
|
||||
let node = document.createElement("span");
|
||||
let desc = document.createElement("span");
|
||||
let line = document.createElement("span");
|
||||
let column = document.createElement("span");
|
||||
let urlNode = this._createDebuggerLinkNode(frameData.url, site.data.line);
|
||||
|
||||
let attempts = site.getAttempts();
|
||||
let lastStrategy = attempts[attempts.length - 1].strategy;
|
||||
|
||||
if (!site.hasSuccessfulOutcome()) {
|
||||
let icon = document.createElement("span");
|
||||
icon.setAttribute("tooltiptext", OPTIMIZATION_FAILURE);
|
||||
icon.setAttribute("severity", "warning");
|
||||
icon.className = "opt-icon";
|
||||
node.appendChild(icon);
|
||||
}
|
||||
|
||||
desc.textContent = `${lastStrategy} - (${site.samples} ${JIT_SAMPLES})`;
|
||||
line.textContent = site.data.line;
|
||||
line.className = "opt-line";
|
||||
column.textContent = site.data.column;
|
||||
column.className = "opt-line";
|
||||
node.appendChild(desc);
|
||||
node.appendChild(urlNode);
|
||||
node.appendChild(line);
|
||||
node.appendChild(column);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates an element for insertion in the raw view for an IonType.
|
||||
*
|
||||
* @see browser/devtools/shared/profiler/jit.js
|
||||
* @param {IonType} ionType
|
||||
* @return {Element}
|
||||
*/
|
||||
|
||||
_createIonNode: function (ionType) {
|
||||
let node = document.createElement("span");
|
||||
let icon = document.createElement("span");
|
||||
let typeNode = document.createElement("span");
|
||||
let siteNode = document.createElement("span");
|
||||
|
||||
typeNode.textContent = ionType.mirType;
|
||||
typeNode.className = "opt-ion-type";
|
||||
siteNode.textContent = `(${ionType.site})`;
|
||||
siteNode.className = "opt-ion-type-site";
|
||||
node.appendChild(typeNode);
|
||||
node.appendChild(siteNode);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates an element for insertion in the raw view for an ObservedType.
|
||||
*
|
||||
* @see browser/devtools/shared/profiler/jit.js
|
||||
* @param {ObservedType} type
|
||||
* @return {Element}
|
||||
*/
|
||||
|
||||
_createObservedTypeNode: function (type) {
|
||||
let node = document.createElement("span");
|
||||
let typeNode = document.createElement("span");
|
||||
|
||||
typeNode.textContent = `${type.keyedBy}` + (type.name ? ` → ${type.name}` : "");
|
||||
typeNode.className = "opt-type";
|
||||
node.appendChild(typeNode);
|
||||
|
||||
// If we have a type and a location, try to make a
|
||||
// link to the debugger
|
||||
if (type.location && type.line) {
|
||||
let urlNode = this._createDebuggerLinkNode(type.location, type.line);
|
||||
node.appendChild(urlNode);
|
||||
}
|
||||
// Otherwise if we just have a location, it could just
|
||||
// be a memory location
|
||||
else if (type.location) {
|
||||
let locNode = document.createElement("span");
|
||||
locNode.textContent = `@${type.location}`;
|
||||
locNode.className = "opt-url";
|
||||
node.appendChild(locNode);
|
||||
}
|
||||
|
||||
if (type.line) {
|
||||
let line = document.createElement("span");
|
||||
line.textContent = type.line;
|
||||
line.className = "opt-line";
|
||||
node.appendChild(line);
|
||||
}
|
||||
return node;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates an element for insertion in the raw view for an OptimizationAttempt.
|
||||
*
|
||||
* @see browser/devtools/shared/profiler/jit.js
|
||||
* @param {OptimizationAttempt} attempt
|
||||
* @return {Element}
|
||||
*/
|
||||
|
||||
_createAttemptNode: function (attempt) {
|
||||
let node = document.createElement("span");
|
||||
let strategyNode = document.createElement("span");
|
||||
let outcomeNode = document.createElement("span");
|
||||
|
||||
strategyNode.textContent = attempt.strategy;
|
||||
strategyNode.className = "opt-strategy";
|
||||
outcomeNode.textContent = attempt.outcome;
|
||||
outcomeNode.className = "opt-outcome";
|
||||
outcomeNode.setAttribute("outcome",
|
||||
JITOptimizations.isSuccessfulOutcome(attempt.outcome) ? "success" : "failure");
|
||||
|
||||
node.appendChild(strategyNode);
|
||||
node.appendChild(outcomeNode);
|
||||
node.className = "opt-attempt";
|
||||
return node;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a new element, linking it up to the debugger upon clicking.
|
||||
* Can also optionally pass in an element to modify it rather than
|
||||
* creating a new one.
|
||||
*
|
||||
* @param {String} url
|
||||
* @param {Number} line
|
||||
* @param {?Element} el
|
||||
* @return {Element}
|
||||
*/
|
||||
|
||||
_createDebuggerLinkNode: function (url, line, el) {
|
||||
let node = el || document.createElement("span");
|
||||
node.className = "opt-url";
|
||||
let fileName;
|
||||
|
||||
if (this._isLinkableURL(url)) {
|
||||
fileName = url.slice(url.lastIndexOf("/") + 1);
|
||||
node.classList.add("debugger-link");
|
||||
node.setAttribute("tooltiptext", URL_LABEL_TOOLTIP + " → " + url);
|
||||
node.addEventListener("click", () => viewSourceInDebugger(url, line));
|
||||
}
|
||||
node.textContent = `@${fileName || url}`;
|
||||
return node;
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the headers with the current frame's data.
|
||||
*/
|
||||
|
||||
_setHeaders: function (frameData) {
|
||||
$("#jit-optimizations-header .header-function-name").textContent = frameData.functionName;
|
||||
this._createDebuggerLinkNode(frameData.url, frameData.line, $("#jit-optimizations-header .header-file"));
|
||||
$("#jit-optimizations-header .header-line").textContent = frameData.line;
|
||||
},
|
||||
|
||||
/**
|
||||
* Takes a string and returns a boolean indicating whether or not
|
||||
* this is a valid url for linking to the debugger.
|
||||
*
|
||||
* @param {String} url
|
||||
* @return {Boolean}
|
||||
*/
|
||||
|
||||
_isLinkableURL: function (url) {
|
||||
return url && url.indexOf &&
|
||||
(url.indexOf("http") === 0 ||
|
||||
url.indexOf("resource://") === 0 ||
|
||||
url.indexOf("file://") === 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles the visibility of the JITOptimizationsView based on the preference
|
||||
* devtools.performance.ui.show-jit-optimizations.
|
||||
*/
|
||||
|
||||
_toggleVisibility: function () {
|
||||
let enabled = this.isEnabled();
|
||||
this.el.hidden = !enabled;
|
||||
|
||||
// If view is toggled on, and there's a frame node selected,
|
||||
// attempt to render it
|
||||
if (enabled) {
|
||||
this.render();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the JSCallTreeView focuses on a frame.
|
||||
*/
|
||||
|
||||
_onFocusFrame: function (_, view) {
|
||||
if (!view.frame) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only attempt to rerender if this is new -- focus is called even
|
||||
// when the window removes focus and comes back, so this prevents
|
||||
// repeating rendering of the same frame
|
||||
let shouldRender = this.getCurrentFrame() !== view.frame;
|
||||
|
||||
// Save the frame even if the view is disabled, so we can
|
||||
// render it if it becomes enabled
|
||||
this.setCurrentFrame(view.frame);
|
||||
|
||||
if (shouldRender) {
|
||||
this.render();
|
||||
}
|
||||
},
|
||||
|
||||
toString: () => "[object JITOptimizationsView]"
|
||||
|
||||
};
|
||||
|
||||
EventEmitter.decorate(JITOptimizationsView);
|
|
@ -33,6 +33,7 @@ EXTRA_JS_MODULES.devtools += [
|
|||
|
||||
EXTRA_JS_MODULES.devtools.shared.profiler += [
|
||||
'profiler/global.js',
|
||||
'profiler/jit.js',
|
||||
'profiler/tree-model.js',
|
||||
'profiler/tree-view.js',
|
||||
]
|
||||
|
|
|
@ -20,7 +20,7 @@ const OptionsView = function (options={}) {
|
|||
this.window = this.menupopup.ownerDocument.defaultView;
|
||||
let { document } = this.window;
|
||||
this.$ = document.querySelector.bind(document);
|
||||
this.$$ = document.querySelectorAll.bind(document);
|
||||
this.$$ = (selector, parent=document) => parent.querySelectorAll(selector);
|
||||
// Get the corresponding button that opens the popup by looking
|
||||
// for an element with a `popup` attribute matching the menu's ID
|
||||
this.button = this.$(`[popup=${this.menupopup.getAttribute("id")}]`);
|
||||
|
|
|
@ -0,0 +1,205 @@
|
|||
/* 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";
|
||||
|
||||
// An outcome of an OptimizationAttempt that is considered successful.
|
||||
const SUCCESSFUL_OUTCOMES = [
|
||||
"GenericSuccess", "Inlined", "DOM", "Monomorphic", "Polymorphic"
|
||||
];
|
||||
|
||||
/**
|
||||
* Model representing JIT optimization sites from the profiler
|
||||
* for a frame (represented by a FrameNode). Requires optimization data from
|
||||
* a profile, which is an array of RawOptimizationSites.
|
||||
*
|
||||
* When the ThreadNode for the profile iterates over the samples' frames, a JITOptimization
|
||||
* model is attached to each frame node, with each sample of the frame, usually with each
|
||||
* sample containing different optimization information for the same frame (one sample may
|
||||
* pick up optimization X on line Y in the frame, with the next sample containing optimization Z
|
||||
* on line W in the same frame, as each frame is only function.
|
||||
*
|
||||
* Each RawOptimizationSite can be sampled multiple times, which multiple calls to
|
||||
* JITOptimizations#addOptimizationSite handles. An OptimizationSite contains
|
||||
* a record of how many times the RawOptimizationSite was sampled, as well as the unique id
|
||||
* based off of the original profiler array, and the RawOptimizationSite itself as a reference.
|
||||
* @see browser/devtools/shared/profiler/tree-model.js
|
||||
*
|
||||
*
|
||||
* @struct RawOptimizationSite
|
||||
* A structure describing a location in a script that was attempted to be optimized.
|
||||
* Contains all the IonTypes observed, and the sequence of OptimizationAttempts that
|
||||
* were attempted, and the line and column in the script. This is retrieved from the
|
||||
* profiler after a recording, and our base data structure. Should always be referenced,
|
||||
* and unmodified.
|
||||
*
|
||||
* @type {Array<IonType>} types
|
||||
* @type {Array<OptimizationAttempt>} attempts
|
||||
* @type {number} line
|
||||
* @type {number} column
|
||||
*
|
||||
*
|
||||
* @struct IonType
|
||||
* IonMonkey attempts to classify each value in an optimization site by some type.
|
||||
* Based off of the observed types for a value (like a variable that could be a
|
||||
* string or an instance of an object), it determines what kind of type it should be classified
|
||||
* as. Each IonType here contains an array of all ObservedTypes under `types`,
|
||||
* the Ion type that IonMonkey decided this value should be (Int32, Object, etc.) as `mirType`,
|
||||
* and the component of this optimization type that this value refers to -- like
|
||||
* a "getter" optimization, `a[b]`, has site `a` (the "Receiver") and `b` (the "Index").
|
||||
*
|
||||
* Generally the more ObservedTypes, the more deoptimized this OptimizationSite is.
|
||||
* There could be no ObservedTypes, in which case `types` is undefined.
|
||||
*
|
||||
* @type {?Array<ObservedType>} types
|
||||
* @type {string} site
|
||||
* @type {string} mirType
|
||||
*
|
||||
*
|
||||
* @struct ObservedType
|
||||
* When IonMonkey attempts to determine what type a value is, it checks on each sample.
|
||||
* The ObservedType can be thought of in more of JavaScripty-terms, rather than C++.
|
||||
* The `keyedBy` property is a high level description of the type, like "primitive",
|
||||
* "constructor", "function", "singleton", "alloc-site" (that one is a bit more weird).
|
||||
* If the `keyedBy` type is a function or constructor, the ObservedType should have a
|
||||
* `name` property, referring to the function or constructor name from the JS source.
|
||||
* If IonMonkey can determine the origin of this type (like where the constructor is defined),
|
||||
* the ObservedType will also have `location` and `line` properties, but `location` can sometimes
|
||||
* be non-URL strings like "self-hosted" or a memory location like "102ca7880", or no location
|
||||
* at all, and maybe `line` is 0 or undefined.
|
||||
*
|
||||
* @type {string} keyedBy
|
||||
* @type {?string} name
|
||||
* @type {?string} location
|
||||
* @type {?string} line
|
||||
*
|
||||
*
|
||||
* @struct OptimizationAttempt
|
||||
* Each RawOptimizationSite contains an array of OptimizationAttempts. Generally, IonMonkey
|
||||
* goes through a series of strategies for each kind of optimization, starting from most-niche
|
||||
* and optimized, to the less-optimized, but more general strategies -- for example, a getter
|
||||
* opt may first try to optimize for the scenario of a getter on an `arguments` object --
|
||||
* that will fail most of the time, as most objects are not arguments objects, but it will attempt
|
||||
* several strategies in order until it finds a strategy that works, or fails. Even in the best
|
||||
* scenarios, some attempts will fail (like the arguments getter example), which is OK,
|
||||
* as long as some attempt succeeds (with the earlier attempts preferred, as those are more optimized).
|
||||
* In an OptimizationAttempt structure, we store just the `strategy` name and `outcome` name,
|
||||
* both from enums in js/public/TrackedOptimizationInfo.h as TRACKED_STRATEGY_LIST and
|
||||
* TRACKED_OUTCOME_LIST, respectively. An array of successful outcome strings are above
|
||||
* in SUCCESSFUL_OUTCOMES.
|
||||
*
|
||||
* @see js/public/TrackedOptimizationInfo.h
|
||||
*
|
||||
* @type {string} strategy
|
||||
* @type {string} outcome
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* A wrapper around RawOptimizationSite to record sample count and ID (referring to the index
|
||||
* of where this is in the initially seeded optimizations data), so we don't mutate
|
||||
* the original data from the profiler. Provides methods to access the underlying optimization
|
||||
* data easily, so understanding the semantics of JIT data isn't necessary.
|
||||
*
|
||||
* @constructor
|
||||
*
|
||||
* @param {Array<RawOptimizationSite>} optimizations
|
||||
* @param {number} optsIndex
|
||||
*
|
||||
* @type {RawOptimizationSite} data
|
||||
* @type {number} samples
|
||||
* @type {number} id
|
||||
*/
|
||||
|
||||
const OptimizationSite = exports.OptimizationSite = function (optimizations, optsIndex) {
|
||||
this.id = optsIndex;
|
||||
this.data = optimizations[optsIndex];
|
||||
this.samples = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a boolean indicating if the passed in OptimizationSite
|
||||
* has a "good" outcome at the end of its attempted strategies.
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
|
||||
OptimizationSite.prototype.hasSuccessfulOutcome = function () {
|
||||
let attempts = this.getAttempts();
|
||||
let lastOutcome = attempts[attempts.length - 1].outcome;
|
||||
return OptimizationSite.isSuccessfulOutcome(lastOutcome);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the last attempted OptimizationAttempt for this OptimizationSite.
|
||||
*
|
||||
* @return {Array<OptimizationAttempt>}
|
||||
*/
|
||||
|
||||
OptimizationSite.prototype.getAttempts = function () {
|
||||
return this.data.attempts;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns all IonTypes in this OptimizationSite.
|
||||
*
|
||||
* @return {Array<IonType>}
|
||||
*/
|
||||
|
||||
OptimizationSite.prototype.getIonTypes = function () {
|
||||
return this.data.types;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Constructor for JITOptimizations. A collection of OptimizationSites for a frame.
|
||||
*
|
||||
* @constructor
|
||||
* @param {Array<RawOptimizationSite>} optimizations
|
||||
* Array of RawOptimizationSites from the profiler. Do not modify this!
|
||||
*/
|
||||
|
||||
const JITOptimizations = exports.JITOptimizations = function (optimizations) {
|
||||
this._opts = optimizations;
|
||||
// Hash of OptimizationSites observed for this frame.
|
||||
this._optSites = {};
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when a sample detects an optimization on this frame. Takes an `optsIndex`,
|
||||
* referring to an optimization in the stored `this._opts` array. Creates a histogram
|
||||
* of optimization site data by creating or incrementing an OptimizationSite
|
||||
* for each observed optimization.
|
||||
*
|
||||
* @param {Number} optsIndex
|
||||
*/
|
||||
|
||||
JITOptimizations.prototype.addOptimizationSite = function (optsIndex) {
|
||||
let op = this._optSites[optsIndex] || (this._optSites[optsIndex] = new OptimizationSite(this._opts, optsIndex));
|
||||
op.samples++;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an array of OptimizationSites, sorted from most to least times sampled.
|
||||
*
|
||||
* @return {Array<OptimizationSite>}
|
||||
*/
|
||||
|
||||
JITOptimizations.prototype.getOptimizationSites = function () {
|
||||
let opts = [];
|
||||
for (let opt of Object.keys(this._optSites)) {
|
||||
opts.push(this._optSites[opt]);
|
||||
}
|
||||
return opts.sort((a, b) => b.samples - a.samples);
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes an "outcome" string from an OptimizationAttempt and returns
|
||||
* a boolean indicating whether or not its a successful outcome.
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
|
||||
OptimizationSite.isSuccessfulOutcome = JITOptimizations.isSuccessfulOutcome = function (outcome) {
|
||||
return !!~SUCCESSFUL_OUTCOMES.indexOf(outcome);
|
||||
};
|
|
@ -12,6 +12,8 @@ loader.lazyRequireGetter(this, "CATEGORY_MAPPINGS",
|
|||
"devtools/shared/profiler/global", true);
|
||||
loader.lazyRequireGetter(this, "CATEGORY_JIT",
|
||||
"devtools/shared/profiler/global", true);
|
||||
loader.lazyRequireGetter(this, "JITOptimizations",
|
||||
"devtools/shared/profiler/jit", true);
|
||||
|
||||
const CHROME_SCHEMES = ["chrome://", "resource://", "jar:file://"];
|
||||
const CONTENT_SCHEMES = ["http://", "https://", "file://", "app://"];
|
||||
|
@ -50,6 +52,8 @@ exports.FrameNode.isContent = isContent;
|
|||
* - number endTime [optional]
|
||||
* - boolean contentOnly [optional]
|
||||
* - boolean invertTree [optional]
|
||||
* - object optimizations [optional]
|
||||
* The raw tracked optimizations array received from the backend.
|
||||
*/
|
||||
function ThreadNode(threadSamples, options = {}) {
|
||||
this.samples = 0;
|
||||
|
@ -76,10 +80,12 @@ ThreadNode.prototype = {
|
|||
* - number endTime: the latest sample to end at (in milliseconds)
|
||||
* - boolean contentOnly: if platform frames shouldn't be used
|
||||
* - boolean invertTree: if the call tree should be inverted
|
||||
* - object optimizations: The array of all indexable optimizations from the backend.
|
||||
*/
|
||||
insert: function(sample, options = {}) {
|
||||
let startTime = options.startTime || 0;
|
||||
let endTime = options.endTime || Infinity;
|
||||
let optimizations = options.optimizations;
|
||||
let sampleTime = sample.time;
|
||||
if (!sampleTime || sampleTime < startTime || sampleTime > endTime) {
|
||||
return;
|
||||
|
@ -112,7 +118,7 @@ ThreadNode.prototype = {
|
|||
this.duration += sampleDuration;
|
||||
|
||||
FrameNode.prototype.insert(
|
||||
sampleFrames, 0, sampleTime, sampleDuration, this.calls);
|
||||
sampleFrames, optimizations, 0, sampleTime, sampleDuration, this.calls);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -125,6 +131,19 @@ ThreadNode.prototype = {
|
|||
functionName: L10N.getStr("table.root"),
|
||||
categoryData: {}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Mimicks the interface of FrameNode, and a ThreadNode can never have
|
||||
* optimization data (at the moment, anyway), so provide a function
|
||||
* to return null so we don't need to check if a frame node is a thread
|
||||
* or not everytime we fetch optimization data.
|
||||
*
|
||||
* @return {null}
|
||||
*/
|
||||
|
||||
hasOptimizations: function () {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -153,6 +172,7 @@ function FrameNode({ location, line, column, category, allocations }) {
|
|||
this.samples = 0;
|
||||
this.duration = 0;
|
||||
this.calls = {};
|
||||
this._optimizations = null;
|
||||
}
|
||||
|
||||
FrameNode.prototype = {
|
||||
|
@ -168,6 +188,8 @@ FrameNode.prototype = {
|
|||
* C D F
|
||||
* @param frames
|
||||
* The sample call stack.
|
||||
* @param optimizations
|
||||
* The array of indexable optimizations.
|
||||
* @param index
|
||||
* The index of the call in the stack representing this node.
|
||||
* @param number time
|
||||
|
@ -175,7 +197,7 @@ FrameNode.prototype = {
|
|||
* @param number duration
|
||||
* The amount of time spent executing all functions on the stack.
|
||||
*/
|
||||
insert: function(frames, index, time, duration, _store = this.calls) {
|
||||
insert: function(frames, optimizations, index, time, duration, _store = this.calls) {
|
||||
let frame = frames[index];
|
||||
if (!frame) {
|
||||
return;
|
||||
|
@ -185,18 +207,30 @@ FrameNode.prototype = {
|
|||
child.sampleTimes.push({ start: time, end: time + duration });
|
||||
child.samples++;
|
||||
child.duration += duration;
|
||||
child.insert(frames, ++index, time, duration);
|
||||
if (optimizations && frame.optsIndex != null) {
|
||||
let opts = child._optimizations || (child._optimizations = new JITOptimizations(optimizations));
|
||||
opts.addOptimizationSite(frame.optsIndex);
|
||||
}
|
||||
child.insert(frames, optimizations, index + 1, time, duration);
|
||||
},
|
||||
|
||||
/**
|
||||
* Parses the raw location of this function call to retrieve the actual
|
||||
* function name and source url.
|
||||
* Returns the parsed location and additional data describing
|
||||
* this frame. Uses cached data if possible.
|
||||
*
|
||||
* @return object
|
||||
* The computed { name, file, url, line } properties for this
|
||||
* function call.
|
||||
*/
|
||||
getInfo: function() {
|
||||
return this._data || this._computeInfo();
|
||||
},
|
||||
|
||||
/**
|
||||
* Parses the raw location of this function call to retrieve the actual
|
||||
* function name and source url.
|
||||
*/
|
||||
_computeInfo: function() {
|
||||
// "EnterJIT" pseudoframes are special, not actually on the stack.
|
||||
if (this.location == "EnterJIT") {
|
||||
this.category = CATEGORY_JIT;
|
||||
|
@ -230,7 +264,7 @@ FrameNode.prototype = {
|
|||
url = null;
|
||||
}
|
||||
|
||||
return {
|
||||
return this._data = {
|
||||
nodeType: "Frame",
|
||||
functionName: functionName,
|
||||
fileName: fileName,
|
||||
|
@ -241,6 +275,25 @@ FrameNode.prototype = {
|
|||
categoryData: categoryData,
|
||||
isContent: !!isContent(this)
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns whether or not the frame node has an JITOptimizations model.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
hasOptimizations: function () {
|
||||
return !!this._optimizations;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the underlying JITOptimizations model representing
|
||||
* the optimization attempts occuring in this frame.
|
||||
*
|
||||
* @return {JITOptimizations|null}
|
||||
*/
|
||||
getOptimizations: function () {
|
||||
return this._optimizations;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -107,3 +107,13 @@
|
|||
- is recorded. -->
|
||||
<!ENTITY profilerUI.enableFramerate "Record Framerate">
|
||||
<!ENTITY profilerUI.enableFramerate.tooltiptext "Record framerate while profiling.">
|
||||
|
||||
<!-- LOCALIZATION NOTE (profilerUI.showJITOptimizations): This string
|
||||
- is displayed next to a checkbox determining whether or not JIT optimization data
|
||||
- should be shown. -->
|
||||
<!ENTITY profilerUI.showJITOptimizations "Show JIT Optimizations">
|
||||
<!ENTITY profilerUI.showJITOptimizations.tooltiptext "Show JIT optimization data sampled in each frame of the JS call tree.">
|
||||
|
||||
<!-- LOCALIZATION NOTE (profilerUI.JITOptimizationsTitle): This string
|
||||
- is displayed as the title of the JIT Optimizations panel. -->
|
||||
<!ENTITY profilerUI.JITOptimizationsTitle "JIT Optimizations">
|
||||
|
|
|
@ -121,3 +121,15 @@ recordingsList.saveDialogJSONFilter=JSON Files
|
|||
# This string is displayed as a filter for saving a recording to disk.
|
||||
recordingsList.saveDialogAllFilter=All Files
|
||||
|
||||
# LOCALIZATION NOTE (jit.optimizationFailure):
|
||||
# This string is displayed in a tooltip when no JIT optimizations were detected.
|
||||
jit.optimizationFailure=Optimization failed
|
||||
|
||||
# LOCALIZATION NOTE (jit.samples):
|
||||
# This string is displayed for the unit representing thenumber of times a
|
||||
# frame is sampled.
|
||||
jit.samples=samples
|
||||
|
||||
# LOCALIZATION NOTE (jit.empty):
|
||||
# This string is displayed when there are no JIT optimizations to display.
|
||||
jit.empty=No JIT optimizations recorded for this frame.
|
||||
|
|
|
@ -25,6 +25,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
|||
"resource://gre/modules/osfile.jsm")
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
||||
"resource://gre/modules/Promise.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
|
||||
"resource://gre/modules/UpdateChannel.jsm");
|
||||
XPCOMUtils.defineLazyGetter(this, "gTextDecoder", () => {
|
||||
return new TextDecoder();
|
||||
});
|
||||
|
@ -215,6 +217,10 @@ let DirectoryLinksProvider = {
|
|||
},
|
||||
|
||||
_cacheSuggestedLinks: function(link) {
|
||||
if (!link.frecent_sites || "sponsored" == link.type) {
|
||||
// Don't cache links that don't have the expected 'frecent_sites' or are sponsored.
|
||||
return;
|
||||
}
|
||||
for (let suggestedSite of link.frecent_sites) {
|
||||
let suggestedMap = this._suggestedLinks.get(suggestedSite) || new Map();
|
||||
suggestedMap.set(link.url, link);
|
||||
|
@ -225,6 +231,7 @@ let DirectoryLinksProvider = {
|
|||
_fetchAndCacheLinks: function DirectoryLinksProvider_fetchAndCacheLinks(uri) {
|
||||
// Replace with the same display locale used for selecting links data
|
||||
uri = uri.replace("%LOCALE%", this.locale);
|
||||
uri = uri.replace("%CHANNEL%", UpdateChannel.get());
|
||||
|
||||
let deferred = Promise.defer();
|
||||
let xmlHttp = new XMLHttpRequest();
|
||||
|
@ -311,13 +318,15 @@ let DirectoryLinksProvider = {
|
|||
* or {'directory': [], 'suggested': []} if read or parse fails.
|
||||
*/
|
||||
_readDirectoryLinksFile: function DirectoryLinksProvider_readDirectoryLinksFile() {
|
||||
let emptyOutput = {directory: [], suggested: []};
|
||||
let emptyOutput = {directory: [], suggested: [], enhanced: []};
|
||||
return OS.File.read(this._directoryFilePath).then(binaryData => {
|
||||
let output;
|
||||
try {
|
||||
let json = gTextDecoder.decode(binaryData);
|
||||
let linksObj = JSON.parse(json);
|
||||
output = {directory: linksObj.directory || [], suggested: linksObj.suggested || []};
|
||||
output = {directory: linksObj.directory || [],
|
||||
suggested: linksObj.suggested || [],
|
||||
enhanced: linksObj.enhanced || []};
|
||||
}
|
||||
catch (e) {
|
||||
Cu.reportError(e);
|
||||
|
@ -415,8 +424,7 @@ let DirectoryLinksProvider = {
|
|||
*/
|
||||
getEnhancedLink: function DirectoryLinksProvider_getEnhancedLink(link) {
|
||||
// Use the provided link if it's already enhanced
|
||||
return link.type == "history" ? null :
|
||||
link.enhancedImageURI && link ? link :
|
||||
return link.enhancedImageURI && link ? link :
|
||||
this._enhancedLinks.get(NewTabUtils.extractSite(link.url));
|
||||
},
|
||||
|
||||
|
@ -456,16 +464,8 @@ let DirectoryLinksProvider = {
|
|||
this.isURLAllowed(link.enhancedImageURI, ALLOWED_IMAGE_SCHEMES);
|
||||
}.bind(this);
|
||||
|
||||
let setCommonProperties = function(link, length, position) {
|
||||
// Stash the enhanced image for the site
|
||||
if (link.enhancedImageURI) {
|
||||
this._enhancedLinks.set(NewTabUtils.extractSite(link.url), link);
|
||||
}
|
||||
link.lastVisitDate = length - position;
|
||||
}.bind(this);
|
||||
|
||||
rawLinks.suggested.filter(validityFilter).forEach((link, position) => {
|
||||
setCommonProperties(link, rawLinks.suggested.length, position);
|
||||
link.lastVisitDate = rawLinks.suggested.length - position;
|
||||
|
||||
// We cache suggested tiles here but do not push any of them in the links list yet.
|
||||
// The decision for which suggested tile to include will be made separately.
|
||||
|
@ -473,8 +473,17 @@ let DirectoryLinksProvider = {
|
|||
this._frequencyCaps.set(link.url, DEFAULT_FREQUENCY_CAP);
|
||||
});
|
||||
|
||||
rawLinks.enhanced.filter(validityFilter).forEach((link, position) => {
|
||||
link.lastVisitDate = rawLinks.enhanced.length - position;
|
||||
|
||||
// Stash the enhanced image for the site
|
||||
if (link.enhancedImageURI) {
|
||||
this._enhancedLinks.set(NewTabUtils.extractSite(link.url), link);
|
||||
}
|
||||
});
|
||||
|
||||
let links = rawLinks.directory.filter(validityFilter).map((link, position) => {
|
||||
setCommonProperties(link, rawLinks.directory.length, position);
|
||||
link.lastVisitDate = rawLinks.directory.length - position;
|
||||
link.frecency = DIRECTORY_FRECENCY;
|
||||
return link;
|
||||
});
|
||||
|
|
|
@ -65,7 +65,7 @@ let gLastRequestPath;
|
|||
let suggestedTile1 = {
|
||||
url: "http://turbotax.com",
|
||||
type: "affiliate",
|
||||
lastVisitDate: 3,
|
||||
lastVisitDate: 4,
|
||||
frecent_sites: [
|
||||
"taxact.com",
|
||||
"hrblock.com",
|
||||
|
@ -76,7 +76,7 @@ let suggestedTile1 = {
|
|||
let suggestedTile2 = {
|
||||
url: "http://irs.gov",
|
||||
type: "affiliate",
|
||||
lastVisitDate: 2,
|
||||
lastVisitDate: 3,
|
||||
frecent_sites: [
|
||||
"taxact.com",
|
||||
"hrblock.com",
|
||||
|
@ -87,7 +87,7 @@ let suggestedTile2 = {
|
|||
let suggestedTile3 = {
|
||||
url: "http://hrblock.com",
|
||||
type: "affiliate",
|
||||
lastVisitDate: 1,
|
||||
lastVisitDate: 2,
|
||||
frecent_sites: [
|
||||
"taxact.com",
|
||||
"freetaxusa.com",
|
||||
|
@ -95,6 +95,14 @@ let suggestedTile3 = {
|
|||
"taxslayer.com"
|
||||
]
|
||||
};
|
||||
let suggestedTile4 = {
|
||||
url: "http://sponsoredtile.com",
|
||||
type: "sponsored",
|
||||
lastVisitDate: 1,
|
||||
frecent_sites: [
|
||||
"sponsoredtarget.com"
|
||||
]
|
||||
}
|
||||
let someOtherSite = {url: "http://someothersite.com", title: "Not_A_Suggested_Site"};
|
||||
|
||||
function getHttpHandler(path) {
|
||||
|
@ -341,7 +349,7 @@ add_task(function test_updateSuggestedTile() {
|
|||
});
|
||||
|
||||
add_task(function test_suggestedLinksMap() {
|
||||
let data = {"suggested": [suggestedTile1, suggestedTile2, suggestedTile3], "directory": [someOtherSite]};
|
||||
let data = {"suggested": [suggestedTile1, suggestedTile2, suggestedTile3, suggestedTile4], "directory": [someOtherSite]};
|
||||
let dataURI = 'data:application/json,' + JSON.stringify(data);
|
||||
|
||||
yield promiseSetupDirectoryLinksProvider({linksURL: dataURI});
|
||||
|
@ -360,6 +368,7 @@ add_task(function test_suggestedLinksMap() {
|
|||
"taxslayer.com": [suggestedTile1, suggestedTile2, suggestedTile3],
|
||||
"freetaxusa.com": [suggestedTile2, suggestedTile3],
|
||||
};
|
||||
do_check_eq([...DirectoryLinksProvider._suggestedLinks.keys()].indexOf("sponsoredtarget.com"), -1);
|
||||
|
||||
DirectoryLinksProvider._suggestedLinks.forEach((suggestedLinks, site) => {
|
||||
let suggestedLinksItr = suggestedLinks.values();
|
||||
|
@ -482,7 +491,7 @@ add_task(function test_frequencyCappedSites_views() {
|
|||
let targets = ["top.site.com"];
|
||||
let data = {
|
||||
suggested: [{
|
||||
type: "sponsored",
|
||||
type: "affiliate",
|
||||
frecent_sites: targets,
|
||||
url: testUrl
|
||||
}],
|
||||
|
@ -525,15 +534,15 @@ add_task(function test_frequencyCappedSites_views() {
|
|||
}
|
||||
|
||||
// Make sure we get 5 views of the link before it is removed
|
||||
checkFirstTypeAndLength("sponsored", 2);
|
||||
checkFirstTypeAndLength("affiliate", 2);
|
||||
synthesizeAction("view");
|
||||
checkFirstTypeAndLength("sponsored", 2);
|
||||
checkFirstTypeAndLength("affiliate", 2);
|
||||
synthesizeAction("view");
|
||||
checkFirstTypeAndLength("sponsored", 2);
|
||||
checkFirstTypeAndLength("affiliate", 2);
|
||||
synthesizeAction("view");
|
||||
checkFirstTypeAndLength("sponsored", 2);
|
||||
checkFirstTypeAndLength("affiliate", 2);
|
||||
synthesizeAction("view");
|
||||
checkFirstTypeAndLength("sponsored", 2);
|
||||
checkFirstTypeAndLength("affiliate", 2);
|
||||
synthesizeAction("view");
|
||||
checkFirstTypeAndLength("organic", 1);
|
||||
|
||||
|
@ -553,7 +562,7 @@ add_task(function test_frequencyCappedSites_click() {
|
|||
let targets = ["top.site.com"];
|
||||
let data = {
|
||||
suggested: [{
|
||||
type: "sponsored",
|
||||
type: "affiliate",
|
||||
frecent_sites: targets,
|
||||
url: testUrl
|
||||
}],
|
||||
|
@ -596,9 +605,9 @@ add_task(function test_frequencyCappedSites_click() {
|
|||
}
|
||||
|
||||
// Make sure the link disappears after the first click
|
||||
checkFirstTypeAndLength("sponsored", 2);
|
||||
checkFirstTypeAndLength("affiliate", 2);
|
||||
synthesizeAction("view");
|
||||
checkFirstTypeAndLength("sponsored", 2);
|
||||
checkFirstTypeAndLength("affiliate", 2);
|
||||
synthesizeAction("click");
|
||||
checkFirstTypeAndLength("organic", 1);
|
||||
|
||||
|
@ -1026,7 +1035,7 @@ add_task(function test_DirectoryLinksProvider_getAllowedEnhancedImages() {
|
|||
});
|
||||
|
||||
add_task(function test_DirectoryLinksProvider_getEnhancedLink() {
|
||||
let data = {"directory": [
|
||||
let data = {"enhanced": [
|
||||
{url: "http://example.net", enhancedImageURI: "data:,net1"},
|
||||
{url: "http://example.com", enhancedImageURI: "data:,com1"},
|
||||
{url: "http://example.com", enhancedImageURI: "data:,com2"},
|
||||
|
@ -1035,7 +1044,7 @@ add_task(function test_DirectoryLinksProvider_getEnhancedLink() {
|
|||
yield promiseSetupDirectoryLinksProvider({linksURL: dataURI});
|
||||
|
||||
let links = yield fetchData();
|
||||
do_check_eq(links.length, 3);
|
||||
do_check_eq(links.length, 0); // There are no directory links.
|
||||
|
||||
function checkEnhanced(url, image) {
|
||||
let enhanced = DirectoryLinksProvider.getEnhancedLink({url: url});
|
||||
|
@ -1066,17 +1075,63 @@ add_task(function test_DirectoryLinksProvider_getEnhancedLink() {
|
|||
checkEnhanced("http://127.0.0.1", undefined);
|
||||
|
||||
// Make sure old data is not cached
|
||||
data = {"directory": [
|
||||
data = {"enhanced": [
|
||||
{url: "http://example.com", enhancedImageURI: "data:,fresh"},
|
||||
]};
|
||||
dataURI = 'data:application/json,' + JSON.stringify(data);
|
||||
yield promiseSetupDirectoryLinksProvider({linksURL: dataURI});
|
||||
links = yield fetchData();
|
||||
do_check_eq(links.length, 1);
|
||||
do_check_eq(links.length, 0); // There are no directory links.
|
||||
checkEnhanced("http://example.net", undefined);
|
||||
checkEnhanced("http://example.com", "data:,fresh");
|
||||
});
|
||||
|
||||
add_task(function test_DirectoryLinksProvider_enhancedURIs() {
|
||||
let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite;
|
||||
NewTabUtils.isTopPlacesSite = () => true;
|
||||
|
||||
let data = {
|
||||
"suggested": [
|
||||
{url: "http://example.net", enhancedImageURI: "data:,net1", title:"SuggestedTitle", frecent_sites: ["test.com"]}
|
||||
],
|
||||
"directory": [
|
||||
{url: "http://example.net", enhancedImageURI: "data:,net2", title:"DirectoryTitle"}
|
||||
]
|
||||
};
|
||||
let dataURI = 'data:application/json,' + JSON.stringify(data);
|
||||
yield promiseSetupDirectoryLinksProvider({linksURL: dataURI});
|
||||
|
||||
// Wait for links to get loaded
|
||||
let gLinks = NewTabUtils.links;
|
||||
gLinks.addProvider(DirectoryLinksProvider);
|
||||
gLinks.populateCache();
|
||||
yield new Promise(resolve => {
|
||||
NewTabUtils.allPages.register({
|
||||
observe: _ => _,
|
||||
update() {
|
||||
NewTabUtils.allPages.unregister(this);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Check that we've saved the directory tile.
|
||||
let links = yield fetchData();
|
||||
do_check_eq(links.length, 1);
|
||||
do_check_eq(links[0].title, "DirectoryTitle");
|
||||
do_check_eq(links[0].enhancedImageURI, "data:,net2");
|
||||
|
||||
// Check that the suggested tile with the same URL replaces the directory tile.
|
||||
links = gLinks.getLinks();
|
||||
do_check_eq(links.length, 1);
|
||||
do_check_eq(links[0].title, "SuggestedTitle");
|
||||
do_check_eq(links[0].enhancedImageURI, "data:,net1");
|
||||
|
||||
// Cleanup.
|
||||
NewTabUtils.isTopPlacesSite = origIsTopPlacesSite;
|
||||
gLinks.removeProvider(DirectoryLinksProvider);
|
||||
});
|
||||
|
||||
add_task(function test_DirectoryLinksProvider_setDefaultEnhanced() {
|
||||
function checkDefault(expected) {
|
||||
Services.prefs.clearUserPref(kNewtabEnhancedPref);
|
||||
|
|
|
@ -233,11 +233,11 @@
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.call-tree-url {
|
||||
.call-tree-url, .tree-widget-item:not(.theme-selected) .opt-url {
|
||||
color: var(--theme-highlight-blue);
|
||||
}
|
||||
|
||||
.call-tree-line {
|
||||
.call-tree-line, .tree-widget-item:not(.theme-selected) .opt-line {
|
||||
color: var(--theme-highlight-orange);
|
||||
}
|
||||
|
||||
|
@ -476,3 +476,124 @@
|
|||
/* Text inside a selected item should not be custom colored. */
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
/**
|
||||
* JIT View
|
||||
*/
|
||||
|
||||
#jit-optimizations-view {
|
||||
width: 350px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
/* override default styles for tree widget */
|
||||
#jit-optimizations-view .tree-widget-empty-text {
|
||||
font-size: inherit;
|
||||
padding: 0px;
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
#jit-optimizations-view:not(.empty) .tree-widget-empty-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#jit-optimizations-toolbar {
|
||||
height: 18px;
|
||||
min-height: 0px; /* override .devtools-toolbar min-height */
|
||||
}
|
||||
|
||||
.jit-optimizations-title {
|
||||
margin: 0px 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#jit-optimizations-raw-view {
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
/* override default .tree-widget-item line-height */
|
||||
#jit-optimizations-raw-view .tree-widget-item {
|
||||
line-height: 20px !important;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#jit-optimizations-raw-view .tree-widget-item[level="1"] {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#jit-optimizations-view .opt-ion-type-site {
|
||||
-moz-margin-start: 4px !important;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
#jit-optimizations-view .opt-outcome::before {
|
||||
content: "→";
|
||||
margin: 4px 0px;
|
||||
color: var(--theme-body-color);
|
||||
}
|
||||
#jit-optimizations-view .theme-selected .opt-outcome::before {
|
||||
color: var(--theme-selection-color);
|
||||
}
|
||||
|
||||
#jit-optimizations-view .tree-widget-item:not(.theme-selected) .opt-outcome[outcome=success] {
|
||||
color: var(--theme-highlight-green);
|
||||
}
|
||||
#jit-optimizations-view .tree-widget-item:not(.theme-selected) .opt-outcome[outcome=failure] {
|
||||
color: var(--theme-highlight-red);
|
||||
}
|
||||
#jit-optimizations-view .tree-widget-container {
|
||||
-moz-margin-end: 0px;
|
||||
}
|
||||
#jit-optimizations-view .tree-widget-container > li,
|
||||
#jit-optimizations-view .tree-widget-children > li {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.opt-line::before {
|
||||
content: ":";
|
||||
color: var(--theme-highlight-orange);
|
||||
}
|
||||
.theme-selected .opt-line::before {
|
||||
color: var(--theme-selection-color);
|
||||
}
|
||||
.opt-line.header-line::before {
|
||||
color: var(--theme-body-color);
|
||||
}
|
||||
#jit-optimizations-view.empty .opt-line.header-line::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.opt-url {
|
||||
-moz-margin-start: 4px !important;
|
||||
}
|
||||
.opt-url:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.opt-url.debugger-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#jit-optimizations-view .opt-icon::before {
|
||||
content: "";
|
||||
background-image: url(chrome://browser/skin/devtools/webconsole.png);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 48px 40px;
|
||||
margin: 5px 6px 0 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
max-height: 8px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#jit-optimizations-view .opt-icon[severity=warning]::before {
|
||||
background-position: -16px -16px;
|
||||
}
|
||||
|
||||
@media (min-resolution: 2dppx) {
|
||||
#jit-optimizations-view .opt-icon::before {
|
||||
background-image: url(chrome://browser/skin/devtools/webconsole@2x.png);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -101,7 +101,11 @@ InitializeOculusCAPI()
|
|||
searchPath.AppendPrintf("%s/Library/Frameworks/LibOVRRT_%d.framework/Versions/%d", PR_GetEnv("HOME"), LIBOVR_PRODUCT_VERSION, LIBOVR_MAJOR_VERSION);
|
||||
libSearchPaths.AppendElement(searchPath);
|
||||
}
|
||||
libName.AppendPrintf("LibOVRRT_%d", LIBOVR_PRODUCT_VERSION);
|
||||
// The following will match the va_list overload of AppendPrintf if the product version is 0
|
||||
// That's bad times.
|
||||
//libName.AppendPrintf("LibOVRRT_%d", LIBOVR_PRODUCT_VERSION);
|
||||
libName.Append("LibOVRRT_");
|
||||
libName.AppendInt(LIBOVR_PRODUCT_VERSION);
|
||||
#else
|
||||
libSearchPaths.AppendElement(nsCString("/usr/local/lib"));
|
||||
libSearchPaths.AppendElement(nsCString("/usr/lib"));
|
||||
|
|
|
@ -41,10 +41,10 @@
|
|||
|
||||
.toolbar-buttons > li {
|
||||
background-position: center;
|
||||
background-size: 32px 32px;
|
||||
background-size: 24px 24px;
|
||||
background-repeat: no-repeat;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
margin: 0 15px;
|
||||
}
|
||||
|
||||
|
|
|
@ -420,10 +420,9 @@ this.BrowserIDManager.prototype = {
|
|||
* The current state of the auth credentials.
|
||||
*
|
||||
* This essentially validates that enough credentials are available to use
|
||||
* Sync, although it effectively ignores the state of the master-password -
|
||||
* if that's locked and that's the only problem we can see, say everything
|
||||
* is OK - unlockAndVerifyAuthState will be used to perform the unlock
|
||||
* and re-verification if necessary.
|
||||
* Sync. It doesn't check we have all the keys we need as the master-password
|
||||
* may have been locked when we tried to get them - we rely on
|
||||
* unlockAndVerifyAuthState to check that for us.
|
||||
*/
|
||||
get currentAuthState() {
|
||||
if (this._authFailureReason) {
|
||||
|
@ -438,15 +437,6 @@ this.BrowserIDManager.prototype = {
|
|||
return LOGIN_FAILED_NO_USERNAME;
|
||||
}
|
||||
|
||||
// No need to check this.syncKey as our getter for that attribute
|
||||
// uses this.syncKeyBundle
|
||||
// If bundle creation started, but failed due to any reason other than
|
||||
// the MP being locked...
|
||||
if (this._shouldHaveSyncKeyBundle && !this.syncKeyBundle && !Utils.mpLocked()) {
|
||||
// Return a state that says a re-auth is necessary so we can get keys.
|
||||
return LOGIN_FAILED_LOGIN_REJECTED;
|
||||
}
|
||||
|
||||
return STATUS_OK;
|
||||
},
|
||||
|
||||
|
@ -467,11 +457,13 @@ this.BrowserIDManager.prototype = {
|
|||
*/
|
||||
unlockAndVerifyAuthState: function() {
|
||||
if (this._canFetchKeys()) {
|
||||
log.debug("unlockAndVerifyAuthState already has (or can fetch) sync keys");
|
||||
return Promise.resolve(STATUS_OK);
|
||||
}
|
||||
// so no keys - ensure MP unlocked.
|
||||
if (!Utils.ensureMPUnlocked()) {
|
||||
// user declined to unlock, so we don't know if they are stored there.
|
||||
log.debug("unlockAndVerifyAuthState: user declined to unlock master-password");
|
||||
return Promise.resolve(MASTER_PASSWORD_LOCKED);
|
||||
}
|
||||
// now we are unlocked we must re-fetch the user data as we may now have
|
||||
|
@ -482,7 +474,9 @@ this.BrowserIDManager.prototype = {
|
|||
// If we still can't get keys it probably means the user authenticated
|
||||
// without unlocking the MP or cleared the saved logins, so we've now
|
||||
// lost them - the user will need to reauth before continuing.
|
||||
return this._canFetchKeys() ? STATUS_OK : LOGIN_FAILED_LOGIN_REJECTED;
|
||||
let result = this._canFetchKeys() ? STATUS_OK : LOGIN_FAILED_LOGIN_REJECTED;
|
||||
log.debug("unlockAndVerifyAuthState re-fetched credentials and is returning", result);
|
||||
return result;
|
||||
}
|
||||
);
|
||||
},
|
||||
|
@ -529,6 +523,7 @@ this.BrowserIDManager.prototype = {
|
|||
// return null for the token - sync calling unlockAndVerifyAuthState()
|
||||
// before actually syncing will setup the error states if necessary.
|
||||
if (!this._canFetchKeys()) {
|
||||
log.info("Unable to fetch keys (master-password locked?), so aborting token fetch");
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
|
|
|
@ -38,6 +38,9 @@ mv *.linux-x86_64.tar.bz2 $HOME/artifacts/target.linux-x86_64.tar.bz2
|
|||
mv *.linux-x86_64.json $HOME/artifacts/target.linux-x86_64.json
|
||||
mv *.tests.zip $HOME/artifacts/target.tests.zip
|
||||
|
||||
# If the simulator does not exist don't fail
|
||||
mv fxos-simulator* $HOME/artifacts/fxos-simulator.xpi || :
|
||||
|
||||
ccache -s
|
||||
|
||||
################################### build.sh ###################################
|
||||
|
|
|
@ -18,8 +18,8 @@ task:
|
|||
schedulerId: task-graph-scheduler
|
||||
|
||||
routes:
|
||||
- 'index.gecko.v1.{{project}}.revision.{{head_rev}}.{{build_name}}.{{build_type}}'
|
||||
- 'index.gecko.v1.{{project}}.latest.{{build_name}}.{{build_type}}'
|
||||
- 'index.gecko.v1.{{project}}.revision.linux.{{head_rev}}.{{build_name}}.{{build_type}}'
|
||||
- 'index.gecko.v1.{{project}}.latest.linux.{{build_name}}.{{build_type}}'
|
||||
scopes:
|
||||
# Nearly all of our build tasks use tc-vcs so just include the scope across
|
||||
# the board.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
$inherits:
|
||||
from: 'tasks/builds/b2g_emulator_base.yml'
|
||||
variables:
|
||||
build_name: 'emualtor-jb'
|
||||
build_name: 'emulator-jb'
|
||||
build_type: 'debug'
|
||||
task:
|
||||
workerType: emulator-jb-debug
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
$inherits:
|
||||
from: 'tasks/builds/b2g_emulator_base.yml'
|
||||
variables:
|
||||
build_name: 'emualtor-jb'
|
||||
build_name: 'emulator-jb'
|
||||
build_type: 'opt'
|
||||
task:
|
||||
workerType: emulator-jb
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
$inherits:
|
||||
from: 'tasks/builds/b2g_emulator_base.yml'
|
||||
variables:
|
||||
build_name: 'emualtor-kk'
|
||||
build_name: 'emulator-kk'
|
||||
build_type: 'debug'
|
||||
task:
|
||||
workerType: emulator-kk-debug
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
$inherits:
|
||||
from: 'tasks/builds/b2g_emulator_base.yml'
|
||||
variables:
|
||||
build_name: 'emualtor-kk'
|
||||
build_name: 'emulator-kk'
|
||||
build_type: 'opt'
|
||||
task:
|
||||
workerType: emulator-kk
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
$inherits:
|
||||
from: 'tasks/builds/b2g_emulator_base.yml'
|
||||
variables:
|
||||
build_name: 'emualtor-l'
|
||||
build_name: 'emulator-l'
|
||||
build_type: 'opt'
|
||||
task:
|
||||
workerType: emulator-l-debug
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
$inherits:
|
||||
from: 'tasks/builds/b2g_emulator_base.yml'
|
||||
variables:
|
||||
build_name: 'emualtor-l'
|
||||
build_name: 'emulator-l'
|
||||
build_type: 'opt'
|
||||
task:
|
||||
workerType: emulator-l
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
$inherits:
|
||||
from: 'tasks/builds/b2g_emulator_x86_base.yml'
|
||||
variables:
|
||||
build_name: 'emualtor-x86-kk'
|
||||
build_name: 'emulator-x86-kk'
|
||||
build_type: 'opt'
|
||||
task:
|
||||
workerType: emualtor-x86-kk
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
$inherits:
|
||||
from: 'tasks/builds/b2g_emulator_base.yml'
|
||||
variables:
|
||||
build_name: 'emualtor-x86-l'
|
||||
build_name: 'emulator-x86-l'
|
||||
build_type: 'opt'
|
||||
task:
|
||||
workerType: emulator-l
|
||||
|
|
|
@ -17,6 +17,10 @@ task:
|
|||
provisionerId: aws-provisioner
|
||||
schedulerId: task-graph-scheduler
|
||||
|
||||
routes:
|
||||
- 'index.gecko.v1.{{project}}.revision.linux.{{head_rev}}.{{build_name}}.{{build_type}}'
|
||||
- 'index.gecko.v1.{{project}}.latest.linux.{{build_name}}.{{build_type}}'
|
||||
|
||||
scopes:
|
||||
# Nearly all of our build tasks use tc-vcs so just include the scope across
|
||||
# the board.
|
||||
|
@ -55,6 +59,8 @@ task:
|
|||
MOZHARNESS_REF: '{{mozharness_ref}}'
|
||||
|
||||
extra:
|
||||
index:
|
||||
rank: {{pushlog_id}}
|
||||
treeherder:
|
||||
groupSymbol: tc
|
||||
groupName: Submitted by taskcluster
|
||||
|
|
|
@ -132,8 +132,9 @@ let WebProgressListener = {
|
|||
json.flags = aFlags;
|
||||
|
||||
// These properties can change even for a sub-frame navigation.
|
||||
json.canGoBack = docShell.canGoBack;
|
||||
json.canGoForward = docShell.canGoForward;
|
||||
let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
|
||||
json.canGoBack = webNav.canGoBack;
|
||||
json.canGoForward = webNav.canGoForward;
|
||||
|
||||
if (aWebProgress && aWebProgress.isTopLevel) {
|
||||
json.documentURI = content.document.documentURIObject.spec;
|
||||
|
|
|
@ -1014,11 +1014,7 @@ let Links = {
|
|||
this._decrementSiteMap(siteMap, existingLink);
|
||||
} else {
|
||||
// Update our copy's properties.
|
||||
for (let prop of this._sortProperties) {
|
||||
if (prop in aLink) {
|
||||
existingLink[prop] = aLink[prop];
|
||||
}
|
||||
}
|
||||
Object.assign(existingLink, aLink);
|
||||
|
||||
// Finally, reinsert our copy below.
|
||||
insertionLink = existingLink;
|
||||
|
|
|
@ -234,6 +234,13 @@ body.loaded {
|
|||
.dark > .container > .content blockquote {
|
||||
-moz-border-start: 2px solid #eeeeee;
|
||||
}
|
||||
.dark *::-moz-selection {
|
||||
background-color: #FFFFFF;
|
||||
color: #0095DD;
|
||||
}
|
||||
.dark a::-moz-selection {
|
||||
color: #DD4800;
|
||||
}
|
||||
|
||||
.content ul,
|
||||
.content ol {
|
||||
|
|
Загрузка…
Ссылка в новой задаче