Merge mozilla-central to mozilla-inbound

This commit is contained in:
Carsten "Tomcat" Book 2015-04-02 14:13:36 +02:00
commit e615b9f3a5
64 changed files with 1944 additions and 437 deletions

View File

@ -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>

View File

@ -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"/>

View File

@ -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"/>

View File

@ -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"/>

View File

@ -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"/>

View File

@ -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"/>

View File

@ -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"/>

View File

@ -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"/>

View File

@ -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"
}

View File

@ -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"/>

View File

@ -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"/>

View File

@ -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);

View File

@ -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");
}

View File

@ -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
====================

View File

@ -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

View File

@ -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
-------

View File

@ -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() {

View File

@ -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)
*/

View File

@ -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);
});
}

View File

@ -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);

View File

@ -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.

View File

@ -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":

View File

@ -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.

View File

@ -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.

View File

@ -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) {

View File

@ -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);
}

View File

@ -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)

View File

@ -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",

View File

@ -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>

View File

@ -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]

View File

@ -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" },
]
}];

View File

@ -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" },
]
}];

View File

@ -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" },
]
}];

View File

@ -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();
}

View File

@ -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;

View File

@ -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]"

View File

@ -19,7 +19,7 @@ let DetailsView = {
requires: ["timeline"]
},
"js-calltree": {
id: "js-calltree-view",
id: "js-profile-view",
view: JsCallTreeView
},
"js-flamegraph": {

View File

@ -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);

View File

@ -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',
]

View File

@ -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")}]`);

View File

@ -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);
};

View File

@ -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;
}
};

View File

@ -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">

View File

@ -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.

View File

@ -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;
});

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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"));

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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 ###################################

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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 {