diff --git a/b2g/app/b2g.js b/b2g/app/b2g.js index b6ffeb375af7..f919129fbdbe 100644 --- a/b2g/app/b2g.js +++ b/b2g/app/b2g.js @@ -1015,7 +1015,6 @@ pref("apz.allow_zooming", true); // Gaia relies heavily on scroll events for now, so lets fire them // more often than the default value (100). -pref("apz.asyncscroll.throttle", 40); pref("apz.pan_repaint_interval", 16); // APZ physics settings, tuned by UX designers diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 3b4da6bc52a7..2a4eda272c8c 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -5009,6 +5009,10 @@ nsBrowserAccess.prototype = { isTabContentWindow: function (aWindow) { return gBrowser.browsers.some(browser => browser.contentWindow == aWindow); }, + + canClose() { + return CanCloseWindow(); + }, } function getTogglableToolbars() { @@ -6565,6 +6569,26 @@ var IndexedDBPromptHelper = { } }; +function CanCloseWindow() +{ + // Avoid redundant calls to canClose from showing multiple + // PermitUnload dialogs. + if (window.skipNextCanClose) { + return true; + } + + for (let browser of gBrowser.browsers) { + let {permitUnload, timedOut} = browser.permitUnload(); + if (timedOut) { + return true; + } + if (!permitUnload) { + return false; + } + } + return true; +} + function WindowIsClosing() { if (TabView.isVisible()) { @@ -6575,27 +6599,19 @@ function WindowIsClosing() if (!closeWindow(false, warnAboutClosingWindow)) return false; - // Bug 967873 - Proxy nsDocumentViewer::PermitUnload to the child process - if (gMultiProcessBrowser) + // In theory we should exit here and the Window's internal Close + // method should trigger canClose on nsBrowserAccess. However, by + // that point it's too late to be able to show a prompt for + // PermitUnload. So we do it here, when we still can. + if (CanCloseWindow()) { + // This flag ensures that the later canClose call does nothing. + // It's only needed to make tests pass, since they detect the + // prompt even when it's not actually shown. + window.skipNextCanClose = true; return true; - - for (let browser of gBrowser.browsers) { - let ds = browser.docShell; - // Passing true to permitUnload indicates we plan on closing the window. - // This means that once unload is permitted, all further calls to - // permitUnload will be ignored. This avoids getting multiple prompts - // to unload the page. - if (ds.contentViewer && !ds.contentViewer.permitUnload(true)) { - // ... however, if the user aborts closing, we need to undo that, - // to ensure they get prompted again when we next try to close the window. - // We do this on the window's toplevel docshell instead of on the tab, so - // that all tabs we iterated before will get this reset. - window.getInterface(Ci.nsIDocShell).contentViewer.resetCloseWindow(); - return false; - } } - return true; + return false; } /** diff --git a/browser/base/content/chatWindow.xul b/browser/base/content/chatWindow.xul index aab0b7005ef3..61b87e2c9109 100644 --- a/browser/base/content/chatWindow.xul +++ b/browser/base/content/chatWindow.xul @@ -131,6 +131,11 @@ chatBrowserAccess.prototype = { isTabContentWindow: function (aWindow) { return this.contentWindow == aWindow; }, + + canClose() { + let {BrowserUtils} = Cu.import("resource://gre/modules/BrowserUtils.jsm", {}); + return BrowserUtils.canCloseWindow(window); + }, }; diff --git a/browser/base/content/sanitize.js b/browser/base/content/sanitize.js index 6110af47e922..f97ab580f73b 100644 --- a/browser/base/content/sanitize.js +++ b/browser/base/content/sanitize.js @@ -1,4 +1,4 @@ -// -*- indent-tabs-mode: nil; js-indent-level: 4 -*- +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- /* 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/. */ @@ -567,28 +567,17 @@ Sanitizer.prototype = { openWindows: { privateStateForNewWindow: "non-private", _canCloseWindow: function(aWindow) { - // Bug 967873 - Proxy nsDocumentViewer::PermitUnload to the child process - if (!aWindow.gMultiProcessBrowser) { - // Cargo-culted out of browser.js' WindowIsClosing because we don't care - // about TabView or the regular 'warn me before closing windows with N tabs' - // stuff here, and more importantly, we want to set aCallerClosesWindow to true - // when calling into permitUnload: - for (let browser of aWindow.gBrowser.browsers) { - let ds = browser.docShell; - // 'true' here means we will be closing the window soon, so please don't dispatch - // another onbeforeunload event when we do so. If unload is *not* permitted somewhere, - // we will reset the flag that this triggers everywhere so that we don't interfere - // with the browser after all: - if (ds.contentViewer && !ds.contentViewer.permitUnload(true)) { - return false; - } - } + if (aWindow.CanCloseWindow()) { + // We already showed PermitUnload for the window, so let's + // make sure we don't do it again when we actually close the + // window. + aWindow.skipNextCanClose = true; + return true; } - return true; }, _resetAllWindowClosures: function(aWindowList) { for (let win of aWindowList) { - win.getInterface(Ci.nsIDocShell).contentViewer.resetCloseWindow(); + win.skipNextCanClose = false; } }, clear: Task.async(function*() { diff --git a/browser/base/content/tabbrowser.xml b/browser/base/content/tabbrowser.xml index 317efad7bd1e..ad93bcb4e882 100644 --- a/browser/base/content/tabbrowser.xml +++ b/browser/base/content/tabbrowser.xml @@ -1156,25 +1156,29 @@ this._tabAttrModified(this.mCurrentTab, ["selected"]); if (oldBrowser != newBrowser && - oldBrowser.docShell && - oldBrowser.docShell.contentViewer.inPermitUnload) { - // Since the user is switching away from a tab that has - // a beforeunload prompt active, we remove the prompt. - // This prevents confusing user flows like the following: - // 1. User attempts to close Firefox - // 2. User switches tabs (ingoring a beforeunload prompt) - // 3. User returns to tab, presses "Leave page" - let promptBox = this.getTabModalPromptBox(oldBrowser); - let prompts = promptBox.listPrompts(); - // There might not be any prompts here if the tab was closed - // while in an onbeforeunload prompt, which will have - // destroyed aforementioned prompt already, so check there's - // something to remove, first: - if (prompts.length) { - // NB: This code assumes that the beforeunload prompt - // is the top-most prompt on the tab. - prompts[prompts.length - 1].abortPrompt(); - } + oldBrowser.getInPermitUnload) { + oldBrowser.getInPermitUnload(inPermitUnload => { + if (!inPermitUnload) { + return; + } + // Since the user is switching away from a tab that has + // a beforeunload prompt active, we remove the prompt. + // This prevents confusing user flows like the following: + // 1. User attempts to close Firefox + // 2. User switches tabs (ingoring a beforeunload prompt) + // 3. User returns to tab, presses "Leave page" + let promptBox = this.getTabModalPromptBox(oldBrowser); + let prompts = promptBox.listPrompts(); + // There might not be any prompts here if the tab was closed + // while in an onbeforeunload prompt, which will have + // destroyed aforementioned prompt already, so check there's + // something to remove, first: + if (prompts.length) { + // NB: This code assumes that the beforeunload prompt + // is the top-most prompt on the tab. + prompts[prompts.length - 1].abortPrompt(); + } + }); } oldBrowser._urlbarFocused = (gURLBar && gURLBar.focused); @@ -2103,6 +2107,7 @@ if (aParams) { var animate = aParams.animate; var byMouse = aParams.byMouse; + var skipPermitUnload = aParams.skipPermitUnload; } // Handle requests for synchronously removing an already @@ -2115,7 +2120,7 @@ var isLastTab = (this.tabs.length - this._removingTabs.length == 1); - if (!this._beginRemoveTab(aTab, false, null, true)) + if (!this._beginRemoveTab(aTab, false, null, true, skipPermitUnload)) return; if (!aTab.pinned && !aTab.hidden && aTab._fullyOpen && byMouse) @@ -2163,6 +2168,7 @@ + diff --git a/browser/base/content/test/general/browser.ini b/browser/base/content/test/general/browser.ini index b85f09e86d77..1e75dfe31c62 100644 --- a/browser/base/content/test/general/browser.ini +++ b/browser/base/content/test/general/browser.ini @@ -149,7 +149,6 @@ skip-if = e10s # Bug 1101993 - times out for unknown reasons when run in the dir [browser_backButtonFitts.js] skip-if = os == "mac" # The Fitt's Law back button is not supported on OS X [browser_beforeunload_duplicate_dialogs.js] -skip-if = e10s # bug 967873 means permitUnload doesn't work in e10s mode [browser_blob-channelname.js] [browser_bookmark_titles.js] skip-if = buildapp == 'mulet' || toolkit == "windows" # Disabled on Windows due to frequent failures (bugs 825739, 841341) diff --git a/browser/base/content/urlbarBindings.xml b/browser/base/content/urlbarBindings.xml index b0482c4db9df..eb6f3043d1b6 100644 --- a/browser/base/content/urlbarBindings.xml +++ b/browser/base/content/urlbarBindings.xml @@ -157,6 +157,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. if (action) { switch (action.type) { case "switchtab": // Fall through. + case "remotetab": // Fall through. case "visiturl": { returnValue = action.params.url; break; @@ -1490,6 +1491,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. ]; break; case "switchtab": + case "remotetab": parts = [ item.getAttribute("title"), action.params.url, diff --git a/browser/components/nsBrowserGlue.js b/browser/components/nsBrowserGlue.js index 71ce7738b26f..e803c3cc5d86 100644 --- a/browser/components/nsBrowserGlue.js +++ b/browser/components/nsBrowserGlue.js @@ -575,6 +575,7 @@ BrowserGlue.prototype = { switchtab: 6, tag: 7, visiturl: 8, + remotetab: 9, }; if (actionType in buckets) { Services.telemetry @@ -1329,7 +1330,7 @@ BrowserGlue.prototype = { let wins = Services.wm.getEnumerator("navigator:browser"); while (wins.hasMoreElements()) { let win = wins.getNext(); - if (win.TabView._tabBrowserHasHiddenTabs() && win.TabView.firstUseExperienced()) { + if (win.TabView._tabBrowserHasHiddenTabs() && win.TabView.firstUseExperienced) { haveTabGroups = true; break; } @@ -1905,7 +1906,7 @@ BrowserGlue.prototype = { }, _migrateUI: function BG__migrateUI() { - const UI_VERSION = 33; + const UI_VERSION = 34; const BROWSER_DOCURL = "chrome://browser/content/browser.xul"; let currentUIVersion = 0; try { @@ -2250,7 +2251,7 @@ BrowserGlue.prototype = { this._notifyNotificationsUpgrade().catch(Cu.reportError); } - if (currentUIVersion < 33) { + if (currentUIVersion < 34) { // We'll do something once windows are open: this._mayNeedToWarnAboutTabGroups = true; } diff --git a/browser/themes/linux/browser.css b/browser/themes/linux/browser.css index c7a8fdfea8aa..0d9c4375349b 100644 --- a/browser/themes/linux/browser.css +++ b/browser/themes/linux/browser.css @@ -1176,7 +1176,7 @@ richlistitem[type~="action"][actiontype="searchengine"][selected="true"] > .ac-t font-size: 0.9em; } -richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-icon { +richlistitem[type~="action"][actiontype$="tab"] > .ac-url-box > .ac-action-icon { list-style-image: url("chrome://browser/skin/actionicon-tab.png"); padding: 0 3px; } diff --git a/browser/themes/osx/browser.css b/browser/themes/osx/browser.css index 7c1d6538934b..131f98ff8162 100644 --- a/browser/themes/osx/browser.css +++ b/browser/themes/osx/browser.css @@ -1854,13 +1854,13 @@ richlistitem[type~="action"][actiontype="searchengine"][selected="true"] > .ac-t color: -moz-nativehyperlinktext; } -richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-icon { +richlistitem[type~="action"][actiontype$="tab"] > .ac-url-box > .ac-action-icon { list-style-image: url("chrome://browser/skin/actionicon-tab.png"); -moz-image-region: rect(0, 16px, 11px, 0); padding: 0 3px; } -richlistitem[type~="action"][actiontype="switchtab"][selected="true"] > .ac-url-box > .ac-action-icon { +richlistitem[type~="action"][actiontype$="tab"][selected="true"] > .ac-url-box > .ac-action-icon { -moz-image-region: rect(11px, 16px, 22px, 0); } @@ -1879,13 +1879,13 @@ richlistitem[type~="action"][actiontype="switchtab"][selected="true"] > .ac-url- list-style-image: url("chrome://browser/skin/places/tag@2x.png"); } - richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-icon { + richlistitem[type~="action"][actiontype$="tab"] > .ac-url-box > .ac-action-icon { list-style-image: url("chrome://browser/skin/actionicon-tab@2x.png"); -moz-image-region: rect(0, 32px, 22px, 0); width: 22px; } - richlistitem[type~="action"][actiontype="switchtab"][selected="true"] > .ac-url-box > .ac-action-icon { + richlistitem[type~="action"][actiontype$="tab"][selected="true"] > .ac-url-box > .ac-action-icon { -moz-image-region: rect(22px, 32px, 44px, 0); } } diff --git a/browser/themes/windows/browser.css b/browser/themes/windows/browser.css index 87dddd92a275..d528c1f47e91 100644 --- a/browser/themes/windows/browser.css +++ b/browser/themes/windows/browser.css @@ -1537,7 +1537,7 @@ richlistitem[type~="action"][actiontype="searchengine"] > .ac-title-box > .ac-si } } -richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-icon { +richlistitem[type~="action"][actiontype$="tab"] > .ac-url-box > .ac-action-icon { list-style-image: url("chrome://browser/skin/actionicon-tab.png"); -moz-image-region: rect(0, 16px, 11px, 0); padding: 0 3px; @@ -1546,7 +1546,7 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action- } @media (min-resolution: 1.1dppx) { - richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-icon { + richlistitem[type~="action"][actiontype$="tab"] > .ac-url-box > .ac-action-icon { list-style-image: url("chrome://browser/skin/actionicon-tab@2x.png"); -moz-image-region: rect(0, 32px, 22px, 0); } @@ -1556,12 +1556,12 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action- not all and (-moz-windows-default-theme) { @media not all and (-moz-os-version: windows-win7), not all and (-moz-windows-default-theme) { - richlistitem[type~="action"][actiontype="switchtab"][selected="true"] > .ac-url-box > .ac-action-icon { + richlistitem[type~="action"][actiontype$="tab"][selected="true"] > .ac-url-box > .ac-action-icon { -moz-image-region: rect(11px, 16px, 22px, 0); } @media (min-resolution: 1.1dppx) { - richlistitem[type~="action"][actiontype="switchtab"][selected="true"] > .ac-url-box > .ac-action-icon { + richlistitem[type~="action"][actiontype$="tab"][selected="true"] > .ac-url-box > .ac-action-icon { -moz-image-region: rect(22px, 32px, 44px, 0); } } diff --git a/caps/nsNullPrincipal.h b/caps/nsNullPrincipal.h index 04c7b0590bd2..c966902c2092 100644 --- a/caps/nsNullPrincipal.h +++ b/caps/nsNullPrincipal.h @@ -71,7 +71,6 @@ public: bool MayLoadInternal(nsIURI* aURI) override; nsCOMPtr mURI; - nsCOMPtr mCSP; }; #endif // nsNullPrincipal_h__ diff --git a/caps/nsPrincipal.cpp b/caps/nsPrincipal.cpp index a97d2b7bd424..80e2c1ad57a7 100644 --- a/caps/nsPrincipal.cpp +++ b/caps/nsPrincipal.cpp @@ -25,6 +25,7 @@ #include "nsNetCID.h" #include "jswrapper.h" +#include "mozilla/dom/nsCSPContext.h" #include "mozilla/dom/ScriptSettings.h" #include "mozilla/Preferences.h" #include "mozilla/HashFunctions.h" @@ -76,7 +77,12 @@ nsPrincipal::nsPrincipal() { } nsPrincipal::~nsPrincipal() -{ } +{ + // let's clear the principal within the csp to avoid a tangling pointer + if (mCSP) { + static_cast(mCSP.get())->clearLoadingPrincipal(); + } +} nsresult nsPrincipal::Init(nsIURI *aCodebase, const OriginAttributes& aOriginAttributes) @@ -404,7 +410,7 @@ nsPrincipal::Read(nsIObjectInputStream* aStream) // need to link in the CSP context here (link in the URI of the protected // resource). if (csp) { - csp->SetRequestContext(codebase, nullptr, nullptr); + csp->SetRequestContext(nullptr, this); } SetDomain(domain); diff --git a/config/external/nss/nss.def b/config/external/nss/nss.def index ea18665d1303..2e631536ce3b 100644 --- a/config/external/nss/nss.def +++ b/config/external/nss/nss.def @@ -668,6 +668,7 @@ SSL_SetStapledOCSPResponses SSL_SetURL SSL_SNISocketConfigHook SSL_VersionRangeGet +SSL_VersionRangeGetDefault SSL_VersionRangeGetSupported SSL_VersionRangeSet SSL_VersionRangeSetDefault diff --git a/config/faster/rules.mk b/config/faster/rules.mk index cbf0f2ca2742..50a383c3ee8c 100644 --- a/config/faster/rules.mk +++ b/config/faster/rules.mk @@ -127,6 +127,8 @@ $(addprefix $(TOPOBJDIR)/,$(MANIFEST_TARGETS)): FORCE # Files to build with the recursive backend and simply copy $(TOPOBJDIR)/dist/bin/platform.ini: $(TOPOBJDIR)/toolkit/xre/platform.ini +$(TOPOBJDIR)/toolkit/xre/platform.ini: $(TOPOBJDIR)/config/buildid + # The xpidl target in config/makefiles/xpidl requires the install manifest for # dist/idl to have been processed. $(TOPOBJDIR)/config/makefiles/xpidl/xpidl: $(TOPOBJDIR)/install-dist_idl diff --git a/devtools/client/locales/en-US/memory.properties b/devtools/client/locales/en-US/memory.properties index 61f6a45b9053..f417a871f77d 100644 --- a/devtools/client/locales/en-US/memory.properties +++ b/devtools/client/locales/en-US/memory.properties @@ -32,6 +32,10 @@ snapshot.io.save=Save # saving a snapshot to disk. snapshot.io.save.window=Save Heap Snapshot +# LOCALIZATION NOTE (snapshot.io.import.window): The title for the window displayed when +# importing a snapshot form disk. +snapshot.io.import.window=Import Heap Snapshot + # LOCALIZATION NOTE (snapshot.io.filter): The title for the filter used to # filter file types (*.fxsnapshot) snapshot.io.filter=Firefox Heap Snapshots @@ -60,6 +64,10 @@ toolbar.breakdownBy=Group by: # taking a snapshot, either as the main label, or a tooltip. take-snapshot=Take snapshot +# LOCALIZATION NOTE (import-snapshot): The label describing the button that initiates +# importing a snapshot. +import-snapshot=Import… + # LOCALIZATION NOTE (filter.placeholder): The placeholder text used for the # memory tool's filter search box. filter.placeholder=Filter @@ -89,6 +97,11 @@ tree-item.percent=%S% # state SAVING, used in the main heap view. snapshot.state.saving.full=Saving snapshot… +# LOCALIZATION NOTE (snapshot.state.importing.full): The label describing the snapshot +# state IMPORTING, used in the main heap view. %S represents the basename of the file +# imported. +snapshot.state.importing.full=Importing %S… + # LOCALIZATION NOTE (snapshot.state.reading.full): The label describing the snapshot # state READING, and SAVED, due to these states being combined visually, used # in the main heap view. @@ -106,6 +119,10 @@ snapshot.state.error.full=There was an error processing this snapshot. # state SAVING, used in the snapshot list view snapshot.state.saving=Saving snapshot… +# LOCALIZATION NOTE (snapshot.state.importing): The label describing the snapshot +# state IMPORTING, used in the snapshot list view +snapshot.state.importing=Importing snapshot… + # LOCALIZATION NOTE (snapshot.state.reading): The label describing the snapshot # state READING, and SAVED, due to these states being combined visually, used # in the snapshot list view. diff --git a/devtools/client/memory/actions/io.js b/devtools/client/memory/actions/io.js index 89bea8ccffbf..0b912c2bf8e7 100644 --- a/devtools/client/memory/actions/io.js +++ b/devtools/client/memory/actions/io.js @@ -3,9 +3,10 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; -const { assert } = require("devtools/shared/DevToolsUtils"); +const { reportException, assert } = require("devtools/shared/DevToolsUtils"); const { snapshotState: states, actions } = require("../constants"); -const { L10N, openFilePicker } = require("../utils"); +const { L10N, openFilePicker, createSnapshot } = require("../utils"); +const { readSnapshot, takeCensus, selectSnapshot } = require("./snapshot"); const { OS } = require("resource://gre/modules/osfile.jsm"); const VALID_EXPORT_STATES = [states.SAVED, states.READ, states.SAVING_CENSUS, states.SAVED_CENSUS]; @@ -14,7 +15,8 @@ exports.pickFileAndExportSnapshot = function (snapshot) { let outputFile = yield openFilePicker({ title: L10N.getFormatStr("snapshot.io.save.window"), defaultName: OS.Path.basename(snapshot.path), - filters: [[L10N.getFormatStr("snapshot.io.filter"), "*.fxsnapshot"]] + filters: [[L10N.getFormatStr("snapshot.io.filter"), "*.fxsnapshot"]], + mode: "save", }); if (!outputFile) { @@ -43,3 +45,43 @@ const exportSnapshot = exports.exportSnapshot = function (snapshot, dest) { dispatch({ type: actions.EXPORT_SNAPSHOT_END, snapshot }); }; }; + +const pickFileAndImportSnapshotAndCensus = exports.pickFileAndImportSnapshotAndCensus = function (heapWorker) { + return function* (dispatch, getState) { + let input = yield openFilePicker({ + title: L10N.getFormatStr("snapshot.io.import.window"), + filters: [[L10N.getFormatStr("snapshot.io.filter"), "*.fxsnapshot"]], + mode: "open", + }); + + if (!input) { + return; + } + + yield dispatch(importSnapshotAndCensus(heapWorker, input.path)); + }; +}; + +const importSnapshotAndCensus = exports.importSnapshotAndCensus = function (heapWorker, path) { + return function* (dispatch, getState) { + let snapshot = createSnapshot(); + + // Override the defaults for a new snapshot + snapshot.path = path; + snapshot.state = states.IMPORTING; + snapshot.imported = true; + + dispatch({ type: actions.IMPORT_SNAPSHOT_START, snapshot }); + dispatch(selectSnapshot(snapshot)); + + try { + yield dispatch(readSnapshot(heapWorker, snapshot)); + yield dispatch(takeCensus(heapWorker, snapshot)); + } catch (error) { + reportException("importSnapshot", error); + dispatch({ type: actions.IMPORT_SNAPSHOT_ERROR, error, snapshot }); + } + + dispatch({ type: actions.IMPORT_SNAPSHOT_END, snapshot }); + }; +}; diff --git a/devtools/client/memory/actions/snapshot.js b/devtools/client/memory/actions/snapshot.js index a8263df1b89b..48991d212d04 100644 --- a/devtools/client/memory/actions/snapshot.js +++ b/devtools/client/memory/actions/snapshot.js @@ -72,7 +72,7 @@ const takeSnapshot = exports.takeSnapshot = function (front) { */ const readSnapshot = exports.readSnapshot = function readSnapshot (heapWorker, snapshot) { return function *(dispatch, getState) { - assert(snapshot.state === states.SAVED, + assert([states.SAVED, states.IMPORTING].includes(snapshot.state), `Should only read a snapshot once. Found snapshot in state ${snapshot.state}`); let creationTime; diff --git a/devtools/client/memory/app.js b/devtools/client/memory/app.js index 878164146097..fa539dff36e4 100644 --- a/devtools/client/memory/app.js +++ b/devtools/client/memory/app.js @@ -9,7 +9,7 @@ const { toggleRecordingAllocationStacks } = require("./actions/allocations"); const { setBreakdownAndRefresh } = require("./actions/breakdown"); const { toggleInvertedAndRefresh } = require("./actions/inverted"); const { setFilterStringAndRefresh } = require("./actions/filter"); -const { pickFileAndExportSnapshot } = require("./actions/io"); +const { pickFileAndExportSnapshot, pickFileAndImportSnapshotAndCensus } = require("./actions/io"); const { selectSnapshotAndRefresh, takeSnapshotAndCensus } = require("./actions/snapshot"); const { breakdownNameToSpec, getBreakdownDisplayData } = require("./utils"); const Toolbar = createFactory(require("./components/toolbar")); @@ -61,6 +61,7 @@ const App = createClass({ Toolbar({ breakdowns: getBreakdownDisplayData(), + onImportClick: () => dispatch(pickFileAndImportSnapshotAndCensus(heapWorker)), onTakeSnapshotClick: () => dispatch(takeSnapshotAndCensus(front, heapWorker)), onBreakdownChange: breakdown => dispatch(setBreakdownAndRefresh(heapWorker, breakdownNameToSpec(breakdown))), diff --git a/devtools/client/memory/components/heap.js b/devtools/client/memory/components/heap.js index ec5a924d0417..8584605c2ef2 100644 --- a/devtools/client/memory/components/heap.js +++ b/devtools/client/memory/components/heap.js @@ -113,6 +113,7 @@ const Heap = module.exports = createClass({ dom.pre({}, safeErrorString(snapshot.error)) ]; break; + case states.IMPORTING: case states.SAVING: case states.SAVED: case states.READING: diff --git a/devtools/client/memory/components/toolbar.js b/devtools/client/memory/components/toolbar.js index 964cf6a3f442..ec72e0dfb7db 100644 --- a/devtools/client/memory/components/toolbar.js +++ b/devtools/client/memory/components/toolbar.js @@ -14,6 +14,7 @@ const Toolbar = module.exports = createClass({ displayName: PropTypes.string.isRequired, })).isRequired, onTakeSnapshotClick: PropTypes.func.isRequired, + onImportClick: PropTypes.func.isRequired, onBreakdownChange: PropTypes.func.isRequired, onToggleRecordAllocationStacks: PropTypes.func.isRequired, allocations: models.allocations, @@ -26,6 +27,7 @@ const Toolbar = module.exports = createClass({ render() { let { onTakeSnapshotClick, + onImportClick, onBreakdownChange, breakdowns, onToggleRecordAllocationStacks, @@ -45,6 +47,14 @@ const Toolbar = module.exports = createClass({ title: L10N.getStr("take-snapshot") }), + dom.button({ + id: "import-snapshot", + className: "devtools-toolbarbutton import-snapshot devtools-button", + onClick: onImportClick, + title: L10N.getStr("import-snapshot"), + "data-text-only": true, + }, L10N.getStr("import-snapshot")), + dom.div({ className: "toolbar-group" }, dom.label({ className: "breakdown-by" }, L10N.getStr("toolbar.breakdownBy"), diff --git a/devtools/client/memory/constants.js b/devtools/client/memory/constants.js index 3e23301c23ed..40a4b5afd1fb 100644 --- a/devtools/client/memory/constants.js +++ b/devtools/client/memory/constants.js @@ -29,6 +29,12 @@ actions.EXPORT_SNAPSHOT_START = "export-snapshot-start"; actions.EXPORT_SNAPSHOT_END = "export-snapshot-end"; actions.EXPORT_SNAPSHOT_ERROR = "export-snapshot-error"; +// When a heap snapshot is being read from a user selected file, +// and represents the entire state until the census is available. +actions.IMPORT_SNAPSHOT_START = "import-snapshot-start"; +actions.IMPORT_SNAPSHOT_END = "import-snapshot-end"; +actions.IMPORT_SNAPSHOT_ERROR = "import-snapshot-error"; + // Fired by UI to select a snapshot to view. actions.SELECT_SNAPSHOT = "select-snapshot"; @@ -93,14 +99,15 @@ const snapshotState = exports.snapshotState = {}; * Various states a snapshot can be in. * An FSM describing snapshot states: * - * SAVING -> SAVED -> READING -> READ <- <- <- SAVED_CENSUS - * ↘ ↗ - * SAVING_CENSUS + * SAVING -> SAVED -> READING -> READ SAVED_CENSUS + * IMPORTING ↗ ↘ ↑ ↓ + * SAVING_CENSUS * * Any of these states may go to the ERROR state, from which they can never * leave (mwah ha ha ha!) */ snapshotState.ERROR = "snapshot-state-error"; +snapshotState.IMPORTING = "snapshot-state-importing"; snapshotState.SAVING = "snapshot-state-saving"; snapshotState.SAVED = "snapshot-state-saved"; snapshotState.READING = "snapshot-state-reading"; diff --git a/devtools/client/memory/models.js b/devtools/client/memory/models.js index 707814c8b5ee..0989ebc1bea7 100644 --- a/devtools/client/memory/models.js +++ b/devtools/client/memory/models.js @@ -39,13 +39,15 @@ let snapshotModel = exports.snapshot = PropTypes.shape({ filter: PropTypes.string, // If an error was thrown while processing this snapshot, the `Error` instance is attached here. error: PropTypes.object, + // Boolean indicating whether or not this snapshot was imported. + imported: PropTypes.bool.isRequired, // The creation time of the snapshot; required after the snapshot has been read. creationTime: PropTypes.number, // The current state the snapshot is in. // @see ./constants.js state: function (snapshot, propName) { let current = snapshot.state; - let shouldHavePath = [states.SAVED, states.READ, states.SAVING_CENSUS, states.SAVED_CENSUS]; + let shouldHavePath = [states.IMPORTING, states.SAVED, states.READ, states.SAVING_CENSUS, states.SAVED_CENSUS]; let shouldHaveCreationTime = [states.READ, states.SAVING_CENSUS, states.SAVED_CENSUS]; let shouldHaveCensus = [states.SAVED_CENSUS]; diff --git a/devtools/client/memory/reducers/snapshots.js b/devtools/client/memory/reducers/snapshots.js index c16157dcaae2..0b18cbb3c00f 100644 --- a/devtools/client/memory/reducers/snapshots.js +++ b/devtools/client/memory/reducers/snapshots.js @@ -28,6 +28,8 @@ handlers[actions.TAKE_SNAPSHOT_END] = function (snapshots, action) { }); }; +handlers[actions.IMPORT_SNAPSHOT_START] = handlers[actions.TAKE_SNAPSHOT_START]; + handlers[actions.READ_SNAPSHOT_START] = function (snapshots, action) { let snapshot = getSnapshot(snapshots, action.snapshot); snapshot.state = states.READING; diff --git a/devtools/client/memory/test/unit/head.js b/devtools/client/memory/test/unit/head.js index c33f47209278..29927572fe5c 100644 --- a/devtools/client/memory/test/unit/head.js +++ b/devtools/client/memory/test/unit/head.js @@ -119,3 +119,12 @@ function isBreakdownType (census, type) { throw new Error(`isBreakdownType does not yet support ${type}`); } } + +function *createTempFile () { + let file = FileUtils.getFile("TmpD", ["tmp.fxsnapshot"]); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); + let destPath = file.path; + let stat = yield OS.File.stat(destPath); + ok(stat.size === 0, "new file is 0 bytes at start"); + return destPath; +} diff --git a/devtools/client/memory/test/unit/test_action-export-snapshot.js b/devtools/client/memory/test/unit/test_action-export-snapshot.js index 22b19b29f0b3..1caa09d535fb 100644 --- a/devtools/client/memory/test/unit/test_action-export-snapshot.js +++ b/devtools/client/memory/test/unit/test_action-export-snapshot.js @@ -18,12 +18,7 @@ add_task(function *() { let store = Store(); const { getState, dispatch } = store; - let file = FileUtils.getFile("TmpD", ["tmp.fxsnapshot"]); - file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); - let destPath = file.path; - let stat = yield OS.File.stat(destPath); - ok(stat.size === 0, "new file is 0 bytes at start"); - + let destPath = yield createTempFile(); dispatch(takeSnapshotAndCensus(front, heapWorker)); yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]); diff --git a/devtools/client/memory/test/unit/test_action-import-snapshot-and-census.js b/devtools/client/memory/test/unit/test_action-import-snapshot-and-census.js new file mode 100644 index 000000000000..5c861086e784 --- /dev/null +++ b/devtools/client/memory/test/unit/test_action-import-snapshot-and-census.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the task creator `importSnapshotAndCensus()` for the whole flow of + * importing a snapshot, and its sub-actions. + */ + +let { actions, snapshotState: states } = require("devtools/client/memory/constants"); +let { breakdownEquals } = require("devtools/client/memory/utils"); +let { exportSnapshot, importSnapshotAndCensus } = require("devtools/client/memory/actions/io"); +let { takeSnapshotAndCensus } = require("devtools/client/memory/actions/snapshot"); + +function run_test() { + run_next_test(); +} + +add_task(function *() { + let front = new StubbedMemoryFront(); + let heapWorker = new HeapAnalysesClient(); + yield front.attach(); + let store = Store(); + let { subscribe, dispatch, getState } = store; + + let destPath = yield createTempFile(); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]); + + let exportEvents = Promise.all([ + waitUntilAction(store, actions.EXPORT_SNAPSHOT_START), + waitUntilAction(store, actions.EXPORT_SNAPSHOT_END) + ]); + dispatch(exportSnapshot(getState().snapshots[0], destPath)); + yield exportEvents; + + // Now import our freshly exported snapshot + let i = 0; + let expected = ["IMPORTING", "READING", "READ", "SAVING_CENSUS", "SAVED_CENSUS"]; + let expectStates = () => { + let snapshot = getState().snapshots[1]; + if (!snapshot) { + return; + } + let isCorrectState = snapshot.state === states[expected[i]]; + if (isCorrectState) { + ok(true, `Found expected state ${expected[i]}`); + i++; + } + }; + + let unsubscribe = subscribe(expectStates); + dispatch(importSnapshotAndCensus(heapWorker, destPath)); + + yield waitUntilState(store, () => i === 5); + unsubscribe(); + equal(i, 5, "importSnapshotAndCensus() produces the correct sequence of states in a snapshot"); + equal(getState().snapshots[1].state, states.SAVED_CENSUS, "imported snapshot is in SAVED_CENSUS state"); + ok(getState().snapshots[1].selected, "imported snapshot is selected"); + + // Check snapshot data + let snapshot1 = getState().snapshots[0]; + let snapshot2 = getState().snapshots[1]; + + ok(breakdownEquals(snapshot1.breakdown, snapshot2.breakdown), + "imported snapshot has correct breakdown"); + + // Clone the census data so we can destructively remove the ID/parents to compare + // equal census data + let census1 = stripUnique(JSON.parse(JSON.stringify(snapshot1.census))); + let census2 = stripUnique(JSON.parse(JSON.stringify(snapshot2.census))); + + equal(JSON.stringify(census1), JSON.stringify(census2), "Imported snapshot has correct census"); + + function stripUnique (obj) { + let children = obj.children || []; + for (let child of children) { + delete child.id; + delete child.parent; + stripUnique(child); + } + delete obj.id; + delete obj.parent; + return obj; + } +}); diff --git a/devtools/client/memory/test/unit/xpcshell.ini b/devtools/client/memory/test/unit/xpcshell.ini index 897db9754b6a..d0f3ed364c82 100644 --- a/devtools/client/memory/test/unit/xpcshell.ini +++ b/devtools/client/memory/test/unit/xpcshell.ini @@ -9,6 +9,7 @@ skip-if = toolkit == 'android' || toolkit == 'gonk' [test_action-filter-01.js] [test_action-filter-02.js] [test_action-filter-03.js] +[test_action-import-snapshot-and-census.js] [test_action-select-snapshot.js] [test_action-set-breakdown.js] [test_action-set-breakdown-and-refresh-01.js] diff --git a/devtools/client/memory/utils.js b/devtools/client/memory/utils.js index f95e4c38b244..9f18900c4c0b 100644 --- a/devtools/client/memory/utils.js +++ b/devtools/client/memory/utils.js @@ -9,6 +9,7 @@ const STRINGS_URI = "chrome://devtools/locale/memory.properties" const L10N = exports.L10N = new ViewHelpers.L10N(STRINGS_URI); const { URL } = require("sdk/url"); +const { OS } = require("resource://gre/modules/osfile.jsm"); const { assert } = require("devtools/shared/DevToolsUtils"); const { Preferences } = require("resource://gre/modules/Preferences.jsm"); const CUSTOM_BREAKDOWN_PREF = "devtools.memory.custom-breakdowns"; @@ -27,6 +28,11 @@ exports.getSnapshotTitle = function (snapshot) { return L10N.getStr("snapshot-title.loading"); } + if (snapshot.imported) { + // Strip out the extension if it's the expected ".fxsnapshot" + return OS.Path.basename(snapshot.path.replace(/\.fxsnapshot$/, "")); + } + let date = new Date(snapshot.creationTime / 1000); return date.toLocaleTimeString(void 0, { year: "2-digit", @@ -124,6 +130,8 @@ exports.getSnapshotStatusText = function (snapshot) { return L10N.getStr("snapshot.state.error"); case states.SAVING: return L10N.getStr("snapshot.state.saving"); + case states.IMPORTING: + return L10N.getStr("snapshot.state.importing"); case states.SAVED: case states.READING: return L10N.getStr("snapshot.state.reading"); @@ -155,6 +163,8 @@ exports.getSnapshotStatusTextFull = function (snapshot) { return L10N.getStr("snapshot.state.error.full"); case states.SAVING: return L10N.getStr("snapshot.state.saving.full"); + case states.IMPORTING: + return L10N.getFormatStr("snapshot.state.importing", OS.Path.basename(snapshot.path)); case states.SAVED: case states.READING: return L10N.getStr("snapshot.state.reading.full"); @@ -198,6 +208,8 @@ exports.createSnapshot = function createSnapshot () { state: states.SAVING, census: null, path: null, + imported: false, + selected: false, }; }; @@ -327,12 +339,21 @@ exports.parseSource = function (source) { * (like "*.json"). * @param {String} .defaultName * The default name chosen by the file picker window. + * @param {String} .mode + * The mode that this filepicker should open in. Can be "open" or "save". * @return {Promise} * The file selected by the user, or null, if cancelled. */ -exports.openFilePicker = function({ title, filters, defaultName }) { +exports.openFilePicker = function({ title, filters, defaultName, mode }) { + mode = mode === "save" ? Ci.nsIFilePicker.modeSave : + mode === "open" ? Ci.nsIFilePicker.modeOpen : null; + + if (mode == void 0) { + throw new Error("No valid mode specified for nsIFilePicker."); + } + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); - fp.init(window, title, Ci.nsIFilePicker.modeSave); + fp.init(window, title, mode); for (let filter of (filters || [])) { fp.appendFilter(filter[0], filter[1]); diff --git a/devtools/client/netmonitor/netmonitor-controller.js b/devtools/client/netmonitor/netmonitor-controller.js index 23e62c7fb962..f0dfdb03db98 100644 --- a/devtools/client/netmonitor/netmonitor-controller.js +++ b/devtools/client/netmonitor/netmonitor-controller.js @@ -18,10 +18,11 @@ const EVENTS = { TARGET_WILL_NAVIGATE: "NetMonitor:TargetWillNavigate", TARGET_DID_NAVIGATE: "NetMonitor:TargetNavigate", - // When a network event is received. + // When a network or timeline event is received. // See https://developer.mozilla.org/docs/Tools/Web_Console/remoting for // more information about what each packet is supposed to deliver. NETWORK_EVENT: "NetMonitor:NetworkEvent", + TIMELINE_EVENT: "NetMonitor:TimelineEvent", // When a network event is added to the view REQUEST_ADDED: "NetMonitor:RequestAdded", @@ -124,6 +125,7 @@ const Editor = require("devtools/client/sourceeditor/editor"); const {Tooltip} = require("devtools/client/shared/widgets/Tooltip"); const {ToolSidebar} = require("devtools/client/framework/sidebar"); const DevToolsUtils = require("devtools/shared/DevToolsUtils"); +const {TimelineFront} = require("devtools/server/actors/timeline"); XPCOMUtils.defineConstant(this, "EVENTS", EVENTS); XPCOMUtils.defineConstant(this, "ACTIVITY_TYPE", ACTIVITY_TYPE); @@ -163,21 +165,22 @@ Object.defineProperty(this, "NetworkHelper", { */ var NetMonitorController = { /** - * Initializes the view. + * Initializes the view and connects the monitor client. * * @return object * A promise that is resolved when the monitor finishes startup. */ - startupNetMonitor: function() { + startupNetMonitor: Task.async(function*() { if (this._startup) { - return this._startup; + return this._startup.promise; } - - NetMonitorView.initialize(); - - // Startup is synchronous, for now. - return this._startup = promise.resolve(); - }, + this._startup = promise.defer(); + { + NetMonitorView.initialize(); + yield this.connect(); + } + this._startup.resolve(); + }), /** * Destroys the view and disconnects the monitor client from the server. @@ -185,19 +188,19 @@ var NetMonitorController = { * @return object * A promise that is resolved when the monitor finishes shutdown. */ - shutdownNetMonitor: function() { + shutdownNetMonitor: Task.async(function*() { if (this._shutdown) { - return this._shutdown; + return this._shutdown.promise; } - - NetMonitorView.destroy(); - this.TargetEventsHandler.disconnect(); - this.NetworkEventsHandler.disconnect(); - this.disconnect(); - - // Shutdown is synchronous, for now. - return this._shutdown = promise.resolve(); - }, + this._shutdown = promise.defer();; + { + NetMonitorView.destroy(); + this.TargetEventsHandler.disconnect(); + this.NetworkEventsHandler.disconnect(); + yield this.disconnect(); + } + this._shutdown.resolve(); + }), /** * Initiates remote or chrome network monitoring based on the current target, @@ -210,47 +213,78 @@ var NetMonitorController = { */ connect: Task.async(function*() { if (this._connection) { - return this._connection; + return this._connection.promise; } + this._connection = promise.defer(); - let deferred = promise.defer(); - this._connection = deferred.promise; - - this.client = this._target.client; // Some actors like AddonActor or RootActor for chrome debugging // aren't actual tabs. if (this._target.isTabActor) { this.tabClient = this._target.activeTab; } - this.webConsoleClient = this._target.activeConsole; - this.webConsoleClient.setPreferences(NET_PREFS, () => { - this.TargetEventsHandler.connect(); - this.NetworkEventsHandler.connect(); - deferred.resolve(); - }); - yield deferred.promise; + let connectWebConsole = () => { + let deferred = promise.defer(); + this.webConsoleClient = this._target.activeConsole; + this.webConsoleClient.setPreferences(NET_PREFS, deferred.resolve); + return deferred.promise; + }; + + let connectTimeline = () => { + // Don't start up waiting for timeline markers if the server isn't + // recent enough to emit the markers we're interested in. + if (this._target.getTrait("documentLoadingMarkers")) { + this.timelineFront = new TimelineFront(this._target.client, this._target.form); + return this.timelineFront.start({ withDocLoadingEvents: true }); + } + }; + + yield connectWebConsole(); + yield connectTimeline(); + + this.TargetEventsHandler.connect(); + this.NetworkEventsHandler.connect(); + window.emit(EVENTS.CONNECTED); + + this._connection.resolve(); + this._connected = true; }), /** * Disconnects the debugger client and removes event handlers as necessary. */ - disconnect: function() { + disconnect: Task.async(function*() { + if (this._disconnection) { + return this._disconnection.promise; + } + this._disconnection = promise.defer(); + + // Wait for the connection to finish first. + yield this._connection.promise; + // When debugging local or a remote instance, the connection is closed by - // the RemoteTarget. - this._connection = null; - this.client = null; + // the RemoteTarget. The webconsole actor is stopped on disconnect. this.tabClient = null; this.webConsoleClient = null; - }, + + // The timeline front wasn't initialized and started if the server wasn't + // recent enough to emit the markers we were interested in. + if (this._target.getTrait("documentLoadingMarkers")) { + yield this.timelineFront.destroy(); + this.timelineFront = null; + } + + this._disconnection.resolve(); + this._connected = false; + }), /** * Checks whether the netmonitor connection is active. * @return boolean */ isConnected: function() { - return !!this.client; + return !!this._connected; }, /** @@ -391,15 +425,7 @@ var NetMonitorController = { get supportsPerfStats() { return this.tabClient && (this.tabClient.traits.reconfigure || !this._target.isApp); - }, - - _startup: null, - _shutdown: null, - _connection: null, - _currentActivity: null, - client: null, - tabClient: null, - webConsoleClient: null + } }; /** @@ -458,6 +484,8 @@ TargetEventsHandler.prototype = { if (NetMonitorController.getCurrentActivity() == ACTIVITY_TYPE.NONE) { NetMonitorView.showNetworkInspectorView(); } + // Clear any accumulated markers. + NetMonitorController.NetworkEventsHandler.clearMarkers(); window.emit(EVENTS.TARGET_WILL_NAVIGATE); break; @@ -481,8 +509,11 @@ TargetEventsHandler.prototype = { * Functions handling target network events. */ function NetworkEventsHandler() { + this._markers = []; + this._onNetworkEvent = this._onNetworkEvent.bind(this); this._onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this); + this._onDocLoadingMarker = this._onDocLoadingMarker.bind(this); this._onRequestHeaders = this._onRequestHeaders.bind(this); this._onRequestCookies = this._onRequestCookies.bind(this); this._onRequestPostData = this._onRequestPostData.bind(this); @@ -501,6 +532,20 @@ NetworkEventsHandler.prototype = { return NetMonitorController.webConsoleClient; }, + get timelineFront() { + return NetMonitorController.timelineFront; + }, + + get firstDocumentDOMContentLoadedTimestamp() { + let marker = this._markers.filter(e => e.name == "document::DOMContentLoaded")[0]; + return marker ? marker.unixTime / 1000 : -1; + }, + + get firstDocumentLoadTimestamp() { + let marker = this._markers.filter(e => e.name == "document::Load")[0]; + return marker ? marker.unixTime / 1000 : -1; + }, + /** * Connect to the current target client. */ @@ -508,9 +553,30 @@ NetworkEventsHandler.prototype = { dumpn("NetworkEventsHandler is connecting..."); this.webConsoleClient.on("networkEvent", this._onNetworkEvent); this.webConsoleClient.on("networkEventUpdate", this._onNetworkEventUpdate); + + if (this.timelineFront) { + this.timelineFront.on("doc-loading", this._onDocLoadingMarker); + } + this._displayCachedEvents(); }, + /** + * Disconnect from the client. + */ + disconnect: function() { + if (!this.client) { + return; + } + dumpn("NetworkEventsHandler is disconnecting..."); + this.webConsoleClient.off("networkEvent", this._onNetworkEvent); + this.webConsoleClient.off("networkEventUpdate", this._onNetworkEventUpdate); + + if (this.timelineFront) { + this.timelineFront.off("doc-loading", this._onDocLoadingMarker); + } + }, + /** * Display any network events already in the cache. */ @@ -531,15 +597,12 @@ NetworkEventsHandler.prototype = { }, /** - * Disconnect from the client. + * The "DOMContentLoaded" and "Load" events sent by the timeline actor. + * @param object marker */ - disconnect: function() { - if (!this.client) { - return; - } - dumpn("NetworkEventsHandler is disconnecting..."); - this.webConsoleClient.off("networkEvent", this._onNetworkEvent); - this.webConsoleClient.off("networkEventUpdate", this._onNetworkEventUpdate); + _onDocLoadingMarker: function(marker) { + window.emit(EVENTS.TIMELINE_EVENT, marker); + this._markers.push(marker); }, /** @@ -741,6 +804,13 @@ NetworkEventsHandler.prototype = { }); }, + /** + * Clears all accumulated markers. + */ + clearMarkers: function() { + this._markers.length = 0; + }, + /** * Fetches the full text of a LongString. * diff --git a/devtools/client/netmonitor/netmonitor-view.js b/devtools/client/netmonitor/netmonitor-view.js index 89e6564f1069..b9781fc8992f 100644 --- a/devtools/client/netmonitor/netmonitor-view.js +++ b/devtools/client/netmonitor/netmonitor-view.js @@ -30,6 +30,8 @@ const REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10; // px const REQUESTS_WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144]; const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32; // byte const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32; // byte +const REQUESTS_WATERFALL_DOMCONTENTLOADED_TICKS_COLOR_RGBA = [255, 0, 0, 128]; +const REQUESTS_WATERFALL_LOAD_TICKS_COLOR_RGBA = [0, 0, 255, 128]; const REQUEST_TIME_DECIMALS = 2; const HEADERS_SIZE_DECIMALS = 3; const CONTENT_SIZE_DECIMALS = 2; @@ -1086,6 +1088,7 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { * Removes all network requests and closes the sidebar if open. */ clear: function() { + NetMonitorController.NetworkEventsHandler.clearMarkers(); NetMonitorView.Sidebar.toggle(false); $("#details-pane-toggle").disabled = true; @@ -1962,6 +1965,19 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { } } + { + let t = NetMonitorController.NetworkEventsHandler.firstDocumentDOMContentLoadedTimestamp; + let delta = Math.floor((t - this._firstRequestStartedMillis) * aScale); + let [r, g, b, a] = REQUESTS_WATERFALL_DOMCONTENTLOADED_TICKS_COLOR_RGBA; + view32bit[delta] = (a << 24) | (r << 16) | (g << 8) | b; + } + { + let t = NetMonitorController.NetworkEventsHandler.firstDocumentLoadTimestamp; + let delta = Math.floor((t - this._firstRequestStartedMillis) * aScale); + let [r, g, b, a] = REQUESTS_WATERFALL_LOAD_TICKS_COLOR_RGBA; + view32bit[delta] = (a << 24) | (r << 16) | (g << 8) | b; + } + // Flush the image data and cache the waterfall background. pixelArray.set(view8bit); ctx.putImageData(imageData, 0, 0); diff --git a/devtools/client/netmonitor/panel.js b/devtools/client/netmonitor/panel.js index 47dd16524b7c..bb413d458aff 100644 --- a/devtools/client/netmonitor/panel.js +++ b/devtools/client/netmonitor/panel.js @@ -13,7 +13,6 @@ const DevToolsUtils = require("devtools/shared/DevToolsUtils"); function NetMonitorPanel(iframeWindow, toolbox) { this.panelWin = iframeWindow; this._toolbox = toolbox; - this._destroyer = null; this._view = this.panelWin.NetMonitorView; this._controller = this.panelWin.NetMonitorController; @@ -31,28 +30,25 @@ NetMonitorPanel.prototype = { * @return object * A promise that is resolved when the NetMonitor completes opening. */ - open: function() { - let targetPromise; + open: Task.async(function*() { + if (this._opening) { + return this._opening; + } + let deferred = promise.defer(); + this._opening = deferred.promise; // Local monitoring needs to make the target remote. if (!this.target.isRemote) { - targetPromise = this.target.makeRemote(); - } else { - targetPromise = promise.resolve(this.target); + yield this.target.makeRemote(); } - return targetPromise - .then(() => this._controller.startupNetMonitor()) - .then(() => this._controller.connect()) - .then(() => { - this.isReady = true; - this.emit("ready"); - return this; - }) - .then(null, function onError(aReason) { - DevToolsUtils.reportException("NetMonitorPanel.prototype.open", aReason); - }); - }, + yield this._controller.startupNetMonitor(); + this.isReady = true; + this.emit("ready"); + + deferred.resolve(this); + return this._opening; + }), // DevToolPanel API @@ -60,14 +56,17 @@ NetMonitorPanel.prototype = { return this._toolbox.target; }, - destroy: function() { - // Make sure this panel is not already destroyed. - if (this._destroyer) { - return this._destroyer; + destroy: Task.async(function*() { + if (this._destroying) { + return this._destroying; } + let deferred = promise.defer(); + this._destroying = deferred.promise; - return this._destroyer = this._controller.shutdownNetMonitor().then(() => { - this.emit("destroyed"); - }); - } + yield this._controller.shutdownNetMonitor(); + this.emit("destroyed"); + + deferred.resolve(); + return this._destroying; + }) }; diff --git a/devtools/client/netmonitor/test/browser.ini b/devtools/client/netmonitor/test/browser.ini index 854363df09b5..d3835d8c69dd 100644 --- a/devtools/client/netmonitor/test/browser.ini +++ b/devtools/client/netmonitor/test/browser.ini @@ -91,6 +91,7 @@ skip-if = e10s # Bug 1091596 [browser_net_prefs-reload.js] [browser_net_raw_headers.js] [browser_net_reload-button.js] +[browser_net_reload-markers.js] [browser_net_req-resp-bodies.js] [browser_net_resend.js] skip-if = e10s # Bug 1091612 diff --git a/devtools/client/netmonitor/test/browser_net_reload-markers.js b/devtools/client/netmonitor/test/browser_net_reload-markers.js new file mode 100644 index 000000000000..723ca3b830ef --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_reload-markers.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the empty-requests reload button works. + */ + + add_task(function* () { + let [tab, debuggee, monitor] = yield initNetMonitor(SINGLE_GET_URL); + info("Starting test... "); + + let { document, EVENTS, NetworkEventsHandler } = monitor.panelWin; + let button = document.querySelector("#requests-menu-reload-notice-button"); + button.click(); + + let deferred = promise.defer(); + let markers = []; + + monitor.panelWin.on(EVENTS.TIMELINE_EVENT, (_, marker) => { + markers.push(marker); + }); + + yield waitForNetworkEvents(monitor, 2); + yield waitUntil(() => markers.length == 2); + + ok(true, "Reloading finished"); + + is(markers[0].name, "document::DOMContentLoaded", + "The first received marker is correct."); + is(markers[1].name, "document::Load", + "The second received marker is correct."); + + teardown(monitor).then(finish); +}); + +function waitUntil(predicate, interval = 10) { + if (predicate()) { + return Promise.resolve(true); + } + return new Promise(resolve => { + setTimeout(function() { + waitUntil(predicate).then(() => resolve(true)); + }, interval); + }); +} diff --git a/devtools/client/netmonitor/test/browser_net_simple-init.js b/devtools/client/netmonitor/test/browser_net_simple-init.js index e132f2ba6ccd..273f3aa140eb 100644 --- a/devtools/client/netmonitor/test/browser_net_simple-init.js +++ b/devtools/client/netmonitor/test/browser_net_simple-init.js @@ -32,12 +32,12 @@ function test() { ok(aMonitor.isReady, "The network monitor panel appears to be ready (" + aTag + ")."); - ok(aMonitor._controller.client, - "There should be a client available at this point (" + aTag + ")."); ok(aMonitor._controller.tabClient, "There should be a tabClient available at this point (" + aTag + ")."); ok(aMonitor._controller.webConsoleClient, "There should be a webConsoleClient available at this point (" + aTag + ")."); + ok(aMonitor._controller.timelineFront, + "There should be a timelineFront available at this point (" + aTag + ")."); } function checkIfDestroyed(aTag) { @@ -50,12 +50,12 @@ function test() { gOk(aMonitor._controller._shutdown, "The network monitor controller object still exists and is destroyed (" + aTag + ")."); - gOk(!aMonitor._controller.client, - "There shouldn't be a client available after destruction (" + aTag + ")."); gOk(!aMonitor._controller.tabClient, "There shouldn't be a tabClient available after destruction (" + aTag + ")."); gOk(!aMonitor._controller.webConsoleClient, "There shouldn't be a webConsoleClient available after destruction (" + aTag + ")."); + gOk(!aMonitor._controller.timelineFront, + "There shouldn't be a timelineFront available after destruction (" + aTag + ")."); } executeSoon(() => { diff --git a/devtools/client/performance/legacy/front.js b/devtools/client/performance/legacy/front.js index 239fdc8977ee..4a47d876e863 100644 --- a/devtools/client/performance/legacy/front.js +++ b/devtools/client/performance/legacy/front.js @@ -46,6 +46,9 @@ const LegacyPerformanceFront = Class({ withMarkers: true, withTicks: true, withMemory: false, + withFrames: false, + withGCEvents: false, + withDocLoadingEvents: false, withAllocations: false, withJITOptimizations: false, }, diff --git a/devtools/client/performance/performance-controller.js b/devtools/client/performance/performance-controller.js index e7a5e13e2487..f44140bc76c4 100644 --- a/devtools/client/performance/performance-controller.js +++ b/devtools/client/performance/performance-controller.js @@ -258,8 +258,10 @@ var PerformanceController = { startRecording: Task.async(function *() { let options = { withMarkers: true, - withMemory: this.getOption("enable-memory"), withTicks: this.getOption("enable-framerate"), + withMemory: this.getOption("enable-memory"), + withFrames: true, + withGCEvents: true, withJITOptimizations: this.getOption("enable-jit-optimizations"), withAllocations: this.getOption("enable-allocations"), allocationsSampleProbability: this.getPref("memory-sample-probability"), diff --git a/devtools/client/themes/memory.css b/devtools/client/themes/memory.css index 806349495ed8..03eddecd209d 100644 --- a/devtools/client/themes/memory.css +++ b/devtools/client/themes/memory.css @@ -64,9 +64,10 @@ html, body, #app, #memory-tool { /** * We want this to be exactly at a --sidebar-width distance from the * toolbar's start boundary. A .devtools-toolbar has a 3px start padding - * and the preceeding .take-snapshot button is exactly 32px. + * and the preceeding .take-snapshot button is exactly 32px, and the import + * button 78px. */ - margin-inline-start: calc(var(--sidebar-width) - 3px - 32px); + margin-inline-start: calc(var(--sidebar-width) - 3px - 32px - 78px); border-inline-start: 1px solid var(--theme-splitter-color); padding-inline-start: 5px; } @@ -96,6 +97,14 @@ html, body, #app, #memory-tool { } } +/** + * Due to toolbar styles of `.devtools-toolbarbutton:not([label])` which overrides + * .devtools-toolbarbutton's min-width of 78px, reset the min-width. + */ +#import-snapshot { + min-width: 78px; +} + .spacer { flex: 1; } diff --git a/devtools/server/actors/performance.js b/devtools/server/actors/performance.js index 84aa5ea5c969..8c4947508a3e 100644 --- a/devtools/server/actors/performance.js +++ b/devtools/server/actors/performance.js @@ -46,8 +46,11 @@ var PerformanceActor = exports.PerformanceActor = protocol.ActorClass({ traits: { features: { withMarkers: true, - withMemory: true, withTicks: true, + withMemory: true, + withFrames: true, + withGCEvents: true, + withDocLoadingEvents: true, withAllocations: true, withJITOptimizations: true, }, diff --git a/devtools/server/actors/root.js b/devtools/server/actors/root.js index eb8d6465436d..a892d8fd854e 100644 --- a/devtools/server/actors/root.js +++ b/devtools/server/actors/root.js @@ -176,6 +176,9 @@ RootActor.prototype = { // Whether or not the MemoryActor's heap snapshot abilities are // fully equipped to handle heap snapshots for the memory tool. Fx44+ heapSnapshots: true, + // Whether or not the timeline actor can emit DOMContentLoaded and Load + // markers, currently in use by the network monitor. Fx45+ + documentLoadingMarkers: true }, /** diff --git a/devtools/server/actors/timeline.js b/devtools/server/actors/timeline.js index 6acd68871eed..f7d1d703bd9f 100644 --- a/devtools/server/actors/timeline.js +++ b/devtools/server/actors/timeline.js @@ -43,6 +43,15 @@ var TimelineActor = exports.TimelineActor = protocol.ActorClass({ typeName: "timeline", events: { + /** + * Events emitted when "DOMContentLoaded" and "Load" markers are received. + */ + "doc-loading" : { + type: "doc-loading", + marker: Arg(0, "json"), + endTime: Arg(0, "number") + }, + /** * The "markers" events emitted every DEFAULT_TIMELINE_DATA_PULL_TIMEOUT ms * at most, when profile markers are found. The timestamps on each marker @@ -139,8 +148,12 @@ var TimelineActor = exports.TimelineActor = protocol.ActorClass({ start: actorBridge("start", { request: { + withMarkers: Option(0, "boolean"), + withTicks: Option(0, "boolean"), withMemory: Option(0, "boolean"), - withTicks: Option(0, "boolean") + withFrames: Option(0, "boolean"), + withGCEvents: Option(0, "boolean"), + withDocLoadingEvents: Option(0, "boolean") }, response: { value: RetVal("number") diff --git a/devtools/server/performance/timeline.js b/devtools/server/performance/timeline.js index 61a4ef093f2f..cb94aad23887 100644 --- a/devtools/server/performance/timeline.js +++ b/devtools/server/performance/timeline.js @@ -68,16 +68,6 @@ var Timeline = exports.Timeline = Class({ events.off(this.tabActor, "window-ready", this._onWindowReady); this.tabActor = null; - - if (this._memory) { - this._memory.destroy(); - this._memory = null; - } - - if (this._framerate) { - this._framerate.destroy(); - this._framerate = null; - } }, /** @@ -91,6 +81,7 @@ var Timeline = exports.Timeline = Class({ */ get docShells() { let originalDocShell; + let docShells = []; if (this.tabActor.isRootActor) { originalDocShell = this.tabActor.docShell; @@ -98,12 +89,15 @@ var Timeline = exports.Timeline = Class({ originalDocShell = this.tabActor.originalDocShell; } + if (!originalDocShell) { + return docShells; + } + let docShellsEnum = originalDocShell.getDocShellEnumerator( Ci.nsIDocShellTreeItem.typeAll, Ci.nsIDocShell.ENUMERATE_FORWARDS ); - let docShells = []; while (docShellsEnum.hasMoreElements()) { let docShell = docShellsEnum.getNext(); docShells.push(docShell.QueryInterface(Ci.nsIDocShell)); @@ -117,44 +111,67 @@ var Timeline = exports.Timeline = Class({ * markers, memory, tick and frames events, if any. */ _pullTimelineData: function() { - if (!this._isRecording || !this.docShells.length) { + let docShells = this.docShells; + if (!this._isRecording || !docShells.length) { return; } - let endTime = this.docShells[0].now(); + let endTime = docShells[0].now(); let markers = []; - for (let docShell of this.docShells) { - markers.push(...docShell.popProfileTimelineMarkers()); - } + // Gather markers if requested. + if (this._withMarkers || this._withDocLoadingEvents) { + for (let docShell of docShells) { + for (let marker of docShell.popProfileTimelineMarkers()) { + markers.push(marker); - // The docshell may return markers with stack traces attached. - // Here we transform the stack traces via the stack frame cache, - // which lets us preserve tail sharing when transferring the - // frames to the client. We must waive xrays here because Firefox - // doesn't understand that the Debugger.Frame object is safe to - // use from chrome. See Tutorial-Alloc-Log-Tree.md. - for (let marker of markers) { - if (marker.stack) { - marker.stack = this._stackFrames.addFrame(Cu.waiveXrays(marker.stack)); - } - if (marker.endStack) { - marker.endStack = this._stackFrames.addFrame(Cu.waiveXrays(marker.endStack)); + // The docshell may return markers with stack traces attached. + // Here we transform the stack traces via the stack frame cache, + // which lets us preserve tail sharing when transferring the + // frames to the client. We must waive xrays here because Firefox + // doesn't understand that the Debugger.Frame object is safe to + // use from chrome. See Tutorial-Alloc-Log-Tree.md. + if (this._withFrames) { + if (marker.stack) { + marker.stack = this._stackFrames.addFrame(Cu.waiveXrays(marker.stack)); + } + if (marker.endStack) { + marker.endStack = this._stackFrames.addFrame(Cu.waiveXrays(marker.endStack)); + } + } + + // Emit some helper events for "DOMContentLoaded" and "Load" markers. + if (this._withDocLoadingEvents) { + if (marker.name == "document::DOMContentLoaded" || + marker.name == "document::Load") { + events.emit(this, "doc-loading", marker, endTime); + } + } + } } } - let frames = this._stackFrames.makeEvent(); - if (frames) { - events.emit(this, "frames", endTime, frames); - } - if (markers.length > 0) { + // Emit markers if requested. + if (this._withMarkers && markers.length > 0) { events.emit(this, "markers", markers, endTime); } + + // Emit framerate data if requested. + if (this._withTicks) { + events.emit(this, "ticks", endTime, this._framerate.getPendingTicks()); + } + + // Emit memory data if requested. if (this._withMemory) { events.emit(this, "memory", endTime, this._memory.measure()); } - if (this._withTicks) { - events.emit(this, "ticks", endTime, this._framerate.getPendingTicks()); + + // Emit stack frames data if requested. + if (this._withFrames && this._withMarkers) { + let frames = this._stackFrames.makeEvent(); + if (frames) { + events.emit(this, "frames", endTime, frames); + } } this._dataPullTimeout = Timers.setTimeout(() => { @@ -172,40 +189,75 @@ var Timeline = exports.Timeline = Class({ /** * Start recording profile markers. * - * @option {boolean} withMemory - * Boolean indiciating whether we want memory measurements sampled. A memory actor - * will be created regardless (to hook into GC events), but this determines - * whether or not a `memory` event gets fired. + * @option {boolean} withMarkers + * Boolean indicating whether or not timeline markers are emitted + * once they're accumulated every `DEFAULT_TIMELINE_DATA_PULL_TIMEOUT` + * milliseconds. * @option {boolean} withTicks - * Boolean indicating whether a `ticks` event is fired and a FramerateActor - * is created. + * Boolean indicating whether a `ticks` event is fired and a + * FramerateActor is created. + * @option {boolean} withMemory + * Boolean indiciating whether we want memory measurements sampled. + * @option {boolean} withFrames + * Boolean indicating whether or not stack frames should be handled + * from timeline markers. + * @option {boolean} withGCEvents + * Boolean indicating whether or not GC markers should be emitted. + * TODO: Remove these fake GC markers altogether in bug 1198127. + * @option {boolean} withDocLoadingEvents + * Boolean indicating whether or not DOMContentLoaded and Load + * marker events are emitted. */ - start: Task.async(function *({ withMemory, withTicks }) { - let startTime = this._startTime = this.docShells[0].now(); - + start: Task.async(function *({ + withMarkers, + withTicks, + withMemory, + withFrames, + withGCEvents, + withDocLoadingEvents, + }) { + let docShells = this.docShells; + if (!docShells.length) { + return -1; + } + let startTime = this._startTime = docShells[0].now(); if (this._isRecording) { return startTime; } this._isRecording = true; - this._stackFrames = new StackFrameCache(); - this._stackFrames.initFrames(); - this._withMemory = withMemory; - this._withTicks = withTicks; + this._withMarkers = !!withMarkers; + this._withTicks = !!withTicks; + this._withMemory = !!withMemory; + this._withFrames = !!withFrames; + this._withGCEvents = !!withGCEvents; + this._withDocLoadingEvents = !!withDocLoadingEvents; - for (let docShell of this.docShells) { - docShell.recordProfileTimelineMarkers = true; + if (this._withMarkers || this._withDocLoadingEvents) { + for (let docShell of docShells) { + docShell.recordProfileTimelineMarkers = true; + } } - this._memory = new Memory(this.tabActor, this._stackFrames); - this._memory.attach(); - events.on(this._memory, "garbage-collection", this._onGarbageCollection); - - if (withTicks) { + if (this._withTicks) { this._framerate = new Framerate(this.tabActor); this._framerate.startRecording(); } + if (this._withMemory || this._withGCEvents) { + this._memory = new Memory(this.tabActor, this._stackFrames); + this._memory.attach(); + } + + if (this._withGCEvents) { + events.on(this._memory, "garbage-collection", this._onGarbageCollection); + } + + if (this._withFrames && this._withMarkers) { + this._stackFrames = new StackFrameCache(); + this._stackFrames.initFrames(); + } + this._pullTimelineData(); return startTime; }), @@ -214,26 +266,51 @@ var Timeline = exports.Timeline = Class({ * Stop recording profile markers. */ stop: Task.async(function *() { - if (!this._isRecording) { - return; + let docShells = this.docShells; + if (!docShells.length) { + return -1; + } + let endTime = this._startTime = docShells[0].now(); + if (!this._isRecording) { + return endTime; } - this._isRecording = false; - this._stackFrames = null; - events.off(this._memory, "garbage-collection", this._onGarbageCollection); - this._memory.detach(); + if (this._withMarkers || this._withDocLoadingEvents) { + for (let docShell of docShells) { + docShell.recordProfileTimelineMarkers = false; + } + } - if (this._framerate) { + if (this._withTicks) { this._framerate.stopRecording(); + this._framerate.destroy(); this._framerate = null; } - for (let docShell of this.docShells) { - docShell.recordProfileTimelineMarkers = false; + if (this._withMemory || this._withGCEvents) { + this._memory.detach(); + this._memory.destroy(); } + if (this._withGCEvents) { + events.off(this._memory, "garbage-collection", this._onGarbageCollection); + } + + if (this._withFrames && this._withMarkers) { + this._stackFrames = null; + } + + this._isRecording = false; + this._withMarkers = false; + this._withTicks = false; + this._withMemory = false; + this._withFrames = false; + this._withDocLoadingEvents = false; + this._withGCEvents = false; + Timers.clearTimeout(this._dataPullTimeout); - return this.docShells[0].now(); + + return endTime; }), /** @@ -259,11 +336,12 @@ var Timeline = exports.Timeline = Class({ * not incrementally collect garbage. */ _onGarbageCollection: function ({ collections, gcCycleNumber, reason, nonincrementalReason }) { - if (!this._isRecording || !this.docShells.length) { + let docShells = this.docShells; + if (!this._isRecording || !docShells.length) { return; } - let endTime = this.docShells[0].now(); + let endTime = docShells[0].now(); events.emit(this, "markers", collections.map(({ startTimestamp: start, endTimestamp: end }) => { return { diff --git a/devtools/server/tests/browser/browser.ini b/devtools/server/tests/browser/browser.ini index d8185a3e8504..244ce192dfe9 100644 --- a/devtools/server/tests/browser/browser.ini +++ b/devtools/server/tests/browser/browser.ini @@ -50,6 +50,9 @@ skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still di [browser_canvasframe_helper_06.js] skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S [browser_markers-cycle-collection.js] +[browser_markers-docloading-01.js] +[browser_markers-docloading-02.js] +[browser_markers-docloading-03.js] [browser_markers-gc.js] [browser_markers-parse-html.js] [browser_markers-styles.js] diff --git a/devtools/server/tests/browser/browser_markers-docloading-01.js b/devtools/server/tests/browser/browser_markers-docloading-01.js new file mode 100644 index 000000000000..054d80b93008 --- /dev/null +++ b/devtools/server/tests/browser/browser_markers-docloading-01.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we get DOMContentLoaded and Load markers + */ + +const { TimelineFront } = require("devtools/server/actors/timeline"); +const MARKER_NAMES = ["document::DOMContentLoaded", "document::Load"]; + +add_task(function*() { + let doc = yield addTab(MAIN_DOMAIN + "doc_innerHTML.html"); + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = TimelineFront(client, form); + let rec = yield front.start({ withMarkers: true }); + + front.once("doc-loading", e => { + ok(false, "Should not be emitting doc-loading events."); + }); + + executeSoon(() => doc.location.reload()); + + yield waitForMarkerType(front, MARKER_NAMES, () => true, e => e, "markers"); + yield front.stop(rec); + + ok(true, "Found the required marker names."); + + // Wait some more time to make sure the 'doc-loading' events never get fired. + yield DevToolsUtils.waitForTime(1000); + + yield closeDebuggerClient(client); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_markers-docloading-02.js b/devtools/server/tests/browser/browser_markers-docloading-02.js new file mode 100644 index 000000000000..ec78ac0f99fe --- /dev/null +++ b/devtools/server/tests/browser/browser_markers-docloading-02.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we get DOMContentLoaded and Load markers + */ + +const { TimelineFront } = require("devtools/server/actors/timeline"); +const MARKER_NAMES = ["document::DOMContentLoaded", "document::Load"]; + +add_task(function*() { + let doc = yield addTab(MAIN_DOMAIN + "doc_innerHTML.html"); + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = TimelineFront(client, form); + let rec = yield front.start({ withMarkers: true, withDocLoadingEvents: true }); + + yield new Promise(resolve => { + front.once("doc-loading", resolve); + doc.location.reload(); + }); + + ok(true, "At least one doc-loading event got fired."); + + yield waitForMarkerType(front, MARKER_NAMES, () => true, e => e, "markers"); + yield front.stop(rec); + + ok(true, "Found the required marker names."); + + yield closeDebuggerClient(client); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_markers-docloading-03.js b/devtools/server/tests/browser/browser_markers-docloading-03.js new file mode 100644 index 000000000000..9ec16c854ca1 --- /dev/null +++ b/devtools/server/tests/browser/browser_markers-docloading-03.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we get DOMContentLoaded and Load markers + */ + +const { TimelineFront } = require("devtools/server/actors/timeline"); +const MARKER_NAMES = ["document::DOMContentLoaded", "document::Load"]; + +add_task(function*() { + let doc = yield addTab(MAIN_DOMAIN + "doc_innerHTML.html"); + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = TimelineFront(client, form); + let rec = yield front.start({ withDocLoadingEvents: true }); + + waitForMarkerType(front, MARKER_NAMES, () => true, e => e, "markers").then(e => { + ok(false, "Should not be emitting doc-loading markers."); + }); + + yield new Promise(resolve => { + front.once("doc-loading", resolve); + doc.location.reload(); + }); + + ok(true, "At least one doc-loading event got fired."); + + yield front.stop(rec); + + // Wait some more time to make sure the 'doc-loading' markers never get fired. + yield DevToolsUtils.waitForTime(1000); + + yield closeDebuggerClient(client); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_timeline.js b/devtools/server/tests/browser/browser_timeline.js index c76f52efb31a..73509ff22f81 100644 --- a/devtools/server/tests/browser/browser_timeline.js +++ b/devtools/server/tests/browser/browser_timeline.js @@ -29,7 +29,7 @@ add_task(function*() { let forceSyncReflow = doc.body.innerHeight; info("Start recording"); - yield front.start(); + yield front.start({ withMarkers: true }); isActive = yield front.isRecording(); ok(isActive, "The TimelineFront is now recording"); diff --git a/devtools/server/tests/browser/browser_timeline_iframes.js b/devtools/server/tests/browser/browser_timeline_iframes.js index 18b8aa69767c..23be278299fb 100644 --- a/devtools/server/tests/browser/browser_timeline_iframes.js +++ b/devtools/server/tests/browser/browser_timeline_iframes.js @@ -18,7 +18,7 @@ add_task(function*() { let front = TimelineFront(client, form); info("Start timeline marker recording"); - yield front.start(); + yield front.start({ withMarkers: true }); // Check that we get markers for a few iterations of the timer that runs in // the child frame. diff --git a/devtools/server/tests/browser/head.js b/devtools/server/tests/browser/head.js index 7d2769325344..3c52770218af 100644 --- a/devtools/server/tests/browser/head.js +++ b/devtools/server/tests/browser/head.js @@ -185,7 +185,10 @@ function waitUntil(predicate, interval = 10) { }); } -function waitForMarkerType(front, types, predicate) { +function waitForMarkerType(front, types, predicate, + unpackFun = (name, data) => data.markers, + eventName = "timeline-data") +{ types = [].concat(types); predicate = predicate || function(){ return true; }; let filteredMarkers = []; @@ -194,21 +197,21 @@ function waitForMarkerType(front, types, predicate) { info("Waiting for markers of type: " + types); function handler (name, data) { - if (name !== "markers") { + if (typeof name === "string" && name !== "markers") { return; } - let markers = data.markers; + let markers = unpackFun(name, data); info("Got markers: " + JSON.stringify(markers, null, 2)); filteredMarkers = filteredMarkers.concat(markers.filter(m => types.indexOf(m.name) !== -1)); if (types.every(t => filteredMarkers.some(m => m.name === t)) && predicate(filteredMarkers)) { - front.off("timeline-data", handler); + front.off(eventName, handler); resolve(filteredMarkers); } } - front.on("timeline-data", handler); + front.on(eventName, handler); return promise; } diff --git a/devtools/server/tests/unit/test_promises_object_timetosettle-02.js b/devtools/server/tests/unit/test_promises_object_timetosettle-02.js index 28c9576fb316..87fab1ef2c8b 100644 --- a/devtools/server/tests/unit/test_promises_object_timetosettle-02.js +++ b/devtools/server/tests/unit/test_promises_object_timetosettle-02.js @@ -15,6 +15,7 @@ var events = require("sdk/event/core"); add_task(function*() { let client = yield startTestDebuggerServer("test-promises-timetosettle"); let chromeActors = yield getChromeActors(client); + yield attachTab(client, chromeActors); ok(Promise.toString().contains("native code"), "Expect native DOM Promise."); @@ -24,6 +25,7 @@ add_task(function*() { let response = yield listTabs(client); let targetTab = findTab(response.tabs, "test-promises-timetosettle"); ok(targetTab, "Found our target tab."); + yield attachTab(client, targetTab); yield testGetTimeToSettle(client, targetTab, v => { const debuggee = @@ -47,9 +49,10 @@ function* testGetTimeToSettle(client, form, makePromise) { for (let p of promises) { if (p.promiseState.state === "fulfilled" && p.promiseState.value === resolution) { - equal(Math.floor(p.promiseState.timeToSettle / 100) * 100, 100, + let timeToSettle = Math.floor(p.promiseState.timeToSettle / 100) * 100; + ok(timeToSettle >= 100, "Expect time to settle for resolved promise to be " + - "approximately 100ms."); + "at least 100ms, got " + timeToSettle + "ms."); found = true; resolve(); } else { diff --git a/devtools/shared/performance/recording-utils.js b/devtools/shared/performance/recording-utils.js index 9b55624c757e..805a1893f17f 100644 --- a/devtools/shared/performance/recording-utils.js +++ b/devtools/shared/performance/recording-utils.js @@ -29,8 +29,12 @@ function mapRecordingOptions (type, options) { if (type === "timeline") { return { - withMemory: options.withMemory, + withMarkers: true, withTicks: options.withTicks, + withMemory: options.withMemory, + withFrames: true, + withGCEvents: true, + withDocLoadingEvents: false }; } diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp index 7a2c8439bb05..66688848f6ed 100644 --- a/docshell/base/nsDocShell.cpp +++ b/docshell/base/nsDocShell.cpp @@ -5982,8 +5982,20 @@ nsDocShell::GetIsOffScreenBrowser(bool* aIsOffScreen) return NS_OK; } +NS_IMETHODIMP +nsDocShell::SetIsActiveAndForeground(bool aIsActive) +{ + return SetIsActiveInternal(aIsActive, false); +} + NS_IMETHODIMP nsDocShell::SetIsActive(bool aIsActive) +{ + return SetIsActiveInternal(aIsActive, true); +} + +nsresult +nsDocShell::SetIsActiveInternal(bool aIsActive, bool aIsHidden) { // We disallow setting active on chrome docshells. if (mItemType == nsIDocShellTreeItem::typeChrome) { @@ -5996,7 +6008,7 @@ nsDocShell::SetIsActive(bool aIsActive) // Tell the PresShell about it. nsCOMPtr pshell = GetPresShell(); if (pshell) { - pshell->SetIsActive(aIsActive); + pshell->SetIsActive(aIsActive, aIsHidden); } // Tell the window about it @@ -6030,7 +6042,11 @@ nsDocShell::SetIsActive(bool aIsActive) } if (!docshell->GetIsBrowserOrApp()) { - docshell->SetIsActive(aIsActive); + if (aIsHidden) { + docshell->SetIsActive(aIsActive); + } else { + docshell->SetIsActiveAndForeground(aIsActive); + } } } @@ -7789,7 +7805,7 @@ nsDocShell::CreateAboutBlankContentViewer(nsIPrincipal* aPrincipal, mTiming->NotifyBeforeUnload(); bool okToUnload; - rv = mContentViewer->PermitUnload(false, &okToUnload); + rv = mContentViewer->PermitUnload(&okToUnload); if (NS_SUCCEEDED(rv) && !okToUnload) { // The user chose not to unload the page, interrupt the load. @@ -10127,7 +10143,7 @@ nsDocShell::InternalLoad(nsIURI* aURI, // protocol handler deals with this for javascript: URLs. if (!isJavaScript && aFileName.IsVoid() && mContentViewer) { bool okToUnload; - rv = mContentViewer->PermitUnload(false, &okToUnload); + rv = mContentViewer->PermitUnload(&okToUnload); if (NS_SUCCEEDED(rv) && !okToUnload) { // The user chose not to unload the page, interrupt the diff --git a/docshell/base/nsDocShell.h b/docshell/base/nsDocShell.h index bcfa8f3d22c1..4b75df163a1a 100644 --- a/docshell/base/nsDocShell.h +++ b/docshell/base/nsDocShell.h @@ -485,6 +485,8 @@ protected: uint32_t aRedirectFlags, uint32_t aStateFlags) override; + nsresult SetIsActiveInternal(bool aIsActive, bool aIsHidden); + /** * Helper function that determines if channel is an HTTP POST. * diff --git a/docshell/base/nsIContentViewer.idl b/docshell/base/nsIContentViewer.idl index 6d53ecd59db6..6484b0a60d6f 100644 --- a/docshell/base/nsIContentViewer.idl +++ b/docshell/base/nsIContentViewer.idl @@ -31,7 +31,7 @@ class nsDOMNavigationTiming; [ptr] native nsDOMNavigationTimingPtr(nsDOMNavigationTiming); [ref] native nsIContentViewerTArray(nsTArray >); -[scriptable, builtinclass, uuid(fbd04c99-e149-473f-8a68-44f53d82f98b)] +[scriptable, builtinclass, uuid(91b6c1f3-fc5f-43a9-88f4-9286bd19387f)] interface nsIContentViewer : nsISupports { [noscript] void init(in nsIWidgetPtr aParentWidget, @@ -45,12 +45,8 @@ interface nsIContentViewer : nsISupports /** * Checks if the document wants to prevent unloading by firing beforeunload on * the document, and if it does, prompts the user. The result is returned. - * - * @param aCallerClosesWindow indicates that the current caller will close the - * window. If the method returns true, all subsequent calls will be - * ignored. */ - boolean permitUnload([optional] in boolean aCallerClosesWindow); + boolean permitUnload(); /** * Exposes whether we're blocked in a call to permitUnload. @@ -62,8 +58,7 @@ interface nsIContentViewer : nsISupports * track of whether the user has responded to a prompt. * Used internally by the scriptable version to ensure we only prompt once. */ - [noscript,nostdcall] boolean permitUnloadInternal(in boolean aCallerClosesWindow, - inout boolean aShouldPrompt); + [noscript,nostdcall] boolean permitUnloadInternal(inout boolean aShouldPrompt); /** * Exposes whether we're in the process of firing the beforeunload event. @@ -71,16 +66,6 @@ interface nsIContentViewer : nsISupports */ readonly attribute boolean beforeUnloadFiring; - /** - * Works in tandem with permitUnload, if the caller decides not to close the - * window it indicated it will, it is the caller's responsibility to reset - * that with this method. - * - * @Note this method is only meant to be called on documents for which the - * caller has indicated that it will close the window. If that is not the case - * the behavior of this method is undefined. - */ - void resetCloseWindow(); void pageHide(in boolean isUnload); /** diff --git a/docshell/base/nsIDocShell.idl b/docshell/base/nsIDocShell.idl index 598d6587e604..c960577a9f16 100644 --- a/docshell/base/nsIDocShell.idl +++ b/docshell/base/nsIDocShell.idl @@ -43,7 +43,7 @@ interface nsITabParent; typedef unsigned long nsLoadFlags; -[scriptable, builtinclass, uuid(b1df6e41-c8dd-45c2-bc18-dd330d986214)] +[scriptable, builtinclass, uuid(41b1cf17-b37b-4a62-9df8-5f67cfecab3f)] interface nsIDocShell : nsIDocShellTreeItem { /** @@ -628,6 +628,12 @@ interface nsIDocShell : nsIDocShellTreeItem */ attribute boolean isActive; + /** + * Sets whether a docshell is active, as above, but ensuring it does + * not discard its layers + */ + void setIsActiveAndForeground(in boolean aIsActive); + /** * Puts the docshell in prerendering mode. noscript because we want only * native code to be able to put a docshell in prerendering. diff --git a/docshell/base/timeline/DocLoadingTimelineMarker.h b/docshell/base/timeline/DocLoadingTimelineMarker.h new file mode 100644 index 000000000000..af2ac17fdafe --- /dev/null +++ b/docshell/base/timeline/DocLoadingTimelineMarker.h @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_DocLoadingTimelineMarker_h_ +#define mozilla_DocLoadingTimelineMarker_h_ + +#include "TimelineMarker.h" +#include "mozilla/dom/ProfileTimelineMarkerBinding.h" + +namespace mozilla { + +class DocLoadingTimelineMarker : public TimelineMarker +{ +public: + explicit DocLoadingTimelineMarker(const char* aName) + : TimelineMarker(aName, MarkerTracingType::TIMESTAMP) + , mUnixTime(PR_Now()) + {} + + virtual void AddDetails(JSContext* aCx, dom::ProfileTimelineMarker& aMarker) override + { + TimelineMarker::AddDetails(aCx, aMarker); + aMarker.mUnixTime.Construct(mUnixTime); + } + +private: + // Certain consumers might use Date.now() or similar for tracing time. + // However, TimelineMarkers use process creation as an epoch, which provides + // more precision. To allow syncing, attach an additional unix timestamp. + // Using this instead of `AbstractTimelineMarker::GetTime()'s` timestamp + // is strongly discouraged. + PRTime mUnixTime; +}; + +} // namespace mozilla + +#endif // mozilla_DocLoadingTimelineMarker_h_ diff --git a/docshell/base/timeline/moz.build b/docshell/base/timeline/moz.build index 69f126596e69..a0462bf3bd2a 100644 --- a/docshell/base/timeline/moz.build +++ b/docshell/base/timeline/moz.build @@ -10,6 +10,7 @@ EXPORTS.mozilla += [ 'AutoTimelineMarker.h', 'CompositeTimelineMarker.h', 'ConsoleTimelineMarker.h', + 'DocLoadingTimelineMarker.h', 'EventTimelineMarker.h', 'JavascriptTimelineMarker.h', 'LayerTimelineMarker.h', diff --git a/docshell/test/browser/browser.ini b/docshell/test/browser/browser.ini index b6215120f4b8..5d94575cd8cf 100644 --- a/docshell/test/browser/browser.ini +++ b/docshell/test/browser/browser.ini @@ -83,7 +83,6 @@ skip-if = e10s # Bug 1220927 - Test tries to do addSHistoryListener on content. [browser_loadURI.js] [browser_multiple_pushState.js] [browser_onbeforeunload_navigation.js] -skip-if = e10s [browser_search_notification.js] [browser_timelineMarkers-01.js] [browser_timelineMarkers-02.js] diff --git a/docshell/test/browser/browser_bug134911.js b/docshell/test/browser/browser_bug134911.js index 1342c9d310f4..aa54cfd1a278 100644 --- a/docshell/test/browser/browser_bug134911.js +++ b/docshell/test/browser/browser_bug134911.js @@ -1,38 +1,41 @@ -/* The test text decoded correctly as Shift_JIS */ -const rightText="\u30E6\u30CB\u30B3\u30FC\u30C9\u306F\u3001\u3059\u3079\u3066\u306E\u6587\u5B57\u306B\u56FA\u6709\u306E\u756A\u53F7\u3092\u4ED8\u4E0E\u3057\u307E\u3059"; +const TEXT = { + /* The test text decoded correctly as Shift_JIS */ + rightText: "\u30E6\u30CB\u30B3\u30FC\u30C9\u306F\u3001\u3059\u3079\u3066\u306E\u6587\u5B57\u306B\u56FA\u6709\u306E\u756A\u53F7\u3092\u4ED8\u4E0E\u3057\u307E\u3059", -const enteredText1="The quick brown fox jumps over the lazy dog"; -const enteredText2="\u03BE\u03B5\u03C3\u03BA\u03B5\u03C0\u03AC\u03B6\u03C9\u0020\u03C4\u1F74\u03BD\u0020\u03C8\u03C5\u03C7\u03BF\u03C6\u03B8\u03CC\u03C1\u03B1\u0020\u03B2\u03B4\u03B5\u03BB\u03C5\u03B3\u03BC\u03AF\u03B1"; + enteredText1: "The quick brown fox jumps over the lazy dog", + enteredText2: "\u03BE\u03B5\u03C3\u03BA\u03B5\u03C0\u03AC\u03B6\u03C9\u0020\u03C4\u1F74\u03BD\u0020\u03C8\u03C5\u03C7\u03BF\u03C6\u03B8\u03CC\u03C1\u03B1\u0020\u03B2\u03B4\u03B5\u03BB\u03C5\u03B3\u03BC\u03AF\u03B1", +}; function test() { waitForExplicitFinish(); var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/"; gBrowser.selectedTab = gBrowser.addTab(rootDir + "test-form_sjis.html"); - gBrowser.selectedBrowser.addEventListener("load", afterOpen, true); + BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(afterOpen); } function afterOpen() { - gBrowser.selectedBrowser.removeEventListener("load", afterOpen, true); - gBrowser.selectedBrowser.addEventListener("load", afterChangeCharset, true); + BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(afterChangeCharset); - gBrowser.contentDocument.getElementById("testtextarea").value = enteredText1; - gBrowser.contentDocument.getElementById("testinput").value = enteredText2; - - /* Force the page encoding to Shift_JIS */ - BrowserSetForcedCharacterSet("Shift_JIS"); + ContentTask.spawn(gBrowser.selectedBrowser, TEXT, function(TEXT) { + content.document.getElementById("testtextarea").value = TEXT.enteredText1; + content.document.getElementById("testinput").value = TEXT.enteredText2; + }).then(() => { + /* Force the page encoding to Shift_JIS */ + BrowserSetForcedCharacterSet("Shift_JIS"); + }); } - + function afterChangeCharset() { - gBrowser.selectedBrowser.removeEventListener("load", afterChangeCharset, true); - - is(gBrowser.contentDocument.getElementById("testpar").innerHTML, rightText, - "encoding successfully changed"); - is(gBrowser.contentDocument.getElementById("testtextarea").value, enteredText1, - "text preserved in